optionals

This commit is contained in:
agra
2026-02-22 22:16:30 +02:00
parent d3e574eae5
commit 1cc67f9b5a
17 changed files with 1952 additions and 32 deletions

View File

@@ -16,7 +16,7 @@ main :: () {
ptr := @v;
copy := ptr.*;
print("copy: {}\n", copy);
// null pointer
np : *Vec2 = null;

97
examples/32-optionals.sx Normal file
View File

@@ -0,0 +1,97 @@
#import "modules/std.sx";
// --- Type declarations ---
OptNode :: struct { value: s32; next: ?s32; }
OptInner :: struct { val: s32; }
OptOuter :: struct { inner: ?OptInner; }
// --- Comptime optionals ---
ct_sum :: () -> s32 {
x: ?s32 = 42;
y: ?s32 = null;
return (x ?? 0) + (y ?? 99);
}
CT_RESULT :: #run ct_sum();
main :: () -> s32 {
// Basic optional creation
x: ?s32 = 42;
y: ?s32 = null;
print("x = {}\n", x);
print("y = {}\n", y);
// Force unwrap
print("x! = {}\n", x!);
// Null coalescing
print("x ?? 0 = {}\n", x ?? 0);
print("y ?? 99 = {}\n", y ?? 99);
// If-binding (safe unwrap)
if val := x {
print("if-bind x: {}\n", val);
}
if val := y {
print("should not print\n");
} else {
print("if-bind y: none\n");
}
// Pattern matching
check :: (v: ?s32) -> s32 {
return if v == {
case .some: (val) { val; }
case .none: { 0; }
};
}
print("match some: {}\n", check(42));
print("match none: {}\n", check(null));
// Optional chaining
p: ?OptNode = OptNode.{ value = 10, next = 20 };
q: ?OptNode = null;
print("p?.value = {}\n", p?.value ?? 0);
print("q?.value = {}\n", q?.value ?? 0);
// Deep chaining
o1 := OptOuter.{ inner = OptInner.{ val = 99 } };
o2 := OptOuter.{ inner = null };
print("o1.inner?.val = {}\n", o1.inner?.val ?? 0);
print("o2.inner?.val = {}\n", o2.inner?.val ?? 0);
// Flow-sensitive narrowing
a: ?s32 = 10;
b: ?s32 = 20;
if a != null {
print("narrowed a: {}\n", a);
}
// Guard narrowing
guard :: (v: ?s32) -> s32 {
if v == null { return 0; }
return v;
}
print("guard 42: {}\n", guard(42));
print("guard null: {}\n", guard(null));
// Compound narrowing
if a != null and b != null {
print("both: {} {}\n", a, b);
}
// Compound guard
guard2 :: (a: ?s32, b: ?s32) -> s32 {
if a == null or b == null { return 0; }
return a + b;
}
print("guard2: {}\n", guard2(3, 4));
// Struct field defaults
n := OptNode.{ value = 10 };
print("default next: {}\n", n.next);
// Comptime result
print("comptime: {}\n", CT_RESULT);
return 0;
}

View File

@@ -1,4 +1,5 @@
#import "modules/std.sx";
#import "modules/math";
pkg :: #import "modules/testpkg";
// ============================================================
@@ -33,6 +34,14 @@ Defaults :: struct {
c: s32 = ---;
}
OptNode :: struct {
value: s32;
next: ?s32;
}
OptInner :: struct { val: s32; }
OptOuter :: struct { inner: ?OptInner; }
MyFloat :: f64;
Perms :: enum flags { read; write; execute; }
@@ -86,6 +95,25 @@ CT_VAL :: #run add(10, 15);
CT_MUL :: #run mul(6, 7);
CT_CHAIN :: #run add(CT_VAL, 5);
// #run compile-time optional tests
ct_opt_coalesce :: () -> s32 {
x: ?s32 = 42;
y: ?s32 = null;
return (x ?? 0) + (y ?? 99);
}
ct_opt_unwrap :: () -> s32 {
x: ?s32 = 77;
return x!;
}
ct_opt_guard :: () -> s32 {
x: ?s32 = 10;
if x == null { return -1; }
return x;
}
CT_OPT_COALESCE :: #run ct_opt_coalesce();
CT_OPT_UNWRAP :: #run ct_opt_unwrap();
CT_OPT_GUARD :: #run ct_opt_guard();
// #insert helpers
gen_code :: () -> string {
return "print(\"insert-ok\\n\");";
@@ -1019,6 +1047,11 @@ END;
// #run chained dependency
print("run-chain: {}\n", CT_CHAIN);
// #run comptime optionals
print("ct-opt-coalesce: {}\n", CT_OPT_COALESCE); // ct-opt-coalesce: 141
print("ct-opt-unwrap: {}\n", CT_OPT_UNWRAP); // ct-opt-unwrap: 77
print("ct-opt-guard: {}\n", CT_OPT_GUARD); // ct-opt-guard: 10
// #insert with function
#insert gen_code();
@@ -1480,5 +1513,211 @@ END;
}
}
// ========================================================
// OPTIONALS
// ========================================================
print("--- optionals ---\n");
// Basic optional creation and null
{
x: ?s32 = 42;
y: ?s32 = null;
print("opt x: {}\n", x); // opt x: 42
print("opt y: {}\n", y); // opt y: null
}
// Force unwrap
{
x: ?s32 = 10;
val := x!;
print("unwrap: {}\n", val); // unwrap: 10
}
// Null coalescing
{
x: ?s32 = 42;
y: ?s32 = null;
a := x ?? 0;
b := y ?? 99;
print("coalesce a: {}\n", a); // coalesce a: 42
print("coalesce b: {}\n", b); // coalesce b: 99
}
// If-binding (safe unwrap)
{
x: ?s32 = 7;
y: ?s32 = null;
if val := x {
print("if-bind x: {}\n", val); // if-bind x: 7
}
if val := y {
print("if-bind y: should not print\n");
} else {
print("if-bind y: none\n"); // if-bind y: none
}
}
// Pattern matching on optionals
{
check :: (v: ?s32) -> s32 {
return if v == {
case .some: (val) { val; }
case .none: { 0; }
};
}
a: ?s32 = 55;
b: ?s32 = null;
print("match some: {}\n", check(a)); // match some: 55
print("match none: {}\n", check(b)); // match none: 0
}
// Optional with implicit wrapping
{
opt_wrap :: (n: s32) -> ?s32 {
if n > 0 {
return n;
}
return null;
}
r1 := opt_wrap(5);
r2 := opt_wrap(0);
print("wrap pos: {}\n", r1); // wrap pos: 5
print("wrap neg: {}\n", r2); // wrap neg: null
}
// Struct field defaults for ?T
{
n := OptNode.{ value = 10 };
print("opt field default: {}\n", n.next); // opt field default: null
m := OptNode.{ value = 20, next = 42 };
print("opt field set: {}\n", m.next); // opt field set: 42
}
// ?T as function parameter
{
opt_process :: (val: ?s32) -> s32 {
return val ?? 0;
}
a: ?s32 = 42;
b: ?s32 = null;
print("opt param a: {}\n", opt_process(a)); // opt param a: 42
print("opt param b: {}\n", opt_process(b)); // opt param b: 0
print("opt param 7: {}\n", opt_process(7)); // opt param 7: 7
}
// Generic function with ?T return
{
first_pos :: ($T: Type, a: T, b: T) -> ?T {
if a > 0 { return a; }
if b > 0 { return b; }
return null;
}
print("generic opt 1: {}\n", first_pos(s32, 5, 10)); // generic opt 1: 5
print("generic opt 2: {}\n", first_pos(s32, 0, 7)); // generic opt 2: 7
print("generic opt 3: {}\n", first_pos(s32, 0, 0)); // generic opt 3: null
}
// Optional chaining (?.)
{
p: ?OptNode = OptNode.{ value = 10, next = 20 };
q: ?OptNode = null;
print("chain some: {}\n", p?.value ?? 0); // chain some: 10
print("chain none: {}\n", q?.value ?? 0); // chain none: 0
print("chain print: {}\n", p?.next); // chain print: 20
print("chain null: {}\n", q?.next); // chain null: null
// Chained: obj.field?.field
o1 := OptOuter.{ inner = OptInner.{ val = 99 } };
o2 := OptOuter.{ inner = null };
print("deep chain 1: {}\n", o1.inner?.val ?? 0); // deep chain 1: 99
print("deep chain 2: {}\n", o2.inner?.val ?? 0); // deep chain 2: 0
}
// Flow-sensitive narrowing
{
x: ?s32 = 42;
y: ?s32 = null;
// if x != null → x is narrowed to s32
if x != null {
print("narrow x: {}\n", x); // narrow x: 42
}
// if y != null → not entered
if y != null {
print("should not print\n");
} else {
print("narrow y else: null\n"); // narrow y else: null
}
// if x == null ... else → else-branch narrowed
if x == null {
print("should not print\n");
} else {
print("narrow else x: {}\n", x); // narrow else x: 42
}
}
// Guard narrowing
{
guard_fn :: (v: ?s32) -> s32 {
if v == null { return 0; }
return v;
}
print("guard some: {}\n", guard_fn(42)); // guard some: 42
print("guard none: {}\n", guard_fn(null)); // guard none: 0
}
// Compound narrowing: && chains
{
a: ?s32 = 10;
b: ?s32 = 20;
c: ?s32 = null;
if a != null and b != null {
print("and both: {} {}\n", a, b); // and both: 10 20
}
if a != null and c != null {
print("should not print\n");
} else {
print("and one null\n"); // and one null
}
}
// Compound guard narrowing: || chains
{
guard2 :: (a: ?s32, b: ?s32) -> s32 {
if a == null or b == null { return 0; }
return a + b;
}
print("or guard: {}\n", guard2(3, 4)); // or guard: 7
print("or guard null: {}\n", guard2(3, null)); // or guard null: 0
}
// Nested if narrowing
{
a: ?s32 = 10;
b: ?s32 = 20;
if a != null {
if b != null {
print("nested narrow: {} {}\n", a, b); // nested narrow: 10 20
}
}
}
// Guard narrowing used in loop
{
guard_loop :: (v: ?s32) -> s32 {
if v == null { return 0; }
sum := 0;
i := 0;
while i < v {
sum = sum + 1;
i = i + 1;
}
return sum;
}
print("guard loop: {}\n", guard_loop(3)); // guard loop: 3
}
print("=== DONE ===\n");
}

116
specs.md
View File

@@ -429,7 +429,7 @@ set_x :: (p: *Vec2, val: f32) {
set_x(@v, 99.0);
```
**Null**: All pointer types are nullable. `null` is the null pointer literal.
**Null**: Pointer types are currently nullable by default. `null` is the null pointer literal.
```sx
np : *Vec2 = null;
```
@@ -451,6 +451,120 @@ val := mp[2]; // 30
**Fat pointer layout**: `[:0]u8`, `string`, and `[]T` are `{ptr, i64}` structs. The raw pointer is always the first field at offset 0. This means `*[:0]u8` works as C's `char**` — a C function dereferences through the outer pointer and reads the raw `char*` from offset 0.
### Optional Types
Optional types represent values that may or may not be present.
#### Type Syntax
```sx
x: ?s32 = 42; // optional s32, has value
y: ?s32 = null; // optional s32, no value
```
Any type `T` can be made optional: `?s32`, `?string`, `?Point`, `?*T`, `?[]T`.
#### LLVM Representation
- Non-pointer optionals (`?s32`, `?Point`): `{ T, i1 }` struct — payload + has_value flag
- Pointer optionals (`?*T`): bare pointer — null represents absence
#### Implicit Wrapping
A value of type `T` implicitly converts to `?T`:
```sx
wrap :: (n: s32) -> ?s32 {
if n > 0 { return n; } // s32 → ?s32 (wraps)
return null; // null → ?s32
}
```
#### Force Unwrap (`!`)
Extracts the payload, traps at runtime if null:
```sx
x: ?s32 = 42;
val := x!; // val : s32 = 42
```
#### Null Coalescing (`??`)
Returns the payload if present, otherwise evaluates the right-hand side:
```sx
x: ?s32 = 42;
y: ?s32 = null;
a := x ?? 0; // 42
b := y ?? 99; // 99
```
#### Safe Unwrap (`if val := expr`)
Binds the payload to a variable if present:
```sx
x: ?s32 = 42;
if val := x {
print("{}\n", val); // val : s32 = 42
} else {
print("none\n");
}
```
#### While-Optional Binding
```sx
while val := get_next() {
// val is the unwrapped value
}
```
#### Pattern Matching
Optionals support `.some` and `.none` virtual enum variants:
```sx
result := if opt == {
case .some: (val) { val * 2; }
case .none: { 0; }
};
```
#### Optional Chaining (`?.`)
Short-circuits field access on optionals:
```sx
x: ?Point = Point.{ x = 1, y = 2 };
y: ?Point = null;
a := x?.x ?? 0; // 1
b := y?.x ?? 0; // 0
```
Result type of `x?.field` is always `?FieldType`.
#### Flow-Sensitive Narrowing
The compiler narrows `?T` to `T` in control flow branches:
```sx
x: ?s32 = 42;
if x != null {
print("{}\n", x); // x is s32 here (narrowed)
}
if x == null { return; }
print("{}\n", x); // x is s32 here (guard narrowing)
```
Compound conditions:
```sx
if a != null and b != null {
// both a and b are narrowed to their inner types
}
if a == null or b == null { return; }
// both a and b are narrowed after the guard
```
Reassignment kills narrowing.
#### Struct Field Defaults
Optional fields in structs default to `null`:
```sx
Node :: struct { value: s32; next: ?s32; }
n := Node.{ value = 10 }; // n.next is null
```
#### Printing
`print("{}", opt)` prints the payload value if present, or `"null"`.
#### Comptime
Optionals work in `#run` blocks — `??`, `!`, `if val :=`, null checks all supported.
### Foreign Function Interface (C Interop)
To call C functions, declare a library constant with `#library` and bind functions with `#foreign`:

View File

@@ -53,6 +53,9 @@ pub const Node = struct {
slice_expr: SliceExpr,
pointer_type_expr: PointerTypeExpr,
many_pointer_type_expr: ManyPointerTypeExpr,
optional_type_expr: OptionalTypeExpr,
force_unwrap: ForceUnwrap,
null_coalesce: NullCoalesce,
deref_expr: DerefExpr,
null_literal: void,
while_expr: WhileExpr,
@@ -192,6 +195,7 @@ pub const Call = struct {
pub const FieldAccess = struct {
object: *Node,
field: []const u8,
is_optional: bool = false,
};
pub const IfExpr = struct {
@@ -199,6 +203,7 @@ pub const IfExpr = struct {
then_branch: *Node,
else_branch: ?*Node,
is_inline: bool, // true for `if cond then a else b`
binding_name: ?[]const u8 = null, // for `if val := expr { ... }` optional binding
};
pub const MatchExpr = struct {
@@ -371,6 +376,19 @@ pub const ManyPointerTypeExpr = struct {
element_type: *Node,
};
pub const OptionalTypeExpr = struct {
inner_type: *Node,
};
pub const ForceUnwrap = struct {
operand: *Node,
};
pub const NullCoalesce = struct {
lhs: *Node,
rhs: *Node,
};
pub const DerefExpr = struct {
operand: *Node,
};
@@ -378,6 +396,7 @@ pub const DerefExpr = struct {
pub const WhileExpr = struct {
condition: *Node,
body: *Node,
binding_name: ?[]const u8 = null, // for `while val := expr { ... }` optional binding
};
pub const ForExpr = struct {

File diff suppressed because it is too large Load Diff

View File

@@ -268,6 +268,9 @@ pub const Instruction = union(enum) {
// Code insertion
eval_insert: InsertInfo, // pop string, parse as code, compile + execute inline
// Optionals
opt_unwrap, // pop value, error if null_val, else push back
// Unions
make_union: UnionMake,
get_union_field: UnionFieldAccess,
@@ -826,18 +829,43 @@ pub const Compiler = struct {
}
},
.if_expr => |ie| {
try self.compileNode(ie.condition);
const jump_false_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_false = 0 }); // placeholder
try self.compileNode(ie.then_branch);
if (ie.else_branch) |eb| {
const jump_end_idx = self.instructions.items.len;
try self.emit(.{ .jump = 0 }); // placeholder
self.patchJumpIfFalse(jump_false_idx);
try self.compileNode(eb);
self.patchJump(jump_end_idx);
if (ie.binding_name) |binding_name| {
// if val := optional_expr { ... } else { ... }
try self.compileNode(ie.condition);
// Dup the optional value, test truthiness
try self.emit(.dup);
const jump_false_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_false = 0 }); // placeholder
// Non-null path: the value is on the stack, bind as local
const slot: u16 = @intCast(self.locals.items.len);
try self.locals.append(self.allocator, .{ .name = binding_name, .depth = self.scope_depth });
try self.emit(.{ .set_local = slot });
try self.compileNode(ie.then_branch);
if (ie.else_branch) |eb| {
const jump_end_idx = self.instructions.items.len;
try self.emit(.{ .jump = 0 }); // placeholder
self.patchJumpIfFalse(jump_false_idx);
try self.emit(.pop); // discard the null value
try self.compileNode(eb);
self.patchJump(jump_end_idx);
} else {
self.patchJumpIfFalse(jump_false_idx);
try self.emit(.pop); // discard the null value
}
} else {
self.patchJumpIfFalse(jump_false_idx);
try self.compileNode(ie.condition);
const jump_false_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_false = 0 }); // placeholder
try self.compileNode(ie.then_branch);
if (ie.else_branch) |eb| {
const jump_end_idx = self.instructions.items.len;
try self.emit(.{ .jump = 0 }); // placeholder
self.patchJumpIfFalse(jump_false_idx);
try self.compileNode(eb);
self.patchJump(jump_end_idx);
} else {
self.patchJumpIfFalse(jump_false_idx);
}
}
},
.call => |call_node| {
@@ -1066,6 +1094,20 @@ pub const Compiler = struct {
.field_names = fnames,
} });
},
.force_unwrap => |fu| {
try self.compileNode(fu.operand);
try self.emit(.opt_unwrap);
},
.null_coalesce => |nc| {
// x ?? y: evaluate x, if non-null keep it, else evaluate y
try self.compileNode(nc.lhs);
try self.emit(.dup);
const jump_idx = self.instructions.items.len;
try self.emit(.{ .jump_if_true = 0 }); // placeholder
try self.emit(.pop); // discard the null
try self.compileNode(nc.rhs);
self.patchJumpIfTrue(jump_idx);
},
.ufcs_alias => {}, // UFCS aliases are resolved at codegen, no-op in comptime
else => {
return error.UnsupportedExpression;
@@ -1574,6 +1616,12 @@ pub const VM = struct {
}
},
.opt_unwrap => {
const val = try self.pop();
if (val == .null_val) return error.NullDereference;
try self.push(val);
},
// Code insertion
.eval_insert => |info| {
// Pop the code string (result of evaluating the inner expression)

View File

@@ -204,6 +204,17 @@ pub const Lexer = struct {
return self.makeToken(.caret, start, self.index);
},
'~' => return self.makeToken(.tilde, start, self.index),
'?' => {
if (self.peek() == '?') {
self.index += 1;
return self.makeToken(.question_question, start, self.index);
}
if (self.peek() == '.') {
self.index += 1;
return self.makeToken(.question_dot, start, self.index);
}
return self.makeToken(.question, start, self.index);
},
'!' => {
if (self.peek() == '=') {
self.index += 1;

View File

@@ -76,6 +76,8 @@ pub const Server = struct {
if (params) |p| self.handleSignatureHelp(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/semanticTokens/full")) {
if (params) |p| self.handleSemanticTokens(id, p) catch |e| self.logError(method, e);
} else if (std.mem.eql(u8, method, "textDocument/inlayHint")) {
if (params) |p| self.handleInlayHint(id, p) catch |e| self.logError(method, e);
}
return true;
@@ -1015,6 +1017,325 @@ pub const Server = struct {
try self.sendResponse(id_json, result_json);
}
// ---- Inlay hints ----
fn handleInlayHint(self: *Server, id: ?std.json.Value, params: std.json.Value) !void {
const ctx = try self.extractRequest(id, params) orelse return;
const id_json = ctx.id_json;
const file_path = uriToFilePath(ctx.uri) orelse "";
const doc = self.documents.get(file_path) orelse {
return try self.sendResponse(id_json, "[]");
};
const sema = doc.sema orelse doc.last_good_sema orelse {
return try self.sendResponse(id_json, "[]");
};
const root = doc.root orelse {
return try self.sendResponse(id_json, "[]");
};
var hints = std.ArrayList(lsp.InlayHint).empty;
collectInlayHints(self.allocator, root, sema.symbols, doc.source, &hints);
self.collectCallHints(doc, root, &hints);
const result_json = try lsp.inlayHintsJson(self.allocator, hints.items);
try self.sendResponse(id_json, result_json);
}
fn collectInlayHints(
allocator: std.mem.Allocator,
node: *const sx.ast.Node,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
) void {
switch (node.data) {
.root => |r| {
for (r.decls) |decl| collectInlayHints(allocator, decl, symbols, source, hints);
},
.block => |b| {
for (b.stmts) |stmt| collectInlayHints(allocator, stmt, symbols, source, hints);
},
.fn_decl => |fd| {
collectInlayHints(allocator, fd.body, symbols, source, hints);
},
.lambda => |lm| {
collectInlayHints(allocator, lm.body, symbols, source, hints);
},
.if_expr => |ie| {
if (ie.binding_name) |bname| {
addBindingHint(allocator, bname, node.span, symbols, source, hints);
}
collectInlayHints(allocator, ie.then_branch, symbols, source, hints);
if (ie.else_branch) |eb| collectInlayHints(allocator, eb, symbols, source, hints);
},
.while_expr => |we| {
if (we.binding_name) |bname| {
addBindingHint(allocator, bname, node.span, symbols, source, hints);
}
collectInlayHints(allocator, we.body, symbols, source, hints);
},
.for_expr => |fe| {
collectInlayHints(allocator, fe.body, symbols, source, hints);
},
.var_decl => |vd| {
// Only show hint when type is inferred (:= syntax)
if (vd.type_annotation != null) return;
if (vd.value == null) return;
addHintForDecl(allocator, vd.name, node.span, symbols, source, hints, true);
},
.const_decl => |cd| {
// Skip if explicit type annotation
if (cd.type_annotation != null) return;
// Skip functions, types, structs, enums, unions, comptime, foreign, library
switch (cd.value.data) {
.lambda, .fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl,
.comptime_expr, .foreign_expr, .library_decl,
=> return,
else => {},
}
addHintForDecl(allocator, cd.name, node.span, symbols, source, hints, false);
},
else => {},
}
}
fn addHintForDecl(
allocator: std.mem.Allocator,
name: []const u8,
span: sx.ast.Span,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
is_colon_equal: bool,
) void {
// Find symbol by matching span start
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
const ty = sym.ty orelse return;
// Skip void types — not useful to display
if (ty == .void_type) return;
const type_name = ty.displayName(allocator) catch return;
if (is_colon_equal) {
// For `:=` declarations: place hint between `:` and `=`
// Scan from after the name to find `:=`
var pos = span.start + @as(u32, @intCast(name.len));
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == '=') {
// Place hint at the `=` position (between `:` and `=`)
const eq_offset = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, eq_offset);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
} else {
// For `::` declarations: place hint between first `:` and second `:`
var pos = span.start + @as(u32, @intCast(name.len));
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == ':') {
const second_colon = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, second_colon);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
}
}
fn addBindingHint(
allocator: std.mem.Allocator,
name: []const u8,
span: sx.ast.Span,
symbols: []const sx.sema.Symbol,
source: [:0]const u8,
hints: *std.ArrayList(lsp.InlayHint),
) void {
// Look up symbol by name + span (sema stores binding with if/while node span)
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
const ty = sym.ty orelse return;
if (ty == .void_type) return;
const type_name = ty.displayName(allocator) catch return;
// Scan from span start to find the `:=` used in the binding
var pos = span.start;
while (pos + 1 < source.len) : (pos += 1) {
if (source[pos] == ':' and source[pos + 1] == '=') {
const eq_offset = pos + 1;
const loc = sx.errors.SourceLoc.compute(source, eq_offset);
if (loc.line == 0 or loc.col == 0) return;
hints.append(allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = type_name,
.padding_left = true,
.padding_right = true,
}) catch {};
return;
}
}
}
fn findSymbolAtSpan(symbols: []const sx.sema.Symbol, span_start: u32, name: []const u8) ?sx.sema.Symbol {
for (symbols) |sym| {
if (sym.def_span.start == span_start and std.mem.eql(u8, sym.name, name)) {
return sym;
}
}
return null;
}
// ---- Parameter name hints at call sites ----
fn collectCallHints(self: *Server, doc: *const Document, node: *const sx.ast.Node, hints: *std.ArrayList(lsp.InlayHint)) void {
switch (node.data) {
.root => |r| {
for (r.decls) |decl| self.collectCallHints(doc, decl, hints);
},
.block => |b| {
for (b.stmts) |stmt| self.collectCallHints(doc, stmt, hints);
},
.fn_decl => |fd| {
self.collectCallHints(doc, fd.body, hints);
},
.lambda => |lm| {
self.collectCallHints(doc, lm.body, hints);
},
.if_expr => |ie| {
self.collectCallHints(doc, ie.condition, hints);
self.collectCallHints(doc, ie.then_branch, hints);
if (ie.else_branch) |eb| self.collectCallHints(doc, eb, hints);
},
.while_expr => |we| {
self.collectCallHints(doc, we.condition, hints);
self.collectCallHints(doc, we.body, hints);
},
.for_expr => |fe| {
self.collectCallHints(doc, fe.iterable, hints);
self.collectCallHints(doc, fe.body, hints);
},
.var_decl => |vd| {
if (vd.value) |val| self.collectCallHints(doc, val, hints);
},
.const_decl => |cd| {
self.collectCallHints(doc, cd.value, hints);
},
.return_stmt => |rs| {
if (rs.value) |val| self.collectCallHints(doc, val, hints);
},
.assignment => |a| {
self.collectCallHints(doc, a.value, hints);
},
.binary_op => |bop| {
self.collectCallHints(doc, bop.lhs, hints);
self.collectCallHints(doc, bop.rhs, hints);
},
.unary_op => |uop| {
self.collectCallHints(doc, uop.operand, hints);
},
.call => |c| {
// Recurse into arguments (they may contain nested calls)
for (c.args) |arg| self.collectCallHints(doc, arg, hints);
// Emit parameter name hints for this call
self.emitCallParamHints(doc, c, hints);
},
.push_stmt => |ps| {
self.collectCallHints(doc, ps.context_expr, hints);
self.collectCallHints(doc, ps.body, hints);
},
.defer_stmt => |ds| {
self.collectCallHints(doc, ds.expr, hints);
},
else => {},
}
}
fn emitCallParamHints(self: *Server, doc: *const Document, call: sx.ast.Call, hints: *std.ArrayList(lsp.InlayHint)) void {
if (call.args.len == 0) return;
// Resolve callee name and find function declaration
var param_offset: usize = 0;
const fd = self.resolveCallTarget(doc, call, &param_offset) orelse return;
// Emit hints for each argument
for (call.args, 0..) |arg, i| {
const param_idx = i + param_offset;
if (param_idx >= fd.params.len) break;
const param = fd.params[param_idx];
// Skip variadic params
if (param.is_variadic) break;
// Skip if arg is an identifier matching the param name
if (arg.data == .identifier) {
if (std.mem.eql(u8, arg.data.identifier.name, param.name)) continue;
}
// Skip _ params
if (std.mem.eql(u8, param.name, "_")) continue;
const loc = sx.errors.SourceLoc.compute(doc.source, arg.span.start);
if (loc.line == 0 or loc.col == 0) continue;
const label = std.fmt.allocPrint(self.allocator, "{s}:", .{param.name}) catch continue;
hints.append(self.allocator, .{
.line = loc.line - 1,
.character = loc.col - 1,
.label = label,
.padding_left = false,
}) catch {};
}
}
fn resolveCallTarget(self: *Server, doc: *const Document, call: sx.ast.Call, param_offset: *usize) ?sx.ast.FnDecl {
param_offset.* = 0;
if (call.callee.data == .identifier) {
const name = call.callee.data.identifier.name;
return self.findFnDeclByName(doc, name);
}
if (call.callee.data == .field_access) {
const fa = call.callee.data.field_access;
// Try namespaced: "ns.func"
if (fa.object.data == .identifier) {
const ns_name = fa.object.data.identifier.name;
const qualified = std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ ns_name, fa.field }) catch return null;
if (self.findFnDeclByName(doc, qualified)) |fd| {
return fd;
}
}
// Try UFCS: bare function name, skip first param (receiver)
if (self.findFnDeclByName(doc, fa.field)) |fd| {
if (fd.params.len == call.args.len + 1) {
param_offset.* = 1;
}
return fd;
}
}
return null;
}
fn classifyToken(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 {
const ST = lsp.SemanticTokenType;
return switch (tok.tag) {
@@ -1084,6 +1405,9 @@ pub const Server = struct {
.pipe_arrow,
.caret,
.caret_equal,
.question,
.question_question,
.question_dot,
.tilde,
.less_less,
.less_less_equal,

View File

@@ -108,7 +108,8 @@ pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
"\"semanticTokensProvider\":{{\"legend\":{{" ++
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
"}},\"full\":true}}}}}}",
"}},\"full\":true}}," ++
"\"inlayHintProvider\":true}}}}",
.{},
);
}
@@ -358,3 +359,29 @@ pub fn publishDiagnosticsJson(allocator: std.mem.Allocator, uri: []const u8, dia
try buf.appendSlice(allocator, "]}");
return buf.items;
}
pub const InlayHint = struct {
line: u32,
character: u32,
label: []const u8,
kind: u32 = 1, // 1 = Type
padding_left: bool = true,
padding_right: bool = false,
};
/// Build inlay hints JSON array response.
pub fn inlayHintsJson(allocator: std.mem.Allocator, hints: []const InlayHint) ![]const u8 {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '[');
for (hints, 0..) |hint, idx| {
if (idx > 0) try buf.append(allocator, ',');
const label_escaped = try jsonString(allocator, hint.label);
const json = try std.fmt.allocPrint(allocator,
"{{\"position\":{{\"line\":{d},\"character\":{d}}},\"label\":{s},\"kind\":{d},\"paddingLeft\":{s},\"paddingRight\":{s}}}",
.{ hint.line, hint.character, label_escaped, hint.kind, if (hint.padding_left) "true" else "false", if (hint.padding_right) "true" else "false" },
);
try buf.appendSlice(allocator, json);
}
try buf.append(allocator, ']');
return buf.items;
}

View File

@@ -327,6 +327,13 @@ pub const Parser = struct {
fn parseTypeExpr(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
// Optional type: ?T
if (self.current.tag == .question) {
self.advance(); // skip '?'
const inner_type = try self.parseTypeExpr();
return try self.createNode(start, .{ .optional_type_expr = .{ .inner_type = inner_type } });
}
// Pointer type: *T
if (self.current.tag == .star) {
self.advance(); // skip '*'
@@ -1128,6 +1135,14 @@ pub const Parser = struct {
continue;
}
// Null coalescing: expr ?? default
if (self.current.tag == .question_question and Prec.null_coalesce >= min_prec) {
self.advance();
const rhs = try self.parseBinary(Prec.null_coalesce + 1);
lhs = try self.createNode(lhs.span.start, .{ .null_coalesce = .{ .lhs = lhs, .rhs = rhs } });
continue;
}
const prec = self.binaryPrec();
if (prec == 0 or prec < min_prec) break;
@@ -1291,6 +1306,20 @@ pub const Parser = struct {
} else {
return self.fail("expected field name or index after '.'");
}
} else if (self.current.tag == .question_dot) {
// Optional chaining: expr?.field
self.advance();
if (self.current.tag == .identifier) {
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field, .is_optional = true } });
} else if (self.current.tag == .int_literal) {
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field, .is_optional = true } });
} else {
return self.fail("expected field name after '?.'");
}
} else if (self.current.tag == .l_bracket) {
// Index or slice access: expr[expr] or expr[start..end]
self.advance();
@@ -1323,6 +1352,11 @@ pub const Parser = struct {
} });
}
}
} else if (self.current.tag == .bang) {
// Force unwrap: expr!
// Only if it's not != (bang_equal would have been lexed as a single token)
self.advance();
expr = try self.createNode(expr.span.start, .{ .force_unwrap = .{ .operand = expr } });
} else {
break;
}
@@ -1532,6 +1566,32 @@ pub const Parser = struct {
const start = self.current.loc.start;
self.advance(); // skip 'if'
// Optional binding: if val := expr { ... }
// Detect: identifier followed by :=
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current);
self.advance(); // skip identifier
self.advance(); // skip :=
const source_expr = try self.parseExpr();
const then_branch = try self.parseBlock();
var else_branch: ?*Node = null;
if (self.current.tag == .kw_else) {
self.advance();
if (self.current.tag == .kw_if) {
else_branch = try self.parseIfExpr();
} else {
else_branch = try self.parseBlock();
}
}
return try self.createNode(start, .{ .if_expr = .{
.condition = source_expr,
.then_branch = then_branch,
.else_branch = else_branch,
.is_inline = false,
.binding_name = binding_name,
} });
}
// Parse condition above comparison level, leaving comparisons
// unconsumed for manual handling with match disambiguation.
var condition = try self.parseBinary(Prec.shift);
@@ -1627,6 +1687,20 @@ pub const Parser = struct {
const start = self.current.loc.start;
self.advance(); // skip 'while'
// Optional binding: while val := expr { ... }
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current);
self.advance(); // skip identifier
self.advance(); // skip :=
const source_expr = try self.parseExpr();
const body = try self.parseBlock();
return try self.createNode(start, .{ .while_expr = .{
.condition = source_expr,
.body = body,
.binding_name = binding_name,
} });
}
const condition = try self.parseExpr();
const body = try self.parseBlock();
@@ -1934,15 +2008,16 @@ pub const Parser = struct {
const Prec = struct {
const none: u8 = 0;
const pipe: u8 = 1; // |>
const logical_or: u8 = 2; // or
const logical_and: u8 = 3; // and
const bit_or: u8 = 4; // |
const bit_xor: u8 = 5; // ^
const bit_and: u8 = 6; // &
const comparison: u8 = 7; // == != < <= > >= in
const shift: u8 = 8; // << >>
const additive: u8 = 9; // + -
const multiplicative: u8 = 10; // * / %
const null_coalesce: u8 = 2; // ??
const logical_or: u8 = 3; // or
const logical_and: u8 = 4; // and
const bit_or: u8 = 5; // |
const bit_xor: u8 = 6; // ^
const bit_and: u8 = 7; // &
const comparison: u8 = 8; // == != < <= > >= in
const shift: u8 = 9; // << >>
const additive: u8 = 10; // + -
const multiplicative: u8 = 11; // * / %
};
fn binaryPrec(self: *const Parser) u8 {

View File

@@ -278,6 +278,13 @@ pub const Analyzer = struct {
const elem_name = elem_type.displayName(self.allocator) catch return .void_type;
return .{ .slice_type = .{ .element_name = elem_name } };
}
// Optional type: ?T
if (tn.data == .optional_type_expr) {
const ote = tn.data.optional_type_expr;
const inner_type = self.resolveTypeNode(ote.inner_type);
const inner_name = inner_type.displayName(self.allocator) catch return .void_type;
return .{ .optional_type = .{ .child_name = inner_name } };
}
// Pointer type: *T
if (tn.data == .pointer_type_expr) {
const pte = tn.data.pointer_type_expr;
@@ -456,6 +463,16 @@ pub const Analyzer = struct {
}
return .void_type;
},
.force_unwrap => |fu| {
const opt_ty = self.inferExprType(fu.operand);
if (opt_ty.isOptional()) return Type.fromName(opt_ty.optional_type.child_name) orelse .void_type;
return .void_type;
},
.null_coalesce => |nc| {
const opt_ty = self.inferExprType(nc.lhs);
if (opt_ty.isOptional()) return Type.fromName(opt_ty.optional_type.child_name) orelse .void_type;
return self.inferExprType(nc.rhs);
},
.deref_expr => |de| {
const ptr_ty = self.inferExprType(de.operand);
if (ptr_ty.isPointer()) return ptr_ty.pointerPointeeType() orelse .void_type;
@@ -711,7 +728,20 @@ pub const Analyzer = struct {
},
.if_expr => |ie| {
try self.analyzeNode(ie.condition);
try self.analyzeNode(ie.then_branch);
if (ie.binding_name) |bname| {
// `if val := expr { ... }` — val is the unwrapped optional
const cond_ty = self.inferExprType(ie.condition);
const inner_ty: ?Type = if (cond_ty.isOptional())
Type.fromName(cond_ty.optional_type.child_name)
else
null;
try self.pushScope();
try self.addSymbol(bname, .variable, inner_ty, node.span);
try self.analyzeNode(ie.then_branch);
self.popScope();
} else {
try self.analyzeNode(ie.then_branch);
}
if (ie.else_branch) |eb| {
try self.analyzeNode(eb);
}
@@ -729,7 +759,19 @@ pub const Analyzer = struct {
},
.while_expr => |we| {
try self.analyzeNode(we.condition);
try self.analyzeNode(we.body);
if (we.binding_name) |bname| {
const cond_ty = self.inferExprType(we.condition);
const inner_ty: ?Type = if (cond_ty.isOptional())
Type.fromName(cond_ty.optional_type.child_name)
else
null;
try self.pushScope();
try self.addSymbol(bname, .variable, inner_ty, node.span);
try self.analyzeNode(we.body);
self.popScope();
} else {
try self.analyzeNode(we.body);
}
},
.for_expr => |fe| {
try self.analyzeNode(fe.iterable);
@@ -812,6 +854,7 @@ pub const Analyzer = struct {
.slice_type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
.optional_type_expr,
.null_literal,
.array_literal,
.parameterized_type_expr,
@@ -829,6 +872,13 @@ pub const Analyzer = struct {
try self.analyzeNode(elem.value);
}
},
.force_unwrap => |fu| {
try self.analyzeNode(fu.operand);
},
.null_coalesce => |nc| {
try self.analyzeNode(nc.lhs);
try self.analyzeNode(nc.rhs);
},
.deref_expr => |de| {
try self.analyzeNode(de.operand);
},
@@ -864,6 +914,8 @@ pub const Analyzer = struct {
.index_expr,
.slice_expr,
.deref_expr,
.force_unwrap,
.null_coalesce,
.null_literal,
.type_expr,
.insert_expr,
@@ -905,7 +957,17 @@ pub const Analyzer = struct {
}
}
}
// For compound types (pointers, slices, arrays), resolve inner type refs
// Compound types: ?T, *T, [*]T, []T, [N]T — delegate to resolveTypeNode
switch (tn.data) {
.optional_type_expr, .pointer_type_expr, .many_pointer_type_expr,
.slice_type_expr, .array_type_expr,
=> {
const resolved = self.resolveTypeNode(tn);
if (resolved != .void_type) return resolved;
},
else => {},
}
// For compound types, resolve inner type refs
self.resolveTypeRef(tn);
}
return null;
@@ -950,6 +1012,9 @@ pub const Analyzer = struct {
.array_type_expr => |ate| {
self.resolveTypeRef(ate.element_type);
},
.optional_type_expr => |ote| {
self.resolveTypeRef(ote.inner_type);
},
else => {},
}
}
@@ -1152,6 +1217,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
.slice_type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
.optional_type_expr,
.null_literal,
.array_literal,
.parameterized_type_expr,
@@ -1165,6 +1231,13 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
if (findNodeAtOffset(elem.value, offset)) |found| return found;
}
},
.null_coalesce => |nc| {
if (findNodeAtOffset(nc.lhs, offset)) |found| return found;
if (findNodeAtOffset(nc.rhs, offset)) |found| return found;
},
.force_unwrap => |fu| {
if (findNodeAtOffset(fu.operand, offset)) |found| return found;
},
.deref_expr => |de| {
if (findNodeAtOffset(de.operand, offset)) |found| return found;
},

View File

@@ -70,6 +70,9 @@ pub const Tag = enum {
pipe_arrow, // |>
caret, // ^
caret_equal, // ^=
question, // ?
question_question, // ??
question_dot, // ?.
tilde, // ~
less_less, // <<
less_less_equal, // <<=
@@ -142,6 +145,9 @@ pub const Tag = enum {
.pipe_arrow => "|>",
.caret => "^",
.caret_equal => "^=",
.question => "?",
.question_question => "??",
.question_dot => "?.",
.tilde => "~",
.less_less => "<<",
.less_less_equal => "<<=",

View File

@@ -23,6 +23,7 @@ pub const Type = union(enum) {
vector_type: VectorTypeInfo,
function_type: FunctionTypeInfo,
any_type,
optional_type: OptionalTypeInfo,
meta_type: MetaTypeInfo,
tuple_type: TupleTypeInfo,
@@ -53,6 +54,10 @@ pub const Type = union(enum) {
length: u32,
};
pub const OptionalTypeInfo = struct {
child_name: []const u8,
};
pub const MetaTypeInfo = struct {
name: []const u8,
};
@@ -90,6 +95,7 @@ pub const Type = union(enum) {
}
return info.return_type.eql(o.return_type.*);
},
.optional_type => |info| std.mem.eql(u8, info.child_name, other.optional_type.child_name),
.meta_type => |info| std.mem.eql(u8, info.name, other.meta_type.name),
.tuple_type => |info| {
const o = other.tuple_type;
@@ -141,6 +147,7 @@ pub const Type = union(enum) {
if (std.mem.eql(u8, name, "f64")) return .f64;
return null;
},
'?' => if (name.len >= 2) .{ .optional_type = .{ .child_name = name[1..] } } else null,
'A' => if (std.mem.eql(u8, name, "Any")) .any_type else null,
'v' => if (std.mem.eql(u8, name, "void")) .void_type else null,
'[' => {
@@ -212,6 +219,20 @@ pub const Type = union(enum) {
};
}
pub fn isOptional(self: Type) bool {
return switch (self) {
.optional_type => true,
else => false,
};
}
pub fn optionalChild(self: Type) ?[]const u8 {
return switch (self) {
.optional_type => |info| info.child_name,
else => null,
};
}
pub fn isAny(self: Type) bool {
return switch (self) {
.any_type => true,
@@ -382,6 +403,30 @@ pub const Type = union(enum) {
return true;
}
// T → ?T: any type implicitly wraps into its optional
if (target.isOptional()) {
const child_name = target.optional_type.child_name;
// null → ?T
if (self.isPointer() and std.mem.eql(u8, self.pointer_type.pointee_name, "void")) return true;
// ?T → ?U when T → U
if (self.isOptional()) {
const self_child = fromName(self.optional_type.child_name) orelse return false;
const target_child = fromName(child_name) orelse return false;
return self_child.isImplicitlyConvertibleTo(target_child);
}
// T → ?T: check if self matches the child type
if (fromName(child_name)) |child_type| {
return self.eql(child_type) or self.isImplicitlyConvertibleTo(child_type);
}
// Non-primitive child (struct/enum name): compare by name
return switch (self) {
.struct_type => |n| std.mem.eql(u8, n, child_name),
.enum_type => |n| std.mem.eql(u8, n, child_name),
.union_type => |n| std.mem.eql(u8, n, child_name),
else => false,
};
}
const src_float = self.isFloat();
const dst_float = target.isFloat();
const src_int = self.isInt();
@@ -461,6 +506,7 @@ pub const Type = union(enum) {
}
return try buf.toOwnedSlice(allocator);
},
.optional_type => |info| return fmtAlloc(allocator, "?{s}", .{info.child_name}),
.meta_type => |info| info.name,
.tuple_type => |info| {
var buf = std.ArrayList(u8).empty;
@@ -531,6 +577,9 @@ pub const Type = union(enum) {
return Type.s(capped);
}
// Optional types: widen inner types
if (a.isOptional() and b.isOptional()) return a;
// Pointer types: both are pointers → return first (all are opaque ptr at LLVM level)
if ((a.isPointer() or a.isManyPointer()) and (b.isPointer() or b.isManyPointer())) return a;

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,20 @@
x = 42
y = null
x! = 42
x ?? 0 = 42
y ?? 99 = 99
if-bind x: 42
if-bind y: none
match some: 42
match none: 0
p?.value = 10
q?.value = 0
o1.inner?.val = 99
o2.inner?.val = 0
narrowed a: 10
guard 42: 42
guard null: 0
both: 10 20
guard2: 7
default next: null
comptime: 141

View File

@@ -249,6 +249,9 @@ cast-int-f64: 42.000000
run-const: 25
run-expr: 42
run-chain: 30
ct-opt-coalesce: 141
ct-opt-unwrap: 77
ct-opt-guard: 10
insert-ok
insert-gen: 42
=== 9. Flags ===
@@ -386,4 +389,41 @@ buf reset: 0
1 == (1)
(1) == 1
1 == 1
--- optionals ---
opt x: 42
opt y: null
unwrap: 10
coalesce a: 42
coalesce b: 99
if-bind x: 7
if-bind y: none
match some: 55
match none: 0
wrap pos: 5
wrap neg: null
opt field default: null
opt field set: 42
opt param a: 42
opt param b: 0
opt param 7: 7
generic opt 1: 5
generic opt 2: 7
generic opt 3: null
chain some: 10
chain none: 0
chain print: 20
chain null: null
deep chain 1: 99
deep chain 2: 0
narrow x: 42
narrow y else: null
narrow else x: 42
guard some: 42
guard none: 0
and both: 10 20
and one null
or guard: 7
or guard null: 0
nested narrow: 10 20
guard loop: 3
=== DONE ===