Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern, foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl, findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→ dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed issues/0043-…-foreign-class-…→…-runtime-class-…. PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/ issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party). Suite green (644 corpus / 443 unit, 0 failed).
12 KiB
0089 — backtick raw-identifier escape + #import c extern-name exemption from the reserved-type-name rule
✅ RESOLVED (foundation step F0.6). Two mechanisms, per Agra's design ruling; the final shape is the universal raw identifier (attempt 4):
`nameis THE LITERAL identifiername, usable in EVERY position — value, declaration, AND type — meaning only "treat this token as a plain identifier, never the reserved keyword/type." The backtick is never part of the name's text.
Backtick raw identifier. The lexer recognises a leading backtick (
`i2) and emits an.identifiertoken whose span excludes the backtick, carrying aToken.is_rawflag ([src/lexer.zig], [src/token.zig]). The flag threads throughast.Identifier,ast.TypeExpr, and EVERY binding / capture / declaration node ([src/ast.zig]):VarDecl/ConstDecl/Param/FnDeclplusIfExpr/WhileExproptional bindings,ForExprcapture + index,MatchArmcapture,CatchExpr/OnFailStmttag bindings,DestructureDeclper-name, protocol-default / runtime-class method params, AND every type-introducing decl —StructDecl/EnumDecl/UnionDecl/ErrorSetDecl/ProtocolDecl/RuntimeClassDecl/UfcsAlias/NamespaceDecl/ImportDecl/CImportDecl/LibraryDecl.
- Value position. The parser skips
Type.fromNamefor a raw identifier in expression position ([src/parser.zig]parsePrimary), so`i2is a value identifier; a later bare reference resolves to the binding.- Type position.
parseTypeExprsets the raw flag on the type ATOM and lets it flow through the SAME continuations as a bare name (attempt 5), so a raw reference parameterizes a reserved-spelled template (`i2(i64)) and composes under the pointer / optional / slice wrappers;ParameterizedTypeExprcarriesis_rawandresolveParameterizedWithBindingsskips theVectorintrinsic when raw. Resolution skips the builtin classifier (TypeResolver.resolveNamed'sskip_builtin, threaded fromte.is_rawin [src/ir/lower.zig] and [src/ir/type_bridge.zig]) and looks up a`i2-declared type (struct / enum / union / alias), else a NORMAL "unknown type 'i2'" error (UnknownTypeChecker.reportIfUnknownTypeskips the builtin-name exemption when raw). A barei2in type position is still the builtin int. The SECOND (editor/LSP) classifier in [src/sema.zig] (Type.fromTypeExpr/resolveTypeNode/resolveTypeNameStr) honorsis_rawtoo, so a backtick reserved-name annotation resolves to the user type in hover/completion, not the builtin (no two-resolver divergence). The raw bit is carried STRUCTURALLY through every COMPOUND shape's inner-name metadata —PointerTypeInfo/OptionalTypeInfo/SliceTypeInfo/ManyPointerTypeInfo/ArrayTypeInfoeach store a REQUIREDis_raw([src/types.zig], no default, so a future construction site cannot drop it) that everyresolveTypeNameStrcall passes as itsskip_builtin— so*`i2,?`i2,[N]`i2,[]`i2,[*]`i2field-access / unwrap / index / deref in the editor index all reach the user type instead of reclassifying the inneri2to the builtin (the divergence the DIRECT-only attempt left for compound forms).- Declaration position. A bare reserved-name declaration of EVERY kind still errors (issue 0076 preserved); the backtick form is exempt. The check and the exemption are made structurally symmetric:
checkBindingName/checkDeclName([src/ir/semantic_diagnostics.zig]) takeis_rawas a REQUIRED argument and skip inside the check — no call site can validate a name without also honoring the exemption, which is what kept the two from desyncing across the earlier attempts. On the PARSER side the symmetry is enforced structurally for the bug-prone node:ConstDecl'sname_span+is_rawcarry NO default (attempt 5), so the compiler rejects any construction site — including the two struct-body const forms (untyped`i2 :: 5and typed`i2 : T : v) that previously dropped both — that omits them.FnDeclis built at every parser site throughparseFnDecl, whosename_is_rawis a REQUIRED parameter (the equivalent guarantee); the type decls likewise route through parse-functions takingname_is_raw.- Member-name positions are exempt (Agra ruling, attempt 7). A struct field name, a union tag name, and a protocol method-signature name accept a bare reserved spelling: these sit in a member slot and are reached via
obj.name/ dispatched by string, so they are never type-classified and never mis-lower — the binding-name walk'sstruct_decl/union_decl/enum_decl/protocol_declarms ([src/ir/semantic_diagnostics.zig]) check only the type name (and method params), not field / tag / variant / method-signature names. The backtick is optional there (obj.i2andobj.`i2resolve to the same member). This bare member-name exemption covers only the identifier-classified reserved spellings —i1..i64,u1..u64,bool,string,void,usize,isize,Any— which all lex as ordinary identifiers. The two keyword-classified spellings,f32andf64, are lexer keywords ([src/token.zig]), and a member-name slot requires an identifier token ([src/parser.zig]); a baref32/f64is therefore rejected at parse (expected field name in struct) even in a member position, and still needs the backtick there too —struct { `f32: i64; }/union { `f64: … }/protocol { `f32 :: () -> i64; }work as field / tag / method names. The exemption stops at member definitions: animplmethod is a real function reached through theimpl_block→fn_declarm, so a reserved-spelled impl method needs the backtick (`i2 :: (self)), no more exempt than a free function (cf.examples/1122). Pinned byexamples/0158-types-reserved-name-member-exempt.sx.
#import cextern-name exemption.c_import.zigsynthesizes externexterndecls withParam.is_raw = true(and the synthesizedFnDeclis_raw = true), so generated C names that collide with reserved type names (i1,i2) import unedited and a reserved-name extern fn is bare-callable.Bare-callable extern / backtick fn.
lowerCallrewrites a.type_exprcallee to an identifier when a function of RAW provenance of that name is in scope ([src/ir/lower.zig]) — scoped to the calleeFnDecl'sis_rawflag, so it only ever fires for a backtick /#import cextern fn (the decl check guarantees no bare reserved-name fn exists).i2(4)resolves to the function (TypeName(val)is not a cast).Regression tests.
examples/0151-types-backtick-raw-identifier.sx(every VALUE position),examples/0152-types-backtick-control-flow.sx(every control-flow / capture form),examples/0153-types-backtick-const-fn-decl.sx(backtick::const + fn decl, bare + backtick call),examples/0154-types-backtick-raw-type-reference.sx(raw in TYPE position — struct / enum / union / alias decl + reference; barei2still the int),examples/0155-types-backtick-typed-const-union-tag.sx(typed const + union tag),examples/0156-types-backtick-struct-const.sx(struct-body const, untyped + typed),examples/0157-types-backtick-parameterized-raw-type.sx(raw parameterized type + pointer/field wrappers),examples/0158-types-reserved-name-member-exempt.sx(bare reserved-name struct fields / union tag / protocol method signature — read & written bare and via backtick; impl method definition takes the backtick),examples/1054-errors-backtick-reserved-binding.sx(catch/onfailtag bindings),examples/1220-ffi-c-import-reserved-name-params.{sx,h,c}(extern param + fn-name exemption, bare-callable extern fn); negativesexamples/1119/1121/1123(bare reserved binding across forms),examples/1140-diagnostics-reserved-name-const-fn-decl.sx(bare const + fn decl),examples/1141-diagnostics-reserved-name-type-decl.sx(bare struct / enum / union / error / typed-const decl),examples/1142-diagnostics-reserved-name-struct-const.sx(bare struct-body const, caret on the name). Backtick lexer +resolveNamed(skip_builtin)unit tests insrc/lexer.zig/src/ir/type_resolver.test.zig; the editor/LSP raw-type resolution (the second classifier) is pinned insrc/sema.test.zig— the direct case plus raw provenance through every compound shape (*`i2field access,?`i2unwrap,[N]`i2index, parameterized`i2(i64)), each with a bare-spelling control that stays the builtin (fail-before verified).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 i1,
i2 (which collide with sx's signed-int type keywords i1..sN) produce:
error: 'i1' is a reserved type name and cannot be used as an identifier
error: 'i2' 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 (i2, 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 i1/i2.
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 extern) declarations are treated as RAW identifiers: extern 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:
`i2 := 2.5; // OK — identifier "i2", distinct from the i2 signed-int type i2 := 2.5; // ERROR — bare i2 is still the reserved type namePrefix form (single leading backtick on the identifier). The raw identifier's TEXT is
i2(the backtick is not part of the name). A barei2used as a TYPE remains the signed-int type.
Reproduction
sx-side (minimal):
#import "modules/std.sx";
main :: () {
`i2 := 2.5; // must compile: identifier i2 = 2.5
print("{}\n", `i2); // 2.5
}
Import-side: a #import c block over a header declaring int i1, i2; (or
stb_truetype.sx) must NOT emit the reserved-type-name error.