feat(lang): reserved-name check covers :: const/fn/type decls + scope call rewrite to raw provenance [F0.6]
A bare reserved-type-name `::` declaration was silently accepted, and the
attempt-2 lowerCall rewrite then made a bare `s2 :: (…) {…}` function callable —
bypassing the backtick rule for handwritten sx. The reserved-name binding check
covered `:=` / typed-local / param / captures but NOT the `::` declaration form.
- ast: `ConstDecl`/`FnDecl` carry `is_raw` + `name_span` threaded from the parser
(parseConstBinding / parseFnDecl, all call sites incl. struct/impl methods).
- semantic_diagnostics: reject a bare reserved spelling at EVERY declaration-name
site — const, function (incl. struct/impl methods), struct/enum/union/error-set,
protocol, foreign-class, ufcs alias, namespaced/library/c-import name. Backtick
(`is_raw`) and the compiler's `#builtin` definition (`string :: []u8 #builtin`)
are the only exemptions; a value whose node is itself a named decl defers to
that node's own check.
- c_import: synthesized foreign fn_decls are `is_raw = true`, so a C function
whose own name collides with a reserved spelling (`int s2(int);`) imports and
bare-calls unedited.
- lower: scope the `.type_expr`→`.identifier` call rewrite to a callee FnDecl of
RAW provenance (`is_raw`) — only a backtick / `#import c` foreign fn can carry a
reserved-name spelling, so a non-raw match never gets rewritten.
- examples: 0153 (positive — backtick `::` const + fn, bare + tick call), 1140
(negative — bare `::` const + fn rejected).
- docs: specs.md + readme.md state the backtick is required at every binding site
including `::` const / function / type declarations; issue 0089 banner updated.
This commit is contained in:
@@ -127,6 +127,11 @@ pub const UnknownTypeChecker = struct {
|
||||
self.checkBindingNames(dd.value);
|
||||
},
|
||||
.fn_decl => |fd| {
|
||||
// A function NAME is a binding site too: a bare reserved-name
|
||||
// `s2 :: (…) {…}` (free fn or struct/impl method) is rejected,
|
||||
// exactly like `s2 := …`. Backtick (`` `s2 :: … ``) and
|
||||
// `#import c` foreign fns set `is_raw` and are exempt (0089).
|
||||
if (!fd.is_raw) self.checkBindingName(fd.name, fd.name_span);
|
||||
self.checkParamNames(fd.params);
|
||||
self.checkBindingNames(fd.body);
|
||||
},
|
||||
@@ -197,33 +202,57 @@ pub const UnknownTypeChecker = struct {
|
||||
// `#objc_class` bodied method is lowered (M1.2), so its reserved
|
||||
// param/local names mis-lower the same as any other.
|
||||
.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, m.param_name_spans, 0..) |pn, sp, i| {
|
||||
if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue;
|
||||
self.checkBindingName(pn, sp);
|
||||
.protocol_decl => |pd| {
|
||||
self.checkDeclName(node, pd.name);
|
||||
for (pd.methods) |m| {
|
||||
if (m.default_body) |body| {
|
||||
for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
|
||||
if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue;
|
||||
self.checkBindingName(pn, sp);
|
||||
}
|
||||
self.checkBindingNames(body);
|
||||
}
|
||||
self.checkBindingNames(body);
|
||||
}
|
||||
},
|
||||
.foreign_class_decl => |fcd| for (fcd.members) |member| switch (member) {
|
||||
.method => |m| if (m.body) |body| {
|
||||
for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
|
||||
if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue;
|
||||
self.checkBindingName(pn, sp);
|
||||
}
|
||||
self.checkBindingNames(body);
|
||||
},
|
||||
.field, .extends, .implements => {},
|
||||
.foreign_class_decl => |fcd| {
|
||||
// The sx-side alias (left of `::`) is a user-chosen name, so a
|
||||
// reserved spelling is rejected like any other type decl (0089).
|
||||
self.checkDeclName(node, fcd.name);
|
||||
for (fcd.members) |member| switch (member) {
|
||||
.method => |m| if (m.body) |body| {
|
||||
for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
|
||||
if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue;
|
||||
self.checkBindingName(pn, sp);
|
||||
}
|
||||
self.checkBindingNames(body);
|
||||
},
|
||||
.field, .extends, .implements => {},
|
||||
};
|
||||
},
|
||||
// ── Container / control-flow / expression nodes: recurse children
|
||||
// so a binding nested anywhere below is still reached. ──
|
||||
// A namespaced import (`mod :: #import "..."`) is wrapped here, its
|
||||
// module decls held inline; descend so an imported module's
|
||||
// reserved-name binding is rejected too (issue 0077).
|
||||
.namespace_decl => |nd| for (nd.decls) |d| self.checkBindingNames(d),
|
||||
.const_decl => |cd| self.checkBindingNames(cd.value),
|
||||
.namespace_decl => |nd| {
|
||||
self.checkDeclName(node, nd.name);
|
||||
for (nd.decls) |d| self.checkBindingNames(d);
|
||||
},
|
||||
.const_decl => |cd| {
|
||||
// A const BINDS `cd.name`. Reject a bare reserved spelling
|
||||
// unless it is backtick-raw (`cd.is_raw`) or the compiler's
|
||||
// blessed builtin definition (`string :: []u8 #builtin`, value
|
||||
// `.builtin_expr`). When the value node is itself a named decl
|
||||
// (struct/enum/union/error/fn), that node carries & checks its
|
||||
// own name on recursion — don't double-check it here (0089).
|
||||
switch (cd.value.data) {
|
||||
.builtin_expr, .struct_decl, .enum_decl, .union_decl, .error_set_decl, .fn_decl => {},
|
||||
else => if (!cd.is_raw) self.checkBindingName(cd.name, cd.name_span),
|
||||
}
|
||||
self.checkBindingNames(cd.value);
|
||||
},
|
||||
.struct_decl => |sd| {
|
||||
self.checkDeclName(node, sd.name);
|
||||
for (sd.methods) |m| self.checkBindingNames(m);
|
||||
for (sd.constants) |c| self.checkBindingNames(c);
|
||||
for (sd.field_defaults) |fdef| if (fdef) |d| self.checkBindingNames(d);
|
||||
@@ -286,12 +315,21 @@ pub const UnknownTypeChecker = struct {
|
||||
.comptime_expr => |ce| self.checkBindingNames(ce.expr),
|
||||
.insert_expr => |ins| self.checkBindingNames(ins.expr),
|
||||
.spread_expr => |se| self.checkBindingNames(se.operand),
|
||||
// ── Named type / alias / import declarations: a bare reserved
|
||||
// spelling as the declared name is rejected (issue 0089). These
|
||||
// have no nested binding sites, so only the name is checked. A
|
||||
// flat `#import`/`#import c` (name == null) binds nothing. ──
|
||||
.enum_decl => |ed| self.checkDeclName(node, ed.name),
|
||||
.union_decl => |ud| self.checkDeclName(node, ud.name),
|
||||
.error_set_decl => |esd| self.checkDeclName(node, esd.name),
|
||||
.ufcs_alias => |ua| self.checkDeclName(node, ua.name),
|
||||
.library_decl => |ld| self.checkDeclName(node, ld.name),
|
||||
.import_decl => |imp| if (imp.name) |n| self.checkDeclName(node, n),
|
||||
.c_import_decl => |cid| if (cid.name) |n| self.checkDeclName(node, n),
|
||||
// ── Leaves & pure type-expression nodes: no binding sites below. ──
|
||||
// Type-expression subtrees carry only type names (no value
|
||||
// bindings); enum / union / error-set declarations carry only field
|
||||
// types + comptime constants. Listing each tag explicitly (rather
|
||||
// than an `else`) is what forces a future binding-bearing node to be
|
||||
// reconsidered here.
|
||||
// bindings). Listing each tag explicitly (rather than an `else`) is
|
||||
// what forces a future binding-bearing node to be reconsidered here.
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.bool_literal,
|
||||
@@ -299,10 +337,6 @@ pub const UnknownTypeChecker = struct {
|
||||
.identifier,
|
||||
.enum_literal,
|
||||
.type_expr,
|
||||
.enum_decl,
|
||||
.union_decl,
|
||||
.error_set_decl,
|
||||
.import_decl,
|
||||
.array_type_expr,
|
||||
.slice_type_expr,
|
||||
.parameterized_type_expr,
|
||||
@@ -321,13 +355,10 @@ pub const UnknownTypeChecker = struct {
|
||||
.builtin_expr,
|
||||
.compiler_expr,
|
||||
.foreign_expr,
|
||||
.library_decl,
|
||||
.framework_decl,
|
||||
.function_type_expr,
|
||||
.closure_type_expr,
|
||||
.tuple_type_expr,
|
||||
.ufcs_alias,
|
||||
.c_import_decl,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
@@ -762,6 +793,18 @@ pub const UnknownTypeChecker = struct {
|
||||
if (isReservedTypeName(name))
|
||||
self.diagnostics.addFmt(.err, span, "'{s}' is a reserved type name and cannot be used as an identifier", .{name});
|
||||
}
|
||||
|
||||
/// Reserved-name check for a `::` declaration whose own name binds an
|
||||
/// identifier but carries no dedicated `name_span` field — struct / enum /
|
||||
/// union / error-set / protocol / foreign-class type decls, ufcs aliases,
|
||||
/// and namespaced imports (issue 0089). Each such node begins at its name
|
||||
/// token, so the name's length isolates the caret onto the name. A
|
||||
/// backtick raw / `#import c` foreign name never reaches here (those forms
|
||||
/// are exempt at their own decl path).
|
||||
fn checkDeclName(self: UnknownTypeChecker, node: *const Node, name: []const u8) void {
|
||||
const span = ast.Span{ .start = node.span.start, .end = node.span.start + @as(u32, @intCast(name.len)) };
|
||||
self.checkBindingName(name, span);
|
||||
}
|
||||
};
|
||||
|
||||
/// A binding name collides with a reserved/builtin type name exactly when the
|
||||
|
||||
Reference in New Issue
Block a user