From 718f27e27f03c4216932cdceeae1089c4d44b0c7 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 20 Jun 2026 10:14:49 +0300 Subject: [PATCH] atomics A.1a: RMW ops + recognizer + methods, emit bails (lock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetch_add/sub/and/or/xor/min/max wired end-to-end except LLVM emission (bails loudly; A.1b makes it real). New IR op atomic_rmw + RmwKind (no nand) + AtomicRmw{ptr, operand, val_ty, ordering, kind}. print arm; comptime_vm arm implements real single-thread RMW (load/compute/store/return-old, signed|unsigned min/max from val_ty). Recognizer extended (rmwKindFromName) — RMW restricted to integer T (float fadd / pointer RMW out of scope, rejected loudly); all orderings valid for RMW. Methods fetch_* on Atomic($T) with comptime $o: Ordering. examples/1701 locked to the bail. Suite green (716/0). --- examples/1701-atomics-rmw.sx | 22 ++++++++++++++ examples/expected/1701-atomics-rmw.exit | 1 + examples/expected/1701-atomics-rmw.stderr | 7 +++++ examples/expected/1701-atomics-rmw.stdout | 1 + library/modules/std/atomic.sx | 19 ++++++++++++ src/backend/llvm/ops.zig | 10 ++++++ src/ir/comptime_vm.zig | 27 +++++++++++++++++ src/ir/emit_llvm.zig | 1 + src/ir/inst.zig | 15 +++++++++ src/ir/lower/call.zig | 37 +++++++++++++++++++++-- src/ir/print.zig | 1 + 11 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 examples/1701-atomics-rmw.sx create mode 100644 examples/expected/1701-atomics-rmw.exit create mode 100644 examples/expected/1701-atomics-rmw.stderr create mode 100644 examples/expected/1701-atomics-rmw.stdout diff --git a/examples/1701-atomics-rmw.sx b/examples/1701-atomics-rmw.sx new file mode 100644 index 00000000..60a65b8f --- /dev/null +++ b/examples/1701-atomics-rmw.sx @@ -0,0 +1,22 @@ +// Atomic($T) read-modify-write: fetch_add/sub/and/or/xor/min/max → LLVM atomicrmw. +// Each returns the OLD value. Stream A (atomics) A.1. Single-thread. +#import "modules/std.sx"; +#import "modules/std/atomic.sx"; + +main :: () { + a := Atomic(i64).init(10); + print("old add: {}\n", a.fetch_add(5, .seq_cst)); // returns 10, now 15 + print("old sub: {}\n", a.fetch_sub(3, .acq_rel)); // returns 15, now 12 + print("now: {}\n", a.load(.acquire)); // 12 + + b := Atomic(i64).init(0xF0); + print("old and: {}\n", b.fetch_and(0x3C, .relaxed));// returns 0xF0(240), now 0x30(48) + print("old or: {}\n", b.fetch_or(0x03, .relaxed)); // returns 0x30(48), now 0x33(51) + print("old xor: {}\n", b.fetch_xor(0x0F, .relaxed));// returns 0x33(51), now 0x3C(60) + print("now: {}\n", b.load(.relaxed)); // 60 + + m := Atomic(i64).init(20); + print("old min: {}\n", m.fetch_min(8, .seq_cst)); // returns 20, now 8 + print("old max: {}\n", m.fetch_max(15, .seq_cst)); // returns 8, now 15 + print("now: {}\n", m.load(.seq_cst)); // 15 +} diff --git a/examples/expected/1701-atomics-rmw.exit b/examples/expected/1701-atomics-rmw.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/expected/1701-atomics-rmw.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1701-atomics-rmw.stderr b/examples/expected/1701-atomics-rmw.stderr new file mode 100644 index 00000000..6711200f --- /dev/null +++ b/examples/expected/1701-atomics-rmw.stderr @@ -0,0 +1,7 @@ +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) +error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b) diff --git a/examples/expected/1701-atomics-rmw.stdout b/examples/expected/1701-atomics-rmw.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1701-atomics-rmw.stdout @@ -0,0 +1 @@ + diff --git a/library/modules/std/atomic.sx b/library/modules/std/atomic.sx index 30049728..89441f1a 100644 --- a/library/modules/std/atomic.sx +++ b/library/modules/std/atomic.sx @@ -23,6 +23,16 @@ Ordering :: enum { atomic_load :: ($T: Type, ptr: *T, o: Ordering) -> T #builtin; atomic_store :: ($T: Type, ptr: *T, v: T, o: Ordering) #builtin; +// Read-modify-write intrinsics — integer T only. Each returns the OLD value. +// `min`/`max` are signed or unsigned per T. (No `nand`.) +atomic_fetch_add :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; +atomic_fetch_sub :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; +atomic_fetch_and :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; +atomic_fetch_or :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; +atomic_fetch_xor :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; +atomic_fetch_min :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; +atomic_fetch_max :: ($T: Type, ptr: *T, operand: T, o: Ordering) -> T #builtin; + // The ordering is a COMPTIME value param (`$o`): it must be known at compile // time because LLVM atomic ordering is an instruction attribute, not a runtime // operand. It is explicit (Rust-style — no default), so the caller always states @@ -42,4 +52,13 @@ Atomic :: struct ($T: Type) { store :: (self: *Atomic(T), v: T, $o: Ordering) { atomic_store(T, @self.value, v, o); } + + // Read-modify-write (integer T). Each returns the value BEFORE the update. + fetch_add :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_add(T, @self.value, v, o); } + fetch_sub :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_sub(T, @self.value, v, o); } + fetch_and :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_and(T, @self.value, v, o); } + fetch_or :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_or(T, @self.value, v, o); } + fetch_xor :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_xor(T, @self.value, v, o); } + fetch_min :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_min(T, @self.value, v, o); } + fetch_max :: (self: *Atomic(T), v: T, $o: Ordering) -> T { return atomic_fetch_max(T, @self.value, v, o); } } diff --git a/src/backend/llvm/ops.zig b/src/backend/llvm/ops.zig index bea3cc0f..77f13585 100644 --- a/src/backend/llvm/ops.zig +++ b/src/backend/llvm/ops.zig @@ -17,6 +17,7 @@ const Subslice = ir_inst.Subslice; const Store = ir_inst.Store; const AtomicLoad = ir_inst.AtomicLoad; const AtomicStore = ir_inst.AtomicStore; +const AtomicRmw = ir_inst.AtomicRmw; const Conversion = ir_inst.Conversion; const GlobalId = ir_inst.GlobalId; const GlobalSet = ir_inst.GlobalSet; @@ -395,6 +396,15 @@ pub const Ops = struct { } } + // A.1a (Stream A) lock: emission BAILS LOUDLY until A.1b wires the real + // LLVMBuildAtomicRMW (binop from kind; signed/unsigned Min/Max from val_ty). + pub fn emitAtomicRmw(self: Ops, instruction: *const Inst, a: AtomicRmw) void { + _ = a; + std.debug.print("error: atomic rmw LLVM emission not yet implemented (Stream A, A.1b)\n", .{}); + self.e.comptime_failed = true; + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(if (instruction.ty == .void) .i64 else instruction.ty))); + } + pub fn emitAtomicStore(self: Ops, a: AtomicStore) void { const ptr = self.e.resolveRef(a.ptr); var val = self.e.resolveRef(a.val); diff --git a/src/ir/comptime_vm.zig b/src/ir/comptime_vm.zig index b8e9bc89..47a588bd 100644 --- a/src/ir/comptime_vm.zig +++ b/src/ir/comptime_vm.zig @@ -684,6 +684,33 @@ pub const Vm = struct { try self.writeField(table, frame.get(a.ptr.index()), vty, frame.get(a.val.index())); return .{ .value = 0 }; }, + // RMW at comptime (single-thread): load old, compute new, store new, + // return old — the ordering is a no-op. min/max pick signed vs + // unsigned compare from the value type. + .atomic_rmw => |a| { + const table = try self.requireTable(); + const vty = if (a.val_ty != .void) a.val_ty else ins.ty; + const old = try self.readField(table, frame.get(a.ptr.index()), ins.ty); + const operand = frame.get(a.operand.index()); + const new_val: Reg = switch (a.kind) { + .add => old +% operand, + .sub => old -% operand, + .@"and" => old & operand, + .@"or" => old | operand, + .xor => old ^ operand, + .min, .max => blk: { + const want_max = a.kind == .max; + if (table.isUnsignedInt(vty)) { + const uo: u64 = @bitCast(old); + const up: u64 = @bitCast(operand); + break :blk @bitCast(if (want_max) @max(uo, up) else @min(uo, up)); + } + break :blk if (want_max) @max(old, operand) else @min(old, operand); + }, + }; + try self.writeField(table, frame.get(a.ptr.index()), vty, new_val); + return .{ .value = old }; + }, .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 c6dcdab8..e434c7a8 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1567,6 +1567,7 @@ pub const LLVMEmitter = struct { .store => |st| self.ops().emitStore(st), .atomic_load => |a| self.ops().emitAtomicLoad(instruction, a), .atomic_store => |a| self.ops().emitAtomicStore(a), + .atomic_rmw => |a| self.ops().emitAtomicRmw(instruction, 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 969b19d4..e557f54e 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -164,6 +164,7 @@ pub const Op = union(enum) { // ── Atomics ───────────────────────────────────────────────────── atomic_load: AtomicLoad, // atomic load from pointer with memory ordering atomic_store: AtomicStore, // atomic store to pointer with memory ordering + atomic_rmw: AtomicRmw, // atomic read-modify-write; result is the OLD value // ── Struct ops ────────────────────────────────────────────────── struct_init: Aggregate, // construct struct from field values @@ -319,6 +320,20 @@ pub const AtomicStore = struct { ordering: AtomicOrdering, }; +/// Atomic read-modify-write operation kind. `min`/`max` pick the signed vs +/// unsigned LLVM op (`Min`/`Max` vs `UMin`/`UMax`) from the value type's +/// signedness at emit time. No `nand` (deliberately omitted). +pub const RmwKind = enum { add, sub, @"and", @"or", xor, min, max }; + +pub const AtomicRmw = struct { + ptr: Ref, + operand: Ref, + /// Declared type of the operand / result (drives byte width + signedness). + val_ty: TypeId = .void, + ordering: AtomicOrdering, + kind: RmwKind, +}; + pub const Conversion = struct { operand: Ref, from: TypeId, diff --git a/src/ir/lower/call.zig b/src/ir/lower/call.zig index 8369f1d5..0ba01c05 100644 --- a/src/ir/lower/call.zig +++ b/src/ir/lower/call.zig @@ -1725,9 +1725,11 @@ fn atomicOrderingFromNode(self: *Lowering, node: *const Node) ?inst_mod.AtomicOr 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 rmw_kind = rmwKindFromName(name); // atomic_fetch_add/sub/and/or/xor/min/max + if (!is_load and !is_store and rmw_kind == null) return null; - const expected: usize = if (is_load) 3 else 4; // ($T, ptr[, val], ordering) + // ($T, ptr[, operand/val], ordering): load=3, store/rmw=4. + const expected: usize = if (is_load) 3 else 4; if (c.args.len != expected) { if (self.diagnostics) |d| d.addFmt(.err, c.callee.span, "{s} expects {d} arguments", .{ name, expected }); return Ref.none; @@ -1748,6 +1750,19 @@ pub fn tryLowerAtomicIntrinsic(self: *Lowering, name: []const u8, c: *const ast. if (self.diagnostics) |d| d.addFmt(.err, c.args[0].span, "atomic ops require a scalar type (integer/float/bool/pointer/enum/vector) of size 1/2/4/8/16 bytes — '{s}' is not eligible", .{self.formatTypeName(elem_ty)}); return Ref.none; } + // RMW (A.1) is restricted to INTEGER types: arithmetic/bitwise/min-max on + // floats (fadd/fsub) and pointers is out of scope — reject loudly rather + // than emit invalid LLVM. + if (rmw_kind != null) { + const int_ok = switch (self.module.types.get(elem_ty)) { + .signed, .unsigned => true, + else => false, + }; + if (!int_ok) { + if (self.diagnostics) |d| d.addFmt(.err, c.args[0].span, "atomic read-modify-write requires an integer type — '{s}' is not eligible", .{self.formatTypeName(elem_ty)}); + return Ref.none; + } + } const ord_node = c.args[expected - 1]; const ordering = atomicOrderingFromNode(self, ord_node) orelse { @@ -1755,7 +1770,7 @@ pub fn tryLowerAtomicIntrinsic(self: *Lowering, name: []const u8, c: *const ast. return Ref.none; }; // Per-op ordering validity (LLVM rejects these). A load can't release; a - // store can't acquire; neither can acq_rel. Loud diagnostic, not invalid IR. + // store can't acquire; neither can acq_rel. (RMW accepts all orderings.) if (is_load and (ordering == .release or ordering == .acq_rel)) { if (self.diagnostics) |d| d.addFmt(.err, ord_node.span, "atomic load ordering cannot be .release or .acq_rel (use .relaxed / .acquire / .seq_cst)", .{}); return Ref.none; @@ -1770,10 +1785,26 @@ pub fn tryLowerAtomicIntrinsic(self: *Lowering, name: []const u8, c: *const ast. return self.builder.emit(.{ .atomic_load = .{ .ptr = ptr, .ordering = ordering } }, elem_ty); } const val = self.lowerExpr(c.args[2]); + if (rmw_kind) |kind| { + // RMW returns the OLD value (result type = T). + return self.builder.emit(.{ .atomic_rmw = .{ .ptr = ptr, .operand = val, .val_ty = elem_ty, .ordering = ordering, .kind = kind } }, elem_ty); + } self.builder.emitVoid(.{ .atomic_store = .{ .ptr = ptr, .val = val, .val_ty = elem_ty, .ordering = ordering } }, .void); return Ref.none; // store has a void result } +/// Map an `atomic_fetch_*` intrinsic name to its RMW kind (null if not one). +fn rmwKindFromName(name: []const u8) ?inst_mod.RmwKind { + if (std.mem.eql(u8, name, "atomic_fetch_add")) return .add; + if (std.mem.eql(u8, name, "atomic_fetch_sub")) return .sub; + if (std.mem.eql(u8, name, "atomic_fetch_and")) return .@"and"; + if (std.mem.eql(u8, name, "atomic_fetch_or")) return .@"or"; + if (std.mem.eql(u8, name, "atomic_fetch_xor")) return .xor; + if (std.mem.eql(u8, name, "atomic_fetch_min")) return .min; + if (std.mem.eql(u8, name, "atomic_fetch_max")) return .max; + return null; +} + /// 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 0acad841..70980fe0 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -238,6 +238,7 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write try writer.print("atomic_store %{d}, %{d} {s}\n", .{ a.ptr.index(), a.val.index(), @tagName(a.ordering) }); return; }, + .atomic_rmw => |a| try writer.print("atomic_rmw.{s} %{d}, %{d} {s} : ", .{ @tagName(a.kind), a.ptr.index(), a.operand.index(), @tagName(a.ordering) }), // ── Struct ops ────────────────────────────────────────── .struct_init => |agg| { try writer.writeAll("struct_init [");