fix(diagnostics): point reserved-type-name binding errors at the binding (issue 0076)

The reserved-type-name binding diagnostic fired correctly but underlined the
enclosing statement / if / while / for / match / protocol / #objc_class block
because every binding-name check reused the parent `node.span`.

Thread each binding name's own span through the AST and parser, and pass it to
`checkBindingNames`:

- ast: add name spans to VarDecl, DestructureDecl, If/WhileExpr, ForExpr
  (capture + index), MatchArm, Catch/OnFailStmt, Protocol/ForeignMethodDecl.
- parser: populate each span at the binding site from the name token's loc;
  destructure reuses each target identifier's own span.
- semantic_diagnostics: every checkBindingName call now passes the binding's
  own span — no site falls back to node.span. fn/lambda params already used
  Param.name_span.

Carets now land on the offending identifier itself. New regression
examples/1125 asserts the protocol default-body and sx-defined #objc_class
method param spans; 0125/1119-1124 expected updated to the precise carets.
This commit is contained in:
agra
2026-06-03 22:06:56 +03:00
parent fcc76b9391
commit 6433eb6155
15 changed files with 144 additions and 51 deletions

View File

@@ -0,0 +1,30 @@
// A reserved/builtin type name used as a PARAMETER name is rejected inside the
// two method-with-body forms that carry their params as bare name lists rather
// than `Param` nodes: a protocol default-body method (`u8`) and a sx-defined
// foreign-class (`#objc_class`) method (`s16`). The declaration-site diagnostic
// underlines the OFFENDING PARAMETER itself, not the enclosing `protocol` /
// `#objc_class` block — each method's `param_name_spans` is threaded from the
// parser so the caret lands on the parameter token.
//
// Regression (issue 0076, attempt-5 span precision). Expected: one error per
// offending parameter, each caret on the parameter name; exit 1.
#import "modules/std.sx";
#import "modules/compiler.sx";
Greeter :: protocol {
greet :: (self: *Self, u8: s64) -> s64 {
return u8;
}
}
SxFoo :: #objc_class("SxFoo") {
counter: s32;
bump :: (self: *Self, s16: s32) {
self.counter += s16;
}
}
main :: () -> s32 {
return 0;
}

View File

@@ -2,4 +2,4 @@ error: 's2' is a reserved type name and cannot be used as an identifier
--> /Users/agra/projects/sx/examples/0125-types-type-named-var-rejected.sx:10:5 --> /Users/agra/projects/sx/examples/0125-types-type-named-var-rejected.sx:10:5
| |
10 | s2 := 42; 10 | s2 := 42;
| ^^^^^^^^^ | ^^

View File

@@ -8,16 +8,16 @@ error: 's64' is a reserved type name and cannot be used as an identifier
--> /Users/agra/projects/sx/examples/1119-diagnostics-reserved-type-name-as-identifier.sx:12:5 --> /Users/agra/projects/sx/examples/1119-diagnostics-reserved-type-name-as-identifier.sx:12:5
| |
12 | s64 : s32 = 3; 12 | s64 : s32 = 3;
| ^^^^^^^^^^^^^^ | ^^^
error: 'bool' is a reserved type name and cannot be used as an identifier error: 'bool' is a reserved type name and cannot be used as an identifier
--> /Users/agra/projects/sx/examples/1119-diagnostics-reserved-type-name-as-identifier.sx:13:5 --> /Users/agra/projects/sx/examples/1119-diagnostics-reserved-type-name-as-identifier.sx:13:5
| |
13 | bool : bool = true; 13 | bool : bool = true;
| ^^^^^^^^^^^^^^^^^^^ | ^^^^
error: 'string' is a reserved type name and cannot be used as an identifier error: 'string' is a reserved type name and cannot be used as an identifier
--> /Users/agra/projects/sx/examples/1119-diagnostics-reserved-type-name-as-identifier.sx:14:5 --> /Users/agra/projects/sx/examples/1119-diagnostics-reserved-type-name-as-identifier.sx:14:5
| |
14 | string := "x"; 14 | string := "x";
| ^^^^^^^^^^^^^^ | ^^^^^^

View File

@@ -2,4 +2,4 @@ error: 's2' is a reserved type name and cannot be used as an identifier
--> examples/1120-diagnostics-imported-reserved-type-name/mod.sx:11:5 --> examples/1120-diagnostics-imported-reserved-type-name/mod.sx:11:5
| |
11 | s2 := Box.{ total = 0, count = 0 }; 11 | s2 := Box.{ total = 0, count = 0 };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^

View File

@@ -2,40 +2,34 @@ error: 's2' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:18:5 --> examples/1121-diagnostics-reserved-name-control-flow.sx:18:5
| |
18 | s2, rest := pair(); // destructure name 18 | s2, rest := pair(); // destructure name
| ^^^^^^^^^^^^^^^^^^^ | ^^
error: 'u8' is a reserved type name and cannot be used as an identifier error: 'u8' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:19:5 --> examples/1121-diagnostics-reserved-name-control-flow.sx:19:8
| |
19 | if u8 := maybe() { } // if optional binding 19 | if u8 := maybe() { } // if optional binding
| ^^^^^^^^^^^^^^^^^^^^ | ^^
error: 's16' is a reserved type name and cannot be used as an identifier error: 's16' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:20:5 --> examples/1121-diagnostics-reserved-name-control-flow.sx:20:11
| |
20 | while s16 := maybe() { break; } // while optional binding 20 | while s16 := maybe() { break; } // while optional binding
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^
error: 'bool' is a reserved type name and cannot be used as an identifier error: 'bool' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:22:5 --> examples/1121-diagnostics-reserved-name-control-flow.sx:22:14
| |
22 | for xs: (bool) { } // for capture name 22 | for xs: (bool) { } // for capture name
| ^^^^^^^^^^^^^^^^^^ | ^^^^
error: 's32' is a reserved type name and cannot be used as an identifier error: 's32' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:23:5 --> examples/1121-diagnostics-reserved-name-control-flow.sx:23:17
| |
23 | for xs: (v, s32) { } // for index name 23 | for xs: (v, s32) { } // for index name
| ^^^^^^^^^^^^^^^^^^^^ | ^^^
error: 'string' is a reserved type name and cannot be used as an identifier error: 'string' is a reserved type name and cannot be used as an identifier
--> examples/1121-diagnostics-reserved-name-control-flow.sx:25:10 --> examples/1121-diagnostics-reserved-name-control-flow.sx:26:22
| |
25 | r := if opt == { // match-arm capture
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | case .some: (string) { 0 } 26 | case .some: (string) { 0 }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^
27 | case .none: { 0 }
| ^^^^^^^^^^^^^^^^^^^^^^^^^
28 | };
| ^^^^^

View File

@@ -8,4 +8,4 @@ error: 's2' is a reserved type name and cannot be used as an identifier
--> examples/1122-diagnostics-reserved-name-impl-method.sx:20:9 --> examples/1122-diagnostics-reserved-name-impl-method.sx:20:9
| |
20 | s2 := Box.{ total = 1 }; 20 | s2 := Box.{ total = 1 };
| ^^^^^^^^^^^^^^^^^^^^^^^^ | ^^

View File

@@ -1,11 +1,11 @@
error: 's64' is a reserved type name and cannot be used as an identifier error: 's64' is a reserved type name and cannot be used as an identifier
--> examples/1123-diagnostics-reserved-name-catch-onfail.sx:20:5 --> examples/1123-diagnostics-reserved-name-catch-onfail.sx:20:12
| |
20 | onfail s64 { } // onfail tag binding 20 | onfail s64 { } // onfail tag binding
| ^^^^^^^^^^^^^^ | ^^^
error: 'u8' is a reserved type name and cannot be used as an identifier error: 'u8' is a reserved type name and cannot be used as an identifier
--> examples/1123-diagnostics-reserved-name-catch-onfail.sx:21:5 --> examples/1123-diagnostics-reserved-name-catch-onfail.sx:21:19
| |
21 | must(n) catch u8 { return; }; // catch tag binding 21 | must(n) catch u8 { return; }; // catch tag binding
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^

View File

@@ -2,4 +2,4 @@ error: 's2' is a reserved type name and cannot be used as an identifier
--> examples/1124-diagnostics-imported-reserved-destructure/mod.sx:6:5 --> examples/1124-diagnostics-imported-reserved-destructure/mod.sx:6:5
| |
6 | s2, rest := pair(); // destructure name in an IMPORTED module 6 | s2, rest := pair(); // destructure name in an IMPORTED module
| ^^^^^^^^^^^^^^^^^^^ | ^^

View File

@@ -0,0 +1,11 @@
error: 'u8' is a reserved type name and cannot be used as an identifier
--> examples/1125-diagnostics-reserved-name-method-param.sx:15:28
|
15 | greet :: (self: *Self, u8: s64) -> s64 {
| ^^
error: 's16' is a reserved type name and cannot be used as an identifier
--> examples/1125-diagnostics-reserved-name-method-param.sx:23:27
|
23 | bump :: (self: *Self, s16: s32) {
| ^^^

View File

@@ -34,6 +34,17 @@
> function's bindings never reach `Scope.put`, yet they must still be rejected at > function's bindings never reach `Scope.put`, yet they must still be rejected at
> their declaration (e.g. `examples/1119`'s never-called `takes_u8`). > their declaration (e.g. `examples/1119`'s never-called `takes_u8`).
> >
> **Span precision (attempt 5).** Every binding form now carries its own
> name span in the AST (`VarDecl.name_span`, `DestructureDecl.name_spans`,
> `IfExpr`/`WhileExpr.binding_span`, `ForExpr.capture_span`/`index_span`,
> `MatchArm.capture_span`, `CatchExpr`/`OnFailStmt.binding_span`,
> `Protocol`/`ForeignMethodDecl.param_name_spans`), populated by the parser at
> each binding site. `checkBindingNames` passes that span to the diagnostic, so
> the caret underlines the offending identifier itself instead of the enclosing
> statement / `if` / `match` / `protocol` / `#objc_class` block. No call site
> falls back to the parent `node.span`. Regular `fn`/lambda params already used
> `Param.name_span`.
>
> **Regression tests:** > **Regression tests:**
> - `examples/0125-types-type-named-var-rejected.sx` — `:=` form (`s2`) rejected. > - `examples/0125-types-type-named-var-rejected.sx` — `:=` form (`s2`) rejected.
> - `examples/1119-diagnostics-reserved-type-name-as-identifier.sx` — parameter > - `examples/1119-diagnostics-reserved-type-name-as-identifier.sx` — parameter
@@ -47,6 +58,9 @@
> `onfail` error-tag bindings. > `onfail` error-tag bindings.
> - `examples/1124-diagnostics-imported-reserved-destructure.sx` — destructure > - `examples/1124-diagnostics-imported-reserved-destructure.sx` — destructure
> name reserved in an IMPORTED module (renders against that module's source). > name reserved in an IMPORTED module (renders against that module's source).
> - `examples/1125-diagnostics-reserved-name-method-param.sx` — protocol
> default-body method param AND sx-defined `#objc_class` method param, each
> caret landing on the parameter token.
> - `examples/0135-types-self-streaming-nonreserved.sx` — positive: `*self` > - `examples/0135-types-self-streaming-nonreserved.sx` — positive: `*self`
> streaming with non-reserved names (`hasher`, `ctx`) accumulates correctly via > streaming with non-reserved names (`hasher`, `ctx`) accumulates correctly via
> both `update(@h, …)` and `h.update(…)`. > both `update(@h, …)` and `h.update(…)`.

View File

@@ -272,6 +272,7 @@ pub const IfExpr = struct {
is_inline: bool, // true for `if cond then a else b` is_inline: bool, // true for `if cond then a else b`
is_comptime: bool = false, // true for `inline if` — compile-time branch elimination is_comptime: bool = false, // true for `inline if` — compile-time branch elimination
binding_name: ?[]const u8 = null, // for `if val := expr { ... }` optional binding binding_name: ?[]const u8 = null, // for `if val := expr { ... }` optional binding
binding_span: ?Span = null, // span of `binding_name` (set iff `binding_name` is)
}; };
pub const MatchExpr = struct { pub const MatchExpr = struct {
@@ -285,6 +286,7 @@ pub const MatchArm = struct {
body: *Node, body: *Node,
is_break: bool, is_break: bool,
capture: ?[]const u8 = null, // payload binding name: case .variant: (name) { ... } capture: ?[]const u8 = null, // payload binding name: case .variant: (name) { ... }
capture_span: ?Span = null, // span of `capture` (set iff `capture` is)
}; };
pub const ConstDecl = struct { pub const ConstDecl = struct {
@@ -295,6 +297,7 @@ pub const ConstDecl = struct {
pub const VarDecl = struct { pub const VarDecl = struct {
name: []const u8, name: []const u8,
name_span: Span,
type_annotation: ?*Node, type_annotation: ?*Node,
value: ?*Node, value: ?*Node,
is_foreign: bool = false, is_foreign: bool = false,
@@ -329,6 +332,7 @@ pub const MultiAssign = struct {
pub const DestructureDecl = struct { pub const DestructureDecl = struct {
names: []const []const u8, names: []const []const u8,
name_spans: []const Span, // one per entry in `names`, same order
value: *Node, value: *Node,
}; };
@@ -449,6 +453,7 @@ pub const TryExpr = struct {
pub const CatchExpr = struct { pub const CatchExpr = struct {
operand: *Node, operand: *Node,
binding: ?[]const u8 = null, binding: ?[]const u8 = null,
binding_span: ?Span = null, // span of `binding` (set iff `binding` is)
body: *Node, body: *Node,
is_match_body: bool = false, is_match_body: bool = false,
}; };
@@ -458,6 +463,7 @@ pub const CatchExpr = struct {
/// a bare expression (`onfail EXPR;`). /// a bare expression (`onfail EXPR;`).
pub const OnFailStmt = struct { pub const OnFailStmt = struct {
binding: ?[]const u8 = null, binding: ?[]const u8 = null,
binding_span: ?Span = null, // span of `binding` (set iff `binding` is)
body: *Node, body: *Node,
}; };
@@ -551,13 +557,16 @@ pub const WhileExpr = struct {
condition: *Node, condition: *Node,
body: *Node, body: *Node,
binding_name: ?[]const u8 = null, // for `while val := expr { ... }` optional binding binding_name: ?[]const u8 = null, // for `while val := expr { ... }` optional binding
binding_span: ?Span = null, // span of `binding_name` (set iff `binding_name` is)
}; };
pub const ForExpr = struct { pub const ForExpr = struct {
iterable: *Node, iterable: *Node,
body: *Node, body: *Node,
capture_name: []const u8, capture_name: []const u8,
capture_span: ?Span = null, // span of `capture_name` (null when omitted, e.g. `for 0..N { }`)
index_name: ?[]const u8 = null, index_name: ?[]const u8 = null,
index_span: ?Span = null, // span of `index_name` (set iff `index_name` is)
/// Range form `for start..end (i) { }`: `iterable` is the start, `range_end` /// Range form `for start..end (i) { }`: `iterable` is the start, `range_end`
/// the (exclusive) end. Null for the iterate-a-collection form /// the (exclusive) end. Null for the iterate-a-collection form
/// (`for coll : (x) { }`). For the range form `capture_name` is the cursor /// (`for coll : (x) { }`). For the range form `capture_name` is the cursor
@@ -645,6 +654,7 @@ pub const ProtocolMethodDecl = struct {
name: []const u8, name: []const u8,
params: []const *Node, // type_expr nodes for parameter types (excluding implicit self) params: []const *Node, // type_expr nodes for parameter types (excluding implicit self)
param_names: []const []const u8, // parameter names (excluding implicit self) param_names: []const []const u8, // parameter names (excluding implicit self)
param_name_spans: []const Span = &.{}, // one per `param_names` entry; empty for synthesized methods
return_type: ?*Node, // null = void return return_type: ?*Node, // null = void return
default_body: ?*Node, // null = required method, non-null = default implementation default_body: ?*Node, // null = required method, non-null = default implementation
}; };
@@ -670,6 +680,7 @@ pub const ForeignMethodDecl = struct {
name: []const u8, name: []const u8,
params: []const *Node, // type_expr nodes — first is `*Self` for instance methods params: []const *Node, // type_expr nodes — first is `*Self` for instance methods
param_names: []const []const u8, param_names: []const []const u8,
param_name_spans: []const Span = &.{}, // one per `param_names` entry; empty for synthesized methods
return_type: ?*Node, // null = void return_type: ?*Node, // null = void
is_static: bool = false, // true for `static name :: ...` is_static: bool = false, // true for `static name :: ...`
jni_descriptor_override: ?[]const u8 = null, // `#jni_method_descriptor("(Sig)Ret")` — JNI runtime only jni_descriptor_override: ?[]const u8 = null, // `#jni_method_descriptor("(Sig)Ret")` — JNI runtime only

View File

@@ -117,11 +117,11 @@ pub const UnknownTypeChecker = struct {
switch (node.data) { switch (node.data) {
// ── Binding-introducing nodes: check the name(s), then recurse. ── // ── Binding-introducing nodes: check the name(s), then recurse. ──
.var_decl => |vd| { .var_decl => |vd| {
self.checkBindingName(vd.name, node.span); self.checkBindingName(vd.name, vd.name_span);
if (vd.value) |v| self.checkBindingNames(v); if (vd.value) |v| self.checkBindingNames(v);
}, },
.destructure_decl => |dd| { .destructure_decl => |dd| {
for (dd.names) |n| self.checkBindingName(n, node.span); for (dd.names, dd.name_spans) |n, sp| self.checkBindingName(n, sp);
self.checkBindingNames(dd.value); self.checkBindingNames(dd.value);
}, },
.fn_decl => |fd| { .fn_decl => |fd| {
@@ -137,19 +137,19 @@ pub const UnknownTypeChecker = struct {
if (p.default_expr) |de| self.checkBindingNames(de); if (p.default_expr) |de| self.checkBindingNames(de);
}, },
.if_expr => |ie| { .if_expr => |ie| {
if (ie.binding_name) |bn| self.checkBindingName(bn, node.span); if (ie.binding_name) |bn| self.checkBindingName(bn, ie.binding_span);
self.checkBindingNames(ie.condition); self.checkBindingNames(ie.condition);
self.checkBindingNames(ie.then_branch); self.checkBindingNames(ie.then_branch);
if (ie.else_branch) |e| self.checkBindingNames(e); if (ie.else_branch) |e| self.checkBindingNames(e);
}, },
.while_expr => |we| { .while_expr => |we| {
if (we.binding_name) |bn| self.checkBindingName(bn, node.span); if (we.binding_name) |bn| self.checkBindingName(bn, we.binding_span);
self.checkBindingNames(we.condition); self.checkBindingNames(we.condition);
self.checkBindingNames(we.body); self.checkBindingNames(we.body);
}, },
.for_expr => |fe| { .for_expr => |fe| {
if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, node.span); if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span);
if (fe.index_name) |idx| self.checkBindingName(idx, node.span); if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span);
self.checkBindingNames(fe.iterable); self.checkBindingNames(fe.iterable);
if (fe.range_end) |re| self.checkBindingNames(re); if (fe.range_end) |re| self.checkBindingNames(re);
self.checkBindingNames(fe.body); self.checkBindingNames(fe.body);
@@ -157,23 +157,23 @@ pub const UnknownTypeChecker = struct {
.match_expr => |me| { .match_expr => |me| {
self.checkBindingNames(me.subject); self.checkBindingNames(me.subject);
for (me.arms) |arm| { for (me.arms) |arm| {
if (arm.capture) |cap| self.checkBindingName(cap, node.span); if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span);
if (arm.pattern) |p| self.checkBindingNames(p); if (arm.pattern) |p| self.checkBindingNames(p);
self.checkBindingNames(arm.body); self.checkBindingNames(arm.body);
} }
}, },
.match_arm => |arm| { .match_arm => |arm| {
if (arm.capture) |cap| self.checkBindingName(cap, node.span); if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span);
if (arm.pattern) |p| self.checkBindingNames(p); if (arm.pattern) |p| self.checkBindingNames(p);
self.checkBindingNames(arm.body); self.checkBindingNames(arm.body);
}, },
.catch_expr => |ce| { .catch_expr => |ce| {
if (ce.binding) |b| self.checkBindingName(b, node.span); if (ce.binding) |b| self.checkBindingName(b, ce.binding_span);
self.checkBindingNames(ce.operand); self.checkBindingNames(ce.operand);
self.checkBindingNames(ce.body); self.checkBindingNames(ce.body);
}, },
.onfail_stmt => |os| { .onfail_stmt => |os| {
if (os.binding) |b| self.checkBindingName(b, node.span); if (os.binding) |b| self.checkBindingName(b, os.binding_span);
self.checkBindingNames(os.body); self.checkBindingNames(os.body);
}, },
// impl / protocol-default / foreign-class method bodies: each // impl / protocol-default / foreign-class method bodies: each
@@ -183,13 +183,13 @@ pub const UnknownTypeChecker = struct {
.impl_block => |ib| for (ib.methods) |m| self.checkBindingNames(m), .impl_block => |ib| for (ib.methods) |m| self.checkBindingNames(m),
.protocol_decl => |pd| for (pd.methods) |m| { .protocol_decl => |pd| for (pd.methods) |m| {
if (m.default_body) |body| { if (m.default_body) |body| {
for (m.param_names) |pn| self.checkBindingName(pn, node.span); for (m.param_names, m.param_name_spans) |pn, sp| self.checkBindingName(pn, sp);
self.checkBindingNames(body); self.checkBindingNames(body);
} }
}, },
.foreign_class_decl => |fcd| for (fcd.members) |member| switch (member) { .foreign_class_decl => |fcd| for (fcd.members) |member| switch (member) {
.method => |m| if (m.body) |body| { .method => |m| if (m.body) |body| {
for (m.param_names) |pn| self.checkBindingName(pn, node.span); for (m.param_names, m.param_name_spans) |pn, sp| self.checkBindingName(pn, sp);
self.checkBindingNames(body); self.checkBindingNames(body);
}, },
.field, .extends, .implements => {}, .field, .extends, .implements => {},

View File

@@ -145,6 +145,7 @@ pub const Parser = struct {
return self.fail("expected identifier at top level"); return self.fail("expected identifier at top level");
} }
const name = self.tokenSlice(self.current); const name = self.tokenSlice(self.current);
const name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
// IDENT :: ... // IDENT :: ...
@@ -157,7 +158,7 @@ pub const Parser = struct {
// IDENT : type = value; (typed variable) // IDENT : type = value; (typed variable)
if (self.current.tag == .colon) { if (self.current.tag == .colon) {
self.advance(); self.advance();
return self.parseTypedBinding(name, start); return self.parseTypedBinding(name, name_span, start);
} }
// IDENT := value; (variable) // IDENT := value; (variable)
@@ -165,7 +166,7 @@ pub const Parser = struct {
self.advance(); self.advance();
const value = try self.parseExpr(); const value = try self.parseExpr();
try self.expectSemicolonAfter(value); try self.expectSemicolonAfter(value);
return try self.createNode(start, .{ .var_decl = .{ .name = name, .type_annotation = null, .value = value } }); return try self.createNode(start, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = null, .value = value } });
} }
return self.fail("expected '::', ':=', or ':' after identifier"); return self.fail("expected '::', ':=', or ':' after identifier");
@@ -382,7 +383,7 @@ pub const Parser = struct {
} }); } });
} }
fn parseTypedBinding(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { fn parseTypedBinding(self: *Parser, name: []const u8, name_span: ast.Span, start_pos: u32) anyerror!*Node {
// After `name :` // After `name :`
// Parse type // Parse type
const type_node = try self.parseTypeExpr(); const type_node = try self.parseTypeExpr();
@@ -400,13 +401,13 @@ pub const Parser = struct {
self.advance(); self.advance();
const value = try self.parseExpr(); const value = try self.parseExpr();
try self.expectSemicolonAfter(value); try self.expectSemicolonAfter(value);
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = value } }); return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = value } });
} }
if (self.current.tag == .semicolon) { if (self.current.tag == .semicolon) {
// name : type; (default-initialized variable) // name : type; (default-initialized variable)
self.advance(); self.advance();
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .type_annotation = type_node, .value = null } }); return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = null } });
} }
if (self.current.tag == .hash_foreign) { if (self.current.tag == .hash_foreign) {
@@ -426,6 +427,7 @@ pub const Parser = struct {
try self.expect(.semicolon); try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .var_decl = .{ return try self.createNode(start_pos, .{ .var_decl = .{
.name = name, .name = name,
.name_span = name_span,
.type_annotation = type_node, .type_annotation = type_node,
.value = null, .value = null,
.is_foreign = true, .is_foreign = true,
@@ -1167,6 +1169,7 @@ pub const Parser = struct {
var param_types = std.ArrayList(*Node).empty; var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList([]const u8).empty; var param_names = std.ArrayList([]const u8).empty;
var param_name_spans = std.ArrayList(ast.Span).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) { while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) { if (param_types.items.len > 0) {
@@ -1178,6 +1181,7 @@ pub const Parser = struct {
return self.fail("expected parameter name in protocol method"); return self.fail("expected parameter name in protocol method");
} }
const pname = self.tokenSlice(self.current); const pname = self.tokenSlice(self.current);
try param_name_spans.append(self.allocator, .{ .start = self.current.loc.start, .end = self.current.loc.end });
self.advance(); self.advance();
try self.expect(.colon); try self.expect(.colon);
const ptype = try self.parseTypeExpr(); const ptype = try self.parseTypeExpr();
@@ -1205,6 +1209,7 @@ pub const Parser = struct {
.name = method_name, .name = method_name,
.params = try param_types.toOwnedSlice(self.allocator), .params = try param_types.toOwnedSlice(self.allocator),
.param_names = try param_names.toOwnedSlice(self.allocator), .param_names = try param_names.toOwnedSlice(self.allocator),
.param_name_spans = try param_name_spans.toOwnedSlice(self.allocator),
.return_type = return_type, .return_type = return_type,
.default_body = default_body, .default_body = default_body,
}); });
@@ -1418,6 +1423,7 @@ pub const Parser = struct {
.name = member_name, .name = member_name,
.params = &.{}, .params = &.{},
.param_names = &.{}, .param_names = &.{},
.param_name_spans = &.{},
.return_type = ret_type, .return_type = ret_type,
.is_static = true, .is_static = true,
.jni_descriptor_override = null, .jni_descriptor_override = null,
@@ -1431,6 +1437,7 @@ pub const Parser = struct {
var param_types = std.ArrayList(*Node).empty; var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList([]const u8).empty; var param_names = std.ArrayList([]const u8).empty;
var param_name_spans = std.ArrayList(ast.Span).empty;
while (self.current.tag != .r_paren and self.current.tag != .eof) { while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) { if (param_types.items.len > 0) {
try self.expect(.comma); try self.expect(.comma);
@@ -1440,6 +1447,7 @@ pub const Parser = struct {
return self.fail("expected parameter name in '#jni_class' method"); return self.fail("expected parameter name in '#jni_class' method");
} }
const pname = self.tokenSlice(self.current); const pname = self.tokenSlice(self.current);
try param_name_spans.append(self.allocator, .{ .start = self.current.loc.start, .end = self.current.loc.end });
self.advance(); self.advance();
try self.expect(.colon); try self.expect(.colon);
const ptype = try self.parseTypeExpr(); const ptype = try self.parseTypeExpr();
@@ -1521,6 +1529,7 @@ pub const Parser = struct {
.name = member_name, .name = member_name,
.params = try param_types.toOwnedSlice(self.allocator), .params = try param_types.toOwnedSlice(self.allocator),
.param_names = try param_names.toOwnedSlice(self.allocator), .param_names = try param_names.toOwnedSlice(self.allocator),
.param_name_spans = try param_name_spans.toOwnedSlice(self.allocator),
.return_type = return_type, .return_type = return_type,
.is_static = is_static, .is_static = is_static,
.jni_descriptor_override = desc_override, .jni_descriptor_override = desc_override,
@@ -1999,6 +2008,7 @@ pub const Parser = struct {
const saved_prev_end = self.prev_end; const saved_prev_end = self.prev_end;
const start = self.current.loc.start; const start = self.current.loc.start;
const name = self.tokenSlice(self.current); const name = self.tokenSlice(self.current);
const name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
if (self.current.tag == .colon_colon) { if (self.current.tag == .colon_colon) {
@@ -2009,11 +2019,11 @@ pub const Parser = struct {
self.advance(); self.advance();
const value = try self.parseExpr(); const value = try self.parseExpr();
try self.expectSemicolonAfter(value); try self.expectSemicolonAfter(value);
return try self.createNode(start, .{ .var_decl = .{ .name = name, .type_annotation = null, .value = value } }); return try self.createNode(start, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = null, .value = value } });
} }
if (self.current.tag == .colon) { if (self.current.tag == .colon) {
self.advance(); self.advance();
return self.parseTypedBinding(name, start); return self.parseTypedBinding(name, name_span, start);
} }
// Multi-target assignment: ident, expr, ... = expr, expr, ...; // Multi-target assignment: ident, expr, ... = expr, expr, ...;
@@ -2094,8 +2104,10 @@ pub const Parser = struct {
const start = self.current.loc.start; const start = self.current.loc.start;
self.advance(); self.advance();
var binding: ?[]const u8 = null; var binding: ?[]const u8 = null;
var binding_span: ?ast.Span = null;
if (self.current.tag == .identifier and self.peekNext() == .l_brace) { if (self.current.tag == .identifier and self.peekNext() == .l_brace) {
binding = self.tokenSlice(self.current); binding = self.tokenSlice(self.current);
binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
} }
const saved_onfail = self.in_onfail_body; const saved_onfail = self.in_onfail_body;
@@ -2108,7 +2120,7 @@ pub const Parser = struct {
try self.expect(.semicolon); try self.expect(.semicolon);
break :blk e; break :blk e;
}; };
return try self.createNode(start, .{ .onfail_stmt = .{ .binding = binding, .body = body } }); return try self.createNode(start, .{ .onfail_stmt = .{ .binding = binding, .binding_span = binding_span, .body = body } });
} }
// Break statement: break; // Break statement: break;
@@ -2539,8 +2551,10 @@ pub const Parser = struct {
// catch e EXPR — binding + bare-expression body // catch e EXPR — binding + bare-expression body
self.advance(); // consume 'catch' self.advance(); // consume 'catch'
var binding: ?[]const u8 = null; var binding: ?[]const u8 = null;
var binding_span: ?ast.Span = null;
if (self.current.tag == .identifier) { if (self.current.tag == .identifier) {
binding = self.tokenSlice(self.current); binding = self.tokenSlice(self.current);
binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
} }
var is_match_body = false; var is_match_body = false;
@@ -2559,6 +2573,7 @@ pub const Parser = struct {
expr = try self.createNode(expr.span.start, .{ .catch_expr = .{ expr = try self.createNode(expr.span.start, .{ .catch_expr = .{
.operand = expr, .operand = expr,
.binding = binding, .binding = binding,
.binding_span = binding_span,
.body = body, .body = body,
.is_match_body = is_match_body, .is_match_body = is_match_body,
} }); } });
@@ -2906,6 +2921,7 @@ pub const Parser = struct {
// Detect: identifier followed by := // Detect: identifier followed by :=
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) { if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current); const binding_name = self.tokenSlice(self.current);
const binding_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); // skip identifier self.advance(); // skip identifier
self.advance(); // skip := self.advance(); // skip :=
const source_expr = try self.parseExpr(); const source_expr = try self.parseExpr();
@@ -2925,6 +2941,7 @@ pub const Parser = struct {
.else_branch = else_branch, .else_branch = else_branch,
.is_inline = false, .is_inline = false,
.binding_name = binding_name, .binding_name = binding_name,
.binding_span = binding_span,
} }); } });
} }
@@ -3026,6 +3043,7 @@ pub const Parser = struct {
// Optional binding: while val := expr { ... } // Optional binding: while val := expr { ... }
if (self.current.tag == .identifier and self.peekNext() == .colon_equal) { if (self.current.tag == .identifier and self.peekNext() == .colon_equal) {
const binding_name = self.tokenSlice(self.current); const binding_name = self.tokenSlice(self.current);
const binding_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); // skip identifier self.advance(); // skip identifier
self.advance(); // skip := self.advance(); // skip :=
const source_expr = try self.parseExpr(); const source_expr = try self.parseExpr();
@@ -3034,6 +3052,7 @@ pub const Parser = struct {
.condition = source_expr, .condition = source_expr,
.body = body, .body = body,
.binding_name = binding_name, .binding_name = binding_name,
.binding_span = binding_span,
} }); } });
} }
@@ -3087,7 +3106,9 @@ pub const Parser = struct {
} }
var capture_name: []const u8 = ""; var capture_name: []const u8 = "";
var capture_span: ?ast.Span = null;
var index_name: ?[]const u8 = null; var index_name: ?[]const u8 = null;
var index_span: ?ast.Span = null;
var capture_by_ref = false; var capture_by_ref = false;
if (range_end != null) { if (range_end != null) {
@@ -3099,6 +3120,7 @@ pub const Parser = struct {
try self.expect(.l_paren); try self.expect(.l_paren);
if (self.current.tag != .identifier) return self.fail("expected cursor variable name"); if (self.current.tag != .identifier) return self.fail("expected cursor variable name");
capture_name = self.tokenSlice(self.current); capture_name = self.tokenSlice(self.current);
capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
try self.expect(.r_paren); try self.expect(.r_paren);
} }
@@ -3113,11 +3135,13 @@ pub const Parser = struct {
} }
if (self.current.tag != .identifier) return self.fail("expected capture variable name"); if (self.current.tag != .identifier) return self.fail("expected capture variable name");
capture_name = self.tokenSlice(self.current); capture_name = self.tokenSlice(self.current);
capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
if (self.current.tag == .comma) { if (self.current.tag == .comma) {
self.advance(); self.advance();
if (self.current.tag != .identifier) return self.fail("expected index variable name"); if (self.current.tag != .identifier) return self.fail("expected index variable name");
index_name = self.tokenSlice(self.current); index_name = self.tokenSlice(self.current);
index_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); self.advance();
} }
try self.expect(.r_paren); try self.expect(.r_paren);
@@ -3129,7 +3153,9 @@ pub const Parser = struct {
.iterable = iterable, .iterable = iterable,
.body = body, .body = body,
.capture_name = capture_name, .capture_name = capture_name,
.capture_span = capture_span,
.index_name = index_name, .index_name = index_name,
.index_span = index_span,
.range_end = range_end, .range_end = range_end,
.capture_by_ref = capture_by_ref, .capture_by_ref = capture_by_ref,
} }); } });
@@ -3154,9 +3180,11 @@ pub const Parser = struct {
// a capture is exactly `( <identifier> )`; anything else is the // a capture is exactly `( <identifier> )`; anything else is the
// arm body (an expression) and is left for the body parse below. // arm body (an expression) and is left for the body parse below.
var capture: ?[]const u8 = null; var capture: ?[]const u8 = null;
var capture_span: ?ast.Span = null;
if (self.current.tag == .l_paren and self.isLoneIdentParen()) { if (self.current.tag == .l_paren and self.isLoneIdentParen()) {
self.advance(); // '(' self.advance(); // '('
capture = self.tokenSlice(self.current); capture = self.tokenSlice(self.current);
capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end };
self.advance(); // ident self.advance(); // ident
try self.expect(.r_paren); try self.expect(.r_paren);
} }
@@ -3165,7 +3193,7 @@ pub const Parser = struct {
self.advance(); self.advance();
try self.expect(.semicolon); try self.expect(.semicolon);
const body = try self.createNode(arm_start, .{ .block = .{ .stmts = &.{} } }); const body = try self.createNode(arm_start, .{ .block = .{ .stmts = &.{} } });
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = true, .capture = capture }); try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = true, .capture = capture, .capture_span = capture_span });
} else if (self.current.tag == .fat_arrow) { } else if (self.current.tag == .fat_arrow) {
// Short form: (ident) => expr; // Short form: (ident) => expr;
self.advance(); self.advance();
@@ -3175,7 +3203,7 @@ pub const Parser = struct {
// `;` is an arm terminator, not a value-discard — match arms are // `;` is an arm terminator, not a value-discard — match arms are
// exempt from the block trailing-`;` rule). // exempt from the block trailing-`;` rule).
const body = try self.createNode(arm_start, .{ .block = .{ .stmts = try self.allocator.dupe(*Node, &.{expr}), .produces_value = true } }); const body = try self.createNode(arm_start, .{ .block = .{ .stmts = try self.allocator.dupe(*Node, &.{expr}), .produces_value = true } });
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture }); try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture, .capture_span = capture_span });
} else { } else {
const stmts_start = self.current.loc.start; const stmts_start = self.current.loc.start;
var stmts = std.ArrayList(*Node).empty; var stmts = std.ArrayList(*Node).empty;
@@ -3186,7 +3214,7 @@ pub const Parser = struct {
// yields its last statement's value — which, for a braced-block // yields its last statement's value — which, for a braced-block
// arm body, still respects that inner block's own flag. // arm body, still respects that inner block's own flag.
const body = try self.createNode(stmts_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator), .produces_value = true } }); const body = try self.createNode(stmts_start, .{ .block = .{ .stmts = try stmts.toOwnedSlice(self.allocator), .produces_value = true } });
try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture }); try arms.append(self.allocator, .{ .pattern = pattern, .body = body, .is_break = false, .capture = capture, .capture_span = capture_span });
} }
} }
// Optional else arm (default) // Optional else arm (default)
@@ -3539,16 +3567,19 @@ pub const Parser = struct {
self.advance(); self.advance();
// All targets must be plain identifiers // All targets must be plain identifiers
var names = std.ArrayList([]const u8).empty; var names = std.ArrayList([]const u8).empty;
var name_spans = std.ArrayList(ast.Span).empty;
for (targets.items) |target| { for (targets.items) |target| {
if (target.data != .identifier) { if (target.data != .identifier) {
return self.fail("destructuring targets must be identifiers"); return self.fail("destructuring targets must be identifiers");
} }
try names.append(self.allocator, target.data.identifier.name); try names.append(self.allocator, target.data.identifier.name);
try name_spans.append(self.allocator, target.span);
} }
const value = try self.parseExpr(); const value = try self.parseExpr();
try self.expectSemicolonAfter(value); try self.expectSemicolonAfter(value);
return try self.createNode(start, .{ .destructure_decl = .{ return try self.createNode(start, .{ .destructure_decl = .{
.names = try names.toOwnedSlice(self.allocator), .names = try names.toOwnedSlice(self.allocator),
.name_spans = try name_spans.toOwnedSlice(self.allocator),
.value = value, .value = value,
} }); } });
} }