diff --git a/current/CHECKPOINT-EXTERN-EXPORT.md b/current/CHECKPOINT-EXTERN-EXPORT.md index b0442a2..7b2c004 100644 --- a/current/CHECKPOINT-EXTERN-EXPORT.md +++ b/current/CHECKPOINT-EXTERN-EXPORT.md @@ -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 diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index c140137..6343964 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -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, diff --git a/src/parser.zig b/src/parser.zig index 543b1a8..0934dad 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -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 {