optionals
This commit is contained in:
@@ -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
97
examples/32-optionals.sx
Normal 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;
|
||||
}
|
||||
@@ -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
116
specs.md
@@ -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`:
|
||||
|
||||
19
src/ast.zig
19
src/ast.zig
@@ -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 {
|
||||
|
||||
789
src/codegen.zig
789
src/codegen.zig
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ¶m_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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
79
src/sema.zig
79
src/sema.zig
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 => "<<=",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
1
tests/expected/32-optionals.exit
Normal file
1
tests/expected/32-optionals.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
20
tests/expected/32-optionals.txt
Normal file
20
tests/expected/32-optionals.txt
Normal 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
|
||||
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user