feat(ffi-linkage): lower extern data globals (Phase 1.2d) — Phase 1 complete
Parser: a 'kw_extern' branch in the var-decl-with-type-annotation path (beside #foreign) parses 'name : type extern [LIB] ["csym"];' into VarDecl.is_extern/extern_lib/extern_name; the trailing diagnostic now lists 'extern'. Lowering: registerTopLevelGlobal uses extern_name orelse foreign_name orelse name for the C symbol and sets is_extern = is_foreign or is_extern; globalInitValue returns null (no initializer) for extern globals too. examples/1225 green: '__stdinp : *void extern;' lowers to '@__stdinp = external global ptr'; @__stdinp reads non-null. Suite green (636 corpus / 443 unit). Phase 1 done: extern functions (bare + rename) and data globals (bare + rename) all work, behavior-equivalent to the matching #foreign form. export (Phase 2), aggregates (Phase 3), docs + A->B gate (Phase 4) remain. green commit.
This commit is contained in:
@@ -5,47 +5,52 @@ Companion to `current/PLAN-EXTERN-EXPORT.md` — one merged plan: **Part A** add
|
||||
every commit, one step at a time per the cadence rule.
|
||||
|
||||
## Last completed step
|
||||
**Phase 1.1** (green) — wired extern fn lowering in `decl.zig`; example **1223 now
|
||||
green**, full suite green (634 corpus / 443 unit, 0 fail). A bare `extern` fn lowers
|
||||
exactly like a lib-less `#foreign` import: `declare i32 @abs(i32) #0` — external
|
||||
linkage, C ABI, NO `__sx_ctx` param; calls emit `call i32 @abs(i32 -7)` and resolve
|
||||
against the default-linked libc. Six edits, all routing `extern` declare-only
|
||||
(mirroring the `foreign_expr` guards): (1) `funcWantsImplicitCtx` suppresses ctx for
|
||||
`.extern_`; (2) `declareFunction` adds `is_extern_decl` and (3) includes it in the
|
||||
C-ABI promotion; (4) `lazyLowerFunction` routes `.extern_`→declare-only; (5)
|
||||
`lowerFunction` declare-only guard; (6) `lowerFunctionBodyInto` never promotes/lowers
|
||||
an extern stub. Hand-authored 1.0b snapshot matched byte-exact — no regen needed. No
|
||||
`.ir` snapshot added (the trivial `declare i32 @abs(i32)` doesn't warrant a 1000-line
|
||||
full-prelude dump; behavioral `.stdout` already catches ctx/linkage regressions).
|
||||
**Phase 1.2d** (green) — **PHASE 1 COMPLETE.** `extern` data globals now parse + lower:
|
||||
added a `kw_extern` branch in the var-decl-with-type-annotation path (`parser.zig:~451`,
|
||||
beside `#foreign`) parsing `[LIB] ["csym"]` into `is_extern`/`extern_lib`/`extern_name`
|
||||
(+ updated the trailing diagnostic to list `extern`); `registerTopLevelGlobal` now uses
|
||||
`extern_name orelse foreign_name orelse name` for the C symbol and sets `is_extern =
|
||||
is_foreign or is_extern`; `globalInitValue` returns null (no init) for extern too.
|
||||
Example **1225 green** — `__stdinp : *void extern;` lowers to `@__stdinp = external
|
||||
global ptr` and `@__stdinp` reads non-null. Full suite green (636 corpus / 443 unit, 0
|
||||
fail). Kickoff exit criteria met: suite green; extern libc fn + global bindings run;
|
||||
`#foreign` unregressed (all its examples pass → snapshots unchanged).
|
||||
|
||||
## Current state
|
||||
Syntax: bare `extern`/`export`, postfix after `callconv(.c)`, `extern ⇒ callconv(.c)`.
|
||||
**Decision 4 revised** (user 2026-06-14): `extern` carries an optional `LIB`+`"csym"`
|
||||
axis (`extern_lib`/`extern_name`) like `#foreign`; the `#library` decl + build-flag
|
||||
linking stays separate. **`extern` FUNCTIONS WORK** (import; bare form, no rename) —
|
||||
parse + lower complete, behavior-equivalent to a lib-less `#foreign` fn. Still TODO in
|
||||
Phase 1: the `extern LIB "csym"` lib/rename axis (fields exist, unconsumed) and the
|
||||
extern-global form. `export` not started (Phase 2). Part B `foreign` footprint to
|
||||
purge: 643 lines / ~57 identifiers in `src/` + 28 doc lines. End-state invariant:
|
||||
**zero `foreign`** (Phase 9.4 gate). **Done**: 0.0 tokens, 0.1 AST/parser plumbing,
|
||||
1.0a fn-path parsing + lib/name fields, 1.0b xfail example, 1.1 fn lowering (green).
|
||||
linking stays separate. **`extern` IS FULLY WORKING** (PHASE 1 DONE): functions —
|
||||
bare (`f :: (…) -> R extern;`) AND renamed (`extern [LIB] "csym"`); data globals —
|
||||
bare (`g : T extern;`) AND renamed. All behavior-equivalent to the matching `#foreign`
|
||||
form (external linkage, C ABI, no sx ctx). `extern_lib` is parsed + stored but is a
|
||||
*reference* only — actual linking stays the `#library`/build-flag axis (same as
|
||||
`#foreign`'s lib ref). `export` NOT started (Phase 2). Aggregates NOT started (Phase 3).
|
||||
Part B `foreign` footprint to purge: 643 lines / ~57 identifiers in `src/` + 28 doc
|
||||
lines. End-state invariant: **zero `foreign`** (Phase 9.4 gate). Examples: 1223 (bare
|
||||
fn), 1224 (fn rename), 1225 (bare global).
|
||||
|
||||
## Next step
|
||||
**Phase 1.2** (green) — two parts, each its own xfail→green or behavior-lock:
|
||||
1. **`extern LIB "csym"` rename for fns** — extend `parseOptionalExternExport()` (or
|
||||
`parseFnDecl`) to parse the optional `LIB` ident + `"csym"` string after the
|
||||
keyword into `FnDecl.extern_lib`/`extern_name`; consume them in `declareFunction`
|
||||
(mirror the `#foreign` c_name block at `decl.zig:~2119`: declare under the C name,
|
||||
map sx→C in `foreign_name_map`/dedupe). New example renaming a libc symbol (e.g.
|
||||
`c_abs :: (n: i32) -> i32 extern "abs";`).
|
||||
2. **extern-global `g : T extern [LIB] ["csym"];`** — parse path at `parser.zig:425`
|
||||
(the var-decl with type annotation): accept postfix `extern` → set
|
||||
`VarDecl.is_extern`/`extern_lib`/`extern_name`; lower like the `#foreign` global
|
||||
(`decl.zig:~1115-1137`, `.is_extern = vd.is_foreign`). New example mirroring
|
||||
`examples/1205-ffi-foreign-global` with `extern`.
|
||||
**SESSION STOP** — kickoff scope was Phases 0–1 only; Phase 1 is complete. Do NOT
|
||||
start Phase 2 here. Next session picks up **Phase 2 — `export`** (define + expose):
|
||||
fills the four export-gap conditions in `decl.zig` (all on the *define* path, not
|
||||
`declareExtern`): (i) force `.external` linkage when `extern_export == .export_`
|
||||
(`:2382`/`:2514`); (ii) promote to C ABI (`:2110`/the `lowerFunction` cc at `:2522`);
|
||||
(iii) symbol-name override via `export "csym"` (already parsed into `extern_name` —
|
||||
just consume on the define path); (iv) ctx already suppressed for `.export_`? NO —
|
||||
`funcWantsImplicitCtx` currently suppresses only `.extern_`; broaden to `!= .none`
|
||||
(or add `.export_`). Start with an xfail multi-file test: an `export fn` called from a
|
||||
companion `.c` caller. Then Phase 3 (aggregates), Phase 4 (interplay/diagnostics/docs
|
||||
+ the A→B gate: unit test that `#foreign` and `extern` lower to identical IR).
|
||||
|
||||
Stop at end of Phase 1 (do NOT start Phase 2 `export` or Part B migration). Then the
|
||||
A→B gate: a unit test that `#foreign` and `extern` lower to identical IR.
|
||||
**Deferred (do in Phase 4):** (a) docs — `specs.md`/`readme.md` document
|
||||
`extern`/`export` (the plan defers docs to Phase 4; `#foreign` stays documented until
|
||||
the Part B cutover); (b) visibility-gate equivalence — bare `extern` (no `extern_lib`)
|
||||
is currently unconditionally visible via the `c_import_bare` gate
|
||||
(`decl.zig:~2241`, `fd.body.data != .foreign_expr → return true`), whereas a lib-less
|
||||
`#foreign` is policed by `visibleOverEdges`. Single-file examples don't exercise this;
|
||||
verify/align it at the A→B gate (a bare-extern lib-less fn should be policed like its
|
||||
`#foreign` twin). Adding it now would be untested — needs a cross-module example.
|
||||
|
||||
## Open decisions
|
||||
Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B (confirm
|
||||
@@ -83,6 +88,10 @@ historical carve-out — keep `issues/*.md` provenance, gate the live tree only.
|
||||
- (1.2c) Added `examples/1225-ffi-extern-global.sx` (`__stdinp : *void extern;`,
|
||||
mirrors `#foreign` global 1205) + success snapshot. RED (636 ran, 1 failed — parse
|
||||
error: var-decl `extern` not accepted). `xfail`; 1.2d greens it.
|
||||
- (1.2d) Parser `kw_extern` branch in the var-decl path (`[LIB] ["csym"]` →
|
||||
`is_extern`/`extern_lib`/`extern_name`) + `registerTopLevelGlobal`/`globalInitValue`
|
||||
consume `is_extern`. 1225 green (`@__stdinp = external global ptr`). Suite green
|
||||
(636/443). `green` commit. **PHASE 1 COMPLETE** — `extern` fns + globals fully work.
|
||||
|
||||
## Known issues
|
||||
- **Workflow hazard (1.2):** an editor format-on-save (or `zig fmt`) clobbered the
|
||||
|
||||
@@ -1112,10 +1112,11 @@ pub fn registerTopLevelGlobal(self: *Lowering, vd: *const ast.VarDecl) void {
|
||||
d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name});
|
||||
break :blk .void;
|
||||
};
|
||||
// Foreign globals reference a symbol defined in libSystem etc.
|
||||
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
|
||||
// name is the optional override or the sx name itself.
|
||||
const sym_name = vd.foreign_name orelse vd.name;
|
||||
// Foreign / extern globals reference a symbol defined in libSystem etc.
|
||||
// (`_NSConcreteStackBlock : *void #foreign;` or `… : *void extern;`). The C
|
||||
// symbol name is the optional override (`extern_name`/`foreign_name`) or the
|
||||
// sx name itself.
|
||||
const sym_name = vd.extern_name orelse vd.foreign_name orelse vd.name;
|
||||
const name_id = self.module.types.internString(sym_name);
|
||||
const init_val = self.globalInitValue(vd, var_ty);
|
||||
const gid = self.module.addGlobal(.{
|
||||
@@ -1123,7 +1124,7 @@ pub fn registerTopLevelGlobal(self: *Lowering, vd: *const ast.VarDecl) void {
|
||||
.ty = var_ty,
|
||||
.init_val = init_val,
|
||||
.is_const = false,
|
||||
.is_extern = vd.is_foreign,
|
||||
.is_extern = vd.is_foreign or vd.is_extern,
|
||||
});
|
||||
self.putGlobal(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty });
|
||||
}
|
||||
@@ -1137,7 +1138,7 @@ pub fn registerTopLevelGlobal(self: *Lowering, vd: *const ast.VarDecl) void {
|
||||
/// is rejected with a diagnostic rather than silently zero-initialized — a
|
||||
/// global has no run site for a dynamic initializer.
|
||||
pub fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId) ?inst_mod.ConstantValue {
|
||||
if (vd.is_foreign) return null;
|
||||
if (vd.is_foreign or vd.is_extern) return null;
|
||||
const v = vd.value orelse return null;
|
||||
return switch (v.data) {
|
||||
.undef_literal => .zeroinit,
|
||||
|
||||
@@ -449,7 +449,35 @@ pub const Parser = struct {
|
||||
} });
|
||||
}
|
||||
|
||||
return self.fail("expected ':', '=', ';' or '#foreign' after type annotation");
|
||||
if (self.current.tag == .kw_extern) {
|
||||
// name : type extern [LIB] ["csym"]; (extern data global — the
|
||||
// extern-named counterpart of `#foreign`; resolved at link time)
|
||||
self.advance();
|
||||
var ext_lib: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier) {
|
||||
ext_lib = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
}
|
||||
var ext_name: ?[]const u8 = null;
|
||||
if (self.current.tag == .string_literal) {
|
||||
const raw = self.tokenSlice(self.current);
|
||||
ext_name = raw[1 .. raw.len - 1];
|
||||
self.advance();
|
||||
}
|
||||
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_extern = true,
|
||||
.extern_lib = ext_lib,
|
||||
.extern_name = ext_name,
|
||||
.is_raw = name_is_raw,
|
||||
} });
|
||||
}
|
||||
|
||||
return self.fail("expected ':', '=', ';', '#foreign', or 'extern' after type annotation");
|
||||
}
|
||||
|
||||
fn parseTypeExpr(self: *Parser) anyerror!*Node {
|
||||
|
||||
Reference in New Issue
Block a user