comptime compiler-API: Phase 1 foundation + Phase 2.1 weld plan

Introduce the welded comptime `compiler` library (`#library "compiler"` +
`abi(.zig) extern compiler`), per design/comptime-compiler-api.md, and unify
`callconv(...)` into the new `abi(...)` annotation.

abi(...) replaces callconv(...):
- New ABI enum { default, c, zig, pure }; `abi(.c|.zig|.pure)` parses in the
  postfix slot before extern/export (and standalone). `kw_callconv` -> `kw_abi`.
- Migrated 52 sx files, the call-convention-mismatch diagnostic, and docs
  (readme/specs) from `callconv(.c)` to `abi(.c)`.

Phase 1 — welded compiler library (parse -> registry -> validation -> bridge):
- `abi(.zig) extern compiler` parses on fn decls (carries abi/extern_lib) and
  struct decls (StructDecl.abi/extern_lib).
- `#library "compiler"` is the comptime-only internal surface — never dlopen'd.
- src/ir/compiler_lib.zig: the binding registry (the safety boundary). `Field`
  welded to StructInfo.Field with layout baked from the real Zig type
  (@offsetOf/@sizeOf); `findType`/`findFn`. Welded structs are layout-validated
  at registration (field set + total size) as a header checked against the impl.
- Host-call bridge: a `fn abi(.zig) extern compiler` dispatches under the
  comptime interp to its registered Zig handler (intern/text_of round-trip),
  never dlsym. IR Function.compiler_welded; validated in declareFunction.
- Comptime-only enforcement: a runtime call to a welded fn is a clean
  build-gating error (emitCall), not an undefined-symbol link failure.

Phase 2.1 — byte-layout weld foundation:
- Decision: full byte-layout weld (sx struct laid out byte-identically to the
  bound Zig type). Registered StructInfo (first non-natural / Zig-reordered
  layout). `computeWeldPlan` — pure offset-ordered element plan + padding +
  sx-field->LLVM-element remap; unit-tested. Emit/interp wiring is the next
  sub-step (2.2+, see current/CHECKPOINT-COMPILER-API.md).

Examples: 0625/0626 (welded struct + fn round-trip), 1183/1184/1185
(layout-mismatch, unexported-fn, runtime-call diagnostics).
This commit is contained in:
agra
2026-06-17 13:31:11 +03:00
parent 3a9b508502
commit cd5b958d19
100 changed files with 1490 additions and 298 deletions

View File

@@ -0,0 +1,273 @@
# CHECKPOINT-COMPILER-API — comptime `compiler` library (`#library "compiler"` + `abi(.zig) extern`)
Companion to the design-of-record
[../design/comptime-compiler-api.md](../design/comptime-compiler-api.md) (the plan
+ phased build order live there). This stream supersedes the metatype
`declare`/`define`/`type_info` `#builtin`s and the `#compiler` struct attribute
with ONE welded mechanism. Branch: `reify` (off `master`). Update after every step.
## Last completed step
**Phase 2, sub-step 1 — the weld-plan layout math + `StructInfo` registered.**
The de-risked core of the byte-layout-override ("GEP") engine, pure + unit-tested,
no emit/interp wiring yet (suite trivially green).
Decision (locked 2026-06-17): **full byte-layout weld** — a welded sx struct is
laid out byte-identically to the bound Zig type (Zig's `@offsetOf`, reordering +
padding included), so it passes to a Zig handler as raw memory with zero
marshalling. (The alternative — handlers reading interp `Value` aggregates
logically, no layout override — was rejected; welded types must also be usable as
runtime data, and the design wants the literal byte weld.)
- Measured: Zig reorders `StructInfo` to `fields`@0, `name`@16, `nominal_id`@20,
`is_protocol`@24, size 32 — vs sx-natural `name`@0, `fields`@8, … So the override
is genuinely required (`Field`'s two-u32 natural layout was the easy case).
- `compiler_lib.zig`: registered `StructInfo` (`weldStruct`, the second
`bound_types` entry). Added `WeldElement` / `WeldPlan` + `computeWeldPlan(alloc,
fields, total)` — pure: orders fields by ascending byte offset, inserts padding
elements for gaps + the alignment tail, and builds the sx-field → LLVM-element
remap. This is what the LLVM type builder + struct-GEP sites will consume.
- Unit-tested (`compiler_lib.test.zig`): `Field` → identity plan (2 elems, no pad);
`StructInfo` → 5 elems `[fields@0, name@16, nominal_id@20, is_protocol@24,
pad@25..32]`, remap `[1,0,3,2]`.
- `zig build` + `zig build test` green.
### Earlier — Phase 1 polish (comptime-only enforcement)
**A RUNTIME call to a `fn abi(.zig) extern compiler` is a clean build-gating error
instead of an undefined-symbol link failure.**
- `emitCall` (`src/backend/llvm/ops.zig`): when the callee is `compiler_welded`
AND the ENCLOSING function is not `is_comptime` (i.e. genuine runtime code, not a
`#run`/`::` initializer wrapper whose LLVM body is dead), print a clear
"comptime-only … cannot be called at runtime" error and set
`comptime_failed` (the driver halts before object/JIT emission). The enclosing
`is_comptime` guard is what keeps the legitimate `#run` use (example 0626) green.
- Corpus: `examples/1185-diagnostics-weld-fn-runtime-call.sx` (runtime `intern(…)`
→ clean error, exit 1, no link failure).
- `zig build` + `zig build test` green (458 unit + 690 corpus).
### Earlier — fifth sub-step (host-call bridge)
**A `fn abi(.zig) extern compiler` dispatches, under the comptime interpreter, to
its registered Zig handler instead of dlsym.**
- `compiler_lib.zig`: function registry — `BoundFn { sx_name, handler }`,
`bound_fns` = `intern(string)->StringId` + `text_of(StringId)->string` (the
string-pool round-trip), `findFn`, and `FnHandler` (`*Interpreter, []Value ->
Value`). `intern` mutates via `interp.mint orelse @constCast(&module.types)`
(the same mutable-table access the metatype mint path uses); `text_of` reads the
const pool. Imports `interp.zig` (the compiler_hooks↔interp cycle pattern).
- IR `Function` gained `compiler_welded: bool`. `declareFunction`
(`src/ir/lower/decl.zig`) sets it via `weldedCompilerFn`, which also VALIDATES:
the bound lib must be `compiler` and the name must be on the function-export
list — else a build-gating `.err` (no silent fall-through to dlsym).
- `interp.call()`: before the dlsym/extern path, a `compiler_welded` function
routes to `compiler_lib.findFn(name).handler(self, args)` (clean bail off the
export list).
- Corpus: `examples/0626-comptime-weld-fn-intern-text-of.sx` (`#run
text_of(intern("hello, compiler"))` folds to a string constant → prints it);
`examples/1184-diagnostics-weld-fn-unexported.sx` (unexported welded-fn name →
build error). `findFn` lookup unit-tested.
- **Runtime-call rejection is NOT yet clean** — welded fns are comptime-only; a
RUNTIME call would emit a reference to a non-existent extern symbol → a loud
LINK error (not silent, but not a tidy diagnostic). The examples call welded fns
only inside `#run`. A dedicated "comptime-only symbol" emit diagnostic is the
immediate follow-up.
- `zig build` + `zig build test` green (458 unit tests + 689 corpus).
### Earlier — fourth sub-step (welded-struct layout validation)
**A `struct abi(.zig) extern compiler { … }` is validated against the binding
registry as a *header checked against the implementation*.**
- `compiler_lib.zig`: `validateStructLayout(bt, sx_fields, total)` — pure, returns
the first `LayoutMismatch` (field count / name / size / total) or null. Plus
`lib_name = "compiler"` and `SxField`. Unit-tested (faithful `Field` passes;
each drift flagged as the right variant).
- `registerStructDecl` (`src/ir/lower/nominal.zig`): for `sd.abi == .zig`,
`validateWeldedStruct` checks the bound lib is `compiler`, the name is on the
export list (`findType`), and the sx layout (field names + `typeSizeBytes` +
total) matches the welded type — emitting a build-gating `.err` (good span into
the struct body) on any failure. No silent reinterpretation.
- `#library "compiler"` is the comptime-only internal surface, NOT a dylib —
`src/main.zig`'s dlopen walker skips it (was emitting a spurious `libcompiler.so`
load warning).
- Corpus: `examples/0625-comptime-weld-struct-field.sx` (faithful `Field` welds,
validates, usable as data → `name=7 ty=3`); `examples/1183-diagnostics-weld-
struct-field-count.sx` (one-field `Field` → build-gating field-count diagnostic).
- **Offset-override / GEP emission for non-natural Zig layouts is NOT here** — it
isn't exercised by `Field` (two u32s = natural layout coincides with the weld).
It arrives with `StructInfo` in Phase 2 (slices/reordering), where the bound
offsets actually differ from the sx-natural ones. The validation already checks
per-field size + total, so a layout drift is caught even before the override
engine exists.
- `zig build` + `zig build test` green (456 unit tests + 687 corpus).
### Earlier — third sub-step (binding registry)
**The binding registry (welded-type lookup, layout baked from the real Zig
type).**
- New `src/ir/compiler_lib.zig` — the `compiler` library's binding registry, the
curated safety boundary. `BoundType { sx_name, size, alignment, fields:
[]FieldLayout{name, offset, size} }`; `weldStruct` bakes the layout from a real
Zig struct via `@sizeOf`/`@alignOf`/`@offsetOf` at compiler-build time (a
sx-field-count mismatch is a `@compileError`, never a silent truncation).
`bound_types` exports `Field` (welded to `types.TypeInfo.StructInfo.Field` —
two `u32`s); `findType(sx_name) ?*const BoundType` is the lookup the welded-decl
resolution path will consult (returns null off the export list — clean boundary,
no silent default).
- Registered in the barrel (`src/ir/ir.zig`): `compiler_lib` + `compiler_lib_tests`.
- Tests (`src/ir/compiler_lib.test.zig`): `findType("Field")` equals the real
`StructInfo.Field` `@sizeOf`/`@alignOf`/`@offsetOf` (8 bytes, two u32s at 0/4);
an unexported name returns null. Break-verified (a wrong size → suite red,
named `ir.compiler_lib.test...`).
- `zig build` + `zig build test` green (454 unit tests).
### Earlier — second sub-step (struct-decl parse)
**`abi(.zig) extern <lib>` PARSES on a STRUCT decl (parse-only, no semantics).**
- `ast.StructDecl` gained `abi: ABI` + `extern_lib: ?[]const u8` binding fields.
- `parseStructDecl` (`src/parser.zig`): after `struct` (and the `#compiler`
check), parse an optional `abi(...)` then optional `extern <lib>` — same slot
order as fn decls — and thread them onto the node. Ordinary structs are
unperturbed (`parseOptionalAbi`/`parseOptionalExternExport` no-op when absent).
- Parser unit tests (`src/parser.test.zig`): `Field :: struct abi(.zig) extern
compiler { name: StringId; ty: Type; }` parses with `abi == .zig`, `extern_lib
== "compiler"`, field list intact; a plain struct leaves `abi == .default` /
`extern_lib == null`. Break-verified (a wrong-sentinel assert turns the suite
red, confirming the test runs).
- `zig build` + `zig build test` green.
### Earlier — first sub-step (fn decls) + the syntax pivot
**`abi(.zig) extern <lib>` PARSES on a fn decl (parse-only).** Plus the syntax
pivot it required.
Syntax decision (locked 2026-06-17, supersedes the doc's original
`extern(.zig) <lib>` single-qualifier form): the ABI/layout selector and the
linkage keyword are two orthogonal annotations.
- `abi(.x)` — ABI / calling-convention annotation in the slot **before**
`extern`/`export`. **Unified replacement for `callconv(...)`, which is removed.**
`ABI = { default, c, zig, pure }`: `.c` (C ABI), `.zig` (Zig-layout weld → the
`compiler` library), `.pure` (naked asm), `.default` (unannotated). Can appear
standalone (no extern) on any fn / fn-type / lambda.
- `extern <lib>` — linkage keyword + binding source (named library).
So a welded binding is `text_of :: (id: StringId) -> string abi(.zig) extern compiler;`.
What landed:
- **AST** (`src/ast.zig`): `CallingConvention` → `ABI { default, c, zig, pure }`;
the `call_conv` field → `abi: ABI` on `FnDecl` / `Lambda` / `FunctionTypeExpr`.
- **Lexer/token** (`src/token.zig`, `src/lexer.zig`): `kw_callconv` → `kw_abi`,
keyword string `"callconv"` → `"abi"`.
- **Parser** (`src/parser.zig`): `parseOptionalCallConv` → `parseOptionalAbi`
(parses `abi(.c|.zig|.pure)`); wired in the fn-decl postfix slot (before
`extern`/`export`), the function-type-expr slot, and the lambda slot;
`isFunctionDef`/`hasFnBodyAfterArrow` recognise `kw_abi`.
- **AST→IR map** (`src/ir/type_resolver.zig`, `src/ir/lower/decl.zig`, `sema.zig`,
`closure.zig`): the AST `.abi == .c` reads kept their C-ABI meaning; the
function-type resolver maps `.zig`/`.pure` → IR `.default` (no fn-pointer-type
CC for those decl-level ABIs; neither occurs in a function-TYPE position yet).
- **CC-mismatch diagnostic** (`src/ir/lower/expr.zig`, `src/sema.zig`): the
user-facing text `callconv(.c)` → `abi(.c)`.
- **sx migration**: 52 `.sx` files `callconv(` → `abi(` (all were function-type
callback annotations — none in the fn-decl postfix slot, so no reordering).
- **Docs**: `readme.md`, `specs.md`, the design doc, snapshots (0114 / 1104 /
1200) regenerated for the rename.
- **Tests**: parser unit tests in `src/parser.test.zig` — `abi(.zig) extern <lib>`
on a fn decl (asserts `abi == .zig`, `extern_export == .extern_`, `extern_lib ==
"compiler"`); bare `extern` leaves `abi == .default`; standalone `abi(.c)` /
`abi(.pure)`. lexer/sema tests updated.
`zig build` + `zig build test` green (450/450 unit + 685 corpus).
## Current state
- `compiler :: #library "compiler";` parses + is recognised as the comptime-only
internal surface (never dlopen'd).
- `abi(.zig) extern compiler` STRUCTS: layout-validated against the registry
(faithful → ok; drift → build-gating diagnostic). `Field` welds + usable.
- `abi(.zig) extern compiler` FUNCTIONS: dispatched under the comptime interp to
their registered Zig handler (`intern`/`text_of` round-trip works); unexported
names rejected at declaration. Comptime-only.
- A RUNTIME call to a welded fn is a clean build-gating error (comptime-only
enforcement at `emitCall`); the legitimate `#run`/`::` use stays green.
- The whole Phase 1 foundation (parse → registry → struct-layout validation →
function host-call bridge → comptime-only enforcement) is in place for the
two-u32 `Field` case + the two string readers.
- **Deferred**: offset-override / LLVM byte-offset GEP for non-natural layouts
(needed by `StructInfo`'s slice field, Phase 2).
## Next step — Phase 2 decomposition (byte-layout weld for `StructInfo`)
The weld plan (sub-step 1) is the pure layout math. The remaining sub-steps wire
it through emit + interp so a non-natural welded struct actually works. Each must
stay green; do ONE per session (the IR-stream split rule).
- **2.2 — LLVM type honours the plan.** In `src/backend/llvm/types.zig` `.@"struct"`
case: if the struct's name is in `compiler_lib.findType`, build the LLVM struct
from `computeWeldPlan` — elements in offset order (real field types + `[N x i8]`
padding), and **assert** `LLVMOffsetOfElement(elem) == plan.elements[e].offset`
for every field element + `LLVMABISizeOfType == total_size` (the build-time
layout-equality assertion; mismatch = a loud emit failure). Cache the plan per
TypeId (the GEP sites + interp need the remap). Prove: a welded struct's LLVM
type has the Zig offsets (an emit-level test or an `.ir`/codegen check).
- **2.3 — field access honours the remap.** Every `struct_gep` / field load+store
for a welded struct maps the sx field index → `plan.sx_to_llvm[i]` before
`LLVMBuildStructGEP2` (`src/backend/llvm/ops.zig` — `emitFieldAccess` /
struct-literal init / the `field_ptr` paths). Prove with a REORDERED welded
struct used as runtime data: construct + read each field back correct.
- **2.4 — interp comptime layout.** The comptime interp represents structs as
`Value.aggregate` by logical index — fine for field access. The byte layout
matters at the handler boundary: serialize a welded-struct `Value` into
Zig-layout memory (via the plan's offsets) so a handler can take `*ZigType`,
and read a Zig-layout result back into a `Value`. (Or: keep handlers reading
`Value` aggregates logically — decide when wiring `register_struct`.)
- **2.5 — `register_struct` / `find_type` handlers.** Bind
`register_struct(StructInfo) -> Type` (guarded: dup field names, kind) +
`find_type(StringId) -> ?Type` over the host-call bridge, consuming a welded
`StructInfo`. Prove: build a struct programmatically + round-trip a source one.
- **2.6 — re-express `type_info`/`define` (struct) as sx** over `register_struct`/
`find_type`; migrate `examples/0622`; delete the bespoke struct interp arms
(`defineStruct`/`reflectTypeInfo` struct path). Design build-order steps 23.
Then Phase 3+: widen to enum/tuple (`EnumInfo`/`TaggedUnionInfo`/`TupleInfo`,
optional fields → sentinels), migrate `BuildOptions` to `abi(.zig) extern
compiler` (the `#compiler` registry re-homes under the `compiler` lib), delete
`#compiler`.
## Known issues
- None for this stream. (Metatype's deferred enhancement is issue 0141 — comptime
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
## Log
- **Phase 2.1 — weld-plan layout math + `StructInfo` registered.** Decision:
full byte-layout weld (not logical-field marshalling). `computeWeldPlan`
(offset-order elements + padding + sx→element remap), pure + unit-tested
against `Field` (identity) and `StructInfo` (reordered, remap `[1,0,3,2]`).
No emit/interp wiring yet. Build + suite green.
- **Phase 1 polish — comptime-only enforcement.** A runtime call to a welded fn is
a clean build-gating error (`emitCall` gate, guarded by enclosing-`is_comptime`
so `#run`/`::` uses stay green), not a link failure. Example 1185. Build + suite
green (458 unit, 690 corpus).
- **Phase 1.1 fifth sub-step — host-call bridge (welded functions).**
`compiler_lib` function registry (`intern`/`text_of`) + `findFn`; IR `Function`
`compiler_welded` flag set/validated in `declareFunction` (`weldedCompilerFn`);
`interp.call()` dispatches welded calls to the Zig handler. Examples 0626 (round-
trip) + 1184 (unexported-fn diagnostic); `findFn` unit-tested. Runtime-call clean
rejection deferred (loud link error today). Build + suite green (458 unit, 689
corpus).
- **Phase 1.1 fourth sub-step — welded-struct layout validation.**
`validateStructLayout` (pure, unit-tested) + `validateWeldedStruct` wired into
`registerStructDecl`: a `struct abi(.zig) extern compiler` is validated against
the registry (lib == compiler, name exported, layout matches) with build-gating
diagnostics. `#library "compiler"` no longer dlopen'd. Examples 0625 (faithful
Field) + 1183 (field-count mismatch diagnostic). Offset-override/GEP deferred to
Phase 2 (not exercised by Field's natural layout). Build + suite green (456 unit,
687 corpus).
- **Phase 1.1 third sub-step — binding registry.** New `src/ir/compiler_lib.zig`:
the `compiler` lib's welded-type registry; `Field` welded to
`StructInfo.Field` with layout baked from the real Zig type
(`@offsetOf`/`@sizeOf`/`@alignOf`); `findType` lookup proven by unit test
(+ null off the export list). Standalone island — not yet consumed by lowering.
Build + suite green (454 unit tests). Break-verified.
- **Phase 1.1 second sub-step — struct-decl binding parses.** `ast.StructDecl`
gained `abi` + `extern_lib`; `parseStructDecl` parses `abi(.zig) extern <lib>`
after `struct`. Parser unit tests (welded `Field` + plain struct), break-verified.
Build + suite green. Parse-only sub-step (fns + structs) of Phase 1.1 complete.
- **Phase 1.1 first sub-step + `callconv`→`abi` unification.** Parsed `abi(.zig)
extern <lib>` on fn decls; unified `callconv` into `abi(.c|.zig|.pure)` (removed
the `callconv` keyword), migrated 52 sx files + compiler diagnostics + docs +
snapshots. Build + suite green. The original design's `extern(.zig)` single
qualifier was split into `abi(.zig)` (ABI/layout, before extern) + `extern
<lib>` (linkage + source) — recorded in the design doc's syntax-decision note.

View File

@@ -1,4 +1,4 @@
# Comptime Compiler API — `#library "compiler"` + `extern(.zig)`
# Comptime Compiler API — `#library "compiler"` + `abi(.zig) extern`
> **Status: design-of-record (not yet an active stream).** Captures a unified
> mechanism for sx↔compiler binding that subsumes the metatype `declare`/`define`
@@ -49,32 +49,44 @@ internal surface (Zig types + functions). Two defining properties:
`is_comptime` boundary. (Welded *types* are still usable as plain runtime data;
only the *functions* are comptime-gated.)
### `extern(.zig) <lib>` — postfix attribute
### `abi(.zig)` + `extern <lib>` — the binding surface
Slots where `#builtin` / `#compiler` go (postfix, after the return type for fns,
after `struct` for types), with the library handle following:
> **Syntax decision (2026-06-17, supersedes the original `extern(.zig) <lib>`
> single-qualifier form).** The ABI/layout selector and the linkage keyword are
> two orthogonal things, so they are two annotations, not one fused qualifier:
> - `abi(.x)` — the ABI / calling-convention annotation, in the postfix slot
> **before** `extern`/`export`. It is the unified replacement for the old
> `callconv(...)` (which is removed): `ABI = { default, c, zig, pure }` —
> `.c` (C ABI / cdecl), `.zig` (Zig-layout weld → the `compiler` library),
> `.pure` (naked asm). `.default` = unannotated (ordinary sx convention).
> - `extern <lib>` — the linkage keyword + binding source (the named library).
`abi(...)` sits where `callconv(...)` went (after the return type for fns); the
`extern`/`export` keyword and the library handle follow. For welded types, the
same `abi(.zig)` + `extern <lib>` pair sits after `struct`:
```sx
// functions:
text_of :: (id: StringId) -> string extern(.zig) compiler;
intern :: (s: string) -> StringId extern(.zig) compiler;
register_type :: (info: StructInfo) -> Type extern(.zig) compiler;
find_type :: (name: StringId) -> ?Type extern(.zig) compiler;
text_of :: (id: StringId) -> string abi(.zig) extern compiler;
intern :: (s: string) -> StringId abi(.zig) extern compiler;
register_type :: (info: StructInfo) -> Type abi(.zig) extern compiler;
find_type :: (name: StringId) -> ?Type abi(.zig) extern compiler;
// types (layout-welded to the lib's real Zig type):
Field :: struct extern(.zig) compiler { name: StringId; ty: Type; };
StructInfo :: struct extern(.zig) compiler {
Field :: struct abi(.zig) extern compiler { name: StringId; ty: Type; };
StructInfo :: struct abi(.zig) extern compiler {
name: StringId; fields: []Field; is_protocol: bool; nominal_id: u32;
};
```
`extern(.zig)` = "Zig ABI / Zig layout"; `<lib>` = the binding source.
`abi(.zig)` = "Zig ABI / Zig layout"; `extern compiler` = the linkage + binding
source.
### Layout welding — why it's exact, not brittle
The sx compiler is itself a Zig program; `types.zig` is part of it. So at
**compiler-build time** the real record's layout is available via
`@offsetOf` / `@sizeOf` / `@alignOf`. An `extern(.zig) compiler` struct is laid out
`@offsetOf` / `@sizeOf` / `@alignOf`. An `abi(.zig) extern compiler` struct is laid out
to the bound Zig type's EXACT offsets (queried, not guessed), and the compiler
ASSERTS the sx declaration matches the Zig type byte-for-byte (a mismatch is a
build error — the sx side is a header checked against the implementation). Because
@@ -134,8 +146,8 @@ instead of the user threading `declare`→forward-slot→`define`→eval-timing
## BuildOptions migration
`BuildOptions :: struct #compiler { ... }` + `build_options() #compiler`
`extern(.zig) compiler`: the setter/getter hook-methods become `extern(.zig)
compiler` functions (or methods on a welded/handle `BuildOptions`), backed by the
`abi(.zig) extern compiler`: the setter/getter hook-methods become `abi(.zig)
extern compiler` functions (or methods on a welded/handle `BuildOptions`), backed by the
same `BuildConfig` state. The `compiler_hooks.zig` registry becomes the `compiler`
lib's function/type registry. Net: the build DSL and the metatype API ride one
mechanism.
@@ -157,12 +169,17 @@ Foundation that ALREADY exists:
- `extern` / `export` are keywords (`src/token.zig:46`, `kw_extern`/`kw_export`).
New work for Phase 1:
- **Lexer/parser**: the `(.zig)` ABI qualifier on `extern`, and the trailing
`<lib>` identifier — `… extern(.zig) <lib>` postfix on FN decls (after the
return type, beside `#builtin`/`#compiler` at `src/parser.zig:233`/`:315`) and
STRUCT decls (beside `struct #compiler`, `src/parser.zig:953`).
- **AST**: an abi/layout-binding field on `FnDecl` and the struct decl (`abi:
.c | .zig`, `lib: ?name`).
- **Lexer/parser**: the `abi(.zig)` annotation (a new `abi` keyword replacing
`callconv`; `ABI = { default, c, zig, pure }`) in the slot before `extern`,
followed by the `<lib>` handle — `… abi(.zig) extern <lib>` postfix on FN decls
(after the return type, before `extern`) and STRUCT decls (beside
`struct #compiler`). **DONE (parse-only)**`parseOptionalAbi`
(`src/parser.zig`) wired on fn decls AND struct decls, `ast.ABI`, parser unit
tests; the `callconv``abi` rename migrated 52 sx files + the compiler's
CC-mismatch diagnostic.
- **AST**: the `abi: ABI` field lives on `FnDecl` / `Lambda` / `FunctionTypeExpr`
(carries `.zig` for a welded fn); `StructDecl` gained `abi: ABI` +
`extern_lib: ?[]const u8`. **DONE.**
- **Binding registry**: re-home / generalize `src/ir/compiler_hooks.zig` (today's
`#compiler` registry) into the `compiler` lib's type+function registry, keyed by
exported sx name → Zig type (`@offsetOf` layout) / Zig fn (host-call).
@@ -174,12 +191,13 @@ New work for Phase 1:
## Build order (each phase keeps `zig build test` green)
1. **`extern(.zig)` + `#library` foundation** — parse the postfix attribute (the
`#library` decl already exists); a binding registry (sx name → Zig type/fn);
the layout engine honoring the bound type's `@offsetOf` offsets + LLVM emission
that hits them; **build-time layout-equality assertion**. Prove with `Field`
(two u32s). First testable sub-step: `extern(.zig) <lib>` PARSES on a fn decl
(parser unit test), AST carries the binding — no semantics yet.
1. **`abi(.zig) extern <lib>` + `#library` foundation** — parse the postfix
annotation (the `#library` decl already exists); a binding registry (sx name →
Zig type/fn); the layout engine honoring the bound type's `@offsetOf` offsets +
LLVM emission that hits them; **build-time layout-equality assertion**. Prove
with `Field` (two u32s). First testable sub-step **DONE**: `abi(.zig) extern
<lib>` PARSES on a fn decl (parser unit test), AST carries the binding (`abi ==
.zig`, `extern_lib`) — no semantics yet.
2. **Weld `StructInfo`** + `StringId` accessors (`intern`/`text_of`) over the
host-call bridge.
3. **Re-express `type_info`/`define` (struct)** as sx over `register_struct`/
@@ -187,7 +205,7 @@ New work for Phase 1:
4. **Widen to enum/tuple** — weld `EnumInfo`/`TaggedUnionInfo`/`TupleInfo`
(optional fields → sentinels: `backing_type` `.unresolved`, `explicit_values`
len-0); migrate `examples/0619`/`0623`; delete the enum/tuple interp arms.
5. **Migrate `BuildOptions`** to `extern(.zig) compiler`.
5. **Migrate `BuildOptions`** to `abi(.zig) extern compiler`.
6. **Delete `#compiler`**; suite green.
## Risks / open questions

View File

@@ -1,6 +1,6 @@
// Generic `Into(Block)` impl with a `string`-typed arg in the
// closure signature. The block trampoline declares the param with
// callconv(.c); without the abi-collapse fix, sx `string` got
// abi(.c); without the abi-collapse fix, sx `string` got
// silently collapsed to `ptr` (the libc `char *` heuristic) and
// the caller's 16-byte `{ptr, len}` value mismatched the
// trampoline's 8-byte `ptr` slot. Result: segfault inside the
@@ -8,7 +8,7 @@
//
// The fix lives in `abiCoerceParamTypeEx`: the `string`/`slice` →
// `ptr` collapse only applies to `is_extern` extern decls (libc
// interop). sx-internal `callconv(.c)` keeps the full slice
// interop). sx-internal `abi(.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.
@@ -20,7 +20,7 @@ g_s: string = "";
main :: () -> i32 {
cl := (s: string) => { g_s = s; };
b : Block = xx cl;
invoke_fn : (*Block, string) -> void callconv(.c) = xx b.invoke;
invoke_fn : (*Block, string) -> void abi(.c) = xx b.invoke;
invoke_fn(@b, "hello");
if g_s.len == 0 { print("FAIL: empty\n"); return 1; }
print("got: <{}>\n", g_s);

View File

@@ -1,7 +1,7 @@
// FFI plan step 5.2 — generic `Into(Block) for Closure(..$args) ->
// $R` impl. One impl in stdlib covers every closure shape; the
// compiler monomorphises the impl body per call shape and emits a
// dedicated `__invoke` `callconv(.c)` trampoline + Block literal
// dedicated `__invoke` `abi(.c)` trampoline + Block literal
// (via `#insert build_block_convert($args, $R);`).
//
// This test exercises a closure shape (`Closure(i64, i64) -> void`)
@@ -13,7 +13,7 @@
// the per-shape trampoline ferries control back to the sx closure.
//
// The block is invoked directly through `b.invoke` (a typed
// `callconv(.c)` fn-pointer) — the same shape the Apple Block
// `abi(.c)` fn-pointer) — the same shape the Apple Block
// runtime calls when a UIKit/Foundation API hands the block back
// to its registered invoke.
@@ -27,7 +27,7 @@ main :: () -> i32 {
cl := (a: i64, b: i64) => { g_a = a; g_b = b; };
blk : Block = xx cl;
invoke_fn : (*Block, i64, i64) -> void callconv(.c) = xx blk.invoke;
invoke_fn : (*Block, i64, i64) -> void abi(.c) = xx blk.invoke;
invoke_fn(@blk, 10, 20);
if g_a != 10 { print("FAIL: g_a={}\n", g_a); return 1; }

View File

@@ -0,0 +1,24 @@
// Comptime compiler API — a layout-welded struct binding.
//
// `Field :: struct abi(.zig) extern compiler { … }` binds the sx struct to the
// compiler's real internal Zig type (`StructInfo.Field`, two u32s) via the
// `compiler` library. The compiler validates the sx declaration against the
// welded type's layout at registration time (the sx side is a header checked
// against the implementation) — a faithful declaration validates clean and the
// struct is otherwise ordinary data. The `compiler` library is the comptime-only
// internal surface, so `#library "compiler"` is NOT dlopen'd.
//
// Phase 1 (foundation): the weld is layout-validated; field offsets coincide with
// the natural layout for `Field` (two u32s). Welded host-call functions land in a
// later phase.
#import "modules/std.sx";
compiler :: #library "compiler";
Field :: struct abi(.zig) extern compiler { name: u32; ty: u32; }
main :: () {
f := Field.{ name = 7, ty = 3 };
print("name={} ty={}\n", f.name, f.ty);
}

View File

@@ -0,0 +1,24 @@
// Comptime compiler API — welded compiler FUNCTIONS over the host-call bridge.
//
// `intern` / `text_of` are bound to the `compiler` library via
// `abi(.zig) extern compiler`. They have no real symbol — under the comptime
// interpreter the call dispatches to the compiler's registered Zig handler
// (the string pool), never dlsym. Comptime-only: here they run inside `#run`,
// folding to a plain string constant the runtime `main` prints.
//
// Round-trip: `text_of(intern(s)) == s` — interning a string yields a handle,
// and resolving the handle gives the text back.
#import "modules/std.sx";
compiler :: #library "compiler";
StringId :: u32;
intern :: (s: string) -> StringId abi(.zig) extern compiler;
text_of :: (id: StringId) -> string abi(.zig) extern compiler;
greeting :: #run text_of(intern("hello, compiler"));
main :: () {
print("{}\n", greeting);
}

View File

@@ -1,4 +1,4 @@
// Passing a default-conv sx function as a callconv(.c) fn-pointer
// Passing a default-conv sx function as a abi(.c) fn-pointer
// silently mismatches ABIs — historically that meant the C-side caller
// supplied no `__sx_ctx` slot 0 and the sx-side body read garbage.
// The compiler now rejects the coercion outright with a "call-convention
@@ -9,6 +9,6 @@
sx_handler :: (arg: *void) -> *void { return arg; }
main :: () -> i32 {
fp : (*void) -> *void callconv(.c) = sx_handler;
fp : (*void) -> *void abi(.c) = sx_handler;
return 0;
}

View File

@@ -0,0 +1,17 @@
// Diagnostic: a layout-welded struct whose sx declaration does NOT faithfully
// mirror the compiler's real Zig type is a build error — the sx side is a header
// checked against the implementation, not a free reinterpretation.
//
// `Field` is two u32s (`name`, `ty`) in the compiler library; declaring it with a
// single field must be rejected at registration with a clear field-count message.
#import "modules/std.sx";
compiler :: #library "compiler";
Field :: struct abi(.zig) extern compiler { name: u32; }
main :: () {
f := Field.{ name = 1 };
print("{}\n", f.name);
}

View File

@@ -0,0 +1,11 @@
// Diagnostic: a `fn abi(.zig) extern compiler` whose name is NOT on the compiler
// library's function-export list is a build error — the export list is the
// safety boundary, so an unbound name can't silently fall through to dlsym.
#import "modules/std.sx";
compiler :: #library "compiler";
not_a_real_compiler_fn :: (x: i64) -> i64 abi(.zig) extern compiler;
main :: () { print("unreached\n"); }

View File

@@ -0,0 +1,17 @@
// Diagnostic: a welded `compiler`-library function is comptime-only — it has no
// runtime symbol (the comptime interpreter dispatches it to a Zig handler).
// Calling one from runtime code is a build error with a clear message, NOT an
// undefined-symbol link failure. (A comptime use — inside `#run` or a `::` —
// is fine; see examples/0626.)
#import "modules/std.sx";
compiler :: #library "compiler";
StringId :: u32;
intern :: (s: string) -> StringId abi(.zig) extern compiler;
main :: () {
id := intern("called at runtime");
print("{}\n", id);
}

View File

@@ -1,16 +1,16 @@
// `callconv(.c)` on function pointers passed to extern callbacks — ensures
// `abi(.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";
// A function with C calling convention
add_c :: (a: i64, b: i64) -> i64 callconv(.c) {
add_c :: (a: i64, b: i64) -> i64 abi(.c) {
a + b
}
main :: () {
// Call it directly — should work like any other function
result := add_c(10, 32);
print("callconv(.c): {}\n", result);
print("abi(.c): {}\n", result);
}

View File

@@ -1,4 +1,4 @@
// `callconv(.c)` callbacks accessing struct methods on global pointers —
// `abi(.c)` callbacks accessing struct methods on global pointers —
// regression coverage for prior data-corruption when the callback dispatches
// through a global pointer to a method on the pointed-to struct.
@@ -29,7 +29,7 @@ do_render :: () {
print("wrapper: pw={}, ph={}, frame={}\n", g_pipe.pw, g_pipe.ph, g_pipe.frame);
}
callback_inline :: (userdata: *void, code: i64) -> bool callconv(.c) {
callback_inline :: (userdata: *void, code: i64) -> bool abi(.c) {
g_width = xx code;
g_height = xx (code + 1);
g_pipe.resize(xx g_width, xx g_height);
@@ -38,7 +38,7 @@ callback_inline :: (userdata: *void, code: i64) -> bool callconv(.c) {
true
}
callback_wrapper :: (userdata: *void, code: i64) -> bool callconv(.c) {
callback_wrapper :: (userdata: *void, code: i64) -> bool abi(.c) {
g_width = xx code;
g_height = xx (code + 1);
do_render();

View File

@@ -1,6 +1,6 @@
// Regression test for issue-0025 path A.
//
// sx functions declared with `callconv(.c)` that take a composite > 16 bytes
// sx functions declared with `abi(.c)` that take a composite > 16 bytes
// by value must marshal the arg through `ptr byval(<T>)` per AAPCS64 / SysV
// AArch64: the caller copies the struct to an alloca, passes the alloca
// pointer with a `byval(<T>)` attribute, and the callee's entry block loads
@@ -20,7 +20,7 @@ Wide :: struct {
d: i64;
}
accept_c :: (w: Wide) -> i64 callconv(.c) {
accept_c :: (w: Wide) -> i64 abi(.c) {
w.a + w.b + w.c + w.d
}

View File

@@ -1,6 +1,6 @@
// Regression test for issue-0025 path B.
//
// When a fn-pointer's type is spelled with `callconv(.c)`, the indirect
// When a fn-pointer's type is spelled with `abi(.c)`, the indirect
// call must apply the same C-ABI byval coercion that direct C-ABI calls
// do at the call site (path A): >16-byte non-HFA aggregates are passed
// as `ptr byval(<T>)`. Without the fix, the indirect call site builds
@@ -9,8 +9,8 @@
// ways that don't match the byval-attributed callee signature — the
// callee then reads garbage out of the wrong machine-state slots.
//
// The opt-in is the `callconv(.c)` on the fn-pointer type spelling.
// Pure-sx fn-pointer casts (no callconv suffix) keep their default
// The opt-in is the `abi(.c)` on the fn-pointer type spelling.
// Pure-sx fn-pointer casts (no abi suffix) keep their default
// calling convention — verified by examples/87-fnptr-cast-large-aggregate.sx.
#import "modules/std.sx";
@@ -22,7 +22,7 @@ Wide :: struct {
d: i64;
}
accept_c :: (w: Wide) -> i64 callconv(.c) {
accept_c :: (w: Wide) -> i64 abi(.c) {
w.a + w.b + w.c + w.d
}
@@ -30,7 +30,7 @@ main :: () -> i32 {
w := Wide.{ a = 1, b = 10, c = 100, d = 1000 };
if accept_c(w) != 1111 { return 1; }
fn_ptr : (Wide) -> i64 callconv(.c) = xx accept_c;
fn_ptr : (Wide) -> i64 abi(.c) = xx accept_c;
if fn_ptr(w) != 1111 { return 2; }
0

View File

@@ -1,10 +1,10 @@
// Pure-sx fn-pointer cast: a function-pointer typed without `callconv(.c)`
// Pure-sx fn-pointer cast: a function-pointer typed without `abi(.c)`
// keeps the default (sx) calling convention. Passing a >16-byte aggregate
// through that pointer must not get the C-ABI byval coercion — the sx-CC
// callee expects the struct as an SSA value, not as a `ptr byval(<T>)`.
//
// Pair with examples/86-callconv-c-fnptr-large-aggregate.sx, which covers
// the opposite arm (fn-pointer typed `callconv(.c)` does get byval).
// Pair with examples/86-abi-c-fnptr-large-aggregate.sx, which covers
// the opposite arm (fn-pointer typed `abi(.c)` does get byval).
#import "modules/std.sx";

View File

@@ -16,19 +16,19 @@
#source "1214-ffi-06-callback.c";
};
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;
ffi_apply_callback :: (cb: (i32) -> i32 abi(.c), value: i32) -> i32 extern;
ffi_apply_callback2 :: (cb: (*void, i32) -> i32 abi(.c), ctx: *void, v: i32) -> i32 extern;
g_callback_hits : i32 = 0;
g_callback_sum : i32 = 0;
double_it :: (x: i32) -> i32 callconv(.c) {
double_it :: (x: i32) -> i32 abi(.c) {
g_callback_hits += 1;
g_callback_sum += x;
x * 2
}
add_with_ctx :: (ctx: *void, v: i32) -> i32 callconv(.c) {
add_with_ctx :: (ctx: *void, v: i32) -> i32 abi(.c) {
g_callback_hits += 1;
// Pass a sentinel via ctx to prove the pointer arg also survives the
// round-trip — read it back as an i32 through *i32.

View File

@@ -18,10 +18,10 @@ main :: () -> i32 {
sel_with_utf8 := sel_registerName("stringWithUTF8String:".ptr);
sel_utf8 := sel_registerName("UTF8String".ptr);
msg_3 : (*void, *void, [*]u8) -> *void callconv(.c) = xx objc_msgSend;
msg_3 : (*void, *void, [*]u8) -> *void abi(.c) = xx objc_msgSend;
ns_str := msg_3(ns_class, sel_with_utf8, "hi".ptr);
msg_2 : (*void, *void) -> [*]u8 callconv(.c) = xx objc_msgSend;
msg_2 : (*void, *void) -> [*]u8 abi(.c) = xx objc_msgSend;
back := msg_2(ns_str, sel_utf8);
return xx (back[0] + back[1]); // 'h' + 'i' = 104 + 105 = 209

View File

@@ -17,7 +17,7 @@ g_marker : i32 = 0;
// IMP for `hello`. Must use C calling convention so `self` and `_cmd` land in
// x0 and x1 the way the Obj-C runtime expects.
hello_imp :: (self: *void, _cmd: *void) callconv(.c) {
hello_imp :: (self: *void, _cmd: *void) abi(.c) {
g_marker = 42;
}
@@ -34,7 +34,7 @@ main :: () -> i32 {
if obj == xx 0 { return 2; }
// [obj hello]
msg : (*void, *void) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg(obj, sel_hello);
return g_marker; // 42 if hello_imp ran

View File

@@ -11,7 +11,7 @@
main :: () -> i32 {
cl := () => { print("noop block ran\n"); };
b : Block = xx cl;
invoke_fn : (*Block) -> void callconv(.c) = xx b.invoke;
invoke_fn : (*Block) -> void abi(.c) = xx b.invoke;
invoke_fn(@b);
0
}

View File

@@ -11,7 +11,7 @@ main :: () -> i32 {
y : i64 = 100;
cl := () => { print("x + y = {}\n", x + y); };
b : Block = xx cl;
invoke_fn : (*Block) -> void callconv(.c) = xx b.invoke;
invoke_fn : (*Block) -> void abi(.c) = xx b.invoke;
invoke_fn(@b);
0
}

View File

@@ -18,7 +18,7 @@
// Trampoline matching `void (^)(int, void*)` — the C ABI Apple's
// runtime calls. Forwards through to the sx closure with the
// standard `(__sx_ctx, env, ...args)` shape.
__block_invoke_void_i32_p :: (block_self: *Block, arg0: i32, arg1: *void) callconv(.c) {
__block_invoke_void_i32_p :: (block_self: *Block, arg0: i32, arg1: *void) abi(.c) {
typed_fn : (*void, i32, *void) -> void = xx block_self.sx_fn;
typed_fn(block_self.sx_env, arg0, arg1);
}
@@ -49,7 +49,7 @@ main :: () -> i32 {
};
b : Block = xx cl;
invoke_fn : (*Block, i32, *void) -> void callconv(.c) = xx b.invoke;
invoke_fn : (*Block, i32, *void) -> void abi(.c) = xx b.invoke;
sentinel: i32 = 42;
invoke_fn(@b, 41, xx @sentinel);

View File

@@ -6,7 +6,7 @@
#import "modules/ffi/objc_block.sx";
invoke_once :: (b: *Block) {
invoke_fn : (*Block) -> void callconv(.c) = xx b.invoke;
invoke_fn : (*Block) -> void abi(.c) = xx b.invoke;
invoke_fn(b);
}

View File

@@ -35,7 +35,7 @@ main :: () -> i32 {
// [SxFoo alloc] — invokes the synthesized +alloc IMP.
sel_alloc : SEL = sel_registerName("alloc".ptr);
msg_fn : (cls: *void, sel: *void) -> *void callconv(.c) = xx objc_msgSend;
msg_fn : (cls: *void, sel: *void) -> *void abi(.c) = xx objc_msgSend;
instance : *void = msg_fn(cls, sel_alloc);
if instance == null { print("FAIL: +alloc returned null\n"); return 1; }

View File

@@ -39,12 +39,12 @@ main :: () -> i32 {
// alloc + release — synthesized -dealloc IMP fires inside.
sel_alloc : SEL = sel_registerName("alloc".ptr);
alloc_fn : (cls: *void, sel: *void) -> *void callconv(.c) = xx objc_msgSend;
alloc_fn : (cls: *void, sel: *void) -> *void abi(.c) = xx objc_msgSend;
instance : *void = alloc_fn(cls, sel_alloc);
if instance == null { print("FAIL: +alloc returned null\n"); return 1; }
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void callconv(.c) = xx objc_msgSend;
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(instance, sel_release);
// Run another cycle to confirm dealloc didn't corrupt runtime state.

View File

@@ -35,7 +35,7 @@ main :: () -> i32 {
if method == null { print("FAIL: class method not on metaclass\n"); return 1; }
// Invoke via objc_msgSend: [SxFoo answer] → 42.
msg_fn : (cls: *void, sel: *void) -> i32 callconv(.c) = xx objc_msgSend;
msg_fn : (cls: *void, sel: *void) -> i32 abi(.c) = xx objc_msgSend;
result : i32 = msg_fn(cls, sel_answer);
if result != 42 { print("FAIL: expected 42, got {}\n", result); return 1; }

View File

@@ -38,13 +38,13 @@ main :: () -> i32 {
// [SxThing answer] → 42
sel_answer : SEL = sel_registerName("answer".ptr);
msg_int : (cls: *void, sel: *void) -> i32 callconv(.c) = xx objc_msgSend;
msg_int : (cls: *void, sel: *void) -> i32 abi(.c) = xx objc_msgSend;
r := msg_int(cls, sel_answer);
if r != 42 { print("FAIL: answer expected 42, got {}\n", r); return 1; }
// [SxThing seedClass] returns a non-null NSObject.
sel_seed : SEL = sel_registerName("seedClass".ptr);
msg_ptr : (cls: *void, sel: *void) -> *void callconv(.c) = xx objc_msgSend;
msg_ptr : (cls: *void, sel: *void) -> *void abi(.c) = xx objc_msgSend;
seed := msg_ptr(cls, sel_seed);
if seed == null { print("FAIL: seedClass returned null\n"); return 1; }

View File

@@ -23,10 +23,10 @@
// runtime ivar. Property dispatch should round-trip through them.
g_probe_tag: i32 = 0;
probe_get_tag :: (self: *void, _cmd: *void) -> i32 callconv(.c) {
probe_get_tag :: (self: *void, _cmd: *void) -> i32 abi(.c) {
return g_probe_tag;
}
probe_set_tag :: (self: *void, _cmd: *void, v: i32) callconv(.c) {
probe_set_tag :: (self: *void, _cmd: *void, v: i32) abi(.c) {
g_probe_tag = v;
}

View File

@@ -5,7 +5,7 @@
// through it and returns void.
//
// Register a runtime-built Obj-C class with a method that returns
// a fixed `Triple`. The IMP is a plain sx fn (callconv .c) — its
// a fixed `Triple`. The IMP is a plain sx fn (abi .c) — its
// sret-shaped lowering already works (Phase 0.3 fix for plain
// `extern` returns). The `#objc_call` dispatch side now produces
// the matching call shape: `call void @objc_msgSend(ptr sret %slot,
@@ -20,7 +20,7 @@ Triple :: struct { a: i64; b: i64; c: i64; }
// IMP for the runtime-installed method. Obj-C convention: implicit
// (self, _cmd) prefix, then declared args. Returns the value bytes.
triple_imp :: (self: *void, _cmd: *void) -> Triple callconv(.c) {
triple_imp :: (self: *void, _cmd: *void) -> Triple abi(.c) {
Triple.{ a = 11, b = 22, c = 33 }
}

View File

@@ -20,7 +20,7 @@ UIEdgeInsets :: struct {
right: f64;
}
insets_imp :: (self: *void, _cmd: *void) -> UIEdgeInsets callconv(.c) {
insets_imp :: (self: *void, _cmd: *void) -> UIEdgeInsets abi(.c) {
UIEdgeInsets.{ top = 1.5, left = 2.5, bottom = 3.5, right = 4.5 }
}

View File

@@ -13,7 +13,7 @@
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
combine_imp :: (self: *void, _cmd: *void, a: i32, b: i32) -> i32 callconv(.c) {
combine_imp :: (self: *void, _cmd: *void, a: i32, b: i32) -> i32 abi(.c) {
a * 100 + b
}

View File

@@ -13,8 +13,8 @@
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
yes_imp :: (self: *void, _cmd: *void) -> bool callconv(.c) { true }
no_imp :: (self: *void, _cmd: *void) -> bool callconv(.c) { false }
yes_imp :: (self: *void, _cmd: *void) -> bool abi(.c) { true }
no_imp :: (self: *void, _cmd: *void) -> bool abi(.c) { false }
main :: () -> i32 {
inline if OS == .macos {

View File

@@ -20,11 +20,11 @@ CGRect :: struct {
height: f64;
}
rect_imp :: (self: *void, _cmd: *void) -> CGRect callconv(.c) {
rect_imp :: (self: *void, _cmd: *void) -> CGRect abi(.c) {
CGRect.{ x = 10.5, y = 20.5, width = 30.5, height = 40.5 }
}
u64_imp :: (self: *void, _cmd: *void) -> u64 callconv(.c) {
u64_imp :: (self: *void, _cmd: *void) -> u64 abi(.c) {
// sx integer-literal parser rejects values ≥ 2^63 even when the
// receiving type is u64, so the leading bit stays clear.
0x7FEDCBA987654321

View File

@@ -51,7 +51,7 @@ main :: () -> i32 {
// release
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void callconv(.c) = xx objc_msgSend;
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(xx f, sel_release);
}
inline if OS != .macos {

View File

@@ -52,7 +52,7 @@ main :: () -> i32 {
print("at: ({}, {})\n", p.x, p.y); // expected: at: (7.500000, 8.250000)
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void callconv(.c) = xx objc_msgSend;
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(xx m, sel_release);
}
inline if OS != .macos {

View File

@@ -20,7 +20,7 @@ SxProbeNiladic :: #objc_class("SxProbeNiladic") extern {
length :: (self: *Self) -> i32;
}
length_imp :: (self: *void, _cmd: *void) -> i32 callconv(.c) {
length_imp :: (self: *void, _cmd: *void) -> i32 abi(.c) {
42
}

View File

@@ -12,7 +12,7 @@ SxProbeOneArg :: #objc_class("SxProbeOneArg") extern {
addObject :: (self: *Self, val: i32) -> i32;
}
addObject_imp :: (self: *void, _cmd: *void, val: i32) -> i32 callconv(.c) {
addObject_imp :: (self: *void, _cmd: *void, val: i32) -> i32 abi(.c) {
val * 2
}

View File

@@ -11,7 +11,7 @@ SxProbeMultiKeyword :: #objc_class("SxProbeMultiKeyword") extern {
combine_and :: (self: *Self, a: i32, b: i32) -> i32;
}
combine_imp :: (self: *void, _cmd: *void, a: i32, b: i32) -> i32 callconv(.c) {
combine_imp :: (self: *void, _cmd: *void, a: i32, b: i32) -> i32 abi(.c) {
a * 100 + b
}

View File

@@ -27,7 +27,7 @@ SxManglingProbe :: #objc_class("SxManglingProbe") extern {
custom_name :: (self: *Self) -> i32 #selector("actualSelectorName");
}
universal_imp :: (self: *void, _cmd: *void, a: i32, b: i32, c: i32, d: i32) -> i32 callconv(.c) {
universal_imp :: (self: *void, _cmd: *void, a: i32, b: i32, c: i32, d: i32) -> i32 abi(.c) {
// Returns the arg count's witness; the test doesn't check return
// values, only that dispatch succeeds for each selector shape.
a + b + c + d

View File

@@ -31,7 +31,7 @@ main :: () -> i32 {
print("counter: {}\n", b.get()); // expected: 2
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void callconv(.c) = xx objc_msgSend;
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(xx b, sel_release);
}
inline if OS != .macos {

View File

@@ -21,7 +21,7 @@ UIApplicationMain :: (argc: i32, argv: *void, principal_class: *NSString, delega
// IMP for application:didFinishLaunchingWithOptions:
// Obj-C: -(BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)opts
// Type encoding: "c@:@@" -- BOOL (signed char), self, _cmd, id, id
did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) {
did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 abi(.c) {
NSLog(xx "[sx] application:didFinishLaunchingWithOptions: called\n");
return 1; // YES
}

View File

@@ -25,14 +25,14 @@ g_window : *void = ---;
// AppDelegate's `window` property. iOS queries this getter to discover the
// app's key window; without it, the legacy code path creates its own empty
// window and ignores anything we configure.
window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) {
window_getter :: (self: *void, _cmd: *void) -> *void abi(.c) {
return g_window;
}
window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) {
window_setter :: (self: *void, _cmd: *void, w: *void) abi(.c) {
g_window = w;
}
did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) {
did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 abi(.c) {
UIWindow := objc_getClass("UIWindow".ptr);
UIViewController := objc_getClass("UIViewController".ptr);
UIColor := objc_getClass("UIColor".ptr);
@@ -48,10 +48,10 @@ did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u
sel_connected_scenes := sel_registerName("connectedScenes".ptr);
sel_any_object := sel_registerName("anyObject".ptr);
msg_o : (*void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_v : (*void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_ooo : (*void, *void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void abi(.c) = xx objc_msgSend;
msg_v : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void abi(.c) = xx objc_msgSend;
msg_ooo : (*void, *void, *void) -> *void abi(.c) = xx objc_msgSend;
// Modern iOS path: get the connected windowScene, then construct the
// window via initWithWindowScene: so UIKit auto-sizes it and attaches

View File

@@ -39,7 +39,7 @@ configure :: () {
// IMP for application:didFinishLaunchingWithOptions:
// Obj-C: -(BOOL)application:(UIApplication *)app didFinishLaunchingWithOptions:(NSDictionary *)opts
// Type encoding: "c@:@@" -- BOOL (signed char), self, _cmd, id, id
did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) {
did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 abi(.c) {
NSLog(xx "[sx-device-probe] launched\n");
return 1; // YES
}

View File

@@ -1,11 +1,11 @@
// PLAN-HTTPZ C2: an sx `callconv(.c)` function passes by name as a
// PLAN-HTTPZ C2: an sx `abi(.c)` function passes by name as a
// typed C function pointer — libc qsort drives an sx comparator.
#import "modules/std.sx";
clib :: #library "c";
qsort :: (base: [*]u8, nel: usize, width: usize, compar: (*void, *void) -> i32 callconv(.c)) extern clib;
qsort :: (base: [*]u8, nel: usize, width: usize, compar: (*void, *void) -> i32 abi(.c)) extern clib;
cmp_i32 :: (a: *void, b: *void) -> i32 callconv(.c) {
cmp_i32 :: (a: *void, b: *void) -> i32 abi(.c) {
pa : *i32 = xx a;
pb : *i32 = xx b;
if pa.* < pb.* { return -1; }

View File

@@ -1,14 +1,14 @@
// PLAN-HTTPZ C2: the C->sx re-entry contract. A real OS thread enters
// sx through a `callconv(.c)` entry (which has NO implicit context),
// sx through a `abi(.c)` entry (which has NO implicit context),
// fabricates its own Context via `push Context.{ allocator = xx gpa }`,
// and calls default-conv sx code that allocates through it.
#import "modules/std.sx";
clib :: #library "c";
pthread_create :: (thread: *usize, attr: *void, start: (*void) -> *void callconv(.c), arg: *void) -> i32 extern clib;
pthread_create :: (thread: *usize, attr: *void, start: (*void) -> *void abi(.c), arg: *void) -> i32 extern clib;
pthread_join :: (thread: usize, retval: **void) -> i32 extern clib;
entry :: (arg: *void) -> *void callconv(.c) {
entry :: (arg: *void) -> *void abi(.c) {
p : *i64 = xx arg;
gpa := GPA.init();
push Context.{ allocator = xx gpa } {

View File

@@ -11,7 +11,7 @@ Shared :: struct {
}
// Raw-thread entry: C->sx boundary, own fabricated context.
bump_entry :: (arg: *void) -> *void callconv(.c) {
bump_entry :: (arg: *void) -> *void abi(.c) {
sh : *Shared = xx arg;
gpa := GPA.init();
push Context.{ allocator = xx gpa } {

View File

@@ -2,7 +2,7 @@
// CALLS BACK into an sx function (`cb`) by its symbol, then returns. For the asm
// `bl _cb` to resolve, the sx callback needs EXTERNAL LINKAGE and a stable C
// symbol — that is exactly what `export` provides (it also implies the C ABI, so
// no hidden context parameter). `callconv(.c)` alone is NOT enough: it sets the
// no hidden context parameter). `abi(.c)` alone is NOT enough: it sets the
// ABI but leaves the function `internal`, so it is dead-code-eliminated (nothing
// in the IR references it — the `bl` is opaque to the optimizer) and `_cb` is
// undefined at link. macOS gives `export "cb"` the symbol `_cb` (leading

View File

@@ -1,10 +1,10 @@
--- void / 0 args ---
__invoke :: (block_self: *Block) -> void callconv(.c) { typed_fn : (*void) -> void = xx block_self.sx_fn; typed_fn(block_self.sx_env); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
__invoke :: (block_self: *Block) -> void abi(.c) { typed_fn : (*void) -> void = xx block_self.sx_fn; typed_fn(block_self.sx_env); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
--- void / bool ---
__invoke :: (block_self: *Block, arg0: bool) -> void callconv(.c) { typed_fn : (*void, bool) -> void = xx block_self.sx_fn; typed_fn(block_self.sx_env, arg0); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
__invoke :: (block_self: *Block, arg0: bool) -> void abi(.c) { typed_fn : (*void, bool) -> void = xx block_self.sx_fn; typed_fn(block_self.sx_env, arg0); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
--- void / i64, string ---
__invoke :: (block_self: *Block, arg0: i64, arg1: string) -> void callconv(.c) { typed_fn : (*void, i64, string) -> void = xx block_self.sx_fn; typed_fn(block_self.sx_env, arg0, arg1); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
__invoke :: (block_self: *Block, arg0: i64, arg1: string) -> void abi(.c) { typed_fn : (*void, i64, string) -> void = xx block_self.sx_fn; typed_fn(block_self.sx_env, arg0, arg1); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
--- i32 / f64 ---
__invoke :: (block_self: *Block, arg0: f64) -> i32 callconv(.c) { typed_fn : (*void, f64) -> i32 = xx block_self.sx_fn; return typed_fn(block_self.sx_env, arg0); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
__invoke :: (block_self: *Block, arg0: f64) -> i32 abi(.c) { typed_fn : (*void, f64) -> i32 = xx block_self.sx_fn; return typed_fn(block_self.sx_env, arg0); } return .{ isa = @_NSConcreteStackBlock, flags = 0, reserved = 0, invoke = xx @__invoke, descriptor = xx @__sx_block_descriptor, sx_env = self.env, sx_fn = self.fn_ptr, };
--- build done ---
rt

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
name=7 ty=3

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
hello, compiler

View File

@@ -1,5 +1,5 @@
error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects callconv(.c)
--> examples/1104-diagnostics-callconv-mismatch-diagnostic.sx:12:42
error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects abi(.c)
--> examples/1104-diagnostics-callconv-mismatch-diagnostic.sx:12:37
|
12 | fp : (*void) -> *void callconv(.c) = sx_handler;
| ^^^^^^^^^^
12 | fp : (*void) -> *void abi(.c) = sx_handler;
| ^^^^^^^^^^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: welded type 'Field' has 2 field(s) in the compiler library but the declaration has 1
--> examples/1183-diagnostics-weld-struct-field-count.sx:12:51
|
12 | Field :: struct abi(.zig) extern compiler { name: u32; }
| ^^^

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,5 @@
error: 'not_a_real_compiler_fn' is not a function exported by the 'compiler' library
--> examples/1184-diagnostics-weld-fn-unexported.sx:9:1
|
9 | not_a_real_compiler_fn :: (x: i64) -> i64 abi(.zig) extern compiler;
| ^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1 @@
error: 'intern' is a comptime-only compiler-library function — it cannot be called at runtime (use it inside #run or a comptime '::')

View File

@@ -1 +1 @@
callconv(.c): 42
abi(.c): 42

View File

@@ -53,7 +53,7 @@ objc_msgSend :: (recv: *void, sel: *void) -> *void extern objc;
//
// IMPs (method implementations) are function pointers with the implicit
// Obj-C method shape: `(self: *void, _cmd: *void, ...args) -> ret` with
// `callconv(.c)` so they land args in the standard C registers.
// `abi(.c)` so they land args in the standard C registers.
//
// Method type encoding strings follow Apple's runtime DSL:
// v = void c = char/BOOL i = int l = long f = float d = double
@@ -120,7 +120,7 @@ impl Into(*NSString) for string {
convert :: (self: string) -> *NSString {
cls := objc_getClass("NSString".ptr);
sel := sel_registerName("stringWithUTF8String:".ptr);
msg : (*void, *void, [*]u8) -> *void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, [*]u8) -> *void abi(.c) = xx objc_msgSend;
return xx msg(cls, sel, self.ptr);
}
}

View File

@@ -63,7 +63,7 @@ __sx_block_descriptor : BlockDescriptor = .{
// `$args` is the bound pack of arg types and `$R` is the bound
// return type. The `#insert` evaluates `build_block_convert` at
// comptime and substitutes the resulting source — a nested
// `callconv(.c)` trampoline + the Block literal that points its
// `abi(.c)` trampoline + the Block literal that points its
// `invoke` slot at it. One impl in stdlib replaces every per-
// signature hand-rolled `__block_invoke_*` + `Into(Block)` pair.
impl Into(Block) for Closure(..$args) -> $R {
@@ -77,7 +77,7 @@ impl Into(Block) for Closure(..$args) -> $R {
// (`args`, a comptime `[]Type`) and the closure's return type
// (`$ret`), emits source that:
//
// 1. Declares a nested `__invoke` `callconv(.c)` trampoline whose
// 1. Declares a nested `__invoke` `abi(.c)` trampoline whose
// signature matches the per-shape Apple Block ABI: first arg is
// `block_self: *Block`, then the pack types verbatim. The body
// reconstructs a typed fn-pointer from `block_self.sx_fn`,
@@ -105,7 +105,7 @@ build_block_convert :: (args: []Type, $ret: Type) -> string {
}
code = concat(code, ") -> ");
code = concat(code, ret_name);
code = concat(code, " callconv(.c) { typed_fn : (*void");
code = concat(code, " abi(.c) { typed_fn : (*void");
i = 0;
while i < args.len {
code = concat(code, ", ");

View File

@@ -25,39 +25,39 @@ GL_LINE :u32: 0x1B01;
GL_FILL :u32: 0x1B02;
// Function pointer variables (mutable, loaded at runtime)
glClearColor : (f32, f32, f32, f32) -> void callconv(.c) = ---;
glClear : (u32) -> void callconv(.c) = ---;
glEnable : (u32) -> void callconv(.c) = ---;
glDisable : (u32) -> void callconv(.c) = ---;
glViewport : (i32, i32, i32, i32) -> void callconv(.c) = ---;
glFlush : () -> void callconv(.c) = ---;
glDrawArrays : (u32, i32, i32) -> void callconv(.c) = ---;
glPolygonMode : (u32, u32) -> void callconv(.c) = ---;
glLineWidth : (f32) -> void callconv(.c) = ---;
glCreateShader : (u32) -> u32 callconv(.c) = ---;
glShaderSource : (u32, i32, *[:0]u8, *i32) -> void callconv(.c) = ---;
glCompileShader : (u32) -> void callconv(.c) = ---;
glGetShaderiv : (u32, u32, *i32) -> void callconv(.c) = ---;
glGetShaderInfoLog : (u32, i32, *i32, [*]u8) -> void callconv(.c) = ---;
glCreateProgram : () -> u32 callconv(.c) = ---;
glAttachShader : (u32, u32) -> void callconv(.c) = ---;
glLinkProgram : (u32) -> void callconv(.c) = ---;
glGetProgramiv : (u32, u32, *i32) -> void callconv(.c) = ---;
glGetProgramInfoLog : (u32, i32, *i32, [*]u8) -> void callconv(.c) = ---;
glUseProgram : (u32) -> void callconv(.c) = ---;
glDeleteShader : (u32) -> void callconv(.c) = ---;
glGenVertexArrays : (i32, *u32) -> void callconv(.c) = ---;
glGenBuffers : (i32, *u32) -> void callconv(.c) = ---;
glBindVertexArray : (u32) -> void callconv(.c) = ---;
glBindBuffer : (u32, u32) -> void callconv(.c) = ---;
glBufferData : (u32, isize, *void, u32) -> void callconv(.c) = ---;
glVertexAttribPointer : (u32, i32, u32, u8, i32, *void) -> void callconv(.c) = ---;
glEnableVertexAttribArray : (u32) -> void callconv(.c) = ---;
glGetUniformLocation : (u32, [*]u8) -> i32 callconv(.c) = ---;
glUniformMatrix4fv : (i32, i32, u8, [*]f32) -> void callconv(.c) = ---;
glUniform3f : (i32, f32, f32, f32) -> void callconv(.c) = ---;
glDepthFunc : (u32) -> void callconv(.c) = ---;
glUniform1f : (i32, f32) -> void callconv(.c) = ---;
glClearColor : (f32, f32, f32, f32) -> void abi(.c) = ---;
glClear : (u32) -> void abi(.c) = ---;
glEnable : (u32) -> void abi(.c) = ---;
glDisable : (u32) -> void abi(.c) = ---;
glViewport : (i32, i32, i32, i32) -> void abi(.c) = ---;
glFlush : () -> void abi(.c) = ---;
glDrawArrays : (u32, i32, i32) -> void abi(.c) = ---;
glPolygonMode : (u32, u32) -> void abi(.c) = ---;
glLineWidth : (f32) -> void abi(.c) = ---;
glCreateShader : (u32) -> u32 abi(.c) = ---;
glShaderSource : (u32, i32, *[:0]u8, *i32) -> void abi(.c) = ---;
glCompileShader : (u32) -> void abi(.c) = ---;
glGetShaderiv : (u32, u32, *i32) -> void abi(.c) = ---;
glGetShaderInfoLog : (u32, i32, *i32, [*]u8) -> void abi(.c) = ---;
glCreateProgram : () -> u32 abi(.c) = ---;
glAttachShader : (u32, u32) -> void abi(.c) = ---;
glLinkProgram : (u32) -> void abi(.c) = ---;
glGetProgramiv : (u32, u32, *i32) -> void abi(.c) = ---;
glGetProgramInfoLog : (u32, i32, *i32, [*]u8) -> void abi(.c) = ---;
glUseProgram : (u32) -> void abi(.c) = ---;
glDeleteShader : (u32) -> void abi(.c) = ---;
glGenVertexArrays : (i32, *u32) -> void abi(.c) = ---;
glGenBuffers : (i32, *u32) -> void abi(.c) = ---;
glBindVertexArray : (u32) -> void abi(.c) = ---;
glBindBuffer : (u32, u32) -> void abi(.c) = ---;
glBufferData : (u32, isize, *void, u32) -> void abi(.c) = ---;
glVertexAttribPointer : (u32, i32, u32, u8, i32, *void) -> void abi(.c) = ---;
glEnableVertexAttribArray : (u32) -> void abi(.c) = ---;
glGetUniformLocation : (u32, [*]u8) -> i32 abi(.c) = ---;
glUniformMatrix4fv : (i32, i32, u8, [*]f32) -> void abi(.c) = ---;
glUniform3f : (i32, f32, f32, f32) -> void abi(.c) = ---;
glDepthFunc : (u32) -> void abi(.c) = ---;
glUniform1f : (i32, f32) -> void abi(.c) = ---;
GL_LESS :u32: 0x0201;
GL_LEQUAL :u32: 0x0203;
GL_SCISSOR_TEST :u32: 0x0C11;
@@ -76,27 +76,27 @@ GL_RED :u32: 0x1903;
GL_R8 :u32: 0x8229;
GL_UNPACK_ALIGNMENT :u32: 0x0CF5;
glScissor : (i32, i32, i32, i32) -> void callconv(.c) = ---;
glBufferSubData : (u32, isize, isize, *void) -> void callconv(.c) = ---;
glGenTextures : (i32, *u32) -> void callconv(.c) = ---;
glBindTexture : (u32, u32) -> void callconv(.c) = ---;
glTexImage2D : (u32, i32, i32, i32, i32, i32, u32, u32, *void) -> void callconv(.c) = ---;
glTexParameteri : (u32, u32, i32) -> void callconv(.c) = ---;
glBlendFunc : (u32, u32) -> void callconv(.c) = ---;
glReadPixels : (i32, i32, i32, i32, u32, u32, *void) -> void callconv(.c) = ---;
glActiveTexture : (u32) -> void callconv(.c) = ---;
glUniform1i : (i32, i32) -> void callconv(.c) = ---;
glPixelStorei : (u32, i32) -> void callconv(.c) = ---;
glTexSubImage2D : (u32, i32, i32, i32, i32, i32, u32, u32, *void) -> void callconv(.c) = ---;
glDeleteTextures : (i32, *u32) -> void callconv(.c) = ---;
glScissor : (i32, i32, i32, i32) -> void abi(.c) = ---;
glBufferSubData : (u32, isize, isize, *void) -> void abi(.c) = ---;
glGenTextures : (i32, *u32) -> void abi(.c) = ---;
glBindTexture : (u32, u32) -> void abi(.c) = ---;
glTexImage2D : (u32, i32, i32, i32, i32, i32, u32, u32, *void) -> void abi(.c) = ---;
glTexParameteri : (u32, u32, i32) -> void abi(.c) = ---;
glBlendFunc : (u32, u32) -> void abi(.c) = ---;
glReadPixels : (i32, i32, i32, i32, u32, u32, *void) -> void abi(.c) = ---;
glActiveTexture : (u32) -> void abi(.c) = ---;
glUniform1i : (i32, i32) -> void abi(.c) = ---;
glPixelStorei : (u32, i32) -> void abi(.c) = ---;
glTexSubImage2D : (u32, i32, i32, i32, i32, i32, u32, u32, *void) -> void abi(.c) = ---;
glDeleteTextures : (i32, *u32) -> void abi(.c) = ---;
glGenFramebuffers : (i32, *u32) -> void callconv(.c) = ---;
glGenRenderbuffers : (i32, *u32) -> void callconv(.c) = ---;
glBindFramebuffer : (u32, u32) -> void callconv(.c) = ---;
glBindRenderbuffer : (u32, u32) -> void callconv(.c) = ---;
glFramebufferRenderbuffer : (u32, u32, u32, u32) -> void callconv(.c) = ---;
glGetRenderbufferParameteriv : (u32, u32, *i32) -> void callconv(.c) = ---;
glCheckFramebufferStatus : (u32) -> u32 callconv(.c) = ---;
glGenFramebuffers : (i32, *u32) -> void abi(.c) = ---;
glGenRenderbuffers : (i32, *u32) -> void abi(.c) = ---;
glBindFramebuffer : (u32, u32) -> void abi(.c) = ---;
glBindRenderbuffer : (u32, u32) -> void abi(.c) = ---;
glFramebufferRenderbuffer : (u32, u32, u32, u32) -> void abi(.c) = ---;
glGetRenderbufferParameteriv : (u32, u32, *i32) -> void abi(.c) = ---;
glCheckFramebufferStatus : (u32) -> u32 abi(.c) = ---;
GL_TEXTURE_WRAP_S :u32: 0x2802;
GL_TEXTURE_WRAP_T :u32: 0x2803;
@@ -104,7 +104,7 @@ GL_CLAMP_TO_EDGE :u32: 0x812F;
// Loader: call once after creating GL context
// Pass in a proc loader (e.g. SDL_GL_GetProcAddress)
load_gl :: (get_proc: ([*]u8) -> *void callconv(.c)) {
load_gl :: (get_proc: ([*]u8) -> *void abi(.c)) {
glClearColor = xx get_proc("glClearColor");
glClear = xx get_proc("glClear");
glEnable = xx get_proc("glEnable");

View File

@@ -58,7 +58,7 @@ MTLClearColor :: struct {
// MTLRegion is 48 bytes and MTLScissorRect is 32 bytes; both are passed
// by value to the Obj-C runtime, which the compiler marshals as
// `ptr byval(<T>)` via the C-ABI byval coercion. The fn-pointer cast
// must spell `callconv(.c)` so the indirect call applies that coercion.
// must spell `abi(.c)` so the indirect call applies that coercion.
MTLOrigin :: struct { x: u64; y: u64; z: u64; }
MTLSize :: struct { width: u64; height: u64; depth: u64; }
MTLRegion :: struct { origin: MTLOrigin; size: MTLSize; }
@@ -257,11 +257,11 @@ metal_init_ios :: (self: *MetalGPU) -> bool {
if self.device == null { return false; }
}
msg_oo : (*void, *void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void callconv(.c) = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void callconv(.c) = xx objc_msgSend;
msg_osize : (*void, *void, CGSize) -> void callconv(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void abi(.c) = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void abi(.c) = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void abi(.c) = xx objc_msgSend;
msg_osize : (*void, *void, CGSize) -> void abi(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void abi(.c) = xx objc_msgSend;
if self.queue == null {
self.queue = msg_o(self.device, sel_registerName("newCommandQueue".ptr));
@@ -290,7 +290,7 @@ metal_resize_ios :: (self: *MetalGPU) {
inline if OS != .ios { return; }
if self.layer == null { return; }
msg_osize : (*void, *void, CGSize) -> void callconv(.c) = xx objc_msgSend;
msg_osize : (*void, *void, CGSize) -> void abi(.c) = xx objc_msgSend;
size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h };
msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size);
}
@@ -301,12 +301,12 @@ metal_begin_frame_ios :: (self: *MetalGPU, clear: ClearColor) -> bool {
if self.queue == null { return false; }
if self.pixel_w <= 0 or self.pixel_h <= 0 { return false; }
msg_o : (*void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_oo_ret : (*void, *void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void callconv(.c) = xx objc_msgSend;
msg_ouret : (*void, *void, u64) -> *void callconv(.c) = xx objc_msgSend;
msg_oclear : (*void, *void, MTLClearColor) -> void callconv(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void abi(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void abi(.c) = xx objc_msgSend;
msg_oo_ret : (*void, *void, *void) -> *void abi(.c) = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void abi(.c) = xx objc_msgSend;
msg_ouret : (*void, *void, u64) -> *void abi(.c) = xx objc_msgSend;
msg_oclear : (*void, *void, MTLClearColor) -> void abi(.c) = xx objc_msgSend;
// drawable = [layer nextDrawable]
self.drawable = msg_o(self.layer, sel_registerName("nextDrawable".ptr));
@@ -353,9 +353,9 @@ metal_end_frame_ios :: (self: *MetalGPU, target_time: f64) {
if self.cmd_buffer == null { return; }
if self.drawable == null { return; }
msg_v : (*void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_ood : (*void, *void, *void, f64) -> void callconv(.c) = xx objc_msgSend;
msg_v : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void abi(.c) = xx objc_msgSend;
msg_ood : (*void, *void, *void, f64) -> void abi(.c) = xx objc_msgSend;
msg_v(self.encoder, sel_registerName("endEncoding".ptr));
@@ -386,15 +386,15 @@ metal_create_shader_ios :: (self: *MetalGPU, src: string) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
msg_o : (*void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void callconv(.c) = xx objc_msgSend;
msg_oo_r : (*void, *void, *NSString) -> *void callconv(.c) = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void callconv(.c) = xx objc_msgSend;
msg_ouret: (*void, *void, u64) -> *void callconv(.c) = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void callconv(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void abi(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void abi(.c) = xx objc_msgSend;
msg_oo_r : (*void, *void, *NSString) -> *void abi(.c) = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void abi(.c) = xx objc_msgSend;
msg_ouret: (*void, *void, u64) -> *void abi(.c) = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void abi(.c) = xx objc_msgSend;
// [device newLibraryWithSource:src options:nil error:&err]
msg_lib : (*void, *void, *NSString, *void, **void) -> *void callconv(.c) = xx objc_msgSend;
msg_lib : (*void, *void, *NSString, *void, **void) -> *void abi(.c) = xx objc_msgSend;
err : *void = null;
library := msg_lib(self.device,
sel_registerName("newLibraryWithSource:options:error:".ptr),
@@ -428,7 +428,7 @@ metal_create_shader_ios :: (self: *MetalGPU, src: string) -> u32 {
msg_ou(att0, sel_registerName("setSourceAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA);
msg_ou(att0, sel_registerName("setDestinationAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A);
msg_pipe : (*void, *void, *void, **void) -> *void callconv(.c) = xx objc_msgSend;
msg_pipe : (*void, *void, *void, **void) -> *void abi(.c) = xx objc_msgSend;
err2 : *void = null;
state := msg_pipe(self.device,
sel_registerName("newRenderPipelineStateWithDescriptor:error:".ptr),
@@ -452,7 +452,7 @@ metal_create_buffer_ios :: (self: *MetalGPU, size_bytes: i64) -> u32 {
if size_bytes <= 0 { return 0; }
// MTLResourceStorageModeShared is the default (option value 0).
msg_buf : (*void, *void, u64, u64) -> *void callconv(.c) = xx objc_msgSend;
msg_buf : (*void, *void, u64, u64) -> *void abi(.c) = xx objc_msgSend;
buf := msg_buf(self.device,
sel_registerName("newBufferWithLength:options:".ptr),
xx size_bytes, 0);
@@ -469,7 +469,7 @@ metal_update_buffer_ios :: (self: *MetalGPU, handle: u32, data: *void, size_byte
if data == null { return; }
if size_bytes <= 0 { return; }
msg_o : (*void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void abi(.c) = xx objc_msgSend;
dst := msg_o(buf, sel_registerName("contents".ptr));
if dst == null { return; }
memcpy(dst, data, size_bytes);
@@ -483,7 +483,7 @@ metal_update_buffer_at_ios :: (self: *MetalGPU, handle: u32, data: *void, size_b
if size_bytes <= 0 { return; }
if byte_offset < 0 { return; }
msg_o : (*void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_o : (*void, *void) -> *void abi(.c) = xx objc_msgSend;
base := msg_o(buf, sel_registerName("contents".ptr));
if base == null { return; }
// Add byte_offset via integer arithmetic — `@dst[i]` on `[*]u8`
@@ -530,7 +530,7 @@ metal_create_texture_ios :: (self: *MetalGPU, w: i32, h: i32, format: TextureFor
// [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:width:height:mipmapped:]
MTLTextureDescriptor := objc_getClass("MTLTextureDescriptor".ptr);
msg_desc : (*void, *void, u64, u64, u64, u8) -> *void callconv(.c) = xx objc_msgSend;
msg_desc : (*void, *void, u64, u64, u64, u8) -> *void abi(.c) = xx objc_msgSend;
desc := msg_desc(MTLTextureDescriptor,
sel_registerName("texture2DDescriptorWithPixelFormat:width:height:mipmapped:".ptr),
pixel_format, xx w, xx h, 0);
@@ -539,10 +539,10 @@ metal_create_texture_ios :: (self: *MetalGPU, w: i32, h: i32, format: TextureFor
// Force shared storage so the CPU can keep writing pixels (atlas updates,
// sprite uploads). On iOS-sim under Apple Silicon the convenience class
// method's default storage isn't reliably shared for every format.
msg_ou_void : (*void, *void, u64) -> void callconv(.c) = xx objc_msgSend;
msg_ou_void : (*void, *void, u64) -> void abi(.c) = xx objc_msgSend;
msg_ou_void(desc, sel_registerName("setStorageMode:".ptr), MTL_STORAGE_MODE_SHARED);
msg_oo : (*void, *void, *void) -> *void callconv(.c) = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> *void abi(.c) = xx objc_msgSend;
tex := msg_oo(self.device, sel_registerName("newTextureWithDescriptor:".ptr), desc);
if tex == null { return 0; }
@@ -575,7 +575,7 @@ metal_update_texture_region_ios :: (self: *MetalGPU, handle: u32, x: i32, y: i32
bytes_per_row : u64 = xx (slot.bytes_per_pixel * cast(u32) w);
// [tex replaceRegion:region mipmapLevel:0 withBytes:pixels bytesPerRow:bytes_per_row]
msg_replace : (*void, *void, MTLRegion, u64, *void, u64) -> void callconv(.c) = xx objc_msgSend;
msg_replace : (*void, *void, MTLRegion, u64, *void, u64) -> void abi(.c) = xx objc_msgSend;
msg_replace(slot.tex,
sel_registerName("replaceRegion:mipmapLevel:withBytes:bytesPerRow:".ptr),
region, 0, pixels, bytes_per_row);
@@ -596,7 +596,7 @@ metal_destroy_shader_ios :: (self: *MetalGPU, handle: u32) {
if h64 > self.shaders.len { return; }
obj := self.shaders.items[handle - 1];
if obj == null { return; }
msg : (*void, *void) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg(obj, sel_registerName("release".ptr));
self.shaders.items[handle - 1] = null;
}
@@ -608,7 +608,7 @@ metal_destroy_buffer_ios :: (self: *MetalGPU, handle: u32) {
if h64 > self.buffers.len { return; }
obj := self.buffers.items[handle - 1];
if obj == null { return; }
msg : (*void, *void) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg(obj, sel_registerName("release".ptr));
self.buffers.items[handle - 1] = null;
}
@@ -620,7 +620,7 @@ metal_destroy_texture_ios :: (self: *MetalGPU, handle: u32) {
if h64 > self.textures.len { return; }
obj := self.textures.items[handle - 1].tex;
if obj == null { return; }
msg : (*void, *void) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg(obj, sel_registerName("release".ptr));
self.textures.items[handle - 1].tex = null;
self.textures.items[handle - 1].bytes_per_pixel = 0;
@@ -633,7 +633,7 @@ metal_set_shader_ios :: (self: *MetalGPU, sh: u32) {
if self.encoder == null { return; }
state := metal_lookup_shader(self, sh);
if state == null { return; }
msg : (*void, *void, *void) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, *void) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("setRenderPipelineState:".ptr), state);
}
@@ -643,7 +643,7 @@ metal_set_vertex_buffer_ios :: (self: *MetalGPU, h: u32) {
buf := metal_lookup_buffer(self, h);
if buf == null { return; }
// [encoder setVertexBuffer:buf offset:0 atIndex:0]
msg : (*void, *void, *void, u64, u64) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, *void, u64, u64) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("setVertexBuffer:offset:atIndex:".ptr), buf, 0, 0);
}
@@ -656,7 +656,7 @@ metal_set_texture_ios :: (self: *MetalGPU, slot: u32, h: u32) {
tex := self.textures.items[h - 1].tex;
if tex == null { return; }
// [encoder setFragmentTexture:tex atIndex:slot]
msg : (*void, *void, *void, u64) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, *void, u64) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("setFragmentTexture:atIndex:".ptr), tex, xx slot);
}
@@ -666,7 +666,7 @@ metal_set_vertex_constants_ios :: (self: *MetalGPU, slot: u32, data: *void, size
if data == null { return; }
if size_bytes <= 0 { return; }
// [encoder setVertexBytes:data length:size_bytes atIndex:slot]
msg : (*void, *void, *void, u64, u64) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, *void, u64, u64) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("setVertexBytes:length:atIndex:".ptr),
data, xx size_bytes, xx slot);
}
@@ -676,7 +676,7 @@ metal_set_scissor_ios :: (self: *MetalGPU, x: i32, y: i32, w: i32, h: i32) {
if self.encoder == null { return; }
rect : MTLScissorRect = .{ x = xx x, y = xx y, width = xx w, height = xx h };
// [encoder setScissorRect:rect] (MTLScissorRect is 32 bytes → ptr byval)
msg : (*void, *void, MTLScissorRect) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, MTLScissorRect) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("setScissorRect:".ptr), rect);
}
@@ -686,7 +686,7 @@ metal_disable_scissor_ios :: (self: *MetalGPU) {
// Metal has no "disable scissor" — set the rect to cover the full
// drawable so subsequent draws aren't clipped.
rect : MTLScissorRect = .{ x = 0, y = 0, width = xx self.pixel_w, height = xx self.pixel_h };
msg : (*void, *void, MTLScissorRect) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, MTLScissorRect) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("setScissorRect:".ptr), rect);
}
@@ -695,7 +695,7 @@ metal_draw_triangles_ios :: (self: *MetalGPU, vertex_offset: i32, vertex_count:
if self.encoder == null { return; }
if vertex_count <= 0 { return; }
// [encoder drawPrimitives:.triangle vertexStart:offset vertexCount:count]
msg : (*void, *void, u64, u64, u64) -> void callconv(.c) = xx objc_msgSend;
msg : (*void, *void, u64, u64, u64) -> void abi(.c) = xx objc_msgSend;
msg(self.encoder, sel_registerName("drawPrimitives:vertexStart:vertexCount:".ptr),
MTL_PRIMITIVE_TYPE_TRIANGLE, xx vertex_offset, xx vertex_count);
}

View File

@@ -85,7 +85,7 @@ ANativeWindow_setBuffersGeometry :: (w: *void, width: i32, height: i32, fmt: i32
AAssetManager_fromJava :: (env: *void, mgr: *void) -> *void extern;
// pthread (link libpthread is built into bionic).
pthread_create :: (thread: *u64, attr: *void, start: (*void) -> *void callconv(.c), arg: *void) -> i32 extern;
pthread_create :: (thread: *u64, attr: *void, start: (*void) -> *void abi(.c), arg: *void) -> i32 extern;
pthread_mutex_init :: (m: *void, attr: *void) -> i32 extern;
pthread_mutex_lock :: (m: *void) -> i32 extern;
pthread_mutex_unlock :: (m: *void) -> i32 extern;
@@ -254,7 +254,7 @@ sx_android_start_render_thread :: (plat: *AndroidPlatform, entry_fn: () -> void)
plat.render_thread_started = true;
}
sx_android_render_thread_entry :: (arg: *void) -> *void callconv(.c) {
sx_android_render_thread_entry :: (arg: *void) -> *void abi(.c) {
plat : *AndroidPlatform = xx arg;
while plat.app_window == null and !plat.should_stop {
usleep(1000);

View File

@@ -229,7 +229,7 @@ impl Platform for SdlPlatform {
// SDL fires the watch synchronously when events are added — including during
// macOS's modal resize-drag, when SDL_PollEvent can't run. Re-invoking the
// frame closure here keeps content rendering at the new size during the drag.
sdl_event_watch :: (userdata: *void, event: *SDL_Event) -> bool callconv(.c) {
sdl_event_watch :: (userdata: *void, event: *SDL_Event) -> bool abi(.c) {
plat : *SdlPlatform = xx userdata;
if event.* == {
case .window_resized: (data) {
@@ -245,7 +245,7 @@ sdl_event_watch :: (userdata: *void, event: *SDL_Event) -> bool callconv(.c) {
true
}
sdl_wasm_tick :: () callconv(.c) {
sdl_wasm_tick :: () abi(.c) {
if g_sdl_plat == null { return; }
if !g_sdl_plat.has_frame_closure { return; }
fn := g_sdl_plat.frame_closure;

View File

@@ -790,8 +790,8 @@ impl Platform for UIKitPlatform {
}
// dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2.
// callconv(.c) so this is callable from `load_gl`'s C-conv proc-loader slot.
ios_gl_proc :: (name: [*]u8) -> *void callconv(.c) {
// abi(.c) so this is callable from `load_gl`'s C-conv proc-loader slot.
ios_gl_proc :: (name: [*]u8) -> *void abi(.c) {
rtld_default : *void = xx (0 - 2);
dlsym(rtld_default, name)
}

View File

@@ -2,7 +2,7 @@
// pthreads (PLAN-HTTPZ S6).
//
// THE RE-ENTRY CONTRACT (pinned by examples/1636): a thread entry is a
// `callconv(.c)` function — it has NO implicit context — and enters
// `abi(.c)` function — it has NO implicit context — and enters
// the sx world by fabricating one: `push Context.{ allocator = xx gpa }`
// around the default-conv code it runs. Pool workers do exactly that,
// each with its own malloc-backed GPA, so tasks allocate freely and
@@ -26,7 +26,7 @@
tlib :: #library "c";
pthread_create :: (thread: *usize, attr: *void, start: (*void) -> *void callconv(.c), arg: *void) -> i32 extern tlib;
pthread_create :: (thread: *usize, attr: *void, start: (*void) -> *void abi(.c), arg: *void) -> i32 extern tlib;
pthread_join :: (thread: usize, retval: **void) -> i32 extern tlib;
pthread_detach :: (thread: usize) -> i32 extern tlib;
@@ -104,9 +104,9 @@ Cond :: struct {
Thread :: struct {
handle: usize = 0;
// `entry` is the C->sx boundary: callconv(.c), fabricates its own
// `entry` is the C->sx boundary: abi(.c), fabricates its own
// Context before touching default-conv sx code (examples/1636).
spawn :: (entry: (*void) -> *void callconv(.c), arg: *void) -> (Thread, !ThreadErr) {
spawn :: (entry: (*void) -> *void abi(.c), arg: *void) -> (Thread, !ThreadErr) {
t : Thread = .{};
if pthread_create(@t.handle, null, entry, arg) != 0 { raise error.Spawn; }
return t;
@@ -200,7 +200,7 @@ Pool :: struct {
// The worker loop: C entry, own fabricated Context, then
// pop-task/run-task until stop with an empty queue.
pool_worker :: (arg: *void) -> *void callconv(.c) {
pool_worker :: (arg: *void) -> *void abi(.c) {
p : *Pool = xx arg;
gpa := GPA.init();
push Context.{ allocator = xx gpa } {

View File

@@ -427,7 +427,7 @@ write_fd :: (fd: i32, buf: [*]u8, count: u64) -> i64 extern libc "write";
`extern` / `export` are the keyword surface for C linkage. `extern` imports a
symbol defined elsewhere; `export` is its dual — define a function in sx and
expose it under the C ABI so C can call back in. Both imply `callconv(.c)` and take
expose it under the C ABI so C can call back in. Both imply `abi(.c)` and take
the same optional `[LIB] ["csym"]` rename tail; they also apply to data globals and
to Obj-C / JNI runtime-class aggregates (postfix after the `#objc_class(…)` directive).
```sx

View File

@@ -1194,8 +1194,9 @@ C linkage is expressed with the postfix `extern` (import) and `export` (define +
expose) keywords. `extern` declares a symbol defined elsewhere — a C function or
data global resolved at link time; `export` is its dual — **define** a symbol in
sx and expose it under the C ABI so C (or asm, or another language) can call it.
Both imply `callconv(.c)`, carry external linkage, and suppress the implicit sx
context parameter. They are postfix modifiers, written where `callconv` would go.
Both imply `abi(.c)`, carry external linkage, and suppress the implicit sx
context parameter. They are postfix modifiers, written in the slot after the
`abi(...)` annotation.
```sx
// Declare a named library constant

View File

@@ -122,14 +122,26 @@ pub const Root = struct {
decls: []const *Node,
};
pub const CallingConvention = enum { default, c };
/// ABI / calling-convention annotation written as the postfix `abi(.x)` form on a
/// function declaration, function-type literal, or lambda. Subsumes the old
/// `callconv(...)` spelling.
/// - `.default` — no annotation: the ordinary sx-internal convention (implicit
/// context, sx ABI). There is no surface spelling for `.default`; it is the
/// value when `abi(...)` is absent.
/// - `.c` — C ABI / cdecl, no implicit context (what `callconv(.c)` meant).
/// - `.zig` — welded to the real internal Zig type/fn: layout follows the bound
/// Zig type, functions dispatch over the comptime host-call bridge. The
/// `compiler` library (`design/comptime-compiler-api.md`) binds via `abi(.zig)`.
/// - `.pure` — a pure / naked function (inline asm body), no calling-convention
/// prologue/epilogue.
pub const ABI = enum { default, c, zig, pure };
/// Linkage modifier written in the postfix slot after `callconv(...)`:
/// `name :: (sig) -> Ret [callconv(.x)] [extern | export] [;|{…}];`
/// `extern` = import (external linkage, C ABI, no sx ctx — `extern`'s role);
/// `export` = define + expose (body + external linkage + C ABI + no ctx).
/// Both imply `callconv(.c)`. Variants carry a trailing `_` to dodge the Zig
/// keywords. `.none` = no linkage modifier (the ordinary sx-internal decl).
/// Linkage modifier written in the postfix slot before `abi(...)`:
/// `name :: (sig) -> Ret [extern | export] [abi(.x)] [lib] [;|{…}];`
/// `extern` = import (external linkage, no sx ctx — `extern`'s role);
/// `export` = define + expose (body + external linkage + no ctx).
/// Variants carry a trailing `_` to dodge the Zig keywords. `.none` = no linkage
/// modifier (the ordinary sx-internal decl).
pub const ExternExportModifier = enum { none, extern_, export_ };
pub const FnDecl = struct {
@@ -139,10 +151,15 @@ pub const FnDecl = struct {
body: *Node,
type_params: []const StructTypeParam = &.{},
is_arrow: bool = false,
call_conv: CallingConvention = .default,
/// Postfix linkage modifier (`extern`/`export`) written after the
/// `callconv(...)` slot. `.none` for an ordinary sx-internal function.
/// Parsed in Phase 0.1; not consumed by the fn-decl path until Phase 1.
/// ABI / calling-convention annotation (`abi(.c)` / `abi(.zig)` / `abi(.pure)`)
/// in the postfix slot after `extern`/`export`. `.default` = unannotated.
/// `.zig` marks a function bound to the comptime `compiler` library — its
/// signature is welded to the real internal Zig fn and it dispatches over the
/// host-call bridge at comptime (consumed by the binding registry + host-call
/// bridge in later phases).
abi: ABI = .default,
/// Postfix linkage modifier (`extern`/`export`) written before the `abi(...)`
/// slot. `.none` for an ordinary sx-internal function.
extern_export: ExternExportModifier = .none,
/// Optional library reference + symbol-name override for an `extern`/`export`
/// function, the optional library + symbol-name override. Both
@@ -510,6 +527,15 @@ pub const StructDecl = struct {
using_entries: []const UsingEntry = &.{},
methods: []const *Node = &.{}, // fn_decl nodes for struct methods
constants: []const *Node = &.{}, // const_decl nodes for struct-level constants
/// ABI / layout annotation (`struct abi(.zig) extern <lib> { … }`). `.default`
/// for an ordinary struct. `.zig` marks a layout-welded binding to the named
/// `compiler` library's real Zig type — its field offsets are taken from the
/// bound Zig type (`@offsetOf`) and asserted equal at compiler-build time.
/// Parsed in Phase 1; consumed by the binding registry + layout engine later.
abi: ABI = .default,
/// The bound library handle for an `abi(.zig) extern <lib>` welded struct
/// (e.g. `compiler`); null for an ordinary struct.
extern_lib: ?[]const u8 = null,
/// True when the declared NAME was a backtick raw identifier
/// (`` `i2 :: struct { … } ``) — exempt from the reserved-type-name decl
/// check. A bare reserved-name decl still errors.
@@ -533,7 +559,7 @@ pub const Lambda = struct {
return_type: ?*Node,
body: *Node,
type_params: []const StructTypeParam = &.{},
call_conv: CallingConvention = .default,
abi: ABI = .default,
};
pub const TypeExpr = struct {
@@ -805,7 +831,7 @@ pub const FunctionTypeExpr = struct {
param_types: []const *Node,
param_names: ?[]const ?[]const u8 = null, // optional documentation names
return_type: ?*Node, // null = void return
call_conv: CallingConvention = .default,
abi: ABI = .default,
};
pub const ClosureTypeExpr = struct {

View File

@@ -29,12 +29,12 @@ pub const AbiLowering = struct {
/// `is_extern_c_api` knob. When true, sx `string` / `[]T` slices
/// collapse to `ptr` — the libc convention where the user writes
/// `string` to mean `char *` and the length is dropped. When
/// false (sx-internal `callconv(.c)` like block trampolines), the
/// false (sx-internal `abi(.c)` like block trampolines), the
/// full slice shape is preserved and goes through the general
/// struct-coerce path (16-byte slice → `[2 x i64]`, lands in two
/// registers on AArch64 — the true C ABI for a 16-byte
/// aggregate). Without the split, sx-to-sx calls through a
/// `(*Block, string) -> void callconv(.c)` fn-pointer mismatched
/// `(*Block, string) -> void abi(.c)` fn-pointer mismatched
/// the caller's `{ptr, i64}` value against the trampoline's
/// collapsed `ptr` param.
pub fn abiCoerceParamTypeEx(self: AbiLowering, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef, is_extern_c_api: bool) c.LLVMTypeRef {

View File

@@ -1118,6 +1118,23 @@ pub const Ops = struct {
pub fn emitCall(self: Ops, instruction: *const Inst, call_op: Call) void {
// Evaluate comptime functions at compile time
const callee_func = &self.e.ir_mod.functions.items[call_op.callee.index()];
// Welded `compiler`-library functions are comptime-only — they have no
// runtime symbol (the comptime interp dispatches them to a Zig handler).
// A welded call inside a RUNTIME function is illegal; surface a clean
// build-gating error instead of an undefined-symbol link failure. A
// welded call inside a COMPTIME function (a `#run` / `::` initializer
// wrapper, `is_comptime`) is fine — that body is interp-evaluated and its
// LLVM emission is dead, so skip the gate there.
const enclosing = &self.e.ir_mod.functions.items[self.e.current_func_idx];
if (callee_func.compiler_welded and !enclosing.is_comptime) {
const fname = self.e.ir_mod.types.getString(callee_func.name);
std.debug.print("error: '{s}' is a comptime-only compiler-library function — it cannot be called at runtime (use it inside #run or a comptime '::')\n", .{fname});
self.e.comptime_failed = true;
self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty)));
return;
}
if (callee_func.is_comptime and call_op.args.len == 0) {
var interp_inst = Interpreter.init(self.e.ir_mod, self.e.alloc);
interp_inst.build_config = &self.e.build_config;

View File

@@ -0,0 +1,177 @@
// Tests for the comptime `compiler` library's binding registry.
const std = @import("std");
const compiler_lib = @import("compiler_lib.zig");
const types = @import("types.zig");
// Lock: `findType("Field")` resolves to the welded `StructInfo.Field` type, and
// its baked layout EQUALS the real Zig type's `@sizeOf`/`@alignOf`/`@offsetOf`.
// This is the foundation the layout sub-step builds on — the welded record's
// offsets come from the implementation, so they can't drift.
test "compiler_lib: Field welds to StructInfo.Field's real layout" {
const FieldZig = types.TypeInfo.StructInfo.Field;
const bt = compiler_lib.findType("Field") orelse return error.MissingBoundType;
try std.testing.expectEqualStrings("Field", bt.sx_name);
try std.testing.expectEqual(@sizeOf(FieldZig), bt.size);
try std.testing.expectEqual(@alignOf(FieldZig), bt.alignment);
// Two u32 fields, in declaration order.
try std.testing.expectEqual(@as(usize, 2), bt.fields.len);
try std.testing.expectEqualStrings("name", bt.fields[0].name);
try std.testing.expectEqual(@offsetOf(FieldZig, "name"), bt.fields[0].offset);
try std.testing.expectEqual(@as(usize, 4), bt.fields[0].size);
try std.testing.expectEqualStrings("ty", bt.fields[1].name);
try std.testing.expectEqual(@offsetOf(FieldZig, "ty"), bt.fields[1].offset);
try std.testing.expectEqual(@as(usize, 4), bt.fields[1].size);
// Sanity: the concrete shape the design calls out — two u32s, 8 bytes.
try std.testing.expectEqual(@as(usize, 8), bt.size);
try std.testing.expectEqual(@as(usize, 0), bt.fields[0].offset);
try std.testing.expectEqual(@as(usize, 4), bt.fields[1].offset);
}
// Lock: a name NOT on the export list is unreachable — `findType` returns null
// (the safety boundary; the welded-decl path falls through to a clean error,
// never a silent default).
test "compiler_lib: unexported name returns null" {
try std.testing.expect(compiler_lib.findType("NotExported") == null);
try std.testing.expect(compiler_lib.findType("") == null);
}
// Lock: a faithful sx header for `Field` validates clean (the natural two-u32
// layout matches the welded type).
test "compiler_lib: validateStructLayout accepts a faithful Field header" {
const bt = compiler_lib.findType("Field").?;
const sx = [_]compiler_lib.SxField{
.{ .name = "name", .size = 4 },
.{ .name = "ty", .size = 4 },
};
try std.testing.expect(compiler_lib.validateStructLayout(bt, &sx, 8) == null);
}
// Lock: every drift the assertion is meant to catch surfaces as the right
// `LayoutMismatch` variant (field count / name / size / total), and the first
// mismatch wins.
test "compiler_lib: validateStructLayout flags each kind of drift" {
const bt = compiler_lib.findType("Field").?;
// Wrong field count (one field instead of two).
{
const sx = [_]compiler_lib.SxField{.{ .name = "name", .size = 4 }};
const m = compiler_lib.validateStructLayout(bt, &sx, 4).?;
try std.testing.expect(m == .field_count);
try std.testing.expectEqual(@as(usize, 2), m.field_count.expected);
try std.testing.expectEqual(@as(usize, 1), m.field_count.got);
}
// Wrong field name (reorder / rename) at index 1.
{
const sx = [_]compiler_lib.SxField{
.{ .name = "name", .size = 4 },
.{ .name = "kind", .size = 4 },
};
const m = compiler_lib.validateStructLayout(bt, &sx, 8).?;
try std.testing.expect(m == .field_name);
try std.testing.expectEqual(@as(usize, 1), m.field_name.index);
try std.testing.expectEqualStrings("ty", m.field_name.expected);
try std.testing.expectEqualStrings("kind", m.field_name.got);
}
// Wrong field size (retype to an 8-byte field).
{
const sx = [_]compiler_lib.SxField{
.{ .name = "name", .size = 4 },
.{ .name = "ty", .size = 8 },
};
const m = compiler_lib.validateStructLayout(bt, &sx, 12).?;
try std.testing.expect(m == .field_size);
try std.testing.expectEqual(@as(usize, 1), m.field_size.index);
try std.testing.expectEqual(@as(usize, 4), m.field_size.expected);
try std.testing.expectEqual(@as(usize, 8), m.field_size.got);
}
// Right fields, wrong total (padding drift).
{
const sx = [_]compiler_lib.SxField{
.{ .name = "name", .size = 4 },
.{ .name = "ty", .size = 4 },
};
const m = compiler_lib.validateStructLayout(bt, &sx, 16).?;
try std.testing.expect(m == .total_size);
try std.testing.expectEqual(@as(usize, 8), m.total_size.expected);
try std.testing.expectEqual(@as(usize, 16), m.total_size.got);
}
}
// Lock: `Field` (natural two-u32 layout) has the trivial weld plan — two field
// elements in declaration order, no padding, identity remap.
test "compiler_lib: weld plan for Field is the identity (no reorder, no pad)" {
const alloc = std.testing.allocator;
const bt = compiler_lib.findType("Field").?;
var plan = try compiler_lib.computeWeldPlan(alloc, bt.fields, bt.size);
defer plan.deinit(alloc);
try std.testing.expectEqual(@as(usize, 8), plan.total_size);
try std.testing.expectEqual(@as(usize, 2), plan.elements.len);
// Identity remap.
try std.testing.expectEqual(@as(usize, 0), plan.sx_to_llvm[0]);
try std.testing.expectEqual(@as(usize, 1), plan.sx_to_llvm[1]);
// Both elements are real fields at 0 and 4.
try std.testing.expectEqual(@as(?usize, 0), plan.elements[0].sx_field);
try std.testing.expectEqual(@as(usize, 0), plan.elements[0].offset);
try std.testing.expectEqual(@as(?usize, 1), plan.elements[1].sx_field);
try std.testing.expectEqual(@as(usize, 4), plan.elements[1].offset);
}
// Lock: `StructInfo` is the first NON-natural layout — Zig reorders it to
// (fields@0, name@16, nominal_id@20, is_protocol@24) with a 7-byte alignment
// tail. The plan must reproduce exactly that order + the sx→element remap, so the
// LLVM type built from it is byte-identical to the Zig type. sx declaration order
// is (name, fields, is_protocol, nominal_id) = sx indices 0,1,2,3.
test "compiler_lib: weld plan for StructInfo reorders + pads to the Zig layout" {
const alloc = std.testing.allocator;
const bt = compiler_lib.findType("StructInfo").?;
var plan = try compiler_lib.computeWeldPlan(alloc, bt.fields, bt.size);
defer plan.deinit(alloc);
try std.testing.expectEqual(@as(usize, 32), plan.total_size);
// Elements in ascending offset order: fields, name, nominal_id, is_protocol,
// then a trailing 7-byte pad (25 → 32).
try std.testing.expectEqual(@as(usize, 5), plan.elements.len);
// elem 0: fields (sx index 1) @ 0, size 16
try std.testing.expectEqual(@as(?usize, 1), plan.elements[0].sx_field);
try std.testing.expectEqual(@as(usize, 0), plan.elements[0].offset);
try std.testing.expectEqual(@as(usize, 16), plan.elements[0].size);
// elem 1: name (sx index 0) @ 16, size 4
try std.testing.expectEqual(@as(?usize, 0), plan.elements[1].sx_field);
try std.testing.expectEqual(@as(usize, 16), plan.elements[1].offset);
// elem 2: nominal_id (sx index 3) @ 20, size 4
try std.testing.expectEqual(@as(?usize, 3), plan.elements[2].sx_field);
try std.testing.expectEqual(@as(usize, 20), plan.elements[2].offset);
// elem 3: is_protocol (sx index 2) @ 24, size 1
try std.testing.expectEqual(@as(?usize, 2), plan.elements[3].sx_field);
try std.testing.expectEqual(@as(usize, 24), plan.elements[3].offset);
// elem 4: trailing pad @ 25, size 7
try std.testing.expectEqual(@as(?usize, null), plan.elements[4].sx_field);
try std.testing.expectEqual(@as(usize, 25), plan.elements[4].offset);
try std.testing.expectEqual(@as(usize, 7), plan.elements[4].size);
// sx → element remap: name→1, fields→0, is_protocol→3, nominal_id→2.
try std.testing.expectEqual(@as(usize, 1), plan.sx_to_llvm[0]);
try std.testing.expectEqual(@as(usize, 0), plan.sx_to_llvm[1]);
try std.testing.expectEqual(@as(usize, 3), plan.sx_to_llvm[2]);
try std.testing.expectEqual(@as(usize, 2), plan.sx_to_llvm[3]);
}
// Lock: the welded-function export list resolves the round-trip readers and
// rejects unexported names (the boundary the interp's dispatch consults).
test "compiler_lib: findFn resolves exported functions, rejects others" {
try std.testing.expect(compiler_lib.findFn("intern") != null);
try std.testing.expect(compiler_lib.findFn("text_of") != null);
try std.testing.expectEqualStrings("intern", compiler_lib.findFn("intern").?.sx_name);
try std.testing.expect(compiler_lib.findFn("not_exported") == null);
try std.testing.expect(compiler_lib.findFn("") == null);
}

291
src/ir/compiler_lib.zig Normal file
View File

@@ -0,0 +1,291 @@
//! The comptime `compiler` library's binding registry — the curated surface of
//! the compiler's own types (layout-welded) and functions (host-call bridged)
//! reachable from comptime sx via `abi(.zig) extern compiler`. See
//! `design/comptime-compiler-api.md`.
//!
//! **This registry IS the safety boundary.** Only the entries registered here
//! are bindable from user comptime code; anything not on the export list is
//! unreachable. A welded `Name :: struct abi(.zig) extern compiler { … }` (or a
//! welded fn) resolves its layout/dispatch against this table, not the ordinary
//! extern-lib path.
//!
//! **Layout is welded, not guessed.** Because the sx compiler is itself a Zig
//! program, the real internal type's layout is available at compiler-build time:
//! each `BoundType` bakes `@sizeOf`/`@alignOf`/`@offsetOf` from the bound Zig
//! type. A `types.zig` change re-bakes the offsets on the next build, so both
//! sides move together. The sx-side `struct abi(.zig) …` declaration is then a
//! *header* checked against these offsets (the build-time layout-equality
//! assertion lands in the layout sub-step).
const std = @import("std");
const types = @import("types.zig");
const interp_mod = @import("interp.zig");
const Value = interp_mod.Value;
const Interpreter = interp_mod.Interpreter;
const InterpError = interp_mod.InterpError;
const StringId = types.StringId;
/// One field of a welded type: its sx-visible name plus the byte offset + size
/// taken from the bound Zig type.
pub const FieldLayout = struct {
name: []const u8,
offset: usize,
size: usize,
};
/// A type exported by the `compiler` library, welded to a real internal Zig
/// type. `size`/`alignment`/`fields` are baked from that Zig type at
/// compiler-build time (so they cannot drift from the implementation).
pub const BoundType = struct {
/// The sx-side name a welded `struct abi(.zig) extern compiler` uses.
sx_name: []const u8,
size: usize,
alignment: usize,
fields: []const FieldLayout,
};
/// The real internal Zig type each welded export binds to. Kept as named
/// aliases so the binding sites read as a curated list.
const FieldZig = types.TypeInfo.StructInfo.Field; // { name: StringId, ty: TypeId } — two u32s
const StructInfoZig = types.TypeInfo.StructInfo; // { name, fields: []Field, is_protocol, nominal_id } — Zig-reordered
/// Bake a `BoundType` from a real Zig struct type `T`. Field offsets/sizes come
/// from `@offsetOf`/`@sizeOf` on `T`; `sx_field_names` supplies the sx-visible
/// names positionally (must match `T`'s field order and count — a mismatch is a
/// compile error, never a silent truncation).
fn weldStruct(
comptime sx_name: []const u8,
comptime T: type,
comptime sx_field_names: []const []const u8,
) BoundType {
const zig_fields = @typeInfo(T).@"struct".fields;
if (zig_fields.len != sx_field_names.len)
@compileError("compiler-lib weld '" ++ sx_name ++ "': sx field count != Zig field count");
comptime var layouts: [zig_fields.len]FieldLayout = undefined;
inline for (zig_fields, 0..) |zf, i| {
layouts[i] = .{
.name = sx_field_names[i],
.offset = @offsetOf(T, zf.name),
.size = @sizeOf(zf.type),
};
}
const frozen = layouts;
return .{
.sx_name = sx_name,
.size = @sizeOf(T),
.alignment = @alignOf(T),
.fields = &frozen,
};
}
/// The welded-type export list. `Field` (two u32s, natural layout) proved the
/// weld in Phase 1; `StructInfo` (Phase 2) is the first NON-natural layout —
/// Zig reorders its fields (`fields`@0, `name`@16, `nominal_id`@20,
/// `is_protocol`@24), so it exercises the offset-override engine. `EnumInfo` /
/// `TaggedUnionInfo` / `TupleInfo` join later.
pub const bound_types = [_]BoundType{
weldStruct("Field", FieldZig, &.{ "name", "ty" }),
weldStruct("StructInfo", StructInfoZig, &.{ "name", "fields", "is_protocol", "nominal_id" }),
};
/// Look up a welded type by its sx name. Returns null when the name is not on
/// the `compiler` library's export list (the lookup the welded-decl resolution
/// path consults instead of the ordinary extern-lib path).
pub fn findType(sx_name: []const u8) ?*const BoundType {
for (&bound_types) |*bt| {
if (std.mem.eql(u8, bt.sx_name, sx_name)) return bt;
}
return null;
}
/// The name of the only welded library. A `struct abi(.zig) extern <lib>` with a
/// different `<lib>` is rejected — `compiler` is the sole comptime weld source.
pub const lib_name = "compiler";
/// One field of an sx welded-struct declaration, as the lowering observed it:
/// the field's sx name plus the size the sx type system computed for its type.
pub const SxField = struct {
name: []const u8,
size: usize,
};
/// The first way an sx welded-struct declaration fails to faithfully mirror the
/// bound Zig type. The sx declaration is a *header* checked against the real
/// implementation, so any drift is a build error rather than a silent
/// reinterpretation. The caller renders the chosen variant into a diagnostic.
pub const LayoutMismatch = union(enum) {
/// The sx declaration has a different field count than the welded type.
field_count: struct { expected: usize, got: usize },
/// Field `index` carries the wrong sx name (a weld is positional + by-name).
field_name: struct { index: usize, expected: []const u8, got: []const u8 },
/// Field `index` (`name`) is a different size than the welded type's field.
field_size: struct { index: usize, name: []const u8, expected: usize, got: usize },
/// The total struct size differs (padding / alignment drift).
total_size: struct { expected: usize, got: usize },
};
/// Check an sx welded-struct declaration against the bound Zig type. Returns the
/// FIRST mismatch, or null if the sx declaration is a faithful header. Fields are
/// checked positionally + by name + by size, and the total size is compared — for
/// a natural (C-like) layout this catches a missing/extra field (count), a rename
/// or reorder (name), a retype (size), and padding drift (total). Explicit
/// per-field OFFSET overrides (for non-natural Zig layouts — slices, reordered or
/// `union(enum)` fields) arrive with `StructInfo` in Phase 2; `Field`'s two-u32
/// natural layout needs none.
pub fn validateStructLayout(
bt: *const BoundType,
sx_fields: []const SxField,
sx_total_size: usize,
) ?LayoutMismatch {
if (sx_fields.len != bt.fields.len)
return .{ .field_count = .{ .expected = bt.fields.len, .got = sx_fields.len } };
for (sx_fields, bt.fields, 0..) |sf, bf, i| {
if (!std.mem.eql(u8, sf.name, bf.name))
return .{ .field_name = .{ .index = i, .expected = bf.name, .got = sf.name } };
if (sf.size != bf.size)
return .{ .field_size = .{ .index = i, .name = bf.name, .expected = bf.size, .got = sf.size } };
}
if (sx_total_size != bt.size)
return .{ .total_size = .{ .expected = bt.size, .got = sx_total_size } };
return null;
}
// ── Weld plan (byte-layout override) ────────────────────────────────────────
//
// A welded struct must be laid out byte-identically to the bound Zig type, whose
// fields Zig may REORDER (and pad). The sx struct's natural layout generally
// won't match — so the compiler imposes the Zig layout: it builds the struct's
// LLVM type as the fields in ascending-OFFSET order, with explicit padding
// elements filling the gaps, and remaps each sx field index to its LLVM element
// index. `computeWeldPlan` is that pure layout math; the LLVM type builder + the
// struct-GEP / field-access sites consume the plan (later sub-steps), and the
// interp serializes comptime struct Values through the same offsets.
/// One element of a welded struct's LLVM layout: either a real field (carrying
/// its sx field index) or a padding gap. Always in ascending `offset` order.
pub const WeldElement = struct {
/// The sx field index this element holds, or null for a padding gap.
sx_field: ?usize,
/// Byte offset of this element within the struct (the bound Zig offset).
offset: usize,
/// Byte width of this element (the field's size, or the gap width).
size: usize,
};
/// The byte-layout plan for a welded struct: its LLVM elements in offset order
/// (fields + padding) and the sx-field → LLVM-element-index remap. Owns its
/// slices — `deinit` with the same allocator passed to `computeWeldPlan`.
pub const WeldPlan = struct {
elements: []const WeldElement,
/// `sx_to_llvm[i]` is the index into `elements` of sx field `i`.
sx_to_llvm: []const usize,
total_size: usize,
pub fn deinit(self: *WeldPlan, alloc: std.mem.Allocator) void {
alloc.free(self.elements);
alloc.free(self.sx_to_llvm);
}
};
/// Compute the byte-layout plan for a struct whose fields carry their bound Zig
/// offsets (`fields[i].offset`/`.size`, e.g. from a `BoundType`). `total_size` is
/// the bound Zig `@sizeOf`. The result lists LLVM elements in ascending-offset
/// order — real fields interleaved with padding gaps — plus the sx-field →
/// element-index remap that struct-GEP uses. Pure; allocates the result slices.
pub fn computeWeldPlan(
alloc: std.mem.Allocator,
fields: []const FieldLayout,
total_size: usize,
) !WeldPlan {
// Order the sx field indices by ascending byte offset (stable).
const order = try alloc.alloc(usize, fields.len);
defer alloc.free(order);
for (order, 0..) |*o, i| o.* = i;
std.sort.insertion(usize, order, fields, struct {
fn lessThan(fs: []const FieldLayout, a: usize, b: usize) bool {
return fs[a].offset < fs[b].offset;
}
}.lessThan);
var elements = std.ArrayList(WeldElement).empty;
errdefer elements.deinit(alloc);
const sx_to_llvm = try alloc.alloc(usize, fields.len);
errdefer alloc.free(sx_to_llvm);
var cursor: usize = 0;
for (order) |sx_i| {
const f = fields[sx_i];
// Fill any gap before this field with a padding element.
if (f.offset > cursor) {
try elements.append(alloc, .{ .sx_field = null, .offset = cursor, .size = f.offset - cursor });
}
sx_to_llvm[sx_i] = elements.items.len;
try elements.append(alloc, .{ .sx_field = sx_i, .offset = f.offset, .size = f.size });
cursor = f.offset + f.size;
}
// Trailing padding up to the bound total size (alignment tail).
if (total_size > cursor) {
try elements.append(alloc, .{ .sx_field = null, .offset = cursor, .size = total_size - cursor });
}
return .{
.elements = try elements.toOwnedSlice(alloc),
.sx_to_llvm = sx_to_llvm,
.total_size = total_size,
};
}
// ── Functions (comptime-only, host-call bridged) ────────────────────────────
/// A welded `compiler` function: dispatched under the comptime interpreter to its
/// Zig handler (never dlsym'd). The handler receives the interpreter (for the
/// string pool / type table) and the already-evaluated argument `Value`s, and
/// returns the result `Value`.
pub const FnHandler = *const fn (interp: *Interpreter, args: []const Value) InterpError!Value;
pub const BoundFn = struct {
sx_name: []const u8,
handler: FnHandler,
};
/// The welded-function export list. Start small (Phase 1): the `StringId`
/// round-trip readers. `find_type` / the guarded `register_*` mutators join in
/// later phases.
pub const bound_fns = [_]BoundFn{
.{ .sx_name = "intern", .handler = handleIntern },
.{ .sx_name = "text_of", .handler = handleTextOf },
};
/// Look up a welded function by its sx name. Returns null when the name is not on
/// the `compiler` library's function-export list.
pub fn findFn(sx_name: []const u8) ?*const BoundFn {
for (&bound_fns) |*bf| {
if (std.mem.eql(u8, bf.sx_name, sx_name)) return bf;
}
return null;
}
/// The comptime type table to intern into: the host's mutable mint target when
/// set (the metatype-construction path), else the module's table reached through
/// a const-cast — the same access the interp's mint path uses (interp.zig). The
/// underlying table is genuinely mutable; the interp merely holds it `const`.
fn mintTable(interp: *Interpreter) *types.TypeTable {
return interp.mint orelse @constCast(&interp.module.types);
}
/// `intern(s: string) -> StringId` — intern `s` into the compiler's string pool
/// and return its handle. The inverse of `text_of`.
fn handleIntern(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .string) return error.TypeError;
const id = mintTable(interp).internString(args[0].string);
return Value{ .int = @intFromEnum(id) };
}
/// `text_of(id: StringId) -> string` — resolve a string handle back to its text.
/// The inverse of `intern`.
fn handleTextOf(interp: *Interpreter, args: []const Value) InterpError!Value {
if (args.len != 1 or args[0] != .int) return error.TypeError;
if (args[0].int < 0 or args[0].int > std.math.maxInt(u32)) return error.TypeError;
const id: StringId = @enumFromInt(@as(u32, @intCast(args[0].int)));
return Value{ .string = interp.module.types.getString(id) };
}

View File

@@ -1273,7 +1273,7 @@ pub const LLVMEmitter = struct {
else if (needs_c_abi) self.abiCoerceParamTypeEx(func.ret, raw_ret_ty, func.is_extern)
else raw_ret_ty;
// Build parameter types — apply C ABI coercion for extern/callconv(.c) functions.
// Build parameter types — apply C ABI coercion for extern/abi(.c) functions.
// When uses_sret, prepend the sret pointer at index 0.
const sret_offset: usize = if (uses_sret) 1 else 0;
const param_count: c_uint = @intCast(func.params.len + sret_offset);

View File

@@ -575,8 +575,14 @@ pub const Function = struct {
/// parameter that every default-conv sx function receives. Callers
/// read this flag to decide whether to prepend their current
/// `__sx_ctx` value to the args of a call. Extern decls and
/// `callconv(.c)` functions have it false.
/// `abi(.c)` functions have it false.
has_implicit_ctx: bool = false,
/// True for a `fn abi(.zig) extern compiler` welded to the comptime
/// `compiler` library. Such a function has no real symbol — the comptime
/// interpreter dispatches it to its registered Zig handler
/// (`compiler_lib.findFn`) instead of dlsym. Comptime-only; a runtime call
/// has no backing symbol. See design/comptime-compiler-api.md.
compiler_welded: bool = false,
pub const Param = struct {
name: StringId,

View File

@@ -162,6 +162,7 @@ pub const InterpError = error{
const compiler_hooks = @import("compiler_hooks.zig");
pub const BuildConfig = compiler_hooks.BuildConfig;
const compiler_lib = @import("compiler_lib.zig");
const host_ffi = @import("host_ffi.zig");
// ── Interpreter ─────────────────────────────────────────────────────────
@@ -579,6 +580,15 @@ pub const Interpreter = struct {
defer self.call_depth -= 1;
const func = self.module.getFunction(func_id);
// Welded `compiler`-library function: dispatch to its registered Zig
// handler (comptime-only), never dlsym. The binding registry IS the
// safety boundary — a name not on the export list is a clean bail.
if (func.compiler_welded) {
const fname = self.module.types.getString(func.name);
const bf = compiler_lib.findFn(fname) orelse
return bailDetail("comptime compiler call: function not exported by the compiler library");
return bf.handler(self, args);
}
if (func.is_extern or func.blocks.items.len == 0) {
// Dispatch to host libc via dlsym. Lets `#run` (and the
// post-link bundler) call ordinary extern symbols like

View File

@@ -60,6 +60,7 @@ pub const ObjcLowering = ffi_objc.ObjcLowering;
pub const ErrorFacts = error_analysis.ErrorFacts;
pub const compiler_hooks = @import("compiler_hooks.zig");
pub const compiler_lib = @import("compiler_lib.zig");
pub const emit_llvm = @import("emit_llvm.zig");
pub const LLVMEmitter = emit_llvm.LLVMEmitter;
@@ -89,6 +90,7 @@ pub const type_bridge_tests = @import("type_bridge.test.zig");
pub const emit_llvm_tests = @import("emit_llvm.test.zig");
pub const jni_descriptor_tests = @import("jni_descriptor.test.zig");
pub const jni_java_emit_tests = @import("jni_java_emit.test.zig");
pub const compiler_lib_tests = @import("compiler_lib.test.zig");
test {
@import("std").testing.refAllDecls(@This());

View File

@@ -78,7 +78,7 @@ pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
// Without implicit_ctx, env is slot 0 and user params follow.
var params = std.ArrayList(Function.Param).empty;
const env_ptr_ty = self.module.types.ptrTo(.void);
const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c;
const lambda_wants_ctx = self.implicit_ctx_enabled and lam.abi != .c;
if (lambda_wants_ctx) {
params.append(self.alloc, .{
.name = self.module.types.internString("__sx_ctx"),
@@ -168,7 +168,7 @@ pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
};
const name_id = self.module.types.internString(name);
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
if (lam.call_conv == .c) {
if (lam.abi == .c) {
self.module.getFunctionMut(func_id).call_conv = .c;
}
self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx;

View File

@@ -10,6 +10,7 @@ const unescape = @import("../../unescape.zig");
const errors = @import("../../errors.zig");
const program_index_mod = @import("../program_index.zig");
const resolver_mod = @import("../resolver.zig");
const compiler_lib = @import("../compiler_lib.zig");
const ProgramIndex = program_index_mod.ProgramIndex;
const GlobalInfo = program_index_mod.GlobalInfo;
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
@@ -497,7 +498,7 @@ pub fn detectContextDecl(decls: []const *const Node) bool {
/// `__sx_<name>_impl` with the ctx param) lands in Step 4 proper.
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
if (!self.implicit_ctx_enabled) return false;
if (fd.call_conv == .c) return false;
if (fd.abi == .c) return false;
// `extern` imports and `export` defines are external C symbols —
// C ABI, no sx context (Phase 2, gap iv).
if (fd.extern_export != .none) return false;
@@ -2256,11 +2257,11 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
}
// `extern` declarations are external C symbols by definition — promote
// them to callconv(.c) when the user didn't write it explicitly. This keeps
// them to abi(.c) when the user didn't write it explicitly. This keeps
// fn-ptr coercion type-safe: anything typed by name as `(args) -> ret` of an
// `extern` decl can be assigned to / passed as a `callconv(.c)` fn-pointer
// `extern` decl can be assigned to / passed as a `abi(.c)` fn-pointer
// without a call-convention mismatch.
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_extern_decl or fd.extern_export == .export_) .c else .default;
const cc: Function.CallingConvention = if (fd.abi == .c or is_extern_decl or fd.extern_export == .export_) .c else .default;
// Symbol-name override: `extern … "csym"` / `export … "csym"` (fd.extern_name).
// Declare under the C name and map the sx name → C name so call sites resolve
@@ -2296,9 +2297,29 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true;
self.fn_decl_fids.put(fd, fid) catch {};
}
/// A `fn abi(.zig) extern <lib>` binds the comptime `compiler` library. Validate
/// it (the bound lib must be `compiler`; the name must be on the function-export
/// list) and return whether it is a welded compiler function — the interpreter
/// dispatches such a call to its registered Zig handler instead of dlsym. Any
/// failure is a build-gating `.err` (never a silent fall-through to dlsym).
fn weldedCompilerFn(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) bool {
if (fd.abi != .zig) return false;
const diags = self.diagnostics;
if (fd.extern_lib == null or !std.mem.eql(u8, fd.extern_lib.?, compiler_lib.lib_name)) {
if (diags) |d| d.addFmt(.err, fd.name_span, "abi(.zig) function '{s}' must bind the compiler library — write `extern {s}`", .{ name, compiler_lib.lib_name });
return false;
}
if (compiler_lib.findFn(name) == null) {
if (diags) |d| d.addFmt(.err, fd.name_span, "'{s}' is not a function exported by the '{s}' library", .{ name, compiler_lib.lib_name });
return false;
}
return true;
}
/// Register a namespaced import's OWN functions under their module-qualified
/// name (`ns.fn`), giving each a UNIQUE FuncId in the function table. Two
/// modules each exporting a top-level `parse` otherwise collide in the
@@ -2553,7 +2574,7 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId
func.is_extern = false; // promote from extern stub to real function
// `export` defines force external linkage + C ABI (Phase 2, gaps i+ii).
func.linkage = if (isExportedEntryName(name) or fd.extern_export == .export_) .external else .internal;
if (fd.call_conv == .c or fd.extern_export == .export_) func.call_conv = .c;
if (fd.abi == .c or fd.extern_export == .export_) func.call_conv = .c;
// Set inst_counter to param count (params occupy refs 0..N-1). IR params
// = AST params + 1 if the function carries `__sx_ctx` at slot 0.
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
@@ -2589,7 +2610,7 @@ pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
// Inbound entry points + callconv(.c) sx functions: bind current_ctx_ref
// Inbound entry points + abi(.c) sx functions: bind current_ctx_ref
// to the static default before any user code runs.
if (!wants_ctx and self.implicit_ctx_enabled) {
if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| {
@@ -2695,7 +2716,7 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i
}
// Set calling convention. `export` defines promote to C ABI (gap ii).
if (fd.call_conv == .c or fd.extern_export == .export_) {
if (fd.abi == .c or fd.extern_export == .export_) {
self.builder.currentFunc().call_conv = .c;
}
@@ -2729,7 +2750,7 @@ pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, i
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
// Inbound entry points + callconv(.c) sx functions: bind
// Inbound entry points + abi(.c) sx functions: bind
// current_ctx_ref to &__sx_default_context. See companion comment
// in `lowerFunction` for the same case.
if (!wants_ctx_lf and self.implicit_ctx_enabled) {

View File

@@ -1894,7 +1894,7 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
// Coercing a bare fn name to a fn-pointer
// type — the call_conv must match. A
// default-conv sx fn assigned to a
// callconv(.c) slot (e.g. passed to
// abi(.c) slot (e.g. passed to
// pthread_create) would otherwise crash at
// runtime when the C caller doesn't supply
// the implicit __sx_ctx arg.
@@ -1902,8 +1902,8 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv;
if (func_cc != tt_info.function.call_conv) {
if (self.diagnostics) |d| {
const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention";
const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention";
const want_cc = if (tt_info.function.call_conv == .c) "abi(.c)" else "default sx convention";
const have_cc = if (func_cc == .c) "abi(.c)" else "default sx convention";
d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc });
}
break :blk self.emitPlaceholder(eff_fn_name);

View File

@@ -1133,7 +1133,7 @@ pub fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.RuntimeClassDecl,
// JNI native methods are C-callable entry points — install the
// static default Context so `context.X` reads in the method body
// resolve through `current_ctx_ref`. Mirror the same binding
// `lowerFunction` does for callconv(.c) / isExportedEntryName.
// `lowerFunction` does for abi(.c) / isExportedEntryName.
const saved_ctx_ref_jni = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref_jni;
if (self.implicit_ctx_enabled) {

View File

@@ -6,6 +6,7 @@ const mod_mod = @import("../module.zig");
const type_bridge = @import("../type_bridge.zig");
const program_index_mod = @import("../program_index.zig");
const resolver_mod = @import("../resolver.zig");
const compiler_lib = @import("../compiler_lib.zig");
const StructTemplate = program_index_mod.StructTemplate;
const TemplateParam = program_index_mod.TemplateParam;
@@ -673,7 +674,13 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil
// any forward-reference stub. Same-name structs in DIFFERENT sources get
// distinct TypeIds instead of last-wins clobbering the first.
const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } };
_ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
const struct_tid = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
// Welded `struct abi(.zig) extern compiler { … }`: the sx declaration is a
// header checked against the compiler's real Zig type — validate the layout
// matches the binding registry (a mismatch is a build error). See
// design/comptime-compiler-api.md.
if (sd.abi == .zig) validateWeldedStruct(self, sd, struct_tid, fields.items);
// Store field defaults for struct literal lowering
if (sd.field_defaults.len > 0) {
@@ -709,6 +716,51 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil
}
}
/// Validate a welded `struct abi(.zig) extern <lib> { … }` against the `compiler`
/// library's binding registry: the bound library must be `compiler`, the name
/// must be on the export list, and the sx-declared layout must match the real Zig
/// type's (the sx side is a *header* checked against the implementation). Any
/// failure is a build-gating `.err` diagnostic — never a silent reinterpretation.
fn validateWeldedStruct(self: *Lowering, sd: *const ast.StructDecl, tid: TypeId, fields: []const types.TypeInfo.StructInfo.Field) void {
const diags = self.diagnostics orelse return;
const table = &self.module.types;
// A span that points into the struct (its first field, else zero) — the decl
// has no name span of its own.
const span: ast.Span = if (sd.field_types.len > 0) sd.field_types[0].span else .{ .start = 0, .end = 0 };
// The bound library must be the sole welded source.
if (sd.extern_lib == null or !std.mem.eql(u8, sd.extern_lib.?, compiler_lib.lib_name)) {
diags.addFmt(.err, span, "abi(.zig) struct '{s}' must bind the compiler library — write `extern {s}`", .{ sd.name, compiler_lib.lib_name });
return;
}
// The name must be on the curated export list (the safety boundary).
const bt = compiler_lib.findType(sd.name) orelse {
diags.addFmt(.err, span, "'{s}' is not a type exported by the '{s}' library", .{ sd.name, compiler_lib.lib_name });
return;
};
// Build the observed sx layout (field name + computed size) and total size.
var sx_fields = std.ArrayList(compiler_lib.SxField).empty;
defer sx_fields.deinit(self.alloc);
for (fields) |f| {
sx_fields.append(self.alloc, .{
.name = table.getString(f.name),
.size = table.typeSizeBytes(f.ty),
}) catch return;
}
const total = table.typeSizeBytes(tid);
const mismatch = compiler_lib.validateStructLayout(bt, sx_fields.items, total) orelse return;
switch (mismatch) {
.field_count => |m| diags.addFmt(.err, span, "welded type '{s}' has {d} field(s) in the compiler library but the declaration has {d}", .{ sd.name, m.expected, m.got }),
.field_name => |m| diags.addFmt(.err, span, "welded type '{s}' field {d} is named '{s}' in the compiler library, not '{s}'", .{ sd.name, m.index, m.expected, m.got }),
.field_size => |m| diags.addFmt(.err, span, "welded type '{s}' field '{s}' is {d} byte(s) in the compiler library but {d} as declared", .{ sd.name, m.name, m.expected, m.got }),
.total_size => |m| diags.addFmt(.err, span, "welded type '{s}' is {d} byte(s) in the compiler library but {d} as declared (padding/alignment mismatch)", .{ sd.name, m.expected, m.got }),
}
}
/// Register a top-level ENUM decl under a per-decl nominal identity (E6a) —
/// the enum twin of `registerStructDecl`. A GENUINE same-name shadow already
/// reserved its DISTINCT slot up-front in `scanDecls` (the first at id 0, the

View File

@@ -255,7 +255,7 @@ pub fn lowerObjcPropertySetter(self: *Lowering, obj_expr: *const ast.Node, field
} }, .void);
}
/// Get a FuncId for an external C-callconv function. If a function
/// Get a FuncId for an external C-ABI function. If a function
/// with this exported name already exists in the module (e.g.
/// declared by stdlib `extern` decl), return it; otherwise
/// declare it fresh with the given signature.
@@ -290,7 +290,7 @@ pub fn ensureCRuntimeDecl(self: *Lowering, name: []const u8, param_tys: []const
/// 2. Calls `object_getIvar(obj, ivar)` to get the `*<Cls>State`
/// state pointer.
/// 3. Calls the sx body `@<Cls>.<method>(__sx_default_context,
/// state, ...user_args)` (default sx-callconv).
/// state, ...user_args)` (default sx convention).
/// 4. Returns the result (or `ret void`).
///
/// IMP name: `__<ClassName>_<methodName>_imp`. emit_llvm's

View File

@@ -91,7 +91,7 @@ pub const Module = struct {
pub const ObjcDefinedMethodEntry = struct {
sel: []const u8, // mangled Obj-C selector (`add:and:`)
encoding: []const u8, // Apple-runtime type encoding (`v@:ii`)
imp_name: []const u8, // C-callconv trampoline symbol (`__Cls_method_imp`)
imp_name: []const u8, // C-ABI trampoline symbol (`__Cls_method_imp`)
is_class: bool = false, // true ⇒ register on the metaclass (M2.1 class methods)
};

View File

@@ -224,9 +224,15 @@ pub const TypeResolver = struct {
defer param_ids.deinit(table.alloc);
for (ft.param_types) |pt| param_ids.append(table.alloc, inner.resolveInner(pt)) catch return .unresolved;
const ret_ty = if (ft.return_type) |rt| inner.resolveInner(rt) else TypeId.void;
const cc: types.TypeInfo.CallConv = switch (ft.call_conv) {
const cc: types.TypeInfo.CallConv = switch (ft.abi) {
.default => .default,
.c => .c,
// `.zig` (compiler-lib weld) and `.pure` (naked asm) are
// decl-level ABIs with no function-pointer-type calling
// convention of their own; the IR function-type CC models only
// sx-default vs C. Neither occurs in a function-TYPE position in
// current usage — treated as sx-default here.
.zig, .pure => .default,
};
break :blk table.functionTypeCC(param_ids.items, ret_ty, cc);
},

View File

@@ -537,9 +537,9 @@ test "lex keywords" {
}
test "lex linkage keywords" {
// extern / export are keywords (FFI-linkage stream), lexed beside callconv.
var lex = Lexer.init("callconv extern export");
const expected = [_]Tag{ .kw_callconv, .kw_extern, .kw_export };
// extern / export are keywords (FFI-linkage stream), lexed beside abi.
var lex = Lexer.init("abi extern export");
const expected = [_]Tag{ .kw_abi, .kw_extern, .kw_export };
for (expected) |exp| {
try std.testing.expectEqual(exp, lex.next().tag);
}

View File

@@ -1682,7 +1682,7 @@ pub const Server = struct {
.kw_protocol,
.kw_impl,
.kw_inline,
.kw_callconv,
.kw_abi,
.kw_extern,
.kw_export,
.kw_asm,

View File

@@ -909,6 +909,11 @@ fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]c
for (decls) |d| {
switch (d.data) {
.library_decl => |ld| {
// The `compiler` library is the comptime-only internal
// surface (welded types / host-call functions), not a
// linkable dylib — never dlopen it. See
// design/comptime-compiler-api.md.
if (std.mem.eql(u8, ld.lib_name, sx.ir.compiler_lib.lib_name)) continue;
if (s.contains(ld.lib_name)) continue;
try s.put(ld.lib_name, {});
try l.append(a, ld.lib_name);

View File

@@ -78,3 +78,148 @@ test "parser: comptime type-metaprogramming surface parses" {
try std.testing.expect(d.data.fn_decl.return_type != null);
}
}
// Lock: the `compiler`-library binding surface PARSES — `name :: #library "x";`
// (already supported) plus the new postfix `abi(.zig)` annotation (in the slot
// before `extern`) followed by the library handle, on a function declaration. The
// AST must carry the binding: `abi == .zig`, `extern_export == .extern_`, and the
// library handle in `extern_lib`. No semantics yet — this is the first testable
// sub-step of Phase 1 (parse only).
test "parser: abi(.zig) extern <lib> binding parses on a fn decl" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\compiler :: #library "compiler";
\\text_of :: (id: StringId) -> string abi(.zig) extern compiler;
\\intern :: (s: string) -> StringId abi(.zig) extern compiler;
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
try std.testing.expect(root.data == .root);
const decls = root.data.root.decls;
try std.testing.expectEqual(@as(usize, 3), decls.len);
// The `#library` decl still parses to a `library_decl` node carrying the name.
try std.testing.expect(decls[0].data == .library_decl);
try std.testing.expectEqualStrings("compiler", decls[0].data.library_decl.name);
try std.testing.expectEqualStrings("compiler", decls[0].data.library_decl.lib_name);
// The two `abi(.zig) extern compiler` fns: `.fn_decl` with the binding fields set.
for ([_][]const u8{ "text_of", "intern" }) |bn| {
var found: ?*const Node = null;
for (decls) |d| {
if (d.data.declName()) |n| {
if (std.mem.eql(u8, n, bn)) found = d;
}
}
const d = found orelse return error.MissingDecl;
try std.testing.expect(d.data == .fn_decl);
const fd = d.data.fn_decl;
try std.testing.expectEqual(ast.ABI.zig, fd.abi);
try std.testing.expectEqual(ast.ExternExportModifier.extern_, fd.extern_export);
try std.testing.expect(fd.extern_lib != null);
try std.testing.expectEqualStrings("compiler", fd.extern_lib.?);
// Bodyless extern import: synthesized empty block, no `#builtin`/`#compiler`.
try std.testing.expect(fd.body.data == .block);
}
}
// Lock: a bare `extern` (no abi annotation) leaves `abi == .default` — the
// unannotated case is unchanged by the new `abi(...)` slot.
test "parser: bare extern leaves abi == .default" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\puts :: (s: *u8) -> i32 extern;
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
const decls = root.data.root.decls;
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0].data == .fn_decl);
const fd = decls[0].data.fn_decl;
try std.testing.expectEqual(ast.ExternExportModifier.extern_, fd.extern_export);
try std.testing.expectEqual(ast.ABI.default, fd.abi);
}
// Lock: `abi(.c)` parses standalone (no extern/export) in the postfix slot — the
// migrated spelling of the old `callconv(.c)` on an ordinary function pointer /
// fn decl. And `abi(.pure)` parses (naked-asm ABI).
test "parser: abi(.c) and abi(.pure) parse standalone" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\cb :: () -> i64 abi(.c) { 0; }
\\nk :: () -> i64 abi(.pure) { 0; }
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
const decls = root.data.root.decls;
try std.testing.expectEqual(@as(usize, 2), decls.len);
try std.testing.expect(decls[0].data == .fn_decl);
try std.testing.expectEqual(ast.ABI.c, decls[0].data.fn_decl.abi);
try std.testing.expectEqual(ast.ExternExportModifier.none, decls[0].data.fn_decl.extern_export);
try std.testing.expect(decls[1].data == .fn_decl);
try std.testing.expectEqual(ast.ABI.pure, decls[1].data.fn_decl.abi);
}
// Lock: the `compiler`-library binding PARSES on a STRUCT decl — `Name :: struct
// abi(.zig) extern <lib> { … }`. The AST struct_decl must carry `abi == .zig` and
// the library handle in `extern_lib`, with the field list intact. No semantics
// yet (parse-only) — this is the second testable sub-step of Phase 1.
test "parser: abi(.zig) extern <lib> binding parses on a struct decl" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\compiler :: #library "compiler";
\\Field :: struct abi(.zig) extern compiler { name: StringId; ty: Type; }
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
const decls = root.data.root.decls;
try std.testing.expectEqual(@as(usize, 2), decls.len);
try std.testing.expect(decls[1].data == .struct_decl);
const sd = decls[1].data.struct_decl;
try std.testing.expectEqual(ast.ABI.zig, sd.abi);
try std.testing.expect(sd.extern_lib != null);
try std.testing.expectEqualStrings("compiler", sd.extern_lib.?);
// Field list survives the binding annotation.
try std.testing.expectEqual(@as(usize, 2), sd.field_names.len);
try std.testing.expectEqualStrings("name", sd.field_names[0]);
try std.testing.expectEqualStrings("ty", sd.field_names[1]);
}
// Lock: an ordinary struct (no binding) leaves `abi == .default` / `extern_lib ==
// null` — the new annotation slot doesn't perturb the common case.
test "parser: plain struct leaves abi == .default, extern_lib == null" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\Point :: struct { x: i64; y: i64; }
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
const decls = root.data.root.decls;
try std.testing.expectEqual(@as(usize, 1), decls.len);
try std.testing.expect(decls[0].data == .struct_decl);
const sd = decls[0].data.struct_decl;
try std.testing.expectEqual(ast.ABI.default, sd.abi);
try std.testing.expect(sd.extern_lib == null);
}

View File

@@ -598,12 +598,12 @@ pub const Parser = struct {
// '->' present: function type
self.advance(); // skip '->'
const return_type = try self.parseTypeExpr();
const call_conv = try self.parseOptionalCallConv();
const abi = try self.parseOptionalAbi();
return try self.createNode(start, .{ .function_type_expr = .{
.param_types = try param_types.toOwnedSlice(self.allocator),
.param_names = if (has_names) try param_names.toOwnedSlice(self.allocator) else null,
.return_type = return_type,
.call_conv = call_conv,
.abi = abi,
} });
}
// No '->': tuple type (even for single element). Keep field names
@@ -959,6 +959,18 @@ pub const Parser = struct {
self.advance();
}
// Optional welded-binding annotation: `struct abi(.zig) extern <lib> { … }`.
// `abi(...)` (the ABI/layout selector) sits before the `extern` linkage
// keyword, mirroring the fn-decl slot order; the library handle follows.
// Parse-only for now — no layout/registry semantics yet.
const struct_abi = try self.parseOptionalAbi();
const struct_extern = self.parseOptionalExternExport();
var struct_extern_lib: ?[]const u8 = null;
if (struct_extern != .none and self.current.tag == .identifier) {
struct_extern_lib = self.tokenSlice(self.current);
self.advance();
}
// Optional type params: struct($N: u32, $T: Type) { ... }
var type_params = std.ArrayList(ast.StructTypeParam).empty;
if (self.current.tag == .l_paren) {
@@ -1146,6 +1158,8 @@ pub const Parser = struct {
.using_entries = try using_entries.toOwnedSlice(self.allocator),
.methods = try methods.toOwnedSlice(self.allocator),
.constants = try constants.toOwnedSlice(self.allocator),
.abi = struct_abi,
.extern_lib = struct_extern_lib,
.is_raw = name_is_raw,
} });
}
@@ -1937,8 +1951,11 @@ pub const Parser = struct {
return_type = try self.parseTypeExpr();
}
// Optional calling convention: callconv(.c)
const call_conv = try self.parseOptionalCallConv();
// Optional ABI / calling-convention annotation: `abi(.c)` / `abi(.zig)` /
// `abi(.pure)`. Sits in the postfix slot BEFORE the `extern`/`export`
// linkage keyword (it is part of the function declaration). `abi(.zig)`
// marks a binding to the comptime `compiler` library.
const abi = try self.parseOptionalAbi();
// Optional postfix linkage modifier: `extern` (import) / `export` (define).
const extern_export = self.parseOptionalExternExport();
@@ -2018,7 +2035,7 @@ pub const Parser = struct {
.body = body,
.type_params = type_params,
.is_arrow = is_arrow,
.call_conv = call_conv,
.abi = abi,
.extern_export = extern_export,
.extern_lib = extern_lib,
.extern_name = extern_name,
@@ -3688,8 +3705,8 @@ pub const Parser = struct {
return_type = try self.parseTypeExpr();
}
// Optional calling convention: callconv(.c)
const call_conv = try self.parseOptionalCallConv();
// Optional ABI annotation: abi(.c) / abi(.zig) / abi(.pure)
const abi = try self.parseOptionalAbi();
// A closure is its own function boundary: clear the cleanup-body flags
// so control-flow exits inside the closure body (`return` from the
@@ -3719,7 +3736,7 @@ pub const Parser = struct {
.return_type = return_type,
.body = body,
.type_params = type_params,
.call_conv = call_conv,
.abi = abi,
} });
}
@@ -3745,8 +3762,8 @@ pub const Parser = struct {
// builtin marker) is a function-type literal, not a function def.
if (tag == .arrow) return self.hasFnBodyAfterArrow();
// `kw_extern`/`kw_export`: a postfix linkage modifier (e.g. `f :: () extern;`
// with no return type) marks a fn decl just like `callconv`.
return tag == .l_brace or tag == .hash_builtin or tag == .hash_compiler or tag == .fat_arrow or tag == .kw_callconv or tag == .kw_extern or tag == .kw_export;
// with no return type) marks a fn decl just like `abi(...)`.
return tag == .l_brace or tag == .hash_builtin or tag == .hash_compiler or tag == .fat_arrow or tag == .kw_abi or tag == .kw_extern or tag == .kw_export;
}
fn hasFnBodyAfterArrow(self: *Parser) bool {
@@ -3773,9 +3790,9 @@ pub const Parser = struct {
if (self.current.tag == .fat_arrow) return true;
if (self.current.tag == .l_brace) return true;
if (self.current.tag == .hash_builtin or self.current.tag == .hash_compiler) return true;
if (self.current.tag == .kw_callconv) return true;
if (self.current.tag == .kw_abi) return true;
// Postfix linkage modifier after the return type: `-> R extern;` /
// `-> R export { … }` (and `-> R callconv(.c) extern`). Marks a fn def.
// `-> R export { … }` (and `-> R abi(.c) extern`). Marks a fn def.
if (self.current.tag == .kw_extern or self.current.tag == .kw_export) return true;
// Inside a `struct #compiler` block, a `(...) -> Ret;` ending
// with `;` after the return type is a `#compiler` method
@@ -3806,25 +3823,32 @@ pub const Parser = struct {
return false;
}
fn parseOptionalCallConv(self: *Parser) anyerror!ast.CallingConvention {
if (self.current.tag != .kw_callconv) return .default;
/// Optional ABI / calling-convention annotation `abi(.c)` / `abi(.zig)` /
/// `abi(.pure)` in the postfix slot before `extern`/`export`. `.default` when
/// absent. Subsumes the old `callconv(...)` spelling.
fn parseOptionalAbi(self: *Parser) anyerror!ast.ABI {
if (self.current.tag != .kw_abi) return .default;
self.advance();
try self.expect(.l_paren);
try self.expect(.dot);
if (self.current.tag != .identifier)
return self.fail("expected calling convention name after '.'");
const cc_name = self.tokenSlice(self.current);
const cc: ast.CallingConvention = if (std.mem.eql(u8, cc_name, "c")) .c else return self.fail("unknown calling convention");
return self.fail("expected ABI name ('.c', '.zig', or '.pure') after '.'");
const abi_name = self.tokenSlice(self.current);
const abi: ast.ABI = if (std.mem.eql(u8, abi_name, "c"))
.c
else if (std.mem.eql(u8, abi_name, "zig"))
.zig
else if (std.mem.eql(u8, abi_name, "pure"))
.pure
else
return self.fail("unknown ABI (expected '.c', '.zig', or '.pure')");
self.advance();
try self.expect(.r_paren);
return cc;
return abi;
}
/// Postfix linkage modifier in the slot after `callconv(...)`:
/// Postfix linkage modifier in the slot after `abi(...)`:
/// `extern` (import) or `export` (define + expose), or `.none` if neither.
/// Mirrors `parseOptionalCallConv`. Bare-keyword today; the optional
/// `"csym"` symbol-name override lands in Phase 1.2/2.2. Defined here in
/// Phase 0.1 but NOT yet called from any decl path (wired in Phase 1.0).
fn parseOptionalExternExport(self: *Parser) ast.ExternExportModifier {
switch (self.current.tag) {
.kw_extern => {

View File

@@ -830,7 +830,7 @@ pub const Analyzer = struct {
switch (node.data) {
.fn_decl => |fd| {
const saved_cc = self.in_c_conv;
self.in_c_conv = fd.call_conv == .c;
self.in_c_conv = fd.abi == .c;
try self.pushScope();
try self.analyzeParams(fd.params);
try self.analyzeNode(fd.body);
@@ -852,7 +852,7 @@ pub const Analyzer = struct {
if (mnode.data == .fn_decl) {
const m = mnode.data.fn_decl;
const saved_cc = self.in_c_conv;
self.in_c_conv = m.call_conv == .c;
self.in_c_conv = m.abi == .c;
try self.pushScope();
try self.analyzeParams(m.params);
try self.analyzeNode(m.body);
@@ -979,7 +979,7 @@ pub const Analyzer = struct {
try self.diagnostics.append(self.allocator, .{
.level = .warn,
.span = span,
.message = "`context` is unavailable in a `callconv(.c)` function — the C ABI has no implicit context parameter; pass what you need explicitly",
.message = "`context` is unavailable in an `abi(.c)` function — the C ABI has no implicit context parameter; pass what you need explicitly",
});
return;
}
@@ -1032,7 +1032,7 @@ pub const Analyzer = struct {
});
}
const saved_cc = self.in_c_conv;
self.in_c_conv = fd.call_conv == .c;
self.in_c_conv = fd.abi == .c;
try self.pushScope();
try self.analyzeParams(fd.params);
try self.analyzeNode(fd.body);
@@ -2366,7 +2366,7 @@ test "sema: member references record fields, methods, and enum variants" {
try std.testing.expect(red_use);
}
test "sema: context in a callconv(.c) function reports a specific diagnostic" {
test "sema: context in an abi(.c) function reports a specific diagnostic" {
const parser_mod = @import("parser.zig");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -2374,7 +2374,7 @@ test "sema: context in a callconv(.c) function reports a specific diagnostic" {
const source =
"Context :: struct { allocator: i64; data: i64; }" ++
"cb :: () -> i64 callconv(.c) { context; 0; }" ++
"cb :: () -> i64 abi(.c) { context; 0; }" ++
"ok :: () -> i64 { context; 0; }";
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();
@@ -2385,7 +2385,7 @@ test "sema: context in a callconv(.c) function reports a specific diagnostic" {
var c_conv_diag = false;
var undefined_diag = false;
for (res.diagnostics) |d| {
if (std.mem.indexOf(u8, d.message, "callconv(.c)") != null) c_conv_diag = true;
if (std.mem.indexOf(u8, d.message, "abi(.c)") != null) c_conv_diag = true;
if (std.mem.indexOf(u8, d.message, "undefined") != null) undefined_diag = true;
}
try std.testing.expect(c_conv_diag); // `cb` accesses context under the C ABI

View File

@@ -42,7 +42,7 @@ pub const Tag = enum {
kw_impl, // impl
kw_Self, // Self (in protocol declarations)
kw_inline, // inline (compile-time if/for/while)
kw_callconv, // callconv (calling convention annotation)
kw_abi, // abi (ABI / calling-convention annotation: abi(.c)/abi(.zig)/abi(.pure))
kw_extern, // extern (import: external linkage, C ABI, no body)
kw_export, // export (define + expose: external linkage, C ABI)
kw_asm, // asm (inline assembly expression / global asm decl)
@@ -281,7 +281,7 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{
.{ "impl", .kw_impl },
.{ "Self", .kw_Self },
.{ "inline", .kw_inline },
.{ "callconv", .kw_callconv },
.{ "abi", .kw_abi },
.{ "extern", .kw_extern },
.{ "export", .kw_export },
// `asm` is a real keyword; `volatile` / `clobbers` stay OUT of this table