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.
6.6 KiB
0089 — backtick raw-identifier escape + #import c foreign-name exemption from the reserved-type-name rule
✅ RESOLVED (foundation step F0.6). Two mechanisms, per Agra's design ruling:
Backtick raw identifier. The lexer recognises a leading backtick (
`s2) and emits an.identifiertoken whose span excludes the backtick, carrying aToken.is_rawflag ([src/lexer.zig], [src/token.zig]). A raw identifier is NEVER type-classified — the parser skipsType.fromNamefor it in expression position ([src/parser.zig]parsePrimary), so it is always a value identifier. Theis_rawflag threads throughast.Identifierand EVERY binding/capture form ([src/ast.zig]):VarDecl/ParamplusIfExpr/WhileExproptional bindings,ForExprcapture + index,MatchArmcapture,CatchExpr/OnFailStmttag bindings,DestructureDeclper-name, and the protocol-default-body / foreign-class method param lists.UnknownTypeCheckerskips the reserved-name check at each of those arms when raw ([src/ir/semantic_diagnostics.zig]). The backtick works in every identifier position (local, global, param, field, function name, struct member, later reference, and all the control-flow/capture/binding forms).The
::DECLARATION forms are binding sites too and are equally covered (F0.6 attempt-3): a bare reserved-name constant (s2 :: 5), function (s2 :: (…) {…}, incl. struct/impl methods), or type declaration (struct/enum/union/error/alias/protocol/foreign-class/ufcs/namespace) is rejected, exactly likes2 := ….ConstDecl/FnDeclcarryis_raw+name_spanthreaded from the parser (parseConstBinding/parseFnDecl), so the backtick form (`s2 :: …) is exempt; the compiler's own builtin definition (string :: []u8 #builtin) is the sole non-backtick exemption (a#builtinconstant defines the reserved type). This closed the attempt-2 hole where a bares2 :: (…) {…}compiled silently and the call rewrite made it callable.
#import cforeign-name exemption.c_import.zigsynthesizes foreign#foreigndecls withParam.is_raw = true, so generated C param names that collide with reserved type names (s1,s2) import unedited.Boundary rules. A raw identifier is a value name and is NEVER a type: using one in type position (
x :s2 = 1) is a clean parse error ([src/parser.zig]parseTypeExpratom). A reserved-spelled FUNCTION (backtick-declared or#import cforeign) is bare-callable:lowerCallrewrites a.type_exprcallee to an identifier when a function **of RAW provenance** of that name is in scope ([src/ir/lower.zig]) — the rewrite is scoped to the calleeFnDecl'sis_rawflag (F0.6 attempt-3), so it only ever fires for a backtick /#import cforeign fn (the decl check guarantees no bare reserved-name fn exists), sos2(4)resolves to the function (TypeName(val)is not a cast). A later BARE reference in value position resolves to the binding; a bares2` in type position is still the type.A bare reserved-name binding in sx still errors (issue 0076 preserved): the
is_raw-gated skip only fires for backtick / foreign names. Regression tests:examples/0151-types-backtick-raw-identifier.sx(backtick, decl positions),examples/0152-types-backtick-control-flow.sx(every control-flow/capture form
- bare ref/call/member access),
examples/1054-errors-backtick-reserved-binding.sx(catch/onfailtag bindings),examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}(foreign param + function-name exemption, bare-callable foreign fn),examples/1139-diagnostics-backtick-raw-not-a-type.sx(negative — raw in type position),examples/1119/1121/1123(negative — bare reserved binding still rejected across all forms),examples/0153-types-backtick-const-fn-decl.sx(positive — backtick::const + function decl, bare + backtick call), andexamples/1140-diagnostics-reserved-name-const-fn-decl.sx(negative — bare::const + function decl rejected). Backtick lexer unit tests insrc/lexer.zig.The original report is preserved below.
Symptom
Importing non-sx source whose names collide with sx reserved type names is
rejected. library/modules/stb_truetype.sx is a #import c { ... } block over a
vendored C header (vendors/stb_truetype/stb_truetype.h); C identifiers s1,
s2 (which collide with sx's signed-int type keywords s1..sN) produce:
error: 's1' is a reserved type name and cannot be used as an identifier
error: 's2' is a reserved type name and cannot be used as an identifier
The user cannot hand-edit these — they are generated from the vendored C header. Separately, sx-authored code has NO way to deliberately use a reserved-name-spelled identifier even when it wants to.
Root cause
The parser classifies any reserved-type-name spelling (s2, u8, f64, …) as a
.type_expr via name_class.Type.fromName, never as an .identifier. The F0.1 /
issue-0076 fix added UnknownTypeChecker.checkBindingName
(src/ir/semantic_diagnostics.zig) to reject a value binding / param spelled as
a reserved type name (the .type_expr-vs-.identifier mismatch otherwise breaks
address-of / autoref lowering). F0.1 deliberately extended this check to imported
declarations — which is what now fires on the C-imported s1/s2.
Desired behaviour (Agra ruling)
External / imported source does NOT need to conform to sx naming standards. Two mechanisms:
-
Auto-exempt imports.
#import c(and other foreign) declarations are treated as RAW identifiers: foreign names are never type-classified and never reserved-checked, so generated bindings "just work" with zero user edits. -
Backtick raw-identifier for sx code. A leading backtick makes the following identifier raw — an identifier that is NEVER type-classified, so it bypasses the reserved-name rule:
`s2 := 2.5; // OK — identifier "s2", distinct from the s2 signed-int type s2 := 2.5; // ERROR — bare s2 is still the reserved type namePrefix form (single leading backtick on the identifier). The raw identifier's TEXT is
s2(the backtick is not part of the name). A bares2used as a TYPE remains the signed-int type.
Reproduction
sx-side (minimal):
#import "modules/std.sx";
main :: () {
`s2 := 2.5; // must compile: identifier s2 = 2.5
print("{}\n", `s2); // 2.5
}
Import-side: a #import c block over a header declaring int s1, s2; (or
stb_truetype.sx) must NOT emit the reserved-type-name error.