atomics A.1a: RMW ops + recognizer + methods, emit bails (lock)
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).
This commit is contained in:
22
examples/1701-atomics-rmw.sx
Normal file
22
examples/1701-atomics-rmw.sx
Normal file
@@ -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
|
||||
}
|
||||
1
examples/expected/1701-atomics-rmw.exit
Normal file
1
examples/expected/1701-atomics-rmw.exit
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
7
examples/expected/1701-atomics-rmw.stderr
Normal file
7
examples/expected/1701-atomics-rmw.stderr
Normal file
@@ -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)
|
||||
1
examples/expected/1701-atomics-rmw.stdout
Normal file
1
examples/expected/1701-atomics-rmw.stdout
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 [");
|
||||
|
||||
Reference in New Issue
Block a user