From 6433eb6155ca64ae8c1aea9077c84548505bda3b Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 22:06:56 +0300 Subject: [PATCH] fix(diagnostics): point reserved-type-name binding errors at the binding (issue 0076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...-diagnostics-reserved-name-method-param.sx | 30 +++++++++++ .../0125-types-type-named-var-rejected.stderr | 2 +- ...cs-reserved-type-name-as-identifier.stderr | 6 +-- ...nostics-imported-reserved-type-name.stderr | 2 +- ...gnostics-reserved-name-control-flow.stderr | 28 ++++------ ...agnostics-reserved-name-impl-method.stderr | 2 +- ...gnostics-reserved-name-catch-onfail.stderr | 8 +-- ...stics-imported-reserved-destructure.stderr | 2 +- ...iagnostics-reserved-name-method-param.exit | 1 + ...gnostics-reserved-name-method-param.stderr | 11 ++++ ...gnostics-reserved-name-method-param.stdout | 1 + ...076-stack-struct-addrof-passed-by-value.md | 14 +++++ src/ast.zig | 11 ++++ src/ir/semantic_diagnostics.zig | 24 ++++----- src/parser.zig | 53 +++++++++++++++---- 15 files changed, 144 insertions(+), 51 deletions(-) create mode 100644 examples/1125-diagnostics-reserved-name-method-param.sx create mode 100644 examples/expected/1125-diagnostics-reserved-name-method-param.exit create mode 100644 examples/expected/1125-diagnostics-reserved-name-method-param.stderr create mode 100644 examples/expected/1125-diagnostics-reserved-name-method-param.stdout diff --git a/examples/1125-diagnostics-reserved-name-method-param.sx b/examples/1125-diagnostics-reserved-name-method-param.sx new file mode 100644 index 0000000..12d383a --- /dev/null +++ b/examples/1125-diagnostics-reserved-name-method-param.sx @@ -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; +} diff --git a/examples/expected/0125-types-type-named-var-rejected.stderr b/examples/expected/0125-types-type-named-var-rejected.stderr index d2798b1..6b02235 100644 --- a/examples/expected/0125-types-type-named-var-rejected.stderr +++ b/examples/expected/0125-types-type-named-var-rejected.stderr @@ -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 | 10 | s2 := 42; - | ^^^^^^^^^ + | ^^ diff --git a/examples/expected/1119-diagnostics-reserved-type-name-as-identifier.stderr b/examples/expected/1119-diagnostics-reserved-type-name-as-identifier.stderr index 4b5e737..b15964c 100644 --- a/examples/expected/1119-diagnostics-reserved-type-name-as-identifier.stderr +++ b/examples/expected/1119-diagnostics-reserved-type-name-as-identifier.stderr @@ -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 | 12 | s64 : s32 = 3; - | ^^^^^^^^^^^^^^ + | ^^^ 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 | 13 | bool : bool = true; - | ^^^^^^^^^^^^^^^^^^^ + | ^^^^ 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 | 14 | string := "x"; - | ^^^^^^^^^^^^^^ + | ^^^^^^ diff --git a/examples/expected/1120-diagnostics-imported-reserved-type-name.stderr b/examples/expected/1120-diagnostics-imported-reserved-type-name.stderr index f5469a0..9b7397d 100644 --- a/examples/expected/1120-diagnostics-imported-reserved-type-name.stderr +++ b/examples/expected/1120-diagnostics-imported-reserved-type-name.stderr @@ -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 | 11 | s2 := Box.{ total = 0, count = 0 }; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^ diff --git a/examples/expected/1121-diagnostics-reserved-name-control-flow.stderr b/examples/expected/1121-diagnostics-reserved-name-control-flow.stderr index 1ecdc5c..4032ee2 100644 --- a/examples/expected/1121-diagnostics-reserved-name-control-flow.stderr +++ b/examples/expected/1121-diagnostics-reserved-name-control-flow.stderr @@ -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 | 18 | s2, rest := pair(); // destructure name - | ^^^^^^^^^^^^^^^^^^^ + | ^^ 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 - | ^^^^^^^^^^^^^^^^^^^^ + | ^^ 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 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^^ 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 - | ^^^^^^^^^^^^^^^^^^ + | ^^^^ 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 - | ^^^^^^^^^^^^^^^^^^^^ + | ^^^ 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 } - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -27 | case .none: { 0 } - | ^^^^^^^^^^^^^^^^^^^^^^^^^ -28 | }; - | ^^^^^ + | ^^^^^^ diff --git a/examples/expected/1122-diagnostics-reserved-name-impl-method.stderr b/examples/expected/1122-diagnostics-reserved-name-impl-method.stderr index cc1cf72..393565c 100644 --- a/examples/expected/1122-diagnostics-reserved-name-impl-method.stderr +++ b/examples/expected/1122-diagnostics-reserved-name-impl-method.stderr @@ -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 | 20 | s2 := Box.{ total = 1 }; - | ^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^ diff --git a/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr b/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr index e6865b8..f63404e 100644 --- a/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr +++ b/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr @@ -1,11 +1,11 @@ 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 - | ^^^^^^^^^^^^^^ + | ^^^ 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 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^ diff --git a/examples/expected/1124-diagnostics-imported-reserved-destructure.stderr b/examples/expected/1124-diagnostics-imported-reserved-destructure.stderr index 2de26c2..668ff2c 100644 --- a/examples/expected/1124-diagnostics-imported-reserved-destructure.stderr +++ b/examples/expected/1124-diagnostics-imported-reserved-destructure.stderr @@ -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 | 6 | s2, rest := pair(); // destructure name in an IMPORTED module - | ^^^^^^^^^^^^^^^^^^^ + | ^^ diff --git a/examples/expected/1125-diagnostics-reserved-name-method-param.exit b/examples/expected/1125-diagnostics-reserved-name-method-param.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1125-diagnostics-reserved-name-method-param.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1125-diagnostics-reserved-name-method-param.stderr b/examples/expected/1125-diagnostics-reserved-name-method-param.stderr new file mode 100644 index 0000000..d3b4b67 --- /dev/null +++ b/examples/expected/1125-diagnostics-reserved-name-method-param.stderr @@ -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) { + | ^^^ diff --git a/examples/expected/1125-diagnostics-reserved-name-method-param.stdout b/examples/expected/1125-diagnostics-reserved-name-method-param.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1125-diagnostics-reserved-name-method-param.stdout @@ -0,0 +1 @@ + diff --git a/issues/0076-stack-struct-addrof-passed-by-value.md b/issues/0076-stack-struct-addrof-passed-by-value.md index 8ed80a1..fe2013e 100644 --- a/issues/0076-stack-struct-addrof-passed-by-value.md +++ b/issues/0076-stack-struct-addrof-passed-by-value.md @@ -34,6 +34,17 @@ > 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`). > +> **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:** > - `examples/0125-types-type-named-var-rejected.sx` — `:=` form (`s2`) rejected. > - `examples/1119-diagnostics-reserved-type-name-as-identifier.sx` — parameter @@ -47,6 +58,9 @@ > `onfail` error-tag bindings. > - `examples/1124-diagnostics-imported-reserved-destructure.sx` — destructure > 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` > streaming with non-reserved names (`hasher`, `ctx`) accumulates correctly via > both `update(@h, …)` and `h.update(…)`. diff --git a/src/ast.zig b/src/ast.zig index 7ce14fa..fa085e5 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -272,6 +272,7 @@ pub const IfExpr = struct { is_inline: bool, // true for `if cond then a else b` is_comptime: bool = false, // true for `inline if` — compile-time branch elimination 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 { @@ -285,6 +286,7 @@ pub const MatchArm = struct { body: *Node, is_break: bool, 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 { @@ -295,6 +297,7 @@ pub const ConstDecl = struct { pub const VarDecl = struct { name: []const u8, + name_span: Span, type_annotation: ?*Node, value: ?*Node, is_foreign: bool = false, @@ -329,6 +332,7 @@ pub const MultiAssign = struct { pub const DestructureDecl = struct { names: []const []const u8, + name_spans: []const Span, // one per entry in `names`, same order value: *Node, }; @@ -449,6 +453,7 @@ pub const TryExpr = struct { pub const CatchExpr = struct { operand: *Node, binding: ?[]const u8 = null, + binding_span: ?Span = null, // span of `binding` (set iff `binding` is) body: *Node, is_match_body: bool = false, }; @@ -458,6 +463,7 @@ pub const CatchExpr = struct { /// a bare expression (`onfail EXPR;`). pub const OnFailStmt = struct { binding: ?[]const u8 = null, + binding_span: ?Span = null, // span of `binding` (set iff `binding` is) body: *Node, }; @@ -551,13 +557,16 @@ pub const WhileExpr = struct { condition: *Node, body: *Node, 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 { iterable: *Node, body: *Node, 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_span: ?Span = null, // span of `index_name` (set iff `index_name` is) /// Range form `for start..end (i) { }`: `iterable` is the start, `range_end` /// the (exclusive) end. Null for the iterate-a-collection form /// (`for coll : (x) { }`). For the range form `capture_name` is the cursor @@ -645,6 +654,7 @@ pub const ProtocolMethodDecl = struct { name: []const u8, params: []const *Node, // type_expr nodes for parameter types (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 default_body: ?*Node, // null = required method, non-null = default implementation }; @@ -670,6 +680,7 @@ pub const ForeignMethodDecl = struct { name: []const u8, params: []const *Node, // type_expr nodes — first is `*Self` for instance methods param_names: []const []const u8, + param_name_spans: []const Span = &.{}, // one per `param_names` entry; empty for synthesized methods return_type: ?*Node, // null = void is_static: bool = false, // true for `static name :: ...` jni_descriptor_override: ?[]const u8 = null, // `#jni_method_descriptor("(Sig)Ret")` — JNI runtime only diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index 659bb26..33e297d 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -117,11 +117,11 @@ pub const UnknownTypeChecker = struct { switch (node.data) { // ── Binding-introducing nodes: check the name(s), then recurse. ── .var_decl => |vd| { - self.checkBindingName(vd.name, node.span); + self.checkBindingName(vd.name, vd.name_span); if (vd.value) |v| self.checkBindingNames(v); }, .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); }, .fn_decl => |fd| { @@ -137,19 +137,19 @@ pub const UnknownTypeChecker = struct { if (p.default_expr) |de| self.checkBindingNames(de); }, .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.then_branch); if (ie.else_branch) |e| self.checkBindingNames(e); }, .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.body); }, .for_expr => |fe| { - if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, node.span); - if (fe.index_name) |idx| self.checkBindingName(idx, node.span); + if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span); + if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span); self.checkBindingNames(fe.iterable); if (fe.range_end) |re| self.checkBindingNames(re); self.checkBindingNames(fe.body); @@ -157,23 +157,23 @@ pub const UnknownTypeChecker = struct { .match_expr => |me| { self.checkBindingNames(me.subject); 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); self.checkBindingNames(arm.body); } }, .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); self.checkBindingNames(arm.body); }, .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.body); }, .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); }, // 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), .protocol_decl => |pd| for (pd.methods) |m| { 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); } }, .foreign_class_decl => |fcd| for (fcd.members) |member| switch (member) { .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); }, .field, .extends, .implements => {}, diff --git a/src/parser.zig b/src/parser.zig index 13980c1..7113e13 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -145,6 +145,7 @@ pub const Parser = struct { return self.fail("expected identifier at top level"); } const name = self.tokenSlice(self.current); + const name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); // IDENT :: ... @@ -157,7 +158,7 @@ pub const Parser = struct { // IDENT : type = value; (typed variable) if (self.current.tag == .colon) { self.advance(); - return self.parseTypedBinding(name, start); + return self.parseTypedBinding(name, name_span, start); } // IDENT := value; (variable) @@ -165,7 +166,7 @@ pub const Parser = struct { self.advance(); const value = try self.parseExpr(); 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"); @@ -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 :` // Parse type const type_node = try self.parseTypeExpr(); @@ -400,13 +401,13 @@ pub const Parser = struct { self.advance(); const value = try self.parseExpr(); 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) { // name : type; (default-initialized variable) 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) { @@ -426,6 +427,7 @@ pub const Parser = struct { try self.expect(.semicolon); return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, + .name_span = name_span, .type_annotation = type_node, .value = null, .is_foreign = true, @@ -1167,6 +1169,7 @@ pub const Parser = struct { var param_types = std.ArrayList(*Node).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) { if (param_types.items.len > 0) { @@ -1178,6 +1181,7 @@ pub const Parser = struct { return self.fail("expected parameter name in protocol method"); } 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(); try self.expect(.colon); const ptype = try self.parseTypeExpr(); @@ -1205,6 +1209,7 @@ pub const Parser = struct { .name = method_name, .params = try param_types.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, .default_body = default_body, }); @@ -1418,6 +1423,7 @@ pub const Parser = struct { .name = member_name, .params = &.{}, .param_names = &.{}, + .param_name_spans = &.{}, .return_type = ret_type, .is_static = true, .jni_descriptor_override = null, @@ -1431,6 +1437,7 @@ pub const Parser = struct { var param_types = std.ArrayList(*Node).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) { if (param_types.items.len > 0) { try self.expect(.comma); @@ -1440,6 +1447,7 @@ pub const Parser = struct { return self.fail("expected parameter name in '#jni_class' method"); } 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(); try self.expect(.colon); const ptype = try self.parseTypeExpr(); @@ -1521,6 +1529,7 @@ pub const Parser = struct { .name = member_name, .params = try param_types.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, .is_static = is_static, .jni_descriptor_override = desc_override, @@ -1999,6 +2008,7 @@ pub const Parser = struct { const saved_prev_end = self.prev_end; const start = self.current.loc.start; const name = self.tokenSlice(self.current); + const name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); if (self.current.tag == .colon_colon) { @@ -2009,11 +2019,11 @@ pub const Parser = struct { self.advance(); const value = try self.parseExpr(); 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) { self.advance(); - return self.parseTypedBinding(name, start); + return self.parseTypedBinding(name, name_span, start); } // Multi-target assignment: ident, expr, ... = expr, expr, ...; @@ -2094,8 +2104,10 @@ pub const Parser = struct { const start = self.current.loc.start; self.advance(); var binding: ?[]const u8 = null; + var binding_span: ?ast.Span = null; if (self.current.tag == .identifier and self.peekNext() == .l_brace) { binding = self.tokenSlice(self.current); + binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); } const saved_onfail = self.in_onfail_body; @@ -2108,7 +2120,7 @@ pub const Parser = struct { try self.expect(.semicolon); 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; @@ -2539,8 +2551,10 @@ pub const Parser = struct { // catch e EXPR — binding + bare-expression body self.advance(); // consume 'catch' var binding: ?[]const u8 = null; + var binding_span: ?ast.Span = null; if (self.current.tag == .identifier) { binding = self.tokenSlice(self.current); + binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); } var is_match_body = false; @@ -2559,6 +2573,7 @@ pub const Parser = struct { expr = try self.createNode(expr.span.start, .{ .catch_expr = .{ .operand = expr, .binding = binding, + .binding_span = binding_span, .body = body, .is_match_body = is_match_body, } }); @@ -2906,6 +2921,7 @@ pub const Parser = struct { // Detect: identifier followed by := if (self.current.tag == .identifier and self.peekNext() == .colon_equal) { 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 := const source_expr = try self.parseExpr(); @@ -2925,6 +2941,7 @@ pub const Parser = struct { .else_branch = else_branch, .is_inline = false, .binding_name = binding_name, + .binding_span = binding_span, } }); } @@ -3026,6 +3043,7 @@ pub const Parser = struct { // Optional binding: while val := expr { ... } if (self.current.tag == .identifier and self.peekNext() == .colon_equal) { 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 := const source_expr = try self.parseExpr(); @@ -3034,6 +3052,7 @@ pub const Parser = struct { .condition = source_expr, .body = body, .binding_name = binding_name, + .binding_span = binding_span, } }); } @@ -3087,7 +3106,9 @@ pub const Parser = struct { } var capture_name: []const u8 = ""; + var capture_span: ?ast.Span = null; var index_name: ?[]const u8 = null; + var index_span: ?ast.Span = null; var capture_by_ref = false; if (range_end != null) { @@ -3099,6 +3120,7 @@ pub const Parser = struct { try self.expect(.l_paren); if (self.current.tag != .identifier) return self.fail("expected cursor variable name"); capture_name = self.tokenSlice(self.current); + capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); 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"); capture_name = self.tokenSlice(self.current); + capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); if (self.current.tag == .comma) { self.advance(); if (self.current.tag != .identifier) return self.fail("expected index variable name"); index_name = self.tokenSlice(self.current); + index_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); } try self.expect(.r_paren); @@ -3129,7 +3153,9 @@ pub const Parser = struct { .iterable = iterable, .body = body, .capture_name = capture_name, + .capture_span = capture_span, .index_name = index_name, + .index_span = index_span, .range_end = range_end, .capture_by_ref = capture_by_ref, } }); @@ -3154,9 +3180,11 @@ pub const Parser = struct { // a capture is exactly `( )`; anything else is the // arm body (an expression) and is left for the body parse below. var capture: ?[]const u8 = null; + var capture_span: ?ast.Span = null; if (self.current.tag == .l_paren and self.isLoneIdentParen()) { self.advance(); // '(' capture = self.tokenSlice(self.current); + capture_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; self.advance(); // ident try self.expect(.r_paren); } @@ -3165,7 +3193,7 @@ pub const Parser = struct { self.advance(); try self.expect(.semicolon); 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) { // Short form: (ident) => expr; self.advance(); @@ -3175,7 +3203,7 @@ pub const Parser = struct { // `;` is an arm terminator, not a value-discard — match arms are // exempt from the block trailing-`;` rule). 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 { const stmts_start = self.current.loc.start; 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 // 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 } }); - 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) @@ -3539,16 +3567,19 @@ pub const Parser = struct { self.advance(); // All targets must be plain identifiers var names = std.ArrayList([]const u8).empty; + var name_spans = std.ArrayList(ast.Span).empty; for (targets.items) |target| { if (target.data != .identifier) { return self.fail("destructuring targets must be identifiers"); } try names.append(self.allocator, target.data.identifier.name); + try name_spans.append(self.allocator, target.span); } const value = try self.parseExpr(); try self.expectSemicolonAfter(value); return try self.createNode(start, .{ .destructure_decl = .{ .names = try names.toOwnedSlice(self.allocator), + .name_spans = try name_spans.toOwnedSlice(self.allocator), .value = value, } }); }