optionals
This commit is contained in:
@@ -16,7 +16,7 @@ main :: () {
|
|||||||
ptr := @v;
|
ptr := @v;
|
||||||
copy := ptr.*;
|
copy := ptr.*;
|
||||||
print("copy: {}\n", copy);
|
print("copy: {}\n", copy);
|
||||||
|
|
||||||
// null pointer
|
// null pointer
|
||||||
np : *Vec2 = null;
|
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/std.sx";
|
||||||
|
#import "modules/math";
|
||||||
pkg :: #import "modules/testpkg";
|
pkg :: #import "modules/testpkg";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -33,6 +34,14 @@ Defaults :: struct {
|
|||||||
c: s32 = ---;
|
c: s32 = ---;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OptNode :: struct {
|
||||||
|
value: s32;
|
||||||
|
next: ?s32;
|
||||||
|
}
|
||||||
|
|
||||||
|
OptInner :: struct { val: s32; }
|
||||||
|
OptOuter :: struct { inner: ?OptInner; }
|
||||||
|
|
||||||
MyFloat :: f64;
|
MyFloat :: f64;
|
||||||
|
|
||||||
Perms :: enum flags { read; write; execute; }
|
Perms :: enum flags { read; write; execute; }
|
||||||
@@ -86,6 +95,25 @@ CT_VAL :: #run add(10, 15);
|
|||||||
CT_MUL :: #run mul(6, 7);
|
CT_MUL :: #run mul(6, 7);
|
||||||
CT_CHAIN :: #run add(CT_VAL, 5);
|
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
|
// #insert helpers
|
||||||
gen_code :: () -> string {
|
gen_code :: () -> string {
|
||||||
return "print(\"insert-ok\\n\");";
|
return "print(\"insert-ok\\n\");";
|
||||||
@@ -1019,6 +1047,11 @@ END;
|
|||||||
// #run chained dependency
|
// #run chained dependency
|
||||||
print("run-chain: {}\n", CT_CHAIN);
|
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 with function
|
||||||
#insert gen_code();
|
#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");
|
print("=== DONE ===\n");
|
||||||
}
|
}
|
||||||
|
|||||||
116
specs.md
116
specs.md
@@ -429,7 +429,7 @@ set_x :: (p: *Vec2, val: f32) {
|
|||||||
set_x(@v, 99.0);
|
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
|
```sx
|
||||||
np : *Vec2 = null;
|
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.
|
**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)
|
### Foreign Function Interface (C Interop)
|
||||||
|
|
||||||
To call C functions, declare a library constant with `#library` and bind functions with `#foreign`:
|
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,
|
slice_expr: SliceExpr,
|
||||||
pointer_type_expr: PointerTypeExpr,
|
pointer_type_expr: PointerTypeExpr,
|
||||||
many_pointer_type_expr: ManyPointerTypeExpr,
|
many_pointer_type_expr: ManyPointerTypeExpr,
|
||||||
|
optional_type_expr: OptionalTypeExpr,
|
||||||
|
force_unwrap: ForceUnwrap,
|
||||||
|
null_coalesce: NullCoalesce,
|
||||||
deref_expr: DerefExpr,
|
deref_expr: DerefExpr,
|
||||||
null_literal: void,
|
null_literal: void,
|
||||||
while_expr: WhileExpr,
|
while_expr: WhileExpr,
|
||||||
@@ -192,6 +195,7 @@ pub const Call = struct {
|
|||||||
pub const FieldAccess = struct {
|
pub const FieldAccess = struct {
|
||||||
object: *Node,
|
object: *Node,
|
||||||
field: []const u8,
|
field: []const u8,
|
||||||
|
is_optional: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const IfExpr = struct {
|
pub const IfExpr = struct {
|
||||||
@@ -199,6 +203,7 @@ pub const IfExpr = struct {
|
|||||||
then_branch: *Node,
|
then_branch: *Node,
|
||||||
else_branch: ?*Node,
|
else_branch: ?*Node,
|
||||||
is_inline: bool, // true for `if cond then a else b`
|
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 {
|
pub const MatchExpr = struct {
|
||||||
@@ -371,6 +376,19 @@ pub const ManyPointerTypeExpr = struct {
|
|||||||
element_type: *Node,
|
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 {
|
pub const DerefExpr = struct {
|
||||||
operand: *Node,
|
operand: *Node,
|
||||||
};
|
};
|
||||||
@@ -378,6 +396,7 @@ pub const DerefExpr = struct {
|
|||||||
pub const WhileExpr = struct {
|
pub const WhileExpr = struct {
|
||||||
condition: *Node,
|
condition: *Node,
|
||||||
body: *Node,
|
body: *Node,
|
||||||
|
binding_name: ?[]const u8 = null, // for `while val := expr { ... }` optional binding
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ForExpr = struct {
|
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
|
// Code insertion
|
||||||
eval_insert: InsertInfo, // pop string, parse as code, compile + execute inline
|
eval_insert: InsertInfo, // pop string, parse as code, compile + execute inline
|
||||||
|
|
||||||
|
// Optionals
|
||||||
|
opt_unwrap, // pop value, error if null_val, else push back
|
||||||
|
|
||||||
// Unions
|
// Unions
|
||||||
make_union: UnionMake,
|
make_union: UnionMake,
|
||||||
get_union_field: UnionFieldAccess,
|
get_union_field: UnionFieldAccess,
|
||||||
@@ -826,18 +829,43 @@ pub const Compiler = struct {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.if_expr => |ie| {
|
.if_expr => |ie| {
|
||||||
try self.compileNode(ie.condition);
|
if (ie.binding_name) |binding_name| {
|
||||||
const jump_false_idx = self.instructions.items.len;
|
// if val := optional_expr { ... } else { ... }
|
||||||
try self.emit(.{ .jump_if_false = 0 }); // placeholder
|
try self.compileNode(ie.condition);
|
||||||
try self.compileNode(ie.then_branch);
|
// Dup the optional value, test truthiness
|
||||||
if (ie.else_branch) |eb| {
|
try self.emit(.dup);
|
||||||
const jump_end_idx = self.instructions.items.len;
|
const jump_false_idx = self.instructions.items.len;
|
||||||
try self.emit(.{ .jump = 0 }); // placeholder
|
try self.emit(.{ .jump_if_false = 0 }); // placeholder
|
||||||
self.patchJumpIfFalse(jump_false_idx);
|
// Non-null path: the value is on the stack, bind as local
|
||||||
try self.compileNode(eb);
|
const slot: u16 = @intCast(self.locals.items.len);
|
||||||
self.patchJump(jump_end_idx);
|
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 {
|
} 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| {
|
.call => |call_node| {
|
||||||
@@ -1066,6 +1094,20 @@ pub const Compiler = struct {
|
|||||||
.field_names = fnames,
|
.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
|
.ufcs_alias => {}, // UFCS aliases are resolved at codegen, no-op in comptime
|
||||||
else => {
|
else => {
|
||||||
return error.UnsupportedExpression;
|
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
|
// Code insertion
|
||||||
.eval_insert => |info| {
|
.eval_insert => |info| {
|
||||||
// Pop the code string (result of evaluating the inner expression)
|
// 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(.caret, start, self.index);
|
||||||
},
|
},
|
||||||
'~' => return self.makeToken(.tilde, 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() == '=') {
|
if (self.peek() == '=') {
|
||||||
self.index += 1;
|
self.index += 1;
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ pub const Server = struct {
|
|||||||
if (params) |p| self.handleSignatureHelp(id, p) catch |e| self.logError(method, e);
|
if (params) |p| self.handleSignatureHelp(id, p) catch |e| self.logError(method, e);
|
||||||
} else if (std.mem.eql(u8, method, "textDocument/semanticTokens/full")) {
|
} else if (std.mem.eql(u8, method, "textDocument/semanticTokens/full")) {
|
||||||
if (params) |p| self.handleSemanticTokens(id, p) catch |e| self.logError(method, e);
|
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;
|
return true;
|
||||||
@@ -1015,6 +1017,325 @@ pub const Server = struct {
|
|||||||
try self.sendResponse(id_json, result_json);
|
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 {
|
fn classifyToken(tok: sx.token.Token, sema: SemaResult, source: [:0]const u8) ?u32 {
|
||||||
const ST = lsp.SemanticTokenType;
|
const ST = lsp.SemanticTokenType;
|
||||||
return switch (tok.tag) {
|
return switch (tok.tag) {
|
||||||
@@ -1084,6 +1405,9 @@ pub const Server = struct {
|
|||||||
.pipe_arrow,
|
.pipe_arrow,
|
||||||
.caret,
|
.caret,
|
||||||
.caret_equal,
|
.caret_equal,
|
||||||
|
.question,
|
||||||
|
.question_question,
|
||||||
|
.question_dot,
|
||||||
.tilde,
|
.tilde,
|
||||||
.less_less,
|
.less_less,
|
||||||
.less_less_equal,
|
.less_less_equal,
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ pub fn initializeResultJson(allocator: std.mem.Allocator) ![]const u8 {
|
|||||||
"\"semanticTokensProvider\":{{\"legend\":{{" ++
|
"\"semanticTokensProvider\":{{\"legend\":{{" ++
|
||||||
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
|
"\"tokenTypes\":[\"namespace\",\"type\",\"enum\",\"struct\",\"parameter\",\"variable\",\"enumMember\",\"function\",\"keyword\",\"number\",\"string\",\"operator\"]," ++
|
||||||
"\"tokenModifiers\":[\"declaration\",\"readonly\"]" ++
|
"\"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, "]}");
|
try buf.appendSlice(allocator, "]}");
|
||||||
return buf.items;
|
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 {
|
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||||
const start = self.current.loc.start;
|
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
|
// Pointer type: *T
|
||||||
if (self.current.tag == .star) {
|
if (self.current.tag == .star) {
|
||||||
self.advance(); // skip '*'
|
self.advance(); // skip '*'
|
||||||
@@ -1128,6 +1135,14 @@ pub const Parser = struct {
|
|||||||
continue;
|
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();
|
const prec = self.binaryPrec();
|
||||||
if (prec == 0 or prec < min_prec) break;
|
if (prec == 0 or prec < min_prec) break;
|
||||||
|
|
||||||
@@ -1291,6 +1306,20 @@ pub const Parser = struct {
|
|||||||
} else {
|
} else {
|
||||||
return self.fail("expected field name or index after '.'");
|
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) {
|
} else if (self.current.tag == .l_bracket) {
|
||||||
// Index or slice access: expr[expr] or expr[start..end]
|
// Index or slice access: expr[expr] or expr[start..end]
|
||||||
self.advance();
|
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 {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1532,6 +1566,32 @@ pub const Parser = struct {
|
|||||||
const start = self.current.loc.start;
|
const start = self.current.loc.start;
|
||||||
self.advance(); // skip 'if'
|
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
|
// Parse condition above comparison level, leaving comparisons
|
||||||
// unconsumed for manual handling with match disambiguation.
|
// unconsumed for manual handling with match disambiguation.
|
||||||
var condition = try self.parseBinary(Prec.shift);
|
var condition = try self.parseBinary(Prec.shift);
|
||||||
@@ -1627,6 +1687,20 @@ pub const Parser = struct {
|
|||||||
const start = self.current.loc.start;
|
const start = self.current.loc.start;
|
||||||
self.advance(); // skip 'while'
|
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 condition = try self.parseExpr();
|
||||||
const body = try self.parseBlock();
|
const body = try self.parseBlock();
|
||||||
|
|
||||||
@@ -1934,15 +2008,16 @@ pub const Parser = struct {
|
|||||||
const Prec = struct {
|
const Prec = struct {
|
||||||
const none: u8 = 0;
|
const none: u8 = 0;
|
||||||
const pipe: u8 = 1; // |>
|
const pipe: u8 = 1; // |>
|
||||||
const logical_or: u8 = 2; // or
|
const null_coalesce: u8 = 2; // ??
|
||||||
const logical_and: u8 = 3; // and
|
const logical_or: u8 = 3; // or
|
||||||
const bit_or: u8 = 4; // |
|
const logical_and: u8 = 4; // and
|
||||||
const bit_xor: u8 = 5; // ^
|
const bit_or: u8 = 5; // |
|
||||||
const bit_and: u8 = 6; // &
|
const bit_xor: u8 = 6; // ^
|
||||||
const comparison: u8 = 7; // == != < <= > >= in
|
const bit_and: u8 = 7; // &
|
||||||
const shift: u8 = 8; // << >>
|
const comparison: u8 = 8; // == != < <= > >= in
|
||||||
const additive: u8 = 9; // + -
|
const shift: u8 = 9; // << >>
|
||||||
const multiplicative: u8 = 10; // * / %
|
const additive: u8 = 10; // + -
|
||||||
|
const multiplicative: u8 = 11; // * / %
|
||||||
};
|
};
|
||||||
|
|
||||||
fn binaryPrec(self: *const Parser) u8 {
|
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;
|
const elem_name = elem_type.displayName(self.allocator) catch return .void_type;
|
||||||
return .{ .slice_type = .{ .element_name = elem_name } };
|
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
|
// Pointer type: *T
|
||||||
if (tn.data == .pointer_type_expr) {
|
if (tn.data == .pointer_type_expr) {
|
||||||
const pte = tn.data.pointer_type_expr;
|
const pte = tn.data.pointer_type_expr;
|
||||||
@@ -456,6 +463,16 @@ pub const Analyzer = struct {
|
|||||||
}
|
}
|
||||||
return .void_type;
|
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| {
|
.deref_expr => |de| {
|
||||||
const ptr_ty = self.inferExprType(de.operand);
|
const ptr_ty = self.inferExprType(de.operand);
|
||||||
if (ptr_ty.isPointer()) return ptr_ty.pointerPointeeType() orelse .void_type;
|
if (ptr_ty.isPointer()) return ptr_ty.pointerPointeeType() orelse .void_type;
|
||||||
@@ -711,7 +728,20 @@ pub const Analyzer = struct {
|
|||||||
},
|
},
|
||||||
.if_expr => |ie| {
|
.if_expr => |ie| {
|
||||||
try self.analyzeNode(ie.condition);
|
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| {
|
if (ie.else_branch) |eb| {
|
||||||
try self.analyzeNode(eb);
|
try self.analyzeNode(eb);
|
||||||
}
|
}
|
||||||
@@ -729,7 +759,19 @@ pub const Analyzer = struct {
|
|||||||
},
|
},
|
||||||
.while_expr => |we| {
|
.while_expr => |we| {
|
||||||
try self.analyzeNode(we.condition);
|
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| {
|
.for_expr => |fe| {
|
||||||
try self.analyzeNode(fe.iterable);
|
try self.analyzeNode(fe.iterable);
|
||||||
@@ -812,6 +854,7 @@ pub const Analyzer = struct {
|
|||||||
.slice_type_expr,
|
.slice_type_expr,
|
||||||
.pointer_type_expr,
|
.pointer_type_expr,
|
||||||
.many_pointer_type_expr,
|
.many_pointer_type_expr,
|
||||||
|
.optional_type_expr,
|
||||||
.null_literal,
|
.null_literal,
|
||||||
.array_literal,
|
.array_literal,
|
||||||
.parameterized_type_expr,
|
.parameterized_type_expr,
|
||||||
@@ -829,6 +872,13 @@ pub const Analyzer = struct {
|
|||||||
try self.analyzeNode(elem.value);
|
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| {
|
.deref_expr => |de| {
|
||||||
try self.analyzeNode(de.operand);
|
try self.analyzeNode(de.operand);
|
||||||
},
|
},
|
||||||
@@ -864,6 +914,8 @@ pub const Analyzer = struct {
|
|||||||
.index_expr,
|
.index_expr,
|
||||||
.slice_expr,
|
.slice_expr,
|
||||||
.deref_expr,
|
.deref_expr,
|
||||||
|
.force_unwrap,
|
||||||
|
.null_coalesce,
|
||||||
.null_literal,
|
.null_literal,
|
||||||
.type_expr,
|
.type_expr,
|
||||||
.insert_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);
|
self.resolveTypeRef(tn);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -950,6 +1012,9 @@ pub const Analyzer = struct {
|
|||||||
.array_type_expr => |ate| {
|
.array_type_expr => |ate| {
|
||||||
self.resolveTypeRef(ate.element_type);
|
self.resolveTypeRef(ate.element_type);
|
||||||
},
|
},
|
||||||
|
.optional_type_expr => |ote| {
|
||||||
|
self.resolveTypeRef(ote.inner_type);
|
||||||
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1152,6 +1217,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
|
|||||||
.slice_type_expr,
|
.slice_type_expr,
|
||||||
.pointer_type_expr,
|
.pointer_type_expr,
|
||||||
.many_pointer_type_expr,
|
.many_pointer_type_expr,
|
||||||
|
.optional_type_expr,
|
||||||
.null_literal,
|
.null_literal,
|
||||||
.array_literal,
|
.array_literal,
|
||||||
.parameterized_type_expr,
|
.parameterized_type_expr,
|
||||||
@@ -1165,6 +1231,13 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
|
|||||||
if (findNodeAtOffset(elem.value, offset)) |found| return found;
|
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| {
|
.deref_expr => |de| {
|
||||||
if (findNodeAtOffset(de.operand, offset)) |found| return found;
|
if (findNodeAtOffset(de.operand, offset)) |found| return found;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ pub const Tag = enum {
|
|||||||
pipe_arrow, // |>
|
pipe_arrow, // |>
|
||||||
caret, // ^
|
caret, // ^
|
||||||
caret_equal, // ^=
|
caret_equal, // ^=
|
||||||
|
question, // ?
|
||||||
|
question_question, // ??
|
||||||
|
question_dot, // ?.
|
||||||
tilde, // ~
|
tilde, // ~
|
||||||
less_less, // <<
|
less_less, // <<
|
||||||
less_less_equal, // <<=
|
less_less_equal, // <<=
|
||||||
@@ -142,6 +145,9 @@ pub const Tag = enum {
|
|||||||
.pipe_arrow => "|>",
|
.pipe_arrow => "|>",
|
||||||
.caret => "^",
|
.caret => "^",
|
||||||
.caret_equal => "^=",
|
.caret_equal => "^=",
|
||||||
|
.question => "?",
|
||||||
|
.question_question => "??",
|
||||||
|
.question_dot => "?.",
|
||||||
.tilde => "~",
|
.tilde => "~",
|
||||||
.less_less => "<<",
|
.less_less => "<<",
|
||||||
.less_less_equal => "<<=",
|
.less_less_equal => "<<=",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub const Type = union(enum) {
|
|||||||
vector_type: VectorTypeInfo,
|
vector_type: VectorTypeInfo,
|
||||||
function_type: FunctionTypeInfo,
|
function_type: FunctionTypeInfo,
|
||||||
any_type,
|
any_type,
|
||||||
|
optional_type: OptionalTypeInfo,
|
||||||
meta_type: MetaTypeInfo,
|
meta_type: MetaTypeInfo,
|
||||||
tuple_type: TupleTypeInfo,
|
tuple_type: TupleTypeInfo,
|
||||||
|
|
||||||
@@ -53,6 +54,10 @@ pub const Type = union(enum) {
|
|||||||
length: u32,
|
length: u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const OptionalTypeInfo = struct {
|
||||||
|
child_name: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
pub const MetaTypeInfo = struct {
|
pub const MetaTypeInfo = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
};
|
};
|
||||||
@@ -90,6 +95,7 @@ pub const Type = union(enum) {
|
|||||||
}
|
}
|
||||||
return info.return_type.eql(o.return_type.*);
|
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),
|
.meta_type => |info| std.mem.eql(u8, info.name, other.meta_type.name),
|
||||||
.tuple_type => |info| {
|
.tuple_type => |info| {
|
||||||
const o = other.tuple_type;
|
const o = other.tuple_type;
|
||||||
@@ -141,6 +147,7 @@ pub const Type = union(enum) {
|
|||||||
if (std.mem.eql(u8, name, "f64")) return .f64;
|
if (std.mem.eql(u8, name, "f64")) return .f64;
|
||||||
return null;
|
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,
|
'A' => if (std.mem.eql(u8, name, "Any")) .any_type else null,
|
||||||
'v' => if (std.mem.eql(u8, name, "void")) .void_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 {
|
pub fn isAny(self: Type) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.any_type => true,
|
.any_type => true,
|
||||||
@@ -382,6 +403,30 @@ pub const Type = union(enum) {
|
|||||||
return true;
|
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 src_float = self.isFloat();
|
||||||
const dst_float = target.isFloat();
|
const dst_float = target.isFloat();
|
||||||
const src_int = self.isInt();
|
const src_int = self.isInt();
|
||||||
@@ -461,6 +506,7 @@ pub const Type = union(enum) {
|
|||||||
}
|
}
|
||||||
return try buf.toOwnedSlice(allocator);
|
return try buf.toOwnedSlice(allocator);
|
||||||
},
|
},
|
||||||
|
.optional_type => |info| return fmtAlloc(allocator, "?{s}", .{info.child_name}),
|
||||||
.meta_type => |info| info.name,
|
.meta_type => |info| info.name,
|
||||||
.tuple_type => |info| {
|
.tuple_type => |info| {
|
||||||
var buf = std.ArrayList(u8).empty;
|
var buf = std.ArrayList(u8).empty;
|
||||||
@@ -531,6 +577,9 @@ pub const Type = union(enum) {
|
|||||||
return Type.s(capped);
|
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)
|
// 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;
|
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-const: 25
|
||||||
run-expr: 42
|
run-expr: 42
|
||||||
run-chain: 30
|
run-chain: 30
|
||||||
|
ct-opt-coalesce: 141
|
||||||
|
ct-opt-unwrap: 77
|
||||||
|
ct-opt-guard: 10
|
||||||
insert-ok
|
insert-ok
|
||||||
insert-gen: 42
|
insert-gen: 42
|
||||||
=== 9. Flags ===
|
=== 9. Flags ===
|
||||||
@@ -386,4 +389,41 @@ buf reset: 0
|
|||||||
1 == (1)
|
1 == (1)
|
||||||
(1) == 1
|
(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 ===
|
=== DONE ===
|
||||||
|
|||||||
Reference in New Issue
Block a user