Compare commits
90 Commits
e386a0d0b4
...
8a3bdbe7b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3bdbe7b5 | ||
|
|
f3c9747f5a | ||
|
|
c1ab2cbfc0 | ||
|
|
b9cfe2554f | ||
|
|
b52d424369 | ||
|
|
9719432e79 | ||
|
|
dfae690b31 | ||
|
|
811a280517 | ||
|
|
dc51c4b5bf | ||
|
|
e99383fcb4 | ||
|
|
145b6d8eff | ||
|
|
8cca3b9dde | ||
|
|
a15a868391 | ||
|
|
d27be42a93 | ||
|
|
5c8af6eb73 | ||
|
|
3354446412 | ||
|
|
7ffdc7d2a2 | ||
|
|
98264b8640 | ||
|
|
cd147942e4 | ||
|
|
b78e7ddeb1 | ||
|
|
b838f6383f | ||
|
|
7ca074e1b0 | ||
|
|
3811311e12 | ||
|
|
8180faf839 | ||
|
|
d132aab232 | ||
|
|
720556b24e | ||
|
|
2cce6a3a26 | ||
|
|
8b91677a1b | ||
|
|
1a8991ab27 | ||
|
|
2888f6fc00 | ||
|
|
a68f7c2e64 | ||
|
|
496390e442 | ||
|
|
731fb8de64 | ||
|
|
d3425fa287 | ||
|
|
32a7628297 | ||
|
|
666a2e20e1 | ||
|
|
48a8769d19 | ||
|
|
59f90d2939 | ||
|
|
0fbcee7e36 | ||
|
|
2cd5d7ba82 | ||
|
|
32e83c90cc | ||
|
|
410a52e4ca | ||
|
|
346d4a81c3 | ||
|
|
93e7b6f727 | ||
|
|
bde284ee21 | ||
|
|
6b94bb6bba | ||
|
|
4dca38881e | ||
|
|
ad6aed3d7a | ||
|
|
38c32400f5 | ||
|
|
3c94c14b5e | ||
|
|
270652186e | ||
|
|
b5411efeb8 | ||
|
|
0fdc82154f | ||
|
|
9a2c78d6b9 | ||
|
|
28d38f2f2f | ||
|
|
7d8ba1aabc | ||
|
|
717c35d26d | ||
|
|
47aaf3662a | ||
|
|
e5ddfbe09a | ||
|
|
9ad04e2dda | ||
|
|
0d39a1e168 | ||
|
|
fde767913b | ||
|
|
aafcbf6d78 | ||
|
|
422c6577cf | ||
|
|
847a027fb1 | ||
|
|
a8e0a8961b | ||
|
|
4101cbc3e7 | ||
|
|
dd927c2e94 | ||
|
|
5d4a2c26c1 | ||
|
|
d4f683f525 | ||
|
|
91d70bd864 | ||
|
|
a9a6d53dc0 | ||
|
|
9f1d7be105 | ||
|
|
0bde545f24 | ||
|
|
5ba8d302c2 | ||
|
|
23feea6a0c | ||
|
|
66d9169e59 | ||
|
|
a47ef20ad3 | ||
|
|
6a539ca057 | ||
|
|
6932426c41 | ||
|
|
235f74a8c9 | ||
|
|
5777ff62ad | ||
|
|
5f946a3d44 | ||
|
|
18c43984e1 | ||
|
|
78e304f552 | ||
|
|
df6b675e67 | ||
|
|
62a3b46f6e | ||
|
|
bf6ef8370f | ||
|
|
78f7bb7857 | ||
|
|
c562fe236d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,5 +3,4 @@ zig-out
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.sx-cache
|
||||
.sx-tmp
|
||||
current/
|
||||
.sx-tmp
|
||||
19
CLAUDE.md
19
CLAUDE.md
@@ -430,15 +430,11 @@ After any compiler change:
|
||||
preferred way to update snapshots — no shell script needed.
|
||||
- A test is still keyed off its `expected/<name>.exit` marker, so seed an
|
||||
empty marker first for a brand-new example (see "Adding a feature").
|
||||
3. **Standalone corpus run** (optional): `bash tests/run_examples.sh`
|
||||
- Runs the corpus independent of `zig build test` (used by
|
||||
`tools/verify-step.sh`). `--update` still regenerates snapshots and
|
||||
produces byte-identical output to `-Dupdate-goldens`.
|
||||
- Every test must show `ok` (currently 626); zero failures, zero timeouts.
|
||||
- Uses GNU `timeout`/`gtimeout` when present (Homebrew coreutils on macOS)
|
||||
and runs without a per-test wall-clock guard when neither is found.
|
||||
- The two normalizers (`normalize`/`normalize_ir` in the script and the
|
||||
mirrors in `src/corpus_run.test.zig`) must stay in lockstep.
|
||||
`zig build test` is the only way to run the corpus — there is no standalone
|
||||
shell runner (the legacy `tests/run_examples.sh` was removed). An
|
||||
`expected/<name>.aot` marker switches an example from JIT `sx run` to a
|
||||
`sx build` + execute flow (needed to exercise a C-ABI symbol exported FROM sx
|
||||
— a JIT-resident symbol is invisible to a dlopen'd C dylib).
|
||||
|
||||
### Test layout
|
||||
|
||||
@@ -495,7 +491,6 @@ There is no monolithic smoke file — each feature is its own focused example.
|
||||
| `issues/NNNN-slug.md` | Open-issue / bug-report writeup (mark RESOLVED in a banner when fixed; the `.md` stays). |
|
||||
| `issues/NNNN-slug.sx` (+ `issues/NNNN-slug/`) | The issue's minimal repro, co-located with the `.md`. A repro with an `issues/expected/NNNN-slug.exit` marker runs in the suite; unpinned ones don't. |
|
||||
| `src/corpus_run.test.zig` | The corpus runner inside `zig build test` — spawns `sx` per example, diffs stdout/stderr/exit (+ optional IR); regenerates snapshots under `-Dupdate-goldens`. |
|
||||
| `tests/run_examples.sh` | Standalone shell runner (used by `tools/verify-step.sh`); same compare + `--update` as the Zig test. |
|
||||
|
||||
### Unit test file convention
|
||||
|
||||
@@ -558,7 +553,7 @@ The compiler shrinks to: parse → IR → codegen → link → invoke a sx
|
||||
function. Codesigning / Info.plist / AndroidManifest / javac / d8 /
|
||||
aapt2 / zipalign / apksigner / framework embed / entitlements / asset
|
||||
trees all run in the IR interpreter post-link via libc / process.run
|
||||
foreign calls.
|
||||
extern calls.
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
@@ -568,7 +563,7 @@ foreign calls.
|
||||
| [library/modules/build.sx](library/modules/build.sx) | `BuildOptions` setters + accessors. Adding a new bundling parameter = add a setter here + a hook in compiler_hooks.zig. |
|
||||
| [library/modules/platform/android.sx](library/modules/platform/android.sx) | `AndroidPlatform` (state-on-struct, no module globals). `sx_android_*` helpers take `plat: *AndroidPlatform` as first arg. `logical_w` field drives `dpi_scale = pixel_w / logical_w` so consumer's design-width fits any physical resolution. |
|
||||
| [src/ir/compiler_hooks.zig](src/ir/compiler_hooks.zig) | `BuildConfig` + every `BuildOptions.*` hook. Hook registry is in `Registry.registerDefaults`. |
|
||||
| [src/ir/host_ffi.zig](src/ir/host_ffi.zig) | `dlsym(RTLD_DEFAULT)` + arity-switched cdecl trampolines. Lets `#foreign("c")` decls resolve at `#run` / post-link time against host libc. |
|
||||
| [src/ir/host_ffi.zig](src/ir/host_ffi.zig) | `dlsym(RTLD_DEFAULT)` + arity-switched cdecl trampolines. Lets `extern "c"` decls resolve at `#run` / post-link time against host libc. |
|
||||
| [src/main.zig](src/main.zig) | After `target.link()`, threads target_triple + frameworks + jni_main emissions into BuildConfig, then invokes the post-link callback by FuncId (or by `<module>.bundle_main` name). `--bundle` / `--apk` flags feed `bundle_path`; auto-fallback to `post_link_module = "platform.bundle"` when bundle_path is set without a registered callback. |
|
||||
|
||||
Specifics in [specs.md §10.5](specs.md). The full bundling pipeline
|
||||
|
||||
24
current/CHECKPOINT-ASM.md
Normal file
24
current/CHECKPOINT-ASM.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# sx Inline Assembly — Checkpoint (ASM stream)
|
||||
|
||||
Companion to `current/PLAN-ASM.md`; design in
|
||||
[docs/inline-asm-design.md](../docs/inline-asm-design.md). Update after every
|
||||
commit, one step at a time per the cadence rule (no commit may both add a test
|
||||
and make it pass).
|
||||
|
||||
## Last completed step
|
||||
None — plan authored, not yet started.
|
||||
|
||||
## Current state
|
||||
Design fully converged (`docs/inline-asm-design.md`). Feasibility confirmed:
|
||||
`llvm_api.c.*` exposes `LLVMGetInlineAsm` / `LLVMBuildCall2` /
|
||||
`LLVMAppendModuleInlineAsm` (LLVM@19). No code written.
|
||||
|
||||
## Next step
|
||||
**A.0** — add the `kw_asm` keyword (`src/token.zig` Tag + `StaticStringMap`) and a
|
||||
unit lex test. Then A.1 (parse `asm { … }` → `AsmExpr`, lowering bails loudly).
|
||||
|
||||
## Log
|
||||
- (init) Plan + design doc written; ASM stream opened.
|
||||
|
||||
## Known issues
|
||||
None yet.
|
||||
861
current/CHECKPOINT-EXTERN-EXPORT.md
Normal file
861
current/CHECKPOINT-EXTERN-EXPORT.md
Normal file
@@ -0,0 +1,861 @@
|
||||
# sx `extern`/`export` + `#foreign` retirement — Checkpoint (FFI-linkage stream)
|
||||
|
||||
Companion to `current/PLAN-EXTERN-EXPORT.md` — one merged plan: **Part A** adds
|
||||
`extern`/`export`, **Part B** migrates `#foreign` and purges `foreign`. Update after
|
||||
every commit, one step at a time per the cadence rule.
|
||||
|
||||
## Last completed step
|
||||
**Phase 9 COMPLETE — total `foreign` purge; 9.4 GATE PASSES.** **THE ENTIRE
|
||||
FFI-LINKAGE STREAM (Parts A + B, Phases 0–9) IS DONE.** Final commits: 9.0 token
|
||||
delete (`dfae690`), 9.3 src/docs/example/library comment purge (`811a280`, `e99383f`,
|
||||
`dc51c4b`, + the capital-Foreign sweep), 9.3 example filename renames + dedup
|
||||
(`b52d424`), 9.3/9.4 issues/*.md purge + GATE (`b9cfe25`).
|
||||
- **9.0:** deleted the `hash_foreign` token entirely (token/lexer/parser/lsp + the lex
|
||||
test); `#foreign` now → a generic "expected ';'" parse error (accepted UX cost);
|
||||
deleted the obsolete 1176 rejection test.
|
||||
- **9.1/9.2:** all internal identifiers renamed (linkage→`extern`/`is_extern`,
|
||||
runtime-class→`Runtime*`/`runtime_*` per Decision 5, `foreign_path`→`runtime_path`
|
||||
across the build-hook boundary); `foreign_expr` node eliminated.
|
||||
- **9.3:** purged every `foreign` COMMENT (src caps + lowercase, examples, docs incl.
|
||||
the obsolete inline-asm Deviation 6, editors/vscode grammar) + renamed all 10
|
||||
`*-foreign*` example files (+ companions/expected/refs) to extern/runtime names
|
||||
(dedup'd 1218↔1229, removed orphan 1620 dir) + rewrote 20 issues/*.md writeups +
|
||||
renamed issues/0043.
|
||||
- **9.4 GATE:** `grep -rniIE 'foreign' src/ library/ examples/ issues/ docs/ editors/
|
||||
specs.md readme.md CLAUDE.md` → **0**, excluding only the legitimate keeps:
|
||||
`SQLITE_CONSTRAINT_FOREIGNKEY` (SQLite API const) + vendored `library/vendors/sqlite/
|
||||
c/*` (upstream third-party C). No `foreign`-named files in the tree (node_modules +
|
||||
.sx-tmp are gitignored third-party/scratch). Suite green (644 corpus / 443 unit, 0
|
||||
failed).
|
||||
|
||||
### Prior: Phase 9.3 — text/comment purge (src + docs + example comments) (commits
|
||||
`e99383f` docs, `dc51c4b` src, + examples purge STAGED pending a classifier outage —
|
||||
commit message ready; `git commit` the staged `examples/` changes when Bash is back).
|
||||
`foreign` is now purged from: **all `src/` comments** (reworded to extern/runtime-class;
|
||||
fixed 2 user-facing diagnostics — the type-annotation parse error no longer lists
|
||||
`#foreign`, and the Android no-`#jni_main` help shows `#jni_class(…) extern`), **specs/
|
||||
readme/CLAUDE** ("Foreign Function Interface"→"C Interop", etc.), and **all example .sx
|
||||
comments** (1219 stdout labels Foreign→Extern, snapshot regenerated). Suite green
|
||||
(646/444) throughout; snapshot-neutral except the intentional 1219 regen.
|
||||
|
||||
**What still contains `foreign` (the analyzed keep-list + the not-yet-done):**
|
||||
- **KEEP (gate-exempt):** `src/` `hash_foreign` token + lexer entry + `lex hash_foreign`
|
||||
test (`#foreignx`) + the 4 parser rejection messages ("`#foreign` has been removed…");
|
||||
`1176-diagnostics-foreign-removed.sx` (its `#foreign` decl + comments ARE the rejection
|
||||
test); `SQLITE_CONSTRAINT_FOREIGNKEY` + vendored `library/vendors/sqlite/c/*`.
|
||||
- **NOT YET DONE:** example FILENAMES (`*-foreign*.sx` + the `0729`/`1205`/`1218`/`1219`/
|
||||
`1306`/`1318`/`1216`/`1217` families) and their `#import`/`#include`/`#source` path refs
|
||||
+ `expected/` files — needs a git-mv rename step; and **`issues/*.md`** (~20 writeups).
|
||||
|
||||
### Prior: Phase 9.1 + 9.2 — internal IDENTIFIER purge COMPLETE (commits 9.1a `b838f63`,
|
||||
9.1b `b78e7dd`, 9.1c `cd14794`, 9.1d `7ffdc7d`, 9.2a `3354446`, 9.2b `5c8af6e`,
|
||||
9.2b-fix `a15a868`, 9.2c `d27be42`, 9.2d `8cca3b9`). **Every `foreign` IDENTIFIER in
|
||||
`src/` is renamed** — the only `foreign` left in `src/` is COMMENTS + the kept token
|
||||
(`hash_foreign` + its `#foreignx` lexer-boundary test) + the rejection-message string.
|
||||
Suite green (646/444) at every commit.
|
||||
- **9.1d** eliminated the `foreign_expr` AST node: migrated `c_import.zig` auto-synth
|
||||
to the extern shape, deleted the node + `ForeignExpr` + all readers.
|
||||
- **9.2a/b/c/d** ran the runtime-class family rename (Decision 5 → `Runtime*`):
|
||||
types `ForeignClassDecl`→`RuntimeClassDecl` etc.; fns `parseForeignClassDecl`→
|
||||
`parseRuntimeClassDecl`, `lowerForeignMethodCall`→`lowerRuntimeMethodCall`, …; state
|
||||
`foreign_class_map`→`runtime_class_map`, `foreign_class_decl` variant→
|
||||
`runtime_class_decl`; the extern-ref validators → `Extern` (linkage, `checkExternRefs`);
|
||||
the reference flag → **`is_extern`** (per user: reuse existing terminology, not a new
|
||||
`is_reference`); and `foreign_path`→`runtime_path` COUPLED across the hook boundary
|
||||
(build.sx accessor `jni_main_runtime_path_at` + the registered hook string +
|
||||
bundle.sx + specs.md), with 37 `.ir` snapshots regenerated for the renamed
|
||||
`@BuildOptions.jni_main_runtime_path_at` declare stub (symbol-name change only).
|
||||
- **9.1a/b/c** (linkage): 5 collision-free renames (callExtern, …); "foreign symbol"
|
||||
diagnostic + panic → "extern symbol" (1172 regen); deleted dead VarDecl legacy fields.
|
||||
|
||||
### Prior: Phase 9.1 (partial) — internal linkage-identifier purge (commits `b838f63` 9.1a,
|
||||
`b78e7dd` 9.1b, `cd14794` 9.1c). **PHASE 9 STARTED.** Decision 6 = PURGE EVERYTHING,
|
||||
scoped (user, 2026-06-15): purge `foreign` from **all `.sx` files + all documentation +
|
||||
all our Zig (`src/`)**, analyzing each grep hit — **legitimate keeps stay**
|
||||
(SQLITE_CONSTRAINT_FOREIGNKEY + other SQLite API constant names, vendored
|
||||
`library/vendors/sqlite/c/*`, `1176-diagnostics-foreign-removed.sx` [the rejection test
|
||||
MUST contain `#foreign`], the parser rejection-message string + `hash_foreign` token
|
||||
[kept so `#foreign` keeps its friendly deprecation error]).
|
||||
- **9.1a** (`b838f63`): 5 collision-free linkage renames — `callForeign`→`callExtern`,
|
||||
`marshalForeignArg`→`marshalExternArg`, `dedupeForeignSymbol`→`dedupeExternSymbol`,
|
||||
`foreign_name_map`→`extern_name_map`, `is_foreign_c_api`→`is_extern_c_api`.
|
||||
- **9.1b** (`b78e7dd`): the "foreign symbol already bound" diagnostic (decl.zig) +
|
||||
resolveFuncByName panic (call.zig) → "extern symbol". Intentional 1172 regen.
|
||||
- **9.1c** (`cd14794`): **deleted** the dead `VarDecl.is_foreign`/`foreign_lib`/
|
||||
`foreign_name` fields (the global `#foreign` path rejects → write-dead; 3 coalescing
|
||||
readers in decl.zig simplified to `vd.extern_name`/`vd.is_extern`).
|
||||
All snapshot-neutral except the intentional 1172 regen; suite green (646/444).
|
||||
|
||||
**COLLISION ANALYSIS (done — drives the rest of 9.1/9.2):**
|
||||
- `is_foreign` lives on FnDecl?(no — flipped to `extern_export`), **VarDecl (deleted in
|
||||
9.1c)**, and **ForeignClassDecl (ast.zig:903 — STILL LIVE**, distinguishes runtime-class
|
||||
reference vs define; renamed in 9.2, not 9.1).
|
||||
- `is_extern`/`extern_lib`/`extern_name` already exist (VarDecl + IR insts) — so the
|
||||
old `foreign_*` linkage names could NOT be blind-renamed onto them; 9.1c deleted the
|
||||
dead VarDecl trio instead of renaming.
|
||||
- `foreign_expr` (25) is **still BUILT by `c_import.zig` auto-synthesis** (`#import c
|
||||
{#include}` synthesizes fn bodies as `foreign_expr`). To eliminate it: migrate that
|
||||
synth path to build the extern shape (empty-block body + `extern_export = .extern_`),
|
||||
exactly the Phase 5.0 fn-body flip but for auto-synth — THEN delete the `foreign_expr`
|
||||
node + all readers. This is the last 9.1 item.
|
||||
|
||||
### Prior: Phase 8 — CUTOVER: parser hard-rejects `#foreign` (`feat!` commit `3811311`,
|
||||
preceded by the 8.0 xfail `8180faf` + 3 pre-cutover `refactor`s `2cce6a3`/`720556b`/
|
||||
`d132aab`). **PHASE 8 COMPLETE.** The prefix `#foreign` linkage directive is removed:
|
||||
all four parse sites (const-with-type 316, data global 425, fn body 2065, runtime-class
|
||||
prefix via caller 260) reject it with the migration message *"`#foreign` has been
|
||||
removed; use the postfix `extern` (import) / `export` (define) linkage keyword
|
||||
instead"*; added a span-aware `failAt` for the runtime-class case (the lookahead
|
||||
consumes the token before the reject decision). New example **1176**
|
||||
(`diagnostics-foreign-removed`) pins it. **Pre-cutover migrations** (all green,
|
||||
behavior-preserving): the 7 identity `ffi-foreign-*` test DECLS (`2cce6a3`), the two
|
||||
keyword-neutral diagnostic tests 1172 + 1228 with intentional snapshot regens
|
||||
(`720556b`), and the 4 multi-file example companions Phase 7 missed (0729/a+b, 1617/c,
|
||||
1623/mod — `d132aab`). **Deleted** obsolete tests 1174 (`#foreign`+postfix conflict, now
|
||||
unreachable) + 1620 (`#foreign nosuchunit`, superseded by extern twin 1231), the GATE
|
||||
A→B unit test + `lowerSrcToIr` helper (nothing left to compare), and converted the
|
||||
in-source `parse void function with foreign body` parser test to postfix `extern`.
|
||||
specs.md + readme.md document `extern`/`export` as the sole C-linkage surface. Suite
|
||||
green (646 corpus / 444 unit, 0 failed).
|
||||
|
||||
### Prior: Phase 7.4 — migrate straggler examples `#foreign`→`extern` (`refactor` commit
|
||||
`1a8991a`). **PHASE 7 MIGRATABLE WORK COMPLETE (7.1–7.4 done).** Migrated 16 fn/global
|
||||
examples across categories (0415/0602/0603/1024/1025/1605/1607-1609/1611/1616/1619/
|
||||
1622/1628/1635/1636). Marker'd ones corpus-validated; the 3 unmarked uikit importers
|
||||
(1607/1608/1616) verified byte-identical via `sx ir` probes. Empty snapshot diff; suite
|
||||
green (647/444).
|
||||
|
||||
**Phase 7 net result:** every example that uses `#foreign` *incidentally* (FFI plumbing,
|
||||
output-preserving) is now on `extern`/`export`. The **24 files still holding `#foreign`
|
||||
are exactly the intended keep-list**, all deferred to the Phase 8 cutover:
|
||||
- **`foreign`-asserting diagnostics** (migrating changes a snapshot): 1172, 1174, 1219
|
||||
(stdout label), 1228 (equivalence test), 1620.
|
||||
- **Identity `ffi-foreign-*` feature tests** (real decls; rename/dedup at cutover):
|
||||
1205-global, 1205-global-helper, 1207, 1218, 1219, 1306, 1318.
|
||||
- **Comment-only / provenance prose** (decls=0; `#foreign` only in comments): 0716, 0729,
|
||||
1216, 1223, 1224, 1225, 1229, 1230, 1231, 1332, 1348, 1349, 1426, + issues/0030.
|
||||
**Lesson (7.3):** the robust class-prefix transform is the GENERAL form
|
||||
`s/#foreign\s+#(\w+)\((\"[^\"]*\")\)\s*\{/#$1($2) extern {/` — 1417 also used
|
||||
`#jni_interface`/`#objc_protocol`/`#swift_class`/`#swift_struct`/`#swift_protocol`, and a
|
||||
`#(objc|jni)_class`-only regex left `extern` in *prefix* position → parse error. All such
|
||||
directives accept the postfix modifier (probed). Bare defined `#objc_class`/`#jni_class`
|
||||
examples (no `#foreign`) were left untouched — not a purge target (define→export is an
|
||||
optional consistency pass, deferrable).
|
||||
|
||||
### Prior: Phase 7.1 — migrate incidental 12xx ffi examples (`refactor` commit `731fb8d`).
|
||||
Migrated 12 plain-C examples (1200/1206/1209-1215/1220/1221/1222); established the
|
||||
keep-list policy above. Phase 7.2 (`a68f7c2`): 18 13xx obj-c examples (prefix→postfix
|
||||
classes). Phase 7.3 (`2888f6f`): 13 14xx jni examples incl. 1417 multi-runtime.
|
||||
|
||||
### Prior: Phase 6.5 — migrate `gpu/` `#foreign`→`extern`; `library/` now `#foreign`-free
|
||||
(`refactor` commit `32a7628`). **PHASE 6 COMPLETE.** Final batch: gpu/gles3.sx
|
||||
(eglGetProcAddress + 1 comment) + gpu/metal.sx (MTLCreateSystemDefaultDevice), bare
|
||||
fn markers → `extern`. Verified byte-identical `sx ir` on importers 1610 (gles3) +
|
||||
1606 (metal). **Zero `#foreign` remains anywhere under `library/`** — verified by
|
||||
`grep -rln '#foreign' library/` → no matches. Suite green (647 corpus / 444 unit, 0
|
||||
failed).
|
||||
|
||||
### Prior: Phase 6.4 — migrate `ffi/` `#foreign`→`extern` (`refactor` commit `666a2e2`).
|
||||
objc/objc_block/raylib/sdl3/wasm (~51 sites): fn markers + objc.sx's 2 import runtime
|
||||
classes (prefix→postfix `extern`). objc + objc_block validated by the 50 marked 13xx
|
||||
corpus examples (incl. import classes 1300/1301 + defined classes 1339/1349);
|
||||
raylib/ffi-sdl3/wasm verified by byte-identical `sx ir` probes pre/post. Empty snapshot
|
||||
diff; suite green.
|
||||
|
||||
### Prior: Phase 6.3 — migrate `std/` `#foreign`→`extern` (`refactor` commit `59f90d2`).
|
||||
Pure source rename across 11 std modules (~60 sites):
|
||||
cli/core/fmt/fs/log/net.kqueue/process/socket/thread/time/trace. All fn-decl markers
|
||||
— bare `#foreign;`, `#foreign libc;`/`#foreign tlib;` (LIB ref), and
|
||||
`#foreign libc "csym";` (LIB+rename) → the same `extern …` tail (`extern` carries the
|
||||
identical `[LIB] ["csym"]` axis); plus 2 stale comment mentions (fmt/fs). No class
|
||||
forms in std. These modules ARE host-corpus-exercised → empty snapshot diff is direct
|
||||
validation. Suite green (647 corpus / 444 unit, 0 failed). Remaining Phase 6 batches:
|
||||
6.4 ffi (~50, has runtime classes), 6.5 remainder.
|
||||
|
||||
### Prior: Phase 6.2 — migrate `platform/` `#foreign`→`extern`/`export` (`refactor` commit
|
||||
`2cd5d7b`). Pure source rename across uikit/android/android_jni/sdl3 (~64 sites):
|
||||
30 fn `… #foreign;`→`… extern;`; 34 import runtime classes
|
||||
`#foreign #objc_class/#jni_class("X") {`→`#…_class("X") extern {` (prefix→postfix);
|
||||
4 defined `Sx*` obj-c classes `#objc_class("X") {`→`… export {`. Behavior-preserving;
|
||||
empty snapshot diff. **Verification (these modules are largely uncompiled by the
|
||||
host corpus** — bundle examples import `bundle.sx`, not the runtime modules; android.sx
|
||||
only compiles under `OS==.android`): byte-identical `sx ir` on uikit importers 1610 +
|
||||
1606 (which DO compile uikit incl. the 4 defined `Sx*` classes on host) and an sdl3
|
||||
direct-import probe; android.sx verified by an identical 4-error dedup set (host
|
||||
pthread clashes — the keyword-neutral "foreign symbol already bound" dedup message is
|
||||
unchanged, and the probe parsed all migrated `extern` jni classes + EGL fns cleanly
|
||||
before hitting them). Suite green (647 corpus / 444 unit, 0 failed). Remaining Phase 6
|
||||
batches: 6.3 std (~60), 6.4 ffi (~50), 6.5 remainder.
|
||||
|
||||
### Prior: Phase 6.1 — migrate `vendors/sqlite` `#foreign`→`extern` (`refactor` commit
|
||||
`410a52e`). **PART B PHASE 6 STARTED.** Pure source rename: all 97
|
||||
`sqlite3_* … #foreign sqlib "csym";` fn decls → `extern sqlib "csym";` (+ the one
|
||||
stale header-comment reference, line 9). The `extern_lib` axis references the `sqlib`
|
||||
`#import c` unit identically to `#foreign sqlib`, so IR/output is byte-identical —
|
||||
empty snapshot diff (only `sqlite.sx` changed), and example 1624
|
||||
(`vendor-sqlite-module`) stdout byte-unchanged. Suite green (647 corpus / 444 unit,
|
||||
0 failed).
|
||||
|
||||
### Prior: Phase 5.1 — annotate A→B gate post-flip + add fn-rename case (`test` commit
|
||||
`93e7b6f`). **PHASE 5 COMPLETE → PART B Phase 5 done.** The A→B gate
|
||||
(`lower.test.zig`) already asserted `#foreign` ≡ `extern`/`export` byte-identical IR
|
||||
for fn / global / Obj-C class; post-Phase-5.0 the fn-decl + data-global paths build
|
||||
the SAME extern-named AST, so cases 1/2 are now STRUCTURALLY identical (guaranteed by
|
||||
construction, not coincidence). Annotated the gate header to record this and keep it
|
||||
as a regression tripwire (catches a future reader re-diverging the spellings, or a
|
||||
revert of the flip); case 3 (runtime class) stays behaviorally — not structurally —
|
||||
equal via the single `is_foreign_eff` field. Added a fn-rename case (case 2b,
|
||||
`extern_name` axis: `c_abs` → `"abs"`) to broaden coverage beyond bare import
|
||||
(verified IR-identical via `sx ir` probe before adding). Test-only, no snapshot churn.
|
||||
Suite green (647 corpus / 444 unit, 0 failed).
|
||||
|
||||
### Prior: Phase 5.0 — fn-decl `#foreign` body-marker FLIP (`refactor` commit `6b94bb6`).
|
||||
**PHASE 5.0 PARSER ROUTING COMPLETE.** The fn-body `#foreign [LIB] ["csym"]` marker
|
||||
now builds the SAME extern AST postfix `extern` produces (`extern_export = .extern_`
|
||||
+ `extern_lib`/`extern_name` + empty-block body) instead of a `foreign_expr` body.
|
||||
Behavior-preserving — all four prereqs (visibility, variadic, plain-free, lib-ref)
|
||||
ensure every downstream reader coalesces `is_foreign` with `extern_export`, so IR +
|
||||
runtime are byte-identical (full corpus + A→B gate green). Decision 7 churn realised:
|
||||
example 1620's lib-ref error flips "#foreign library" → "extern library" (the only
|
||||
snapshot moved; hand-edited, not regen). Parser unit test updated to assert the extern
|
||||
shape. Spot-checked 1219/1218/0729 (foreign rename / cvariadic / same-name) end-to-end.
|
||||
**All four `#foreign` parser paths now resolved:** global (`e5ddfbe`) + fn-body
|
||||
(`6b94bb6`) flipped onto extern; const-with-type is dead (deferred); runtime-class is
|
||||
already coalesced (`is_foreign_eff`). `c_import.zig` auto-synthesis STILL emits
|
||||
`foreign_expr` bodies (Phase 6+), so both shapes coexist — every reader stays dual.
|
||||
Suite green (647 corpus / 444 unit, 0 failed).
|
||||
|
||||
### Prior: Phase 5.0 prereqs 3 & 4 — plain-free classification + extern lib-ref validation
|
||||
(plain-free: xfail `2706521` → fix `3c94c14`; lib-ref: xfail `38c3240` → fix
|
||||
`ad6aed3`). Two MORE extern/#foreign divergences found while de-risking the fn-path
|
||||
flip, both now closed. **FOUR prereqs total done — the fn-decl flip fully de-risked.**
|
||||
- **Prereq 3 (plain-free):** `isPlainFreeFn`/`isPlainFreeFnDecl` (resolver.zig:178,
|
||||
generic.zig:815) excluded a `#foreign` body but classified an empty-block `extern`
|
||||
fn as a plain free fn — so existing extern fns were wrongly counted in the bare-call
|
||||
ambiguity verdict (example: two same-name `extern libc "abs"` authors errored
|
||||
ambiguous, while the `#foreign` twin 0729 compiles). Both predicates now also
|
||||
exclude `extern_export == .extern_`; `export` (real body) stays plain-free. Example
|
||||
**1230**.
|
||||
- **Prereq 4 (lib-ref validation):** `checkForeignRefs` (c_import.zig) validated only
|
||||
`foreign_expr.library_ref`, so a bogus `extern nosuchunit "abs"` compiled silently
|
||||
while `#foreign nosuchunit` errors (1620). Now reads the lib ref from EITHER spelling
|
||||
and names the surface keyword in the diagnostic (so 1620 stays byte-unchanged).
|
||||
Example **1231**.
|
||||
- Two OTHER classifying sites probed and found BENIGN for extern (no flip prereq):
|
||||
namespace/qualified dispatch (`registerQualifiedFn` decl.zig:2208, namespace gate
|
||||
call.zig:729) — a namespaced `extern` fn resolves identically to its `#foreign` twin
|
||||
(probe: `cm.c_abs(-9)` → 9 both ways; the registered qualified alias resolves to the
|
||||
same extern symbol).
|
||||
|
||||
### Prior: Phase 5.0 prereq — extern C-variadic tail (xfail `9a2c78d` → fix `0fdc821`) — the SECOND deferred fn-path prerequisite. **BOTH original fn-path prereqs done.** The C-variadic `...` handling was keyed on the `#foreign` (`foreign_expr`)
|
||||
body shape at two sites — the `is_variadic` drop in `declareFunction`
|
||||
(`decl.zig:2097`) and the call-site early-out in `packVariadicCallArgs`
|
||||
(`pack.zig:302`). A variadic `extern` therefore kept its trailing slice param and
|
||||
slice-packed the extras → garbage at the C ABI (probe: `sum_ints(3,10,20,30)` →
|
||||
53316585, not 60). Both gates now also fire for `extern_export == .extern_`, so a
|
||||
variadic `extern` drops the `..args: []T`, sets `is_variadic`, and passes extras
|
||||
through the C `...` slot with default argument promotion — byte-identical to its
|
||||
`#foreign` twin. New example **1229** (`1229-ffi-extern-cvariadic`, JIT `#source`,
|
||||
int-sum + double-avg). Suite green (645 corpus / 444 unit, 0 failed).
|
||||
|
||||
### Prior: Phase 5.0 prereq — visibility-gate equivalence (xfail `717c35d` → fix `7d8ba1a`) — the first of the two deferred fn-path prerequisites.
|
||||
The non-transitive C-import visibility gate (`isVisible(.c_import_bare)`,
|
||||
`decl.zig:2249`) used to recognise only the legacy `#foreign` body shape; a bare
|
||||
`extern` fn (empty-block body + `extern_export == .extern_`) escaped the gate via
|
||||
the `body != foreign_expr → return true` arm and was caught only by the general
|
||||
`isNameVisible` gate — yielding the generic "not visible" wording instead of the
|
||||
C-specific "C function not visible; add #import" one. Now BOTH lib-less spellings
|
||||
route to `visibleOverEdges`, and a library-bound `extern LIB` (like `#foreign LIB`)
|
||||
stays unconditionally visible — so a future fn-decl `#foreign`→`extern` migration
|
||||
is byte-identical at this gate. New cross-module example **1228**
|
||||
(`examples/1228-ffi-extern-c-non-transitive`, main → b → c) pins the equivalence:
|
||||
referencing c's lib-less `#foreign` AND `extern` twins transitively both produce
|
||||
the identical C-specific diagnostic. Suite green (644 corpus / 444 unit, 0 failed).
|
||||
**Empirical finding** (probe, not yet acted on): the bare-extern twin was NEVER a
|
||||
silent visibility hole — the general `isNameVisible` gate already denied it; only
|
||||
the *diagnostic wording* diverged. The fix aligns the wording + gate ownership.
|
||||
|
||||
### Prior: Phase 5.0 (global path) (`refactor` lock, commit `e5ddfbe`) — **PART B STARTED.**
|
||||
First of the four `#foreign` parser paths migrated onto the extern AST: the
|
||||
data-global form `name : T #foreign [lib] ["csym"];` now builds the same
|
||||
extern-named `VarDecl` (`is_extern`/`extern_lib`/`extern_name`) that postfix
|
||||
`extern` already produces, instead of `is_foreign`/`foreign_lib`/`foreign_name`.
|
||||
Behavior-preserving — lowering coalesces both forms identically
|
||||
(`decl.zig:1119,1127,1141`), so zero snapshot churn. The fn-decl, const-with-type,
|
||||
and runtime-class `#foreign` paths still build the legacy AST.
|
||||
|
||||
### Prior: Phase 4 (green) — **PHASE 4 COMPLETE → PART A DONE; GATE A→B LOCKED.** Four pieces:
|
||||
(1) **GATE A→B unit test** (`lower.test.zig`, `lowerSrcToIr` helper + "GATE A→B" test) —
|
||||
asserts `#foreign` and `extern`/`export` lower to byte-identical printed IR for a sample
|
||||
fn, data global, and Obj-C runtime class. This is the hard gate: Part B may not start
|
||||
migrating `#foreign` until it's green. Verified live (negative-probe: mutating one side
|
||||
fails the assertion). (2) **Diagnostic — `#foreign` + postfix conflict** (1174): prefix
|
||||
`#foreign` combined with postfix `extern`/`export` on an aggregate is now a clean parse
|
||||
error (was a confusing internal "compiler bug" during class synthesis). (3) **Diagnostic
|
||||
— `extern`+`export` mutual exclusion** (1175): both keywords on one fn decl is a clean
|
||||
error (was bare "expected ';'"). (4) **Docs**: `specs.md` + `readme.md` document the three
|
||||
`extern`/`export` axes (fns, globals, aggregates) alongside `#foreign` (which stays
|
||||
documented until the Part B cutover). Suite green (643 corpus / 444 unit, 0 fail).
|
||||
NOTE: `extern`+`callconv` redundancy needs no diagnostic — `callconv(.c) extern` is a
|
||||
harmless dup (both `.c`), and any non-`.c` callconv already errors on its own.
|
||||
|
||||
### Prior: Phase 3.1 (green) — **PHASE 3 COMPLETE.** Postfix `extern`/`export` on `#objc_class`/
|
||||
`#jni_class` aggregates fully works. `parseForeignClassDecl` now parses an optional
|
||||
`extern`/`export` modifier in the slot **between** the `("X")` directive args and the `{`
|
||||
body (`parser.zig:~1409`): `extern`→`is_foreign_eff = true` (reference an existing runtime
|
||||
class, == legacy `#foreign`); `export`→`is_foreign_eff = false` (define + register a new sx
|
||||
class, == bare `#objc_class` with no `#foreign`). The modifier maps straight onto the same
|
||||
`is_foreign` decision the prefix `#foreign` already fed the node, so **no `objc_class.zig`/
|
||||
lowering change was needed** — the new surface reuses the existing reference-vs-define path.
|
||||
Examples: **1348** (objc `extern` import, dispatches `NSObject.alloc().init()` → green via
|
||||
JIT), **1349** (objc `export` defined class, `SxBar.alloc()`/`bump`/`get` → `counter: 2`),
|
||||
**1426** (jni `extern` import, parse-only `parse-only ok`). Suite green (641 corpus / 443
|
||||
unit, 0 fail).
|
||||
|
||||
### Prior: Phase 2.2 (green) — **PHASE 2 COMPLETE.** `export` (define + expose) fully works:
|
||||
external linkage + C ABI + no sx ctx + force-lowered root + optional `"csym"` rename.
|
||||
All four export-gap conditions filled in `decl.zig`: (i) `.external` linkage for
|
||||
`extern_export == .export_` on both define paths (`lowerFunctionBodyInto`,
|
||||
`lowerFunction`); (ii) C-ABI promotion on the define paths + `declareFunction` stub cc;
|
||||
(iv) `funcWantsImplicitCtx` returns false for any non-`.none` modifier; **force-lower**:
|
||||
`export` fns are lowering roots in `lowerMainAndComptime` (else an uncalled export fn
|
||||
stays a bodiless `declare`); (iii) `export … "csym"` declares the stub under the C name
|
||||
+ `lazyLowerFunction` promotes the body into it via `foreign_name_map`. Examples **1226**
|
||||
(bare export, C calls `sx_square` → 37/82) + **1227** (`export "triple_c"`, C calls
|
||||
`triple_c` → 22) green via the new **AOT corpus mode**. Suite green (638 corpus / 443
|
||||
unit, 0 fail).
|
||||
|
||||
**AOT corpus mode + run_examples.sh retired.** C→sx-by-name can't link under the
|
||||
corpus's `sx run` JIT mode (a JIT-resident symbol is invisible to a dlopen'd C dylib's
|
||||
flat-namespace lookup), so an `expected/<name>.aot` marker switches an example to a
|
||||
`sx build` + execute flow. The standalone `tests/run_examples.sh` was deleted —
|
||||
`zig build test` is now the sole corpus runner (verify-step.sh + CLAUDE.md updated).
|
||||
|
||||
## 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` (PHASE 1) + `export` (PHASE 2) FULLY WORKING.**
|
||||
extern: functions — bare (`f :: (…) -> R extern;`) AND renamed (`extern [LIB] "csym"`);
|
||||
data globals — bare + renamed. export: functions — bare (`f :: (…) -> R export {…}`)
|
||||
AND renamed (`export "csym"`); external linkage, C ABI, no ctx, force-lowered as a root.
|
||||
All behavior-equivalent to the matching `#foreign` form. `extern_lib` is parsed + stored
|
||||
but is a *reference* only — actual linking stays the `#library`/build-flag axis.
|
||||
**Aggregates DONE (Phase 3)**: postfix `extern`/`export` on `#objc_class`/`#jni_class`
|
||||
(reference vs define+register). **Interplay/diagnostics/docs DONE (Phase 4)** + the
|
||||
**A→B GATE IS LOCKED** (`#foreign` ≡ `extern`/`export` IR for fn/global/class). **PART A
|
||||
COMPLETE.** 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
|
||||
(extern bare fn), 1224 (extern fn rename), 1225 (extern bare global), 1226 (export bare fn,
|
||||
AOT), 1227 (export fn rename, AOT), 1348 (objc extern class), 1349 (objc export class), 1426
|
||||
(jni extern class), 1174/1175 (interplay diagnostics).
|
||||
|
||||
## Next step
|
||||
**NONE — the FFI-linkage stream is COMPLETE.** `extern`/`export` fully replace
|
||||
`#foreign`; the keyword is rejected; zero `foreign` remains in the gated tree (Parts
|
||||
A + B, Phases 0–9 all done; the 9.4 gate passes). This stream can be archived.
|
||||
|
||||
Follow-ups (both DONE 2026-06-15, post-stream polish):
|
||||
- ✅ Added `extern`/`export` to the editors/vscode tmLanguage keyword list as a
|
||||
`storage.modifier.sx` pattern (`editors/vscode/syntaxes/sx.tmLanguage.json`).
|
||||
- ✅ Dropped the vestigial `RuntimeClassPrefix.is_extern` field +
|
||||
`parseRuntimeClassDecl`'s `is_extern` param (always-false dead path; the postfix
|
||||
`extern`/`export` keyword is the sole reference-vs-define decider). Suite green
|
||||
(644 corpus / 442 unit, 0 failed).
|
||||
|
||||
--- (historical: the finish-Phase-9 plan, now done) ---
|
||||
**PART B — finish Phase 9: example FILENAME renames + `issues/*.md` + 9.0/9.4.**
|
||||
(All `src/` identifiers + AST node + all comments/docs/example-comments are DONE.)
|
||||
|
||||
0. **FIRST: commit the staged `examples/` comment purge** (a classifier outage blocked
|
||||
the commit; changes are `git add`ed). Message: "refactor(ffi-linkage): Phase
|
||||
9.3-examples — purge 'foreign' from example .sx comments".
|
||||
1. **Example filename rename** (git-mv step, snapshot-careful): rename the `*-foreign*`
|
||||
example files to extern/runtime names and update every `#import`/`#include`/`#source`
|
||||
ref + the `expected/<name>.*` companions. Families: `0729-modules-flat-same-name-foreign`
|
||||
(+ `/a.sx`,`/b.sx` dir), `1205-ffi-foreign-global`(+`-helper`), `1207-ffi-foreign-global-from-helper`,
|
||||
`1216-ffi-08-foreign-in-method`(+`.h`/`.c`), `1217-ffi-09-foreign-result-chain`(+`.h`/`.c`),
|
||||
`1218-ffi-foreign-cvariadic`(+`.c`), `1219-ffi-foreign`, `1306-ffi-objc-foreign-class-chained-dispatch`,
|
||||
`1318-ffi-objc-property-foreign`. ⚠ A renamed file with an `.ir`/`.stderr` snapshot that
|
||||
echoes its own path will need that snapshot regenerated (intentional). Pick new names
|
||||
that drop "foreign" (e.g. `…-extern-global`, `…-extern-in-method`, `…-runtime-class-chained-dispatch`).
|
||||
NOTE: keep `1176-diagnostics-foreign-removed.sx` name (it's the rejection test — fine to keep "foreign").
|
||||
2. **issues/*.md** (~20) — rewrite writeup prose `#foreign`/`foreign`→`extern`/`runtime-class`.
|
||||
2b. **`docs/*.md`** — ALSO in the gate scope (was missed; the gate areas are now
|
||||
`src/ library/ examples/ issues/ docs/ specs.md readme.md CLAUDE.md`). `docs/debugger.md`
|
||||
referenced the renamed `callForeign` (fixed → `callExtern`, UNCOMMITTED with the staged
|
||||
batch); sweep all of `docs/` for stale renamed-identifier refs + `foreign` prose.
|
||||
3. **9.0 surface decision — RATIFIED (user, 2026-06-15): DELETE the `hash_foreign` token.**
|
||||
The user explicitly flagged token.zig:121 + lsp/server.zig:1693 "this also needs to
|
||||
go" — total purge, accept `#foreign`→generic error (no friendly migration hint). This
|
||||
is the LAST src change; it is load-bearing → needs a build + test + 1176 regen (do it
|
||||
when mutating Bash is back). Steps:
|
||||
- token.zig: remove `hash_foreign` enum (121).
|
||||
- lexer.zig: remove the `.{ "#foreign", Tag.hash_foreign }` map entry (91), drop
|
||||
`#foreign` from the directive-list comment (72), DELETE the `lex hash_foreign` test
|
||||
(626-631, incl. `#foreignx`).
|
||||
- parser.zig: remove the 4 `self.current.tag == .hash_foreign` rejection sites (268
|
||||
caller / 327 / 419 / 2024) + their messages, AND the 2 lookahead refs (`hasFnBody…`
|
||||
~3658 + ~3676). ⚠ Decide what `#foreign` lexes to with no keyword entry (likely an
|
||||
error/unknown-directive token) and confirm the parser surfaces a sane error.
|
||||
- lsp/server.zig: remove the `.hash_foreign,` arm (1693).
|
||||
- **1176-diagnostics-foreign-removed**: its expected stderr is the now-deleted
|
||||
"`#foreign` has been removed…" message → it WILL change. Regen 1176's snapshot to
|
||||
whatever the generic post-deletion error is (intentional), OR delete 1176 entirely
|
||||
(its purpose — a friendly rejection — no longer exists). Recommend: keep 1176 as a
|
||||
"`#foreign` is no longer a directive" regression, regen its snapshot. NOTE: after
|
||||
this, 1176 may still contain `#foreign` in its SOURCE (the rejected token) — that's
|
||||
the only legitimately-remaining `foreign` in `.sx`, OR rename/rework it to avoid even
|
||||
that if the gate must be absolute.
|
||||
4. **9.4 gate** — `grep -rniIE 'foreign'` over `.sx` + docs + `src/` → 0 (no keep-list
|
||||
left except possibly 1176's source token + `SQLITE_CONSTRAINT_FOREIGNKEY` + vendored C).
|
||||
|
||||
--- (historical: the prose-purge plan, now mostly done) ---
|
||||
**PART B — finish Phase 9: the COMMENT / DOC / issues text purge** (all `src/`
|
||||
identifiers + the AST node are already done; remaining is prose). Lower-risk than the
|
||||
renames (text only, mostly snapshot-neutral) but needs per-instance reading — NOT a
|
||||
blind sed. Footprint: `src/` ~205 (all comments now), `examples/*.sx` ~100 comments,
|
||||
`issues/*.md` ~20 files, docs (specs/readme/CLAUDE).
|
||||
|
||||
Order:
|
||||
1. **src/ comments** (~200) — reword `foreign`→`extern`/`runtime-class` to match the
|
||||
renamed identifiers. KEEP: the rejection-message string, the `hash_foreign` token +
|
||||
its `#foreignx`/`lex hash_foreign` test, and any comment that legitimately explains
|
||||
the cutover (it must name `#foreign` to say it's removed). The ast.zig FnDecl comment
|
||||
still says "mirroring `#foreign LIB "csym"` (foreign_lib/foreign_name)" — reword.
|
||||
2. **examples/*.sx comments** — the deferred provenance comments (full list in the prior
|
||||
Next-step revision / git). ⚠ Many CONTRAST `#foreign` vs `extern` — reword to stay
|
||||
coherent. ⚠ `1219-ffi-foreign.sx` prints `"foreign-rename: {}"`/`"=== 15. Foreign ==="`
|
||||
to STDOUT — changing those regens its snapshot (intentional). `1176`/`1216` legitimately
|
||||
discuss `#foreign` removal — keep minimal `#foreign` mentions where the test IS about it.
|
||||
3. **issues/*.md** (~20) — rewrite writeup prose to `extern`/`export`/`runtime-class`.
|
||||
4. **docs** — specs.md (rename "Foreign Function Interface" heading → "C Interop"; the
|
||||
`#import c` "foreign declarations" prose; the comptime "foreign function calls" line;
|
||||
§3344 "foreign code can't observe the error channel"), readme.md (211-212 the `#import
|
||||
c` exemption prose), CLAUDE.md (host_ffi `#foreign("c")` ref → `extern`; "foreign calls").
|
||||
5. **9.0 surface decision** (recommend KEEP `hash_foreign` token + rejection for a good
|
||||
deprecation — then it + the message + 1176 + `#foreignx` are permanent gate-exempt keeps).
|
||||
6. **9.4 gate** — `grep -rniIE 'foreign'` over `.sx` + docs + `src/` minus the keep-list → 0.
|
||||
KEEP-LIST (gate-exempt): `SQLITE_CONSTRAINT_FOREIGNKEY` + SQLite API names, vendored
|
||||
`library/vendors/sqlite/c/*`, the `hash_foreign` token + `#foreignx` test + rejection
|
||||
message string, `1176-diagnostics-foreign-removed.sx` (rejection test must contain it).
|
||||
**Gate (scoped per user 2026-06-15):** `grep -rniIE 'foreign'` → 0 across `.sx` files,
|
||||
all docs, and our `src/` Zig — EXCLUDING the legitimate keeps listed in Last completed
|
||||
step (SQLite API names, vendored C, the rejection test/message + `hash_foreign` token).
|
||||
|
||||
Remaining, in suggested (dependency-safe) order:
|
||||
1. **9.1d — eliminate `foreign_expr`** (last linkage item): migrate `c_import.zig`
|
||||
auto-synthesis to build the extern shape instead of a `foreign_expr` body (the Phase
|
||||
5.0 fn-body flip applied to auto-synth), then delete the `foreign_expr` AST node +
|
||||
`ForeignExpr` + all readers (25). Snapshot-neutral; verify full corpus (the `#import c`
|
||||
examples 1215/1216/1217 + sqlite 1624 exercise it).
|
||||
2. **9.2 — runtime-class family rename → `Runtime*` (Decision 5).** The BIG one, do as
|
||||
small per-identifier commits with `zig build` after each (snapshot-neutral). Targets
|
||||
(counts): `ForeignClassDecl`(65)→`RuntimeClassDecl` · `foreign_path`(62)→`runtime_path`
|
||||
· `foreign_class_map`(44) · `current_foreign_class`(34)/`_method` · `ForeignMethodDecl`(31)
|
||||
· `foreign_class_decl`(30) · `foreign_expr`-gone-by-now · `ForeignClassMember`(20) ·
|
||||
`ForeignFieldDecl`(15) · `ForeignClassDecl.is_foreign`(the live one)→e.g. `is_reference`
|
||||
· `parse/tryParseForeignClass*` · `lowerForeign{Method,Static}Call` ·
|
||||
`findForeign*InChain` · `resolveForeign*` · `register*ForeignClass*` · `*ForeignRefs` ·
|
||||
`ForeignRuntime` · `current_foreign_class`/`_method`. ⚠ COUPLED .sx↔.zig hook names:
|
||||
`jni_main_foreign_path_at`/`jni_main_foreign_paths`/`hookJniMainForeignPathAt`/
|
||||
`foreignPathToJavaName`/`splitForeignPath` span build.sx + bundle.sx + compiler_hooks.zig
|
||||
+ specs.md (2975/3049) — rename all four sites together.
|
||||
3. **9.x-src-comments** — the ~200 bare-`foreign` comments in `src/` (rename last, since
|
||||
many reference identifiers that 9.1d/9.2 rename; do AFTER those so the comment text
|
||||
matches the new names).
|
||||
4. **9.3-examples comments** — the deferred `.sx` provenance comments (0716, 0729, 1205/
|
||||
1207/1216/1218/1219/1220, 1223-1231, 1306/1308/1315/1318/1320/1321/1331/1332/1348/1349,
|
||||
1412/1414/1417/1418/1419/1426, 0117/0415, 1140/1141/1125, issues/0030.sx). ⚠ Many
|
||||
CONTRAST `#foreign` vs `extern` ("no `#foreign`, no `#library`") or reference renamed
|
||||
internals — rewrite each to stay coherent (NOT blind sed). ALSO: `1219-ffi-foreign.sx`
|
||||
prints `"foreign-rename: {}"` to STDOUT — changing it regens the snapshot (intentional).
|
||||
5. **9.3-issues** — `issues/*.md` writeups (~20 files) → rewrite `#foreign`/`foreign` to
|
||||
`extern`/`export`/`runtime-class` per the renames.
|
||||
6. **9.3-docs** — specs.md (12: rename "Foreign Function Interface" heading → "C Interop";
|
||||
the `#import c` "foreign declarations" prose; the jni_main_foreign_path_at refs with #2),
|
||||
readme.md (2), CLAUDE.md (2: host_ffi `#foreign("c")` ref + "foreign calls").
|
||||
7. **9.0 surface decision** — keep `hash_foreign` token + rejection (recommended: good
|
||||
deprecation) vs delete it. If kept, the token + the rejection-message string + 1176 are
|
||||
permanent legitimate keeps; the gate excludes them.
|
||||
8. **9.4 gate** — `grep -rniIE 'foreign'` over the gated set minus the keep-list → 0.
|
||||
- **6.2 verification note (carry forward):** the `platform/` runtime modules
|
||||
(uikit/android/android_jni) are NOT compiled by any marker'd host corpus test — verify
|
||||
future platform-adjacent migrations via direct `sx ir` on importers (1610/1606 compile
|
||||
uikit on host) or import probes, not the corpus alone.
|
||||
- **Phases 6–7** (`refactor` batches, empty snapshot diff per batch): migrate the
|
||||
stdlib + examples from `#foreign` spelling to `extern`. Because the AST is already
|
||||
unified, this is a pure SOURCE rename (`… #foreign LIB "sym";` → `… extern LIB "sym";`
|
||||
for fns; the global/const forms similarly), and IR/output must be byte-identical per
|
||||
batch. NOTE: `c_import.zig` auto-synthesis (`#import c {#include}`) still BUILDS
|
||||
`foreign_expr` bodies internally — that's a compiler-internal path, migrated separately
|
||||
(likely Phase 8/9 area), not a source-spelling change.
|
||||
- **Then Phase 8** (cutover: hard-reject the `#foreign` keyword) and **Phase 9** (purge
|
||||
all `foreign` identifiers — needs Decision 5 [done, `Runtime*Class*`] + Decision 6
|
||||
[open, historical carve-out]).
|
||||
|
||||
**Watch items carried forward:**
|
||||
- `c_import.zig:262` auto-synthesis still emits `foreign_expr` — both shapes coexist
|
||||
until that path is migrated; keep every `body.data == .foreign_expr` reader dual
|
||||
(checked exhaustively this stream).
|
||||
- const-with-type `#foreign` parser path (`parser.zig:316`) is still on `foreign_expr`
|
||||
but DEAD (registers no const); migrate or delete it at the Phase 8 cutover.
|
||||
- The `decl.zig:2055` "foreign symbol … already bound" dedupe message is keyword-neutral
|
||||
and fires for both forms — no churn, but reword to "extern" at cutover for consistency. Route the fn-decl `#foreign` path so a
|
||||
`#foreign` fn builds the SAME extern AST that postfix `extern` already produces,
|
||||
instead of a `foreign_expr` body. This is the highest-value path (the bulk of
|
||||
`#foreign` usage). Key sub-questions to resolve before/while routing:
|
||||
- The `foreign_expr` node carries `library_ref` + `c_name`; the `extern` fn carries
|
||||
`extern_export = .extern_` + `extern_lib` + `extern_name` on the FnDecl with an
|
||||
empty-block body. Migration = the parser's fn-body `#foreign` arm
|
||||
(`parser.zig:~2062`) builds the extern shape (set `extern_export`, map
|
||||
`library_ref→extern_lib`, `c_name→extern_name`) rather than a `foreign_expr`.
|
||||
- Lowering ALREADY coalesces the two at every fn site checked this stream
|
||||
(`decl.zig` 2088/2124/2132/2156/2324/2531 read `is_foreign OR extern_export`),
|
||||
and the two prereq gates (visibility `decl.zig:2249`, variadic `decl.zig:2097` +
|
||||
`pack.zig:302`) now do too — so the migration should be behavior-preserving with
|
||||
ZERO snapshot churn. VERIFY with the A→B gate test (`lower.test.zig`) + a full
|
||||
`zig build test` after routing; any churn means a site still reads `foreign_expr`
|
||||
structurally and must be coalesced first.
|
||||
- ⚠ This ALSO migrates the **const-with-type** path implicitly IF it shares the same
|
||||
`foreign_expr`→extern reshape (it builds `const_decl{value=foreign_expr}`). Decide:
|
||||
reshape the const path's value node alongside, or leave the dead const path on
|
||||
`foreign_expr` until Phase 8 cutover. The const path is dead (see findings below),
|
||||
so leaving it is acceptable; but the parser arm is shared-ish — check whether the
|
||||
fn-body arm change touches it.
|
||||
- Cadence: because the migration is behavior-preserving (no churn), it's a single
|
||||
`refactor`/lock commit (like the 5.0 global-path commit `e5ddfbe`), NOT an
|
||||
xfail→fix pair.
|
||||
|
||||
**Investigation findings (this session — reorder the remaining paths):**
|
||||
- **const-with-type** (`parser.zig:316`, `name :: type_expr #foreign …`) is a
|
||||
**DEAD path**: it builds `const_decl{value = foreign_expr}`, but
|
||||
`registerTypedModuleConst` (`decl.zig:848-851`) bails on a `foreign_expr` value
|
||||
(`else => return`), so it registers no const and emits no symbol — a probe
|
||||
(`g_abs :: FP #foreign "abs";`) returns `unresolved 'g_abs'` at the use site, and
|
||||
the form is used NOWHERE in `library`/`examples`/`issues`. Its migration target is
|
||||
ambiguous because the `foreign_expr` value node is SHARED with the fn-decl path,
|
||||
which isn't migrated yet. **Decision (user, 2026-06-14): defer it — migrate it
|
||||
alongside the fn-decl path once `foreign_expr`'s extern shape is decided.** The
|
||||
checkpoint's old "lowest-risk, route to the extern-named shape" note is wrong: the
|
||||
"confirm the value-node lowering path coalesces" gate can't be met (nothing lowers it).
|
||||
- **runtime-class prefix** (`parser.zig:~1351`, `#foreign #objc_class/#jni_class`) is
|
||||
**ALREADY coalesced**: both prefix `#foreign` and postfix `extern` feed the single
|
||||
`is_foreign_eff`→`is_foreign` field on `foreign_class_decl` (`parser.zig:1421-1432`),
|
||||
so there is NO Phase 5.0 AST change for it — only the Phase 9.2 `Runtime*Class*`
|
||||
rename remains. Drop it from the Phase 5.0 path list.
|
||||
|
||||
So Phase 5.0's real remaining work collapses to: the fn-path variadic prereq, then
|
||||
the fn-decl `#foreign` body-marker migration. const-with-type + runtime-class need
|
||||
no standalone Phase 5.0 commit.
|
||||
|
||||
Then Phase 5.1 (`lock`): unit test that `#foreign` and `extern` produce identical IR (the
|
||||
A→B gate already covers fn/global/class — extend or reuse `lowerSrcToIr`). Then Phases 6–7
|
||||
migrate stdlib + examples (empty snapshot diff per batch), Phase 8 cutover (hard-reject
|
||||
`#foreign`), Phase 9 total `foreign` purge.
|
||||
|
||||
**⚠ CONFIRM BEFORE PART B (Open decisions 5 & 6):** runtime-class rename target
|
||||
(`Runtime*Class*` recommended vs `Extern*Class*`) and the historical carve-out (keep
|
||||
`issues/*.md` provenance, gate live tree only — recommended). These decide Phase 9 renames;
|
||||
the plan says confirm before Phase 9, but worth raising with the user before sinking Part B
|
||||
effort. **Also pick up the two Deferred items below at the start of Part B** (the
|
||||
visibility-gate equivalence in particular needs a cross-module example).
|
||||
|
||||
**FUTURE MILESTONE — C→sx-by-name in JIT (`sx run`).** Investigated this session
|
||||
(user-requested spike, RESOLVED feasible-but-blocked). Adding the C `#source` objects
|
||||
directly into the ORC JITDylib (`LLVMOrcLLJITAddObjectFile`) instead of dlopen'ing a
|
||||
dylib makes C↔sx cross-references resolve both ways in one link domain — proven: a
|
||||
~20-line spike ran 1226 via `sx run` (37/82) and all 13 existing `#source` FFI examples
|
||||
still passed. BLOCKER: C objects using `_Thread_local` (the return-trace runtime
|
||||
`sx_trace.c`) SIGABRT under JITLink — MachO thread-local-variable handling needs the ORC
|
||||
`MachOPlatform` set up (the bare `LLVMOrcCreateLLJIT` default doesn't), and C
|
||||
constructors/`__mod_init_func` won't run without ORC initializer support. 42 `errors-*`
|
||||
examples crashed in the spike. A real impl needs a C++ shim in `llvm_shim.c`
|
||||
(`LLJITBuilder().setObjectLinkingLayerCreator(...)` + `MachOPlatform::Create`) — its own
|
||||
milestone, NOT Phase 2/3 scope. The AOT `.aot`-marker corpus mode is the pragmatic test
|
||||
path and works today. Spike fully reverted (target.zig/main.zig at HEAD).
|
||||
|
||||
**Deferred (carry into Part B):** (a) ~~docs~~ — DONE in Phase 4 (`specs.md`/`readme.md`
|
||||
document `extern`/`export`; `#foreign` stays until the Part B cutover); (b) ~~visibility-gate
|
||||
equivalence~~ — **DONE** (`717c35d`/`7d8ba1a`): the `c_import_bare` gate now polices a
|
||||
lib-less `extern` fn identically to its lib-less `#foreign` twin (same C-specific
|
||||
diagnostic); a library-bound `extern LIB` stays unconditionally visible. Locked by the
|
||||
cross-module example 1228. (Empirical: the bare-extern twin was never a silent hole — the
|
||||
general `isNameVisible` gate already denied it; only the diagnostic wording diverged.)
|
||||
|
||||
## Open decisions
|
||||
Part A ratified (bare / postfix / `⇒ callconv(.c)` / lib-separate). Part B:
|
||||
- **Decision 5 RATIFIED** (user, 2026-06-14): runtime-class rename target = `Runtime*Class*`
|
||||
(object-model axis, not linkage). Drives the Phase 9.2 identifier renames.
|
||||
- **Decision 6 RATIFIED** (user, 2026-06-15): **PURGE EVERYTHING** — the Phase 9.4 gate is
|
||||
absolute, including `issues/*.md` writeups (NOT the recommended keep-provenance default).
|
||||
Every `#foreign`/`foreign` reference in the gated tree (`src/ library/ examples/ issues/
|
||||
specs.md readme.md CLAUDE.md`) is rewritten to `extern`/`export`; provenance lives in git
|
||||
history + `(Regression issue NNNN)` notes, not the keyword spelling.
|
||||
- **Decision 7 RATIFIED** (user, 2026-06-15): **accept the churn** — `#foreign`-spelled
|
||||
decls produce `extern`-worded diagnostics; example 1620 regenerated (only snapshot moved).
|
||||
Aligns with Part B's extern-only end state; the interim oddity is cosmetic and removed at
|
||||
the Phase 8 cutover. Landed in the fn-body flip `6b94bb6`. (Original framing below.)
|
||||
— interim diagnostic wording for `#foreign`-spelled decls (gated the fn-body flip). Once the flip lands, a `#foreign`-spelled fn builds the extern AST, so any
|
||||
diagnostic that reads the unified AST can no longer tell the user wrote `#foreign` vs
|
||||
`extern`. Concretely, example 1620's lib-ref error flips "#foreign library…" →
|
||||
"extern library…". Options: **(A, recommended)** accept the narrow churn — regen 1620 as
|
||||
intentional; it aligns with Part B's `extern`-only end state and the interim oddity
|
||||
(`#foreign` source → "extern" message) is cosmetic and short-lived (Phase 8 cutover
|
||||
removes `#foreign`). **(B)** retain a one-bit surface marker on `FnDecl` (`wrote_foreign`)
|
||||
so interim diagnostics stay keyword-accurate (zero churn, small extra plumbing, marker
|
||||
deleted at cutover). Affects only diagnostic wording — IR/behavior identical either way.
|
||||
|
||||
## Log
|
||||
- (9.0 + 9.3 + 9.4) **PHASE 9 COMPLETE — STREAM DONE; 9.4 GATE PASSES.** Deleted the
|
||||
hash_foreign token (9.0, `dfae690`); purged all `foreign` comments incl. capital-F
|
||||
(src/examples/docs/editors); renamed 10 `*-foreign*` example files + dedup'd 1218
|
||||
(`b52d424`); rewrote 20 issues/*.md + renamed 0043 (`b9cfe25`). Gate: zero `foreign`
|
||||
in the gated tree except `SQLITE_CONSTRAINT_FOREIGNKEY` + vendored sqlite c/. Suite
|
||||
green (644/443). User flagged several leftover areas mid-purge (docs/, editors/,
|
||||
capital-Foreign comments, the token) — all addressed.
|
||||
- (9.3 src capital-Foreign) Fixed the case-sensitivity gap — my earlier src verify grep
|
||||
was case-sensitive, missing ~21 capital `Foreign`/`FOREIGN` comments (Foreign-class→
|
||||
Runtime-class, Foreign path→Runtime path, Foreign decls→Extern decls, FOREIGN function→
|
||||
extern function, etc.) across calls/inst/ffi_objc/jni_descriptor/emit_llvm/c_import/
|
||||
lower.* /ops.zig. All reworded via Edit (comments only — no build impact). UNCOMMITTED
|
||||
(mutating Bash blocked by a classifier outage). After this, src `foreign` = ONLY the
|
||||
`hash_foreign` token machinery + 4 rejection messages (the 9.0-delete targets).
|
||||
- (9.0 RATIFIED) User: DELETE the hash_foreign token (total purge). Pending build+regen.
|
||||
- (9.3 text purge) Purged `foreign` from all `src/` comments (`dc51c4b`), specs/readme/
|
||||
CLAUDE (`e99383f`), and all example .sx comments (STAGED, commit pending a classifier
|
||||
outage). Fixed 2 user-facing diagnostics (type-annotation error, Android jni_main help).
|
||||
1219 stdout labels Foreign→Extern (regen). Suite green (646/444). Remaining: example
|
||||
FILENAMES + issues/*.md + the 9.0 token decision + 9.4 gate.
|
||||
- (9.2a-d) **RUNTIME-CLASS IDENTIFIER PURGE COMPLETE** (Decision 5 → `Runtime*`).
|
||||
9.2a types (`3354446`), 9.2b fns+state+`is_extern` flag (`5c8af6e`, fixed `a15a868`
|
||||
per user: reuse `is_extern` not new `is_reference`), 9.2c extern-ref validators →
|
||||
`Extern` (`d27be42`), 9.2d `foreign_path`→`runtime_path` coupled across the build-hook
|
||||
boundary + 37 `.ir` regens (`8cca3b9`). `src/` now has ZERO `foreign` identifiers
|
||||
(only comments + the kept token/message remain). Suite green throughout.
|
||||
- (9.1d) Eliminated the `foreign_expr` AST node — migrated `c_import.zig` auto-synth to
|
||||
the extern shape, deleted the node + all readers. `refactor` `7ffdc7d`.
|
||||
- (9.1c) Deleted dead `VarDecl.is_foreign`/`foreign_lib`/`foreign_name` (global `#foreign`
|
||||
rejects → write-dead); 3 decl.zig readers simplified to `vd.extern_name`/`vd.is_extern`.
|
||||
Snapshot-neutral; suite green (646/444). `refactor` `cd14794`.
|
||||
- (9.1b) "foreign symbol already bound" diagnostic + resolveFuncByName panic → "extern
|
||||
symbol"; intentional 1172 regen. Suite green. `refactor` `b78e7dd`.
|
||||
- (9.1a) **PHASE 9 STARTED.** 5 collision-free linkage renames (callForeign→callExtern,
|
||||
marshalForeignArg, dedupeForeignSymbol, foreign_name_map→extern_name_map,
|
||||
is_foreign_c_api). Snapshot-neutral; suite green. `refactor` `b838f63`. Decision 6
|
||||
scoped by user: purge `.sx` + docs + our `src/` Zig, keep legitimate hits (SQLite API
|
||||
names, vendored C, the rejection test/message + hash_foreign token).
|
||||
- (8.1 cutover) **PHASE 8 COMPLETE.** Parser hard-rejects `#foreign` at all 4 sites
|
||||
(const/global/fn-body via `self.fail`; runtime-class via `self.failAt` at the caller,
|
||||
new helper); greens xfail 1176. Deleted obsolete 1174 + 1620, the GATE A→B test +
|
||||
`lowerSrcToIr` helper; converted the in-source parser test to postfix `extern`;
|
||||
`extern_export` → `const`. specs.md + readme.md drop `#foreign`. Suite green
|
||||
(646/444). `feat!` `3811311`.
|
||||
- (8.0 xfail) Added `1176-diagnostics-foreign-removed.sx` pinning the desired rejection.
|
||||
RED (still accepted). `test`/xfail `8180faf`.
|
||||
- (8 pre-cutover) Migrated the 4 multi-file example companions Phase 7 missed
|
||||
(0729/a+b, 1617/c, 1623/mod). `refactor` `d132aab`.
|
||||
- (8 pre-cutover) Migrated keyword-neutral diagnostics 1172 (decl→extern, message stays
|
||||
internal "foreign symbol") + 1228 (→ two foreign-free extern symbols c_abs_one/_two),
|
||||
intentional snapshot regens reviewed. `refactor` `720556b`.
|
||||
- (8 pre-cutover) Migrated the 7 identity `ffi-foreign-*` test decls to extern/export
|
||||
(decls only; comments left for Phase 9.3). `refactor` `2cce6a3`.
|
||||
- (7.4 stragglers) **PHASE 7 MIGRATABLE WORK COMPLETE.** Migrated 16 fn/global examples
|
||||
(0415/0602/0603/1024/1025/1605/1607-1609/1611/1616/1619/1622/1628/1635/1636) `#foreign`→
|
||||
`extern`; 1607/1608/1616 (unmarked) verified by `sx ir` probes. 24-file keep-list remains
|
||||
by design (deferred to Phase 8). Suite green (647/444). `refactor` `1a8991a`.
|
||||
- (7.3 14xx) Migrated 13 jni examples (1410-1419/1423/1424/1425). 1417 (all-runtimes) hit
|
||||
a parse-error trap: a `#(objc|jni)_class`-only regex left `extern` in PREFIX position on
|
||||
`#jni_interface`/`#objc_protocol`/`#swift_*` lines → fixed with the GENERAL
|
||||
`#foreign #(\w+)("X") {`→`#$1("X") extern {` rewrite (all such directives accept the
|
||||
postfix modifier, probed). Kept 1426 (comment-only). Suite green. `refactor` `2888f6f`.
|
||||
- (7.2 13xx) Migrated 18 obj-c examples (1308/1311-1321/1341-1347): prefix→postfix import
|
||||
classes + fn markers. Kept identity 1306/1318, comment-only 1332/1348/1349. No 13xx
|
||||
snapshot asserts on foreign. Suite green. `refactor` `a68f7c2`.
|
||||
- (7.1 12xx) **PHASE 7 STARTED.** Migrated 12 incidental plain-C examples
|
||||
(1200/1206/1209-1215/1220/1221/1222) `#foreign`→`extern`; output byte-identical,
|
||||
empty snapshot diff, corpus-validated. Established the keep-list policy (see Last
|
||||
completed step): kept 1172/1174/1620/1228 + ffi-foreign-* (1205/1207/1216/1218/1219)
|
||||
+ comment-only 1223/1229/1230/1231 for Phase 8. Suite green (647/444). `refactor`
|
||||
`731fb8d`.
|
||||
- (6.5 gpu) **PHASE 6 COMPLETE.** Migrated `gpu/gles3.sx` + `gpu/metal.sx` (3 sites);
|
||||
`library/` now `#foreign`-free (`grep -rln '#foreign' library/` → 0). Verified
|
||||
byte-identical `sx ir` on importers 1610/1606. Suite green (647/444). `refactor`
|
||||
`32a7628`.
|
||||
- (6.4 ffi) Migrated `ffi/` objc/objc_block/raylib/sdl3/wasm (~51 sites): fn markers +
|
||||
objc.sx's 2 import classes (prefix→postfix `extern`). objc/objc_block validated by 50
|
||||
marked 13xx examples; raylib/ffi-sdl3/wasm by `sx ir` probes pre/post. Empty snapshot
|
||||
diff; suite green (647/444). `refactor` `666a2e2`.
|
||||
- (6.3 std) Migrated 11 `std/` modules (~60 sites): cli/core/fmt/fs/log/net.kqueue/
|
||||
process/socket/thread/time/trace. All fn-decl markers (bare / `libc`|`tlib` LIB ref /
|
||||
`libc "csym"` rename) → `extern …` + 2 comment mentions; no class forms. Host-corpus-
|
||||
exercised → empty snapshot diff validates. Suite green (647/444). `refactor` `59f90d2`.
|
||||
- (6.2 platform) Migrated `platform/` (uikit/android/android_jni/sdl3, ~64 sites):
|
||||
30 fn `#foreign;`→`extern;`, 34 import classes prefix `#foreign #objc/jni_class`→
|
||||
postfix `… extern {`, 4 defined `Sx*` objc classes → `… export {`. Behavior-
|
||||
preserving, empty snapshot diff. Verified byte-identical `sx ir` on uikit importers
|
||||
1610/1606 + sdl3 probe; android via identical 4-error dedup set (host-only module).
|
||||
Suite green (647/444). `refactor` `2cd5d7b`. NOTE: these runtime modules aren't in
|
||||
the marker'd host corpus — verified out-of-band.
|
||||
- (6.1 sqlite) **PHASE 6 STARTED.** Migrated `vendors/sqlite/sqlite.sx`: 97
|
||||
`… #foreign sqlib "csym";` fn decls → `extern sqlib "csym";` (+ line-9 comment).
|
||||
`extern_lib` references the `sqlib` `#import c` unit like `#foreign sqlib`; IR
|
||||
byte-identical, empty snapshot diff, example 1624 stdout unchanged. Suite green
|
||||
(647/444). `refactor` `410a52e`.
|
||||
- (5.1 gate annotate) **PHASE 5 COMPLETE.** Annotated the A→B gate header
|
||||
(`lower.test.zig`) to record that post-Phase-5.0 the fn/global `#foreign` paths
|
||||
build the same extern-named AST → cases 1/2 are structurally (not coincidentally)
|
||||
identical; the gate stays as a regression tripwire. Added fn-rename case 2b
|
||||
(`c_abs` → `"abs"`, `extern_name` axis), IR-identical per a `sx ir` probe.
|
||||
Test-only, no snapshot churn. Suite green (647/444). `test` `93e7b6f`.
|
||||
- (5.0 fn-body flip) **PHASE 5.0 PARSER ROUTING COMPLETE.** Flipped the fn-body
|
||||
`#foreign` parser arm (`parser.zig:~2062`) onto the extern AST (empty-block body +
|
||||
`extern_export = .extern_` + extern_lib/extern_name); `extern_export` made `var` so
|
||||
the body arm can route onto it. Updated the parser unit test to assert the extern
|
||||
shape. Behavior-preserving via the four prereqs; only example 1620's lib-ref message
|
||||
churned ("#foreign library"→"extern library", Decision 7, hand-edited). Suite green
|
||||
(647 corpus / 444 unit). `refactor` `6b94bb6`.
|
||||
- (5.0 prereq plain-free xfail) Added `1230-ffi-extern-same-name-authors` (two flat
|
||||
authors of `absval` via `extern libc "abs"`; the `extern` twin of `#foreign` 0729).
|
||||
RED — extern authors wrongly counted as ambiguous (646/1 fail). `test`/xfail `2706521`.
|
||||
- (5.0 prereq plain-free fix) `isPlainFreeFn`/`isPlainFreeFnDecl` now also exclude
|
||||
`extern_export == .extern_` (external C symbol, no sx body; name-keyed first-wins like
|
||||
`#foreign`); `export` stays plain-free. 1230 green (`absval = 7`). Suite green (646/444).
|
||||
`fix`/green `3c94c14`.
|
||||
- (5.0 prereq lib-ref xfail) Added `1231-ffi-extern-undeclared-lib` (`extern nosuchunit
|
||||
"abs"` — bogus lib ref). RED — compiles silently (extern lib ref unvalidated).
|
||||
`test`/xfail `38c3240`.
|
||||
- (5.0 prereq lib-ref fix) `checkForeignRefs` (c_import.zig) now reads the lib ref from
|
||||
either spelling (foreign_expr.library_ref OR extern_lib) and names the surface keyword,
|
||||
so 1620 (#foreign) is byte-unchanged and 1231 (extern) gets "extern library … not
|
||||
declared". 1231 green. Suite green (647/444). `fix`/green `ad6aed3`. **ALL FOUR fn-path
|
||||
prereqs DONE → fn-body flip de-risked; awaiting Decision 7 (interim wording).**
|
||||
- (5.0 prereq variadic xfail) Added `1229-ffi-extern-cvariadic` (JIT `#source`,
|
||||
int-sum + double-avg, `extern` C-variadic). Expected snapshot pins the DESIRED
|
||||
correct output. RED (variadic `extern` slice-packs extras → garbage:
|
||||
`sum_ints(3,10,20,30)` → 53316585; doubles → 0.0). `test`/xfail `9a2c78d`.
|
||||
- (5.0 prereq variadic fix) Extended the two C-variadic gates — the `is_variadic`
|
||||
drop in `declareFunction` (`decl.zig:2097`) and the early-out in
|
||||
`packVariadicCallArgs` (`pack.zig:302`) — to fire for `extern_export == .extern_`
|
||||
as well as a `foreign_expr` body. 1229 green (`60` / `2.000000`). Suite green
|
||||
(645 corpus / 444 unit, 0 failed). `fix`/green `0fdc821`. **BOTH fn-path prereqs
|
||||
DONE → fn-decl `#foreign` body-marker migration unblocked.**
|
||||
- (5.0 prereq vis xfail) Added cross-module example `1228-ffi-extern-c-non-transitive`
|
||||
(main → b → c). Main references c's lib-less `#foreign` + `extern` twins
|
||||
transitively; expected snapshot pins the DESIRED equivalent C-specific
|
||||
diagnostic for both. RED (extern twin gets the generic "not visible" wording —
|
||||
443/444). `test`/xfail commit `717c35d`; the fix greens it.
|
||||
- (5.0 prereq vis fix) Extended `isVisible(.c_import_bare)` (`decl.zig:2249`) to
|
||||
switch on the body: a `foreign_expr` body OR an `extern_export == .extern_` decl
|
||||
with no lib both route to `visibleOverEdges`; a library-bound decl stays
|
||||
unconditionally visible. 1228 green — both twins emit "C function not visible".
|
||||
Suite green (644 corpus / 444 unit, 0 failed). `fix`/green commit `7d8ba1a`.
|
||||
**Deferred prereq (b) CLOSED.** Investigation this session also found
|
||||
const-with-type is a DEAD parser path (defer per user) and the runtime-class
|
||||
prefix is already coalesced (no Phase 5.0 change) — see Next step.
|
||||
- (5.0 global) **PART B STARTED.** Routed the `#foreign` data-global parser path
|
||||
(`parser.zig:425`) onto the extern-named `VarDecl` (`is_extern`/`extern_lib`/
|
||||
`extern_name`) — the same AST postfix `extern` builds. Behavior-preserving
|
||||
(lowering coalesces both at `decl.zig:1119,1127,1141`); zero snapshot churn. Suite
|
||||
green (444/444 unit, 643 corpus). `refactor` lock, commit `e5ddfbe`. Remaining
|
||||
Phase 5.0 paths: const-with-type (316), fn-body (2059, needs visibility+variadic
|
||||
prereqs), runtime-class prefix (1305).
|
||||
- (init) Plan written; FFI-linkage stream opened.
|
||||
- (merge) Folded FOREIGN-MIGRATION in as Part B; deleted the split plan + checkpoint.
|
||||
- (0.0) Added `kw_extern`/`kw_export` tokens + keyword-map entries + LSP keyword
|
||||
classification + `lex linkage keywords` test. Suite green; no identifier collisions
|
||||
in the corpus. `lock` commit.
|
||||
- (0.1) Added `ast.ExternExportModifier` + `FnDecl.extern_export` +
|
||||
`VarDecl.is_extern`/`extern_name` + `parseOptionalExternExport()` (unconsumed) + 2
|
||||
parser unit tests. Suite green (443/633). `lock` commit.
|
||||
- (1.0a) Wired fn-path extern parsing (`parseFnDecl` + both lookahead predicates) +
|
||||
added `FnDecl.extern_lib`/`extern_name` + `VarDecl.extern_lib` per user feedback
|
||||
(decision 4 revised: extern carries an optional lib axis). Unconsumed by lowering.
|
||||
Suite green (443/633). `lock` commit.
|
||||
- (1.0b) Added `examples/1223-ffi-extern-fn.sx` + hand-authored success snapshots.
|
||||
RED (634 ran, 1 failed — sema `body produces no value`). `xfail` commit; 1.1 greens it.
|
||||
- (1.1) Wired extern fn lowering (6 edits in `decl.zig`, all declare-only routing
|
||||
mirroring `foreign_expr`): `funcWantsImplicitCtx` + `declareFunction` cc +
|
||||
`lazyLowerFunction`/`lowerFunction`/`lowerFunctionBodyInto` guards. 1223 green;
|
||||
`declare i32 @abs(i32)` (C ABI, no ctx). Suite green (634/443). `green` commit.
|
||||
- (1.2a) Added `examples/1224-ffi-extern-fn-rename.sx` (`c_abs :: … extern "abs";`) +
|
||||
hand-authored success snapshot (`c_abs(-42) = 42`). RED (635 ran, 1 failed — parse
|
||||
error: `"abs"` after `extern` not yet accepted). `xfail`; 1.2b greens it. (Also
|
||||
recovered a formatter-clobbered `parser.zig` — see Known issues.)
|
||||
- (1.2b) `parseFnDecl` parses the optional `[LIB] ["csym"]` tail into
|
||||
`extern_lib`/`extern_name`; `declareFunction` unifies the rename (foreign c_name OR
|
||||
extern_name → declare under C name, map sx→C) and extends the dedupe guard to
|
||||
extern. 1224 green (`c_abs`→`abs`); 1223 unregressed. Suite green (635/443).
|
||||
`green` commit. extern_lib parsed+stored (lib linking stays the `#library` axis).
|
||||
- (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.
|
||||
- (JIT spike) User-requested feasibility investigation of C→sx-by-name in `sx run`
|
||||
(JIT). Verdict: feasible via `LLVMOrcLLJITAddObjectFile` (C objects into the ORC
|
||||
JITDylib) — proven by a throwaway spike — but blocked by JITLink MachO TLV handling
|
||||
(`sx_trace.c`'s `_Thread_local` SIGABRTs without the ORC `MachOPlatform`). Own future
|
||||
milestone (see Next step). Spike reverted; no commit.
|
||||
- (2.0) Added the **AOT corpus mode** (`expected/<name>.aot` → `sx build` + execute) to
|
||||
`corpus_run.test.zig` + retired `tests/run_examples.sh` (verify-step.sh/CLAUDE.md
|
||||
updated) + `examples/1226-ffi-export-fn.{sx,c,h}` (C calls `sx_square` back). RED (AOT
|
||||
link fails: `_sx_square` undefined — export not lowered). `xfail`; 2.1 greens it.
|
||||
- (2.1) Filled export gaps i/ii/iv in `decl.zig` (`.external` linkage + `.c` cc on both
|
||||
define paths; `funcWantsImplicitCtx` false for any non-`.none` modifier) + force-lower
|
||||
export fns as roots in `lowerMainAndComptime`. 1226 green via AOT (37/82). Suite green
|
||||
(637/443). `green` commit.
|
||||
- (2.2a) Added `examples/1227-ffi-export-fn-rename.sx` (`export "triple_c"`, C calls
|
||||
`triple_c`). RED (define path emits `@sx_triple`, ignores `extern_name` → C ref
|
||||
undefined). `xfail`; 2.2b greens it.
|
||||
- (2.2b) `declareFunction` rename branch fires for `export` (stub under C name +
|
||||
sx→C in `foreign_name_map`); `lazyLowerFunction` resolves the stub by that C name so
|
||||
the body promotes into the C-named function (`define @triple_c`). sx-side call sites
|
||||
resolve via the same map (probe: 5*5→25). 1227 green (22); 1226 unregressed. Suite
|
||||
green (638/443). `green` commit. **PHASE 2 COMPLETE** — `export` fully works.
|
||||
- (3.0) Added `examples/1348-ffi-objc-extern-class.sx` (postfix `extern` on `#objc_class`,
|
||||
new spelling of `#foreign #objc_class`). RED (parser: `expected '{'` after the
|
||||
directive). Hand-authored green snapshots. `xfail` commit; 3.1 greens it.
|
||||
- (3.1a) Wired the postfix `extern`/`export` aggregate slot in `parseForeignClassDecl`
|
||||
(optional modifier between `("X")` and `{`; `var is_foreign_eff` overrides the passed
|
||||
`is_foreign`, threaded into the `foreign_class_decl` node). No lowering change — reuses
|
||||
the existing `is_foreign` reference-vs-define path. 1348 green. Suite green (639/443).
|
||||
`green` commit. **PHASE 3 COMPLETE.**
|
||||
- (3.1b) Behavior-lock: added `examples/1426-ffi-jni-extern-class.sx` (jni `extern`,
|
||||
parse-only) + `examples/1349-ffi-objc-export-class.sx` (objc `export` defined class,
|
||||
`counter: 2`). Both pass against the 3.1a parser change (locked in their own commit per
|
||||
the cadence rule). Suite green (641/443). `lock` commit. (Note: `-Dupdate-goldens`
|
||||
newline-normalizes empty stderr → reverted unrelated 1226/1227 churn, kept new stderr
|
||||
0-byte per repo convention; runner normalizes both.)
|
||||
- (4.gate) **GATE A→B** — added `lowerSrcToIr` helper + "GATE A→B" test to `lower.test.zig`:
|
||||
`#foreign` ≡ `extern`/`export` byte-identical printed IR for fn / global / Obj-C class.
|
||||
Verified live via negative-probe (mutate one side → assertion fails). Behavior-lock; the
|
||||
equivalence was prototyped first with `sx ir` (LLVM IR byte-identical for all three).
|
||||
Suite green (641/444). `test` commit.
|
||||
- (4.diag1) Added `examples/1174-diagnostics-foreign-postfix-conflict.sx` — prefix `#foreign`
|
||||
+ postfix `export` on an aggregate previously surfaced a confusing internal
|
||||
"emitObjcDefinedClassAllocImp … compiler bug". `xfail` (golden = clean message) → `green`:
|
||||
`parseForeignClassDecl` rejects the combo at the postfix keyword (`failFmt`). Suite green.
|
||||
- (4.docs) `specs.md` (new "`extern`/`export` linkage keywords" subsection after the
|
||||
`#foreign` FFI docs) + `readme.md` (C Interop section) document the three axes. `docs` commit.
|
||||
- (4.diag2) Added `examples/1175-diagnostics-extern-export-conflict.sx` — `extern export` on
|
||||
one fn decl previously gave bare "expected ';'". `xfail` (golden = clean message) → `green`:
|
||||
`parseFnDecl` rejects a second linkage keyword after `parseOptionalExternExport`. Suite
|
||||
green (643/444). **PHASE 4 COMPLETE → PART A DONE.**
|
||||
- (golden-fix) **`-Dupdate-goldens` churn RESOLVED.** Root cause was NOT a code bug:
|
||||
`writeGolden` always writes `content + "\n"` (empty → canonical 1-byte `\n`, used by 484
|
||||
of 489 empty goldens). The 5 churning stderr files [1226/1227/1348/1349/1426] were 0-byte
|
||||
*outliers* (verify trims trailing `\n` so both forms passed, but regen always rewrote them
|
||||
to 1-byte). Conformed all 5 to the 1-byte form → `-Dupdate-goldens` is now idempotent, no
|
||||
more churn. (Separately: a flaky `0712-sha256-streaming` >10s timeout appears only under
|
||||
concurrent `zig build` load — not a real failure; re-run serially.)
|
||||
|
||||
## Known issues
|
||||
- **Workflow hazard (1.2):** an editor format-on-save (or `zig fmt`) clobbered the
|
||||
working-tree `src/parser.zig` between commits — it reformatted one-liners AND
|
||||
silently dropped my `hasFnBodyAfterArrow` extern edit, reverting 1223 to a parse
|
||||
error. Recovered with `git checkout src/parser.zig` (HEAD had the correct,
|
||||
committed version). **After any Edit-tool change to a file the IDE may have open,
|
||||
rebuild + run the affected example before trusting the edit.**
|
||||
82
current/PLAN-ASM.md
Normal file
82
current/PLAN-ASM.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# sx Inline Assembly — Implementation Plan (ASM stream)
|
||||
|
||||
**Design source of truth:** [docs/inline-asm-design.md](../docs/inline-asm-design.md).
|
||||
This plan turns that doc's §II.7 stage-map + §II.8 phasing into ordered,
|
||||
commit-sized, testable steps. Read the design doc first — this file is the
|
||||
*how/when*, not the *what/why*.
|
||||
|
||||
**Surface (decided):**
|
||||
`asm volatile { "template", "=r" -> T, "r" = expr, clobbers(.cc, .memory) }`
|
||||
— brace block; `->` output / `=` input; `clobbers(.…)` dot-name list; N `-> Type`
|
||||
outputs return a tuple; templates are pure AT&T (via LLVM).
|
||||
|
||||
**Feasibility (confirmed):** sx links LLVM@19; `src/llvm_api.zig` `@cImport`s
|
||||
`llvm-c/Core.h`, so `llvm_api.c.*` already exposes `LLVMGetInlineAsm` (9-arg),
|
||||
`LLVMInlineAsmDialectATT`, `LLVMBuildCall2`, `LLVMAppendModuleInlineAsm`. No shim.
|
||||
|
||||
**Relationship to other streams:**
|
||||
- Phases A–E (the inline-asm *expression*) are independent of EXTERN-EXPORT.
|
||||
- Phase F (global asm) consumes `extern`/`export` to import/expose asm symbols —
|
||||
do it **after** `PLAN-EXTERN-EXPORT.md` Phase 2.
|
||||
|
||||
## Cadence (IMPASSIBLE)
|
||||
No commit may both add a test AND make it pass. Each feature step is either a
|
||||
behavior-locking PASSING test, or an xfail test the *next* commit turns green.
|
||||
Arch-pinned tests live in `examples/16xx-platform-asm-*` (must declare `target=`).
|
||||
Never regenerate snapshots while red.
|
||||
|
||||
## Phase A — keyword + AST + parser (parses; no codegen)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| A.0 | lock | add `kw_asm` keyword + map entry; unit lex test `asm → kw_asm` | `src/token.zig`, `src/lexer.zig` + `.test.zig` |
|
||||
| A.1 | xfail | parse `asm { … }` → `AsmExpr`/`AsmOperand` in `parsePrimary`; pin an AST/`sx ir` parse snapshot; lowering still `bailDetail("inline asm codegen unimplemented")` | `src/ast.zig` (:85 union arm, :721 structs), `src/parser.zig` (parsePrimary), `src/ir/interp.zig` |
|
||||
| A.2 | green | parse-shape snapshot lands green; the unimplemented bail is loud + named | — |
|
||||
|
||||
## Phase B — sema / typing
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| B.0 | xfail | result-type rule (0→`void` / 1→`T` / N→named-or-positional tuple) + checklist (no-output⇒`volatile`, layout, comptime-string template) — pin error messages | `src/ir/expr_typer.zig` |
|
||||
| B.1 | green | typing + diagnostics implemented; `.unresolved` sentinel on failure (no silent default) | `src/ir/expr_typer.zig`, `src/ir/semantic_diagnostics.zig` |
|
||||
|
||||
## Phase C — IR op + lowering
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| C.0 | lock | add `inline_asm: InlineAsm` to `Op` + `AsmOperand` (role/name/constraint/operand) + interp `bailDetail` arm; unit tests for the IR shape | `src/ir/inst.zig` (:80), `src/ir/interp.zig` |
|
||||
| C.1 | xfail→green | `lowerAsmExpr` in `lowerExpr` dispatch — interns template/constraints/clobber-names, lowers input `Ref`s, sets result `TypeId` | `src/ir/lower/expr.zig` |
|
||||
|
||||
## Phase D — LLVM emit (single value-output; the core)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| D.0 | xfail | `examples/16xx-platform-asm-syscall-write.sx` + `…-register-read.sx` + `…-no-output-volatile.sx` + `…-missing-volatile.sx` (expected compile error) — all red | examples + `expected/` markers |
|
||||
| D.1 | green | `emitInlineAsm`: **port `FuncGen.airAssembly`** — constraint-string assembler (outputs `=`/`+`, inputs, `clobbers(.name)`→`~{name}`), `%[name]`→`${N}` / `%%` / `%=` template rewriter, `LLVMGetInlineAsm`+`LLVMBuildCall2`, `sideeffect=volatile`, AT&T dialect | `src/ir/emit_llvm.zig` (emitInst dispatch + handler) |
|
||||
| D.2 | green | lock the template-rewrite + constraint string via an `expected/*.ir` snapshot on `…-template-subst.sx` | examples |
|
||||
|
||||
**Phase D verification:** `zig build test`; the syscall example runs on
|
||||
`x86_64-linux`; IR snapshot matches the design doc's worked `sys_write` lowering.
|
||||
|
||||
## Phase E — multi-return tuples + `clobbers(.…)`
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| E.0 | xfail | `…-asm-multi-return.sx` (`divmod`→`(quot,rem)`, `cpuid`→4-tuple) red | examples |
|
||||
| E.1 | green | N `out_value` → LLVM struct return + `extractvalue i` → sx tuple (named when operands named); `clobbers(.name)` dot-name lowering finalized | `src/ir/emit_llvm.zig`, `src/ir/lower/expr.zig` |
|
||||
|
||||
## Phase F — global asm (needs EXTERN-EXPORT Phase 2)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| F.0 | xfail | top-level `asm { … }` decl parsed (reject operands/`volatile`); `…-asm-global.sx` (defines a symbol, imported via `extern`) red | `src/parser.zig`, `src/ast.zig` |
|
||||
| F.1 | green | lower `asm_global` → `c.LLVMAppendModuleInlineAsm`; comptime-call guard (dlsym-miss is loud); blocks concatenate in source order | `src/ir/lower/decl.zig`, `src/ir/emit_llvm.zig`, `src/ir/interp.zig` |
|
||||
|
||||
## Phase G — later (own steps when scheduled)
|
||||
`-> @place` write-through + read-write (`"+r" -> @place`) + indirect-memory
|
||||
(`"=*m"`) outputs · `%=` unique-id · output-to-const rejection · Intel-dialect
|
||||
opt-in · naked functions (`callconv(.naked)`, coordinate with EXTERN-EXPORT).
|
||||
|
||||
## Open decisions (design doc §II.10)
|
||||
Dialect (AT&T-only v1, recommended) · `volatile` contextual-keyword (recommended)
|
||||
· brace separator comma (recommended) · `clobbers(.name)` dot-name sugar now →
|
||||
checked per-arch `Clobber` enum later (Phase 4 of the design doc).
|
||||
|
||||
## End-to-end verification (per phase)
|
||||
`zig build && zig build test`; for arch-pinned examples confirm they run on a
|
||||
matching host or assert on `sx ir`/`.s` snapshots. After intentional output
|
||||
changes only: `zig build test -Dupdate-goldens`, then review the diff.
|
||||
207
current/PLAN-EXTERN-EXPORT.md
Normal file
207
current/PLAN-EXTERN-EXPORT.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# sx `extern` / `export` + `#foreign` retirement — Plan (FFI-linkage stream)
|
||||
|
||||
**One stream, two parts.** **Part A** adds `extern`/`export` (the linkage surface);
|
||||
**Part B** migrates every `#foreign` onto it and purges `foreign` from the tree.
|
||||
They are *one* plan: Part B can't start until Part A is a behavior-equivalent
|
||||
superset of `#foreign`, and Part A isn't "done" until Part B reaches the invariant.
|
||||
|
||||
**Design rationale:** [docs/inline-asm-design.md](../docs/inline-asm-design.md) §II.2
|
||||
(Deviation 6) + §II.10 #4 + the syntax evaluation.
|
||||
|
||||
**Decided syntax**
|
||||
```sx
|
||||
name :: (sig) -> Ret [callconv(.x)] [extern | export] [LIB] ["csym"] [;|{…}]; // functions
|
||||
Name :: #objc_class("X") [extern | export] { … }; // aggregates (mirrors `struct #compiler`)
|
||||
g : Type extern [LIB] ["csym"]; // extern global
|
||||
```
|
||||
- `extern` = import (no body, external linkage, C ABI, no sx ctx) — `#foreign`'s role.
|
||||
- `export` = define **and** expose (body + external linkage + C ABI + no ctx) — **new**.
|
||||
- `extern`/`export` imply `callconv(.c)`; write `callconv` only to override.
|
||||
- Optional `LIB` (a `#library` alias) + `"csym"` rename mirror `#foreign LIB "csym"`,
|
||||
so `extern` is a true `#foreign` **superset** (Gate A→B): carried on
|
||||
`extern_lib`/`extern_name`. The `#library` declaration + build-flag linking
|
||||
mechanism stays a separate axis — `extern` *references* a lib, it doesn't fold
|
||||
in `#library` itself. (Revises the original "library fully separate" decision 4.)
|
||||
|
||||
> **END-STATE INVARIANT (hard requirement).** After this stream, `foreign` appears
|
||||
> **nowhere** in the live tree — not the `#foreign` surface, and **not** internal
|
||||
> identifiers. The extern AST is **not** named `foreign_expr`. Enforced by the
|
||||
> Phase 9.4 grep gate. Scope today: 643 `foreign` lines / ~57 identifiers in `src/`
|
||||
> + 28 in live docs — most of it the objc/jni **runtime-class** machinery.
|
||||
|
||||
**Naming constraint (so we can actually reach the invariant):** introduce
|
||||
`extern`-named representations only — do **not** reuse or extend
|
||||
`ForeignExpr`/`foreign_expr`/`VarDecl.is_foreign`. Carry extern/export on a new
|
||||
`FnDecl.extern_export` modifier with a `;`/`{…}` body (so there is **no** `*_expr`
|
||||
node for it) + `FnDecl.extern_lib`/`extern_name`; add `VarDecl.is_extern`/
|
||||
`extern_lib`/`extern_name`. The IR is already extern-named (`Function.is_extern`,
|
||||
`Builder.declareExtern`).
|
||||
|
||||
**Key finding (scopes Part A):** the IR + LLVM emit **already support everything** —
|
||||
`Function.linkage` (`.external/.internal/.private`), `is_extern`, `call_conv`, and a
|
||||
raw un-mangled symbol name are all emitted by `declareFunction`
|
||||
(`emit_llvm.zig:1225-1300`). Part A is a **parser + lowering** job, no codegen change.
|
||||
|
||||
## Cadence (IMPASSIBLE)
|
||||
No commit may both add a test AND make it pass (xfail-then-green, or a behavior-lock).
|
||||
`zig build && zig build test` after every step. Never regenerate snapshots while red.
|
||||
|
||||
---
|
||||
|
||||
# PART A — add `extern` / `export` (alongside `#foreign`)
|
||||
|
||||
## Phase 0 — tokens + parser plumbing
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 0.0 | lock | add `kw_extern`, `kw_export` (Tag enum + `StaticStringMap`, beside `kw_callconv` at `token.zig:45,282`); unit lex test | `src/token.zig` |
|
||||
| 0.1 | lock | `parseOptionalExternExport()` (mirror `parseOptionalCallConv`, `parser.zig:3669`) + `ast.ExternExportModifier` enum + `FnDecl.extern_export` + `VarDecl.is_extern`/`extern_name` fields; **not yet consumed**; unit AST test | `src/parser.zig`, `src/ast.zig` |
|
||||
|
||||
## Phase 1 — `extern` (import; equivalent to lib-less `#foreign`)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 1.0 | xfail | accept postfix `extern` after the callconv slot (`parser.zig:1950`); `examples/12xx-ffi-extern-fn.sx` extern-binds a libc symbol — red (lowering not wired) | `src/parser.zig` |
|
||||
| 1.1 | green | lowering: `extern` ⇒ `is_extern`, `.external`, `callconv(.c)`, no ctx — route through `declareExtern` like a lib-less `#foreign` (anchors `decl.zig:1123,387,2110,2113`). Example green | `src/ir/lower/decl.zig` |
|
||||
| 1.2 | green | optional `extern "csym"` rename + extern-global form `g : T extern;` (`parser.zig:425` path) | `src/parser.zig`, `src/ir/lower/decl.zig` |
|
||||
|
||||
## Phase 2 — `export` (define + expose; the NEW capability)
|
||||
Fills the four export-gap conditions (all in `src/ir/lower/decl.zig`):
|
||||
| Gap | Anchor | Fix |
|
||||
|---|---|---|
|
||||
| (i) linkage forced `.internal` | `:2382`, `:2514` | also `.external` when `extern_export == .export` |
|
||||
| (ii) C ABI not promoted | `:2110` | also `.c` when `== .export` |
|
||||
| (iii) no symbol-name override | `emit_llvm.zig:1226` raw name | parse optional `export "csym"`; map in the name map |
|
||||
| (iv) ctx param not suppressed | `:387` `funcWantsImplicitCtx` | also suppress when `== .export` |
|
||||
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 2.0 | xfail | multi-file test: an `export fn` called from a companion `.c` caller (same `XXXX-` prefix) — red (still internal) | `examples/12xx-ffi-export-fn.{sx,c}` + `expected/` |
|
||||
| 2.1 | green | gaps (i),(ii),(iv): `export` ⇒ external + C-ABI + no-ctx on a **defined** fn (uses `beginFunction`, not `declareExtern`) | `src/ir/lower/decl.zig` |
|
||||
| 2.2 | green | gap (iii): `export "csym"` symbol-name override | `src/parser.zig`, `src/ir/lower/decl.zig` |
|
||||
|
||||
## Phase 3 — aggregates (objc / jni runtime classes)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 3.0 | xfail | `#objc_class("X") extern { … }` (import) + `… export { … }` (define) parse alongside legacy `#foreign #objc_class` | `src/parser.zig` (`tryParseForeignClassPrefix` :1305, `parseForeignClassDecl` :1369) |
|
||||
| 3.1 | green | map postfix `extern`→reference, `export`→define+register; per-runtime tests (objc, jni) | `src/parser.zig`, `src/ir/lower/decl.zig`, `src/ir/lower/objc_class.zig` |
|
||||
|
||||
## Phase 4 — interplay, diagnostics, docs
|
||||
`extern`+`callconv` stacking/redundancy; reject `extern`+`export` together;
|
||||
`specs.md` documents `extern`/`export` (the three axes); `#foreign` still documented
|
||||
until Part B cutover.
|
||||
|
||||
> **GATE A→B.** `extern`/`export` are a behavior-equivalent **superset** of
|
||||
> `#foreign`. Lock with a unit test asserting `#foreign` and `extern` lower to
|
||||
> identical IR for a sample fn / global / class. Do not start Part B before this.
|
||||
|
||||
---
|
||||
|
||||
# PART B — migrate `#foreign` → `extern`/`export`, then purge `foreign`
|
||||
|
||||
**Inventory (drives the batches):** `#foreign` = 466 uses. ~391 sx-code (308 fns
|
||||
[207 lib / 196 rename], 75 classes [39 objc / 31 jni], 8 globals) + ~145 example
|
||||
snapshots. 6 libs (`sqlib`98 `libc`61 `objc`22 `tlib`12 `raylib`7 `clib/pcaplib`3).
|
||||
Hotspots: `vendors/sqlite`(98), `platform/{android,uikit,android_jni,sdl3}`,
|
||||
`std/{socket,thread,fs,time}`, `ffi/{objc,raylib}`.
|
||||
|
||||
## Phase 5 — `#foreign` becomes an alias for `extern`
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 5.0 | lock | route the `#foreign` parser paths (`parser.zig:316,425,1305,1970`) to build the *same extern-named* AST as `extern`/`export`. Suite green, snapshots unchanged | `src/parser.zig` |
|
||||
| 5.1 | lock | unit test: `#foreign` and `extern` produce identical IR (fn/global/class) | `src/ir/lower/decl.test.zig` |
|
||||
|
||||
## Phase 6 — migrate stdlib (behavior-preserving; snapshot diff must be EMPTY)
|
||||
One commit per batch; rewrite `#foreign`→`extern` (fns/globals),
|
||||
`#foreign #objc_class`→`#objc_class … extern`, defined classes → `… export`.
|
||||
| Step | Batch | ~sites |
|
||||
|---|---|---|
|
||||
| 6.1 | `library/vendors/sqlite/` | 98 |
|
||||
| 6.2 | `library/modules/platform/` (uikit/android/android_jni/sdl3) | ~95 |
|
||||
| 6.3 | `library/modules/std/` (socket/thread/fs/time/process/…) | ~60 |
|
||||
| 6.4 | `library/modules/ffi/` (objc/raylib/objc_block/…) | ~50 |
|
||||
| 6.5 | remaining `library/` + `vendors/` | remainder |
|
||||
|
||||
## Phase 7 — migrate examples + issues (empty snapshot diff; review every diff)
|
||||
| Step | Batch |
|
||||
|---|---|
|
||||
| 7.1 | `examples/12xx-ffi-*` (plain C) |
|
||||
| 7.2 | `examples/13xx-ffi-objc-*` |
|
||||
| 7.3 | `examples/14xx-ffi-jni-*` |
|
||||
| 7.4 | `issues/*` repros + stragglers |
|
||||
A non-empty diff ⇒ the alias wasn't behavior-equivalent — stop, fix Phase 5.
|
||||
|
||||
## Phase 8 — cutover
|
||||
| Step | Commit | What |
|
||||
|---|---|---|
|
||||
| 8.0 | xfail | `examples/11xx-diagnostics-foreign-removed.sx` expects a "`#foreign` removed; use `extern`/`export`" diagnostic — still accepted (red) |
|
||||
| 8.1 | green | parser hard-rejects `#foreign` (mirrors the variadic `name: ..T` cutover); `specs.md` drops `#foreign`, documents `extern`/`export` |
|
||||
|
||||
## Phase 9 — total `foreign` purge (the invariant)
|
||||
`foreign` must not appear anywhere in the live tree, surface *or* internal. Each step
|
||||
a mechanical, behavior-preserving rename commit (snapshots unchanged), small
|
||||
per-file/subsystem commits — not one sweep.
|
||||
| Step | What | Identifiers (count → new) |
|
||||
|---|---|---|
|
||||
| 9.0 | delete the surface | `hash_foreign`(11) + lexer entry + the 4 parse paths + the alias |
|
||||
| 9.1 | rename **linkage** → `extern*` | `foreign_expr`(25) **eliminated** (folds into modifier) · `is_foreign`(39)→`is_extern` · `foreign_lib`/`foreign_name`→`extern_*` · `foreign_name_map`→`extern_name_map` · `callForeign`(8)→`callExtern` · `marshalForeignArg`→`marshalExternArg` · `is_foreign_c_api`(5)→`is_extern_c_api` · `dedupeForeignSymbol`→`dedupeExternSymbol` |
|
||||
| 9.2 | rename **runtime-class** machinery → `runtime*` (decision 5) | `ForeignClassDecl`(65) · `ForeignMethodDecl`(31) · `ForeignClassMember`(20) · `ForeignFieldDecl`(15) · `foreign_class_map`(44) · `current_foreign_class`(34)/`_method` · `foreign_path`(62) · `ForeignRuntime` · `parse/tryParseForeignClass*` · `lowerForeign{Method,Static}Call` · `findForeign{Method,Property}InChain` · `resolveForeign*` · `register*ForeignClass*` · `foreignClass*Type` · `*ForeignRefs` |
|
||||
| 9.3 | purge **live docs** (28 lines) | `specs.md`/`readme.md`/`CLAUDE.md`: drop `#foreign`, document `extern`/`export`; fix file-roles + FFI/bundling notes |
|
||||
| 9.4 | **acceptance gate** | `grep -rniE 'foreign' src/ library/ examples/ specs.md readme.md CLAUDE.md` → **0** |
|
||||
|
||||
---
|
||||
|
||||
## Open decisions
|
||||
*Part A (ratified — recommendations stand):* 1. bare keywords (not `#extern`).
|
||||
2. aggregate position postfix (`#objc_class(…) extern`, like `struct #compiler`).
|
||||
3. `extern ⇒ callconv(.c)`. 4. **REVISED** (user, 2026-06-14): `extern` carries an
|
||||
optional `LIB`+`"csym"` axis (`extern_lib`/`extern_name`), mirroring `#foreign LIB
|
||||
"csym"`, so it's a true `#foreign` superset (Gate A→B). The `#library` declaration +
|
||||
build-flag linking mechanism stays separate — `extern` references a lib, doesn't
|
||||
fold in `#library`. (Was: "library fully separate / not on `extern`".)
|
||||
*Part B:* 5. runtime-class rename target — **RATIFIED `Runtime*Class*`** (user, 2026-06-14;
|
||||
it's the object-model axis, not linkage). 6. historical carve-out — **STILL OPEN** (user did
|
||||
not confirm at the Part A milestone): keep `issues/*.md` (+ design-doc prose) as provenance &
|
||||
gate only the live tree (recommended) vs purge everything. Confirm 6 before Phase 9.
|
||||
|
||||
## Relationship to ASM
|
||||
`PLAN-ASM.md` Phase F (global asm) consumes `extern` (import the asm symbol) and
|
||||
`export` (let asm call back into sx) — do it after **Part A Phase 2**.
|
||||
|
||||
---
|
||||
|
||||
## Kickoff prompt (paste into a fresh session to start Part A)
|
||||
|
||||
> Work the FFI-linkage stream per `current/PLAN-EXTERN-EXPORT.md` (+ checkpoint
|
||||
> `current/CHECKPOINT-EXTERN-EXPORT.md`). First read the plan's header (Decided
|
||||
> syntax, Naming constraint, Key finding) and Part A; rationale is in
|
||||
> `docs/inline-asm-design.md` §II.2 (Deviation 6) + §II.10 #4.
|
||||
>
|
||||
> **This session = Part A, Phases 0 and 1 only** (`extern` works as a bare postfix
|
||||
> keyword equivalent to a lib-less `#foreign` fn/global binding; `#foreign` stays
|
||||
> untouched). Do NOT start Phase 2 (`export`) or Part B (migration).
|
||||
>
|
||||
> **Cadence (IMPASSIBLE):** no commit may both add a test and make it pass — lock
|
||||
> behavior with a passing test, or land an xfail the next commit turns green.
|
||||
> `zig build && zig build test` after every step.
|
||||
>
|
||||
> **Naming constraint (hard):** introduce only `extern`-named AST — do NOT reuse or
|
||||
> extend `ForeignExpr`/`foreign_expr`/`VarDecl.is_foreign`. Use a new
|
||||
> `FnDecl.extern_export` modifier (body `;` or `{…}`) and `VarDecl.is_extern`/
|
||||
> `extern_name`. IR is already extern-named (`Function.is_extern`, `declareExtern`).
|
||||
>
|
||||
> Steps (commit after each; update the checkpoint each time):
|
||||
> - 0.0 lock: `kw_extern`/`kw_export` tokens + map entries beside `kw_callconv`
|
||||
> (`src/token.zig:45,282`) + unit lex test.
|
||||
> - 0.1 lock: `parseOptionalExternExport()` (mirror `parseOptionalCallConv`,
|
||||
> `parser.zig:3669`) + `ast.ExternExportModifier` + `FnDecl.extern_export` +
|
||||
> `VarDecl.is_extern`/`extern_name` (parsed, unconsumed) + unit AST test.
|
||||
> - 1.0 xfail: accept postfix `extern` after the callconv slot (`parser.zig:1950`);
|
||||
> add `examples/12xx-ffi-extern-fn.sx` that extern-binds a libc symbol (red).
|
||||
> - 1.1 green: in `src/ir/lower/decl.zig`, lower `extern` like a lib-less `#foreign`
|
||||
> import — `is_extern`, `.external`, `callconv(.c)`, no ctx, via `declareExtern`
|
||||
> (anchors :1123, :387, :2110, :2113). Example goes green.
|
||||
> - 1.2 green: optional `extern "csym"` rename + extern-global `g : T extern;`
|
||||
> (`parser.zig:425`).
|
||||
>
|
||||
> Stop at end of Phase 1. Verify: suite green; the `extern` libc binding runs;
|
||||
> `#foreign` still works with no snapshot diffs. If you hit an unrelated compiler
|
||||
> bug, follow the CLAUDE.md IMPASSIBLE RULE (file an issue, stop).
|
||||
@@ -116,7 +116,7 @@ context:
|
||||
`sx_trace_push` call emitted through the normal call lowering.
|
||||
- **`interp`:** yields the packed `(func_id, span.start)` from its own
|
||||
execution context as the op's value. The separate `sx_trace_push` call
|
||||
op consuming it is executed by the interp as a foreign call (via
|
||||
op consuming it is executed by the interp as an extern call (via
|
||||
`host_ffi`/dlsym, the same path as any extern), storing the packed value
|
||||
in the buffer; the comptime `.trace_resolve` resolver later recovers
|
||||
`file:line:col` from it.
|
||||
@@ -257,7 +257,7 @@ both the trace path and the DWARF path. Items marked ✅ exist today;
|
||||
| [`src/ir/emit_llvm.zig`](../src/ir/emit_llvm.zig) | IR→LLVM orchestrator. Owns `LLVMEmitter` + the source map (`setDebugContext`); dispatches the `.trace_frame` op and the DWARF passes to the helpers below |
|
||||
| [`src/backend/llvm/reflection.zig`](../src/backend/llvm/reflection.zig) | `Reflection`: builds the interned `Frame` table + the tag-name / type-name tables; yields the `.trace_frame` op's value (the `Frame` global's address) — the `sx_trace_push` call itself is emitted by `lower.zig` |
|
||||
| [`src/backend/llvm/debug.zig`](../src/backend/llvm/debug.zig) | `DebugInfo`: builds all DWARF metadata (compile unit, per-function subprograms, per-instruction `DILocation`) |
|
||||
| [`src/ir/interp.zig`](../src/ir/interp.zig) | Comptime IR interpreter. The `.trace_frame` op yields a packed `(func_id, span.start)`; the separate `sx_trace_push` call op runs as a foreign call (dlsym); `.trace_resolve` recovers comptime frames |
|
||||
| [`src/ir/interp.zig`](../src/ir/interp.zig) | Comptime IR interpreter. The `.trace_frame` op yields a packed `(func_id, span.start)`; the separate `sx_trace_push` call op runs as an extern call (dlsym); `.trace_resolve` recovers comptime frames |
|
||||
| [`src/errors.zig`](../src/errors.zig) | `SourceLoc.compute(source, offset) → {line, col}`; the `import_sources` map type |
|
||||
| [`src/ir/inst.zig`](../src/ir/inst.zig) | `Inst.span`, `Function.source_file`, the `Op` union (home of the `.trace_frame` op) |
|
||||
| [`library/vendors/sx_trace_runtime/sx_trace.c`](../library/vendors/sx_trace_runtime/sx_trace.c) | the thread-local ring buffer + `sx_trace_report_unhandled` |
|
||||
@@ -301,8 +301,8 @@ traces and DWARF can never disagree:
|
||||
declared lazily by `getTraceFids()` (which sets `needs_trace_runtime`).
|
||||
3. **Interpreter** (`interp.zig`, same op): pack `(current_func_id,
|
||||
span.start)` into a `u64` and return it as the op's value. The separate
|
||||
`sx_trace_push` call op is then executed by the interp as a foreign call
|
||||
(`callForeign` → `host_ffi.lookupSymbol`/dlsym, the same path as any
|
||||
`sx_trace_push` call op is then executed by the interp as an extern call
|
||||
(`callExtern` → `host_ffi.lookupSymbol`/dlsym, the same path as any
|
||||
extern), storing the packed value in the buffer. The comptime
|
||||
`.trace_resolve` resolver later turns each packed value back into
|
||||
`file:line:col` via the IR/source tables.
|
||||
|
||||
@@ -22,7 +22,7 @@ is never merged (see `S0.2-…`).
|
||||
| **E-series selection rules** — own-wins / not-visible / ambiguity / direct-flat (the E1–E6a behaviors) | **resolver behavior + regression tests** (the baseline-green corpus is the mirror oracle) | S2 behavior; regressions locked S0 |
|
||||
| **CP rule** — body-author == layout-author | **keyed by `InstantiationId{template_decl, resolved_args}`** in the fact store | S4 |
|
||||
| **E6BR routed-signature cases** (the E6BR-1…4 behavioral cells) | **resolver-signature regressions** — the resolver walks every signature reference position; cases live in the resolver-target corpus, flip at S3.9 | S3.9 |
|
||||
| **FFI `foreign_class_map` consumers + FFI corpus (96 entry trees / 95 active markers)** | parallel `DeclId`s land at S1 (map still the consumer); foreign classes keyed by `DeclId` at S4; runtime names stay **payload strings on facts** | S1 → S4 |
|
||||
| **FFI `runtime_class_map` consumers + FFI corpus (96 entry trees / 95 active markers)** | parallel `DeclId`s land at S1 (map still the consumer); runtime classes keyed by `DeclId` at S4; runtime names stay **payload strings on facts** | S1 → S4 |
|
||||
|
||||
## B. DELETED / TRANSITIONAL — removed in S3/S6
|
||||
|
||||
@@ -41,7 +41,7 @@ is never merged (see `S0.2-…`).
|
||||
|
||||
## C. Dropped / absorbed / superseded plan items
|
||||
|
||||
- **E6c / E6d / E6e** (protocol / foreign / type-fn per-kind identity): **DROPPED as
|
||||
- **E6c / E6d / E6e** (protocol / runtime-class / type-fn per-kind identity): **DROPPED as
|
||||
steps.** They become resolver behavior + regression tests — a whole-AST resolver
|
||||
walks every reference position (annotation, `size_of`, dispatch head, `Self`,
|
||||
vtable), closing the protocol surface the per-kind patch structurally could not
|
||||
@@ -50,7 +50,7 @@ is never merged (see `S0.2-…`).
|
||||
(`namespace_edges` → `ResolvedRef.namespace` / member).
|
||||
- **H** (constructor heads): **ABSORBED** into S3 `materializeType` over resolved
|
||||
generic/protocol/type-fn heads.
|
||||
- **I** (protocol + foreign selection, loud-on-≥2): **ABSORBED** into S2 selection + S4
|
||||
- **I** (protocol + runtime-class selection, loud-on-≥2): **ABSORBED** into S2 selection + S4
|
||||
`DeclId` facts.
|
||||
- **K** (delete dead readers): **SUPERSEDED** by the S4 `DeclId`-keyed fact store + the
|
||||
S6 deletions — "just delete the maps" is upgraded to "replace with `DeclId` facts."
|
||||
|
||||
997
docs/inline-asm-design.md
Normal file
997
docs/inline-asm-design.md
Normal file
@@ -0,0 +1,997 @@
|
||||
# Inline Assembly for sx — Design Doc & Proposal
|
||||
|
||||
**Status:** proposal / not yet scheduled into a workstream
|
||||
**Author:** research pass over the Zig compiler (`~/projects/zig`, 0.16-dev) + the sx compiler
|
||||
**Scope:** how Zig implements inline assembly end-to-end, and a minimal-deviation proposal to bring the same model to sx.
|
||||
|
||||
> Guiding constraint for this doc: **mirror Zig's design; deviate only where sx's
|
||||
> grammar or stdlib makes a 1:1 copy impossible, and call every deviation out
|
||||
> explicitly with its justification.** Every deviation below is tagged
|
||||
> **[DEVIATION]** with a reason.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR + feasibility
|
||||
|
||||
* **Feasible today, no new infrastructure.** sx already links LLVM (`build.zig:10`
|
||||
→ `/opt/homebrew/opt/llvm@19`) and `@cImport`s `llvm-c/Core.h`
|
||||
(`src/llvm_api.zig:1-17`). That header exposes everything inline asm needs,
|
||||
reachable right now through `llvm_api.c.*`:
|
||||
* `LLVMGetInlineAsm(Ty, AsmString, AsmStringSize, Constraints, ConstraintsSize, HasSideEffects, IsAlignStack, Dialect, CanThrow)` — builds the asm callee (LLVM 19/21 share this 9-arg signature).
|
||||
* `LLVMInlineAsmDialectATT` / `LLVMInlineAsmDialectIntel`.
|
||||
* `LLVMBuildCall2(...)` — already used pervasively in `src/ir/emit_llvm.zig` (e.g. the Obj-C msgSend path) — calls the asm value like a function.
|
||||
* `LLVMAppendModuleInlineAsm(M, Asm, Len)` — module-level (global) asm.
|
||||
* **The hard part is not codegen.** Codegen is ~80 lines of well-trodden LLVM-C.
|
||||
The real work is (a) the parser grammar, (b) a faithful port of Zig's
|
||||
*LLVM constraint-string assembly* and *`%[name]`→`$N` template rewrite*, and
|
||||
(c) Sema validation rules. All three are fully specified below.
|
||||
* **Surface form (decided, §II.2):** `asm volatile { "tmpl", "=r" -> T, "r" = x, clobbers(.cc, .memory) }`
|
||||
— a brace block; `->` marks outputs / `=` marks inputs (no positional `:`
|
||||
sections); enum-literal `clobbers(.…)`; and N `-> Type` outputs return a
|
||||
**tuple** (sx has tuples — Zig caps at one output).
|
||||
* **Inline asm is never comptime-evaluable.** The interpreter must bail loudly
|
||||
(`bailDetail`), per CLAUDE.md's "no silent unimplemented arms" rule.
|
||||
* **One naming note:** sx already has a `sx asm <file>` *CLI subcommand*
|
||||
(`src/main.zig:203,386`) that emits a `.s` file. That is a compiler output
|
||||
mode, a different namespace from a language token. No conflict, but worth
|
||||
knowing so nobody confuses the two.
|
||||
|
||||
---
|
||||
|
||||
# PART I — How Zig implements inline assembly
|
||||
|
||||
All file references in Part I are under `~/projects/zig` (0.16-dev,
|
||||
commit `3deb86bafd`). Parser/AST/AstGen live in `lib/std/zig/`; Sema/AIR/codegen
|
||||
in `src/`.
|
||||
|
||||
## I.1 Surface syntax
|
||||
|
||||
The canonical example (`doc/langref/inline_assembly.zig`), a Linux x86_64 syscall:
|
||||
|
||||
```zig
|
||||
pub fn syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) usize {
|
||||
return asm volatile ("syscall"
|
||||
: [ret] "={rax}" (-> usize),
|
||||
: [number] "{rax}" (number),
|
||||
[arg1] "{rdi}" (arg1),
|
||||
[arg2] "{rsi}" (arg2),
|
||||
[arg3] "{rdx}" (arg3),
|
||||
: .{ .rcx = true, .r11 = true });
|
||||
}
|
||||
```
|
||||
|
||||
Grammar shape:
|
||||
|
||||
```
|
||||
asm volatile? ( <template-string>
|
||||
: <output-item> , <output-item> , ... # outputs (optional section)
|
||||
: <input-item> , <input-item> , ... # inputs (optional section)
|
||||
: <clobbers> ) # clobbers (optional section)
|
||||
|
||||
output-item : [name] "constraint" (-> Type) # asm result becomes the value
|
||||
| [name] "constraint" (lvalue) # asm writes through the pointer
|
||||
input-item : [name] "constraint" (expr)
|
||||
clobbers : .{ .reg0 = true, .reg1 = true } # struct literal (0.16-dev)
|
||||
```
|
||||
|
||||
Key semantics (from `doc/langref.html.in:4217-4300`):
|
||||
|
||||
* **`volatile`** marks side effects. Without it, an asm expression whose result
|
||||
is unused may be deleted. An asm expression with **no outputs must be
|
||||
`volatile`** (else compile error).
|
||||
* **x86/x86_64 use AT&T syntax** (LLVM provides the parser; Intel support is
|
||||
"buggy and not well tested").
|
||||
* **`%[name]`** in the template refers to a named operand's register; **`%%`** is
|
||||
a literal `%`.
|
||||
* **Clobbers** are registers the asm trashes that are *not* inputs/outputs.
|
||||
`"memory"` (the `.memory = true` field) means "writes to arbitrary memory."
|
||||
Failing to declare a clobber is unchecked illegal behavior.
|
||||
* **Global assembly** = an `asm(...)` in a namespace-level `comptime` block. It
|
||||
has *different rules*: `volatile` is forbidden, there are **no inputs/outputs/
|
||||
clobbers**, no `%` substitution, and all global asm is concatenated verbatim:
|
||||
|
||||
```zig
|
||||
// doc/langref/test_global_assembly.zig
|
||||
comptime {
|
||||
asm (
|
||||
\\.global my_func;
|
||||
\\.type my_func, @function;
|
||||
\\my_func:
|
||||
\\ lea (%rdi,%rsi,1),%eax
|
||||
\\ retq
|
||||
);
|
||||
}
|
||||
extern fn my_func(a: i32, b: i32) i32; // call into the global-asm symbol
|
||||
```
|
||||
|
||||
## I.2 Pipeline, stage by stage
|
||||
|
||||
### Tokenizer — `lib/std/zig/tokenizer.zig`
|
||||
|
||||
Two keywords in the `StaticStringMap`: `.{ "asm", .keyword_asm }` and
|
||||
`.{ "volatile", .keyword_volatile }`.
|
||||
|
||||
### AST — `lib/std/zig/Ast.zig`
|
||||
|
||||
Four node tags (`Ast.zig:3789-3817`):
|
||||
|
||||
* `asm_simple` — `asm(template)` only, no operands.
|
||||
* `@"asm"` — full form; `data` is `node_and_extra` → (template node, `ExtraIndex` to an `Asm`).
|
||||
* `asm_output` — `[a] "b" (-> Type)` or `[a] "b" (ident)`.
|
||||
* `asm_input` — `[a] "b" (expr)`.
|
||||
|
||||
The "full" view the rest of the compiler consumes (`Ast.zig:2797-2809`):
|
||||
|
||||
```zig
|
||||
pub const Asm = struct {
|
||||
ast: Components,
|
||||
volatile_token: ?TokenIndex,
|
||||
outputs: []const Node.Index,
|
||||
inputs: []const Node.Index,
|
||||
pub const Components = struct {
|
||||
asm_token: TokenIndex,
|
||||
template: Node.Index,
|
||||
items: []const Node.Index, // outputs ++ inputs, interleaved order preserved
|
||||
clobbers: Node.OptionalIndex, // a comptime expression (the struct literal)
|
||||
rparen: TokenIndex,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
The on-disk extra record (`Ast.zig:3969-3975`) stores `items_start/items_end`
|
||||
(a span into the node list), `clobbers` (optional node), and `rparen`.
|
||||
|
||||
### Parser — `lib/std/zig/Parse.zig`
|
||||
|
||||
`expectAsmExpr` (`Parse.zig:2771-2838`) implements the grammar:
|
||||
|
||||
```zig
|
||||
fn expectAsmExpr(p: *Parse) !Node.Index {
|
||||
const asm_token = p.assertToken(.keyword_asm);
|
||||
_ = p.eatToken(.keyword_volatile);
|
||||
_ = try p.expectToken(.l_paren);
|
||||
const template = try p.expectExpr();
|
||||
if (p.eatToken(.r_paren)) |rparen| { /* asm_simple */ }
|
||||
_ = try p.expectToken(.colon);
|
||||
// ... parse output items until a `:`/`)` ...
|
||||
const clobbers: Node.OptionalIndex = if (p.eatToken(.colon)) |_| clobbers: {
|
||||
// ... parse input items until a `:`/`)` ...
|
||||
_ = p.eatToken(.colon) orelse break :clobbers .none;
|
||||
break :clobbers (try p.expectExpr()).toOptional(); // clobbers = an expression
|
||||
} else .none;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
* `parseAsmOutputItem` (`Parse.zig:2840-2864`):
|
||||
`LBRACKET IDENT RBRACKET STRINGLITERAL LPAREN (MINUSRARROW TypeExpr | IDENT) RPAREN`.
|
||||
* `parseAsmInputItem` (`Parse.zig:2866-2883`):
|
||||
`LBRACKET IDENT RBRACKET STRINGLITERAL LPAREN Expr RPAREN`.
|
||||
* **Clobbers parse as a generic expression** (`(try p.expectExpr())`), not a
|
||||
string list — this is the 0.16-dev change. It is later coerced to a
|
||||
`std.lang.assembly.Clobbers` struct at Sema time.
|
||||
|
||||
### AST → ZIR — `lib/std/zig/AstGen.zig`
|
||||
|
||||
`asmExpr` (`AstGen.zig:8553-8669`) + `addAsm` (`12257-12310`). The ZIR payload
|
||||
(`lib/std/zig/Zir.zig:2531-2564`):
|
||||
|
||||
```zig
|
||||
pub const Asm = struct {
|
||||
src_node: Ast.Node.Offset,
|
||||
asm_source: NullTerminatedString, // template (string-literal case)
|
||||
output_type_bits: u32, // bit i = output i uses `-> T` (vs ptr)
|
||||
clobbers: Ref, // comptime ref → assembly.Clobbers value
|
||||
pub const Small = packed struct(u16) { is_volatile: bool, outputs_len: u7, inputs_len: u8 };
|
||||
pub const Output = struct { name: NullTerminatedString, constraint: NullTerminatedString, operand: Ref };
|
||||
pub const Input = struct { name: NullTerminatedString, constraint: NullTerminatedString, operand: Ref };
|
||||
};
|
||||
```
|
||||
|
||||
AstGen already enforces the structural rules:
|
||||
|
||||
* Global (container-level) asm: rejects `volatile`, rejects any
|
||||
outputs/inputs/clobbers (`AstGen.zig:8583-8587`).
|
||||
* Local asm: **"assembly expression with no output must be marked volatile."**
|
||||
* `outputs.len < 16`, `inputs.len < 32` (fit `Small.outputs_len`/`inputs_len`).
|
||||
* At most one output may use the `-> T` form ("inline assembly allows up to one
|
||||
output value"); `output_type_bits` records which.
|
||||
* Two ZIR tags: `.@"asm"` (string-literal template) vs `.asm_expr` (comptime
|
||||
expression template).
|
||||
|
||||
### ZIR → AIR (Sema) — `src/Sema.zig`
|
||||
|
||||
`zirAsm` (`Sema.zig:15044-15231`, dispatched at `1396-1397`). This is where all
|
||||
*semantic* validation happens. It:
|
||||
|
||||
* Resolves the template to a comptime string (`resolveConstString`).
|
||||
* **Global asm** (`func_index == .none`): asserts no operands, then
|
||||
`zcu.addGlobalAssembly(owner, asm_source)` and returns `.void_value`.
|
||||
* `requireRuntimeBlock` — local asm can't run at comptime.
|
||||
* Per output: if `-> T`, resolve the type, `ensureLayoutResolved`, set the
|
||||
expression's result type; else resolve the operand pointer. Validates:
|
||||
* **output type has a well-defined in-memory layout** (else error);
|
||||
* **cannot output to a `const` pointer** (`"asm cannot output to const '{s}'"`);
|
||||
* output must be a runtime value (no reference to a comptime var).
|
||||
* Per input: resolve operand, reject comptime-only refs, **coerce
|
||||
`comptime_int`→`usize`, `comptime_float`→`f64`**.
|
||||
* Clobbers: coerce the expression to `std.lang.assembly.Clobbers`, resolve to a
|
||||
comptime value.
|
||||
|
||||
The AIR payload (`src/Air.zig:1485-1497`):
|
||||
|
||||
```zig
|
||||
pub const Asm = struct {
|
||||
source_len: u32,
|
||||
inputs_len: u32,
|
||||
clobbers: InternPool.Index, // comptime assembly.Clobbers value
|
||||
flags: packed struct(u32) { outputs_len: u31, is_volatile: bool },
|
||||
};
|
||||
// trailing: out operand refs, in operand refs, then the template bytes and
|
||||
// (constraint\0 name\0) pairs packed into air_extra.
|
||||
```
|
||||
|
||||
### AIR → LLVM — `src/codegen/llvm/FuncGen.zig`
|
||||
|
||||
`airAssembly` (`FuncGen.zig:2473-2852`) is the crux. **This is the algorithm sx
|
||||
must port.** Three sub-tasks:
|
||||
|
||||
**(a) Assemble the LLVM constraint string.** Comma-separated. For each output:
|
||||
emit `=` (write-only) or `+` (read-write, recorded in `llvm_rw_vals`); a `*`
|
||||
prefix marks an *indirect* (memory) output passed as a pointer parameter; a
|
||||
non-indirect output contributes to the return type. The user's leading `=`/`+`
|
||||
in `constraint[0]` is consumed and re-emitted; the rest is copied with Zig
|
||||
commas rewritten to LLVM `|` (alternative constraints). Inputs are copied
|
||||
similarly (no `=`). Clobbers: iterate the `Clobbers` struct's bool fields as a
|
||||
bigint; for each `true` field emit `~{fieldname}` (via `appendConstraints`,
|
||||
which also expands target-specific aliases).
|
||||
|
||||
**(b) Rewrite the template** `%[name]` → LLVM positional `${N}` (state machine,
|
||||
`FuncGen.zig:2735-2802`):
|
||||
|
||||
| input | output | note |
|
||||
|---|---|---|
|
||||
| `$` | `$$` | escape LLVM's `$` |
|
||||
| `%%` | `%` | literal percent |
|
||||
| `%=` | `${:uid}` | unique id |
|
||||
| `%[name]` | `${N}` | `N` = position in `name_map` |
|
||||
| `%[name:mod]` | `${N:mod}` | with modifier |
|
||||
|
||||
`name_map` maps each operand's `[name]` to its positional index across all
|
||||
outputs+inputs.
|
||||
|
||||
**(c) Build & call.** Pick the LLVM function type:
|
||||
`return_count == 0` → `void`; `== 1` → the single return type; `> 1` → an
|
||||
anonymous struct of the return types. Then:
|
||||
|
||||
```zig
|
||||
const call = try self.wip.callAsm(
|
||||
attributes, llvm_fn_ty,
|
||||
.{ .sideeffect = is_volatile }, // Assembly.Info: sideeffect/alignstack/inteldialect/unwind
|
||||
rendered_template, llvm_constraints, llvm_param_values, "");
|
||||
```
|
||||
|
||||
`callAsm` (`lib/std/zig/llvm/Builder.zig:6131-6143`) is a thin wrapper that
|
||||
builds the asm constant (`asmValue`) and emits a normal `call`. In LLVM-C terms
|
||||
this is exactly `LLVMGetInlineAsm(...)` + `LLVMBuildCall2(...)`. Finally,
|
||||
non-indirect outputs are read back: with one return it's the call result; with
|
||||
several it's `extractvalue i` per output; indirect outputs were already written
|
||||
by the asm via their pointer parameter.
|
||||
|
||||
### C backend — `src/codegen/c.zig`
|
||||
|
||||
No `airAssembly` for *inline* asm in the C backend in this tree; only global asm
|
||||
flows out (as `module asm`). For sx this is irrelevant — sx only has an LLVM
|
||||
backend.
|
||||
|
||||
### Global asm & naked functions
|
||||
|
||||
* **Global asm** bypasses everything above: `Sema.addGlobalAssembly` accumulates
|
||||
the verbatim source; the LLVM object emits it via the module-level asm string
|
||||
(LLVM-C: `LLVMAppendModuleInlineAsm`). Symbols it defines are reached with
|
||||
`extern fn`.
|
||||
* **Naked functions** (`callconv(.naked)`) drop the prologue/epilogue; the body
|
||||
is entirely inline asm. This is an orthogonal calling-convention feature, not
|
||||
part of the asm expression itself.
|
||||
|
||||
---
|
||||
|
||||
# PART II — Proposal for sx
|
||||
|
||||
## II.1 Design principles
|
||||
|
||||
1. **Copy Zig's *semantic* model exactly**: a template + register/memory operands
|
||||
+ clobbers + a `volatile` flag; AT&T syntax via LLVM; "no-output asm must be
|
||||
volatile"; `%[name]` substitution; AT&T-by-default.
|
||||
2. **Copy the LLVM lowering exactly** (the constraint-string assembler + template
|
||||
rewriter from `FuncGen.zig` are reproduced verbatim in §II.6 — these are the
|
||||
parts where "inventing our own" would silently miscompile).
|
||||
3. **Diverge from Zig's *surface* syntax where sx has a better-fitting idiom**, and
|
||||
only there. The deviations (§II.2) are deliberate: a brace block instead of
|
||||
`( … )`; `->`/`=` operand markers instead of positional `:` sections; an
|
||||
enum-literal `clobbers(.…)` list; and — because sx has tuples and Zig does not —
|
||||
**true multiple return values** instead of Zig's one-output cap.
|
||||
|
||||
## II.2 sx surface syntax
|
||||
|
||||
`asm` is an **expression** (it yields the output value/tuple), introduced by a new
|
||||
`asm` keyword. The body is a **brace block** of comma-separated parts: a template
|
||||
string first, then operands, then an optional `clobbers(.…)` clause. Each operand
|
||||
is `[name]? "constraint" <role>`, where the role marker is:
|
||||
|
||||
* **`-> Type`** — an **output** that produces a value (joins the result).
|
||||
* **`-> @place`** — an output that writes through to existing storage (Phase 2).
|
||||
* **`= expr`** — an **input** (the value fed in).
|
||||
|
||||
`->` reuses sx's "produces" arrow (as in `(a: i32) -> i32`); `=` reuses sx's
|
||||
"is set to" binding. There are no positional `:` sections.
|
||||
|
||||
```sx
|
||||
// x86_64-linux — write(2) via syscall
|
||||
sys_write :: (fd: i64, buf: [*]u8, len: u64) -> i64 {
|
||||
return asm volatile {
|
||||
"syscall",
|
||||
"={rax}" -> i64, // output → the expression's value
|
||||
"{rax}" = 1, // SYS_write
|
||||
"{rdi}" = fd,
|
||||
"{rsi}" = buf,
|
||||
"{rdx}" = len,
|
||||
clobbers(.rcx, .r11, .memory),
|
||||
};
|
||||
}
|
||||
|
||||
// read a register, no inputs, named operand for %[out]
|
||||
sp :: () -> u64 {
|
||||
return asm { "mov %%rsp, %[out]", [out] "=r" -> u64 };
|
||||
}
|
||||
```
|
||||
|
||||
Multi-instruction templates use sx's existing **`#string` heredoc**
|
||||
(`src/lexer.zig:402`) or a multi-line `"..."` literal — no new lexer feature:
|
||||
|
||||
```sx
|
||||
serialize :: () {
|
||||
asm volatile {
|
||||
#string ATT
|
||||
mfence
|
||||
lfence
|
||||
ATT,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Outputs and the result type.** A `-> Type` output contributes one value to the
|
||||
asm expression's result; the count decides the shape:
|
||||
|
||||
| `-> Type` outputs | result | spelling |
|
||||
|---|---|---|
|
||||
| 0 | `void` (must be `volatile`) | `asm volatile { … }` |
|
||||
| 1 | that type `T` | `x := asm { …, "=r" -> T };` |
|
||||
| N | a **tuple** `(T1,…,Tn)` (declaration order) | `a, b := asm { … };` |
|
||||
|
||||
A `[name]` on an output becomes a **named tuple field** — the same name you'd use
|
||||
for `%[name]` does double duty:
|
||||
|
||||
```sx
|
||||
// sx has tuples, so asm gets real multiple return values (Zig caps you at one).
|
||||
divmod :: (n: u64, d: u64) -> (quot: u64, rem: u64) {
|
||||
return asm {
|
||||
"divq %[d]",
|
||||
[quot] "={rax}" -> u64, // → .quot (operand 0)
|
||||
[rem] "={rdx}" -> u64, // → .rem (operand 1)
|
||||
"{rax}" = n,
|
||||
"{rdx}" = 0,
|
||||
[d] "r" = d,
|
||||
clobbers(.cc),
|
||||
};
|
||||
}
|
||||
q, r := divmod(17, 5); // q = 3, r = 2
|
||||
```
|
||||
|
||||
### Deviations from Zig (each deliberate; semantics unchanged)
|
||||
|
||||
* **[DEVIATION 1 — brace block, not `( … )`.]** The asm body is `asm { … }`, a
|
||||
comma-separated brace block (trailing comma allowed, per `specs.md:226,501`),
|
||||
not Zig's parenthesised form. Braces read as "a block of code," which is what an
|
||||
asm template is; `#string` heredoc templates especially benefit. `asm` is a
|
||||
keyword, so `asm {` / `asm volatile {` is unambiguous.
|
||||
|
||||
* **[DEVIATION 2 — `->`/`=` operand markers, not `:` sections.]** Zig groups
|
||||
operands into positional `: outputs : inputs : clobbers` sections (count the
|
||||
colons; `: :` for an empty one). sx tags each operand by role instead — `-> Type`
|
||||
/ `-> @place` (output) and `= expr` (input) — so the list is flat,
|
||||
order-independent, with no positional colons. *(`<-` for inputs was considered
|
||||
and rejected: it can't be a global token without mis-lexing `a < -b`; `=` reuses
|
||||
an existing token and the existing "binding" meaning.)*
|
||||
|
||||
* **[DEVIATION 3 — clobbers are an enum-literal list `clobbers(.cc, .memory)`.]**
|
||||
Zig 0.16 uses a struct literal `: .{ .rcx = true }` coerced to a per-arch
|
||||
`std.lang.assembly.Clobbers`; older Zig used a string list. sx uses a dot-literal
|
||||
list, cleaner than both. **v1:** each `.name` is a dot-name lowered straight to
|
||||
`~{name}` (`.memory`/`.cc` are recognized specials; register names pass through
|
||||
verbatim; LLVM validates). **Phase 4:** upgrade `.name` to members of a
|
||||
compile-time-checked per-arch `Clobber` enum — *same syntax*, gains typo-checking.
|
||||
Note the call-looking `clobbers(…)` is a declarative clause, **not** a call —
|
||||
nothing executes; it only feeds the register allocator.
|
||||
|
||||
* **[DEVIATION 4 — `volatile` is a *contextual* keyword.]** sx's keyword set
|
||||
(`specs.md:168`) has neither `asm` nor `volatile`. `asm` becomes a real keyword;
|
||||
`volatile` appears *only* right after `asm`, so it can be recognized contextually
|
||||
(a plain identifier everywhere else), avoiding reserving it globally. The surface
|
||||
is byte-identical to Zig. (Alternative: reserve globally — simpler lexer, small
|
||||
source-compat risk. Recommend contextual.)
|
||||
|
||||
* **[DEVIATION 5 — multiple value-outputs return a tuple (sx ⊃ Zig).]** Zig allows
|
||||
at most one `-> T` output; the rest must be pointer/lvalue outputs. sx has
|
||||
tuples, so N `-> Type` outputs return `(T1,…,Tn)` (named when operands are
|
||||
named), destructured with `a, b := …`. A deliberate *improvement* over Zig,
|
||||
enabled by a feature Zig lacks, and maps onto LLVM's existing multi-output
|
||||
struct return (§II.6). The other output flavor — `-> @place` write-through, plus
|
||||
read-write (`"+r" -> @place`) and indirect-memory (`"=*m"`) outputs — is
|
||||
**Phase 2** (needs indirect-constraint handling); the value-tuple form does not.
|
||||
|
||||
* **[DEVIATION 6 — global asm is a top-level `asm { … }` declaration.]** sx has no
|
||||
namespace-level `comptime {}` block (it has `#run`, `specs.md:2598`), so global
|
||||
asm is a top-level statement:
|
||||
|
||||
```sx
|
||||
asm {
|
||||
#string ATT
|
||||
.global my_func
|
||||
.type my_func, @function
|
||||
my_func:
|
||||
lea (%rdi,%rsi,1), %eax
|
||||
retq
|
||||
ATT,
|
||||
};
|
||||
|
||||
my_func :: (a: i32, b: i32) -> i32 extern; // extern, no library — valid sx today
|
||||
```
|
||||
|
||||
Only the `comptime {}` wrapper is dropped; lowers to `LLVMAppendModuleInlineAsm`.
|
||||
|
||||
**Calling the asm symbol reuses the C-FFI *import* path** (no new mechanism for
|
||||
v1). A lib-less `extern` fn declaration (its library is optional; used in 50+
|
||||
stdlib sites, e.g. `chdir :: (path: [*]u8) -> i32 extern;`) emits exactly the
|
||||
artifact needed to *call into* the asm symbol — an external-linkage,
|
||||
**C-calling-convention**, raw-named, link-time-resolved declaration — the same
|
||||
thing Zig's `extern fn` produces (also C-callconv). The reverse direction (asm
|
||||
calling *back into* an sx function) is handled by `export`, the define-and-expose
|
||||
dual of `extern`.
|
||||
|
||||
Everything *semantic* — comptime-known template, register/memory constraints
|
||||
verbatim to LLVM, clobber meaning, "no-output ⇒ must be volatile," AT&T default,
|
||||
`%[name]`/`%%` substitution — is **identical to Zig**. Only the surface (block,
|
||||
`->`/`=`, `clobbers(.…)`, tuple returns) differs.
|
||||
|
||||
## II.3 sx AST
|
||||
|
||||
sx's AST is a pointer-based tagged union (`Data = union(enum)` at
|
||||
`src/ast.zig:13`, nodes built via `Parser.createNode`), much simpler than Zig's
|
||||
SoA `extra_data` scheme — so we can store slices directly. Add one arm to the
|
||||
`Node.Data` union (`src/ast.zig:13`):
|
||||
|
||||
```zig
|
||||
// in Node.Data union(enum):
|
||||
asm_expr: AsmExpr,
|
||||
|
||||
// new node struct, alongside the other expression node defs:
|
||||
pub const AsmExpr = struct {
|
||||
template: *Node, // string-literal / #string node (comptime string)
|
||||
is_volatile: bool = false,
|
||||
operands: []const AsmOperand, // declaration order preserved (= %N indexing)
|
||||
clobbers: []const []const u8, // dot-names from clobbers(.…): "rcx","cc","memory"
|
||||
};
|
||||
|
||||
pub const AsmOperand = struct {
|
||||
name: ?[]const u8 = null, // optional [name]; only needed for %[name]
|
||||
constraint: []const u8, // verbatim, e.g. "={rax}", "=r", "+r", "{rdi}", "r"
|
||||
role: Role,
|
||||
payload: *Node, // out_value → Type node; out_place/input → expr node
|
||||
|
||||
pub const Role = enum {
|
||||
out_value, // `-> Type` value output; N of these → a tuple result
|
||||
out_place, // `-> @place` write-through to existing storage (Phase 2)
|
||||
input, // `= expr`
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
A single flat `operands` list (not split into outputs/inputs) preserves source
|
||||
order — what the `%0`/`%[name]` indices and the LLVM constraint order key off. The
|
||||
result type is derived in Sema from the `out_value` operands (§II.5).
|
||||
|
||||
## II.4 sx parser
|
||||
|
||||
`asm` is parsed in expression position. sx dispatches primary expressions in
|
||||
`Parser.parsePrimary` (`src/parser.zig`); add a `.kw_asm` case (mirroring how
|
||||
existing keyword/`#`-directive expressions like `#run` are handled):
|
||||
|
||||
1. consume `asm`; contextually consume `volatile` if the next token is the word
|
||||
`volatile` (Deviation 4).
|
||||
2. `expect(.l_brace)`; parse the first element as the **template** expression.
|
||||
3. then a comma-separated list until `}`. Each element is either:
|
||||
* an **operand** — `[name]?` (a bracketed identifier), a string-literal
|
||||
constraint, then a role: `->` `Type` (out_value) · `->` `@`-place
|
||||
(out_place, Phase 2) · `=` `expr` (input); or
|
||||
* the **clobbers clause** — `clobbers` `(` `.`ident (`,` `.`ident)* `)`.
|
||||
4. allow a trailing comma; `expect(.r_brace)`;
|
||||
`createNode(start, .{ .asm_expr = … })`.
|
||||
|
||||
The first element is unambiguously the template (a string not followed by a role
|
||||
marker). `->` vs `=` after the constraint disambiguates output vs input; inside a
|
||||
`->` target, a leading `@` marks a write-through place vs a type.
|
||||
|
||||
Top-level/global asm (Deviation 6): recognize `asm {` at declaration scope and
|
||||
build a dedicated `asm_global` decl (template only — reject operands/`volatile`).
|
||||
|
||||
Lexer/token: add `kw_asm` to the `Token.Tag` enum + keyword `StaticStringMap` in
|
||||
`src/token.zig`; `volatile` and `clobbers` stay out of the global table
|
||||
(contextual). **No new operator tokens** — `->` (`arrow`), `=` (`equal`), `.`
|
||||
(`dot`) and `{}` already exist.
|
||||
|
||||
## II.5 sx Sema / typing
|
||||
|
||||
* **Result type** from the `out_value` operands (`-> Type`), in declaration order:
|
||||
0 → `void` (and the asm **must** be `volatile`); 1 → that operand's type `T`;
|
||||
N → a tuple `(T1,…,Tn)`, **named** when the operands carry `[name]`s
|
||||
(`(name1: T1, …)`), positional otherwise. Implement in the expression typer
|
||||
(`src/ir/expr_typer.zig` / wherever `inferExprType` lives), returning the resolved
|
||||
`TypeId` (a tuple `TypeId` for N>1). **Do not** fall back to a silent default — an
|
||||
unresolvable output type is a real error (CLAUDE.md silent-default rule): emit a
|
||||
diagnostic and return the project's `.unresolved` sentinel.
|
||||
* Port Zig's validation checklist (these are the user-facing error messages):
|
||||
1. no output operand ⇒ the asm **must** be `volatile`;
|
||||
2. each `out_value` result type must have a well-defined in-memory layout;
|
||||
3. inputs must be runtime values; coerce comptime int→`i64`, float→`f64`;
|
||||
4. template must be a comptime-known string;
|
||||
5. (Phase 2) `out_place` cannot write a `const`; indirect-memory rules.
|
||||
* Every `%[name]` referenced in the template must name an operand (best surfaced as
|
||||
a Sema diagnostic; also caught at codegen during the rewrite — §II.6).
|
||||
|
||||
Note: there is **no** "≤1 output" rule (that was Zig's limit; sx's tuples lift it).
|
||||
|
||||
## II.6 sx IR + LLVM codegen (the part that must match Zig bit-for-bit)
|
||||
|
||||
### IR op — `src/ir/inst.zig`
|
||||
|
||||
Add to `Op = union(enum)` (`src/ir/inst.zig:80`), next to `objc_msg_send`
|
||||
(`:219`). Strings are interned (`StringId`, as `const_string` at `:85`); operands
|
||||
are SSA `Ref`s:
|
||||
|
||||
```zig
|
||||
inline_asm: InlineAsm,
|
||||
|
||||
pub const InlineAsm = struct {
|
||||
template: StringId, // interned, RAW (rewritten at emit)
|
||||
operands: []const AsmOperand, // declaration order (= %N indexing)
|
||||
clobbers: []const StringId, // interned dot-names: "rcx","cc","memory"
|
||||
has_side_effects: bool,
|
||||
// result rides on Inst.ty: void / a scalar TypeId / a tuple TypeId (N outputs)
|
||||
};
|
||||
|
||||
pub const AsmOperand = struct {
|
||||
role: enum { out_value, out_place, input },
|
||||
name: StringId, // .none when unnamed
|
||||
constraint: StringId, // verbatim "={rax}" / "=r" / "+r" / "{rdi}"
|
||||
operand: Ref, // out_value → .none; out_place/input → the Ref
|
||||
};
|
||||
```
|
||||
|
||||
### Lowering — `src/ir/lower/expr.zig`
|
||||
|
||||
Add `.asm_expr => self.lowerAsmExpr(...)` to the `lowerExpr` dispatch. It interns
|
||||
the template + constraint strings + clobber names, lowers each input operand to a
|
||||
`Ref`, computes the result `TypeId` (§II.5), and emits the `inline_asm` op. (Same
|
||||
shape as the existing `objc_msg_send` lowering.)
|
||||
|
||||
### Emit — `src/ir/emit_llvm.zig`
|
||||
|
||||
Add `.inline_asm => self.emitInlineAsm(...)` to the `emitInst` dispatch. This is a
|
||||
**direct port of `FuncGen.airAssembly`**. Using the already-imported
|
||||
`llvm_api.c`:
|
||||
|
||||
```zig
|
||||
fn emitInlineAsm(self: *Emitter, inst: *const Inst, a: InlineAsm) void {
|
||||
// 1) result LLVM type + param types/values from constraints
|
||||
const ret_ty = self.lowerType(inst.ty); // void if no typed output
|
||||
var param_tys: ...; var args: ...; // one per `input` constraint
|
||||
// 2) assemble the LLVM constraint string (see algorithm below)
|
||||
// outputs first ("=..."/"+..."), then inputs, then "~{reg}" clobbers, comma-joined
|
||||
// 3) rewrite the template %[name]->${N}, %%->%, %=->${:uid}, $->$$ (state machine below)
|
||||
const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, n_params, 0);
|
||||
const asm_val = c.LLVMGetInlineAsm(
|
||||
fn_ty,
|
||||
rendered_template.ptr, rendered_template.len,
|
||||
constraint_str.ptr, constraint_str.len,
|
||||
@intFromBool(a.has_side_effects), // HasSideEffects (volatile)
|
||||
0, // IsAlignStack
|
||||
c.LLVMInlineAsmDialectATT, // AT&T (Deviation: none — matches Zig default)
|
||||
0, // CanThrow
|
||||
);
|
||||
const result = c.LLVMBuildCall2(self.builder, fn_ty, asm_val, args.ptr, n_params, "");
|
||||
self.mapRef(inst, result); // 1 output: the value; N: extractvalue i per out_value → tuple
|
||||
}
|
||||
```
|
||||
|
||||
(Optionally cache the asm value keyed by `(template, constraints, fn_ty)` the way
|
||||
`emit_llvm.zig:167` caches `objc_msg_send_value` — but per-site construction is
|
||||
fine; LLVM uniques inline-asm constants internally.)
|
||||
|
||||
**Constraint-string assembler (port of `FuncGen.airAssembly`):**
|
||||
|
||||
```
|
||||
parts = []
|
||||
for op in operands where role == out_value or out_place: # outputs first
|
||||
parts.append( op.constraint with ',' replaced by '|' ) # "={rax}", "=r", "+r" …
|
||||
for op in operands where role == input:
|
||||
parts.append( op.constraint with ',' replaced by '|' ) # "{rdi}", "r" …
|
||||
for name in clobbers: # from clobbers(.name,…)
|
||||
parts.append( "~{" + name + "}" ) # "~{rcx}", "~{cc}", "~{memory}"
|
||||
constraint_str = ",".join(parts)
|
||||
```
|
||||
|
||||
LLVM return type follows the `out_value` count: **0** → `void`; **1** → that type;
|
||||
**N** → an anonymous struct `{T1,…,Tn}` — after the call, `extractvalue i` per
|
||||
`out_value` builds the sx tuple (the multi-return path, §II.2 Dev 5). `out_place`
|
||||
outputs are `store`d through their `Ref` afterward instead.
|
||||
|
||||
For `sys_write` (one output): constraint
|
||||
`={rax},{rax},{rdi},{rsi},{rdx},~{rcx},~{r11},~{memory}`, `fn_ty = i64 (i64,ptr,i64)`,
|
||||
`args = [1, fd, buf, len]`, `sideeffect = true`. For `divmod` (two outputs):
|
||||
`={rax},={rdx},{rax},{rdx},r,~{cc}`, `fn_ty = {i64,i64} (i64,i64,i64)`, and the two
|
||||
`extractvalue`s become the `(quot, rem)` tuple.
|
||||
|
||||
**Template rewriter (port verbatim from `FuncGen.zig:2735-2802`):** state machine
|
||||
over the template bytes with a `name_map: [name] -> positional index` built from
|
||||
`outputs ++ inputs`:
|
||||
|
||||
```
|
||||
state start: '%' -> percent ; '$' -> emit "$$" ; else emit byte
|
||||
state percent: '%' -> emit '%', start
|
||||
'[' -> emit "${", state input
|
||||
'=' -> emit "${:uid}", start
|
||||
else -> emit '%', emit byte, start
|
||||
state input: ']' -> emit name_map[name], emit '}', start
|
||||
':' -> emit name_map[name], emit ':', state modifier
|
||||
else accumulate name
|
||||
state modifier:']' -> emit accumulated modifier, emit '}', start
|
||||
else accumulate
|
||||
```
|
||||
|
||||
An unknown `%[name]` is a hard error (mirror Zig's `todo`/diagnostic — **not** a
|
||||
silent pass-through; CLAUDE.md no-silent-arms rule).
|
||||
|
||||
### Interpreter — `src/ir/interp.zig`
|
||||
|
||||
Inline asm cannot be comptime-evaluated. In the interpreter's op switch:
|
||||
|
||||
```zig
|
||||
.inline_asm => return bailDetail("inline asm requires native execution; not available at comptime"),
|
||||
```
|
||||
|
||||
(Same `bailDetail` pattern as the Obj-C/JNI ops — surfaces `op=inline_asm: ...`
|
||||
rather than a silent default.)
|
||||
|
||||
### Global asm (Deviation 6)
|
||||
|
||||
Lower the top-level `asm_global` decl to a one-shot emit:
|
||||
`c.LLVMAppendModuleInlineAsm(module, src.ptr, src.len)` (present in the linked
|
||||
LLVM — `@19/include/llvm-c/Core.h:971`). No operands, no rewrite, no volatile;
|
||||
multiple blocks concatenate in source order (as Zig does).
|
||||
|
||||
**Calling into an asm-defined symbol needs no new machinery** — declare it with a
|
||||
lib-less `extern` (Deviation 6, §II.2): `my_func :: (sig) -> R extern;` emits
|
||||
an external-linkage, raw-named, C-ABI extern that the linker resolves against the
|
||||
`.global` the asm block defines.
|
||||
|
||||
**Guard (CLAUDE.md no-silent-arms):** a global-asm symbol exists only in the final
|
||||
linked binary, not in the `#run`/JIT host process. The interpreter resolves
|
||||
externs via `dlsym(RTLD_DEFAULT)` (`host_ffi.zig`), which won't find it — calling
|
||||
such a symbol at comptime must fail **loudly** (it should already, via the
|
||||
dlsym-miss diagnostic; pin it with a test). Edge case: a symbol referenced *only*
|
||||
by other asm/external code may need `llvm.used` / `.no_dead_strip` to survive
|
||||
dead-stripping; the common "sx references it" case is safe.
|
||||
|
||||
## II.7 Stage-to-file map (implementation checklist)
|
||||
|
||||
| Stage | Zig reference | sx file + insertion point | New code |
|
||||
|---|---|---|---|
|
||||
| Keyword | `tokenizer.zig` keywords | `src/token.zig` — `Token.Tag` + keyword `StaticStringMap` | `kw_asm` (+ contextual `volatile`) |
|
||||
| AST node | `Ast.zig:2797,3789` | `src/ast.zig:13,85,721` — `Node.Data` + new `AsmExpr`/`AsmOperand` | ~25 lines |
|
||||
| Parser | `Parse.zig:2771-2883` | `src/parser.zig` — `parsePrimary` `.kw_asm` case + global-asm at decl scope | ~120 lines |
|
||||
| Sema/typing | `Sema.zig:15044` | `src/ir/expr_typer.zig` (`inferExprType`) + validation | ~80 lines |
|
||||
| IR op | `Air.zig:1485`, `Zir.zig:2531` | `src/ir/inst.zig:80` — `inline_asm: InlineAsm` | ~25 lines |
|
||||
| Lowering | `AstGen.zig:8553` | `src/ir/lower/expr.zig` — `lowerExpr` `.asm_expr` case | ~60 lines |
|
||||
| LLVM emit | `FuncGen.zig:2473-2852` | `src/ir/emit_llvm.zig` — `emitInst` `.inline_asm` case | ~120 lines (constraint asm + template rewrite + `LLVMGetInlineAsm`/`BuildCall2`) |
|
||||
| Global asm | `Sema.addGlobalAssembly` + `module asm` | decl lowering → `c.LLVMAppendModuleInlineAsm` | ~15 lines |
|
||||
| Interp bail | n/a | `src/ir/interp.zig` op switch | 1 line |
|
||||
|
||||
No change to `src/codegen.zig` is needed (the IR/LLVM path owns this).
|
||||
|
||||
## II.8 Phasing
|
||||
|
||||
* **Phase 1 (MVP).** `asm { … }` block; `asm volatile`; string-literal/`#string`
|
||||
template; `= expr` inputs; `-> Type` outputs **including N→tuple multi-return**;
|
||||
`clobbers(.…)` dot-name list; `%[name]`/`%%` substitution; "no-output ⇒ volatile"
|
||||
check; AT&T. Target: Linux/macOS `x86_64` + `aarch64` syscalls, intrinsics, and
|
||||
multi-value ops (`divmod`, `cpuid`, `add_carry`).
|
||||
* **Phase 2.** `-> @place` write-through outputs, read-write (`"+r" -> @place`) and
|
||||
indirect-memory (`"=*m"`) constraints, `%=` unique-id, output-to-const rejection.
|
||||
* **Phase 3.** Global/module asm decl (`LLVMAppendModuleInlineAsm`) + the
|
||||
comptime-call guard, plus Intel-dialect opt-in. Small: the extern-call path
|
||||
already exists (lib-less `extern`).
|
||||
* **Phase 4 (optional).** Upgrade `clobbers(.name)` from dot-name sugar to a
|
||||
compile-time-checked per-architecture `Clobber` enum (typo-checking; same syntax).
|
||||
* **Phase 5 (optional).** Naked functions (`callconv`-equivalent) for full
|
||||
freestanding entry points.
|
||||
|
||||
## II.9 Testing
|
||||
|
||||
asm output is target-specific, so tests must pin a target and assert on
|
||||
emitted IR/exit, not run host-natively unless the host matches. Use the existing
|
||||
corpus harness and the **`16xx` platform block** (the closest fit in the
|
||||
`XXXX-category` scheme; `specs.md`/CLAUDE.md test-layout). Mirror Zig's own
|
||||
matrix:
|
||||
|
||||
* `examples/16xx-platform-asm-syscall-write.sx` — x86_64-linux write(2), assert exit/stdout.
|
||||
* `examples/16xx-platform-asm-register-read.sx` — `mov %%rsp,%[out]`, no-input output.
|
||||
* `examples/16xx-platform-asm-no-output-volatile.sx` — bare `asm volatile { "nop" }`.
|
||||
* `examples/16xx-platform-asm-missing-volatile.sx` — **expected compile error**
|
||||
(no output, no volatile) — pins the diagnostic.
|
||||
* `examples/16xx-platform-asm-template-subst.sx` — `%[a]`/`%%` rewriting, assert
|
||||
on the `sx ir`/`.s` snapshot.
|
||||
* `examples/16xx-platform-asm-multi-return.sx` — `divmod` → `(quot, rem)` tuple, destructured.
|
||||
* `examples/16xx-platform-asm-global.sx` (Phase 3) — global asm + extern call.
|
||||
|
||||
Add an IR/`.s` snapshot (`expected/*.ir`) for the substitution test so the
|
||||
constraint-string + template-rewrite output is locked. Seed markers and
|
||||
regenerate with `zig build test -Dupdate-goldens`, then review the diff
|
||||
(CLAUDE.md snapshot-integrity rule).
|
||||
|
||||
## II.10 Open decisions for the user
|
||||
|
||||
Largely settled through design review; what remains:
|
||||
|
||||
1. **Dialect:** AT&T only (Zig's default) for v1, or expose an Intel opt-in
|
||||
(`LLVMInlineAsmDialectIntel`) from the start? **Recommend AT&T-only v1.**
|
||||
2. **`volatile` keyword (Deviation 4):** contextual *(recommended, no
|
||||
source-compat risk)* vs globally reserved *(simpler lexer)*.
|
||||
3. **Brace separator:** comma *(recommended — trailing-comma-friendly,
|
||||
literal-style)* vs `;` *(matches sx statement blocks)*.
|
||||
4. **Asm-symbol extern spelling (Deviation 6): RESOLVED** — use the lib-less `extern`
|
||||
keyword to call *into* an asm symbol (import), and `export` for the reverse
|
||||
direction (an sx function asm can call *back into*). The dedicated linkage
|
||||
keywords landed (FFI-linkage stream), so no new surface is needed and both
|
||||
directions are covered.
|
||||
|
||||
*Decided:* brace block `{ … }` (Dev 1) · `->`/`=` markers, `:` sections dropped,
|
||||
`<-` rejected (Dev 2) · `clobbers(.…)` enum-literal list, dot-name sugar now →
|
||||
checked enum later (Dev 3) · multiple value-outputs return a tuple (Dev 5). For
|
||||
global asm (Dev 6) the call-*into*-asm direction reuses lib-less `extern` (Decision
|
||||
4, resolved).
|
||||
|
||||
## II.11 Risks
|
||||
|
||||
* **Constraint/template correctness is silent if wrong** — a bad constraint
|
||||
string miscompiles with no diagnostic. Mitigation: port Zig's assembler/rewrite
|
||||
verbatim (don't paraphrase) and lock IR snapshots in tests.
|
||||
* **Register-name validity is unchecked** in v1's `clobbers(.name)` dot-name form —
|
||||
a typo'd register (`.raxx`) surfaces only as an LLVM error. This is exactly the
|
||||
gap the Phase-4 checked `Clobber` enum closes; acceptable for v1 (LLVM validates
|
||||
the emitted `~{…}`).
|
||||
* **`#string` heredoc + AT&T `%`/`$`** interplay: ensure the heredoc delivers the
|
||||
template bytes literally (no sx-level escape processing of `%`/`$`) before the
|
||||
rewrite stage.
|
||||
* **Target gating:** asm examples must declare their target or they break the
|
||||
corpus on other hosts; the test plan pins targets.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A — exact LLVM-C calls (already reachable via `llvm_api.c`)
|
||||
|
||||
```c
|
||||
// src/llvm_api.zig @cInclude("llvm-c/Core.h") exposes all of these:
|
||||
LLVMValueRef LLVMGetInlineAsm(LLVMTypeRef Ty,
|
||||
const char *AsmString, size_t AsmStringSize,
|
||||
const char *Constraints, size_t ConstraintsSize,
|
||||
LLVMBool HasSideEffects, LLVMBool IsAlignStack,
|
||||
LLVMInlineAsmDialect Dialect, LLVMBool CanThrow); // LLVM 19 & 21: identical
|
||||
LLVMValueRef LLVMBuildCall2(LLVMBuilderRef, LLVMTypeRef, LLVMValueRef Fn,
|
||||
LLVMValueRef *Args, unsigned NumArgs, const char *Name);
|
||||
void LLVMAppendModuleInlineAsm(LLVMModuleRef M, const char *Asm, size_t Len); // global asm
|
||||
// enum: LLVMInlineAsmDialectATT, LLVMInlineAsmDialectIntel
|
||||
```
|
||||
|
||||
## Appendix B — file index
|
||||
|
||||
**Zig (reference, `~/projects/zig`):** `lib/std/zig/tokenizer.zig` (keywords) ·
|
||||
`lib/std/zig/Ast.zig:2797,3789,3969` (nodes) · `lib/std/zig/Parse.zig:2771-2883`
|
||||
(grammar) · `lib/std/zig/AstGen.zig:8553-8669,12257` + `lib/std/zig/Zir.zig:2531`
|
||||
(ZIR) · `src/Sema.zig:15044-15231` (validation) · `src/Air.zig:1485` (AIR) ·
|
||||
`src/codegen/llvm/FuncGen.zig:2473-2852` + `lib/std/zig/llvm/Builder.zig:6131`
|
||||
(LLVM) · `doc/langref/inline_assembly.zig`, `doc/langref/test_global_assembly.zig`
|
||||
(syntax) · `doc/langref.html.in:4217-4300` (spec).
|
||||
|
||||
**sx (target, `~/projects/sx`):** `src/token.zig` · `src/lexer.zig:402` (#string) ·
|
||||
`src/ast.zig:13` · `src/parser.zig` (`parsePrimary`), the optional `extern`
|
||||
library tail · `src/ir/expr_typer.zig` · `src/ir/inst.zig:80,219,260` ·
|
||||
`src/ir/lower/expr.zig` · `src/ir/module.zig:300` (`declareExtern`) ·
|
||||
`src/ir/emit_llvm.zig:167` (msgSend cache), `:1244` (extern⇒C-ABI), `:1279`
|
||||
(raw symbol name) · `src/ir/interp.zig` (`bailDetail`) · `src/llvm_api.zig:1-17` ·
|
||||
`build.zig:10` (LLVM@19).
|
||||
|
||||
## Appendix C — Cookbook (final form: `asm { … }`, `->`/`=`, `clobbers(.…)`, pure AT&T)
|
||||
|
||||
```sx
|
||||
// ── v1 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
asm volatile { "nop" }; // bare side-effecting
|
||||
|
||||
// write(2) syscall — register-pinned inputs, one value-output
|
||||
sys_write :: (fd: i64, buf: [*]u8, len: u64) -> i64 {
|
||||
return asm volatile {
|
||||
"syscall",
|
||||
"={rax}" -> i64,
|
||||
"{rax}" = 1, "{rdi}" = fd, "{rsi}" = buf, "{rdx}" = len,
|
||||
clobbers(.rcx, .r11, .memory),
|
||||
};
|
||||
}
|
||||
|
||||
// mmap — full 6-arg syscall ABI (arg4 in r10, not rcx)
|
||||
mmap :: (addr: *void, len: u64, prot: i32, flags: i32, fd: i32, off: i64) -> *void {
|
||||
return asm volatile {
|
||||
"syscall",
|
||||
"={rax}" -> *void,
|
||||
"{rax}" = 9, "{rdi}" = addr, "{rsi}" = len, "{rdx}" = prot,
|
||||
"{r10}" = flags, "{r8}" = fd, "{r9}" = off,
|
||||
clobbers(.rcx, .r11, .memory),
|
||||
};
|
||||
}
|
||||
|
||||
// AT&T scaled-index addressing — arr[i]
|
||||
load_idx :: (arr: *i64, i: u64) -> i64 {
|
||||
return asm {
|
||||
"movq (%[arr],%[i],8), %[out]",
|
||||
[out] "=r" -> i64, [arr] "r" = arr, [i] "r" = i,
|
||||
};
|
||||
}
|
||||
|
||||
// CPUID AVX probe — immediates, heavy clobber set, single value-result
|
||||
has_avx :: () -> bool {
|
||||
return asm volatile {
|
||||
#string ATT
|
||||
movl $1, %%eax
|
||||
cpuid
|
||||
andl $0x10000000, %%ecx
|
||||
setne %[ok]
|
||||
ATT,
|
||||
[ok] "=r" -> bool,
|
||||
clobbers(.rax, .rbx, .rcx, .rdx, .cc),
|
||||
};
|
||||
}
|
||||
|
||||
// SSE packed add — xmm regs, no outputs ⇒ volatile
|
||||
vadd4 :: (a: *f32, b: *f32, out: *f32) {
|
||||
asm volatile {
|
||||
#string ATT
|
||||
movups (%[a]), %%xmm0
|
||||
movups (%[b]), %%xmm1
|
||||
addps %%xmm1, %%xmm0
|
||||
movups %%xmm0, (%[out])
|
||||
ATT,
|
||||
[a] "r" = a, [b] "r" = b, [out] "r" = out,
|
||||
clobbers(.xmm0, .xmm1, .memory),
|
||||
};
|
||||
}
|
||||
|
||||
// ── multi-return (v1; sx has tuples, Zig caps at one output) ────────────────
|
||||
|
||||
// 64-bit divide → (quotient, remainder)
|
||||
divmod :: (n: u64, d: u64) -> (quot: u64, rem: u64) {
|
||||
return asm {
|
||||
"divq %[d]",
|
||||
[quot] "={rax}" -> u64,
|
||||
[rem] "={rdx}" -> u64,
|
||||
"{rax}" = n, "{rdx}" = 0, [d] "r" = d,
|
||||
clobbers(.cc),
|
||||
};
|
||||
}
|
||||
|
||||
// rdtsc → two 32-bit halves, destructured straight out of the asm
|
||||
rdtsc :: () -> u64 {
|
||||
lo, hi := asm volatile {
|
||||
"rdtsc",
|
||||
[lo] "={eax}" -> u32,
|
||||
[hi] "={edx}" -> u32,
|
||||
};
|
||||
return (xx hi << 32) | xx lo;
|
||||
}
|
||||
|
||||
// cpuid → a clean 4-tuple
|
||||
cpuid :: (leaf: u32, subleaf: u32) -> (eax: u32, ebx: u32, ecx: u32, edx: u32) {
|
||||
return asm volatile {
|
||||
"cpuid",
|
||||
[eax] "={eax}" -> u32, [ebx] "={ebx}" -> u32,
|
||||
[ecx] "={ecx}" -> u32, [edx] "={edx}" -> u32,
|
||||
"{eax}" = leaf, "{ecx}" = subleaf,
|
||||
};
|
||||
}
|
||||
|
||||
// add-with-carry → (sum, carry): value-output + tied input + flag capture
|
||||
add_carry :: (a: u64, b: u64) -> (sum: u64, carry: u8) {
|
||||
return asm {
|
||||
#string ATT
|
||||
addq %[b], %[sum]
|
||||
setc %[carry]
|
||||
ATT,
|
||||
[sum] "=r" -> u64,
|
||||
[carry] "=r" -> u8,
|
||||
[a] "0" = a, [b] "r" = b,
|
||||
clobbers(.cc),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Phase 2 (write-through / read-write / indirect) ─────────────────────────
|
||||
|
||||
// byte memcpy — labels, loop, read-write operands
|
||||
memcpy_bytes :: (dst: [*]u8, src: [*]u8, n: u64) {
|
||||
d := dst; s := src; c := n;
|
||||
asm volatile {
|
||||
#string ATT
|
||||
testq %[c], %[c]
|
||||
jz 2f
|
||||
1: movb (%[s]), %%al
|
||||
movb %%al, (%[d])
|
||||
incq %[s]
|
||||
incq %[d]
|
||||
decq %[c]
|
||||
jnz 1b
|
||||
2:
|
||||
ATT,
|
||||
[d] "+r" -> @d, [s] "+r" -> @s, [c] "+r" -> @c,
|
||||
clobbers(.rax, .cc, .memory),
|
||||
};
|
||||
}
|
||||
|
||||
// lock cmpxchg CAS — lock prefix, pinned read-write rax, two outputs
|
||||
cas :: (ptr: *i64, expected: i64, desired: i64) -> bool {
|
||||
old := expected; ok: bool = ---;
|
||||
asm volatile {
|
||||
#string ATT
|
||||
lock cmpxchgq %[desired], (%[ptr])
|
||||
sete %[ok]
|
||||
ATT,
|
||||
[ok] "=r" -> @ok,
|
||||
[old] "+{rax}" -> @old,
|
||||
[ptr] "r" = ptr,
|
||||
[desired] "r" = desired,
|
||||
clobbers(.cc, .memory),
|
||||
};
|
||||
return ok;
|
||||
}
|
||||
|
||||
// fill an existing struct (write-through, no tuple)
|
||||
cpuid_into :: (out: *CpuId, leaf: u32) {
|
||||
asm volatile {
|
||||
"cpuid",
|
||||
"={eax}" -> @out.eax, "={ebx}" -> @out.ebx,
|
||||
"={ecx}" -> @out.ecx, "={edx}" -> @out.edx,
|
||||
"{eax}" = leaf,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Global asm + extern (Phase 3):
|
||||
|
||||
```sx
|
||||
asm {
|
||||
#string ATT
|
||||
.global my_add
|
||||
my_add:
|
||||
lea (%rdi,%rsi,1), %eax
|
||||
retq
|
||||
ATT,
|
||||
};
|
||||
my_add :: (a: i32, b: i32) -> i32 extern; // lib-less extern = Zig's `extern fn`
|
||||
```
|
||||
21
editors/vscode/LICENSE
Normal file
21
editors/vscode/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 agra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
25
editors/vscode/README.md
Normal file
25
editors/vscode/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||

|
||||
|
||||
# sx for Visual Studio Code
|
||||
|
||||
Language support for the [sx programming language](https://git.swipelab.com/lab/sx).
|
||||
|
||||
## Features
|
||||
|
||||
- **Syntax highlighting** for `.sx` files, including embedded GLSL, SQL, HTML, and JSON blocks.
|
||||
- **Language server integration** — the extension launches the `sx` binary's language server (`sx lsp`) to provide editor intelligence.
|
||||
- **Breakpoints** registered for the `sx` language.
|
||||
|
||||
## Requirements
|
||||
|
||||
The `sx` compiler must be installed and on your `PATH` (or point the extension at it via the setting below). The extension shells out to it for the language server.
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `sx.lspPath` | `sx` | Path to the `sx` binary used to start the language server (`sx lsp`). |
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE) © agra
|
||||
BIN
editors/vscode/cover.png
Normal file
BIN
editors/vscode/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
editors/vscode/icon.png
Normal file
BIN
editors/vscode/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
3818
editors/vscode/package-lock.json
generated
3818
editors/vscode/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,26 @@
|
||||
"description": "Language support for the sx programming language",
|
||||
"version": "0.0.1",
|
||||
"publisher": "swipelab",
|
||||
"icon": "icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#000000",
|
||||
"theme": "dark"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.swipelab.com/lab/sx.git"
|
||||
},
|
||||
"homepage": "https://git.swipelab.com/lab/sx",
|
||||
"bugs": {
|
||||
"url": "https://git.swipelab.com/lab/sx/issues"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"license": "MIT",
|
||||
"activationEvents": [
|
||||
"onLanguage:sx"
|
||||
],
|
||||
@@ -73,13 +87,16 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"watch": "tsc -watch -p ."
|
||||
"watch": "tsc -watch -p .",
|
||||
"vscode:prepublish": "npm run build",
|
||||
"package": "vsce package --baseContentUrl https://git.swipelab.com/lab/sx/src/branch/master/editors/vscode --baseImagesUrl https://git.swipelab.com/lab/sx/raw/branch/master/editors/vscode"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.75.0",
|
||||
"@vscode/vsce": "^3.9.2",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -179,7 +179,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.other.directive.sx",
|
||||
"match": "#(?:run|import|insert|builtin|foreign|library)\\b"
|
||||
"match": "#(?:run|import|insert|builtin|library)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -193,6 +193,10 @@
|
||||
"name": "keyword.other.sx",
|
||||
"match": "\\b(enum|struct)\\b"
|
||||
},
|
||||
{
|
||||
"name": "storage.modifier.sx",
|
||||
"match": "\\b(extern|export)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.cast.sx",
|
||||
"match": "\\bxx\\b"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// trampoline's first read.
|
||||
//
|
||||
// The fix lives in `abiCoerceParamTypeEx`: the `string`/`slice` →
|
||||
// `ptr` collapse only applies to `is_extern` foreign decls (libc
|
||||
// `ptr` collapse only applies to `is_extern` extern decls (libc
|
||||
// interop). sx-internal `callconv(.c)` keeps the full slice
|
||||
// shape, which lands as `[2 x i64]` at the LLVM signature site
|
||||
// and matches the caller's two-register pass on AArch64.
|
||||
|
||||
@@ -193,12 +193,12 @@ sm_first :: (a: i32, b: i32) -> (i32, !) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// --- Foreign function binding ---
|
||||
// --- Extern function binding ---
|
||||
|
||||
// --- Foreign function binding ---
|
||||
// --- Extern function binding ---
|
||||
libc :: #library "c";
|
||||
|
||||
c_abs :: (n: i32) -> i32 #foreign libc "abs";
|
||||
c_abs :: (n: i32) -> i32 extern libc "abs";
|
||||
|
||||
// --- Protocol declarations (Phase 1: static dispatch only) ---
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
#import "modules/build.sx";
|
||||
|
||||
libc :: #library "c";
|
||||
popen :: (cmd: [:0]u8, mode: [:0]u8) -> *void #foreign libc;
|
||||
puts :: (s: [:0]u8) -> i32 #foreign libc;
|
||||
popen :: (cmd: [:0]u8, mode: [:0]u8) -> *void extern libc;
|
||||
puts :: (s: [:0]u8) -> i32 extern libc;
|
||||
|
||||
R :: struct { x: i32; }
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
|
||||
puts :: (s: [:0]u8) -> i32 #foreign libc;
|
||||
puts :: (s: [:0]u8) -> i32 extern libc;
|
||||
|
||||
cb :: () -> bool {
|
||||
a := format("{}", "x");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Real OS-argv accessor from `modules/std/cli.sx` (#foreign _NSGetArgv).
|
||||
// Real OS-argv accessor from `modules/std/cli.sx` (extern _NSGetArgv).
|
||||
//
|
||||
// Only DETERMINISTIC structural invariants are asserted — the actual arg
|
||||
// contents depend on how the test is invoked (under `sx run` the process
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// fix-0102c (issue 0102) F3 regression: two flat FILE imports each `#foreign`
|
||||
// fix-0102c (issue 0102) F3 regression: two flat FILE imports each `extern`
|
||||
// the SAME libc symbol under the SAME sx name `absval`. The bare-call resolver
|
||||
// must NOT count `#foreign` (non-plain) authors when deciding ambiguity — it
|
||||
// filters them out, returns "no rerouting", and the existing first-wins foreign
|
||||
// dispatch binds the call. A same-name foreign collision therefore compiles and
|
||||
// must NOT count `extern` (non-plain) authors when deciding ambiguity — it
|
||||
// filters them out, returns "no rerouting", and the existing first-wins extern
|
||||
// dispatch binds the call. A same-name extern collision therefore compiles and
|
||||
// runs (master behavior), it does NOT error as ambiguous.
|
||||
#import "modules/std.sx";
|
||||
#import "0729-modules-flat-same-name-foreign/a.sx";
|
||||
#import "0729-modules-flat-same-name-foreign/b.sx";
|
||||
#import "0729-modules-flat-same-name-extern/a.sx";
|
||||
#import "0729-modules-flat-same-name-extern/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("absval = {}\n", absval(-7));
|
||||
5
examples/0729-modules-flat-same-name-extern/a.sx
Normal file
5
examples/0729-modules-flat-same-name-extern/a.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// One of two flat authors of `absval`, a `extern` libc binding. A consumer
|
||||
// flat-importing BOTH must NOT see this as an ambiguous bare-call collision —
|
||||
// extern authors are never rerouted by the bare-call resolver, so the call
|
||||
// falls to the existing first-wins extern dispatch.
|
||||
absval :: (n: i32) -> i32 extern libc "abs";
|
||||
2
examples/0729-modules-flat-same-name-extern/b.sx
Normal file
2
examples/0729-modules-flat-same-name-extern/b.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
// The second flat author of `absval` — the identical `extern` libc binding.
|
||||
absval :: (n: i32) -> i32 extern libc "abs";
|
||||
@@ -1,5 +0,0 @@
|
||||
// One of two flat authors of `absval`, a `#foreign` libc binding. A consumer
|
||||
// flat-importing BOTH must NOT see this as an ambiguous bare-call collision —
|
||||
// foreign authors are never rerouted by the bare-call resolver, so the call
|
||||
// falls to the existing first-wins foreign dispatch.
|
||||
absval :: (n: i32) -> i32 #foreign libc "abs";
|
||||
@@ -1,2 +0,0 @@
|
||||
// The second flat author of `absval` — the identical `#foreign` libc binding.
|
||||
absval :: (n: i32) -> i32 #foreign libc "abs";
|
||||
@@ -9,7 +9,7 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
// Internal runtime symbol (library/vendors/sx_trace_runtime/sx_trace.c).
|
||||
sx_trace_len :: () -> u32 #foreign;
|
||||
sx_trace_len :: () -> u32 extern;
|
||||
|
||||
E :: error { Bad }
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
trace :: #import "modules/std/trace.sx";
|
||||
|
||||
// Buffer length probe (the runtime symbol; public read API is the trace module).
|
||||
sx_trace_len :: () -> u32 #foreign;
|
||||
sx_trace_len :: () -> u32 extern;
|
||||
|
||||
E :: error { BadInput, Overflow }
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// A reserved/builtin type name used as a PARAMETER name is rejected inside the
|
||||
// two method-with-body forms that carry their params as bare name lists rather
|
||||
// than `Param` nodes: a protocol default-body method (`u8`) and a sx-defined
|
||||
// foreign-class (`#objc_class`) method (`i16`). The declaration-site diagnostic
|
||||
// runtime-class (`#objc_class`) method (`i16`). The declaration-site diagnostic
|
||||
// underlines the OFFENDING PARAMETER itself, not the enclosing `protocol` /
|
||||
// `#objc_class` block — each method's `param_name_spans` is threaded from the
|
||||
// parser so the caret lands on the parameter token.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// reserved-name check, so a bare reserved-name function compiled silently and
|
||||
// became callable — bypassing the backtick rule that handwritten sx must use.
|
||||
// The backtick escape (`` `i2 :: … ``, examples/0153) is the only way to spell
|
||||
// these names; `#import c` foreign decls remain exempt (examples/1220).
|
||||
// these names; `#import c` extern decls remain exempt (examples/1220).
|
||||
//
|
||||
// Regression (issue 0089). Expected: one error per declaration, each caret on
|
||||
// the declared name; exit 1.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// examples/1140). Each is a declaration-name binding site: a bare reserved
|
||||
// spelling there mis-classifies and is rejected, exactly like `i2 := …`. The
|
||||
// backtick escape (`` `i2 :: struct{…} ``, examples/0154) is the only way to
|
||||
// spell these names in handwritten sx; `#import c` foreign decls stay exempt
|
||||
// spell these names in handwritten sx; `#import c` extern decls stay exempt
|
||||
// (examples/1220).
|
||||
//
|
||||
// Regression (issue 0089 — attempt-4: 0076 holds across every decl kind).
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
libc :: #library "c";
|
||||
// std/process.sx already binds getenv as `-> *u8`; this view disagrees.
|
||||
getenv_opt :: (name: [:0]u8) -> ?[:0]u8 #foreign libc "getenv";
|
||||
getenv_opt :: (name: [:0]u8) -> ?[:0]u8 extern libc "getenv";
|
||||
|
||||
main :: () -> i32 {
|
||||
p := getenv_opt("PATH");
|
||||
11
examples/1175-diagnostics-extern-export-conflict.sx
Normal file
11
examples/1175-diagnostics-extern-export-conflict.sx
Normal file
@@ -0,0 +1,11 @@
|
||||
// Phase 4 (FFI-linkage) interplay diagnostic: `extern` and `export` are the two
|
||||
// values of the same linkage axis — a declaration is either an import (`extern`)
|
||||
// or a definition (`export`), never both. The parser rejects the redundant
|
||||
// second keyword with a clear message (instead of the bare "expected ';'" the
|
||||
// body parser would otherwise emit).
|
||||
//
|
||||
// Expected: one error caret on the second keyword; exit 1.
|
||||
|
||||
f :: (a: i32) -> i32 extern export;
|
||||
|
||||
main :: () -> i32 { 0 }
|
||||
@@ -1,5 +1,5 @@
|
||||
// `callconv(.c)` on function pointers passed to foreign callbacks — ensures
|
||||
// the function uses C ABI so it can be safely invoked from `#foreign`
|
||||
// `callconv(.c)` on function pointers passed to extern callbacks — ensures
|
||||
// the function uses C ABI so it can be safely invoked from `extern`
|
||||
// functions like SDL_AddEventWatch.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Companion module for examples/94-foreign-global.sx (PLAN-FFI 0.10).
|
||||
// Declares the same `#foreign` extern global as the main file; the
|
||||
// Companion module for examples/1205-ffi-extern-global.sx (PLAN-FFI 0.10).
|
||||
// Declares the same `extern` extern global as the main file; the
|
||||
// linker should treat both decls as one symbol. We deliberately don't
|
||||
// READ `@__stdinp` from inside a helper fn body — that path is busted
|
||||
// today (see examples/issue-0037.sx) — we just expose a trivial fn so
|
||||
// this file participates in the link and the cross-file decl
|
||||
// coexistence is exercised.
|
||||
|
||||
__stdinp : *void #foreign;
|
||||
__stdinp : *void extern;
|
||||
|
||||
stdinp_addr_present :: () -> i32 {
|
||||
1
|
||||
@@ -1,29 +1,29 @@
|
||||
// Extern data globals via `<name> : <type> #foreign;`. Lets sx code
|
||||
// Extern data globals via `<name> : <type> extern;`. Lets sx code
|
||||
// reference libSystem / framework symbols (NSConcreteStackBlock,
|
||||
// __stdinp, etc.) for FFI bridges. Mirrors the long-standing
|
||||
// `<fn> :: (...) -> ... #foreign;` form on the function side.
|
||||
// `<fn> :: (...) -> ... extern;` form on the function side.
|
||||
//
|
||||
// Cross-file dimension (PLAN-FFI step 0.10): the helper companion
|
||||
// `94-foreign-global-helper.sx` ALSO declares `__stdinp : *void #foreign;`.
|
||||
// `1205-ffi-extern-global-helper.sx` ALSO declares `__stdinp : *void extern;`.
|
||||
// Both files referencing the same extern symbol must link cleanly —
|
||||
// LLVM dedupes the named global, the C linker resolves both refs to
|
||||
// the one libSystem symbol.
|
||||
//
|
||||
// We *don't* check that the helper computes the same address — see
|
||||
// issue-0037 (helper-function-scoped `@foreign_global` lowers to
|
||||
// issue-0037 (helper-function-scoped `@extern_global` lowers to
|
||||
// undef today). When that fixes, fold the helper's address back into
|
||||
// the equality check here.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "1205-ffi-foreign-global-helper.sx";
|
||||
#import "1205-ffi-extern-global-helper.sx";
|
||||
|
||||
__stdinp : *void #foreign;
|
||||
__stdinp : *void extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
addr_bits : u64 = xx @__stdinp;
|
||||
print("stdin extern global non-null: {}\n", addr_bits != 0);
|
||||
// Force the helper symbol to participate in linking (otherwise the
|
||||
// imported file's #foreign decl might get dropped by the
|
||||
// imported file's #extern decl might get dropped by the
|
||||
// dead-code stripper). The actual return value is busted today
|
||||
// — see issue-0037.
|
||||
_ := stdinp_addr_present();
|
||||
@@ -1,4 +1,4 @@
|
||||
// 16-byte integer-only struct passed by value through `#foreign`.
|
||||
// 16-byte integer-only struct passed by value through `extern`.
|
||||
//
|
||||
// emit_llvm.zig's `abiCoerceParamType` routes 9..16-byte non-HFA
|
||||
// structs through `[2 x i64]` for register-pair passing on AAPCS64 /
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
Pair64 :: struct { a: i64; b: i64; }
|
||||
|
||||
ffi_pair64_swap :: (p: Pair64) -> Pair64 #foreign;
|
||||
ffi_pair64_swap :: (p: Pair64) -> Pair64 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
p : Pair64 = .{ a = 1, b = 2 };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// `xx @<foreign_global>` round-trips through a non-main helper
|
||||
// `xx @<extern_global>` round-trips through a non-main helper
|
||||
// function: the helper's `xx @__stdinp` cast lowers to a `bitcast`
|
||||
// IR opcode that emit_llvm.zig dispatches to `LLVMBuildPtrToInt`
|
||||
// (BitCast doesn't accept ptr↔int on modern LLVM with opaque
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
__stdinp : *void #foreign;
|
||||
__stdinp : *void extern;
|
||||
|
||||
stdinp_addr_via_helper :: () -> u64 {
|
||||
xx @__stdinp
|
||||
@@ -1,5 +1,5 @@
|
||||
// Phase 0 baseline (PLAN-FFI.md step 0.1): every primitive type passed
|
||||
// in/out of a C `#foreign` fn via `#import c { #include / #source }`.
|
||||
// in/out of a C `extern` fn via `#import c { #include / #source }`.
|
||||
// Locks today's parameter + return ABI so Phase 1's lowering changes
|
||||
// (`#objc_call` / `#jni_call`) can't silently regress us.
|
||||
//
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Phase 0 baseline (PLAN-FFI.md step 0.2): small structs (≤16 bytes)
|
||||
// passed by value into a C `#foreign` fn and returned by value. Four
|
||||
// passed by value into a C `extern` fn and returned by value. Four
|
||||
// shapes that exercise distinct aggregate ABI paths:
|
||||
// Vec2 — 8 B, two f32 (register pair, float)
|
||||
// Vec4f — 16 B, four f32 (HFA — homogeneous float aggregate)
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
// `#source` only — c_import would rewrite struct-typed params/returns
|
||||
// in the .h to *void (its struct/opaque pointer default), losing the
|
||||
// by-value ABI. The hand-written #foreign decls below keep sx's
|
||||
// by-value ABI. The hand-written extern decls below keep sx's
|
||||
// struct types end-to-end.
|
||||
#import c {
|
||||
#source "1210-ffi-02-small-struct.c";
|
||||
@@ -26,21 +26,21 @@ Vec4f :: struct { x: f32; y: f32; z: f32; w: f32; }
|
||||
Pair64 :: struct { a: i64; b: i64; }
|
||||
Quad32 :: struct { a: i32; b: i32; c: i32; d: i32; }
|
||||
|
||||
ffi_vec2_make :: (x: f32, y: f32) -> Vec2 #foreign;
|
||||
ffi_vec2_swap :: (v: Vec2) -> Vec2 #foreign;
|
||||
ffi_vec2_sum :: (v: Vec2) -> f32 #foreign;
|
||||
ffi_vec2_make :: (x: f32, y: f32) -> Vec2 extern;
|
||||
ffi_vec2_swap :: (v: Vec2) -> Vec2 extern;
|
||||
ffi_vec2_sum :: (v: Vec2) -> f32 extern;
|
||||
|
||||
ffi_vec4f_make :: (x: f32, y: f32, z: f32, w: f32) -> Vec4f #foreign;
|
||||
ffi_vec4f_reverse :: (v: Vec4f) -> Vec4f #foreign;
|
||||
ffi_vec4f_sum :: (v: Vec4f) -> f32 #foreign;
|
||||
ffi_vec4f_make :: (x: f32, y: f32, z: f32, w: f32) -> Vec4f extern;
|
||||
ffi_vec4f_reverse :: (v: Vec4f) -> Vec4f extern;
|
||||
ffi_vec4f_sum :: (v: Vec4f) -> f32 extern;
|
||||
|
||||
ffi_pair64_make :: (a: i64, b: i64) -> Pair64 #foreign;
|
||||
ffi_pair64_swap :: (p: Pair64) -> Pair64 #foreign;
|
||||
ffi_pair64_sum :: (p: Pair64) -> i64 #foreign;
|
||||
ffi_pair64_make :: (a: i64, b: i64) -> Pair64 extern;
|
||||
ffi_pair64_swap :: (p: Pair64) -> Pair64 extern;
|
||||
ffi_pair64_sum :: (p: Pair64) -> i64 extern;
|
||||
|
||||
ffi_quad32_make :: (a: i32, b: i32, c: i32, d: i32) -> Quad32 #foreign;
|
||||
ffi_quad32_reverse :: (q: Quad32) -> Quad32 #foreign;
|
||||
ffi_quad32_sum :: (q: Quad32) -> i32 #foreign;
|
||||
ffi_quad32_make :: (a: i32, b: i32, c: i32, d: i32) -> Quad32 extern;
|
||||
ffi_quad32_reverse :: (q: Quad32) -> Quad32 extern;
|
||||
ffi_quad32_sum :: (q: Quad32) -> i32 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
// ── Vec2 (8 bytes, float pair) ─────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Phase 0 baseline (PLAN-FFI.md step 0.3): structs >16 bytes passed
|
||||
// by value into a C `#foreign` fn and returned by value. Exercises
|
||||
// by value into a C `extern` fn and returned by value. Exercises
|
||||
// the byval-pointer ABI path — the caller copies the struct onto its
|
||||
// stack and hands a pointer to the callee; on AAPCS64 the return
|
||||
// uses the indirect `x8` register; on SysV AMD64 the return is a
|
||||
@@ -25,14 +25,14 @@ Big48 :: struct {
|
||||
d: i64; e: i64; f: i64;
|
||||
}
|
||||
|
||||
ffi_big24_make :: (a: i64, b: i64, c: i64) -> Big24 #foreign;
|
||||
ffi_big24_rotate :: (v: Big24) -> Big24 #foreign;
|
||||
ffi_big24_sum :: (v: Big24) -> i64 #foreign;
|
||||
ffi_big24_make :: (a: i64, b: i64, c: i64) -> Big24 extern;
|
||||
ffi_big24_rotate :: (v: Big24) -> Big24 extern;
|
||||
ffi_big24_sum :: (v: Big24) -> i64 extern;
|
||||
|
||||
ffi_big48_make :: (a: i64, b: i64, c: i64,
|
||||
d: i64, e: i64, f: i64) -> Big48 #foreign;
|
||||
ffi_big48_reverse :: (v: Big48) -> Big48 #foreign;
|
||||
ffi_big48_sum :: (v: Big48) -> i64 #foreign;
|
||||
d: i64, e: i64, f: i64) -> Big48 extern;
|
||||
ffi_big48_reverse :: (v: Big48) -> Big48 extern;
|
||||
ffi_big48_sum :: (v: Big48) -> i64 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
// ── Big24 (24 bytes, byval pointer) ────────────────────────────
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
FQuad :: struct { a: f32; b: f32; c: f32; d: f32; }
|
||||
DQuad :: struct { a: f64; b: f64; c: f64; d: f64; }
|
||||
|
||||
ffi_fquad_make :: (a: f32, b: f32, c: f32, d: f32) -> FQuad #foreign;
|
||||
ffi_fquad_reverse :: (v: FQuad) -> FQuad #foreign;
|
||||
ffi_fquad_sum :: (v: FQuad) -> f32 #foreign;
|
||||
ffi_fquad_make :: (a: f32, b: f32, c: f32, d: f32) -> FQuad extern;
|
||||
ffi_fquad_reverse :: (v: FQuad) -> FQuad extern;
|
||||
ffi_fquad_sum :: (v: FQuad) -> f32 extern;
|
||||
|
||||
ffi_dquad_make :: (a: f64, b: f64, c: f64, d: f64) -> DQuad #foreign;
|
||||
ffi_dquad_reverse :: (v: DQuad) -> DQuad #foreign;
|
||||
ffi_dquad_sum :: (v: DQuad) -> f64 #foreign;
|
||||
ffi_dquad_make :: (a: f64, b: f64, c: f64, d: f64) -> DQuad extern;
|
||||
ffi_dquad_reverse :: (v: DQuad) -> DQuad extern;
|
||||
ffi_dquad_sum :: (v: DQuad) -> f64 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
// ── FQuad (16 B, 4×f32 HFA) ────────────────────────────────────
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
#source "1213-ffi-05-string-args.c";
|
||||
};
|
||||
|
||||
ffi_strlen :: (s: [:0]u8) -> i32 #foreign;
|
||||
ffi_first_byte :: (s: [:0]u8) -> i32 #foreign;
|
||||
ffi_sum_bytes :: (buf: [*]u8, len: i32) -> i32 #foreign;
|
||||
ffi_write_byte :: (buf: [*]u8, idx: i32, v: u8) -> void #foreign;
|
||||
ffi_static_greeting :: () -> [*]u8 #foreign;
|
||||
ffi_strlen :: (s: [:0]u8) -> i32 extern;
|
||||
ffi_first_byte :: (s: [:0]u8) -> i32 extern;
|
||||
ffi_sum_bytes :: (buf: [*]u8, len: i32) -> i32 extern;
|
||||
ffi_write_byte :: (buf: [*]u8, idx: i32, v: u8) -> void extern;
|
||||
ffi_static_greeting :: () -> [*]u8 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
// ── [:0]u8 null-terminated literal ─────────────────────────────
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
#source "1214-ffi-06-callback.c";
|
||||
};
|
||||
|
||||
ffi_apply_callback :: (cb: (i32) -> i32 callconv(.c), value: i32) -> i32 #foreign;
|
||||
ffi_apply_callback2 :: (cb: (*void, i32) -> i32 callconv(.c), ctx: *void, v: i32) -> i32 #foreign;
|
||||
ffi_apply_callback :: (cb: (i32) -> i32 callconv(.c), value: i32) -> i32 extern;
|
||||
ffi_apply_callback2 :: (cb: (*void, i32) -> i32 callconv(.c), ctx: *void, v: i32) -> i32 extern;
|
||||
|
||||
g_callback_hits : i32 = 0;
|
||||
g_callback_sum : i32 = 0;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// search branch (`<exe>/../../library` etc.), not by the CWD or
|
||||
// importing-file's-dir branches.
|
||||
//
|
||||
// `#include` triggers c_import.zig's auto-synthesis of `#foreign`
|
||||
// `#include` triggers c_import.zig's auto-synthesis of `extern`
|
||||
// fn decls from the C header; `#source` adds the .c to the build's
|
||||
// object list. Together they let the sx side call the C functions
|
||||
// by their declared names with no manual decls.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#include "1216-ffi-08-foreign-in-method.h"
|
||||
#include "1216-ffi-08-extern-in-method.h"
|
||||
|
||||
int ffi_method_helper(int x) { return x * 10; }
|
||||
@@ -1,4 +1,4 @@
|
||||
// Phase 0 baseline (PLAN-FFI.md step 0.8): `#foreign` C call sites
|
||||
// Phase 0 baseline (PLAN-FFI.md step 0.8): `extern` C call sites
|
||||
// embedded inside the major sx surface constructs. None of these
|
||||
// touch a new ABI shape — they only verify lowering routes the call
|
||||
// through identically regardless of the enclosing context:
|
||||
@@ -12,11 +12,11 @@
|
||||
#import "modules/build.sx";
|
||||
|
||||
#import c {
|
||||
#include "1216-ffi-08-foreign-in-method.h";
|
||||
#source "1216-ffi-08-foreign-in-method.c";
|
||||
#include "1216-ffi-08-extern-in-method.h";
|
||||
#source "1216-ffi-08-extern-in-method.c";
|
||||
};
|
||||
|
||||
// ── 1. Struct method calling a #foreign fn ───────────────────────────
|
||||
// ── 1. Struct method calling a #extern fn ───────────────────────────
|
||||
Counter :: struct {
|
||||
seed: i32 = 0;
|
||||
next :: (self: *Counter) -> i32 {
|
||||
@@ -26,7 +26,7 @@ Counter :: struct {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Protocol impl method calling a #foreign fn ────────────────────
|
||||
// ── 2. Protocol impl method calling a #extern fn ────────────────────
|
||||
Doubler :: protocol {
|
||||
doubled :: (self: *Self) -> i32;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ impl Doubler for Counter {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Closure body calling a #foreign fn ────────────────────────────
|
||||
// ── 3. Closure body calling a #extern fn ────────────────────────────
|
||||
make_adder :: (bias: i32) -> Closure(i32) -> i32 {
|
||||
closure((x: i32) -> i32 => ffi_method_helper(x) + bias)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "1217-ffi-09-foreign-result-chain.h"
|
||||
#include "1217-ffi-09-extern-result-chain.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
void *ffi_chain_make(int seed) {
|
||||
@@ -12,8 +12,8 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#include "1217-ffi-09-foreign-result-chain.h";
|
||||
#source "1217-ffi-09-foreign-result-chain.c";
|
||||
#include "1217-ffi-09-extern-result-chain.h";
|
||||
#source "1217-ffi-09-extern-result-chain.c";
|
||||
};
|
||||
|
||||
// Struct field hosts an FFI-returned handle.
|
||||
@@ -1,28 +0,0 @@
|
||||
// `#foreign` C-variadic tail: trailing `..args: []T` on a foreign fn maps
|
||||
// to the C calling convention's `...`. Extras at the call site are
|
||||
// passed via the variadic slot with the standard default argument
|
||||
// promotion (i8/i16/bool → i32, f32 → f64) applied implicitly.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#source "1218-ffi-foreign-cvariadic.c";
|
||||
};
|
||||
|
||||
sx_ffi_sum_ints :: (n: i32, ..args: []i32) -> i64 #foreign;
|
||||
sx_ffi_avg_doubles :: (n: i32, ..args: []f64) -> f64 #foreign;
|
||||
sx_ffi_count_args :: (tag: *u8, ..args: []*u8) -> i32 #foreign;
|
||||
|
||||
main :: () -> i32 {
|
||||
print("sum_ints(3, 10, 20, 30) = {}\n", sx_ffi_sum_ints(3, 10, 20, 30));
|
||||
print("sum_ints(0) = {}\n", sx_ffi_sum_ints(0));
|
||||
print("avg_doubles(2) = {}\n", sx_ffi_avg_doubles(2, 1.5, 2.5));
|
||||
print("avg_doubles(3) = {}\n", sx_ffi_avg_doubles(3, 1.0, 2.0, 3.0));
|
||||
|
||||
a := "alpha".ptr;
|
||||
b := "beta".ptr;
|
||||
g := "gamma".ptr;
|
||||
sentinel : *u8 = null;
|
||||
print("count_args(3 strs) = {}\n", sx_ffi_count_args("tag".ptr, a, b, g, sentinel));
|
||||
0
|
||||
}
|
||||
@@ -4,20 +4,20 @@
|
||||
#import "modules/std/test.sx";
|
||||
pkg :: #import "tests/fixtures/testpkg";
|
||||
|
||||
// --- Foreign function binding ---
|
||||
// --- Extern function binding ---
|
||||
libc :: #library "c";
|
||||
|
||||
c_abs :: (n: i32) -> i32 #foreign libc "abs";
|
||||
c_abs :: (n: i32) -> i32 extern libc "abs";
|
||||
|
||||
// --- Protocol declarations (Phase 1: static dispatch only) ---
|
||||
|
||||
main :: () {
|
||||
|
||||
// ========================================================
|
||||
// 15. FOREIGN FUNCTION BINDING
|
||||
// 15. EXTERN FUNCTION BINDING
|
||||
// ========================================================
|
||||
print("=== 15. Foreign ===\n");
|
||||
print("=== 15. Extern ===\n");
|
||||
|
||||
// Symbol rename: c_abs maps to C's abs()
|
||||
print("foreign-rename: {}\n", c_abs(xx -42));
|
||||
print("extern-rename: {}\n", c_abs(xx -42));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Foreign C declarations whose names collide with sx's reserved type spellings.
|
||||
/* Extern C declarations whose names collide with sx's reserved type spellings.
|
||||
The `#import c` exemption must accept these generated names unedited, both as
|
||||
parameter names (`i1`, `i2`) and as a FUNCTION name (`i2`) — and a foreign
|
||||
parameter names (`i1`, `i2`) and as a FUNCTION name (`i2`) — and an extern
|
||||
reserved-name function must be bare-callable (issue 0089). */
|
||||
int ffi_pick(int i1, int i2, int which);
|
||||
int ffi_sum(int i1, int i2);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// `#import c` foreign-name exemption: C names that collide with sx's reserved
|
||||
// type spellings import unedited. Foreign decls are treated as RAW — their names
|
||||
// are never type-classified nor reserved-checked — so the generated `#foreign`
|
||||
// `#import c` extern-name exemption: C names that collide with sx's reserved
|
||||
// type spellings import unedited. Extern decls are treated as RAW — their names
|
||||
// are never type-classified nor reserved-checked — so the generated `extern`
|
||||
// bindings import and call without hand-edits (no backticks needed). This covers
|
||||
// parameter names (`i1`/`i2`), a function whose own NAME is a reserved spelling
|
||||
// (`i2`), and bare-calling that function (its callee spelling parses as a type
|
||||
// but resolves to the foreign fn). Before issue 0089 the params errored with
|
||||
// but resolves to the extern fn). Before issue 0089 the params errored with
|
||||
// "'i1' is a reserved type name and cannot be used as an identifier", and the
|
||||
// bare call errored with "unresolved 'i2'".
|
||||
// Regression (issue 0089).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Foreign `-> [:0]u8` / `-> ?[:0]u8` returns: C hands back ONE `char *`;
|
||||
// Extern `-> [:0]u8` / `-> ?[:0]u8` returns: C hands back ONE `char *`;
|
||||
// the fat sx string is synthesized at the call boundary ({ptr, strlen};
|
||||
// NULL maps to the optional's null / an empty string) — issue 0128.
|
||||
// Pre-fix, the call read the pointer register pair as {ptr, len} and the
|
||||
@@ -6,9 +6,9 @@
|
||||
#import "modules/std.sx";
|
||||
|
||||
libc :: #library "c";
|
||||
err_text :: (code: i32) -> [:0]u8 #foreign libc "strerror";
|
||||
sig_text :: (sig: i32) -> ?[:0]u8 #foreign libc "strsignal";
|
||||
dlerror :: () -> ?[:0]u8 #foreign libc;
|
||||
err_text :: (code: i32) -> [:0]u8 extern libc "strerror";
|
||||
sig_text :: (sig: i32) -> ?[:0]u8 extern libc "strsignal";
|
||||
dlerror :: () -> ?[:0]u8 extern libc;
|
||||
|
||||
main :: () -> i32 {
|
||||
// plain: strerror(0) = "Undefined error: 0" on macOS — assert shape,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// The `cstring` type: ONE pointer to a null-terminated u8 buffer — C's
|
||||
// `char *`. Crosses #foreign boundaries verbatim in both directions;
|
||||
// `char *`. Crosses extern boundaries verbatim in both directions;
|
||||
// `?cstring` is the nullable case (null pointer = absent); string
|
||||
// LITERALS coerce implicitly (terminated constants); arbitrary strings
|
||||
// materialize via to_cstring; from_cstring is the zero-copy view back.
|
||||
#import "modules/std.sx";
|
||||
|
||||
libc :: #library "c";
|
||||
strerror_c :: (code: i32) -> cstring #foreign libc "strerror";
|
||||
getenv_c :: (name: cstring) -> ?cstring #foreign libc "getenv";
|
||||
dlerror_c :: () -> ?cstring #foreign libc "dlerror";
|
||||
strerror_c :: (code: i32) -> cstring extern libc "strerror";
|
||||
getenv_c :: (name: cstring) -> ?cstring extern libc "getenv";
|
||||
dlerror_c :: () -> ?cstring extern libc "dlerror";
|
||||
|
||||
main :: () -> i32 {
|
||||
// literal -> cstring param; cstring return -> view
|
||||
|
||||
14
examples/1223-ffi-extern-fn.sx
Normal file
14
examples/1223-ffi-extern-fn.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
// extern function binding (FFI-linkage stream, Phase 1): bind libc's `abs`
|
||||
// directly via the bare `extern` linkage modifier — no `extern`, no
|
||||
// `#library`. `extern` ⇒ external linkage + C ABI + no sx ctx; the symbol
|
||||
// resolves against the default-linked libc at link time. The sx name `abs`
|
||||
// IS the C symbol (no rename — the `extern LIB "csym"` forms land in 1.2).
|
||||
#import "modules/std.sx";
|
||||
|
||||
abs :: (n: i32) -> i32 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
print("abs(-7) = {}\n", abs(xx -7));
|
||||
print("abs(42) = {}\n", abs(xx 42));
|
||||
0
|
||||
}
|
||||
13
examples/1224-ffi-extern-fn-rename.sx
Normal file
13
examples/1224-ffi-extern-fn-rename.sx
Normal file
@@ -0,0 +1,13 @@
|
||||
// extern with a "csym" rename (FFI-linkage stream, Phase 1.2): the sx name
|
||||
// `c_abs` binds C's `abs` via the optional symbol-name override after the
|
||||
// `extern` keyword — mirrors `extern "abs"`. The optional `LIB` ident slot
|
||||
// (extern_lib) sits before the string; here it's omitted (libc is
|
||||
// default-linked).
|
||||
#import "modules/std.sx";
|
||||
|
||||
c_abs :: (n: i32) -> i32 extern "abs";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("c_abs(-42) = {}\n", c_abs(xx -42));
|
||||
0
|
||||
}
|
||||
15
examples/1225-ffi-extern-global.sx
Normal file
15
examples/1225-ffi-extern-global.sx
Normal file
@@ -0,0 +1,15 @@
|
||||
// extern data global (FFI-linkage stream, Phase 1.2): reference a symbol
|
||||
// defined elsewhere (here libSystem's __stdinp) via the bare `extern`
|
||||
// linkage modifier on a typed var decl — the extern-named counterpart of
|
||||
// `<name> : <type> extern;` (see examples/1205). The optional
|
||||
// `extern [LIB] ["csym"]` tail mirrors the fn form; bare here (the sx name
|
||||
// IS the C symbol, resolved against the default-linked libSystem).
|
||||
#import "modules/std.sx";
|
||||
|
||||
__stdinp : *void extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
addr_bits : u64 = xx @__stdinp;
|
||||
print("stdin extern global non-null: {}\n", addr_bits != 0);
|
||||
0
|
||||
}
|
||||
8
examples/1226-ffi-export-fn.c
Normal file
8
examples/1226-ffi-export-fn.c
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "1226-ffi-export-fn.h"
|
||||
|
||||
// Defined on the sx side via `export` — a plain C-ABI symbol, no sx context.
|
||||
extern int sx_square(int n);
|
||||
|
||||
int call_sx_square(int n) {
|
||||
return sx_square(n) + 1;
|
||||
}
|
||||
7
examples/1226-ffi-export-fn.h
Normal file
7
examples/1226-ffi-export-fn.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef SX_EXPORT_FN_H
|
||||
#define SX_EXPORT_FN_H
|
||||
|
||||
// Calls back into the sx-exported `sx_square` and adds 1.
|
||||
int call_sx_square(int n);
|
||||
|
||||
#endif
|
||||
28
examples/1226-ffi-export-fn.sx
Normal file
28
examples/1226-ffi-export-fn.sx
Normal file
@@ -0,0 +1,28 @@
|
||||
// export function (FFI-linkage stream, Phase 2): define an sx function with
|
||||
// the bare `export` linkage modifier — external linkage + C ABI + no sx ctx —
|
||||
// so a companion C translation unit can call back into it by its plain symbol
|
||||
// name. The C side (`#source`) declares `sx_square` as a normal `extern int`
|
||||
// and calls it; sx `main` drives the C side via `call_sx_square`. Mirrors the
|
||||
// import-direction `extern` examples (1223–1225) for the define direction.
|
||||
//
|
||||
// Without `export`, an sx-defined fn is `internal` linkage + carries the
|
||||
// implicit `__sx_ctx` slot, so the C object can neither resolve nor correctly
|
||||
// call the symbol — this is the gap `export` fills.
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#include "1226-ffi-export-fn.h";
|
||||
#source "1226-ffi-export-fn.c";
|
||||
};
|
||||
|
||||
// sx-defined, exported to C: external linkage + C ABI + no implicit ctx.
|
||||
sx_square :: (n: i32) -> i32 export {
|
||||
return n * n;
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
// call_sx_square (C) calls back into sx_square, adds 1.
|
||||
print("call_sx_square(6) = {}\n", call_sx_square(6));
|
||||
print("call_sx_square(9) = {}\n", call_sx_square(9));
|
||||
0
|
||||
}
|
||||
8
examples/1227-ffi-export-fn-rename.c
Normal file
8
examples/1227-ffi-export-fn-rename.c
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "1227-ffi-export-fn-rename.h"
|
||||
|
||||
// Defined on the sx side via `export "triple_c"` — a plain C-ABI symbol.
|
||||
extern int triple_c(int n);
|
||||
|
||||
int call_triple(int n) {
|
||||
return triple_c(n) + 1;
|
||||
}
|
||||
7
examples/1227-ffi-export-fn-rename.h
Normal file
7
examples/1227-ffi-export-fn-rename.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef SX_EXPORT_FN_RENAME_H
|
||||
#define SX_EXPORT_FN_RENAME_H
|
||||
|
||||
// Calls back into the sx-exported `triple_c` and adds 1.
|
||||
int call_triple(int n);
|
||||
|
||||
#endif
|
||||
23
examples/1227-ffi-export-fn-rename.sx
Normal file
23
examples/1227-ffi-export-fn-rename.sx
Normal file
@@ -0,0 +1,23 @@
|
||||
// export with a "csym" rename (FFI-linkage stream, Phase 2.2): the sx name
|
||||
// `sx_triple` is exposed to C under the symbol `triple_c` via the optional
|
||||
// symbol-name override after `export` — the define-direction mirror of
|
||||
// `extern "csym"` (1224). The companion C calls `triple_c` by that name; sx
|
||||
// `main` drives it via `call_triple`. Runs in AOT mode (see the `.aot`
|
||||
// marker) because a C->sx-by-name call cannot link against a JIT-resident
|
||||
// symbol.
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#include "1227-ffi-export-fn-rename.h";
|
||||
#source "1227-ffi-export-fn-rename.c";
|
||||
};
|
||||
|
||||
// sx-defined, exported to C under the C symbol `triple_c`.
|
||||
sx_triple :: (n: i32) -> i32 export "triple_c" {
|
||||
return n * 3;
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
print("call_triple(7) = {}\n", call_triple(7));
|
||||
0
|
||||
}
|
||||
14
examples/1228-ffi-extern-c-non-transitive.sx
Normal file
14
examples/1228-ffi-extern-c-non-transitive.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
// `#import` is non-transitive for C-import functions: main imports b,
|
||||
// b imports c, so main must NOT see c's lib-less `extern` C functions
|
||||
// directly. Referencing either is rejected by the C-import visibility
|
||||
// gate (lower/decl.zig `c_import_bare`) with a C-specific "not visible"
|
||||
// diagnostic — not the generic top-level-name wording. Two distinct
|
||||
// extern symbols pin that the gate fires per-symbol.
|
||||
#import "modules/std.sx";
|
||||
#import "1228-ffi-extern-c-non-transitive/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("{}\n", c_abs_one(-3));
|
||||
print("{}\n", c_abs_two(-4));
|
||||
return 0;
|
||||
}
|
||||
7
examples/1228-ffi-extern-c-non-transitive/b.sx
Normal file
7
examples/1228-ffi-extern-c-non-transitive/b.sx
Normal file
@@ -0,0 +1,7 @@
|
||||
// Intermediate module: directly imports c.sx, so BOTH of c's lib-less
|
||||
// C functions are legitimately visible here (the legal usage site).
|
||||
#import "c.sx";
|
||||
|
||||
b_use :: () -> i32 {
|
||||
return c_abs_one(-1) + c_abs_two(-2);
|
||||
}
|
||||
5
examples/1228-ffi-extern-c-non-transitive/c.sx
Normal file
5
examples/1228-ffi-extern-c-non-transitive/c.sx
Normal file
@@ -0,0 +1,5 @@
|
||||
// Two lib-less `extern` C-symbol imports: each declares a C function
|
||||
// resolved at link time with no library reference. Both are policed by
|
||||
// the non-transitive C-import visibility gate, per-symbol.
|
||||
c_abs_one :: (x: i32) -> i32 extern;
|
||||
c_abs_two :: (x: i32) -> i32 extern;
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <stdarg.h>
|
||||
|
||||
long long sx_ffi_sum_ints(int n, ...) {
|
||||
long long sx_ext_sum_ints(int n, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, n);
|
||||
long long total = 0;
|
||||
@@ -9,7 +9,7 @@ long long sx_ffi_sum_ints(int n, ...) {
|
||||
return total;
|
||||
}
|
||||
|
||||
double sx_ffi_avg_doubles(int n, ...) {
|
||||
double sx_ext_avg_doubles(int n, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, n);
|
||||
double total = 0.0;
|
||||
@@ -18,13 +18,3 @@ double sx_ffi_avg_doubles(int n, ...) {
|
||||
if (n == 0) return 0.0;
|
||||
return total / n;
|
||||
}
|
||||
|
||||
int sx_ffi_count_args(const char *tag, ...) {
|
||||
(void) tag;
|
||||
va_list ap;
|
||||
va_start(ap, tag);
|
||||
int count = 0;
|
||||
while (va_arg(ap, const char *) != 0) count++;
|
||||
va_end(ap);
|
||||
return count;
|
||||
}
|
||||
26
examples/1229-ffi-extern-cvariadic.sx
Normal file
26
examples/1229-ffi-extern-cvariadic.sx
Normal file
@@ -0,0 +1,26 @@
|
||||
// `extern` C-variadic tail: a trailing `..args: []T` on an `extern` fn
|
||||
// maps to the C calling convention's `...`. Extras at the call site pass through the variadic
|
||||
// slot with standard default argument promotion (i8/i16/bool → i32,
|
||||
// f32 → f64), NOT packed into an sx slice.
|
||||
//
|
||||
// Regression (FFI-linkage Part B): the `is_variadic` drop in
|
||||
// `declareFunction` + the call-site early-out in `packVariadicCallArgs`
|
||||
// were gated on `extern` only, so a migrated variadic `extern` lost
|
||||
// its `...` tail and slice-packed the extras (garbage at the C ABI).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#source "1229-ffi-extern-cvariadic.c";
|
||||
};
|
||||
|
||||
sx_ext_sum_ints :: (n: i32, ..args: []i32) -> i64 extern;
|
||||
sx_ext_avg_doubles :: (n: i32, ..args: []f64) -> f64 extern;
|
||||
|
||||
main :: () -> i32 {
|
||||
print("sum_ints(3, 10, 20, 30) = {}\n", sx_ext_sum_ints(3, 10, 20, 30));
|
||||
print("sum_ints(0) = {}\n", sx_ext_sum_ints(0));
|
||||
print("avg_doubles(2) = {}\n", sx_ext_avg_doubles(2, 1.5, 2.5));
|
||||
print("avg_doubles(3) = {}\n", sx_ext_avg_doubles(3, 1.0, 2.0, 3.0));
|
||||
0
|
||||
}
|
||||
21
examples/1230-ffi-extern-same-name-authors.sx
Normal file
21
examples/1230-ffi-extern-same-name-authors.sx
Normal file
@@ -0,0 +1,21 @@
|
||||
// Two flat FILE imports each declare the SAME libc symbol `absval` via the
|
||||
// `extern` keyword (the linkage-keyword twin of example 0729's `extern`
|
||||
// form). The bare-call resolver must NOT count extern authors when deciding
|
||||
// ambiguity — they are external C symbols, never rerouted by the bare-call
|
||||
// machinery, so the existing first-wins extern dispatch binds the
|
||||
// call and a same-name extern collision compiles + runs (prints 7), it does
|
||||
// NOT error as ambiguous.
|
||||
//
|
||||
// Regression (FFI-linkage Part B): `isPlainFreeFn` / `isPlainFreeFnDecl`
|
||||
// excluded a `extern` body but classified an empty-block `extern` fn as a
|
||||
// plain free function, so the two extern authors were wrongly counted as an
|
||||
// ambiguous bare-call collision. Prerequisite for migrating the fn-decl
|
||||
// `extern` path onto `extern`.
|
||||
#import "modules/std.sx";
|
||||
#import "1230-ffi-extern-same-name-authors/a.sx";
|
||||
#import "1230-ffi-extern-same-name-authors/b.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("absval = {}\n", absval(-7));
|
||||
0
|
||||
}
|
||||
6
examples/1230-ffi-extern-same-name-authors/a.sx
Normal file
6
examples/1230-ffi-extern-same-name-authors/a.sx
Normal file
@@ -0,0 +1,6 @@
|
||||
// One of two flat authors of `absval`, an `extern` libc binding — the
|
||||
// `extern` twin of example 0729's `extern libc "abs"`. A consumer
|
||||
// flat-importing BOTH must NOT see this as an ambiguous bare-call
|
||||
// collision: extern authors (external C symbols) are excluded from the
|
||||
// bare-call ambiguity verdict, exactly like their `extern` twins.
|
||||
absval :: (n: i32) -> i32 extern libc "abs";
|
||||
2
examples/1230-ffi-extern-same-name-authors/b.sx
Normal file
2
examples/1230-ffi-extern-same-name-authors/b.sx
Normal file
@@ -0,0 +1,2 @@
|
||||
// The second flat author of `absval` — the identical `extern` binding.
|
||||
absval :: (n: i32) -> i32 extern libc "abs";
|
||||
19
examples/1231-ffi-extern-undeclared-lib.sx
Normal file
19
examples/1231-ffi-extern-undeclared-lib.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
// An `extern LIB "csym"` reference must name something real, exactly like
|
||||
// its `extern LIB` twin (example 1620): `nosuchunit` names neither a
|
||||
// #library constant nor a named `#import c` unit, so this is a compile-time
|
||||
// diagnostic — the bogus library reference is caught BEFORE the symbol
|
||||
// would silently resolve through whatever image happens to carry it.
|
||||
//
|
||||
// Regression (FFI-linkage Part B): `checkExternRefs` validated only a
|
||||
// `extern` (extern-import shape) library_ref and skipped the `extern` keyword's
|
||||
// `extern_lib`, so a bogus `extern` lib reference compiled silently (the
|
||||
// symbol resolved via the default image and ran). Prerequisite for
|
||||
// migrating the fn-decl `extern` path onto `extern`.
|
||||
#import "modules/std.sx";
|
||||
|
||||
c_abs :: (n: i32) -> i32 extern nosuchunit "abs";
|
||||
|
||||
main :: () -> i32 {
|
||||
print("c_abs = {}\n", c_abs(-5));
|
||||
0
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
// Chained foreign-class method dispatch: `Cls.static().instance(...)`
|
||||
// Chained runtime-class method dispatch: `Cls.static().instance(...)`
|
||||
// resolves the inner call's return type so the outer dispatch's
|
||||
// receiver type is known. Pre-fix this collapsed to i64 in
|
||||
// `inferExprType`, the foreign_class_map lookup missed, and lowering
|
||||
// `inferExprType`, the runtime_class_map lookup missed, and lowering
|
||||
// emitted `error: unresolved 'init'` (or 'initWithWindowScene' etc.)
|
||||
// — see issues/0043 for the chess uikit.sx C4 migration that hit it.
|
||||
//
|
||||
// Two return-type shapes covered: explicit `*ClassName` (alloc here)
|
||||
// and `*Self` (init). Both must propagate through the chain so the
|
||||
// next `.method(...)` finds the foreign-class declaration.
|
||||
// next `.method(...)` finds the runtime-class declaration.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
|
||||
NSObject :: #foreign #objc_class("NSObject") {
|
||||
NSObject :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *NSObject;
|
||||
init :: (self: *Self) -> *Self;
|
||||
}
|
||||
|
||||
NSObjectSelfReturn :: #foreign #objc_class("NSObject") {
|
||||
NSObjectSelfReturn :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *Self;
|
||||
init :: (self: *Self) -> *Self;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// M1.0 (xfail) — '=>' expression-body form inside '#objc_class'
|
||||
// member methods.
|
||||
//
|
||||
// Today: parseForeignClassDecl ([src/parser.zig:1262]) accepts ';'
|
||||
// Today: parseRuntimeClassDecl ([src/parser.zig:1262]) accepts ';'
|
||||
// (declaration) or '{ ... }' (block body) but not '=>'. Trying
|
||||
// '=>' surfaces 'expected ;' at the arrow.
|
||||
//
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// `id`, `Class`, `SEL`, `BOOL` from `modules/ffi/objc.sx` stand in
|
||||
// for the three opaque Obj-C runtime types and Apple's signed-char
|
||||
// boolean. They resolve to `*void` / `i8` at the LLVM layer — no
|
||||
// runtime cost — but make foreign-class and call-site declarations
|
||||
// runtime cost — but make runtime-class and call-site declarations
|
||||
// read closer to Objective-C source.
|
||||
//
|
||||
// `Class(T)` parameterization (phantom T, `#extends`-aware
|
||||
@@ -15,8 +15,8 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
// Foreign-class declaration using the aliases at param/return positions.
|
||||
NSObjectAlias :: #foreign #objc_class("NSObject") {
|
||||
// Runtime-class declaration using the aliases at param/return positions.
|
||||
NSObjectAlias :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *Self;
|
||||
init :: (self: *Self) -> *Self;
|
||||
isKindOfClass :: (self: *Self, cls: Class) -> BOOL;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void #foreign objc;
|
||||
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void extern objc;
|
||||
|
||||
SxFoo :: #objc_class("SxFoo") {
|
||||
counter: i32;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
class_getMethodImplementation :: (cls: *void, sel: *void) -> *void #foreign objc;
|
||||
class_getMethodImplementation :: (cls: *void, sel: *void) -> *void extern objc;
|
||||
|
||||
SxFoo :: #objc_class("SxFoo") {
|
||||
counter: i32;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void #foreign objc;
|
||||
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void extern objc;
|
||||
|
||||
SxFoo :: #objc_class("SxFoo") {
|
||||
counter: i32;
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void #foreign objc;
|
||||
class_getMethodImplementation :: (cls: *void, sel: *void) -> *void #foreign objc;
|
||||
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void extern objc;
|
||||
class_getMethodImplementation :: (cls: *void, sel: *void) -> *void extern objc;
|
||||
|
||||
SxFoo :: #objc_class("SxFoo") {
|
||||
counter: i32;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// M1.3 — `obj.class` accessor on Obj-C pointers.
|
||||
//
|
||||
// Any Obj-C-class pointer (foreign or sx-defined) can be probed
|
||||
// Any Obj-C-class pointer (runtime or sx-defined) can be probed
|
||||
// for its runtime class object via `obj.class`. Lowers to
|
||||
// `object_getClass(obj)`. Returns `Class` (alias for *void —
|
||||
// parameterized `Class(T)` covariance is M1.1.b).
|
||||
//
|
||||
// Verifies both shapes:
|
||||
// 1. (*SxFoo).class — sx-defined class. Returns the SxFoo Class.
|
||||
// 2. (*NSObject).class — foreign class via stdlib. Returns NSObject's
|
||||
// 2. (*NSObject).class — runtime class via stdlib. Returns NSObject's
|
||||
// Class.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
NSObjectFwd :: #foreign #objc_class("NSObject") {
|
||||
NSObjectFwd :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *NSObjectFwd;
|
||||
init :: (self: *NSObjectFwd) -> *NSObjectFwd;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ main :: () -> i32 {
|
||||
expected_f : Class = objc_getClass("SxFoo".ptr);
|
||||
if cls_f != expected_f { print("FAIL: SxFoo.class mismatch\n"); return 1; }
|
||||
|
||||
// foreign class round-trip.
|
||||
// runtime class round-trip.
|
||||
nso := NSObjectFwd.alloc().init();
|
||||
cls_n : Class = nso.class;
|
||||
expected_n : Class = objc_getClass("NSObject".ptr);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
class_getClassMethod :: (cls: *void, sel: *void) -> *void #foreign objc;
|
||||
class_getClassMethod :: (cls: *void, sel: *void) -> *void extern objc;
|
||||
|
||||
SxFoo :: #objc_class("SxFoo") {
|
||||
counter: i32;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
NSObject :: #foreign #objc_class("NSObject") {
|
||||
NSObject :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *NSObject;
|
||||
init :: (self: *NSObject) -> *NSObject;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// M2.2 (first pass) — `#property` directive on foreign-class
|
||||
// M2.2 (first pass) — `#property` directive on runtime-class
|
||||
// fields synthesizes Obj-C-runtime getter/setter dispatch.
|
||||
//
|
||||
// field: T #property[(modifiers)];
|
||||
@@ -10,7 +10,7 @@
|
||||
// of the field name. Modifiers (strong, weak, copy, readonly, ...)
|
||||
// parse but don't yet drive ARC ops — that's Month 4.
|
||||
//
|
||||
// This slice covers FOREIGN-class properties. sx-defined property
|
||||
// This slice covers RUNTIME-class properties. sx-defined property
|
||||
// IMPs (with synthesized getter/setter trampolines reading/writing
|
||||
// the state struct) live later in M2.2.
|
||||
|
||||
@@ -30,8 +30,8 @@ probe_set_tag :: (self: *void, _cmd: *void, v: i32) callconv(.c) {
|
||||
g_probe_tag = v;
|
||||
}
|
||||
|
||||
// Foreign declaration with #property on `tag`.
|
||||
SxPropProbe :: #foreign #objc_class("SxPropProbe") {
|
||||
// Extern declaration with #property on `tag`.
|
||||
SxPropProbe :: #objc_class("SxPropProbe") extern {
|
||||
alloc :: () -> *SxPropProbe;
|
||||
init :: (self: *SxPropProbe) -> *SxPropProbe;
|
||||
tag: i32 #property;
|
||||
@@ -21,7 +21,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
class_getInstanceMethod :: (cls: *void, sel: *void) -> *void #foreign objc;
|
||||
class_getInstanceMethod :: (cls: *void, sel: *void) -> *void extern objc;
|
||||
|
||||
SxBox :: #objc_class("SxBox") {
|
||||
width: i32 #property;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// M2.3 — `#extends ForeignClass` method-resolution chaining.
|
||||
// M2.3 — `#extends a runtime class` method-resolution chaining.
|
||||
//
|
||||
// When `obj.method()` is called on a foreign-class pointer and
|
||||
// When `obj.method()` is called on a runtime-class pointer and
|
||||
// `method` isn't declared directly on the receiver's class, the
|
||||
// compiler walks the `#extends` chain to find an ancestor that
|
||||
// declared it. The runtime dispatch path is unchanged —
|
||||
@@ -12,13 +12,13 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
NSObjectBase :: #foreign #objc_class("NSObject") {
|
||||
NSObjectBase :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *NSObjectBase;
|
||||
init :: (self: *NSObjectBase) -> *NSObjectBase;
|
||||
hash :: (self: *NSObjectBase) -> u64;
|
||||
}
|
||||
|
||||
// Sx-defined class that extends a foreign one. M1.2 registers
|
||||
// Sx-defined class that extends a runtime one. M1.2 registers
|
||||
// the class at module init; `hash` is reached via the M2.3 chain
|
||||
// walk through NSObjectBase, then dispatched by objc_msgSend.
|
||||
SxThing :: #objc_class("SxThing") {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// `xx self` inside a BOOL-returning `#objc_class` method must
|
||||
// resolve to the full receiver pointer at a foreign-class method
|
||||
// resolve to the full receiver pointer at a runtime-class method
|
||||
// call site, not get truncated to i8 by the enclosing function's
|
||||
// BOOL return type. Regression locks in the
|
||||
// `resolveCallParamTypes` fix that threads foreign-class method
|
||||
// param types correctly even when the receiver is a `#foreign
|
||||
// `resolveCallParamTypes` fix that threads runtime-class method
|
||||
// param types correctly even when the receiver is a `extern
|
||||
// #objc_class` alias. Every probe round-trips the receiver pointer
|
||||
// — a regression would read only the low byte and the observer
|
||||
// pointer would appear as e.g. 0xC0 / 0x20 instead of its real
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
g_observer : *void = null;
|
||||
|
||||
// Stand-in for NSNotificationCenter — we just need a foreign-class
|
||||
// Stand-in for NSNotificationCenter — we just need a runtime-class
|
||||
// method with several *void args so the call site's arg-target-type
|
||||
// resolution exercises the same path as uikit.sx's keyboard observer.
|
||||
SxIssue44Bus :: #foreign #objc_class("NSNotificationCenter") {
|
||||
SxIssue44Bus :: #objc_class("NSNotificationCenter") extern {
|
||||
defaultCenter :: () -> *SxIssue44Bus;
|
||||
addObserver_selector_name_object :: (self: *Self, observer: *void, sel: *void, name: *void, obj: *void);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ SxIssue44Foo :: #objc_class("SxIssue44Foo") {
|
||||
// SxAppDelegate-shape: BOOL return + 2 extra *void args. Pre-fix,
|
||||
// the call to addObserver:... would receive `xx this` truncated to
|
||||
// its low byte (because resolveCallParamTypes returned `&.{}` for
|
||||
// foreign-class receivers and `self.target_type` leaked the BOOL
|
||||
// runtime-class receivers and `self.target_type` leaked the BOOL
|
||||
// return type into the call's args).
|
||||
appDelegate_options :: (this: *Self, app: *void, opts: *void) -> BOOL {
|
||||
bus := SxIssue44Bus.defaultCenter();
|
||||
@@ -87,7 +87,7 @@ main :: () -> i32 {
|
||||
print("me: WRONG\n");
|
||||
}
|
||||
|
||||
// The actual repro: BOOL return + foreign-class method call.
|
||||
// The actual repro: BOOL return + runtime-class method call.
|
||||
// Pre-fix: `xx this` truncated to i8, capture_observer receives
|
||||
// (low_byte_of_f) cast back to *void, which won't equal f_void.
|
||||
g_observer = null;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
NSPoint :: struct { x: f64; y: f64; }
|
||||
|
||||
// 16 B integer aggregate (Apple ARM64 — x0/x1 register pair, coerced
|
||||
// via `[2 x i64]` in our foreign-decl path; same trip-up that
|
||||
// via `[2 x i64]` in our extern-decl path; same trip-up that
|
||||
// issue-0036 surfaced).
|
||||
NSRange :: struct { location: u64; length: u64; }
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Register a runtime-built Obj-C class with a method that returns
|
||||
// a fixed `Triple`. The IMP is a plain sx fn (callconv .c) — its
|
||||
// sret-shaped lowering already works (Phase 0.3 fix for plain
|
||||
// `#foreign` returns). The `#objc_call` dispatch side now produces
|
||||
// `extern` returns). The `#objc_call` dispatch side now produces
|
||||
// the matching call shape: `call void @objc_msgSend(ptr sret %slot,
|
||||
// ...)` + load. The two halves must agree on the ABI for the
|
||||
// round-trip to return the right bytes.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
SxProbeNiladic :: #foreign #objc_class("SxProbeNiladic") {
|
||||
SxProbeNiladic :: #objc_class("SxProbeNiladic") extern {
|
||||
length :: (self: *Self) -> i32;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
SxProbeOneArg :: #foreign #objc_class("SxProbeOneArg") {
|
||||
SxProbeOneArg :: #objc_class("SxProbeOneArg") extern {
|
||||
addObject :: (self: *Self, val: i32) -> i32;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
SxProbeMultiKeyword :: #foreign #objc_class("SxProbeMultiKeyword") {
|
||||
SxProbeMultiKeyword :: #objc_class("SxProbeMultiKeyword") extern {
|
||||
combine_and :: (self: *Self, a: i32, b: i32) -> i32;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
|
||||
SxProbeMismatch :: #foreign #objc_class("SxProbeMismatch") {
|
||||
SxProbeMismatch :: #objc_class("SxProbeMismatch") extern {
|
||||
something_extra :: (self: *Self, x: i32) -> i32;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//
|
||||
// Mirrors JNI's static-dispatch surface (`Alias.new(...)` etc.); the
|
||||
// lowering disambiguates static vs instance by looking at
|
||||
// `method.is_static` on the foreign-class member.
|
||||
// `method.is_static` on the runtime-class member.
|
||||
//
|
||||
// Uses NSObject because the cached class slot is populated by a
|
||||
// constructor at module-load — runtime-created test classes wouldn't
|
||||
@@ -15,7 +15,7 @@
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
|
||||
NSObject :: #foreign #objc_class("NSObject") {
|
||||
NSObject :: #objc_class("NSObject") extern {
|
||||
// `+(Class)class` — niladic, name verbatim, selector = "class".
|
||||
// Returns the class object itself. No `self: *Self` first param ⇒
|
||||
// class method (sx parser keys on the param TYPE).
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
|
||||
NSObject :: #foreign #objc_class("NSObject") {
|
||||
NSObject :: #objc_class("NSObject") extern {
|
||||
// Default mangling would yield selector "gimme" — NSObject has no
|
||||
// such IMP. The override pins it to the real selector
|
||||
// "description". Static method (no `self: *Self` first param).
|
||||
@@ -25,7 +25,7 @@ NSObject :: #foreign #objc_class("NSObject") {
|
||||
// only on this side — main only invokes the static path because we
|
||||
// don't have a real NSDictionary in scope, but the declaration locks
|
||||
// in the parser + AST + lowering wiring for the multi-arg shape.
|
||||
NSDictionary :: #foreign #objc_class("NSDictionary") {
|
||||
NSDictionary :: #objc_class("NSDictionary") extern {
|
||||
lookup :: (self: *Self, key: *void) -> *void #selector("objectForKey:");
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#import "modules/build.sx";
|
||||
#import "modules/ffi/objc.sx";
|
||||
|
||||
SxManglingProbe :: #foreign #objc_class("SxManglingProbe") {
|
||||
SxManglingProbe :: #objc_class("SxManglingProbe") extern {
|
||||
length :: (self: *Self) -> i32;
|
||||
addObject :: (self: *Self, a: i32) -> i32;
|
||||
combine_and :: (self: *Self, a: i32, b: i32) -> i32;
|
||||
|
||||
28
examples/1348-ffi-objc-extern-class.sx
Normal file
28
examples/1348-ffi-objc-extern-class.sx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Phase 3.0 (FFI-linkage) — postfix `extern` on an aggregate (`#objc_class`)
|
||||
// is the new spelling of the legacy prefix `#objc_class(…) extern` import.
|
||||
// Mirrors 1306's runtime-class chained dispatch with the new syntax:
|
||||
// Name :: #objc_class("X") extern { … } == Name :: #objc_class(…) extern("X") { … }
|
||||
//
|
||||
// Red until 3.1 wires the postfix-extern aggregate path through the parser
|
||||
// + lowering (maps `extern` → reference, same as `extern`).
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/build.sx";
|
||||
|
||||
NSObject :: #objc_class("NSObject") extern {
|
||||
alloc :: () -> *NSObject;
|
||||
init :: (self: *Self) -> *Self;
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
inline if OS == .macos {
|
||||
a := NSObject.alloc().init();
|
||||
if a != null {
|
||||
print("extern-class dispatch ok\n");
|
||||
}
|
||||
}
|
||||
inline if OS != .macos {
|
||||
print("extern-class dispatch ok\n");
|
||||
}
|
||||
0
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user