atomics A.0a: lib + IR ops + recognizer, emit bails (lock commit)
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.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 [");
|
||||
|
||||
Reference in New Issue
Block a user