Compare commits
139 Commits
39488133c9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ded106333b | ||
|
|
b6a7378af4 | ||
|
|
0e0ee40528 | ||
|
|
066ba54346 | ||
|
|
79042ab9ab | ||
|
|
17e3b91eb9 | ||
|
|
a0face7571 | ||
|
|
10f4137cbd | ||
|
|
c187122531 | ||
|
|
1346a2d020 | ||
|
|
e7eeecc0f3 | ||
|
|
b4d1ce78c3 | ||
|
|
73f5f0ed11 | ||
|
|
ab7fc393b6 | ||
|
|
66e1e39418 | ||
|
|
e954f044d8 | ||
|
|
d5aee7a222 | ||
|
|
cb6c032c58 | ||
|
|
2a43713d7f | ||
|
|
59469f2b2f | ||
|
|
cdd920b692 | ||
|
|
9e7661b915 | ||
|
|
2a954ceeb6 | ||
|
|
c760b92548 | ||
|
|
97a4050462 | ||
|
|
4128416d48 | ||
|
|
335ac52374 | ||
|
|
967005621a | ||
|
|
b8800a234c | ||
|
|
4d75b9323c | ||
|
|
d3c6ffed5a | ||
|
|
5a5e04c6d5 | ||
|
|
6c08de8ec1 | ||
|
|
5f444aae26 | ||
|
|
1040b8c776 | ||
|
|
f8e029d719 | ||
|
|
3c9ecd0b42 | ||
|
|
c92d11e748 | ||
|
|
0095584105 | ||
|
|
c88f4fbcef | ||
|
|
d6a9c4f0c4 | ||
|
|
fe9bd75e09 | ||
|
|
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 | ||
|
|
e386a0d0b4 | ||
|
|
4d32a4d4fb | ||
|
|
8c47268539 | ||
|
|
d3f5cb20cb | ||
|
|
45befed698 | ||
|
|
f13f4abfb1 | ||
|
|
ab3c9202ff |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.vsix filter=lfs diff=lfs merge=lfs -text
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,5 +3,4 @@ zig-out
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.sx-cache
|
||||
.sx-tmp
|
||||
current/
|
||||
.sx-tmp
|
||||
70
CLAUDE.md
70
CLAUDE.md
@@ -405,7 +405,10 @@ any can be advanced independently.
|
||||
After any code change:
|
||||
```sh
|
||||
zig build # must compile
|
||||
zig build test # must pass
|
||||
zig build test # must pass — runs the Zig unit tests
|
||||
# AND the full examples/ + issues/
|
||||
# regression corpus (a failing example
|
||||
# fails the build)
|
||||
```
|
||||
|
||||
After completing a phase's final step, run the phase's end-to-end verification command listed in `current/PLAN.md`.
|
||||
@@ -415,9 +418,25 @@ After completing a phase's final step, run the phase's end-to-end verification c
|
||||
After any compiler change:
|
||||
|
||||
1. **Build**: `zig build && zig build test`
|
||||
2. **Run regression tests**: `bash tests/run_examples.sh`
|
||||
- Every test must show `ok` (currently 324)
|
||||
- Zero failures, zero timeouts
|
||||
- `zig build test` runs the unit tests **and** the example/issue corpus as
|
||||
one suite — a failing example fails the build. The corpus is driven by a
|
||||
pure-Zig test (`src/corpus_run.test.zig`) that spawns the installed `sx`
|
||||
binary per example (subprocess-isolated, with a per-run timeout), so no
|
||||
shell script is involved.
|
||||
2. **Regenerate snapshots**: `zig build test -Dupdate-goldens`
|
||||
- Flips the corpus test to write each example's expected
|
||||
`.exit`/`.stdout`/`.stderr` (+ `.ir` where one already exists) from
|
||||
freshly-normalized output instead of asserting against it. This is the
|
||||
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").
|
||||
`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). Per-example
|
||||
build/run directives live in an optional `expected/<name>.build` **JSON** sidecar
|
||||
(see "Test layout" below): `{ "aot": true }` 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); `{ "target":
|
||||
"x86_64-linux" }` threads `--target` and arch-gates the example.
|
||||
|
||||
### Test layout
|
||||
|
||||
@@ -436,6 +455,7 @@ split into three streams (no more merged `2>&1`) plus an optional IR snapshot:
|
||||
<root>/expected/XXXX-category-name.stdout # normalized stdout
|
||||
<root>/expected/XXXX-category-name.stderr # normalized stderr
|
||||
<root>/expected/XXXX-category-name.ir # optional `sx ir` snapshot
|
||||
<root>/expected/XXXX-category-name.build # optional JSON build/run directives
|
||||
```
|
||||
|
||||
A test is any `<name>.sx` with an `expected/<name>.exit` marker. The runner
|
||||
@@ -443,14 +463,28 @@ scans two roots: `examples/` (the feature suite) and `issues/` (pinned bug
|
||||
repros). Multi-file tests keep companions (`.c`/`.h`, imported `.sx`, fixture
|
||||
dirs) under the same `XXXX-` prefix.
|
||||
|
||||
The optional `<name>.build` JSON sidecar carries per-example directives
|
||||
(unknown keys are a hard error — never silently ignored):
|
||||
|
||||
- `"aot": true` — build a native binary and execute it instead of JIT `sx run`.
|
||||
- `"target": "<triple|shorthand>"` — thread `--target` into every `sx`
|
||||
invocation and gate on the host. If the target's arch+os **match** the host,
|
||||
the example runs normally; if they **mismatch** (e.g. `x86_64-linux` on an
|
||||
aarch64 host), the runner switches to **ir-only** mode — it skips
|
||||
run/build/exec and asserts only `.exit` + `.ir` + `.stderr` from
|
||||
`sx ir --target` (`.stdout` is not asserted). An `.ir` snapshot is **required**
|
||||
in ir-only mode (its absence is a loud failure). This is how arch-pinned
|
||||
examples (e.g. x86_64 inline-asm) are tested on a non-matching dev host while
|
||||
still running end-to-end on a matching CI runner.
|
||||
|
||||
### Snapshot integrity
|
||||
|
||||
**Never run `--update` while tests are failing.** The `--update` flag blindly overwrites expected output with whatever the compiler produces — including error messages. If you update snapshots during a broken state, the test suite will "pass" against garbage output and real regressions become invisible.
|
||||
**Never regenerate snapshots while tests are failing.** `-Dupdate-goldens` (and the legacy `--update`) blindly overwrite expected output with whatever the compiler produces — including error messages. If you regenerate during a broken state, the test suite will "pass" against garbage output and real regressions become invisible.
|
||||
|
||||
Safe workflow:
|
||||
1. Fix the code until `bash tests/run_examples.sh` passes against the **existing** snapshots.
|
||||
2. Only run `--update` when you've intentionally changed output (new feature, new test, changed formatting).
|
||||
3. After `--update`, review the diff (`git diff examples/expected/ issues/expected/`) to confirm no error messages or empty output were captured.
|
||||
1. Fix the code until `zig build test` passes against the **existing** snapshots.
|
||||
2. Only run `zig build test -Dupdate-goldens` when you've intentionally changed output (new feature, new test, changed formatting).
|
||||
3. After regenerating, review the diff (`git diff examples/expected/ issues/expected/`) to confirm no error messages or empty output were captured.
|
||||
|
||||
### Adding a new language feature
|
||||
|
||||
@@ -461,19 +495,19 @@ There is no monolithic smoke file — each feature is its own focused example.
|
||||
2. Run it: `./zig-out/bin/sx run examples/XXXX-<category>-<name>.sx`
|
||||
3. Seed the marker and capture expected output:
|
||||
`: > examples/expected/XXXX-<category>-<name>.exit` then
|
||||
`bash tests/run_examples.sh --update`
|
||||
4. Verify all tests still pass: `bash tests/run_examples.sh`
|
||||
`zig build test -Dupdate-goldens`
|
||||
4. Verify all tests still pass: `zig build test`
|
||||
|
||||
### Test file roles
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `examples/XXXX-category-name.sx` | Focused feature example — one feature per file. |
|
||||
| `examples/expected/XXXX-category-name.{exit,stdout,stderr}` | Expected exit code + the two output streams. Regenerate with `--update`. |
|
||||
| `examples/expected/XXXX-category-name.{exit,stdout,stderr}` | Expected exit code + the two output streams. Regenerate with `zig build test -Dupdate-goldens`. |
|
||||
| `examples/expected/XXXX-category-name.ir` | Optional `sx ir` snapshot — present only where lowering shape is locked. |
|
||||
| `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. |
|
||||
| `tests/run_examples.sh` | Test runner. Scans `examples/` and `issues/`; compares stdout/stderr/exit (+ optional IR) per test. |
|
||||
| `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`. |
|
||||
|
||||
### Unit test file convention
|
||||
|
||||
@@ -496,8 +530,8 @@ All Zig unit tests live in separate `*.test.zig` files alongside the source they
|
||||
open bug, `issues/NNNN-slug.{md,sx}` (repro co-located with the writeup).
|
||||
2. Run it: `./zig-out/bin/sx run <path>.sx`
|
||||
3. Seed the marker (`: > <root>/expected/<name>.exit`) and capture expected:
|
||||
`bash tests/run_examples.sh --update`
|
||||
4. Verify: `bash tests/run_examples.sh`
|
||||
`zig build test -Dupdate-goldens`
|
||||
4. Verify: `zig build test`
|
||||
|
||||
### Resolving an open issue
|
||||
|
||||
@@ -505,8 +539,8 @@ When a bug filed under `issues/NNNN-slug.{md,sx}` is fixed:
|
||||
|
||||
1. Move the repro into the feature suite as a regression test:
|
||||
`git mv issues/NNNN-slug.sx examples/XXXX-<category>-<name>.sx`.
|
||||
2. Seed `examples/expected/XXXX-<category>-<name>.exit`, capture with `--update`,
|
||||
and review the diff.
|
||||
2. Seed `examples/expected/XXXX-<category>-<name>.exit`, capture with
|
||||
`zig build test -Dupdate-goldens`, and review the diff.
|
||||
3. Tighten the example's comment header to describe the feature (keep a one-line
|
||||
`Regression (issue NNNN)` note for provenance).
|
||||
4. Mark `issues/NNNN-slug.md` RESOLVED with a short banner (root cause + fix +
|
||||
@@ -536,7 +570,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 |
|
||||
|------|------|
|
||||
@@ -546,7 +580,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
|
||||
|
||||
33
build.zig
33
build.zig
@@ -193,28 +193,49 @@ pub fn build(b: *std.Build) void {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// Corpus paths for the LSP corpus-sweep test (src/lsp/corpus_sweep.test.zig).
|
||||
// Inject absolute corpus dirs at configure time so the in-process analyzer
|
||||
// sweep is CWD-independent; the test still ENUMERATES the directory
|
||||
// contents at runtime (new examples are covered with no test edit).
|
||||
// Corpus paths for the corpus tests (src/lsp/corpus_sweep.test.zig — the
|
||||
// in-process analyzer sweep — and src/corpus_run.test.zig — the end-to-end
|
||||
// example/issue runner). Inject absolute corpus dirs + the installed `sx`
|
||||
// binary path at configure time so the tests are CWD-independent; the
|
||||
// runner still ENUMERATES the directory contents at runtime, so new
|
||||
// examples are covered with no test edit.
|
||||
const corpus_opts = b.addOptions();
|
||||
corpus_opts.addOption([]const u8, "examples_dir", b.path("examples").getPath(b));
|
||||
corpus_opts.addOption([]const u8, "issues_dir", b.path("issues").getPath(b));
|
||||
corpus_opts.addOption([]const u8, "library_dir", b.path("library").getPath(b));
|
||||
// Absolute path to the installed `sx` binary the corpus runner spawns per
|
||||
// example. The runner test depends on the install step (below) so this
|
||||
// exists — and so the sibling library/ tree the binary loads is in place.
|
||||
corpus_opts.addOption([]const u8, "sx_exe", b.getInstallPath(.bin, "sx"));
|
||||
// `zig build test -Dupdate-goldens` flips src/corpus_run.test.zig from
|
||||
// verify mode to regenerate mode: it overwrites each example's expected
|
||||
// .exit/.stdout/.stderr (+ .ir where one exists) with freshly-normalized
|
||||
// output instead of asserting against it. The in-build equivalent of the
|
||||
// legacy `run_examples.sh --update`.
|
||||
const update_goldens = b.option(
|
||||
bool,
|
||||
"update-goldens",
|
||||
"Regenerate example/issue snapshots instead of verifying them (use with `zig build test`)",
|
||||
) orelse false;
|
||||
corpus_opts.addOption(bool, "update_goldens", update_goldens);
|
||||
mod.addOptions("corpus_paths", corpus_opts);
|
||||
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
});
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
// src/corpus_run.test.zig spawns the installed `sx` binary per example, so
|
||||
// the mod test binary must not run until `zig-out/bin/sx` + `zig-out/library`
|
||||
// are installed. This is what folds the full example/issue regression suite
|
||||
// into `zig build test` — no shell script, just a Zig test.
|
||||
run_mod_tests.step.dependOn(b.getInstallStep());
|
||||
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
const test_step = b.step("test", "Run unit tests + the example/issue regression suite");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
|
||||
}
|
||||
|
||||
394
current/CHECKPOINT-ASM.md
Normal file
394
current/CHECKPOINT-ASM.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# sx Inline Assembly — Checkpoint (ASM stream)
|
||||
|
||||
Companion to `current/PLAN-ASM.md`; design in
|
||||
[design/inline-asm-design.md](../design/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
|
||||
**G (indirect-memory `=*m` place outputs)** — the LAST substantive asm feature.
|
||||
Unlike a write-through `=` output (which returns a value then stored), an
|
||||
indirect output passes the place ADDRESS to the asm and the asm writes through
|
||||
it — no return slot. `emitInlineAsm` (`src/backend/llvm/ops.zig`): indirect
|
||||
outputs are excluded from the LLVM return type; their pointer is an opaque `ptr`
|
||||
call arg placed **first** (arg-consuming constraint order = output-section
|
||||
indirect pointers → inputs → read-write tied seeds); each gets an
|
||||
`elementtype(T)` call-site attribute (required in the opaque-pointer era) via
|
||||
`LLVMCreateTypeAttribute`/`LLVMAddCallSiteAttribute`; the store-back loop skips
|
||||
them. New `asmIsIndirect(e, op)` helper. Lowering (`lowerAsmExpr`) stops
|
||||
rejecting `*` (constraint kept verbatim, `=*m` reaches the constraint string
|
||||
as-is). `asmOperandIndex` unchanged — indirect outputs still count as operands,
|
||||
so `%[name]`→`${N}` holds. Verified by **running** on aarch64: store-through-
|
||||
pointer (`str x9, %[out]` → 42, IR `"=*m,~{x9}"(ptr elementtype(i64) …)`) and a
|
||||
mixed case (indirect + value output + input → `"=*m,=r,r"`, indirect ptr arg
|
||||
first, `${0}/${1}/${2}` correct). Two commits per cadence: (1)
|
||||
`examples/1652-platform-asm-indirect-mem.sx` locked the rejection; (2) implemented
|
||||
+ flipped 1652 to a runnable aarch64-pinned example (`{ "target": "macos" }`,
|
||||
ir-only elsewhere). `zig build test` green (661 corpus, 446 unit). Files:
|
||||
`src/ir/lower/expr.zig`, `src/backend/llvm/ops.zig`, `examples/1652-*`.
|
||||
|
||||
Prior: **G (read-write `+` place outputs)** — a `+r` / `+{reg}` `-> @place` output is now
|
||||
implemented. LLVM has no `+` constraint, so a
|
||||
read-write place lowers to: an output **`=`** constraint (return slot, stored back
|
||||
through the place after the call; the leading `+` rewritten to `=` in
|
||||
`appendAsmConstraints`), **plus** a **tied input** (the decimal index of that
|
||||
output) appended **after** the regular inputs, seeded with the place's loaded
|
||||
value passed as a call arg. Tied inputs come **last** so existing operand indices
|
||||
(`%[name]`→`${N}`) are undisturbed — `asmOperandIndex` unchanged. Lowering
|
||||
(`lowerAsmExpr`) no longer rejects `+` (indirect `*` still rejected loudly).
|
||||
`emitInlineAsm` (`src/backend/llvm/ops.zig`): grows arg/param arrays by the rw
|
||||
count (`n_args = n_inputs + n_rw`), loads each seed (`asm.rw.seed`), emits the
|
||||
tied constraint, and the existing store-back path writes the modified output back.
|
||||
New `asmIsReadWrite(e, op)` helper. Verified by **running**: increment-in-place
|
||||
(41→42, IR `"=r,0"`) and a mixed case (rw place + regular input + value output) →
|
||||
textbook `"=r,=r,r,0"` with correct `${N}` indices and args `(input, seed)`. Two
|
||||
commits per cadence: (1) `examples/1650-platform-asm-rw-place.sx` locked the
|
||||
rejection; (2) implemented + flipped 1650 to a runnable aarch64-pinned example
|
||||
(`{ "target": "macos" }`, ir-only elsewhere). `zig build test` green (658 corpus,
|
||||
446 unit). Files: `src/ir/lower/expr.zig`, `src/backend/llvm/ops.zig`,
|
||||
`examples/1650-*`.
|
||||
|
||||
Prior: **2** — `-> @place` write-through outputs. An asm result can be **stored through
|
||||
a place** (local / struct field) instead of returned; the place output does NOT
|
||||
join the result tuple. Parser: `-> @place` parses the `@place` as an ordinary
|
||||
address-of expression → an `out_place` operand (`src/parser.zig`). Lowering
|
||||
(`lowerAsmExpr`): out_place operand = the lowered `@place` address, `out_ty` =
|
||||
the pointee; read-write (`+`) and indirect-memory (`*`) constraints rejected
|
||||
loudly (not yet implemented). Added `out_ty: TypeId` to the IR `AsmOperand`
|
||||
(`src/ir/inst.zig`) so emit builds the **combined** return struct (ALL outputs).
|
||||
`emitInlineAsm` rewrite (`src/backend/llvm/ops.zig`): the LLVM return type is now
|
||||
built from every output's `out_ty`; after the call, out_place slots are
|
||||
`store`d through their address and out_value slots rebuild the sx result — with a
|
||||
**fast path** (no place outputs → the asm's struct return IS the result, so
|
||||
pure-value asm IR is unchanged). Verified: write-to-local (`get42`→42), struct
|
||||
field (`@p.b`), mixed value+place (`v=10 b=20`), `+` rejected. Locked with
|
||||
`examples/1649-platform-asm-place-output.sx` (mixed, runs on aarch64). `zig build
|
||||
test` green (657 corpus, 446 unit). Files: `src/parser.zig`, `src/ir/inst.zig`,
|
||||
`src/ir/lower/expr.zig`, `src/backend/llvm/ops.zig`, `examples/1649-*`.
|
||||
|
||||
Prior: **F** — global (module-scope) asm. A top-level `asm { "tmpl", };` block (template
|
||||
only) lowers to LLVM `module asm`, and a lib-less `extern` calls into the symbols
|
||||
it defines. New `asm_global` AST node (`src/ast.zig`) + `parseAsmGlobal`
|
||||
(`src/parser.zig`, dispatched from `parseTopLevel` on `kw_asm`) — rejects
|
||||
`volatile` and any operands/clobbers. The node forced (and got) arms in the same
|
||||
three `Node.Data` switches as `asm_expr` (`sema.zig` ×2, `semantic_diagnostics.zig`).
|
||||
`Module` gains a `global_asm: ArrayList([]const u8)` (`src/ir/module.zig`);
|
||||
`lowerMainAndComptime` captures each template (the dead `lowerDecls` is NOT the
|
||||
top-level pass — `lowerRoot` Pass 2 uses `lowerMainAndComptime`); `emit_llvm.zig`'s
|
||||
`emit()` appends each via `LLVMAppendModuleInlineAsm` (source order). Verified
|
||||
end-to-end: an aarch64 `_my_add` global routine called via `extern` returns 42.
|
||||
Locked with `examples/1648-platform-asm-global.sx`
|
||||
(`.build { "aot": true, "target": "macos" }` → AOT build+run on aarch64, ir-only
|
||||
elsewhere). `zig build test` green (656 corpus, 446 unit). **(Correction, later:
|
||||
module asm ALSO runs under the JIT — `sx run` compiles to an in-memory object,
|
||||
the integrated assembler assembles the `module asm` into it, ORC relocates and
|
||||
runs it, so the symbol is resolvable at JIT main execution. The original "AOT
|
||||
only" note was wrong; see 1653 for the JIT sibling. The genuine boundary is a
|
||||
COMPILE-TIME `#run` call into a module-asm symbol, which fails loud via host
|
||||
dlsym-miss — see 1654.)** Files: `src/ast.zig`, `src/parser.zig`, `src/sema.zig`,
|
||||
`src/ir/semantic_diagnostics.zig`, `src/ir/module.zig`, `src/ir/lower/decl.zig`,
|
||||
`src/ir/emit_llvm.zig`, `examples/1648-*`.
|
||||
|
||||
Prior: **E** — multi-output tuples. **Inline asm now returns tuples.** Replaced the
|
||||
N>1 bail with a shared `asmResultType` helper (`src/ir/lower/expr.zig`, mixed
|
||||
into `Lowering`) that derives the result type from the `out_value` operands
|
||||
(0→void, 1→T, N→named tuple, named via the §II.5 effective-name rule). The key
|
||||
realization: `toLLVMType(tuple)` already produces a literal struct `{T1,…,Tn}` —
|
||||
exactly LLVM's multi-output asm return — so **emit needed NO change**; building
|
||||
the op with a tuple result type makes the asm call return the struct, which IS
|
||||
sx's tuple value (destructured by the normal `tuple_get` path). `inferType`'s
|
||||
`.asm_expr` arm now also delegates to `asmResultType` (single owner), so
|
||||
`return asm`, `x := asm`, and `q, r := asm` all agree on the type. Verified
|
||||
end-to-end on aarch64: `split(0x1234)`→`(lo=52, hi=18)`, a udiv/msub divmod→
|
||||
`(3, 2)`. IR is textbook: `call { i64, i64 } asm "divq ${4}",
|
||||
"={rax},={rdx},{rax},{rdx},r,~{cc}"(…)` → extractvalue → tuple. Converted 1640 to
|
||||
the x86_64 multi-output IR lock (ir-only) + added `1647-platform-asm-aarch64-multi`
|
||||
(runs on aarch64). `zig build test` green (655 corpus, 446 unit). Files:
|
||||
`src/ir/lower/expr.zig`, `src/ir/lower.zig`, `src/ir/expr_typer.zig`,
|
||||
`examples/164{0,7}-*`.
|
||||
|
||||
Prior: **C.1 + D** — inline asm CODEGEN (lowering builds the op + LLVM emit). **Inline
|
||||
assembly now runs end-to-end.** `lowerAsmExpr` (`src/ir/lower/expr.zig`) stops
|
||||
bailing: it resolves each operand's effective name (§II.5 auto-naming), interns
|
||||
template/constraints/clobbers, lowers input `Ref`s, derives the result `TypeId`
|
||||
(0→void, 1→T), and builds the `inline_asm` op. Added a `%[name]`-references-a-
|
||||
real-operand check (the last deferred validation). Multi-output (N>1) still bails
|
||||
loudly ("Phase E"). `emitInlineAsm` (`src/backend/llvm/ops.zig`, port of Zig's
|
||||
`airAssembly`): assembles the LLVM constraint string (outputs→inputs→`~{clobber}`,
|
||||
`,`→`|`), rewrites the template (`%[name]`→`${N}`, `%%`→`%`, `$`→`$$`, `%=`→
|
||||
`${:uid}`), then `LLVMGetInlineAsm` + `LLVMBuildCall2` (AT&T). Dispatch wired
|
||||
(`emit_llvm.zig`, replacing the C.0 `@panic`). **`llvm_shim.c`**: added
|
||||
`LLVMInitializeNativeAsmParser()` — the JIT must assemble inline asm at run time.
|
||||
Verified end-to-end: aarch64 `add`/`mov` run on the host (exit 42), `nop volatile`
|
||||
runs (1642 now exit 0), IR is textbook (`call i64 asm "add ${0},${1},${2}",
|
||||
"=r,r,r"(…)`). Locked with `examples/1645-platform-asm-aarch64-add.sx` (runs on
|
||||
aarch64, ir-only elsewhere via `.build` + `.ir`). Also added the `inferType`
|
||||
`.asm_expr` arm (`src/ir/expr_typer.zig`, 0→void / 1→T) — without it a bare
|
||||
`x := asm {…-> T}` binding inferred `.unresolved` and silently produced 0;
|
||||
regression-locked with `examples/1646-platform-asm-value-binding.sx`. Updated
|
||||
1640 (now Phase-E bail) + 1642 (now runs). `zig build test` green (654 corpus,
|
||||
446 unit). Files: `src/ir/lower/expr.zig`, `src/backend/llvm/ops.zig`,
|
||||
`src/ir/emit_llvm.zig`, `src/ir/expr_typer.zig`, `llvm_shim.c`,
|
||||
`examples/164{0,2,5,6}-*`.
|
||||
|
||||
Prior: **C.0** — IR op `inline_asm` (lock; no behavior change). Added `inline_asm:
|
||||
InlineAsm` to the IR `Op` union + the `InlineAsm` struct (`template: StringId`,
|
||||
`operands: []const AsmOperand` {role/name/constraint/operand}, `clobbers:
|
||||
[]const StringId`, `has_side_effects`) in `src/ir/inst.zig` — all strings
|
||||
interned, operands in source order, result on `Inst.ty`. The new variant forced
|
||||
(and got) arms in two exhaustive `Op` switches: `src/ir/interp.zig` (loud
|
||||
`bailDetail` — inline asm is never comptime-evaluable) and `src/ir/print.zig`
|
||||
(IR dump). `src/ir/emit_llvm.zig` gets a `@panic` **tripwire** — emit lands in
|
||||
Phase D, and until then `lowerAsmExpr` still bails so no `inline_asm` op is ever
|
||||
created (reaching emit would be a lowering-switched-over-too-early bug). Unit
|
||||
test `inline_asm op shape` in `src/ir/inst.test.zig`. `zig build test` green
|
||||
(652 corpus, 446 unit). Files: `src/ir/inst.zig`, `src/ir/interp.zig`,
|
||||
`src/ir/print.zig`, `src/ir/emit_llvm.zig`, `src/ir/inst.test.zig`.
|
||||
|
||||
Prior: **B.1** — operand-name validation (design §II.5 auto-naming rule). Extended
|
||||
`lowerAsmExpr` with a `pinnedRegister(constraint)` helper (`"={eax}"`→`eax`,
|
||||
`"+{rax}"`→`rax`, `"=r"`→null) and two checks: (1) **reject the echo form**
|
||||
`[eax] "={eax}"` — a label identical to its own pinned register is redundant
|
||||
(the operand is already auto-named after the register); (2) **reject duplicate
|
||||
operand names** (ambiguous `%[name]` / result field). Locked with
|
||||
`examples/1643-platform-asm-echo-name.sx` + `1644-platform-asm-duplicate-name.sx`.
|
||||
`zig build test` green (652 corpus, 0 failed; 445 unit). Files:
|
||||
`src/ir/lower/expr.zig`.
|
||||
|
||||
Prior: **B.0** — asm shape validation (compile-path diagnostics). Restructured the
|
||||
`.asm_expr` lowering arm into `lowerAsmExpr` (`src/ir/lower/expr.zig`, mixed into
|
||||
`Lowering` in `src/ir/lower.zig`): it validates BEFORE the not-yet-implemented
|
||||
codegen bail, so the user sees the real problem first. Two checklist items now
|
||||
enforced with named diagnostics: (1) **template must be a compile-time-known
|
||||
string** (`"..."` / `#string`); (2) **no value outputs ⇒ must be `volatile`**
|
||||
(mirrors Zig — a result-less asm could be deleted). Valid shapes still bail with
|
||||
the "codegen not yet implemented" message. Result-type derivation + auto-naming
|
||||
stay deferred to a later step (observable only once Phase C produces a real IR
|
||||
op). Locked with `examples/1641-platform-asm-missing-volatile.sx` (volatile
|
||||
error) + `1642-platform-asm-nop-volatile.sx` (volatile no-output accepted →
|
||||
codegen bail). `zig build test` green (650 corpus, 0 failed; 445 unit). Files:
|
||||
`src/ir/lower/expr.zig`, `src/ir/lower.zig`, `examples/164{1,2}-*`.
|
||||
|
||||
Prior: **A.1** — parse `asm { … }` + loud lowering bail (folded A.1+A.2 into one honest
|
||||
lock commit, since the loud bail IS current correct behavior — cadence option
|
||||
(a)). Added `AsmExpr`/`AsmOperand` to `src/ast.zig` + the `asm_expr` `Node.Data`
|
||||
arm; `parseAsmExpr` in `src/parser.zig` (`parsePrimary` `.kw_asm` dispatch) —
|
||||
parses the template, flat operand list (`[name]? "constraint" -> Type` value
|
||||
output / `= expr` input), and `clobbers(.…)`; `volatile`/`clobbers` recognized
|
||||
contextually via `isContextualWord`. The new `asm_expr` tag forced (and got)
|
||||
arms in three exhaustive `Node.Data` switches: `src/sema.zig` `analyzeNode` +
|
||||
`findNodeAtOffset`, `src/ir/semantic_diagnostics.zig` `checkBindingNames` (all
|
||||
recurse into template + operand payloads). Lowering bails LOUD + named in
|
||||
`src/ir/lower/expr.zig` ("inline assembly codegen is not yet implemented…") via
|
||||
an explicit `.asm_expr` arm (not the generic `unknown_expr` else) returning
|
||||
`emitPlaceholder`. `-> @place` write-through is rejected with a clear "Phase 2"
|
||||
parse error. Locked with `examples/1640-platform-asm-parse.sx` (multi-output
|
||||
`divmod`, named operands, register pins, clobbers — parses then bails; called
|
||||
from `main`). `zig build test` green (648 corpus, 0 failed; 445 unit). Files:
|
||||
`src/ast.zig`, `src/parser.zig`, `src/sema.zig`, `src/ir/semantic_diagnostics.zig`,
|
||||
`src/ir/lower/expr.zig`, `examples/1640-*`.
|
||||
|
||||
Prior: **A.0** — `kw_asm` keyword (first compiler code). Added the `kw_asm` `Token.Tag`
|
||||
variant + `.{ "asm", .kw_asm }` keyword-map entry in `src/token.zig`; `volatile` /
|
||||
`clobbers` deliberately stay OUT of the global table (contextual). New exhaustive
|
||||
`Tag` switch in `src/lsp/server.zig` `classifyToken` flagged the missing arm (the
|
||||
intended coverage tripwire) — added `.kw_asm` to the keyword group. Lock test in
|
||||
new `src/lexer.test.zig` (`asm`→`kw_asm`, `volatile`/`clobbers`→`identifier`),
|
||||
wired into the `src/root.zig` barrel as `lexer_tests`. `zig build test` green (648
|
||||
corpus, 0 failed; 445 unit, 0 failed — +1). Files: `src/token.zig`,
|
||||
`src/lexer.test.zig`, `src/root.zig`, `src/lsp/server.zig`.
|
||||
|
||||
Prior: **0.2** — CLAUDE.md docs for `<name>.build`; **Phase 0 COMPLETE**.
|
||||
**0.1** — corpus runner **ir-only branch** for cross-target examples. Replaced
|
||||
0.0's loud placeholder bail: when `cfg.target` doesn't match the host (`ir_only`),
|
||||
`sweepRoot` skips run/build/exec and verifies via `sx ir --target` only —
|
||||
asserting `.exit` (ir cmd) + `.ir` (normalized stdout) + `.stderr`, never
|
||||
`.stdout` (write skipped in update mode, assertion skipped in verify mode). An
|
||||
`.ir` snapshot is **required** in ir-only mode — its absence is a loud failure
|
||||
("needs an .ir snapshot for ir-only mode"). Locked with
|
||||
`examples/1639-platform-target-cross.sx` (asm-free `main :: () -> i64 { return 0;
|
||||
}`), `.build` `{ "target": "x86_64-linux" }`, + checked-in `.ir`. Verified both
|
||||
guards fire: corrupting the `.ir` → IR mismatch; deleting it → the require-failure.
|
||||
`zig build test` green (647 corpus, 0 failed; 444 unit). Files:
|
||||
`src/corpus_run.test.zig`, `examples/1639-*`.
|
||||
|
||||
## Current state
|
||||
**Inline assembly works end-to-end: 0, 1, and N value outputs (tuples).** Full
|
||||
pipeline: lex (A.0) → parse (A.1) → validate (B.0/B.1 + `%[name]` check) → IR op
|
||||
(C.0) → lower-builds-op + LLVM emit + JIT asm-parser init (C.1/D) → multi-output
|
||||
tuples (E). Register-class + register-pinned operands, inputs, **symbol operands
|
||||
(`"s"` → direct `bl`/`call` to a function/global by mangled name)**, clobbers,
|
||||
`#string` multi-instruction templates, `%[name]`/`%%` rewriting, and the §II.5
|
||||
auto-naming rule all work and execute on the host JIT. Global `asm { … }` (Phase F) works via
|
||||
lib-less `extern` under BOTH the JIT (`sx run` → 1653) and AOT (1648) — `sx run`
|
||||
compiles to an object, so the integrated assembler bakes the `module asm` symbol
|
||||
in and ORC resolves it. All three `-> @place` output forms now work and execute
|
||||
on aarch64: **write-through** `=` (Phase 2), **read-write** `+` (tied input), and
|
||||
**indirect-memory** `=*m` (pointer arg + `elementtype`, asm writes through it).
|
||||
**Inline assembly is now feature-complete — no substantive features remain.** The
|
||||
x86_64 syscall-write ir-only example is DONE (1651). Global asm runs under both
|
||||
JIT (1653) and AOT (1648). `readme.md` now has an "Inline Assembly" section.
|
||||
|
||||
Known orthogonal bug: **issue 0137** — `sx run` on a program with no `main`
|
||||
segfaults (`src/target.zig:256-273`, unguarded JIT entry lookup). Pre-existing,
|
||||
asm-independent; does NOT block the ASM stream (every example has a `main`).
|
||||
|
||||
Phase E–F feasibility already confirmed against the live tree
|
||||
(`LLVMGetInlineAsm` / `LLVMBuildCall2` / `LLVMAppendModuleInlineAsm` in LLVM@19
|
||||
`Core.h`; ERR-stream `extractvalue`→tuple in `emit_llvm.zig:726-927`; lib-less
|
||||
`extern`, 60 sites; `--target` a global CLI flag).
|
||||
|
||||
## Next step
|
||||
**Inline assembly is feature-complete.** All substantive features are done:
|
||||
0/1/N value outputs (tuples), register-class + pinned operands, inputs, clobbers,
|
||||
`#string` templates, `%[name]`/`%%`/`$`/`%=` rewriting, §II.5 auto-naming, global
|
||||
`asm { … }` (AOT), and all three `-> @place` output forms — write-through (`=`),
|
||||
read-write (`+`), and indirect-memory (`=*m`). The x86_64 syscall-write ir-only
|
||||
example (1651) and the output-to-`const` rejection (issue 0138) are also done.
|
||||
|
||||
Global asm runs under BOTH the JIT (`sx run` → object → ORC; 1653) and AOT (1648)
|
||||
— the earlier "AOT only / `sx run` mishandles module-asm" note was stale and has
|
||||
been corrected. The one genuine boundary is a COMPILE-TIME `#run` into a
|
||||
module-asm symbol: the interpreter resolves externs via host dlsym, the symbol
|
||||
isn't linked yet, so it already fails loud (`comptime extern call: symbol not
|
||||
found via dlsym`) — pinned by 1654.
|
||||
|
||||
Remaining work, all **polish** (optional):
|
||||
- None substantive. Possible niceties: tighten the `#run`-into-module-asm error
|
||||
text to name module-asm specifically; broaden clobber validation to a checked
|
||||
per-arch enum (design doc Phase 4).
|
||||
|
||||
Orthogonal: **issue 0137** (no-`main` JIT segfault).
|
||||
|
||||
Done since last: output-to-`const` rejection (issue 0138), x86_64 syscall-write
|
||||
ir-only example (1651).
|
||||
|
||||
Orthogonal: **issue 0137** (no-`main` segfault).
|
||||
|
||||
## Log
|
||||
- (init) Plan + design doc written; ASM stream opened.
|
||||
- (0.0) Corpus runner target-gating: `<name>.build` JSON config (replaces `.aot`
|
||||
marker), `--target` threading, `hostMatchesTarget` execute-gate, loud
|
||||
cross-target placeholder bail. Migrated 1226/1227 `.aot`→`.build`; locked with
|
||||
1638 fixture + unit tests. `zig build test` green.
|
||||
- (0.1) ir-only branch: cross-target examples verify via `sx ir --target` only
|
||||
(exit+ir+stderr, no stdout; `.ir` required). Locked with 1639 fixture; verified
|
||||
corrupt-.ir → mismatch and missing-.ir → loud failure. `zig build test` green.
|
||||
- (0.2) docs: CLAUDE.md documents `<name>.build` JSON sidecar (aot + target +
|
||||
ir-only gating), replacing stale `.aot` marker prose. **Phase 0 COMPLETE.**
|
||||
- (A.0) `kw_asm` keyword in token.zig (+ map entry); LSP `classifyToken` switch
|
||||
coverage; lock test in new `lexer.test.zig` (wired via root.zig). `volatile` /
|
||||
`clobbers` stay contextual identifiers. `zig build test` green (445 unit, +1).
|
||||
- (A.1) parse `asm { … }` → `AsmExpr` + loud lowering bail; `asm_expr` arms in 3
|
||||
exhaustive `Node.Data` switches; `-> @place` rejected (Phase 2). Adopted operand
|
||||
auto-naming rule (design §II.5). Locked with 1640 fixture. Filed orthogonal
|
||||
issue 0137 (no-`main` JIT segfault). `zig build test` green (648 corpus, 445 unit).
|
||||
- (B.0) asm shape validation in `lowerAsmExpr`: comptime-string template +
|
||||
no-output⇒volatile, with named diagnostics before the codegen bail. Locked with
|
||||
1641 (volatile error) + 1642 (volatile accepted). `zig build test` green (650
|
||||
corpus, 445 unit).
|
||||
- (B.1) operand-name validation: `pinnedRegister` helper + reject echo form
|
||||
(`[eax] "={eax}"`) and duplicate names. Locked with 1643 + 1644. `zig build
|
||||
test` green (652 corpus, 445 unit).
|
||||
- (C.0) IR op `inline_asm: InlineAsm` + interp `bailDetail` + print arm + emit
|
||||
`@panic` tripwire (Phase D). No behavior change (lowering still bails). Unit
|
||||
test `inline_asm op shape`. `zig build test` green (652 corpus, 446 unit).
|
||||
- (C.1+D) CODEGEN — `lowerAsmExpr` builds the op (effective names, interned
|
||||
strings, input Refs, 0/1 result type) + `%[name]` validation; `emitInlineAsm`
|
||||
(constraint string + template rewrite + `LLVMGetInlineAsm`/`BuildCall2`, AT&T);
|
||||
`inferType` arm; `LLVMInitializeNativeAsmParser` for the JIT. **Inline asm runs
|
||||
end-to-end.** N>1 bails (Phase E). Locked with 1645 (aarch64 add, runs) + 1646
|
||||
(`:=` binding); updated 1640/1642. `zig build test` green (654 corpus, 446 unit).
|
||||
- (E) multi-output tuples — `asmResultType` helper (0→void/1→T/N→named tuple),
|
||||
shared by lowering + `inferType`. `toLLVMType(tuple)` == LLVM multi-output
|
||||
struct, so emit unchanged; the asm struct return IS the sx tuple. Runs on
|
||||
aarch64 (1647: `split`→`(lo,hi)`); 1640 → x86 multi-output IR lock (ir-only).
|
||||
`zig build test` green (655 corpus, 446 unit).
|
||||
- (F) global asm — `asm_global` AST node + `parseAsmGlobal` (top-level, rejects
|
||||
volatile/operands); `Module.global_asm` captured in `lowerMainAndComptime`;
|
||||
`emit()` appends via `LLVMAppendModuleInlineAsm`; call-into via lib-less
|
||||
`extern`. AOT-verified (1648, `_my_add`→42). `zig build test` green (656 corpus).
|
||||
- (docs) readme.md "Inline Assembly" section (b8800a2).
|
||||
- (2) `-> @place` write-through — `out_place` operand; `out_ty` on the IR
|
||||
AsmOperand; `emitInlineAsm` builds the combined output struct + splits
|
||||
(out_place → store-through, out_value → result), fast-path when no places.
|
||||
`+`/`*` rejected. Locked with 1649 (mixed, runs). `zig build test` green (657
|
||||
corpus, 446 unit).
|
||||
- (G) read-write `+` place outputs — `+` lowers to an output `=` + a tied input
|
||||
(output-index constraint) seeded with the place's loaded value, tied inputs
|
||||
appended last (operand indices undisturbed). `appendAsmConstraints` rewrites
|
||||
`+`→`=`; `emitInlineAsm` grows args by the rw count + loads seeds;
|
||||
`asmIsReadWrite` helper. Lowering stops rejecting `+` (`*` still rejected). Two
|
||||
commits (cadence): 1650 locked the rejection, then flipped to a runnable
|
||||
aarch64 example (`"=r,0"` IR). `zig build test` green (658 corpus, 446 unit).
|
||||
- (0138) output-to-`const` rejection — fixed the underlying general bug: scalar
|
||||
`@const` (address-of a folded `::` constant) reinterpreted the value as a
|
||||
pointer (`inttoptr`). `src/ir/lower/expr.zig` `.address_of` now diagnoses a
|
||||
scalar const (local + module) instead of falling through; array/struct consts
|
||||
keep storage. asm `-> @const` gets the clean diagnostic for free (same path).
|
||||
Regression `examples/1177-diagnostics-addr-of-const-rejected.sx`. Issue 0138
|
||||
RESOLVED. `zig build test` green (659 corpus, 446 unit).
|
||||
- (x86 syscall) x86_64 Linux `write(2)` via raw `syscall` — locks the constraint
|
||||
string `={rax},{rax},{rdi},{rsi},{rdx},~{rcx},~{r11},~{memory}` (register-pinned
|
||||
inputs + pinned value output + pointer input + clobbers). ir-only on aarch64
|
||||
(`.ir` asserted), runs on x86_64-linux (hand-authored `"ok\n"` stdout).
|
||||
`examples/1651-platform-asm-x86-syscall-write.sx`. Pure additive lock, no
|
||||
compiler change. `zig build test` green (660 corpus, 446 unit).
|
||||
- (G indirect) indirect-memory `=*m` place outputs — the place address is passed
|
||||
as an opaque `ptr` arg (with an `elementtype(T)` call-site attr), placed before
|
||||
inputs; asm writes through it; no return slot; store-back skips it.
|
||||
`asmIsIndirect` helper; lowering stops rejecting `*`. Verified by running on
|
||||
aarch64 (store-through → 42; mixed indirect+value+input → `"=*m,=r,r"`). Two
|
||||
commits (cadence): 1652 locked the rejection, then flipped to a runnable aarch64
|
||||
example. **Inline asm now feature-complete.** `zig build test` green (661 corpus,
|
||||
446 unit).
|
||||
- (jit) explored "asm in JIT": found it ALREADY works — `sx run` emits an
|
||||
in-memory object (integrated assembler bakes in both in-function inline asm and
|
||||
`module asm`), then ORC relocates+runs it. The stale "AOT only / `sx run`
|
||||
mishandles module-asm" checkpoint prose was corrected. Locked global-asm-under-
|
||||
JIT with `examples/1653-platform-asm-global-jit.sx` (`{ "target": "macos" }`, no
|
||||
aot, → 42). `zig build test` green (662 corpus, 446 unit).
|
||||
- (comptime guard) pinned the one genuine module-asm boundary:
|
||||
`examples/1654-platform-asm-global-comptime-call.sx` — `#run` into a module-asm
|
||||
symbol fails loud (`comptime extern call: symbol not found via dlsym`) because
|
||||
the interpreter resolves externs via host dlsym before link. Arch-independent
|
||||
(no `.build`). `zig build test` green (663 corpus, 446 unit).
|
||||
- (round trip) `examples/1655-platform-asm-callback-into-sx.sx` — global-asm
|
||||
trampoline that `bl _cb` back into an `export`ed sx function (sx→asm→sx, → 42).
|
||||
Documented that `export` (external linkage + C symbol + C ABI) is what makes
|
||||
the callback resolvable; `callconv(.c)` alone leaves it `internal` (DCE'd).
|
||||
`zig build test` green (664 corpus, 446 unit).
|
||||
- (symbol ops) symbol operands (`"s"`) — feed a function/global symbol; the
|
||||
template emits its platform-mangled name so `bl %[fn]` is a DIRECT branch (one
|
||||
fewer indirection than register-indirect `blr`, portable — no hardcoded `_`).
|
||||
Emit passes the operand with its own llvm type (LLVMTypeOf), no coercion
|
||||
(`asmIsSymbol` helper); lowering lowers the function RHS to `ptr @fn`. Decided
|
||||
AGAINST mirroring Zig (which has no symbol operand — 483 std asm sites, none
|
||||
call a function) because the direct `bl` matters. Two commits (cadence): 1656
|
||||
locked the rejection (replacing an LLVM-verifier crash), then implemented +
|
||||
flipped to a runnable aarch64 example (objdump-confirmed direct `bl <_cb>`).
|
||||
`zig build test` green (665 corpus, 446 unit).
|
||||
- (x86 cross-arch) ir-only x86_64 siblings so each emit path is locked on BOTH
|
||||
arches: 1657 read-write (`"incq ${0}","=r,0"`), 1658 indirect (`"movq $$42,
|
||||
${0}","=*m"`(ptr elementtype)), 1659 symbol (`"call ${2:P}"`, direct call). x86
|
||||
templates validated by cross-emitting an object (integrated assembler accepts;
|
||||
objdump confirms 1659's direct `call` reloc). Pure additive locks. `zig build
|
||||
test` green (668 corpus, 446 unit).
|
||||
- (symbol portability) made `%[fn]` portable across arches — `renderAsmTemplate`
|
||||
auto-injects LLVM's `:c` modifier (`${N}`→`${N:c}`) for symbol (`"s"`) operands
|
||||
lacking an explicit modifier (`asmNamedIsSymbol` helper). Without it x86 renders
|
||||
`$cb` (a bad `call` target needing a hand-written `:P`); aarch64 unaffected.
|
||||
Verified `:c` ≡ `:P` for x86-64 calls (both → `R_X86_64_PLT32`). Explicit
|
||||
`%[fn:X]` still wins (escape hatch). 1659 dropped its `:P` → same plain `%[fn]`
|
||||
as aarch64 1656; both IRs regen to `${N:c}`. `zig build test` green (668 corpus,
|
||||
446 unit).
|
||||
|
||||
## Known issues
|
||||
- **0138** — RESOLVED. `@const` (address-of a `::` comptime constant) yielded a
|
||||
wild pointer (`inttoptr (i64 <value> to ptr)`). Fixed by diagnosing scalar
|
||||
`@const` in `src/ir/lower/expr.zig` `.address_of` (no storage; array/struct
|
||||
consts unaffected). Delivered the ASM "output-to-`const` rejection" for free.
|
||||
Regression `examples/1177-diagnostics-addr-of-const-rejected.sx`.
|
||||
- **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry
|
||||
lookup, `src/target.zig:256-273`). Pre-existing, asm-independent. Filed
|
||||
`issues/0137-jit-run-no-main-segfault.md`. Does not block A.1.
|
||||
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.**
|
||||
32
current/CHECKPOINT-REIFY.md
Normal file
32
current/CHECKPOINT-REIFY.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# CHECKPOINT-REIFY — comptime `type_info` / `reify` (async-first foundation, step 3)
|
||||
|
||||
Companion to [PLAN-REIFY.md](PLAN-REIFY.md). Update after every step (one step at a
|
||||
time, per the cadence rule).
|
||||
|
||||
## Last completed step
|
||||
**None — stream just carved.** Design validated (3 codebase reviewers; all five reify
|
||||
contracts confirmed feasible). No code written yet.
|
||||
|
||||
## Current state
|
||||
- The plan + the five locked contracts exist in `PLAN-REIFY.md`; design-of-record is
|
||||
`design/execution-evolution-roadmap.md` §7 step 3 + §8.1.
|
||||
- **Nothing built.** `reify`/`type_info`/`field_type` do not exist in the compiler.
|
||||
- Confirmed against the source (anchors in the plan): type minting via
|
||||
`intern`/`internNominal` is programmatic and AST-free; type-fns memoize by mangled
|
||||
name; enum codegen is fully type-table-driven (zero AST coupling); recursive
|
||||
forward-declaration (reserve→complete) already exists for source types.
|
||||
|
||||
## Next step
|
||||
**Phase 0.0 (lock):** add `TypeInfo`/`EnumInfo`/`EnumVariant` data types + bodyless
|
||||
`#builtin` decls for `reify`/`type_info`/`field_type` to `library/modules/std/core.sx`
|
||||
(parsed, unimplemented → loud bail), with a unit test that the decls parse. Then 0.1
|
||||
(xfail: `examples/06xx-comptime-reify-enum.sx`) → 0.2 (green: implement `reify(.enum_)`).
|
||||
|
||||
## Known issues
|
||||
None yet.
|
||||
|
||||
## Log
|
||||
- **Stream carved.** Selected as the first async-first foundation: `reify` gates both
|
||||
channel result types (`RecvResult($T)`) and `race`'s synthesized union, is fully
|
||||
validated (3 reviewers), and is a self-contained compiler/type-system feature
|
||||
testable in isolation (`06xx` comptime). Generic-enum syntax dropped in its favor.
|
||||
167
current/PLAN-ASM.md
Normal file
167
current/PLAN-ASM.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# sx Inline Assembly — Implementation Plan (ASM stream)
|
||||
|
||||
**Design source of truth:** [design/inline-asm-design.md](../design/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-*` and declare their target
|
||||
via the `expected/<name>.target` sidecar marker (Phase 0). Never regenerate
|
||||
snapshots while red.
|
||||
|
||||
## Phase 0 — corpus target-gating (test-infra prerequisite; no compiler code)
|
||||
**Why first.** The flagship v1 examples are `x86_64` (syscall-write, divmod,
|
||||
cpuid) but the dev host is `aarch64`-Darwin, and the corpus runner
|
||||
([src/corpus_run.test.zig](../src/corpus_run.test.zig)) currently (a) never threads
|
||||
a per-example `--target` and (b) has no host-arch gate — its only skip is "marker
|
||||
has no `.sx`". So D.0's `…-syscall-write` markers asserting exit/stdout describe
|
||||
output the harness *cannot* produce on this host, which would violate the cadence
|
||||
rule (the "next commit turns it green" can never happen). Phase 0 closes that gap.
|
||||
It touches **only the runner + two fixtures** — zero compiler code, zero risk to
|
||||
A–E, and unblocks every arch-pinned asm example.
|
||||
|
||||
**Marker taxonomy (the cleanup).** The runner currently spreads per-example
|
||||
*directives* across standalone boolean/value sidecars (`.aot` now, `.target`
|
||||
proposed, more later). Replace that sprawl with **one optional config file,
|
||||
`expected/<name>.build`**, holding all build/run directives; the output snapshots
|
||||
(`.exit` / `.stdout` / `.stderr` / `.ir`) stay separate — they are
|
||||
machine-regenerated data, not config. `.exit` remains the **test-discovery key**
|
||||
(every test has one; `.build` is optional).
|
||||
|
||||
**`.build` format** — JSON, parsed with `std.json`:
|
||||
```json
|
||||
{ "aot": true, "target": "x86_64-linux" }
|
||||
```
|
||||
Parse via `std.json.parseFromSlice(BuildConfig, …)` into
|
||||
`struct { aot: bool = false, target: ?[]const u8 = null }`. Field defaults cover
|
||||
omitted keys; `std.json`'s default `ignore_unknown_fields = false` makes an
|
||||
**unknown key a loud `error.UnknownField`** (surfaced as a runner failure, never a
|
||||
silent ignore — CLAUDE.md no-silent-default rule). Extensible: future `"cpu"`,
|
||||
`"link"`, `"cwd"` are just new optional struct fields, no new sidecar file and no
|
||||
custom parser.
|
||||
|
||||
**What the directives do:**
|
||||
|
||||
1. **`target = <triple|shorthand>`** threads `--target <value>` into every `sx`
|
||||
invocation for that example (`run` / `build` / `ir` — `--target` is a global
|
||||
flag, confirmed [main.zig:39](../src/main.zig#L39)), AND **host-match selects
|
||||
the mode.** The runner parses the leading `arch` + `os` tokens of the resolved
|
||||
triple and compares them to `@import("builtin").target` (normalizing
|
||||
`arm64`→`aarch64`):
|
||||
- **match** → *execute* exactly as today (`sx run`, or `aot` build+exec) with
|
||||
the target threaded, plus the `.ir` diff if an `.ir` snapshot exists. ⇒ an
|
||||
x86_64 example gives **real end-to-end coverage on an x86_64 CI runner**.
|
||||
- **mismatch** → **ir-only**: run *only* `sx ir <file> --target <t>`; assert
|
||||
`.exit` (the ir command's exit), `.ir` (normalized stdout), and `.stderr`
|
||||
(diagnostics, normally empty). Do **not** run/build/exec; do **not** assert
|
||||
`.stdout`. An `.ir` snapshot is **required** in ir-only mode — its absence is
|
||||
a loud runner failure ("arch-pinned <name>: ir-only mode requires an .ir
|
||||
snapshot"), never a silent pass. Robust even if `sx ir` treats `--target` as
|
||||
a partial no-op: the `inline_asm` op carries the template + constraint string
|
||||
verbatim, so the IR snapshot still locks the exact thing §II.11 flags as
|
||||
silently-miscompiling (the constraint assembler + template rewrite).
|
||||
2. **`aot`** is the existing JIT-vs-build+exec switch, just relocated from the
|
||||
standalone `.aot` marker into `.build`.
|
||||
|
||||
**Negative compile-error examples need NO `.build`.** `…-missing-volatile`
|
||||
(no-output-without-`volatile`) is a Sema diagnostic raised before codegen/JIT, so
|
||||
plain `sx run` reports it identically on any host — it stays a normal example with
|
||||
no config file.
|
||||
|
||||
**update-goldens interaction:** in ir-only mode, `-Dupdate-goldens` writes `.exit`
|
||||
(ir exit) + `.ir` (+ `.stderr` if non-empty) and skips `.stdout`. Execute mode
|
||||
(incl. `aot`) is unchanged. `.build` is hand-authored — update-goldens never
|
||||
writes it.
|
||||
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 0.0 | lock | Add `BuildConfig` + `std.json` parse of `expected/<name>.build` (unknown-key ⇒ `error.UnknownField`); **migrate** the 2 existing `.aot` markers → `.build` (content `{ "aot": true }`) and delete them; thread `target`'s `--target` into the spawned argv; add `hostMatchesTarget(value) bool` (arch+os token parse, `arm64`→`aarch64`) gating the **execute** path. Lock with `examples/16xx-platform-target-host.sx` (trivial `main`) + a `.build` `{ "target": "<host arch triple>" }` (still runs+passes) and unit `test`s for the JSON parse + `hostMatchesTarget`. | `src/corpus_run.test.zig`, `examples/expected/1226-*.{aot→build}`, `…/1227-*`, + fixture |
|
||||
| 0.1 | lock | Implement the **mismatch ⇒ ir-only** branch (skip run/build/exec; assert `.exit`+`.ir`+`.stderr` from `sx ir --target`; require `.ir`). Lock with `examples/16xx-platform-target-cross.sx` (asm-free `() -> i64 { return 0; }`) + `.build` `{ "target": "x86_64-linux" }` + a checked-in `.ir` snapshot — exercises ir-only on the arm64 host. | `src/corpus_run.test.zig` + fixture |
|
||||
| 0.2 | docs | Update CLAUDE.md §"Test layout"/§"Testing" to document `.build` (format + `aot`/`target` keys) replacing the standalone `.aot` marker prose (lines ~435, ~492). | `CLAUDE.md` |
|
||||
|
||||
Both 0.0 and 0.1 are **lock** commits: the runner change and the fixture that
|
||||
exercises it land together and pass the moment they land (the mechanism works
|
||||
immediately — nothing is left red), which is the cadence rule's "lock in current
|
||||
behavior" flavor, not a feature red→green. No asm lowering is gated on either.
|
||||
|
||||
**Phase 0 verification:** `zig build test` green; deliberately corrupt the
|
||||
cross-target `.ir` fixture and confirm the runner reports an IR mismatch (proves
|
||||
ir-only actually asserts, isn't a no-op); delete it and confirm the
|
||||
"requires an .ir snapshot" failure fires.
|
||||
|
||||
**Estimated runner delta:** ~70–90 lines (sidecar read + `--target` argv threading
|
||||
+ `hostMatchesTarget` + the ir-only branch + update-mode tweak). Within the
|
||||
"no step > ~500 new lines" rule; well under the read budget.
|
||||
|
||||
## 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.
|
||||
124
current/PLAN-DIST.md
Normal file
124
current/PLAN-DIST.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# PLAN-DIST — bundle `zig` as sx's hermetic link/libc backend
|
||||
|
||||
## Goal
|
||||
|
||||
`sx build` produces a native binary by driving a **bundled `zig`**
|
||||
(`zig cc`) as the linker, so a distributed sx on Linux needs no system
|
||||
`cc`/lld/libc/CRT. `sx run` (JIT) is unaffected — it never links.
|
||||
|
||||
This is the "be like Zig" move: reuse Zig's hermetic toolchain (lld +
|
||||
crt objects + musl/glibc, all bundled in the `zig` distribution) instead
|
||||
of building our own lld-in-process + libc-from-source pipeline.
|
||||
|
||||
> **Configuration surface** (env vars, flags, resolution order,
|
||||
> activation truth table, target→ABI map, distribution layout) is
|
||||
> specified in [../design/bundled-zig-link-backend-design.md](../design/bundled-zig-link-backend-design.md) — the design-of-record
|
||||
> for how the backend is configured. Keep the two files in sync.
|
||||
|
||||
## Locked decisions
|
||||
|
||||
1. **Default Linux output ABI = static musl** (`x86_64-linux-musl`,
|
||||
`-static`). Output runs on ANY Linux with zero deps — the property
|
||||
that makes Zig binaries portable. glibc/dynamic only via explicit
|
||||
`--target x86_64-linux-gnu`.
|
||||
2. **Activation = auto** when a bundled/resolvable `zig` exists AND the
|
||||
user passed no `--linker`. Falls back to system `cc` otherwise.
|
||||
3. **Dev uses PATH `zig`** (0.16.0 already installed). Defer copying a
|
||||
vendored toolchain into `libexec/` until Phase 3 packaging.
|
||||
|
||||
## Why `zig cc`, not raw `ld.lld`
|
||||
|
||||
`zig cc` is a clang-compatible driver, so it slots into the **existing**
|
||||
cc-style argv branch in `src/target.zig` almost unchanged, and supplies
|
||||
lld + crt objects + musl/glibc automatically per `-target`. Driving
|
||||
`ld.lld` directly would force us to locate/pass crt1.o/crti.o/libc
|
||||
ourselves — exactly the work we're avoiding.
|
||||
|
||||
## Key code anchors (verified)
|
||||
|
||||
- Linker selection hook: `TargetConfig.getLinker()` — `src/target.zig:194-196`
|
||||
(`self.linker orelse "cc"`).
|
||||
- Unix `cc`-style link branch: `src/target.zig:524-564` (this is where
|
||||
the zig backend hooks in; `-o`/`-L`/`-l`/extra objects already pass
|
||||
through clang-compatibly).
|
||||
- Exe-relative resolution pattern to mirror for finding zig:
|
||||
`src/imports.zig:204-227` (`discoverStdlibPaths`, `$SX_STDLIB_PATH`
|
||||
override + `<exe>/..` candidates).
|
||||
- `--linker` CLI flag parsing: `src/main.zig:87-90`.
|
||||
- Emit triple (must agree with link target): `src/ir/emit_llvm.zig`
|
||||
(`LLVMSetTarget`, ~L246-284).
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — Resolve a bundled/host zig
|
||||
- New `src/zig_backend.zig`: `discoverZig(alloc) -> ?[]const u8`.
|
||||
Resolution order:
|
||||
1. `$SX_ZIG` env override.
|
||||
2. `<exe>/../libexec/zig/zig` (install layout, Phase 3).
|
||||
3. `<exe>/../../zig-bundle/zig` (dev vendored layout, Phase 3).
|
||||
4. `zig` on `PATH` (dev fallback — active now).
|
||||
- Add `SX_DEBUG_ZIG` trace, matching existing `SX_DEBUG_*` hooks.
|
||||
- No behavior change yet; just resolution + a debug/print hook to confirm.
|
||||
|
||||
### Phase 1 — `zig cc` link backend (core change)
|
||||
- `src/target.zig`: generalize the linker from a single token to a
|
||||
**driver argv**. Today `getLinker()` returns one string at `argv[0]`;
|
||||
introduce a `LinkBackend` so the internal backend contributes
|
||||
`{zigPath, "cc"}` as leading entries.
|
||||
- In the Unix branch (L524-564), when backend = zig:
|
||||
- prepend `zig cc`,
|
||||
- append `-target <mapped triple>`,
|
||||
- add `-static` for musl,
|
||||
- everything else (`-o`, `-L`, `-l`, extra objects, extra link flags)
|
||||
passes through unchanged.
|
||||
- Add `sxTripleToZig()` mapping (sx shorthand/triple → zig `-target`);
|
||||
unspecified-on-Linux → `x86_64-linux-musl`.
|
||||
- Align emit triple: when the zig backend is selected, set the LLVM
|
||||
module triple in `emit_llvm.zig` to match the link target
|
||||
(x86_64-linux), so the `.o` links cleanly against musl crt.
|
||||
|
||||
### Phase 2 — Activation
|
||||
- Auto-enable: if `discoverZig()` succeeds and no `--linker` override,
|
||||
use the zig backend for `sx build`. System `cc` remains the fallback.
|
||||
- Optional explicit `--self-contained` / `--no-self-contained` to force.
|
||||
- Confirm `sx run`/JIT path is untouched (no link step).
|
||||
|
||||
### Phase 3 — Distribution packaging
|
||||
- `build.zig`: a `dist` step assembling
|
||||
- `bin/sx` (built with `-Dstatic-llvm`),
|
||||
- `libexec/zig/` (vendored zig binary **and its `lib/`**, copied from a
|
||||
pinned ziglang.org release per host arch),
|
||||
- `library/` (stdlib),
|
||||
into a relocatable tarball.
|
||||
- Pin the zig version (currently 0.16.0).
|
||||
|
||||
### Phase 4 — Verify & lock
|
||||
- Manual first: `sx build hello.sx` (auto zig backend) then `file`/`ldd`
|
||||
the output → expect "statically linked".
|
||||
- Honor snapshot-integrity + FFI-cadence rules before adding a corpus
|
||||
test (host/arch-gated, likely a `.build` sidecar).
|
||||
|
||||
## Risks / watch
|
||||
|
||||
- **Bundle size**: zig + its `lib/` ≈ 50–60 MB.
|
||||
- **gnu vs musl ABI**: pure codegen objects link fine against musl;
|
||||
TLS/stack-protector are the only realistic friction. Aligning the emit
|
||||
triple (Phase 1) covers the common path.
|
||||
- **macOS/Windows cross** via the same `zig cc -target` is nearly free
|
||||
after Phase 1, but Apple-SDK linking has caveats — scope to Linux
|
||||
target first; treat the rest as follow-up.
|
||||
- **c_import.zig** also shells `cc` for C imports (JIT). Out of scope
|
||||
here; same backend can absorb it later.
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Phase 0 — resolve zig (`src/zig_backend.zig`)
|
||||
- [x] Phase 1 — zig cc link backend (`target.zig` + `emit_llvm` triple normalize)
|
||||
- [x] Phase 2 — activation (`--self-contained`/`--no-self-contained`; auto on bundled zig)
|
||||
- [ ] Phase 3 — dist packaging (vendor `zig` into `libexec/`)
|
||||
- [ ] Phase 4 — verify & lock (manual ✓ macOS/Linux/Windows; corpus test pending runner `--self-contained` support)
|
||||
|
||||
Scope landed as **macOS + Linux + Windows** (not Linux-first). See the
|
||||
"Implementation status" section in
|
||||
[../design/bundled-zig-link-backend-design.md](../design/bundled-zig-link-backend-design.md)
|
||||
for what refined the original locked decisions.
|
||||
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:** [design/inline-asm-design.md](../design/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
|
||||
> `design/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).
|
||||
125
current/PLAN-REIFY.md
Normal file
125
current/PLAN-REIFY.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# PLAN-REIFY — comptime type reflection + construction (`type_info` / `reify`)
|
||||
|
||||
## Goal
|
||||
|
||||
Add the two comptime metaprogramming builtins — **`type_info($T) -> TypeInfo`**
|
||||
(reflect a type → data) and **`reify(info: TypeInfo) -> Type`** (construct a *new
|
||||
nominal type* from data) — plus the sx-lib helpers (`make_enum`, `field_type`,
|
||||
`RecvResult`/`TryResult`) built over them. This is **step 3 of the async-first
|
||||
sequence** ([../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
|
||||
§7); it gates channel result types (`RecvResult($T)`) and `race`'s synthesized
|
||||
tagged-union, and **replaces** a would-be `enum($T)` generic-enum language feature.
|
||||
|
||||
> Rationale + the five validated contracts: design doc §7 step 3 + §8.1. The approach
|
||||
> was grounded by three codebase reviewers — it is a **small extension reusing existing
|
||||
> machinery**, not net-new architecture.
|
||||
|
||||
## Locked design (the five reify contracts — all codebase-validated)
|
||||
|
||||
1. **Nominal identity via type-fn memoization.** `RecvResult(i64)` is one `TypeId`
|
||||
because type-fns dedup by mangled `(fn,args)` name (`generic.zig:1620-1629`) +
|
||||
reify `findByName`. NOT structural dedup — enums are nominal (`types.zig:1110`).
|
||||
2. **Functional through codegen.** A reify'd enum has **no backing AST decl**, and
|
||||
every enum stage is type-table-driven (layout, construct, match+exhaustiveness,
|
||||
`toLLVMType`, `type_name`/format) — so it flows through **unmodified**.
|
||||
3. **Validate loudly** at the `intern`/`internNominal` choke point (`types.zig:411-439`).
|
||||
4. **Comptime-only, JIT-free** — a type-table op in the interpreter; no S1 dependency.
|
||||
5. **Reference-based self-reference** (`*Self`/`[]Self`) via reserve-placeholder→
|
||||
complete (`nominal.zig:86/108/120`, `types.zig:442`); **by-value recursion rejected**.
|
||||
|
||||
Surface follows the **`#builtin`** pattern of the existing reflection builtins
|
||||
(`type_of`/`field_count`/`field_name` in `library/modules/std/core.sx`,
|
||||
`specs.md:2594-2600`) — NOT the BuildOptions compiler-hook registry.
|
||||
|
||||
## Key code anchors (verified by review)
|
||||
|
||||
- Type minting: `TypeTable.intern` / `internNominal` — `src/ir/types.zig:411-439`.
|
||||
- Type-fn instantiation + mangled-name cache — `src/ir/lower/generic.zig:1575-1689`
|
||||
(cache check `:1620-1629`; register inline-struct result `:1663-1689`).
|
||||
- Forward-declare reserve (recursive types) — `src/ir/lower/nominal.zig:86/108/120`;
|
||||
complete a forward-declared type — `src/ir/types.zig:442`.
|
||||
- Enum codegen (all type-table-driven, the reify target shape): size `types.zig:633-636`;
|
||||
`resolveVariantIndex` `lower/expr.zig:1159-1177`; match `lower/control_flow.zig:748-945`;
|
||||
`toLLVMType` `backend/llvm/types.zig:111-154`; `type_name` `types.zig:846-882`.
|
||||
- Existing reflection builtins to mirror — `core.sx` (`#builtin`) + their interp/lower
|
||||
handlers (`src/ir/interp.zig` `type_name`/reflection at ~`:1911`).
|
||||
- Match form — `specs.md:408-424`.
|
||||
|
||||
## 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.
|
||||
Examples: `06xx` (comptime, deterministic), `11xx` (diagnostics for loud failures).
|
||||
|
||||
---
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — `reify` of a flat enum (the core)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 0.0 | lock | `TypeInfo`/`EnumInfo`/`EnumVariant` lib types in `core.sx` (data only); `reify`/`type_info`/`field_type` as bodyless `#builtin` decls (parsed, unimplemented → loud bail). Unit: decls parse. | `library/modules/std/core.sx`, `src/ir/interp.zig` |
|
||||
| 0.1 | xfail | `examples/06xx-comptime-reify-enum.sx` — `reify(.enum_(.{variants=[.{name="value",payload=i64},.{name="closed",payload=void}]}))`, construct `.value(3)`, match it. Red (reify unimplemented). | `examples/06xx-*` |
|
||||
| 0.2 | green | implement `reify(.enum_)` → build `EnumInfo`/`TaggedUnionInfo` `TypeInfo`, `internNominal(info, fresh_nominal_id)`, return `TypeId`. Example green; construct + match work unmodified (Contract 2). | `src/ir/interp.zig`, (`src/ir/types.zig` if a helper is wanted) |
|
||||
|
||||
### Phase 1 — type-fn → reify identity
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 1.0 | xfail | `examples/06xx-comptime-reify-typefn-identity.sx` — `R :: ($T)->Type { reify(...) }`; assert `R(i64)` from two sites is ONE type (assignable/matchable across sites). Red if reify-result not registered by mangled name. | `examples/06xx-*` |
|
||||
| 1.1 | green | register a reify-returning type-fn's result under the instantiation mangled name (mirror the inline-struct path `generic.zig:1663-1689`). Identity holds (Contract 1). | `src/ir/lower/generic.zig` |
|
||||
|
||||
### Phase 2 — `type_info` (reflect) + `field_type`
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 2.0 | xfail | reflect a struct/tuple → read variant/field names + **types** (`field_type($T,i)`). Red. | `examples/06xx-*` |
|
||||
| 2.1 | green | implement `type_info`/`field_type` over the type table (reuse the `field_count`/`field_name` reflection path). | `src/ir/interp.zig` |
|
||||
|
||||
### Phase 3 — `make_enum` + `RecvResult`/`TryResult` (sx lib)
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 3.0 | lock | `make_enum(variants) -> Type` (sx lib over `reify`); `RecvResult($T)`/`TryResult($T)` as type-fns. Behavior-lock: `RecvResult(i64)` constructs + matches. | `library/modules/std/*` |
|
||||
|
||||
### Phase 4 — reference-based self-reference
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 4.0 | xfail | recursive enum via `*Self` (tree/list): `reify_rec((self)=> .enum_(... payload = ptr_to(self) ...))`. Red. | `examples/06xx-*` |
|
||||
| 4.1 | green | `reify_rec` reserve-placeholder→complete (reuse `nominal.zig:86`/`types.zig:442` recursive path). | `src/ir/interp.zig`, `src/ir/types.zig` |
|
||||
|
||||
### Phase 5 — validation + loud diagnostics
|
||||
| Step | Commit | What | Files |
|
||||
|---|---|---|---|
|
||||
| 5.0 | xfail | `examples/11xx-diagnostics-reify-*` — dup variant names, non-integer backing, **by-value self-reference** ("infinite size; use `*Self`"). Pin the messages. | `examples/11xx-*` |
|
||||
| 5.1 | green | validate `TypeInfo` at the `intern`/`internNominal` choke point; emit diagnostics, never a broken type (Contract 3). | `src/ir/interp.zig` / `src/ir/types.zig` |
|
||||
|
||||
> `RaceResult` (tuple→tagged-union synthesis) is **not** in this stream — it lands with
|
||||
> `race` (async cluster), but it consumes exactly the `type_info`+`field_type`+`reify`
|
||||
> primitives built here.
|
||||
|
||||
## Risks / watch
|
||||
|
||||
- **Mangled-name plumbing (Phase 1)** is the one real unknown — confirm the type-fn
|
||||
path registers a *reify-returned* result (not just inline `struct {…}` literals).
|
||||
Fallback: have `reify` itself name the type by the instantiation key + `findByName`.
|
||||
- **Self-ref completion (Phase 4)** must reuse the existing recursive-type
|
||||
reserve→complete path; do not invent a new mutate-after-intern mechanism.
|
||||
- Keep `reify` **comptime-only**: a `reify` reached at runtime is a hard error.
|
||||
|
||||
## Status
|
||||
|
||||
- [ ] Phase 0 — `reify` flat enum
|
||||
- [ ] Phase 1 — type-fn identity
|
||||
- [ ] Phase 2 — `type_info` + `field_type`
|
||||
- [ ] Phase 3 — `make_enum` + `RecvResult`/`TryResult`
|
||||
- [ ] Phase 4 — reference self-reference
|
||||
- [ ] Phase 5 — validation + diagnostics
|
||||
|
||||
## Kickoff prompt (paste into a fresh session)
|
||||
|
||||
> Work the REIFY stream per `current/PLAN-REIFY.md` (+ checkpoint
|
||||
> `current/CHECKPOINT-REIFY.md`). Read the plan header (goal, five locked contracts,
|
||||
> key anchors) first; rationale is in `design/execution-evolution-roadmap.md` §7 step 3
|
||||
> + §8.1. **This session = Phase 0 only** (`TypeInfo` lib types + `reify` of a flat
|
||||
> enum: construct + match). Cadence (IMPASSIBLE): no commit both adds a test and makes
|
||||
> it pass — lock, then xfail→green. `zig build && zig build test` after every step. If
|
||||
> you hit an unrelated compiler bug, follow the CLAUDE.md IMPASSIBLE RULE (file an
|
||||
> issue, stop). Stop at the end of Phase 0; update the checkpoint.
|
||||
384
design/bundled-zig-link-backend-design.md
Normal file
384
design/bundled-zig-link-backend-design.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# Bundled `zig` Link Backend for sx — Design Doc & Proposal
|
||||
|
||||
> Status: **core landed (macOS / Linux / Windows).** This is the
|
||||
> design-of-record for how a distributed sx links native binaries
|
||||
> hermetically. The phased plan lives in
|
||||
> [../current/PLAN-DIST.md](../current/PLAN-DIST.md); keep the two in sync.
|
||||
> User-facing surface is documented in `readme.md` (Cross-Compilation §).
|
||||
|
||||
---
|
||||
|
||||
## Implementation status (landed)
|
||||
|
||||
The core backend is implemented and verified on a macOS host:
|
||||
|
||||
| Target | Result | Notes |
|
||||
|--------|--------|-------|
|
||||
| `--target linux-musl` | static ELF | `zig cc -target x86_64-linux-musl -static` |
|
||||
| `--target windows-gnu` | PE32+ | `zig cc -target x86_64-windows-gnu` |
|
||||
| `--target macos` | Mach-O (runs) | `zig cc -target <arch>-macos`, no `-static` |
|
||||
|
||||
What shipped, and where it **refined** the original locked decisions:
|
||||
|
||||
- **Scope = macOS + Linux + Windows** (not Linux-first). iOS/Android/wasm keep
|
||||
their specialized toolchains. (`TargetConfig.zigBackendInScope`.)
|
||||
- **Auto-activation = a *bundled* zig is found** (a real distribution, or a
|
||||
pinned `$SX_ZIG`). A `PATH`-only zig is the dev fallback and engages **only**
|
||||
under `--self-contained` — so native dev/CI builds are never silently
|
||||
rerouted, across all three OSes. This is the precise meaning of the §5.5
|
||||
"zig found (B)" column: **B = bundled**. *(Refinement of "auto when zig
|
||||
found": PATH-zig does not auto-engage; the musl-only auto gating considered
|
||||
mid-design was dropped in favor of bundled-vs-PATH, which is OS-agnostic.)*
|
||||
- **No translation table** (per the triple-scheme decision): sx triples are
|
||||
passed straight to `zig cc`, and `emit_llvm` runs them through
|
||||
`LLVMNormalizeTargetTriple` so vendor-less zig triples (e.g.
|
||||
`x86_64-windows-gnu`) land their OS/env in LLVM's canonical positions —
|
||||
otherwise "windows" sits in the vendor slot and the object silently falls
|
||||
back to ELF. The one unavoidable exception is **macOS**: the object must be
|
||||
emitted from Apple's `apple-darwin` triple (LLVM needs it for Mach-O), but
|
||||
zig's `-target` parser rejects that scheme, so the *linker* triple alone is
|
||||
the vendor-less `<arch>-macos`. One OS-specific line, not a table.
|
||||
- **New shorthands:** `linux-musl`, `linux-musl-arm`, `windows-gnu` (zig
|
||||
scheme). The existing `linux`/`linux-arm` shorthands were also de-vendored
|
||||
(`x86_64-linux-gnu`, matching the corpus runner's own expander).
|
||||
|
||||
Files: `src/zig_backend.zig` (discovery), `src/target.zig`
|
||||
(`selectZigLinker` / `emitZigLinkArgv` / `zigTargetTriple` / dispatch in
|
||||
`link`), `src/ir/emit_llvm.zig` (triple normalization), `src/main.zig`
|
||||
(`--self-contained` / `--no-self-contained` + shorthands).
|
||||
|
||||
Not yet done: distribution packaging (Phase 3 — vendoring `zig` into
|
||||
`libexec/`), and a corpus regression test (needs the runner to thread
|
||||
`--self-contained`; manual verification only so far).
|
||||
|
||||
The sections below are the original proposal; where they say "Linux-first" or
|
||||
"follow-up" for macOS/Windows, the table above supersedes them.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR + feasibility
|
||||
|
||||
**Problem.** A distributed `sx` compiler can run on a Linux box (static-LLVM
|
||||
binary + relocatable `library/`), but it cannot *finish a build*: the final
|
||||
link step shells out to the host's `cc`, and relies on the host's libc + CRT
|
||||
objects. No `cc`/glibc/SDK on the box → no binary. That is the gap between
|
||||
"sx runs here" and "sx is a toolchain here."
|
||||
|
||||
**Proposal.** Bundle a pinned `zig` binary inside the sx distribution and use
|
||||
`zig cc` as the link backend for `sx build`. `zig cc` brings its own lld,
|
||||
CRT objects, and libc (musl or glibc) for the chosen target. Default Linux
|
||||
output is **statically-linked musl**, which runs on any Linux with zero
|
||||
dependencies — the property that makes Zig's own output portable.
|
||||
|
||||
**Feasibility: high.** The change is contained:
|
||||
- The linker is selected through a single hook —
|
||||
`TargetConfig.getLinker()` at `src/target.zig:194-196` — and the final
|
||||
link argv is built in one place, the Unix `cc`-style branch at
|
||||
`src/target.zig:524-564`.
|
||||
- `zig cc` is a clang-compatible driver, so `-o` / `-L` / `-l` / extra
|
||||
objects pass through that branch unchanged. The backend only has to
|
||||
prepend `zig cc` and add `-target …` / `-static`.
|
||||
- Exe-relative resolution (for finding the bundled zig) is already solved
|
||||
for the stdlib in `src/imports.zig:204-227` and can be mirrored.
|
||||
- `sx run` is JIT and never links, so it is wholly unaffected.
|
||||
|
||||
The cost is a ~50–60 MB vendored `zig` (binary + its `lib/`) in the
|
||||
distribution, and version-pinning discipline.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation & background
|
||||
|
||||
### 1.1 Current state
|
||||
|
||||
| Concern | Today | File |
|
||||
|---------|-------|------|
|
||||
| Compiler binary | Self-containable via `-Dstatic-llvm` (no system LLVM) | `build.zig:9-10,156-162` |
|
||||
| Stdlib | Relocatable, found relative to the exe | `src/imports.zig:204-227` |
|
||||
| **Linking** | **Shells to system `cc`** | `src/target.zig:524-564` |
|
||||
| **libc / CRT** | **Provided by the host `cc` driver implicitly** | (no `-lc`/crt passed) |
|
||||
|
||||
So two of three legs of a portable toolchain already stand. The third — the
|
||||
linker and the libc/CRT it pulls in — is the host dependency this design
|
||||
removes.
|
||||
|
||||
### 1.2 Why this matters for distribution
|
||||
|
||||
The goal is to hand someone a tarball and have `sx build app.sx` produce a
|
||||
working binary on a stock Linux machine — a fresh container, a minimal CI
|
||||
image, a box without `build-essential`. Today that fails at the link step.
|
||||
Zig solved exactly this problem for its own users; since sx is *built with*
|
||||
Zig, the cleanest fix is to stand on Zig's hermetic toolchain rather than
|
||||
re-implement it.
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals & non-goals
|
||||
|
||||
### Goals
|
||||
- `sx build` produces a native Linux binary with **no host `cc`/ld/libc/SDK**.
|
||||
- Default Linux output is **portable** (static musl): runs on any Linux.
|
||||
- **Zero-config in the common case**: a bundled or PATH `zig` is detected and
|
||||
used automatically; the operator sets nothing.
|
||||
- A fully-specified, documented configuration surface (this document) for the
|
||||
cases that *do* need tuning.
|
||||
- No regression for existing users: system `cc` remains a fallback, and any
|
||||
explicit `--linker` still wins.
|
||||
|
||||
### Non-goals (this iteration)
|
||||
- Reimplementing lld in-process or building libc from source (see §7 —
|
||||
Zig already does both; we reuse it).
|
||||
- First-class Windows/macOS cross-compilation (nearly free as a follow-up,
|
||||
but unverified — §11).
|
||||
- Routing C-import compilation (`src/c_import.zig`, which also shells `cc`)
|
||||
through the backend.
|
||||
- Glibc-floor version pinning (`…-gnu.2.28`); exposed only if needed.
|
||||
|
||||
---
|
||||
|
||||
## 3. How Zig achieves hermetic builds (the model we're borrowing)
|
||||
|
||||
Zig's turnkey cross-compilation rests on bundling the two things sx borrows
|
||||
from the host:
|
||||
|
||||
1. **In-process lld.** Zig embeds LLVM's lld (ELF/COFF/Mach-O/wasm) and links
|
||||
without spawning an external linker.
|
||||
2. **libc as data.** Zig ships musl *source* (builds `libc.a` + `crt*.o` on
|
||||
demand, cached → static, no dynamic linker → portable output) and glibc
|
||||
stubs generated from `.abilist` per version. For Windows it ships mingw
|
||||
`.def` files and synthesizes import libraries.
|
||||
|
||||
`zig cc` exposes all of this behind a clang-compatible driver: `zig cc
|
||||
-target x86_64-linux-musl -static foo.o -o foo` yields a portable binary on
|
||||
any host, with nothing installed. **This design consumes that driver rather
|
||||
than rebuilding its internals** — the whole second column above arrives for
|
||||
free by vendoring the `zig` binary.
|
||||
|
||||
---
|
||||
|
||||
## 4. Design overview
|
||||
|
||||
`sx build` gains a **link backend** abstraction with two implementations:
|
||||
|
||||
- `system_cc` — today's behavior (shell `cc`, host libc).
|
||||
- `bundled_zig` — shell `<zig> cc -target <triple> [-static] …`.
|
||||
|
||||
Selection is automatic (§5.5): if a usable `zig` is discovered and the user
|
||||
gave no explicit `--linker`, `bundled_zig` is used; otherwise `system_cc`.
|
||||
The backend plugs into the existing Unix link branch — it contributes the
|
||||
leading `zig cc` tokens and the `-target`/`-static` flags; the rest of the
|
||||
argv assembly is unchanged because `zig cc` is clang-compatible.
|
||||
|
||||
One supporting change: when `bundled_zig` is active, the triple handed to
|
||||
LLVM in `src/ir/emit_llvm.zig` is aligned to the link target (`x86_64-linux`)
|
||||
so the emitted object links cleanly against the selected musl CRT.
|
||||
|
||||
---
|
||||
|
||||
## 5. Detailed design (the configuration surface)
|
||||
|
||||
### 5.1 zig discovery — resolution order
|
||||
|
||||
`discoverZig()` (new `src/zig_backend.zig`) returns the first hit:
|
||||
|
||||
1. `$SX_ZIG` — explicit override.
|
||||
2. `<exe_dir>/../libexec/zig/zig` — **install layout** (§6).
|
||||
3. `<exe_dir>/../../zig-bundle/zig` — **dev vendored layout** (§6).
|
||||
4. `zig` on `PATH` — **dev fallback** (the only one active today).
|
||||
|
||||
`<exe_dir>` is resolved exactly as `src/imports.zig` resolves the stdlib.
|
||||
If none resolve, behavior depends on activation (§5.5): auto-mode silently
|
||||
falls back to `system_cc`; `--self-contained` errors.
|
||||
|
||||
### 5.2 Environment variables
|
||||
|
||||
| Var | Effect | Default |
|
||||
|-----|--------|---------|
|
||||
| `SX_ZIG` | Absolute path to the `zig` used as the link backend. Highest-priority discovery source. | unset |
|
||||
| `ZIG_LIB_DIR` | Path to the bundled zig's `lib/`. Needed **only** if `zig` was relocated away from its `lib/`. In the supported layout (§6) they ship together and zig self-locates — leave unset. | unset |
|
||||
| `SX_DEBUG_ZIG` | Trace discovery: each candidate path and the chosen one (or "none → cc"). Mirrors `SX_DEBUG_STDLIB`. | unset |
|
||||
| `SX_DEBUG_LINK` | **Existing.** Prints the full link argv — shows the exact `zig cc …` invocation. | unset |
|
||||
| `SX_STDLIB_PATH` | **Existing.** Stdlib override; unrelated to linking but noted because a full distribution sets neither and relies on exe-relative discovery for both. | unset |
|
||||
|
||||
### 5.3 CLI flags (`sx build`)
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--self-contained` | Force `bundled_zig` ON. If no usable zig is found, **error** — do not silently fall back. |
|
||||
| `--no-self-contained` | Force `system_cc`. |
|
||||
| `--linker <cmd>` | **Existing.** Explicit linker; supplying it **disables** auto-activation (user's choice wins). To pin a specific zig, prefer `SX_ZIG` + `--self-contained`. |
|
||||
| `--target <triple\|shorthand>` | **Existing.** Selects target + ABI (§5.4). With `bundled_zig` active and target unspecified on a Linux host → `x86_64-linux-musl` static. |
|
||||
| `--sysroot <path>` | **Existing.** Forwarded to the linker; rarely needed with `bundled_zig` (zig brings its own sysroot). |
|
||||
|
||||
### 5.4 Target → ABI mapping
|
||||
|
||||
The default (no `--target`) deliberately differs from the legacy `linux`
|
||||
shorthand, because portable static output is the entire point.
|
||||
|
||||
| `sx` invocation | zig `-target` | Link mode | Portable? |
|
||||
|-----------------|---------------|-----------|-----------|
|
||||
| *(no `--target`, Linux host)* | `x86_64-linux-musl` | `-static` | ✅ any Linux |
|
||||
| `--target linux-musl` *(new)* | `x86_64-linux-musl` | `-static` | ✅ |
|
||||
| `--target linux` / `linux-x86` | `x86_64-linux-gnu` | dynamic | ❌ host glibc, versioned |
|
||||
| `--target linux-arm` | `aarch64-linux-musl` | `-static` | ✅ |
|
||||
| `--target windows` | `x86_64-windows-gnu` | per zig | follow-up (§11) |
|
||||
| `--target macos` / `macos-arm` | `aarch64-macos` | per zig | follow-up (§11) |
|
||||
|
||||
- A **new** `linux-musl` shorthand is added; the existing `linux` shorthand
|
||||
keeps its current gnu/dynamic meaning for back-compat.
|
||||
- The LLVM emit triple is aligned to the link target so the `.o` links
|
||||
cleanly against the selected libc/CRT (§4).
|
||||
|
||||
### 5.5 Activation truth table
|
||||
|
||||
`B` = a usable zig was discovered (§5.1). Subcommand = `sx build`.
|
||||
|
||||
| `--self-contained` | `--no-self-contained` | `--linker` | zig found (B) | Result |
|
||||
|:---:|:---:|:---:|:---:|--------|
|
||||
| — | — | no | yes | **bundled_zig** (auto) |
|
||||
| — | — | no | no | system `cc` (silent fallback) |
|
||||
| — | — | yes | * | user's `--linker` |
|
||||
| yes | — | * | yes | **bundled_zig** (forced) |
|
||||
| yes | — | * | no | **error**: `--self-contained` but no zig |
|
||||
| — | yes | * | * | system `cc` (forced off) |
|
||||
|
||||
- `--self-contained` + `--linker` together: backend choice goes to
|
||||
`--self-contained`; treat the literal combination as a usage error
|
||||
(document, don't guess).
|
||||
- `sx run` / `sx ir` / `sx asm` never link → backend not consulted.
|
||||
|
||||
### 5.6 Emit-triple alignment
|
||||
|
||||
`src/ir/emit_llvm.zig` (`LLVMSetTarget`, ~L246-284) currently uses the host
|
||||
default triple when `--target` is unspecified (on Linux,
|
||||
`x86_64-unknown-linux-gnu`). When `bundled_zig` is active, set the module
|
||||
triple to match the link target (`x86_64-linux`) so codegen and the musl CRT
|
||||
agree. Pure codegen objects are ABI-compatible across gnu/musl; aligning the
|
||||
triple removes the edge-case risk (TLS model, stack protector) up front.
|
||||
|
||||
---
|
||||
|
||||
## 6. Distribution layout (packaging)
|
||||
|
||||
A relocatable tree; everything resolves relative to `bin/sx`, so the whole
|
||||
directory moves/untars anywhere with no env vars set:
|
||||
|
||||
```
|
||||
sx-<os>-<arch>/
|
||||
├── bin/
|
||||
│ └── sx # built -Dstatic-llvm (no system LLVM dep)
|
||||
├── libexec/
|
||||
│ └── zig/
|
||||
│ ├── zig # pinned zig binary
|
||||
│ └── lib/ # zig's lib/ (musl/glibc sources, lld data, …)
|
||||
└── library/ # sx stdlib (existing discovery)
|
||||
└── modules/…
|
||||
```
|
||||
|
||||
Rules:
|
||||
- `zig` and its `lib/` **must** ship together under `libexec/zig/` so zig
|
||||
self-locates `lib/`; splitting them forces `ZIG_LIB_DIR`.
|
||||
- Pinned zig version: **0.16.0** (matches the build toolchain). Record the
|
||||
exact version in the release manifest — a mismatched `zig cc` CLI is the
|
||||
likeliest future breakage.
|
||||
- Vendor the matching zig release per host os/arch from ziglang.org at
|
||||
package time.
|
||||
|
||||
---
|
||||
|
||||
## 7. Alternatives considered
|
||||
|
||||
| Alternative | Why not (now) |
|
||||
|-------------|---------------|
|
||||
| **In-process lld + bundled musl sysroot** (sx owns the pipeline; no zig) | Requires a custom LLVM build *with* lld — the Homebrew `llvm@19` here ships none (`liblld*.a`, headers, `ld.lld` all absent) — plus a C++ lld shim and per-arch prebuilt musl. Strictly more work for the same user-visible result. The right *eventual* target if we want zero foreign binaries; tracked as a follow-up. |
|
||||
| **Full Zig-style: build libc from source on demand** | Most flexible (any arch/libc version, no prebuilt blobs) but the most work; only worth it after the in-process-lld path exists. |
|
||||
| **Document a hard dependency on system `cc`** | Zero engineering, but defeats the goal — the box still needs `build-essential`. Acceptable only as the current fallback, not the distribution story. |
|
||||
| **Bundle just `ld.lld` + a musl sysroot (no full zig)** | Smaller than a whole zig, but we'd hand-manage crt object selection, dynamic-linker paths, and import libs — i.e. re-derive what `zig cc` already encapsulates. Bundle-size saving doesn't justify the fragility. |
|
||||
|
||||
Vendoring `zig` wins on effort-to-result because sx already builds with Zig:
|
||||
it's a first-party dependency, not a foreign toolchain, and it unlocks
|
||||
Windows/macOS targets later for nearly free.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phasing
|
||||
|
||||
Detail in [../current/PLAN-DIST.md](../current/PLAN-DIST.md). Summary:
|
||||
|
||||
0. **Resolve zig** — `discoverZig()` + `SX_DEBUG_ZIG`; PATH fallback only.
|
||||
1. **Link backend** — generalize the linker to a driver argv; emit
|
||||
`zig cc -target … -static`; align the emit triple.
|
||||
2. **Auto activation** — wire the §5.5 truth table; `cc` fallback intact.
|
||||
3. **Packaging** — `build.zig` `dist` step assembling the §6 tree.
|
||||
4. **Verify & lock** — `file`/`ldd` shows "statically linked"; host/arch-gated
|
||||
corpus test honoring the snapshot-integrity + FFI-cadence rules.
|
||||
|
||||
The minimum end-to-end proof is Phases 0+1 against PATH zig.
|
||||
|
||||
---
|
||||
|
||||
## 9. Open decisions
|
||||
|
||||
**Locked:**
|
||||
- Default Linux ABI = **static musl** (portable output).
|
||||
- Activation = **auto** when a usable zig is found and no `--linker`.
|
||||
- Dev uses **PATH zig**; vendoring deferred to Phase 3.
|
||||
|
||||
**Still open:**
|
||||
- Exact spelling of the force flags (`--self-contained` vs e.g.
|
||||
`--bundled-linker`); name chosen here pending review.
|
||||
- Whether auto-mode should *warn* on silent `cc` fallback or stay quiet
|
||||
(leaning quiet, with `SX_DEBUG_ZIG` for diagnosis).
|
||||
- Whether to gate the Phase-4 corpus test behind a `.build` `target`
|
||||
sidecar or keep it manual until a Linux CI runner exists.
|
||||
|
||||
---
|
||||
|
||||
## 10. Risks
|
||||
|
||||
- **Bundle size** ≈ 50–60 MB (zig + `lib/`). Acceptable for a toolchain;
|
||||
call it out in release notes.
|
||||
- **zig CLI drift** across versions — pin hard, record in the manifest;
|
||||
the most likely future breakage.
|
||||
- **gnu vs musl ABI** for the emitted object — covered by the emit-triple
|
||||
alignment (§5.6); TLS/stack-protector are the only realistic friction.
|
||||
- **Operator confusion**: default-no-target (musl) diverging from the
|
||||
`linux` shorthand (gnu). Mitigated by the new `linux-musl` shorthand and
|
||||
explicit documentation (§5.4).
|
||||
|
||||
---
|
||||
|
||||
## 11. Out of scope / follow-ups
|
||||
|
||||
- **Windows / macOS targets** via the same `zig cc -target`: nearly free
|
||||
after the Linux path, but Apple-SDK and Windows specifics need their own
|
||||
verification — not documented as supported until tested.
|
||||
- **`src/c_import.zig`** still shells system `cc` for C imports in JIT mode;
|
||||
route through the backend later.
|
||||
- **In-process lld** (alternative in §7) as the eventual zero-foreign-binary
|
||||
endgame.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — quick recipes (once implemented)
|
||||
|
||||
```sh
|
||||
# Portable static Linux binary (default when a bundled zig is present):
|
||||
sx build app.sx -o app
|
||||
file app # → "ELF 64-bit … statically linked"
|
||||
|
||||
# Force the backend; fail loudly if no zig is bundled:
|
||||
sx build app.sx --self-contained
|
||||
|
||||
# Use a specific zig:
|
||||
SX_ZIG=/opt/zig-0.16.0/zig sx build app.sx --self-contained
|
||||
|
||||
# Opt out, use the system toolchain:
|
||||
sx build app.sx --no-self-contained
|
||||
|
||||
# Dynamic glibc instead of static musl:
|
||||
sx build app.sx --target linux
|
||||
|
||||
# Debug discovery + the exact link invocation:
|
||||
SX_DEBUG_ZIG=1 SX_DEBUG_LINK=1 sx build app.sx
|
||||
```
|
||||
625
design/execution-evolution-roadmap.md
Normal file
625
design/execution-evolution-roadmap.md
Normal file
@@ -0,0 +1,625 @@
|
||||
# Execution-Model Evolution — Roadmap (comptime JIT · async · concurrency · hot-reload)
|
||||
|
||||
> Status: **exploratory design-of-record.** Captures the forward plan for sx's
|
||||
> execution model across five interlocking threads. Not yet an active
|
||||
> `PLAN-*`/`CHECKPOINT-*` stream — this is the shared design the streams would be
|
||||
> carved from. Cross-platform shipping (the bundled-zig backend + the sx bundler)
|
||||
> is **already landed**; see [bundled-zig-link-backend-design.md](bundled-zig-link-backend-design.md)
|
||||
> and [../current/PLAN-DIST.md](../current/PLAN-DIST.md).
|
||||
|
||||
---
|
||||
|
||||
## 0. The thesis
|
||||
|
||||
sx's compiler stays small by pushing capability into **library sx + three general
|
||||
primitives** (`inline asm`, `extern`/`export`, `atomics`) rather than baking
|
||||
features into codegen. Concretely:
|
||||
|
||||
- **Async is a library, not a language feature** — colorblind, stackful fibers
|
||||
behind an `Io` interface (Zig-inspired). No function coloring, no
|
||||
async→state-machine transform. The implementation is pure sx down to a per-arch
|
||||
inline-asm context switch.
|
||||
- **Comptime gains a JIT escape hatch** — the interpreter stays the default
|
||||
(debuggable, portable), but drops to a host-JIT for the one thing it can't
|
||||
walk (inline asm) and, later, for whole fragments (the bundler).
|
||||
- **One shared substrate** — a persistent ORC LLJIT + host-target emitter — serves
|
||||
comptime-asm, the bundler, and JIT-resident hot-reload.
|
||||
|
||||
The honest trade is **small *surface*, but each primitive is *deep*** — not "small
|
||||
compiler." The net-new **compiler** obligations this plan adds (all verified absent
|
||||
today): **atomics lowering** (N1), **generic enums** `enum($T)`, **`type_info` +
|
||||
`reify` + `field_type`** (comptime type construction), **`callconv(.naked)`**,
|
||||
**repointable-`context` codegen** (+ per-fiber stack-limit), the **S1 persistent JIT
|
||||
spine**, **C1 thunk synthesis**, **comptime-asm lifting** (C3), and (later) the **S2
|
||||
ORC C++ shim**. Async itself is genuinely a library; the *enabling primitives* are a
|
||||
major codegen/runtime investment. Already landed: `inline asm` (in flight),
|
||||
`extern`/`export`, the `!`/`try`/`catch`/`onfail`/`raise` ERR stream, value-level
|
||||
reflection, the `sx run` ORC LLJIT, and the host-FFI trampolines.
|
||||
|
||||
---
|
||||
|
||||
## 1. The spine (shared substrate)
|
||||
|
||||
| ID | Piece | What | Size |
|
||||
|----|-------|------|------|
|
||||
| **S1** | Persistent JIT executor | A long-lived ORC LLJIT + a host-triple `LLVMEmitter` + a compiled-fragment cache, plumbed into the interpreter. Today the LLJIT exists only for `sx run`'s `main` ([target.zig:319](../src/target.zig#L319)); the emitter carries one target machine ([emit_llvm.zig:274](../src/ir/emit_llvm.zig#L274)). | L |
|
||||
| **S2** | ORC C++ shim | `MachOPlatform::Create` + redirectable/lazy-reexport symbols. The bare `LLVMOrcCreateLLJIT` can't do thread-locals, C constructors, or symbol redefinition — the wall the C-with-sx JIT spike hit (`_Thread_local` SIGABRT; `errors-*` examples crashed). Required by any non-trivial JIT or symbol repoint. | M |
|
||||
|
||||
S1/S2 are the spine: built once, consumed by **C1** (the FFI thunks — the main
|
||||
near-term consumer), **C3**, and (later) **R2**. S1 alone suffices for C1/C3 (bare
|
||||
calling/asm thunks — no TLS/ctors); S2 is only needed for R2 and JIT-ing C-with-sx.
|
||||
|
||||
---
|
||||
|
||||
## 2. Comptime / build layer
|
||||
|
||||
| ID | Piece | Unblocks | Depends | Size |
|
||||
|----|-------|----------|---------|------|
|
||||
| **C1** | **Real comptime FFI — JIT calling-thunks (LLVM = single ABI authority).** Trivial calls (scalar/ptr/string args, single-reg return) keep the existing `host_ffi.zig` trampoline fast-path; everything else (floats, structs-by-value, aggregate returns, >8 args, varargs) synthesizes a per-signature thunk, JIT-compiles it via **S1**, and calls it with an args buffer the interpreter fills by known layout (`type_info`). **LLVM emits the ABI-correct call — the same lowering as runtime codegen — so comptime and runtime FFI share ONE ABI implementation.** Rejected: libffi (foreign 2nd ABI impl), hand-rolled sx+asm (3rd impl + drift risk + needs C3 to run its own asm leaf anyway). | struct/string/slice/float signatures at comptime; full C interop in `#run`; lifts the bundler's API straightjacket; unifies comptime+runtime FFI | S1 (fast-path: none) | L |
|
||||
| **C2** | **`#compiler` → `extern` collapse** — BuildOptions hooks become real exported C symbols resolved through C1; `*BuildConfig` threaded via global/handle; delete `.compiler_expr`/`compiler_call`/Registry. | one FFI mechanism, not two | C1 (`extern`/`export` already shipped) | M |
|
||||
| **C3** | **Comptime asm via host-JIT** — stop bailing on `inline_asm` ([interp.zig:1019](../src/ir/interp.zig#L1019)); lift the block (operand model at [inst.zig:354](../src/ir/inst.zig#L354): inputs/`out_value`/`out_place`/`out_ty`/clobbers) to a host-arch thunk via `LLVMGetInlineAsm`, JIT, call through C1, cache by template+sig. | running asm-containing code at comptime | S1, C1 (+S2 non-trivial) | M |
|
||||
| **C4** *(DROPPED)* | **JIT-the-bundler** — **not built** (Decision 6). Interp+C1 is the shipping bundler (I/O-bound, so native speed is moot; C1 closes the only capability gap). Remains an always-available S1 optimization if profiling ever shows the bundler's *own logic* is a hotspot. | — | — | — |
|
||||
|
||||
**Residue:** cross-arch comptime asm (C3) can't run on the host — narrows the bail
|
||||
to the cross-compile case; needs a sharp diagnostic ("asm targets `<arch>`, host
|
||||
is `<host>`").
|
||||
|
||||
---
|
||||
|
||||
## 3. Concurrency primitives (atomics + threads)
|
||||
|
||||
> **Why this is its own section:** we are doing **multiple OS threads**, so the
|
||||
> async runtime and any lock-free structure need real atomics. OS threads already
|
||||
> exist; atomics do not.
|
||||
|
||||
| ID | Piece | State | Size |
|
||||
|----|-------|-------|------|
|
||||
| **N1** | **Atomics — NET-NEW compiler feature.** Atomic load/store/RMW (`add/sub/and/or/xor/swap` + `fetch_min`/`fetch_max`; no `nand`), `compare_exchange`/`_weak` (→ `?T`, **null = success**), and fences, with orderings (relaxed/acquire/release/acq_rel/seq_cst). LLVM provides all — an **emit** feature, not a runtime library. **Surface LOCKED = `Atomic($T)` wrapper + `Ordering` enum** (not `@atomic_*` — `@` is address-of in sx). | **lowering absent** — zero LLVM `atomicrmw`/`cmpxchg`/`fence` emission today; some IR/inference scaffolding exists | M |
|
||||
| **N2** | **OS threads + pthread Mutex/Cond + worker Pool** | **landed** — [std/thread.sx](../library/modules/std/thread.sx) (`pthread_create`/`join`/`detach`, in-place `Mutex`/`Cond`, bounded `Pool`). NOTE: pthread mutex **blocks the OS thread** — it is *not* fiber-aware (it would park every fiber on that thread); fiber-aware sync is N3, built on N1. | — |
|
||||
| **N3** | **Fiber-aware sync** — mutex / channel / waitgroup that **suspend the fiber**, not the OS thread. Hybrid: atomic fast-path (N1) + fiber-suspend slow-path (A2/A5). Distinct from the pthread primitives in N2. | new library | M |
|
||||
|
||||
**Compiler obligation for N1:** the emit must map sx orderings to LLVM's and **not
|
||||
reorder across atomics/fences**. Comptime is single-threaded, so the interpreter
|
||||
can treat atomic ops as ordinary ops (seq_cst is trivially satisfied with one
|
||||
thread) — no interp atomics machinery needed.
|
||||
|
||||
**N1 is a prerequisite for M:N scheduling (A5) and N3, and is broadly useful**
|
||||
(lock-free queues, refcounts, the allocator). It is the load-bearing new primitive
|
||||
this revision adds.
|
||||
|
||||
---
|
||||
|
||||
## 4. Async — colorblind, stackful, pure-sx
|
||||
|
||||
**Commitment:** no function coloring, no async→state-machine transform. Async is a
|
||||
capability carried in `context` (like `context.allocator`), not a property of a
|
||||
function's signature. A function does I/O through `context.io`; whether the call
|
||||
suspends is decided by the `Io` *implementation*, transparently.
|
||||
|
||||
| ID | Piece | Notes | Size |
|
||||
|----|-------|-------|------|
|
||||
| **A1** | **`Io` interface + `context.io`** — a protocol/vtable threaded like `Allocator`. `io.async(fn,args) → Future`, `future.await`, cancellation. | leverages protocols + context | M |
|
||||
| **A2** | **Stackful coroutine runtime — in sx lib, NOT a compiler builtin.** The context-switch is a `callconv(.naked)` sx fn with an inline-asm body (save callee-saved + SP/LR into `*from`, load from `*to`, `ret`); fiber bootstrap + stack alloc (`mmap`+guard via `extern`) also sx. The **compiler's** job is only (a) the general primitives — inline asm, `callconv(.naked)`, atomics — and (b) **fiber-safe codegen**: `context` lowered as a *repointable indirection* (never raw TLS) so the switch can repoint it, and stack-limit guards (if emitted) read from a swappable per-fiber location. Most arch-delicate sx in the tree (must match the platform callee-saved set + the compiler ABI), but it's inspectable sx, not a black box. | per-arch, arch-gated; co-validate vs codegen | M |
|
||||
| **A3** | **Event-loop `Io` impls** — kqueue / epoll / io_uring drive readiness, then the (now-ready) syscall via C1. Plus a trivial **blocking `Io`**. | pure sx around syscall `extern`s | L |
|
||||
| **A4** | **Stdlib I/O rework** — fs/socket/process take/use `context.io` instead of raw blocking syscalls, so existing calls participate in async. | mirrors the allocator-threading rule | M |
|
||||
| **A5** | **Schedulers — M:1 → N×(M:1) → M:N, all sx std-lib `Io` vtables (committed; M:N last, not deferred).** M:1 first (minimal vehicle to validate the colorblind stack; covers I/O-bound). N×(M:1) = first parallel step (per-thread M:1 loops + `std/thread.sx` spawn; shared state uses N1 atomics — expected under parallelism, not a wart). M:N work-stealing last (most machinery: thread-safe steal queues + migration + errno/TLS discipline). All over N1 atomics + the A2 asm context-switch + `extern` syscalls. **pinning** API for thread-affine work (UI main thread, GL context). | see §4.3 | M (M:1) / M (N×M:1) / L (M:N) |
|
||||
|
||||
### 4.1 How control enters sx (the colorblind model)
|
||||
|
||||
- **sx→sx is ordinary.** The whole call chain lives on the fiber stack; a suspend
|
||||
at a leaf `io.*` freezes the native stack verbatim. No frame knows it suspended.
|
||||
**Zero special handling at call boundaries** — that's the point.
|
||||
- **Three inbound boundaries** where the runtime enters sx:
|
||||
1. **Task entry** (`io.async(fn)`) — a trampoline starts `fn` on a fresh fiber
|
||||
stack via the normal calling convention.
|
||||
2. **Resumption** — a context-switch (asm), *not* a call; sx continues mid-stack.
|
||||
3. **C callback → sx** — must be `export`/`callconv(.c)`; runs on the event-loop
|
||||
stack (not a fiber) so it **cannot itself suspend** — it may resume/enqueue a
|
||||
fiber or run a non-suspending sx fn to completion (leaf-only).
|
||||
|
||||
### 4.2 `context` is fiber-local (the key obligation)
|
||||
|
||||
`context.io`/`context.allocator`/the `push Context` stack are dynamically scoped.
|
||||
Fibers time-share OS threads (and **migrate** under M:N), so `context` must travel
|
||||
**with the fiber** — saved/restored on every context-switch — **never a raw TLS
|
||||
read.** A spawned task snapshots the spawner's context, then evolves its own
|
||||
`push Context` stack. This is the CLAUDE.md "capture your owning allocator" rule one
|
||||
level up: ambient state that outlives a suspension point must be carried by the
|
||||
fiber.
|
||||
|
||||
### 4.3 Threads & the two hazard classes (why atomics)
|
||||
|
||||
| Model | Parallelism | Migration | Hazards |
|
||||
|-------|-------------|-----------|---------|
|
||||
| **M:1** (1 OS thread) | none | none | cooperative, race-free — simplest |
|
||||
| **N×(M:1)** (per-thread schedulers, no migration) | yes | none | **data races** on shared state → atomics/locks |
|
||||
| **M:N** (work-stealing) | yes | yes | data races **+** TLS-migration hazards |
|
||||
|
||||
- **Parallelism hazard** (any N>1): shared mutable state races → needs **N1
|
||||
atomics** + N3 fiber-aware sync. The M:1 "no locks" simplicity is gone.
|
||||
- **Migration hazard** (M:N only): a fiber that moves threads across a suspend
|
||||
reads the *wrong* thread's TLS. **`errno` must be captured immediately** after
|
||||
each syscall; **`context` must be fiber-local** (§4.2) — non-negotiable under M:N.
|
||||
- **Pinning** (`io.pinToThread()`): some work must stay put — the **UI main
|
||||
thread** (UIKit/macOS/Android — directly the app targets in §6), OpenGL
|
||||
current-context, TLS-using FFI. M:N needs a "don't migrate / main-thread-only"
|
||||
fiber attribute (Go's `LockOSThread`).
|
||||
|
||||
### 4.4 Pure-sx boundary
|
||||
|
||||
Everything is sx except the irreducible FFI floor: the **asm context-switch**
|
||||
(per-arch, in `.sx`), **syscall `extern`s** (kernel-implemented, like any libc
|
||||
binding), and **raw stack memory** (`mmap`). The schedulers, event loops, futures,
|
||||
cancellation, and sync primitives are ordinary sx. Payoff: **swappable `Io`
|
||||
vtables** — blocking, io_uring, kqueue, a **mock `Io`** for tests, a
|
||||
**deterministic-simulation `Io`** (fake clock, scripted readiness) for reproducible
|
||||
concurrency tests — all libraries.
|
||||
|
||||
### 4.5 Comptime async = blocking `Io`
|
||||
|
||||
At comptime install the **blocking `Io`**: `io.*` just blocks; no fibers, no
|
||||
scheduler, no suspend. Same source, different vtable. The interpreter never needs
|
||||
suspend/resume, and the FFI (C1) needs no async awareness. This is *why* the
|
||||
colorblind model resolves comptime async for free.
|
||||
|
||||
### 4.6 Syntax surface (grounded against the grammar)
|
||||
|
||||
All of the concurrency/atomics surface lands on **existing** sx grammar — `enum`
|
||||
tagged unions + `if x == { case … }` match ([specs.md:364,408](../specs.md#L408)),
|
||||
first-class **tuples** with named fields ([specs.md:815-852](../specs.md#L815)),
|
||||
`=>` closures, `struct($T)` generics, `callconv(...)`, and the ERR keywords
|
||||
(`try`/`catch`/`onfail`/`raise`/`error`). `race`/`async`/`await`/`atomic` are **not
|
||||
reserved words** ([specs.md:168](../specs.md#L168)), so they stay library
|
||||
types/methods — no keyword additions. One genuinely-new compiler capability is
|
||||
required (see end).
|
||||
|
||||
**Atomics (N1) — generic wrapper type.**
|
||||
```sx
|
||||
Ordering :: enum { relaxed; acquire; release; acq_rel; seq_cst; }
|
||||
Atomic :: ($T: Type) -> Type #builtin; // atomicity carried by the type
|
||||
|
||||
counter : Atomic(i64) = .init(0);
|
||||
counter.store(0, .relaxed);
|
||||
n := counter.load(.acquire);
|
||||
prev := counter.fetch_add(1, .seq_cst); // + fetch_sub/and/or/xor (min/max: open)
|
||||
old := counter.swap(42, .acq_rel);
|
||||
got := counter.compare_exchange(old, new, .acq_rel, .acquire); // strong → ?T (null = success)
|
||||
got2 := counter.compare_exchange_weak(old, new, .acq_rel, .acquire); // may fail spuriously; for retry loops
|
||||
fence(.seq_cst);
|
||||
```
|
||||
- CAS takes **two orderings** (success, failure); failure ordering may not be
|
||||
`release`/`acq_rel` nor stronger than success — enforce in the compiler.
|
||||
- Weak vs strong matters on **aarch64** (LL/SC) — weak in a loop is the idiom;
|
||||
both compile identically on x86.
|
||||
|
||||
**Channels (N3) — methods only (no `<-`); `recv` returns a tagged union (not `(v, ok)`).**
|
||||
```sx
|
||||
RecvResult :: enum($T: Type) { value: T; closed; } // ordinary generic enum (not the race-synthesized union)
|
||||
TryResult :: enum($T: Type) { value: T; empty; closed; } // non-blocking: 3 states a bool can't express
|
||||
|
||||
ch := Channel(i64).make(16); // capacity; .make() unbuffered
|
||||
ch.send(v);
|
||||
if ch.recv() == { case .value: (v) { use(v); } case .closed: { /* drained */ } }
|
||||
ch.close();
|
||||
// ergonomic layer: `for ch (v) { … }` consumes until closed, hiding RecvResult
|
||||
```
|
||||
|
||||
**Fiber-aware locks (N3) — explicit lock + `defer` (no guard sugar).**
|
||||
```sx
|
||||
m : Mutex;
|
||||
m.lock(); defer m.unlock();
|
||||
```
|
||||
|
||||
**Futures & spawn (A1).**
|
||||
```sx
|
||||
f := context.io.async(worker, arg); // Future(R)
|
||||
r := f.await(); // suspends this fiber
|
||||
f.cancel();
|
||||
d := context.io.timeout(5000); // a Future too — raceable like any other
|
||||
```
|
||||
|
||||
**Pinning (A5) — spawn attribute, accepts a thread handle.**
|
||||
```sx
|
||||
PinTarget :: enum { any; main; on: Thread; } // default = .any (may migrate)
|
||||
f := context.io.async(render, pin = .main);
|
||||
f := context.io.async(worker, pin = .on(some_thread));
|
||||
```
|
||||
|
||||
**`race` (Zig model — over futures, named tuple in → synthesized tagged-union out).**
|
||||
The input is a **named tuple** (positional also allowed → `.0`/`.1` tags); the
|
||||
result is an anonymous tagged union whose variants mirror the tuple's labels, each
|
||||
payload = that field's `Future(T)` projected to `T`. Losers are **cancelled and
|
||||
joined** before `race` returns (structured).
|
||||
```sx
|
||||
fa := context.io.async(read_a, conn); // Future(A)
|
||||
fb := context.io.async(read_b, conn); // Future(B)
|
||||
|
||||
winner := context.io.race((a: fa, b: fb)); // RaceResult = enum { a: A; b: B }
|
||||
if winner == {
|
||||
case .a: (v) { handle_a(v); } // v : A
|
||||
case .b: (v) { handle_b(v); } // v : B
|
||||
}
|
||||
// positional form: race((fa, fb)) → tags .0 / .1
|
||||
```
|
||||
The Go-style handler-map and the map literal that propped it up are **dropped** —
|
||||
`race` over futures subsumes select, and cancellation handles the losers.
|
||||
|
||||
**Cancellation rides ERR.** A cancelled `io.*` **raises**; the fiber unwinds
|
||||
through `defer`/`onfail` (`try`/`catch`/`raise` are real keywords). Cancellation is
|
||||
**cooperative** (observed only at suspend points — every `io.*` is a cancellation
|
||||
point) and **structured** (`race` joins losers' teardown before returning). No
|
||||
parallel unwind path — it reuses the error channel.
|
||||
|
||||
**Context switch (A2).**
|
||||
```sx
|
||||
swap_context :: (from: *Fiber, to: *Fiber) callconv(.naked) {
|
||||
asm { /* save callee-saved + SP into *from; load from *to; ret */ };
|
||||
}
|
||||
```
|
||||
`callconv(.naked)` ≠ `callconv(.c)`: **no prologue/epilogue/frame** — required
|
||||
because a context switch deliberately makes SP-in ≠ SP-out (a `.c` epilogue would
|
||||
restore from the wrong stack). Body is a single `asm` block; you emit your own
|
||||
`ret`. Args arrive in ABI registers, read directly from asm.
|
||||
|
||||
**One new compiler capability (gates `race`):** *comptime tuple→tagged-union
|
||||
synthesis.* Reflection today only **reads** types (`field_count`/`field_name`/
|
||||
`type_of`); `RaceResult(T)` must **construct** an anonymous `enum` from a tuple's
|
||||
`(label, payload-type)` pairs. Supporting pieces: a `field_type($T, i) -> Type`
|
||||
reflection accessor (we have value-level `field_value` + `type_of`, but type-only
|
||||
field projection is missing) and `Future(T) → T` projection (falls out of
|
||||
generics). This is the generic "derive a sum from a product" — useful beyond
|
||||
`race`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dev loop / hot-reload
|
||||
|
||||
| ID | Piece | Notes | Depends | Size |
|
||||
|----|-------|-------|---------|------|
|
||||
| **R1** | **Hot-reload (dylib swap)** — host owns `State`+allocator; reloadable module is a `.dylib` with a fixed `export` interface; watch→rebuild→`dlopen`→rebind→`dlclose`. State survives (host-owned). | leans on `export` (shipped); sidesteps S2; native | — | M |
|
||||
| **R2** | **Hot-reload (JIT-resident)** — program runs under S1's LLJIT; reloadable calls route through ORC indirection stubs, repointed on change. Finer granularity; same spine. | | S1, S2 | L |
|
||||
| **R3** | **Incremental compilation** — dependency tracking + recompile-only-changed. Perf enabler; coarse per-file v1 suffices first. | | — | L |
|
||||
|
||||
**Core rule:** the data that must survive a reload cannot be owned by the code that
|
||||
reloads. Code/state separation — the CLAUDE.md owning-allocator discipline, one
|
||||
level up.
|
||||
|
||||
**Residue — state migration on layout change:** body-only changes hot-swap;
|
||||
layout/signature/global-type changes are **detected** (compare new vs running
|
||||
`State` layout via `types.zig`) and trigger **rebuild+restart**. Migration hooks
|
||||
(`on_reload(old)→new`) are a hard later item. Design against *silent* corruption.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cross-platform (mostly landed) — from a macOS laptop
|
||||
|
||||
### 6.1 Landed
|
||||
|
||||
| Capability | State | Reach from a mac |
|
||||
|---|---|---|
|
||||
| `extern`/`export` C linkage | done (replaced `#foreign`) | all targets |
|
||||
| Bundled-`zig cc` cross-link backend | Phases 0–2 done; packaging pending | **macOS, Linux(-musl/static), Windows(-gnu)** verified |
|
||||
| sx-side bundler (`.app`/`.apk`) | done | macOS, iOS sim/device, Android |
|
||||
| JIT `sx run` (ORC LLJIT) | done | host |
|
||||
| Target shorthands | done | `macos[-arm]`, `linux[-musl[-arm]]`, `windows[-gnu]`, `ios[-arm]`, `ios-sim[-arm/-x86]`, `android[-arm64/-x86_64]`, `wasm` |
|
||||
|
||||
### 6.2 Workflows
|
||||
|
||||
```sh
|
||||
# macOS (native): inner loop is JIT; ship is Mach-O / .app
|
||||
sx run app.sx
|
||||
sx build app.sx -o app
|
||||
sx build app.sx --bundle MyApp.app
|
||||
|
||||
# Linux (cross, landed killer feature): static, zero-dep ELF
|
||||
sx build app.sx --target linux-musl -o app # scp anywhere, runs
|
||||
|
||||
# Windows (cross, landed, MinGW path): PE32+
|
||||
sx build app.sx --target windows-gnu -o app.exe # cf. example 1660 (win32)
|
||||
|
||||
# iOS simulator (mac-only host)
|
||||
sx build app.sx --target ios-sim --bundle App.app
|
||||
|
||||
# iOS device — signing threaded via the build program (BuildOptions setters)
|
||||
# #run { o := build_options(); o.set_bundle_id(...); o.set_codesign_identity(...);
|
||||
# o.set_provisioning_profile(...); }
|
||||
sx build build.sx --target ios --bundle App.app
|
||||
|
||||
# Android (cross + bundle): javac → d8 → aapt2 → zipalign → apksigner, then adb
|
||||
sx build app.sx --target android --apk app.apk
|
||||
```
|
||||
|
||||
### 6.3 Where the roadmap lights up cross-platform
|
||||
|
||||
- **C1 + C4** → the iOS/Android **bundlers** (orchestrate ~a dozen host tools at
|
||||
comptime; biggest win; always host-arch so no cross-arch risk).
|
||||
- **R1/R2 + A1–A5** → the **inner dev loop for non-host targets**: push-a-dylib +
|
||||
remote-trigger-reload over an async laptop↔device channel — a capability that
|
||||
*doesn't exist today* short of full rebuild+reinstall.
|
||||
- **A1/A2 colorblind `Io`** → the dev tooling is itself async, and the **same
|
||||
networking code runs blocking inside the bundler** (`adb push`) and async in the
|
||||
live session — no coloring.
|
||||
- **Pinning (A5)** → the UI render fiber pins to the main OS thread on every app
|
||||
target.
|
||||
|
||||
**The single hard constraint the matrix exposes:** cross builds mean target arch ≠
|
||||
host arch, so **C3's residue bites** — comptime/`#run` code reaching *target-arch*
|
||||
inline asm can't execute on the mac. Native macOS dev never hits it; every cross
|
||||
target must gate comptime asm to host-arch (`when host_arch == …`) or get a loud
|
||||
diagnostic.
|
||||
|
||||
---
|
||||
|
||||
## 7. Linear build sequence (async-first — no parallel streams)
|
||||
|
||||
Single ordered list; deps satisfied at every step. **Async-first** (user-chosen): the
|
||||
async story needs no JIT spine (syscalls use the existing trampoline FFI; comptime
|
||||
async = blocking `Io`), so the FFI/JIT cluster comes *after*. C4 is omitted (dropped —
|
||||
an S1 optimization if ever profiled). Net-new compiler prereqs (per the codebase
|
||||
grounding) are explicit steps, not buried.
|
||||
|
||||
**Foundations — compiler primitives the async story needs (all net-new):**
|
||||
1. **N1 — Atomics lowering.** IR/inference scaffolding exists; add LLVM
|
||||
`atomicrmw`/`cmpxchg`/`fence` emission + orderings. Surface = `Atomic($T)` wrapper.
|
||||
Gates channels/N3 + parallel schedulers.
|
||||
2. ~~**Generic enums** `enum($T)`~~ **DROPPED.** `RecvResult($T)`/`TryResult($T)` are
|
||||
**type-fns over `reify`** (step 3), not a new `enum($T)` language feature — and
|
||||
type-fns (user `($T)->Type` in type position) **already work** (e.g.
|
||||
[`Make`](../examples/0208-generics-value-param-type-function.sx),
|
||||
[`Complex`](../examples/0201-generics-generic-struct.sx)). A declarative `enum($T)`
|
||||
surface, if ever wanted, is later *sugar* desugaring to a type-fn-over-`reify`.
|
||||
3. **`type_info` + `reify` + `field_type`** — comptime metaprogramming floor. Gates
|
||||
`race` synthesis **and** channel `RecvResult`/`TryResult` (all type-fns over
|
||||
`reify`; **generic-enum syntax dropped**). **Validated against the codebase (3
|
||||
reviewers): a small extension reusing existing machinery throughout — not net-new
|
||||
architecture.** Five contracts:
|
||||
1. **Nominal identity via type-fn memoization** — type-fns dedup by mangled
|
||||
`(fn,args)` name (generic.zig:1620-1629) + reify `findByName`, so `RecvResult(i64)`
|
||||
is one `TypeId` and the body runs once. (NOT structural dedup — enums are
|
||||
nominal via `nominal_id`, types.zig:1110.)
|
||||
2. **Functional through codegen** — layout / construct / match+exhaustiveness /
|
||||
`toLLVMType` / `type_name`+format are **all type-table-driven, zero AST
|
||||
coupling**, so a backing-decl-less reify'd enum flows through unmodified.
|
||||
3. **Validate loudly** at the single `intern`/`internNominal` choke point
|
||||
(types.zig:411-439): reject dup variants / bad backing / unresolved payloads.
|
||||
4. **Comptime-only, JIT-free** — a type-table op in the interp; no S1 dependency
|
||||
(keeps reify, hence channels + `race`, off the JIT critical path).
|
||||
5. **Reference-based self-reference (v1)** — `*Self`/`[]Self` payloads via the
|
||||
reserve-placeholder→complete path recursive *source* types already use
|
||||
(nominal.zig:86/108/120, types.zig:442); **by-value recursion rejected** (loud,
|
||||
infinite size). reify gains a `reify_rec((self) => …)` builder form.
|
||||
- **Type-minting precedents (7):** monomorphization, protocol vtables, tuples,
|
||||
vector/array, ptr/slice ctors, FFI stubs, **type-fn instantiation** — all
|
||||
construct `TypeInfo` programmatically + `intern()`. **Residual = plumbing, not
|
||||
capability:** name reify-results by the instantiation's mangled name (done for
|
||||
inline-struct bodies — extend to reify-results) + reify input validation.
|
||||
4. **`callconv(.naked)`** — extend `CallConv {default, c}` (types.zig:169) + skip
|
||||
prologue/epilogue lowering. Gates A2.
|
||||
5. **Repointable-`context` codegen** — lower `context` as a swappable indirection
|
||||
(never raw TLS) + per-fiber stack-limit. Compiler obligation; gates A2 *and*
|
||||
cross-fiber `context.io` correctness. (Reviewer note: this is a **prerequisite**
|
||||
of A2, not a successor.)
|
||||
|
||||
**Async runtime — sx lib over the primitives:**
|
||||
6. **A1 — `Io` interface + `context.io` + `Future` + `cancel()` API.**
|
||||
7. **A2 — fiber runtime** (naked context-switch asm, bootstrap, `mmap` stacks).
|
||||
8. **A3 — blocking `Io` → deterministic-sim `Io` (keystone, calibrated) → event-loop `Io`.**
|
||||
9. **A5·M:1 — single-thread scheduler.**
|
||||
10. **N3 — fiber-aware sync** (channels/mutex/waitgroup; `recv → RecvResult`).
|
||||
11. **A6 — Cancellation.** `.canceled` in the `!` channel (model a); per-fiber atomic
|
||||
flag (N1); every `io.*` a cancellation point; structured cancel-and-join; **masked
|
||||
during cleanup**.
|
||||
12. **A4 — stdlib I/O rework** (fs/socket/process onto `context.io`).
|
||||
13. **A5·N×(M:1)** — first parallel (errno-capture + `context`-fiber-local discipline).
|
||||
14. **A5·M:N** — work-stealing (steal queues + migration + pinning).
|
||||
|
||||
**Then comptime / FFI / JIT cluster:**
|
||||
15. **S1 — persistent JIT spine** → 16. **C1 — real FFI (LLVM = ABI authority, on S1)**
|
||||
→ 17. **C2 — `#compiler`→`extern`** → 18. **C3 — comptime asm** (S1 + C1; +S2 if
|
||||
TLS/ctors).
|
||||
|
||||
**Deferred tail:**
|
||||
19. **S2 — ORC C++ shim** (highest-risk — see §8; macOS `MachOPlatform`; ELF/COFF
|
||||
unplanned) → 20. **R1 — dylib reload** (shipped `export`) → 21. **R2 —
|
||||
JIT-resident reload** (S1 + S2; **↔ async live-fiber coupling**, §8) → 22. **R3 —
|
||||
incremental compilation**.
|
||||
|
||||
Hard edges to remember: **C1 depends on S1** (the non-trivial FFI cases); **C3 depends
|
||||
on C1** (calls through its thunk path); **R1/R2 couple to the async runtime** (can't
|
||||
hot-swap code with live suspended fibers — runtime + long-lived fibers stay
|
||||
persistent, only leaf logic reloads).
|
||||
|
||||
---
|
||||
|
||||
## 8. Irreducible hard problems (detect-and-degrade, don't pretend)
|
||||
|
||||
1. **State migration across layout change** (R1/R2) → v1 detects + rebuild/restart;
|
||||
migration hooks later.
|
||||
2. **Cross-arch comptime asm** (C3) → can't run on host; narrows the bail + loud
|
||||
diagnostic; gate to host-arch.
|
||||
3. **M:N migration hazards** (A5) → errno-capture discipline + fiber-local context
|
||||
(mandatory), pinning for thread-affine work.
|
||||
|
||||
### 8.1 Highest technical risks (from review — ranked, async-first lens)
|
||||
|
||||
1. **A2 context-switch correctness** (in the async critical path). Silent stack
|
||||
corruption, per-arch, **untestable by the deterministic-`Io` harness** (it tests
|
||||
*scheduling*, not the *switch*); a one-register slip is invisible until it crashes
|
||||
on the right arch. Couples *library asm* to the *compiler ABI* — ABI drift breaks
|
||||
it silently later. → needs a dedicated **switch-stress test** (§10).
|
||||
2. **`reify` → anonymous-tagged-union → match-codegen** (gates `race` + channels).
|
||||
**DE-RISKED by review** (§7 step 3): all enum stages are type-table-driven with
|
||||
zero AST coupling, identity is handled by existing type-fn mangled-name memoization,
|
||||
and forward-declaration for self-ref already exists. Residual is *plumbing*
|
||||
(name reify-results by mangled name + input validation), not new architecture.
|
||||
3. **Deterministic-`Io` is the test keystone yet itself uncalibrated** — a buggy
|
||||
deterministic scheduler yields deterministic-*wrong* stdout that snapshots lock in.
|
||||
→ calibrate against the blocking `Io` / property-test fixed order (§10).
|
||||
4. **`context`-fiber-local + errno discipline** (A5 M:N). "Non-negotiable" but
|
||||
enforced by manual rule, not the compiler; M:1 can't even exercise migration.
|
||||
5. **S2 ORC shim** (deferred, but highest-risk when reached): only C++ in the tree,
|
||||
**already failed a spike** (`_Thread_local` SIGABRT), `MachOPlatform` is
|
||||
macOS-specific — **Linux/Windows JIT-resident reload + non-Mac TLS/ctor JIT have no
|
||||
named plan**. One "M" box hides a per-OS effort.
|
||||
6. **C1 args-buffer layout-vs-ABI** — "LLVM emits the call" covers the *call*, not the
|
||||
interpreter's *buffer pack* from `type_info`. Disagreement on edge layouts
|
||||
(over-aligned/empty structs, aarch64 small-struct register splitting, `bool`) =
|
||||
silent comptime corruption. → adversarial layout cases (§10).
|
||||
|
||||
---
|
||||
|
||||
## 9. Decisions log (all resolved)
|
||||
|
||||
**Sequencing — locked:** **async-first** (§7). The async cluster (steps 1–14)
|
||||
precedes the FFI/JIT cluster (15–18) because async needs no JIT spine. **Cancellation
|
||||
(A6) = model (a)** — a `.canceled` variant in the **existing `!` error channel** that
|
||||
`io.*` already returns (I/O is inherently fallible, so `io.*` is already `!`-typed —
|
||||
the "keep calls clean" argument for the non-local-`raise` model is moot). Reuses
|
||||
`!`/`try`/`catch`/`onfail`; no new unwind primitive. **Net-new prereq surfaced by
|
||||
grounding:** `callconv(.naked)` (only `.default`/`.c` today). **Generic enums dropped**
|
||||
— `RecvResult($T)`/`TryResult($T)` are **type-fns over `reify`** (type-fns already work
|
||||
in type position, e.g. `Make`/`Complex`), so no `enum($T)` feature is needed; `reify`
|
||||
gains two contracts (deterministic identity + functional-enum output, §7 step 3).
|
||||
|
||||
**Locked (see §4.6 for the grounded surface):**
|
||||
- **N1 atomics surface = generic wrapper `Atomic($T)`** + `Ordering` enum, `.init`,
|
||||
`compare_exchange`/`_weak` returning `?T` (**null = success** — pinned, opposite of
|
||||
most priors). (Not `@atomic_*` builtins — `@` is address-of in sx.) **RMW set** =
|
||||
`add/sub/and/or/xor/swap` + `fetch_min`/`fetch_max` (free from LLVM); **no `nand`**.
|
||||
- **`race` = over futures** (Zig model), **single named-tuple in** (`race((a: fa, b:
|
||||
fb))`) → synthesized tagged-union out; Go-style handler-map + map literal
|
||||
**dropped**. **No `async` spawn-sugar** — always `context.io.async(...)`.
|
||||
- **Channels** = `send`/`recv` methods (no `<-`); **`recv` returns a tagged union**
|
||||
`RecvResult($T){ value; closed }` (not `(v, ok)`), `try_recv` → `{ value; empty;
|
||||
closed }`; optional `for ch (v) {…}` iteration sugar. **locks** = `lock()` + `defer
|
||||
unlock()` (no guard sugar). `race`/`async`/`await` stay library, not keywords.
|
||||
- **Comptime type metaprogramming = `type_info` + `reify` builtins only** (Zig
|
||||
`@typeInfo`/`@Type` model). **Everything else is sx lib** — `make_enum`,
|
||||
`field_type`, `RaceResult`. `reify` coverage starts at **enum/struct/tuple**, grows
|
||||
later. `Future($T)` exposes `Value :: T` so `Future(X)→X` is plain member access
|
||||
(no `type_arg` builtin).
|
||||
- **C1 FFI engine = LLVM as single ABI authority** — per-signature JIT calling-thunks
|
||||
via S1 (LLVM emits the ABI-correct call, same as runtime codegen); trampoline
|
||||
fast-path for trivial calls. **libffi/dyncall + hand-rolled-sx rejected** (2nd/3rd
|
||||
ABI impl; hand-rolled needs C3 for its own asm leaf anyway). Promotes **S1 to
|
||||
foundational** (shared by C1, C3).
|
||||
|
||||
**Scheduler (Decision 5) — locked:** **M:1 → N×(M:1) → M:N**, all **sx std-lib `Io`
|
||||
vtables** (compiler only provides N1 atomics + the A2 asm context-switch + `extern`
|
||||
syscalls). M:1 ships first (validates the colorblind stack, covers I/O-bound);
|
||||
N×(M:1) is the first parallel step; **M:N is last in sequence but committed — not
|
||||
deferred.** Data races under parallelism are expected and handled with atomics +
|
||||
fiber-aware sync — that *is* parallelism, not a wart; M:1's lock-freedom is just a
|
||||
property of the single-threaded case.
|
||||
|
||||
**Deferred, orthogonal additions (Decisions 6–7) — both addable later without
|
||||
revisiting anything locked:**
|
||||
- **C4 (Decision 6) — fully orthogonal; not built now.** Pure deferred optimization
|
||||
riding S1 (already present for C1/C3): JIT the bundler subgraph instead of
|
||||
interpreting it. Zero coupling — same bundler sx, same C1 FFI. Apply only if
|
||||
profiling ever shows the bundler's *own logic* is a hotspot (it's I/O-bound, so
|
||||
unlikely). Interp+C1 is the shipping bundler.
|
||||
- **Hot-reload (Decision 7) — deferred; mechanism additive.** Substrate ready: R1
|
||||
(dylib-swap) needs only shipped `export`; R2 (JIT-resident) needs S1 + the S2 ORC
|
||||
shim. **R1-vs-R2 chosen at pickup.** One coupling (a design constraint, not a
|
||||
decision change): you can't hot-swap code with **live suspended fibers** pointing
|
||||
into the old module — so the async runtime + long-lived fibers stay on the
|
||||
*persistent* side, only transient **leaf logic** is reloadable (or quiesce fibers
|
||||
before swap).
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing & gates
|
||||
|
||||
Inherits the project cadence (CLAUDE.md): `zig build && zig build test` after every
|
||||
step; **xfail-then-green or behavior-lock — no commit both adds a test AND makes it
|
||||
pass**; never regenerate snapshots while red; corpus = `examples/` + `issues/` with
|
||||
`.exit`/`.stdout`/`.stderr`/`.ir` snapshots. Per-*step* gates live in the eventual
|
||||
`PLAN-*` streams; this section is the design-level verification strategy that those
|
||||
streams must implement.
|
||||
|
||||
### 10.1 The async test harness = the deterministic-simulation `Io` (the keystone)
|
||||
|
||||
Concurrency is nondeterministic (scheduling/readiness order), which **breaks snapshot
|
||||
testing** outright. So the **deterministic-sim `Io`** (fixed clock, scripted
|
||||
readiness, deterministic single-stepping scheduler) is not merely a feature — it is
|
||||
**the test harness for everything async**. Every concurrency example runs under it →
|
||||
reproducible stdout → snapshottable. Consequence for sequencing: **build the
|
||||
deterministic `Io` right after the blocking `Io`** (it's the simplest scheduler after
|
||||
blocking and it *gates the ability to test* fibers/channels/race/schedulers at all).
|
||||
The 10 patterns in §4.6-adjacent examples become corpus tests only because they run
|
||||
under it.
|
||||
|
||||
### 10.2 What is NOT snapshot-testable
|
||||
|
||||
True parallel **data races** (N×M:1 / M:N) are nondeterministic by construction. They
|
||||
run under the deterministic `Io` for *correctness* repro, but race-detection needs a
|
||||
separate **stress harness** (run-N-times / TSan-style), **not** the corpus. Any such
|
||||
coverage bound must be stated loudly (a `log()`-style note in the harness), never
|
||||
silently skipped — per the REJECTED-PATTERNS rule against silent gaps.
|
||||
|
||||
### 10.3 Arch-sensitive lowering — atomics + context-switch
|
||||
|
||||
Atomic orderings lower differently per arch (x86 `lock`-prefix / plain MOV vs aarch64
|
||||
LL/SC / `ldar`/`stlr`), and the A2 context-switch is per-arch asm. Lock both with the
|
||||
**existing inline-asm cross-arch sibling pattern**: a `.build` `{"target": "…"}`
|
||||
sidecar runs **ir-only** on a non-matching host (asserts `.ir` + `.exit` + `.stderr`
|
||||
from `sx ir --target`) and **end-to-end** on a matching CI runner. So `Atomic`
|
||||
lowering carries **x86_64 + aarch64 `.ir`** snapshots; the context-switch gets
|
||||
per-arch run tests on matching runners.
|
||||
|
||||
### 10.4 New corpus categories
|
||||
|
||||
`17xx` atomics · `18xx` concurrency (fibers/channels/race/async, all under the
|
||||
deterministic `Io`). Comptime metaprogramming (`type_info`/`reify`) + comptime-asm
|
||||
extend `06xx`; C1 FFI extends `12xx`; the cross-arch comptime-asm **loud bail** and
|
||||
the cancellation diagnostics are `11xx`.
|
||||
|
||||
### 10.5 Per-piece gates (design level)
|
||||
|
||||
| Piece | Locks via |
|
||||
|---|---|
|
||||
| **N1 atomics** | unit `emit_llvm.test.zig` (LLVM `atomicrmw`/`cmpxchg`/`fence` + ordering emission); corpus `17xx` single-thread (deterministic); arch-gated `.ir` (x86_64 + aarch64) |
|
||||
| **type_info / reify** | unit (reflect round-trips; reify'd enum has correct layout/match codegen); corpus `06xx` comptime (deterministic) |
|
||||
| **C1 FFI** | **behavior-lock** existing trampoline cases first; then xfail→green `12xx` comptime extern with floats / structs-by-value / aggregate (`{ptr,len}`) returns; unit for thunk-synth + args-buffer marshal |
|
||||
| **S1 spine** | infra — exercised transitively via C1/C3 examples; unit for LLJIT lifecycle + thunk cache |
|
||||
| **C3 comptime asm** | corpus `06xx` host-arch `#run` asm computes a value; `11xx` diagnostic asserts the cross-arch loud bail |
|
||||
| **A1/A2 fibers** | unit (scheduler step, fiber bootstrap); context-switch arch-gated run tests; corpus `18xx` under deterministic `Io` |
|
||||
| **A3/A5 schedulers, channels, race, cancel** | corpus `18xx` (the 10 patterns) under deterministic `Io` → deterministic snapshots; cancellation cleanup (`onfail`/`defer`) asserted via stdout ordering |
|
||||
|
||||
### 10.6 Cadence example (atomics, N1)
|
||||
|
||||
1. **xfail** — add `examples/17xx-atomics-fetch-add.sx` using `Atomic(i64).fetch_add`; seed the `.exit` marker → **red** (codegen missing). *(test added, not yet passing)*
|
||||
2. **green** — emit LLVM `atomicrmw add` + ordering; example passes; capture `.stdout` + x86_64/aarch64 `.ir` snapshots; review the diff. *(makes it pass, no new test)*
|
||||
|
||||
This satisfies "no commit both adds a test and makes it pass," and every other piece
|
||||
follows the same xfail→green (or behavior-lock→extend) shape.
|
||||
|
||||
### 10.7 Review-surfaced gaps (the high-corruption-risk pieces need *correctness*, not existence, tests)
|
||||
|
||||
The §10.5 gates prove things *run*; the §8.1 risks are silent-corruption modes a
|
||||
run/snapshot test won't catch. Each needs an explicit adversarial gate:
|
||||
|
||||
- **A2 context-switch — switch-stress test.** Scribble *every* callee-saved register
|
||||
+ a stack-canary before suspend; deep/recursive fiber chains; verify all survive
|
||||
post-resume. Run/snapshot tests don't prove register preservation. (The single
|
||||
highest-corruption-risk piece, §8.1.1.)
|
||||
- **Deterministic-`Io` — calibrate the oracle.** Cross-check a handful of cases
|
||||
against the blocking `Io` and property-test that scheduling order is actually fixed,
|
||||
*before* trusting it to gate everything async (a deterministic-but-wrong scheduler
|
||||
snapshots garbage).
|
||||
- **`context`-fiber-local invariant — named test at the N×M:1/M:N step.** M:1 can't
|
||||
exercise migration; add a test that forces a fiber to migrate and asserts it reads
|
||||
*its* `context`/`errno`, not the new thread's.
|
||||
- **N1 ordering *semantics* are out of snapshot scope — state it loudly.** `.ir`
|
||||
snapshots prove the *keyword emitted*, not weak-memory correctness (e.g. `relaxed`
|
||||
where `acquire` was needed ships green). Declare this out-of-scope parallel to
|
||||
§10.2's race carve-out; lock-free structures need the stress harness.
|
||||
- **C1 args-buffer — adversarial layout cases.** Over-aligned structs, empty structs,
|
||||
aarch64 small-struct register splitting, `bool` — a wrong layout that happens to
|
||||
print right passes a stdout test. Call these out explicitly, not just
|
||||
"structs-by-value."
|
||||
- **S2 — has no gate today despite a prior spike failure.** When reached, add a TLS +
|
||||
C-constructor JIT test (the exact `_Thread_local` SIGABRT case), per host OS.
|
||||
- **Hot-reload — no row today.** When picked up: state-survival test + the
|
||||
live-suspended-fiber-into-stale-module hazard (R1/R2).
|
||||
1031
design/inline-asm-design.md
Normal file
1031
design/inline-asm-design.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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."
|
||||
|
||||
437
docs/inline-assembly.md
Normal file
437
docs/inline-assembly.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# Inline Assembly in sx
|
||||
|
||||
A guide to writing inline assembly in sx — emitting raw target
|
||||
instructions, wiring values in and out, writing through memory, and
|
||||
defining whole routines in assembly.
|
||||
|
||||
> Looking for the *why* behind the design (how it maps to LLVM, the
|
||||
> Zig comparison, the emit algorithm)? That lives in
|
||||
> [inline-asm-design.md](../design/inline-asm-design.md). This page is the
|
||||
> user-facing how-to.
|
||||
|
||||
---
|
||||
|
||||
## The mental model
|
||||
|
||||
`asm` is an **expression**. It drops to the machine: you write a
|
||||
template of real instructions, declare which sx values feed registers
|
||||
going in and which come back out, and the block evaluates to the
|
||||
output value (or a tuple of them).
|
||||
|
||||
```sx
|
||||
add :: (a: i64, b: i64) -> i64 {
|
||||
return asm { "add %[out], %[a], %[b]", [out] "=r" -> i64, [a] "r" = a, [b] "r" = b };
|
||||
}
|
||||
```
|
||||
|
||||
Three things to know up front:
|
||||
|
||||
1. **The body is a brace block of comma-separated parts:** the template
|
||||
string first, then operands, then an optional `clobbers(.…)` clause.
|
||||
2. **Each operand is tagged by role**, not by position: `-> Type` is a
|
||||
value output, `= expr` is an input, `-> @place` writes through to
|
||||
existing storage. The list is flat and order-independent — there are
|
||||
no positional `:` sections.
|
||||
3. **The outputs decide the result.** Zero outputs → `void` (and the
|
||||
block must be `volatile`); one → that type; many → a tuple.
|
||||
|
||||
Templates are **AT&T syntax** (lowered through LLVM), **target-specific**,
|
||||
and **never run at compile time** — see [When it runs](#when-it-runs).
|
||||
|
||||
---
|
||||
|
||||
## Operands
|
||||
|
||||
An operand is `[name]? "constraint" <role>`. The constraint string is
|
||||
the LLVM/GCC-style constraint; the role marker says what the operand
|
||||
does.
|
||||
|
||||
### Inputs — `= expr`
|
||||
|
||||
`= expr` feeds a value in. The constraint picks where it lands:
|
||||
|
||||
```sx
|
||||
[a] "r" = a // any general register
|
||||
"{rdi}" = fd // pinned to a specific register (x86_64 rdi)
|
||||
```
|
||||
|
||||
### Symbol inputs — `"s" = fn`
|
||||
|
||||
A `"s"` input feeds a **function or global symbol** (not a runtime value).
|
||||
In the template, `%[name]` expands to the symbol's **platform-mangled
|
||||
name**, so you can branch or call straight to it:
|
||||
|
||||
```sx
|
||||
cb :: (n: i64) -> i64 export "cb" { return n + 1; }
|
||||
|
||||
trampoline :: (n: i64) -> i64 {
|
||||
return asm volatile {
|
||||
#string ASM
|
||||
mov x0, %[arg]
|
||||
bl %[fn] // DIRECT call — `bl _cb` on macOS, `bl cb` on Linux
|
||||
mov %[res], x0
|
||||
ASM,
|
||||
[res] "=r" -> i64,
|
||||
[arg] "r" = n,
|
||||
[fn] "s" = cb, // symbol operand
|
||||
clobbers(.x0, .x30, .memory),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The same `%[fn]` works on **x86_64** — just the branch mnemonic differs:
|
||||
|
||||
```sx
|
||||
return asm volatile {
|
||||
"call %[fn]", // x86_64 — same portable %[fn]
|
||||
[ret] "={rax}" -> i64,
|
||||
"{rdi}" = n,
|
||||
[fn] "s" = cb,
|
||||
clobbers(.rcx, .rdx, .rsi, .r8, .r9, .r10, .r11, .memory),
|
||||
};
|
||||
```
|
||||
|
||||
Two reasons to prefer this over passing a function *pointer* in a plain
|
||||
`"r"` register and using an indirect `blr`/`call *`:
|
||||
|
||||
- **One fewer indirection** — a direct PC-relative branch, no pointer
|
||||
load into a register, and a predictable (non-indirect) branch.
|
||||
- **Portable** — `%[fn]` is the same on every target; the backend emits
|
||||
the correctly-mangled name, so you never hardcode the macOS leading
|
||||
underscore *or* a per-arch operand modifier.
|
||||
|
||||
**How the portability works.** A bare `%[fn]` would render differently
|
||||
per target — on x86 the symbol prints as `$cb` (an immediate `$`-prefix
|
||||
that `call` rejects), while aarch64 prints it bare. So for a symbol (`"s"`)
|
||||
operand the compiler **auto-injects LLVM's `:c` operand modifier** (`%[fn]`
|
||||
→ `${N:c}`, "print the constant with no punctuation"). `:c` prints the
|
||||
plain symbol on every target — equivalent to the GCC `:P`/`%P0` call-target
|
||||
idiom on x86 (both emit the same `R_X86_64_PLT32` relocation) and a no-op
|
||||
on aarch64. You can still override it with an explicit `%[fn:X]` if you
|
||||
ever need a different rendering, but for a call/branch you never should.
|
||||
|
||||
The callee needs a stable, externally-linked symbol — i.e. `export`
|
||||
(which also gives it the C ABI). A plain or `callconv(.c)`-only function
|
||||
is `internal` and gets dead-code-eliminated, so the symbol won't link.
|
||||
(A global-scope `asm { … }` routine has no operand list, so it can't use
|
||||
a symbol operand — it references the literal symbol in its text.)
|
||||
|
||||
### Value outputs — `-> Type`
|
||||
|
||||
`-> Type` produces a value that becomes (part of) the block's result:
|
||||
|
||||
```sx
|
||||
[out] "=r" -> i64 // result in any register
|
||||
"={rax}" -> i64 // result pinned to rax
|
||||
```
|
||||
|
||||
### Naming and `%[name]`
|
||||
|
||||
Inside the template, `%[name]` refers to an operand by its **effective
|
||||
name**. An operand pinned to a register is **auto-named after that
|
||||
register** — `"{rdi}"` is reachable as `%[rdi]`, `"={rax}"` as `%[rax]`
|
||||
— so an explicit `[name]` is only needed:
|
||||
|
||||
- for a register-**class** operand (`"=r"`, `"r"`), which has no register
|
||||
to name it; or
|
||||
- to give a pinned operand a name *different* from its register.
|
||||
|
||||
Two labels are rejected so names stay unambiguous:
|
||||
|
||||
- the **echo form** `[rax] "={rax}"` — the label just repeats the pin, so
|
||||
drop it (the operand is already `%[rax]`); and
|
||||
- **duplicate** operand names.
|
||||
|
||||
In the template, `%%` is a literal `%`, and `%=` expands to a unique id
|
||||
(handy for a local label that must differ across inlinings).
|
||||
|
||||
### The result type
|
||||
|
||||
The number of **value** outputs (`-> Type`) decides the block's type:
|
||||
|
||||
| `-> Type` outputs | result | example |
|
||||
|---|---|---|
|
||||
| 0 | `void` — must be `volatile` | `asm volatile { "dmb ish" }` |
|
||||
| 1 | that type `T` | `x := asm { …, "=r" -> i64 }` |
|
||||
| N | a **tuple**, fields named by each operand's name | `lo, hi := asm { … }` |
|
||||
|
||||
With multiple outputs you get real multiple return values — a named
|
||||
operand becomes a named tuple field:
|
||||
|
||||
```sx
|
||||
// aarch64 — split a value into low/high bytes
|
||||
split :: (x: u64) -> (lo: u64, hi: u64) {
|
||||
return asm {
|
||||
#string ASM
|
||||
and %[l], %[x], #0xff
|
||||
lsr %[h], %[x], #8
|
||||
ASM,
|
||||
[l] "=r" -> u64, // → .lo (operand 0)
|
||||
[h] "=r" -> u64, // → .hi (operand 1)
|
||||
[x] "r" = x,
|
||||
};
|
||||
}
|
||||
lo, hi := split(0x1234); // (0x34, 0x12) = (52, 18)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `volatile`
|
||||
|
||||
`asm volatile { … }` marks the block as having side effects, so the
|
||||
optimizer won't move or delete it. It is **required whenever there are
|
||||
no value outputs** — a result-less, non-volatile asm would be dead code.
|
||||
|
||||
```sx
|
||||
barrier :: () { asm volatile { "dmb ish" }; } // aarch64 full barrier
|
||||
```
|
||||
|
||||
A block with outputs may still be `volatile` when its effects matter
|
||||
beyond the returned value (e.g. a syscall).
|
||||
|
||||
---
|
||||
|
||||
## `clobbers(.…)`
|
||||
|
||||
`clobbers(.…)` is a dot-name list of registers and flags the asm trashes
|
||||
that aren't already operands — so the register allocator keeps clear of
|
||||
them:
|
||||
|
||||
```sx
|
||||
clobbers(.rcx, .r11, .memory) // x86_64 syscall trashes rcx, r11, and memory
|
||||
clobbers(.cc) // condition flags
|
||||
```
|
||||
|
||||
`.memory` means "this asm reads or writes memory the compiler can't see,"
|
||||
and `.cc` means "the condition flags are modified."
|
||||
|
||||
---
|
||||
|
||||
## Writing through memory — `-> @place`
|
||||
|
||||
Sometimes the asm should write into existing storage (a local, a struct
|
||||
field) rather than *return* a value. `-> @place` does that: the place
|
||||
output does **not** join the result tuple. There are three forms,
|
||||
distinguished by the constraint.
|
||||
|
||||
### Write-through — `= …` constraint
|
||||
|
||||
The asm computes a value into a register; sx stores it through the
|
||||
place's address afterward.
|
||||
|
||||
```sx
|
||||
compute :: () -> i64 {
|
||||
other : i64 = 0;
|
||||
main_val := asm volatile {
|
||||
#string ASM
|
||||
mov %[m], #5
|
||||
mov %[o], #37
|
||||
ASM,
|
||||
[m] "=r" -> i64, // value output → returned into main_val
|
||||
[o] "=r" -> @other, // place output → stored through @other
|
||||
};
|
||||
return main_val + other; // 5 + 37 = 42
|
||||
}
|
||||
```
|
||||
|
||||
A value output and one or more place outputs can mix freely; only the
|
||||
value outputs build the returned tuple.
|
||||
|
||||
### Read-write — `+` constraint
|
||||
|
||||
A `+` operand is read **and** written: the place's current value is fed
|
||||
in, the asm updates it in place, and the result is stored back.
|
||||
|
||||
```sx
|
||||
// increment-in-place: x is loaded, the asm adds 1, the result is stored back
|
||||
bump :: () -> i64 {
|
||||
x : i64 = 41;
|
||||
asm volatile { "add %[v], %[v], #1", [v] "+r" -> @x };
|
||||
return x; // 42
|
||||
}
|
||||
```
|
||||
|
||||
### Indirect memory — `=*m` constraint
|
||||
|
||||
An `=*m` operand passes the place's **address** to the asm, which writes
|
||||
through it directly (no register round-trip, no return slot):
|
||||
|
||||
```sx
|
||||
// store 42 straight into x's storage
|
||||
poke :: () -> i64 {
|
||||
x : i64 = 0;
|
||||
asm volatile {
|
||||
#string ASM
|
||||
mov x9, #42
|
||||
str x9, %[out]
|
||||
ASM,
|
||||
[out] "=*m" -> @x,
|
||||
clobbers(.x9),
|
||||
};
|
||||
return x; // 42
|
||||
}
|
||||
```
|
||||
|
||||
**The place must be mutable storage.** Taking the address of a scalar
|
||||
`::` constant has no meaning — a scalar constant folds to its value and
|
||||
has no storage — so `-> @SOME_CONST` is a compile error:
|
||||
|
||||
```
|
||||
cannot take the address of constant 'SOME_CONST' — a scalar '::'
|
||||
constant has no storage (use a '=' variable or a local copy)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-instruction templates
|
||||
|
||||
A single `"…"` string is one fragment. For several instructions, use a
|
||||
multi-line string literal or sx's **`#string` heredoc**, which is
|
||||
delivered **verbatim** — no escape processing — so you write assembly
|
||||
exactly as it should appear:
|
||||
|
||||
```sx
|
||||
serialize :: () {
|
||||
asm volatile {
|
||||
#string ASM
|
||||
mfence
|
||||
lfence
|
||||
ASM,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Global (module-scope) assembly
|
||||
|
||||
A top-level `asm { … }` block is **global assembly** — template only
|
||||
(no operands, no `volatile`), emitted as module-level assembly. It is
|
||||
the place to define a whole routine in assembly. Symbols it defines are
|
||||
reached from sx with a **lib-less `extern`** declaration:
|
||||
|
||||
```sx
|
||||
asm {
|
||||
#string ASM
|
||||
.global _my_add
|
||||
_my_add:
|
||||
add x0, x0, x1
|
||||
ret
|
||||
ASM,
|
||||
};
|
||||
|
||||
my_add :: (a: i64, b: i64) -> i64 extern;
|
||||
|
||||
main :: () -> i64 {
|
||||
return my_add(40, 2); // 42 — computed by the global-asm routine
|
||||
}
|
||||
```
|
||||
|
||||
Multiple global blocks concatenate in source order. (Symbol naming
|
||||
follows the platform convention — a leading underscore on macOS, none
|
||||
on Linux.)
|
||||
|
||||
---
|
||||
|
||||
## When it runs
|
||||
|
||||
Inline assembly is emitted into the program and runs at **runtime**,
|
||||
under both execution paths:
|
||||
|
||||
- **`sx run` (JIT)** — the module is compiled to an in-memory object
|
||||
(the integrated assembler assembles your asm, including global blocks),
|
||||
then run. Both inline and global asm work.
|
||||
- **`sx build` (AOT)** — same, into a native binary.
|
||||
|
||||
It does **not** run at **compile time**. A `#run` (comptime) call into a
|
||||
global-asm symbol fails loudly:
|
||||
|
||||
```sx
|
||||
COMPUTED :: #run my_add(40, 2); // error: the symbol isn't linked yet at comptime
|
||||
```
|
||||
|
||||
```
|
||||
comptime extern call: symbol not found via dlsym
|
||||
```
|
||||
|
||||
The comptime interpreter resolves `extern` calls against the host
|
||||
process; a module-asm symbol only exists once the program is
|
||||
assembled and linked, so call it at runtime, not in a `#run`.
|
||||
|
||||
---
|
||||
|
||||
## Cookbook
|
||||
|
||||
**Read a register** (no inputs):
|
||||
|
||||
```sx
|
||||
stack_ptr :: () -> u64 {
|
||||
return asm { "mov %[out], sp", [out] "=r" -> u64 }; // aarch64
|
||||
}
|
||||
```
|
||||
|
||||
**x86_64 syscall** — `write(2)`, with pinned registers and clobbers:
|
||||
|
||||
```sx
|
||||
sys_write :: (fd: i64, buf: *u8, count: i64) -> i64 {
|
||||
return asm volatile {
|
||||
"syscall",
|
||||
[ret] "={rax}" -> i64, // bytes written, in rax
|
||||
"{rax}" = 1, // SYS_write
|
||||
"{rdi}" = fd,
|
||||
"{rsi}" = buf,
|
||||
"{rdx}" = count,
|
||||
clobbers(.rcx, .r11, .memory),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**x86_64 divmod** — one instruction, two outputs, returned as a tuple:
|
||||
|
||||
```sx
|
||||
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),
|
||||
};
|
||||
}
|
||||
q, r := divmod(17, 5); // (3, 2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules of thumb
|
||||
|
||||
- **`asm` yields a value.** Bind it (`x := asm { … }`), `return` it, or
|
||||
destructure a multi-output tuple (`a, b := asm { … }`). A block with no
|
||||
value outputs must be `volatile`.
|
||||
- **Pinned operands name themselves.** `"{rdi}"` is `%[rdi]`; only add
|
||||
`[name]` for register-class operands or to rename. Don't echo a pin
|
||||
(`[rax] "={rax}"`).
|
||||
- **`%%` for a literal percent; `%[name]` for an operand.** Templates are
|
||||
AT&T.
|
||||
- **List everything you trash** in `clobbers(.…)` — scratch registers,
|
||||
`.cc`, and `.memory` if the asm touches memory the compiler can't see.
|
||||
- **`-> @place` writes storage; pick the form:** `=` (compute then
|
||||
store), `+` (read-modify-write), `=*m` (write through the address).
|
||||
The place must be mutable — not a scalar `::` constant.
|
||||
- **Global `asm { … }`** defines symbols; import them with a lib-less
|
||||
`extern`. They run under JIT and AOT, but **not** in a `#run`.
|
||||
- **It's target-specific.** Gate or pick instructions per architecture;
|
||||
there is no portable instruction set.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [inline-asm-design.md](../design/inline-asm-design.md) — the design rationale and
|
||||
LLVM mapping.
|
||||
- `examples/16xx-platform-asm-*` — the full, runnable example matrix
|
||||
(basic in/out, tuples, the three `-> @place` forms, global asm, the
|
||||
x86_64 syscall, and the comptime-boundary guard).
|
||||
- The "Inline Assembly" section of [readme.md](../readme.md) for a
|
||||
one-screen overview.
|
||||
```
|
||||
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.
|
||||
|
||||
22
examples/0184-types-union-member-struct-literal-assign.sx
Normal file
22
examples/0184-types-union-member-struct-literal-assign.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// Assigning a struct LITERAL to a named-struct member of a plain `union`.
|
||||
// `u.b = .{ code = 9 }` types the literal as the union member's struct type
|
||||
// `S` and stores it — the target type propagates to a union-member lvalue
|
||||
// exactly as it does to a struct field.
|
||||
//
|
||||
// Regression (issue 0133): the literal used to lower as `.unresolved` (the
|
||||
// target-type path only inspected struct fields, not union members) and trip
|
||||
// the LLVM-emission tripwire in emitStructInit.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
S :: struct { code: i64; }
|
||||
U :: union { a: i64; b: S; }
|
||||
|
||||
main :: () {
|
||||
u : U = ---;
|
||||
u.b = .{ code = 9 }; // union member <- struct literal
|
||||
print("code={}\n", u.b.code); // 9
|
||||
|
||||
u.a = 5; // scalar member still works
|
||||
print("a={}\n", u.a); // 5
|
||||
}
|
||||
20
examples/0185-types-tagged-union-member-assign-rejected.sx
Normal file
20
examples/0185-types-tagged-union-member-assign-rejected.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// A direct write to a tagged-union (enum-with-payload) variant member is
|
||||
// rejected: a tagged union is laid out `{ tag, payload }`, and a member write
|
||||
// would set the payload but leave the tag stale. The variant is set via
|
||||
// construction (`s = .rect(...)`), which writes both tag and payload.
|
||||
//
|
||||
// Regression (issue 0136): `s.rect = .{...}` used to silently store the payload
|
||||
// only, desyncing the tag so a later `match` took the wrong arm. It now errors.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Shape :: enum {
|
||||
circle: f32;
|
||||
rect: struct { w, h: f32; };
|
||||
}
|
||||
|
||||
main :: () {
|
||||
s : Shape = .circle(1.0);
|
||||
s.rect = .{ w = 4.0, h = 2.0 }; // rejected — use `s = .rect(.{...})` instead
|
||||
print("unreachable: {}\n", s.rect.w);
|
||||
}
|
||||
22
examples/0186-types-tagged-union-nested-field-write.sx
Normal file
22
examples/0186-types-tagged-union-nested-field-write.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
// A write to a sub-field of a tagged-union variant's payload (`s.rect.w = ...`)
|
||||
// is NOT rejected: the immediate object is the payload struct, so it mutates a
|
||||
// field of the already-active variant in place and leaves the tag alone. This
|
||||
// pins the scope of issue 0136's guard — only a WHOLE-variant member write
|
||||
// (`s.rect = ...`) is rejected; nested sub-field writes keep working.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Shape :: enum {
|
||||
circle: f32;
|
||||
rect: struct { w, h: f32; };
|
||||
}
|
||||
|
||||
main :: () {
|
||||
s : Shape = .rect(.{ w = 1.0, h = 2.0 });
|
||||
s.rect.w = 9.0; // nested sub-field write — allowed
|
||||
r := s.rect;
|
||||
print("w={} h={}\n", r.w, r.h); // 9 2
|
||||
|
||||
s = .circle(3.5); // construction reassign — allowed
|
||||
print("c={}\n", s.circle); // 3.5
|
||||
}
|
||||
@@ -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) ---
|
||||
|
||||
|
||||
44
examples/0417-protocols-protocol-return-name-collision.sx
Normal file
44
examples/0417-protocols-protocol-return-name-collision.sx
Normal file
@@ -0,0 +1,44 @@
|
||||
// Protocol method signatures resolve their param/return type NAMES in the
|
||||
// protocol's OWN declaring module (own-wins visibility), so a bare type name
|
||||
// that collides with a same-name namespaced import binds to the local author.
|
||||
//
|
||||
// Here the user's `Event` enum shares its name with the stdlib
|
||||
// `std/event.sx` `Event :: struct` (pulled in, namespaced as `event`, by
|
||||
// `#import "modules/std.sx"`). `Plat.one_event` returns the user's `Event`;
|
||||
// `ev := g_plat.one_event()` infers that type, so the `case .key_up:(e)`
|
||||
// payload binds a `KeyData` and `.escape` resolves against `Keycode`.
|
||||
//
|
||||
// Regression (issue 0132): `registerProtocolDecl` used to resolve method
|
||||
// signature types through the flat, visibility-UNAWARE `type_bridge`
|
||||
// resolver, which picked the stdlib `event.Event` struct instead — typing
|
||||
// `ev` as a fieldless struct, binding `.unresolved`, and emitting
|
||||
// "enum literal '.escape' has no destination type to resolve against". The
|
||||
// fix pins resolution to `pd.source_file`, mirroring the parameterized-
|
||||
// protocol and concrete-fn signature paths.
|
||||
//
|
||||
// Expect: prints `escape!`, exit 0.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Keycode :: enum { unknown; escape; enter; }
|
||||
KeyData :: struct { key: Keycode; }
|
||||
Event :: enum { none; key_up: KeyData; }
|
||||
|
||||
Plat :: protocol { one_event :: () -> Event; }
|
||||
|
||||
Impl :: struct { dummy: i64; }
|
||||
impl Plat for Impl {
|
||||
one_event :: (self: *Impl) -> Event { return .key_up(.{ key = .escape }); }
|
||||
}
|
||||
|
||||
main :: () {
|
||||
impl : Impl = .{ dummy = 0 };
|
||||
g_plat : Plat = xx @impl;
|
||||
ev := g_plat.one_event(); // type INFERRED from protocol return
|
||||
if ev == {
|
||||
case .key_up: (e) {
|
||||
// `e` is KeyData (payload of the user's Event), `.escape` a Keycode
|
||||
if e.key == .escape { print("escape!\n"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
27
examples/0547-packs-xx-pack-index-to-protocol.sx
Normal file
27
examples/0547-packs-xx-pack-index-to-protocol.sx
Normal file
@@ -0,0 +1,27 @@
|
||||
// `xx <pack>[i]` erased to a protocol-typed local.
|
||||
//
|
||||
// Erasing a single comptime-pack element to a protocol scalar routes through
|
||||
// buildProtocolErasure. A pack index is a comptime rvalue (a pack has no
|
||||
// runtime storage — `sources[i]` resolves to the call-site arg, which only
|
||||
// gets storage when lowered as a value), so the erasure must heap-copy the
|
||||
// materialized element rather than take its address.
|
||||
//
|
||||
// Regression (issue 0135): `xx sources[0]` used to lower the bare pack as a
|
||||
// value and error with "pack 'sources' has no runtime value".
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
VL :: protocol(T: Type) { get :: () -> T; }
|
||||
IntCell :: struct { v: i64; }
|
||||
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
|
||||
|
||||
first :: (..sources: VL) -> i64 {
|
||||
x : VL(i64) = xx sources[0]; // erase element 0 to VL(i64)
|
||||
return x.get();
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
print("{}\n", first(IntCell.{ v = 7 })); // 7
|
||||
print("{}\n", first(IntCell.{ v = 42 }, IntCell.{ v = 99 })); // 42 (element 0)
|
||||
0
|
||||
}
|
||||
25
examples/0548-packs-xx-pack-index-two-elements.sx
Normal file
25
examples/0548-packs-xx-pack-index-two-elements.sx
Normal file
@@ -0,0 +1,25 @@
|
||||
// Erase two DISTINCT comptime-pack elements to protocol locals — each gets
|
||||
// its own heap copy and resolves to its OWN concrete type's method (IntCell.get
|
||||
// vs Doubler.get), proving the per-element erasure picks the right vtable.
|
||||
//
|
||||
// Regression (issue 0135): single-element `xx pack[i]` erasure to a protocol
|
||||
// scalar was unsupported (the bare pack lowered as a value and errored).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
VL :: protocol(T: Type) { get :: () -> T; }
|
||||
IntCell :: struct { v: i64; }
|
||||
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
|
||||
Doubler :: struct { n: i64; }
|
||||
impl VL(i64) for Doubler { get :: (self: *Doubler) -> i64 => self.n * 2; }
|
||||
|
||||
sum_two :: (..sources: VL) -> i64 {
|
||||
a : VL(i64) = xx sources[0]; // erase element 0
|
||||
b : VL(i64) = xx sources[1]; // erase element 1
|
||||
return a.get() + b.get();
|
||||
}
|
||||
|
||||
main :: () -> i32 {
|
||||
print("{}\n", sum_two(IntCell.{ v = 10 }, Doubler.{ n = 16 })); // 10 + (16*2) = 42
|
||||
0
|
||||
}
|
||||
@@ -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";
|
||||
29
examples/0781-modules-same-name-enum-payload-own-wins.sx
Normal file
29
examples/0781-modules-same-name-enum-payload-own-wins.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Own-wins for ENUM-PAYLOAD type registration over a NAMESPACED import.
|
||||
// Regression (issue 0132, broader class).
|
||||
//
|
||||
// `#import "modules/std.sx"` carries the stdlib `event.Event` struct — it is
|
||||
// NAMESPACED (reachable only as `event.Event`), never flat-visible. This file
|
||||
// ALSO authors its OWN `Event :: struct { code }`, used as the payload of the
|
||||
// enum `Wrap`. The payload type name `Event` must resolve at REGISTRATION to
|
||||
// THIS file's own `Event` (which has `code`), not the namespaced stdlib struct.
|
||||
//
|
||||
// Fail-before: `registerEnumDecl` built the tagged-union body through the
|
||||
// stateless `type_bridge.buildEnumInfo`, whose flat `findByName` picked the
|
||||
// wrong same-name author — `got`'s payload became the stdlib `Event`, so
|
||||
// `e.code` errored "field 'code' not found on type 'Event'". Fixed by threading
|
||||
// the visibility-aware resolver (`*Lowering` as the `resolveInner` hook) through
|
||||
// `buildEnumInfo` / `buildUnionInfo`, matching what `registerStructDecl` already
|
||||
// does for struct fields.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Event :: struct { code: i64; }
|
||||
Wrap :: enum { none; got: Event; }
|
||||
|
||||
main :: () {
|
||||
w : Wrap = .got(.{ code = 7 });
|
||||
if w == {
|
||||
case .got: (e) { print("code={}\n", e.code); }
|
||||
case .none: print("none\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Own-wins for an INLINE struct field's member type over a NAMESPACED import.
|
||||
// Regression (issue 0132's broader class — the inline-decl resolution boundary).
|
||||
//
|
||||
// `Holder.inner` is an inline `struct { e: Event }`. The member type `Event`
|
||||
// must resolve to THIS file's `Event` (which has `code`), not the namespaced
|
||||
// stdlib `event.Event` struct carried by `#import "modules/std.sx"` (reachable
|
||||
// only as `event.Event`, never bare).
|
||||
//
|
||||
// Fail-before: `Lowering.resolveTypeWithBindings` delegated inline `struct_decl`
|
||||
// field types to the FLAT `type_bridge.resolveAstType`, dropping the visibility
|
||||
// context — so `e: Event` resolved via global `findByName` to the stdlib struct
|
||||
// and `h.inner.e.code` errored "field 'code' not found on type 'Event'". Fixed
|
||||
// by routing inline enum/struct/union decls through the `inner` recursion hook
|
||||
// with `self` (visibility-aware), the same own-wins rule top-level decls use.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Event :: struct { code: i64; }
|
||||
Holder :: struct { inner: struct { e: Event; }; }
|
||||
|
||||
main :: () {
|
||||
h : Holder = ---;
|
||||
h.inner.e = .{ code = 5 };
|
||||
print("code={}\n", h.inner.e.code);
|
||||
}
|
||||
@@ -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 }
|
||||
13
examples/1176-diagnostics-import-parse-error-location.sx
Normal file
13
examples/1176-diagnostics-import-parse-error-location.sx
Normal file
@@ -0,0 +1,13 @@
|
||||
// A parse error in an IMPORTED file must be located in THAT file, not the
|
||||
// importer. Regression: `import_sources` was wired to the diagnostics only
|
||||
// AFTER import resolution finished, so a parse error raised MID-resolution
|
||||
// (which aborts before that wiring) could not resolve the imported file's
|
||||
// source — the caret fell back to the root file and landed on an unrelated
|
||||
// line. The fix wires `import_sources` before resolving and pins the
|
||||
// diagnostic's `source_file` + offset to the imported file.
|
||||
//
|
||||
// The companion's error sits several lines down (after comments) so a caret
|
||||
// mislocated against THIS importer would be unmistakable.
|
||||
#import "1176-diagnostics-import-parse-error-location/broken.sx";
|
||||
|
||||
main :: () {}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Deliberately broken: exercises import parse-error LOCATION reporting.
|
||||
// These leading comment lines push the parse error down so its line number
|
||||
// differs from the importer's — a mislocated caret would point here-or-wrong
|
||||
// instead of at the real offending token below.
|
||||
//
|
||||
broken :: 1 2;
|
||||
18
examples/1177-diagnostics-addr-of-const-rejected.sx
Normal file
18
examples/1177-diagnostics-addr-of-const-rejected.sx
Normal file
@@ -0,0 +1,18 @@
|
||||
// Taking the address of a scalar `::` constant is a compile error: a scalar
|
||||
// constant folds to its value and has NO storage (only array/struct constants
|
||||
// are immutable globals with a real address — see 0177). Covers a module-scope
|
||||
// const, a local const, and an inline-asm `-> @const` write-through (the path
|
||||
// that surfaced the bug). Before the fix, `@N` lowered to `inttoptr (i64 40 to
|
||||
// ptr)` — a wild pointer that segfaulted on deref and emitted invalid stores
|
||||
// for asm `-> @const`. Regression (issue 0138).
|
||||
|
||||
takes :: (p: *i64) {}
|
||||
|
||||
N :: 40;
|
||||
|
||||
main :: () {
|
||||
takes(@N); // module scalar const — no storage
|
||||
x :: 7;
|
||||
takes(@x); // local scalar const — no storage
|
||||
asm volatile { "mov %[c], #99", [c] "=r" -> @N }; // write-through to a const
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user