From 22af40413d5a804ae9744d44116f625b53afb16c Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 08:47:07 +0300 Subject: [PATCH] atomics A.0a: lib + IR ops + recognizer, emit bails (lock commit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stream A (atomics) foundation. Net-new atomic load/store codegen path, wired end-to-end except LLVM emission, which deliberately bails loudly so the example locks to a clean diagnostic (A.0b turns it green — cadence: no commit both adds a test and makes it pass). - library/modules/std/atomic.sx: Ordering enum, Atomic($T) transparent wrapper (init/load/store, seq_cst-only for now), atomic_load/atomic_store #builtin intrinsics. Opt-in import, NOT in the universal std facade (Ordering in the prelude grows every program's type table + churns 37 .ir snapshots). - IR: atomic_load/atomic_store ops + AtomicOrdering (all 5) + structs (inst.zig); print arms; comptime_vm arms reuse load/store (single-thread correct); recognizer tryLowerAtomicIntrinsic (const-ordering + scalar-size guards, both loud); emit dispatch -> emitAtomicLoad/Store bail via comptime_failed. - examples/1700-atomics-load-store.sx locked to the bail diagnostic. Full ordering surface (a.load(.acquire)) blocked on comptime-constant ordering propagation (comptime enum value params) — A.0.5, migrated not legacy. --- current/CHECKPOINT-ATOMICS.md | 66 ++++++ current/PLAN-ATOMICS.md | 210 ++++++++++++++++++ examples/1700-atomics-load-store.sx | 17 ++ .../expected/1700-atomics-load-store.exit | 1 + .../expected/1700-atomics-load-store.stderr | 2 + .../expected/1700-atomics-load-store.stdout | 1 + library/modules/std/atomic.sx | 45 ++++ src/backend/llvm/ops.zig | 25 +++ src/ir/comptime_vm.zig | 14 ++ src/ir/emit_llvm.zig | 2 + src/ir/inst.zig | 25 +++ src/ir/lower.zig | 1 + src/ir/lower/call.zig | 59 +++++ src/ir/print.zig | 5 + 14 files changed, 473 insertions(+) create mode 100644 current/CHECKPOINT-ATOMICS.md create mode 100644 current/PLAN-ATOMICS.md create mode 100644 examples/1700-atomics-load-store.sx create mode 100644 examples/expected/1700-atomics-load-store.exit create mode 100644 examples/expected/1700-atomics-load-store.stderr create mode 100644 examples/expected/1700-atomics-load-store.stdout create mode 100644 library/modules/std/atomic.sx diff --git a/current/CHECKPOINT-ATOMICS.md b/current/CHECKPOINT-ATOMICS.md new file mode 100644 index 00000000..77232f79 --- /dev/null +++ b/current/CHECKPOINT-ATOMICS.md @@ -0,0 +1,66 @@ +# CHECKPOINT-ATOMICS — Stream A (atomics lowering) + +Companion to [PLAN-ATOMICS.md](PLAN-ATOMICS.md). Update after every step (one step at a +time, per the cadence rule). New corpus category: `17xx`. + +## Last completed step +**A.0a (lock commit) — DONE.** Full atomic load/store plumbing landed with LLVM emission +deliberately bailing loudly; `examples/1700-atomics-load-store.sx` locked to the bail +diagnostic (exit 1). Suite green (710 examples, 0 failed; 476 units). +- `library/modules/std/atomic.sx`: `Ordering` enum, `Atomic($T)` struct (`init`/`load`/ + `store`, **seq_cst-only** — see capability gap below), `atomic_load`/`atomic_store` + `#builtin` decls. **Opt-in import**, NOT in the universal `std.sx` facade (mirrors + `trace`) — putting `Ordering` in the prelude grew every program's type table 378→380 and + churned 37 `.ir` snapshots; reverted. +- IR ops `atomic_load`/`atomic_store` + `AtomicOrdering` (all 5) + structs (inst.zig); + print arms (print.zig); comptime_vm arms reuse load/store (single-thread correct); + recognizer `tryLowerAtomicIntrinsic` (call.zig) — const-ordering-literal guard + + scalar-size guard, both loud; emit dispatch arms (emit_llvm.zig) → `emitAtomicLoad`/ + `emitAtomicStore` (ops.zig) currently BAIL via `comptime_failed`. + +## Current state +- A.0a committed; suite green. +- The recognizer + IR + emit already handle ALL FIVE orderings; only the surface bakes a + `.seq_cst` literal (the methods can't yet forward a runtime/comptime ordering — gap below). +- emit bodies are the ONLY placeholder; A.0b swaps them for real builders. + +## Next step +**A.0b** — replace the `emitAtomicLoad`/`emitAtomicStore` bail with real +`LLVMBuildLoad2`+`SetOrdering`+`SetAlignment` / `LLVMBuildStore`+`SetOrdering`+ +`SetAlignment` (explicit sx-tag→LLVM ordering switch); regen 1700 → green (7/42/43) + host +`.ir`; add `emit_llvm.test.zig` unit. Then adversarial review, then the comptime-enum worker ++ A.0.5 migration to the full ordering surface. + +## Known issues / capability gaps +- **Comptime-constant ordering propagation MISSING (blocks the full surface).** A runtime + `Ordering` method param can't reach LLVM (orderings are instruction attributes, not + operands), and comptime enum value params don't exist (`$o: Ordering` → `o` unresolved in + body; `resolveValueParamArg` folds integers only). So A.0 ships seq_cst-only; A.0.5 closes + the gap (worker: implement comptime enum value params) and MIGRATES the methods — NO + legacy left by stream end. +- **Latent (observed, not yet filed):** calling an *unrecognized* bodiless `#builtin` + silently returns 0 / no-ops with exit 0 (that's how 1700 behaved before recognition + landed) — a silent-fallback footgun in the generic builtin-call path, independent of + atomics. Flag to user; candidate `issues/` entry. + +## Decisions (Stream A specifics; surface locked in design §4.6) +- `Atomic($T)` = pure-sx transparent 1-field struct (NO new IR type); ops = `#builtin` + intrinsics emitted as new IR ops. Minimal compiler surface. +- Ordering is compile-time-only (const enum literal), baked into the op as a Zig enum; + non-literal = loud diagnostic. sx tag → LLVM ordering via explicit switch (LLVM enum is + non-contiguous: 2/4/5/6/7). +- Atomic load/store REQUIRE explicit alignment (`LLVMSetAlignment`) — verifier mandate. +- Comptime VM treats atomics as ordinary load/store (single-thread ⇒ correct), not a bail. +- **Snapshot scope corrected:** `.ir` (LLVM IR) is arch-invariant for atomics → ONE host + `.ir` per op, not arch-gated x86/aarch64 pairs (they'd be byte-identical). Asm-level arch + divergence + weak-memory semantics are OUT of corpus scope (stress harness, Stream C). + +## Log +- **carve** — wrote PLAN-ATOMICS.md + CHECKPOINT-ATOMICS.md; grounded the intrinsic path, + switch sites, LLVM-C API (no `LLVMBuildAtomicLoad`; use `LLVMBuildLoad2`+`SetOrdering`+ + `SetAlignment`), and corrected the arch-`.ir` misconception (`sx ir` emits arch-invariant + LLVM IR). Stream ready; A.0a is the first implementation step. +- **A.0a** — landed lib (atomic.sx, opt-in import) + IR ops (atomic_load/atomic_store + + AtomicOrdering) + recognizer + print/vm arms + emit BAIL; locked `examples/1700` to the + bail diagnostic. Reverted a universal-facade wiring that churned 37 `.ir` snapshots + (Ordering would bloat every program's type table). Suite green (710/0). diff --git a/current/PLAN-ATOMICS.md b/current/PLAN-ATOMICS.md new file mode 100644 index 00000000..c5082a8d --- /dev/null +++ b/current/PLAN-ATOMICS.md @@ -0,0 +1,210 @@ +# PLAN-ATOMICS — Stream A (atomics lowering) + +Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream A + the design-of-record +[../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md) §3 (N1) ++ §4.6 (locked surface). Progress in [CHECKPOINT-ATOMICS.md](CHECKPOINT-ATOMICS.md). + +**Goal:** net-new LLVM atomic codegen. Surface = a pure-sx `Atomic($T)` generic struct + +an `Ordering` enum (ordinary sx), with the actual atomic operations recognized as +`#builtin` intrinsics at lower-time and emitted as new IR ops. This is **100% net-new** — +no atomics scaffolding exists (the only `lower.zig` "ordering" is *comparison* ordering +`< <= >=`, unrelated to memory ordering — do not mistake it for groundwork). + +**Cadence (IMPASSIBLE):** no commit both adds a test AND makes it pass (lock-to-bail, then +flip to green); `zig build && zig build test` green after every step; never regen snapshots +while red; scope regens with `-Dname=examples/NNNN-…sx -Dupdate-goldens` + review the diff. +New corpus category: `17xx` atomics. + +--- + +## Design (grounded against the tree) + +### Representation — minimal compiler surface +- **`Ordering`** is an ordinary sx enum, zero compiler coupling: + ```sx + Ordering :: enum { relaxed; acquire; release; acq_rel; seq_cst; } // tags 0..4 + ``` +- **`Atomic($T)`** is an ordinary sx **generic struct** (mirrors `List :: struct ($T: Type)` + at [list.sx:5](../library/modules/std/list.sx#L5)), a transparent 1-field wrapper — + atomicity is a property of the *operation*, not the storage, so `Atomic(i64)` has the + exact layout/size/align of `i64`. NO new IR *type*, NO type-system coupling: + ```sx + Atomic :: struct ($T: Type) { + value: T; + init :: (v: T) -> Atomic(T) { return .{ value = v }; } + load :: (self: *Atomic(T), o: Ordering) -> T { return atomic_load(T, @self.value, o); } + store :: (self: *Atomic(T), v: T, o: Ordering) { atomic_store(T, @self.value, v, o); } + } + ``` +- The **operations** are `#builtin` intrinsic free functions, recognized by name at + lower-time (the established pattern — `size_of`/`type_info` in + [`tryLowerReflectionCall`](../src/ir/lower/call.zig#L1672), recognized BEFORE arg lowering): + ```sx + atomic_load :: ($T: Type, ptr: *T, o: Ordering) -> T #builtin; + atomic_store :: ($T: Type, ptr: *T, v: T, o: Ordering) #builtin; + ``` + Explicit `$T` first arg follows the `size_of($T)` / `field_name($T, idx)` mixed + type+value precedent (lowest-risk; the reflection path already resolves type args). + +### Ordering is compile-time-only by construction — and that forces a capability gap +LLVM atomic ordering is an **instruction attribute**, not a runtime operand, so the +ordering MUST be known at emit time. The lower-time handler reads the ordering arg's +variant name statically (it must be a **constant enum literal** `.seq_cst`) and bakes it +into the IR op as a Zig enum field (`AtomicOrdering`). A non-literal ordering is a **loud +diagnostic**, never a silent default (REJECTED-PATTERNS). + +**Discovered gap (grounded):** a generic `Atomic(T)` method `load(self, o: Ordering)` would +forward `o` — a *runtime parameter* — to the intrinsic, where it is NOT a literal. And +**comptime enum value params don't exist** (`$o: Ordering` → `o` is "unresolved" in the +body; `resolveValueParamArg` folds integer constraints only). A runtime dispatch hack +(`if o == { case .acquire: atomic_load(…, .acquire) … }`) also fails: `load` with a +`release`/`acq_rel` ordering is *invalid LLVM*, so the arms can't be uniform. Therefore the +**full ordering surface is blocked on a net-new capability** (comptime-constant ordering +propagation — either comptime enum value params, or compiler-recognized `Atomic` method +calls). That capability is its **own step (A.0.5)**, sequenced before ordering-bearing ops. + +### sx tag → LLVM ordering is EXPLICIT (non-contiguous!) +LLVM's `LLVMAtomicOrdering` is **not** 0..4: `Monotonic=2, Acquire=4, Release=5, +AcquireRelease=6, SequentiallyConsistent=7` ([Core.h:338-354]). The sx `Ordering` tags +(relaxed=0…seq_cst=4) map via an explicit `switch`, never an identity cast: +`relaxed→Monotonic, acquire→Acquire, release→Release, acq_rel→AcquireRelease, +seq_cst→SequentiallyConsistent`. + +### LLVM-C API (verified present in `llvm-c/Core.h`, no new extern decls needed) +- Atomic load = `LLVMBuildLoad2` + `LLVMSetOrdering(v, ord)` + `LLVMSetAlignment(v, size)` + (**alignment is mandatory** on atomic load/store — LLVM verifier rejects atomics without + it). There is **no** `LLVMBuildAtomicLoad`/`Store` (the Explore agent was wrong). +- Atomic store = `LLVMBuildStore` + `LLVMSetOrdering` + `LLVMSetAlignment`. +- (Later) `LLVMBuildAtomicRMW(B, op, ptr, val, ord, singleThread)`, + `LLVMBuildAtomicCmpXchg(B, ptr, cmp, new, succOrd, failOrd, singleThread)`, + `LLVMBuildFence(B, ord, singleThread, name)`, `LLVMSetWeak`. +- `singleThread = 0` (multi-thread / cross-thread ordering). Atomic-eligible `T` = + integer / pointer / float of size 1·2·4·8(·16). **Reject non-scalar / bad-size `T` + loudly** (diagnostic), do not silently emit. + +### Comptime VM treats atomics as ordinary load/store +Comptime is single-threaded, so seq_cst is trivially satisfied — the +[`comptime_vm`](../src/ir/comptime_vm.zig#L659) arms for `atomic_load`/`atomic_store` +reuse the ordinary `load`/`store` paths (correct, NOT a bail). `sx run` JITs via LLVM so +runtime atomics execute the real ops; the VM arm only matters for `#run`/const-init. + +### Files the new IR op variants force (exhaustive switches) +`atomic_load` / `atomic_store` variants must be handled in every `Op` switch or the Zig +build fails (this is the desired tripwire): +- [inst.zig:159](../src/ir/inst.zig#L159) — add `atomic_load: AtomicLoad`, `atomic_store: AtomicStore` + the structs (mirror `Store` at [inst.zig:286](../src/ir/inst.zig#L286)). +- [lower/call.zig:1672](../src/ir/lower/call.zig#L1672) — recognize the intrinsics, emit the ops (new `tryLowerAtomicIntrinsic`, called alongside `tryLowerReflectionCall` at [call.zig:80](../src/ir/lower/call.zig#L80)). +- [print.zig:231](../src/ir/print.zig#L231) — print arms (sx-IR / `ir-dump`). +- [emit_llvm.zig:1566](../src/ir/emit_llvm.zig#L1566) — dispatch arms → ops.zig. +- [backend/llvm/ops.zig:325](../src/backend/llvm/ops.zig#L325) — `emitAtomicLoad`/`emitAtomicStore` (mirror `emitLoad`/`emitStore`). +- [comptime_vm.zig:659](../src/ir/comptime_vm.zig#L659) — arms reusing load/store. +- Any other `.op` switch the Zig compiler flags (module.zig / program_index.zig) — let the build tell you. + +### Test snapshots — the arch-`.ir` requirement is a MISCONCEPTION for atomics +`sx ir` = [`emitIR`](../src/main.zig#L210), which emits **LLVM IR** (respects `--target`); +`sx ir-dump` is the sx-IR printer. At the **LLVM-IR level**, `load atomic i64, ptr %x +seq_cst, align 8` is **arch-invariant** — identical text for x86_64 and aarch64. The +x86-`lock`/MOV vs aarch64-`ldar`/`stlr` divergence happens only at *instruction selection* +(`sx asm`), which the corpus does **not** snapshot. So: +- **A single host `.ir` snapshot** proves the achievable gate (the `load atomic ` + keyword + correct ordering + alignment emitted). PLAN-POST §A / design §10.3's + "arch-gated x86_64 + aarch64 `.ir`" would capture **byte-identical** files — drop it. +- Optionally add ONE cross-arch ir-only example (`.build {"target":"x86_64-linux"}` on an + aarch64 host) purely as a **cross-target-emission-doesn't-crash** smoke — note in its + header that the IR body is identical to host. +- **State loudly (out of snapshot scope, parallel to the ordering-semantics caveat):** + asm-level arch lowering AND weak-memory ordering *semantics* are NOT proven by `.ir`; + those need the Stream-C stress harness, not the corpus. + +--- + +## Phases + +### A.0 — `Atomic($T)` + `Ordering` + **`seq_cst`-only** `load`/`store` ← START HERE +**Scope (descoped per the discovered gap above):** ship the net-new atomic load/store +codegen with a **`seq_cst` literal baked in the method bodies** — `load(self) -> T` / +`store(self, v)` (NO ordering param yet). The intrinsic still carries the full +`AtomicOrdering` field (always `.seq_cst` here); the recognizer + emit handle all five +orderings already, so A.0.5 only has to plumb the *constant* through. Explicit orderings +(`a.load(.acquire)`) land in A.0.5. seq_cst-only is correct (conservative-strongest), not a +silent fallback. + +Two-commit cadence (lock-to-bail → green): + +- **A.0a (lock)** — land the lib + IR plumbing with emit deliberately bailing: + 1. New `library/modules/std/atomic.sx`: `Ordering` enum, `Atomic($T)` struct (value + + `init`/`load`/`store`), `atomic_load`/`atomic_store` `#builtin` decls. **Opt-in import + (`#import "modules/std/atomic.sx"`), NOT carried by the universal `std.sx` facade** — + mirrors `trace`. Rationale (grounded): adding the concrete `Ordering` enum to the + universal prelude registers it into EVERY program's global type table, growing + `@__sx_type_is_unsigned` (378→380) and shifting all string-global numbering → churned + 37 unrelated `.ir` snapshots + bloats every binary. Atomics is a deliberate concurrency + capability, so consumers import it explicitly. + 2. Add IR ops `atomic_load`/`atomic_store` + `AtomicOrdering` + the two op structs + (inst.zig); print arms; comptime_vm arms (reuse load/store); lower recognition + (`tryLowerAtomicIntrinsic`) incl. the const-ordering-literal guard + non-scalar-`T` + reject. + 3. emit_llvm/ops.zig arms **bail loudly** for now: `emitAtomicLoad`/`Store` call the + emitter's bail-with-diagnostic path ("atomic load/store LLVM emission not yet + implemented") so the Zig build is exhaustive but the example is red-by-diagnostic. + 4. Add `examples/1700-atomics-load-store.sx` (construct `Atomic(i64).init`, `store`, + `load`, `print`). Seed marker; capture snapshot = the emit-bail diagnostic (nonzero + exit). `zig build && zig build test` green (matches the locked bail snapshot). Commit. +- **A.0b (green)** — replace the emit bail with real emission: + `LLVMBuildLoad2`+`LLVMSetOrdering`+`LLVMSetAlignment` / `LLVMBuildStore`+`LLVMSetOrdering` + +`LLVMSetAlignment`, ordering via the explicit sx-tag→LLVM `switch`. Regen `1700` to + success output + capture its host `.ir` (asserts `load atomic`/`store atomic` + ordering). + Add a unit test in `emit_llvm.test.zig` (correct op + ordering + alignment emission). + Review the diff (no stray error text). Commit. + +### A.0.5 — comptime-constant ordering propagation (the capability gap) +Enable `a.load(.acquire)` etc. — i.e. an `Ordering` that reaches the intrinsic as a +compile-time constant through a method. Two candidate designs (pick at pickup): +- **(a) comptime enum value params** — make `$o: Ordering` resolve in the body to its + variant tag (extend `comptime_value_bindings`/the typer beyond integers). General, + reusable; larger typer change. +- **(b) compiler-recognized `Atomic` methods** — special-case `Atomic(T).load/store/…` + calls (read the literal ordering arg at the method call site), bounded coupling to the + std `Atomic` type (cf. how `Vector` is special-cased). Smaller; less general. +Also enforce per-op ordering validity (load: relaxed/acquire/seq_cst; store: +relaxed/release/seq_cst; CAS's dual orderings) as **compile errors**, which is exactly what +the constant-ordering path buys. Retrofit the ordering param onto `load`/`store` here. + +### A.1 — RMW: `fetch_add/sub/and/or/xor` + `fetch_min/max` → `atomicrmw` (no `nand`) +One IR op `atomic_rmw` carrying an `RmwKind` (maps to `LLVMAtomicRMWBinOp*`). Signed vs +unsigned min/max picks `Max/Min` vs `UMax/UMin` from `T`'s signedness. Same lock→green +cadence; `17xx` examples. + +### A.2 — `compare_exchange`/`_weak` → `cmpxchg` (returns **`?T`, null = success**) +`atomic_cmpxchg` op (ptr, cmp, new, success_ord, failure_ord, weak). LLVM `cmpxchg` +returns `{T, i1}`; lower to `?T` where **null = success** (extract the i1, invert). +**Validate the two orderings in the compiler** (design §4.6): failure ordering may not be +`release`/`acq_rel` nor stronger than success — loud diagnostic. `_weak` sets `LLVMSetWeak`. + +### A.3 — `swap` + `fence(.ordering)` +`swap` = `atomic_rmw` with `Xchg` kind (folds into A.1's op). `fence` = a new `atomic_fence` +op (ordering only) → `LLVMBuildFence`. `17xx` examples. + +--- + +## Gates (per the corrected snapshot story) +- **unit** `emit_llvm.test.zig`: each op emits the right LLVM builder + ordering + alignment. +- **corpus** `17xx` single-thread deterministic (`sx run`, JIT executes real atomics). +- **host `.ir`** snapshot per op proves the keyword/ordering/alignment lowered. +- **OUT of snapshot scope, stated loudly:** asm-level arch divergence (`sx asm`) and + weak-memory ordering *semantics* — Stream-C stress harness territory, not the corpus. + +## Kickoff prompt (A.0a — paste into a fresh session) +> Implement Stream A step A.0a (atomics lock commit) per `current/PLAN-ATOMICS.md`. Verify +> `zig build && zig build test` is green first. Then: (1) create +> `library/modules/std/atomic.sx` with the `Ordering` enum, `Atomic($T)` struct, and +> `atomic_load`/`atomic_store` `#builtin` decls; wire into `library/modules/std.sx`'s tail. +> (2) Add the `atomic_load`/`atomic_store` IR ops + `AtomicOrdering` + op structs in +> `src/ir/inst.zig`; handle them in every exhaustive `Op` switch the Zig build flags +> (print.zig, comptime_vm.zig reuse load/store, emit_llvm dispatch). (3) Add +> `tryLowerAtomicIntrinsic` in `src/ir/lower/call.zig` (recognize the two builtins, bake the +> const ordering literal into the op, loud-reject non-literal ordering AND non-scalar/bad-size +> `T`). (4) Make `emitAtomicLoad`/`emitAtomicStore` in `src/backend/llvm/ops.zig` BAIL loudly +> ("not yet implemented") this commit. (5) Add `examples/1700-atomics-load-store.sx`, seed the +> marker, capture the bail diagnostic as the locked snapshot, confirm `zig build test` green, +> commit. STOP — A.0b (real emission) is the next step. Do NOT implement emission in the same +> commit that adds the example. diff --git a/examples/1700-atomics-load-store.sx b/examples/1700-atomics-load-store.sx new file mode 100644 index 00000000..4130c5fa --- /dev/null +++ b/examples/1700-atomics-load-store.sx @@ -0,0 +1,17 @@ +// Atomic($T) load/store (seq_cst), single-thread. +// Stream A (atomics) A.0 — the first net-new atomic codegen example. +// Explicit orderings (a.load(.acquire)) arrive in A.0.5; see PLAN-ATOMICS.md. +// Atomics is an opt-in import (not in the universal prelude) — like `trace`. +#import "modules/std.sx"; +#import "modules/std/atomic.sx"; + +main :: () { + a := Atomic(i64).init(7); + print("init: {}\n", a.load()); + + a.store(42); + print("after store: {}\n", a.load()); + + a.store(a.load() + 1); + print("incremented: {}\n", a.load()); +} diff --git a/examples/expected/1700-atomics-load-store.exit b/examples/expected/1700-atomics-load-store.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/expected/1700-atomics-load-store.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1700-atomics-load-store.stderr b/examples/expected/1700-atomics-load-store.stderr new file mode 100644 index 00000000..ef75e5a3 --- /dev/null +++ b/examples/expected/1700-atomics-load-store.stderr @@ -0,0 +1,2 @@ +error: atomic load LLVM emission not yet implemented (Stream A, A.0b) +error: atomic store LLVM emission not yet implemented (Stream A, A.0b) diff --git a/examples/expected/1700-atomics-load-store.stdout b/examples/expected/1700-atomics-load-store.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1700-atomics-load-store.stdout @@ -0,0 +1 @@ + diff --git a/library/modules/std/atomic.sx b/library/modules/std/atomic.sx new file mode 100644 index 00000000..af4812bc --- /dev/null +++ b/library/modules/std/atomic.sx @@ -0,0 +1,45 @@ +// Atomics — `Atomic($T)` wrapper + `Ordering`, over the compiler's atomic +// load/store (later: rmw/cas/fence) IR ops. Consumers reach these through +// std.sx (`Atomic` / `Ordering` re-exports), never by importing this file. +// +// Atomicity is a property of the OPERATION, not the storage: `Atomic(T)` is a +// transparent 1-field wrapper with `T`'s exact layout/size/align. The ops are +// `#builtin` intrinsics recognized by name at lower-time +// (`tryLowerAtomicIntrinsic`, src/ir/lower/call.zig) and emitted as dedicated +// atomic IR ops; the `Ordering` argument MUST be a constant enum literal. +#import "modules/std/core.sx"; + +Ordering :: enum { + relaxed; // → LLVM Monotonic + acquire; // → LLVM Acquire + release; // → LLVM Release + acq_rel; // → LLVM AcquireRelease + seq_cst; // → LLVM SequentiallyConsistent +} + +// Compiler intrinsics. Not called directly by users — `Atomic(T)`'s methods +// forward to them. Recognized by name in lowering; the `Ordering` arg MUST be a +// constant enum literal (a non-literal is a loud diagnostic). +atomic_load :: ($T: Type, ptr: *T, o: Ordering) -> T #builtin; +atomic_store :: ($T: Type, ptr: *T, v: T, o: Ordering) #builtin; + +// NOTE (A.0): the methods bake a literal `.seq_cst` (strongest, conservative) +// rather than taking an `o: Ordering` parameter. A runtime ordering param can't +// reach the intrinsic as the compile-time constant LLVM requires, and comptime +// enum value params don't exist yet — so explicit orderings (`a.load(.acquire)`) +// land in A.0.5 once that capability does. See current/PLAN-ATOMICS.md. +Atomic :: struct ($T: Type) { + value: T; + + init :: (v: T) -> Atomic(T) { + return .{ value = v }; + } + + load :: (self: *Atomic(T)) -> T { + return atomic_load(T, @self.value, .seq_cst); + } + + store :: (self: *Atomic(T), v: T) { + atomic_store(T, @self.value, v, .seq_cst); + } +} diff --git a/src/backend/llvm/ops.zig b/src/backend/llvm/ops.zig index 46947e77..5f753983 100644 --- a/src/backend/llvm/ops.zig +++ b/src/backend/llvm/ops.zig @@ -15,6 +15,8 @@ const FieldAccess = ir_inst.FieldAccess; const EnumInit = ir_inst.EnumInit; const Subslice = ir_inst.Subslice; const Store = ir_inst.Store; +const AtomicLoad = ir_inst.AtomicLoad; +const AtomicStore = ir_inst.AtomicStore; const Conversion = ir_inst.Conversion; const GlobalId = ir_inst.GlobalId; const GlobalSet = ir_inst.GlobalSet; @@ -363,6 +365,29 @@ pub const Ops = struct { self.e.advanceRefCounter(); } + // ── Atomics ─────────────────────────────────────────── + // A.0a (Stream A) lock: the IR ops, lowering, and comptime VM are wired, + // but LLVM emission deliberately BAILS LOUDLY (clean diagnostic + build + // abort via `comptime_failed`) rather than silently emitting a non-atomic + // load/store. A.0b replaces these bodies with the real builders: + // load: LLVMBuildLoad2 + LLVMSetOrdering + LLVMSetAlignment + // store: LLVMBuildStore + LLVMSetOrdering + LLVMSetAlignment + // (ordering via an explicit sx-tag → LLVMAtomicOrdering switch). + pub fn emitAtomicLoad(self: Ops, instruction: *const Inst, a: AtomicLoad) void { + _ = a; + std.debug.print("error: atomic load LLVM emission not yet implemented (Stream A, A.0b)\n", .{}); + self.e.comptime_failed = true; + // Keep emit from crashing downstream: yield an undef of the result type. + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(if (instruction.ty == .void) .i64 else instruction.ty))); + } + + pub fn emitAtomicStore(self: Ops, a: AtomicStore) void { + _ = a; + std.debug.print("error: atomic store LLVM emission not yet implemented (Stream A, A.0b)\n", .{}); + self.e.comptime_failed = true; + self.e.advanceRefCounter(); + } + // ── Globals ─────────────────────────────────────────── pub fn emitGlobalGet(self: Ops, instruction: *const Inst, gid: GlobalId) void { const llvm_global = self.e.global_map.get(gid.index()) orelse { diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index 042bf18b..b8e9bc89 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -670,6 +670,20 @@ pub const Vm = struct { try self.writeField(table, frame.get(s.ptr.index()), vty, frame.get(s.val.index())); return .{ .value = 0 }; // store has a void result but still occupies a Ref slot }, + // Comptime is single-threaded, so seq_cst is trivially satisfied — + // atomic load/store are ordinary load/store here (the ordering is + // a no-op at comptime). Mirrors the design (§3): the interp needs no + // atomics machinery. + .atomic_load => |a| { + const table = try self.requireTable(); + return .{ .value = try self.readField(table, frame.get(a.ptr.index()), ins.ty) }; + }, + .atomic_store => |a| { + const table = try self.requireTable(); + const vty = if (a.val_ty != .void) a.val_ty else (try self.refTy(ref_types, a.val)); + try self.writeField(table, frame.get(a.ptr.index()), vty, frame.get(a.val.index())); + return .{ .value = 0 }; + }, .struct_init => |agg| { const table = try self.requireTable(); const sty = ins.ty; diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index c2580ac0..c6dcdab8 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1565,6 +1565,8 @@ pub const LLVMEmitter = struct { .alloca => |elem_ty| self.ops().emitAlloca(elem_ty), .load => |un| self.ops().emitLoad(instruction, un), .store => |st| self.ops().emitStore(st), + .atomic_load => |a| self.ops().emitAtomicLoad(instruction, a), + .atomic_store => |a| self.ops().emitAtomicStore(a), // ── Globals ─────────────────────────────────────────── .global_get => |gid| self.ops().emitGlobalGet(instruction, gid), .global_addr => |gid| self.ops().emitGlobalAddr(gid), diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 46e93e83..969b19d4 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -161,6 +161,10 @@ pub const Op = union(enum) { load: UnaryOp, // load from pointer store: Store, // store value to pointer + // ── Atomics ───────────────────────────────────────────────────── + atomic_load: AtomicLoad, // atomic load from pointer with memory ordering + atomic_store: AtomicStore, // atomic store to pointer with memory ordering + // ── Struct ops ────────────────────────────────────────────────── struct_init: Aggregate, // construct struct from field values struct_get: FieldAccess, // read struct field by index @@ -294,6 +298,27 @@ pub const Store = struct { val_ty: TypeId = .void, }; +/// Memory ordering for atomic ops. The sx-surface `Ordering` enum +/// (`relaxed`/`acquire`/`release`/`acq_rel`/`seq_cst`) is read statically at +/// lower-time (the arg MUST be a constant enum literal) and baked here, so the +/// op carries no runtime ordering operand. The LLVM mapping is EXPLICIT (LLVM's +/// `LLVMAtomicOrdering` is non-contiguous: Monotonic=2/Acquire=4/…/SeqCst=7) — +/// never an identity cast. +pub const AtomicOrdering = enum { relaxed, acquire, release, acq_rel, seq_cst }; + +pub const AtomicLoad = struct { + ptr: Ref, + ordering: AtomicOrdering, +}; + +pub const AtomicStore = struct { + ptr: Ref, + val: Ref, + /// Declared type of the stored value (same role as `Store.val_ty`). + val_ty: TypeId = .void, + ordering: AtomicOrdering, +}; + pub const Conversion = struct { operand: Ref, from: TypeId, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 20204869..ca3d67f3 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1836,6 +1836,7 @@ pub const Lowering = struct { pub const hasCastWithRuntimeType = lower_call.hasCastWithRuntimeType; pub const lowerRuntimeDispatchCall = lower_call.lowerRuntimeDispatchCall; pub const tryLowerReflectionCall = lower_call.tryLowerReflectionCall; + pub const tryLowerAtomicIntrinsic = lower_call.tryLowerAtomicIntrinsic; pub const reflectionArgIsType = lower_call.reflectionArgIsType; pub const reflectionTypeArgGuard = lower_call.reflectionTypeArgGuard; pub const reflectionErrorSentinel = lower_call.reflectionErrorSentinel; diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index e0533899..0d495aea 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -80,6 +80,9 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref { // Check reflection builtins first (before lowering args — some args are type names, not values) if (c.callee.data == .identifier) { if (self.tryLowerReflectionCall(c.callee.data.identifier.name, c)) |ref| return ref; + // Atomic intrinsics (atomic_load/atomic_store): a type arg + value args, + // so lower them here (before generic arg lowering) like reflection calls. + if (self.tryLowerAtomicIntrinsic(c.callee.data.identifier.name, c)) |ref| return ref; } // Check for runtime dispatch pattern BEFORE lowering args. @@ -1667,6 +1670,62 @@ pub fn lowerRuntimeDispatchCall( return self.builder.constInt(0, .void); } +/// Map a bare ordering enum literal (`.seq_cst`) to the IR `AtomicOrdering`. +/// Returns null for anything that is not one of the five constant literals — +/// the caller turns that into a loud "must be a constant ordering literal" +/// diagnostic (never a silent default). +fn atomicOrderingFromNode(node: *const Node) ?inst_mod.AtomicOrdering { + if (node.data != .enum_literal) return null; + const n = node.data.enum_literal.name; + if (std.mem.eql(u8, n, "relaxed")) return .relaxed; + if (std.mem.eql(u8, n, "acquire")) return .acquire; + if (std.mem.eql(u8, n, "release")) return .release; + if (std.mem.eql(u8, n, "acq_rel")) return .acq_rel; + if (std.mem.eql(u8, n, "seq_cst")) return .seq_cst; + return null; +} + +/// Recognize the atomic `#builtin` intrinsics and lower them to dedicated atomic +/// IR ops: +/// atomic_load($T, ptr: *T, o: Ordering) -> T +/// atomic_store($T, ptr: *T, v: T, o: Ordering) +/// The `Ordering` arg MUST be a constant enum literal — read statically here and +/// baked into the op (the op carries no runtime ordering operand). `T` must be a +/// scalar of size 1/2/4/8/16. Both constraints are loud diagnostics, never silent +/// defaults. Returns null if `name` is not an atomic intrinsic. +pub fn tryLowerAtomicIntrinsic(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { + const is_load = std.mem.eql(u8, name, "atomic_load"); + const is_store = std.mem.eql(u8, name, "atomic_store"); + if (!is_load and !is_store) return null; + + const expected: usize = if (is_load) 3 else 4; // ($T, ptr[, val], ordering) + if (c.args.len != expected) { + if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "{s} expects {d} arguments", .{ name, expected }); + return Ref.none; + } + + const elem_ty = self.resolveTypeArg(c.args[0]); + const size = self.typeSizeBytes(elem_ty); + if (size != 1 and size != 2 and size != 4 and size != 8 and size != 16) { + if (self.diagnostics) |d| d.addFmt(.err, c.args[0].span, "atomic ops require a scalar type of size 1/2/4/8/16 bytes — '{s}' is {d} bytes", .{ self.formatTypeName(elem_ty), size }); + return Ref.none; + } + + const ord_node = c.args[expected - 1]; + const ordering = atomicOrderingFromNode(ord_node) orelse { + if (self.diagnostics) |d| d.addFmt(.err, ord_node.span, "atomic ordering must be a constant ordering literal (.relaxed / .acquire / .release / .acq_rel / .seq_cst)", .{}); + return Ref.none; + }; + + const ptr = self.lowerExpr(c.args[1]); + if (is_load) { + return self.builder.emit(.{ .atomic_load = .{ .ptr = ptr, .ordering = ordering } }, elem_ty); + } + const val = self.lowerExpr(c.args[2]); + self.builder.emitVoid(.{ .atomic_store = .{ .ptr = ptr, .val = val, .val_ty = elem_ty, .ordering = ordering } }, .void); + return Ref.none; // store has a void result +} + /// Try to lower a call as a reflection builtin (expanded inline during lowering). /// Returns null if the call is not a recognized reflection builtin. pub fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref { diff --git a/src/ir/print.zig b/src/ir/print.zig index 95b617bc..0acad841 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -233,6 +233,11 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write try writer.print("store %{d}, %{d}\n", .{ s.ptr.index(), s.val.index() }); return; }, + .atomic_load => |a| try writer.print("atomic_load %{d} {s} : ", .{ a.ptr.index(), @tagName(a.ordering) }), + .atomic_store => |a| { + try writer.print("atomic_store %{d}, %{d} {s}\n", .{ a.ptr.index(), a.val.index(), @tagName(a.ordering) }); + return; + }, // ── Struct ops ────────────────────────────────────────── .struct_init => |agg| { try writer.writeAll("struct_init [");