Any assignment / compound-assignment whose target chain is ROOTED at a
constant — a const-flagged global (array consts, #run consts) or a
module value const (struct consts incl.) — diagnoses 'cannot assign
through constant X' at compile time. A struct const's field write used
to compile and bus-error at runtime (issue 0116); scalars misfired
silently. A deref along the chain (p.*) breaks the root — pointer
writes stay the documented escape until the const-ness steps; a local
shadowing the const name stays writable.
Also: typed struct constants ('W : Color : Color.{...}') register —
the shape list skipped struct_literal, leaving the typed form
unresolved while the untyped one worked.
Examples: 1162 (all rejection shapes incl. the 0116 crash repro),
0178 (typed struct const reads + copy independence).
1319 lines
62 KiB
Zig
1319 lines
62 KiB
Zig
const std = @import("std");
|
|
const ast = @import("../../ast.zig");
|
|
const Node = ast.Node;
|
|
const types = @import("../types.zig");
|
|
const inst_mod = @import("../inst.zig");
|
|
const mod_mod = @import("../module.zig");
|
|
const errors = @import("../../errors.zig");
|
|
|
|
const TypeId = types.TypeId;
|
|
const Ref = inst_mod.Ref;
|
|
const Module = mod_mod.Module;
|
|
|
|
const lower = @import("../lower.zig");
|
|
const Lowering = lower.Lowering;
|
|
const Scope = lower.Scope;
|
|
|
|
pub fn lowerBlock(self: *Lowering, node: *const Node) void {
|
|
switch (node.data) {
|
|
.block => |blk| {
|
|
// Create a child scope for block-level variable shadowing
|
|
var block_scope = Scope.init(self.alloc, self.scope);
|
|
const saved_scope = self.scope;
|
|
self.scope = &block_scope;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.emitBlockDefers(saved_defer_len);
|
|
self.scope = saved_scope;
|
|
block_scope.deinit();
|
|
}
|
|
for (blk.stmts) |stmt| {
|
|
if (self.block_terminated) break;
|
|
self.lowerStmt(stmt);
|
|
// A bare `return`/`raise` mid-block terminates the current
|
|
// basic block but deliberately does NOT set `block_terminated`
|
|
// (that flag would leak past an `if cond { return }` merge
|
|
// block, skipping its trailing statements — see lowerReturn).
|
|
// Stop here so dead statements after the terminator aren't
|
|
// emitted into an already-closed block (invalid LLVM IR).
|
|
if (self.currentBlockHasTerminator()) break;
|
|
}
|
|
},
|
|
else => {
|
|
// Single expression as body (arrow functions)
|
|
self.lowerStmt(node);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Lower an `inline if` branch — block body emits statements, expression returns value.
|
|
pub fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref {
|
|
if (node.data == .block) {
|
|
self.lowerBlock(node);
|
|
// A `return` inside the branch terminates the current LLVM block; propagate
|
|
// that up so the enclosing block lowering stops emitting fall-through.
|
|
if (self.currentBlockHasTerminator()) {
|
|
self.block_terminated = true;
|
|
return .none;
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
return self.lowerExpr(node);
|
|
}
|
|
|
|
/// Lower a block and return the last expression's value (for implicit returns).
|
|
pub fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref {
|
|
// Set force_block_value so nested if-else expressions produce values
|
|
const saved = self.force_block_value;
|
|
self.force_block_value = true;
|
|
defer self.force_block_value = saved;
|
|
|
|
switch (node.data) {
|
|
.block => |blk| {
|
|
if (blk.stmts.len == 0) return null;
|
|
// Create a child scope for block-level variable shadowing
|
|
var block_scope = Scope.init(self.alloc, self.scope);
|
|
const saved_scope = self.scope;
|
|
self.scope = &block_scope;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.emitBlockDefers(saved_defer_len);
|
|
self.scope = saved_scope;
|
|
block_scope.deinit();
|
|
}
|
|
// A block whose last statement is `;`-terminated (or not an
|
|
// expression) discards its value: lower every statement as a
|
|
// statement and yield nothing.
|
|
if (!blk.produces_value) {
|
|
self.force_block_value = false;
|
|
for (blk.stmts) |stmt| {
|
|
if (self.block_terminated) return null;
|
|
self.lowerStmt(stmt);
|
|
if (self.currentBlockHasTerminator()) return null;
|
|
}
|
|
return null;
|
|
}
|
|
// Lower all statements except the last normally
|
|
self.force_block_value = false; // don't force for non-last statements
|
|
for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| {
|
|
if (self.block_terminated) return null;
|
|
self.lowerStmt(stmt);
|
|
// A bare `return`/`raise` mid-block closes the current basic
|
|
// block (without setting `block_terminated`); the remaining
|
|
// statements — including the value-expr — are dead.
|
|
if (self.currentBlockHasTerminator()) return null;
|
|
}
|
|
if (self.block_terminated) return null;
|
|
// Last statement (no trailing `;`): its value is the block's.
|
|
self.force_block_value = true;
|
|
const last = blk.stmts[blk.stmts.len - 1];
|
|
return self.tryLowerAsExpr(last);
|
|
},
|
|
else => {
|
|
// Single expression as body (arrow functions)
|
|
return self.tryLowerAsExpr(node);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Lower a value-returning function body and emit the implicit return.
|
|
/// Emits a hard error when the body yields no value — its last statement is
|
|
/// `;`-terminated (value discarded) or void — and the body doesn't already
|
|
/// terminate via `return`/`raise`. Replaces the old silent default-return.
|
|
pub fn lowerValueBody(self: *Lowering, body: *const Node, ret_ty: TypeId) void {
|
|
const body_val = self.lowerBlockValue(body);
|
|
if (self.currentBlockHasTerminator()) return;
|
|
if (body_val) |val| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (val_ty != .void) {
|
|
const coerced = self.coerceToType(val, val_ty, ret_ty);
|
|
self.builder.ret(coerced, ret_ty);
|
|
return;
|
|
}
|
|
}
|
|
// A PURE-failable function (`-> !` / `-> !Named`, whose entire return IS
|
|
// the error channel) carries no success value — a void body is a normal
|
|
// success exit, not a missing value. `ensureTerminator` emits the
|
|
// error-slot-zero success return.
|
|
if (self.errorChannelOf(ret_ty)) |chan| {
|
|
if (chan == ret_ty) {
|
|
self.ensureTerminator(ret_ty);
|
|
return;
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
if (body.data == .block and body.data.block.discarded_semi != null) {
|
|
diags.addFmt(.err, body.data.block.discarded_semi.?, "function returns '{s}' but the last expression's value is discarded by this `;` — drop the `;` to return it (or use an explicit `return`)", .{self.formatTypeName(ret_ty)});
|
|
} else {
|
|
const span = blk: {
|
|
if (body.data == .block) {
|
|
const stmts = body.data.block.stmts;
|
|
if (stmts.len > 0) break :blk stmts[stmts.len - 1].span;
|
|
}
|
|
break :blk body.span;
|
|
};
|
|
diags.addFmt(.err, span, "function returns '{s}' but its body produces no value — end it with a trailing expression (no `;`) or an explicit `return`", .{self.formatTypeName(ret_ty)});
|
|
}
|
|
}
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
|
|
/// Try to lower a node as an expression, returning its value.
|
|
/// Statement nodes are lowered as statements (returning null).
|
|
pub fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref {
|
|
return switch (node.data) {
|
|
.var_decl, .const_decl, .fn_decl, .return_stmt, .raise_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => {
|
|
self.lowerStmt(node);
|
|
return null;
|
|
},
|
|
else => self.lowerExpr(node),
|
|
};
|
|
}
|
|
|
|
pub fn lowerStmt(self: *Lowering, node: *const Node) void {
|
|
// Stamp this statement's span onto its instructions (ERR E3.0); see
|
|
// `lowerExpr`.
|
|
const saved_span = self.builder.current_span;
|
|
defer self.builder.current_span = saved_span;
|
|
if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end };
|
|
switch (node.data) {
|
|
.var_decl => |vd| self.lowerVarDecl(&vd),
|
|
.const_decl => |cd| self.lowerConstDecl(&cd),
|
|
.fn_decl => |fd| self.lowerLocalFnDecl(&fd),
|
|
.return_stmt => |rs| self.lowerReturn(&rs),
|
|
.raise_stmt => |rs| self.lowerRaise(&rs, node.span),
|
|
.assignment => |asgn| self.lowerAssignment(&asgn),
|
|
.defer_stmt => |ds| self.lowerDefer(&ds),
|
|
.onfail_stmt => |ofs| self.lowerOnFail(&ofs, node.span),
|
|
.push_stmt => |ps| self.lowerPush(&ps),
|
|
.multi_assign => |ma| self.lowerMultiAssign(&ma),
|
|
.destructure_decl => |dd| self.lowerDestructureDecl(&dd),
|
|
.insert_expr => |ins| self.lowerInsertExpr(ins.expr),
|
|
.block => self.lowerBlock(node),
|
|
.jni_env_block => |eb| {
|
|
// Compile-time stack push for lexical-direct env resolution
|
|
// (2.16b — `#jni_call` in the same fn picks up env from
|
|
// jni_env_stack directly, no TL read).
|
|
//
|
|
// Runtime TL save/set/restore (2.16c) for cross-function
|
|
// helpers: callees in OTHER fns invoked from inside the
|
|
// body read the slot via `sx_jni_env_tl_get`. Storage
|
|
// lives in a separately-linked C helper (see
|
|
// library/vendors/sx_jni_runtime/sx_jni_env_tl.c) so the
|
|
// JIT doesn't need orc_rt for TLS.
|
|
const env_ref = self.lowerExpr(eb.env);
|
|
const fids = self.getJniEnvTlFids();
|
|
const ptr_ty = self.module.types.ptrTo(.void);
|
|
const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
|
const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void);
|
|
self.jni_env_stack.append(self.alloc, env_ref) catch unreachable;
|
|
self.lowerBlock(eb.body);
|
|
_ = self.jni_env_stack.pop();
|
|
const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void);
|
|
},
|
|
// Block-local type declarations
|
|
.struct_decl => |sd| {
|
|
self.recordLocalTypeName(sd.name);
|
|
self.registerStructDecl(&node.data.struct_decl, node.source_file orelse self.current_source_file);
|
|
},
|
|
.enum_decl => {
|
|
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
|
|
self.registerEnumDecl(&node.data.enum_decl);
|
|
},
|
|
.union_decl => {
|
|
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
|
|
self.registerUnionDecl(&node.data.union_decl);
|
|
},
|
|
.error_set_decl => {
|
|
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
|
|
self.registerErrorSetDecl(node);
|
|
},
|
|
.ufcs_alias => |ua| {
|
|
self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {};
|
|
},
|
|
// Expression statement
|
|
else => {
|
|
_ = self.lowerExpr(node);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
|
|
if (vd.value) |val| {
|
|
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
|
const ph = self.diagPackAsValue(val.data.identifier.name, val.span, .storage);
|
|
// Bind the name to the placeholder so later uses don't cascade
|
|
// into a second "unresolved" error after this one.
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = ph, .ty = .unresolved, .is_alloca = false });
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (vd.type_annotation) |ta| {
|
|
// Explicit type annotation — resolve type first, then lower value
|
|
const ty = self.resolveType(ta);
|
|
const slot = self.builder.alloca(ty);
|
|
if (vd.value) |val| {
|
|
// = --- (undef_literal) on tuple types: zero-initialize
|
|
if (val.data == .undef_literal and !ty.isBuiltin()) {
|
|
const ti = self.module.types.get(ty);
|
|
if (ti == .tuple) {
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
for (ti.tuple.fields) |f| {
|
|
field_vals.append(self.alloc, self.builder.constInt(0, f)) catch unreachable;
|
|
}
|
|
const zero = self.builder.emit(.{
|
|
.tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable },
|
|
}, ty);
|
|
self.builder.store(slot, zero);
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// A compile-time float initializer narrowing into an integer
|
|
// local follows the unified rule (integral folds, non-integral
|
|
// errors); a runtime float / `xx` cast falls through to the
|
|
// normal lower+coerce below.
|
|
if (self.foldComptimeFloatInit(val, ty)) |folded| {
|
|
self.builder.store(slot, folded);
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
return;
|
|
}
|
|
const saved_target = self.target_type;
|
|
const saved_fbv = self.force_block_value;
|
|
self.target_type = ty;
|
|
self.force_block_value = true;
|
|
var ref = self.lowerExpr(val);
|
|
self.target_type = saved_target;
|
|
self.force_block_value = saved_fbv;
|
|
// If target is optional and value isn't null, wrap with optional_wrap
|
|
if (!ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(ty);
|
|
if (ty_info == .optional and val.data != .null_literal) {
|
|
ref = self.builder.optionalWrap(ref, ty);
|
|
} else if (ty_info == .slice) {
|
|
// Array → slice promotion: if value is an array, convert to slice
|
|
const ref_ty = self.builder.getRefType(ref);
|
|
if (!ref_ty.isBuiltin()) {
|
|
const ref_info = self.module.types.get(ref_ty);
|
|
if (ref_info == .array) {
|
|
ref = self.builder.emit(.{ .array_to_slice = .{ .operand = ref } }, ty);
|
|
}
|
|
}
|
|
} else if (self.getProtocolInfo(ty) != null) {
|
|
// Auto type erasure: concrete → protocol
|
|
const ref_ty = self.builder.getRefType(ref);
|
|
if (ref_ty != ty) {
|
|
ref = self.buildProtocolErasure(ref, val, ref_ty, ty);
|
|
}
|
|
}
|
|
}
|
|
// Coerce value to match target type (e.g. u8 → s64 widening)
|
|
{
|
|
const ref_ty = self.builder.getRefType(ref);
|
|
if (ref_ty != ty and ref_ty != .void and ty != .void) {
|
|
ref = self.coerceToType(ref, ref_ty, ty);
|
|
}
|
|
}
|
|
self.builder.store(slot, ref);
|
|
} else {
|
|
// No value: zero-initialize or apply struct defaults
|
|
const zero = self.buildDefaultValue(ty);
|
|
self.builder.store(slot, zero);
|
|
}
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
} else if (vd.value) |val| {
|
|
// No type annotation — lower expr first, then get type from result.
|
|
// This is critical for generic calls where the return type is only
|
|
// known after monomorphization.
|
|
const saved_fbv = self.force_block_value;
|
|
const saved_target = self.target_type;
|
|
self.force_block_value = true;
|
|
// An unannotated decl provides no target type: clear the ambient one
|
|
// (the enclosing fn's implicit-return target) so literal initializers
|
|
// take their spec defaults (s64/f64) instead of adopting it.
|
|
self.target_type = null;
|
|
const ref = self.lowerExpr(val);
|
|
self.force_block_value = saved_fbv;
|
|
self.target_type = saved_target;
|
|
const ty = self.builder.getRefType(ref);
|
|
const slot = self.builder.alloca(ty);
|
|
self.builder.store(slot, ref);
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
} else {
|
|
const ty = TypeId.s64;
|
|
const slot = self.builder.alloca(ty);
|
|
self.builder.store(slot, self.zeroValue(ty));
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handle a bare fn_decl node as a local function declaration.
|
|
/// The parser produces `fn_decl` (not `const_decl`) for `name :: (params) -> T { body }`.
|
|
pub fn lowerLocalFnDecl(self: *Lowering, fd: *const ast.FnDecl) void {
|
|
// Use mangled name for local functions to support block-scoped shadowing
|
|
const name = if (self.scope) |scope| blk: {
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ fd.name, self.local_fn_counter }) catch fd.name;
|
|
self.local_fn_counter += 1;
|
|
scope.fn_names.put(fd.name, mangled) catch {};
|
|
break :blk mangled;
|
|
} else fd.name;
|
|
self.program_index.fn_ast_map.put(name, fd) catch {};
|
|
self.lazyLowerFunction(name);
|
|
}
|
|
|
|
pub fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void {
|
|
// Handle local function declarations: fx :: (s:s3) -> s3 { ... }
|
|
if (cd.value.data == .fn_decl) {
|
|
const fd = &cd.value.data.fn_decl;
|
|
// Use mangled name for local functions to support block-scoped shadowing
|
|
const name = if (self.scope != null) blk: {
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ cd.name, self.local_fn_counter }) catch cd.name;
|
|
self.local_fn_counter += 1;
|
|
// Register the bare→mangled mapping in the current scope
|
|
if (self.scope) |scope| {
|
|
scope.fn_names.put(cd.name, mangled) catch {};
|
|
}
|
|
break :blk mangled;
|
|
} else cd.name;
|
|
// Register in fn_ast_map so it can be resolved by lowerCall
|
|
self.program_index.fn_ast_map.put(name, fd) catch {};
|
|
// Lower the function body (saves/restores builder state)
|
|
self.lazyLowerFunction(name);
|
|
return;
|
|
}
|
|
|
|
// Handle local type declarations: MyType :: struct/union/enum { ... }
|
|
if (cd.value.data == .struct_decl) {
|
|
self.recordLocalTypeName(cd.name);
|
|
self.registerStructDecl(&cd.value.data.struct_decl, self.current_source_file);
|
|
return;
|
|
}
|
|
if (cd.value.data == .enum_decl) {
|
|
self.recordLocalTypeName(cd.name);
|
|
self.registerEnumDecl(&cd.value.data.enum_decl);
|
|
return;
|
|
}
|
|
if (cd.value.data == .union_decl) {
|
|
self.recordLocalTypeName(cd.name);
|
|
self.registerUnionDecl(&cd.value.data.union_decl);
|
|
return;
|
|
}
|
|
|
|
const ref = self.lowerExpr(cd.value);
|
|
// If there's an explicit type annotation, use it. Otherwise, infer from the expression.
|
|
const ty = if (cd.type_annotation) |ta|
|
|
self.resolveType(ta)
|
|
else
|
|
self.builder.getRefType(ref);
|
|
|
|
if (self.scope) |scope| {
|
|
scope.put(cd.name, .{ .ref = ref, .ty = ty, .is_alloca = false });
|
|
}
|
|
}
|
|
|
|
pub fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void {
|
|
if (rs.value) |val| {
|
|
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
|
_ = self.diagPackAsValue(val.data.identifier.name, val.span, .return_value);
|
|
return;
|
|
}
|
|
}
|
|
// Set target_type to function return type so null_literal etc. get the right type.
|
|
// When inlining a comptime body, the *inlined* fn's declared return type wins
|
|
// over the caller's — otherwise `return 42` inside a `-> s64` body lowered into
|
|
// a `-> s32` caller would coerce 42 to s32 before storing into the s64 slot.
|
|
const old_target = self.target_type;
|
|
const ret_ty_for_target: TypeId = if (self.inline_return_target) |iri|
|
|
iri.ret_ty
|
|
else if (self.builder.func) |fid|
|
|
self.module.functions.items[@intFromEnum(fid)].ret
|
|
else
|
|
TypeId.s64;
|
|
// A value-carrying failable (`-> (T..., !)`) returns its VALUE part and
|
|
// the success error slot (0) is appended by lowerFailableSuccessReturn.
|
|
// Resolve a BARE returned value against that value type, NOT the failable
|
|
// tuple: a bare enum literal `.variant` resolves its tag against
|
|
// `target_type`, and against the tuple it matches no variant (tag 0) and
|
|
// is stamped with the tuple type — which the success-return path then
|
|
// mistakes for a forwarded full tuple, dropping the appended `0` slot.
|
|
// An explicit full failable tuple return (`return (v..., e)`) keeps the
|
|
// full-tuple target so its trailing error element resolves against the
|
|
// error set; it is then forwarded as-is. Applies to the inlined
|
|
// comptime-body return path too (iri.ret_ty is the failable tuple there).
|
|
const target_for_value = self.failableReturnTarget(ret_ty_for_target, rs.value);
|
|
if (target_for_value != .void) self.target_type = target_for_value;
|
|
// Evaluate return value first (before defers)
|
|
const ret_val = if (rs.value) |val| self.lowerExpr(val) else null;
|
|
self.target_type = old_target;
|
|
|
|
// Inlined-comptime-body return: store into the slot the inliner
|
|
// gave us and branch to the inliner's "return-done" basic block.
|
|
// The branch is the basic block's terminator — so subsequent
|
|
// dead code in the same block trips the LLVM verifier (the
|
|
// SAME behaviour as a regular `return X;` followed by code).
|
|
//
|
|
// We DO NOT set `block_terminated = true`: that flag would
|
|
// leak past structured control flow (e.g. an `if cond { return
|
|
// X; }` whose merge block continues to subsequent statements)
|
|
// and incorrectly skip the trailing statements. CFG-level
|
|
// termination is what we actually want — let the basic-block
|
|
// terminator do its job.
|
|
if (self.inline_return_target) |iri| {
|
|
if (ret_val) |ref| {
|
|
// Value-carrying failable inlined body: append the success error
|
|
// slot (0) exactly like the real-return path below.
|
|
// lowerFailableSuccessReturn routes through emitTupleRet, which
|
|
// stores into iri.slot and branches to iri.done_bb for an inline
|
|
// target. Defers first, so the returned SSA value is materialized
|
|
// before they run (matching the real-return ordering).
|
|
if (!iri.ret_ty.isBuiltin() and
|
|
self.module.types.get(iri.ret_ty) == .tuple and
|
|
self.errorChannelOf(iri.ret_ty) != null)
|
|
{
|
|
self.emitBlockDefers(self.func_defer_base);
|
|
self.lowerFailableSuccessReturn(ref, iri.ret_ty, rs.value.?.span);
|
|
return;
|
|
}
|
|
const val_ty = self.builder.getRefType(ref);
|
|
const coerced = if (val_ty != iri.ret_ty)
|
|
self.coerceToType(ref, val_ty, iri.ret_ty)
|
|
else
|
|
ref;
|
|
self.builder.store(iri.slot, coerced);
|
|
}
|
|
// Drain block-scoped defers up to the inlined-body base so
|
|
// they fire on this return path the same as a real fn return.
|
|
self.emitBlockDefers(self.func_defer_base);
|
|
self.builder.br(iri.done_bb, &.{});
|
|
return;
|
|
}
|
|
|
|
// Emit ALL pending defers for THIS function in LIFO order before the return
|
|
self.emitBlockDefers(self.func_defer_base);
|
|
|
|
if (ret_val) |ref| {
|
|
const ret_ty = if (self.builder.func) |fid|
|
|
self.module.functions.items[@intFromEnum(fid)].ret
|
|
else
|
|
TypeId.s64;
|
|
if (ret_ty == .void) {
|
|
// Void function — just return void (the value expression was evaluated for side effects)
|
|
self.builder.retVoid();
|
|
} else if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) {
|
|
// Value-carrying failable `-> (T..., !)`: the user returns the
|
|
// value part; the compiler appends the success error slot (0).
|
|
self.lowerFailableSuccessReturn(ref, ret_ty, rs.value.?.span);
|
|
} else {
|
|
// Coerce return value to match function return type (e.g., ?s32 → s32)
|
|
const val_ty = self.builder.getRefType(ref);
|
|
const coerced = self.coerceToType(ref, val_ty, ret_ty);
|
|
self.builder.ret(coerced, ret_ty);
|
|
}
|
|
} else {
|
|
// A bare `return;` in a pure failable function (`-> !` / `-> !Named`,
|
|
// whose return type IS the error set) is the success exit — the
|
|
// error slot carries 0 ("no error"). Everything else is a void return.
|
|
const ret_ty = if (self.builder.func) |fid|
|
|
self.module.functions.items[@intFromEnum(fid)].ret
|
|
else
|
|
TypeId.void;
|
|
if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .error_set) {
|
|
self.builder.ret(self.builder.constInt(0, ret_ty), ret_ty);
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The ROOT identifier of an assignment-target chain (`K[0].x` → "K",
|
|
/// `WHITE.r` → "WHITE"). A deref along the chain (`p.*`, `p.*[i]`) breaks
|
|
/// it — writing through a pointer VALUE is not a write to the named root.
|
|
fn assignmentRootIdent(target: *const Node) ?[]const u8 {
|
|
var n = target;
|
|
while (true) {
|
|
switch (n.data) {
|
|
.identifier => |id| return id.name,
|
|
.index_expr => |ie| n = ie.object,
|
|
.field_access => |fa| n = fa.object,
|
|
else => return null,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// True when `root` names a module CONSTANT from the current source —
|
|
/// a const-flagged global (array/struct consts, #run consts) or a
|
|
/// module value const. Locals shadow (caller checks scope first).
|
|
fn rootIsConstant(self: *Lowering, root: []const u8) bool {
|
|
switch (self.selectGlobalAuthor(root)) {
|
|
.resolved => |g| if (self.module.globals.items[g.id.index()].is_const) return true,
|
|
else => {},
|
|
}
|
|
return switch (self.selectModuleConst(root)) {
|
|
.resolved, .own_opaque => true,
|
|
.ambiguous, .none => false,
|
|
};
|
|
}
|
|
|
|
pub fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
|
// Writes through a constant are rejected at compile time (issue 0116):
|
|
// the target chain's root naming a const global (array/struct consts,
|
|
// #run consts) or a module value const cannot be stored to — for a
|
|
// struct const the store previously compiled and bus-errored at
|
|
// runtime; for scalars it silently misfired.
|
|
if (assignmentRootIdent(asgn.target)) |root| {
|
|
const shadowed = if (self.scope) |s| s.lookup(root) != null else false;
|
|
if (!shadowed and rootIsConstant(self, root)) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, asgn.target.span, "cannot assign through constant '{s}' — constants are immutable (use a '=' global or a local copy for mutable data)", .{root});
|
|
return;
|
|
}
|
|
}
|
|
// Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.)
|
|
const old_target = self.target_type;
|
|
if (asgn.target.data == .identifier) {
|
|
var found_local = false;
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(asgn.target.data.identifier.name)) |binding| {
|
|
self.target_type = binding.ty;
|
|
found_local = true;
|
|
}
|
|
}
|
|
if (!found_local) {
|
|
// Quiet author-aware lookup (type inference only; the store
|
|
// site diagnoses ambiguity / visibility).
|
|
if (self.program_index.global_names.get(asgn.target.data.identifier.name)) |gi| {
|
|
switch (self.selectGlobalAuthor(asgn.target.data.identifier.name)) {
|
|
.resolved => |g| self.target_type = g.ty,
|
|
.untracked => self.target_type = gi.ty,
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
} else if (asgn.target.data == .index_expr) {
|
|
// For array[i] = val, set target_type to the element type
|
|
const tgt_obj_ty = self.inferExprType(asgn.target.data.index_expr.object);
|
|
const elem_ty = self.ptrToArrayElem(tgt_obj_ty) orelse self.getElementType(tgt_obj_ty);
|
|
if (elem_ty != .void) self.target_type = elem_ty;
|
|
} else if (asgn.target.data == .field_access) {
|
|
// For obj.field = val, set target_type to the field's type so RHS
|
|
// sub-expressions (enum/struct literals, branch arms, xx casts) can
|
|
// resolve against it. Skipped for forms that would forward the type
|
|
// unchanged into method-call arg slots (`resolveCallParamTypes` can't
|
|
// override target_type per-arg).
|
|
const needs_target = switch (asgn.value.data) {
|
|
.enum_literal, .struct_literal, .tuple_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true,
|
|
.call => |vc| vc.callee.data == .enum_literal,
|
|
else => false,
|
|
};
|
|
if (needs_target) {
|
|
const fa = asgn.target.data.field_access;
|
|
const obj_ty_raw = self.inferExprType(fa.object);
|
|
const obj_ty = if (!obj_ty_raw.isBuiltin()) blk: {
|
|
const pinfo = self.module.types.get(obj_ty_raw);
|
|
break :blk if (pinfo == .pointer) pinfo.pointer.pointee else obj_ty_raw;
|
|
} else obj_ty_raw;
|
|
if (!obj_ty.isBuiltin()) {
|
|
const field_name_id = self.module.types.internString(fa.field);
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields) |f| {
|
|
if (f.name == field_name_id) {
|
|
self.target_type = f.ty;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const val = self.lowerExpr(asgn.value);
|
|
self.target_type = old_target;
|
|
|
|
switch (asgn.target.data) {
|
|
.identifier => |id| {
|
|
var handled = false;
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
handled = true;
|
|
if (asgn.op == .assign) {
|
|
// Coerce value to match binding type (e.g., f32 → ?f32, concrete → protocol)
|
|
var store_val = val;
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) {
|
|
store_val = self.coerceToType(val, val_ty, binding.ty);
|
|
}
|
|
self.builder.store(binding.ref, store_val);
|
|
} else {
|
|
// Compound assignment: load, op, store
|
|
const loaded = self.builder.load(binding.ref, binding.ty);
|
|
const result = self.emitCompoundOp(loaded, val, asgn.op, binding.ty);
|
|
self.builder.store(binding.ref, result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Fallback: global variable assignment — source-aware (issue
|
|
// 0115): write the AUTHOR's global, never an unrelated module's
|
|
// same-named one.
|
|
if (!handled) {
|
|
if (self.resolveGlobalRef(id.name, asgn.target.span)) |gi| {
|
|
if (asgn.op == .assign) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void)
|
|
self.coerceToType(val, val_ty, gi.ty)
|
|
else
|
|
val;
|
|
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void);
|
|
} else {
|
|
// Compound assignment: load current value, apply op, store back
|
|
const loaded = self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
const result = self.emitCompoundOp(loaded, val, asgn.op, gi.ty);
|
|
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = result } }, .void);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
// M2.2 — `obj.field = val` for an Obj-C `#property` field
|
|
// dispatches via objc_msgSend `setField:`. Skip struct-
|
|
// pointer / GEP entirely; receivers are opaque Obj-C ids.
|
|
// Compound ops on properties are deferred (need load-via-
|
|
// getter + op + store-via-setter — Month 4 ARC territory).
|
|
if (asgn.op == .assign) {
|
|
if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| {
|
|
self.lowerObjcPropertySetter(fa.object, prop, val);
|
|
return;
|
|
}
|
|
}
|
|
// M1.2 A.3 — `self.field [op]= val` on a sx-defined Obj-C
|
|
// class instance field (NOT a #property): write through
|
|
// the __sx_state ivar. Handles plain assignment AND
|
|
// compound ops (+=, -=, etc.) via storeOrCompound.
|
|
if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| {
|
|
const obj_ref = self.lowerExpr(fa.object);
|
|
const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return;
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state_ptr,
|
|
.field_index = info.field_idx,
|
|
.base_type = info.state_ty,
|
|
} }, ptr_void);
|
|
self.storeOrCompound(field_addr, val, asgn.op, info.field_ty);
|
|
return;
|
|
}
|
|
|
|
var obj_ptr = self.lowerExprAsPtr(fa.object);
|
|
var obj_ty = self.inferExprType(fa.object);
|
|
// Auto-deref: if the object is a pointer field from a non-identifier
|
|
// (i.e., result of structGep on a pointer slot), load the pointer value.
|
|
if (fa.object.data != .identifier and !obj_ty.isBuiltin()) {
|
|
const pinfo = self.module.types.get(obj_ty);
|
|
if (pinfo == .pointer) {
|
|
obj_ptr = self.builder.load(obj_ptr, obj_ty);
|
|
obj_ty = pinfo.pointer.pointee;
|
|
}
|
|
}
|
|
|
|
// Special .len/.ptr handling only for slices, strings, arrays — NOT structs
|
|
const is_special_container = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: {
|
|
const obj_info = self.module.types.get(obj_ty);
|
|
break :blk obj_info == .slice or obj_info == .array or obj_info == .vector;
|
|
} else false);
|
|
|
|
if (is_special_container and std.mem.eql(u8, fa.field, "len")) {
|
|
const gep = self.builder.structGepTyped(obj_ptr, 1, .s64, obj_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, .s64);
|
|
} else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) {
|
|
const gep = self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, .s64);
|
|
} else if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |fl| {
|
|
// Resolve the target field (struct / union direct / promoted
|
|
// anonymous-struct member / tuple element / vector lane) via
|
|
// the shared lvalue resolver — the same one the address-of
|
|
// and multi-target store paths use — so the three never
|
|
// resolve a field to a different slot or default field 0
|
|
// (two-resolver defect class). fl.ptr is
|
|
// *field_ty (the store handler unwraps one pointer level);
|
|
// fl.ty is the value type to coerce the rhs to.
|
|
const src_ty = self.builder.getRefType(val);
|
|
const coerced = self.coerceToType(val, src_ty, fl.ty);
|
|
self.storeOrCompound(fl.ptr, coerced, asgn.op, fl.ty);
|
|
} else {
|
|
// No struct / union / tuple / vector field matches the
|
|
// assignment target. Emit the same field-not-found
|
|
// diagnostic the read path uses (emitFieldError) and bail;
|
|
// building a pointer with field_ty = .unresolved would
|
|
// otherwise store through a pointer-to-.unresolved that
|
|
// panics at LLVM emission.
|
|
_ = self.emitFieldError(obj_ty, fa.field, asgn.target.span);
|
|
}
|
|
},
|
|
.index_expr => |ie| {
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
// For fixed-size array assignment targets, use the alloca pointer directly
|
|
// so that the store modifies the original variable (not a loaded copy).
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null;
|
|
if (obj_alloca) |alloca_ref| {
|
|
// Array alloca: single-index GEP with element stride
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, elem_ty);
|
|
} else if (is_array) {
|
|
// Array in a struct field or other composite: get pointer to array in-place
|
|
const obj_ptr = self.lowerExprAsPtr(ie.object);
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj_ptr, .rhs = idx } }, ptr_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, elem_ty);
|
|
} else {
|
|
// Pointer/slice: load the pointer value and GEP
|
|
const obj = self.lowerExpr(ie.object);
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, elem_ty);
|
|
}
|
|
},
|
|
.deref_expr => |de| {
|
|
const ptr = self.lowerExpr(de.operand);
|
|
if (asgn.op == .assign) {
|
|
const pointee_ty = blk: {
|
|
const ptr_ty = self.inferExprType(de.operand);
|
|
if (!ptr_ty.isBuiltin()) {
|
|
const info = self.module.types.get(ptr_ty);
|
|
if (info == .pointer) break :blk info.pointer.pointee;
|
|
}
|
|
break :blk ptr_ty;
|
|
};
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
|
self.coerceToType(val, val_ty, pointee_ty)
|
|
else
|
|
val;
|
|
self.builder.store(ptr, store_val);
|
|
} else {
|
|
const pointee_ty = self.inferExprType(de.operand);
|
|
const elem_ty = blk: {
|
|
if (!pointee_ty.isBuiltin()) {
|
|
const info = self.module.types.get(pointee_ty);
|
|
if (info == .pointer) break :blk info.pointer.pointee;
|
|
}
|
|
break :blk pointee_ty;
|
|
};
|
|
self.storeOrCompound(ptr, val, asgn.op, elem_ty);
|
|
}
|
|
},
|
|
else => {
|
|
_ = self.emitError("assignment_target", asgn.target.span);
|
|
},
|
|
}
|
|
}
|
|
|
|
const FieldLvalue = struct { ptr: Ref, ty: TypeId };
|
|
|
|
/// Resolve `obj.field` — where `obj_ptr` already points at the aggregate —
|
|
/// to a typed pointer into the field's storage plus the field's value type.
|
|
/// Handles union direct fields, promoted anonymous-struct union members,
|
|
/// tuple elements (numeric or named), vector lanes (`.x`/`.y`/`.z`/`.w` and
|
|
/// the colour aliases), and plain struct fields. Returns null when no field
|
|
/// matches; the caller emits the field-not-found diagnostic.
|
|
///
|
|
/// `ptr`'s IR type is `*field_ty` (a pointer to the field), NOT the field
|
|
/// value type: `emitStore` reads the store-target pointer's IR type and
|
|
/// unwraps one `.pointer` level to find the stored value's type. Labelling
|
|
/// the GEP with the bare field type instead would make a field whose own
|
|
/// type is a pointer-to-aggregate (`*Pair`) coerce the stored pointer into
|
|
/// the aggregate (closure auto-promotion in `coerceArg`), storing an
|
|
/// oversized struct that clobbers the neighbouring field. `.ty` carries the
|
|
/// field's value type for the caller's coercion.
|
|
///
|
|
/// Single source of lvalue field resolution shared by all three store/
|
|
/// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr
|
|
/// (address-of), and lowerMultiAssign (multi-target store) — so they never
|
|
/// resolve a field to a different slot or default field 0.
|
|
pub fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue {
|
|
if (obj_ty.isBuiltin()) return null;
|
|
const field_name_id = self.module.types.internString(field);
|
|
const type_info = self.module.types.get(obj_ty);
|
|
|
|
// Union / tagged-union: variants overlay at offset 0. A direct field is
|
|
// a union_gep; a promoted anonymous-struct member is a union_gep into
|
|
// the variant followed by a struct_gep into the member.
|
|
const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) {
|
|
.@"union" => |u| u.fields,
|
|
.tagged_union => |u| u.fields,
|
|
else => null,
|
|
};
|
|
if (union_fields) |fields| {
|
|
for (fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
|
|
return .{ .ptr = ptr, .ty = f.ty };
|
|
}
|
|
if (!f.ty.isBuiltin()) {
|
|
const fi = self.module.types.get(f.ty);
|
|
if (fi == .@"struct") {
|
|
for (fi.@"struct".fields, 0..) |sf, si| {
|
|
if (sf.name == field_name_id) {
|
|
const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
|
|
const ptr = self.builder.structGepTyped(ug, @intCast(si), self.module.types.ptrTo(sf.ty), f.ty);
|
|
return .{ .ptr = ptr, .ty = sf.ty };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Tuple element: `.0` (numeric) or `.name`.
|
|
if (type_info == .tuple) {
|
|
const tup = type_info.tuple;
|
|
var elem_idx: ?usize = null;
|
|
if (std.fmt.parseInt(usize, field, 10)) |n| {
|
|
if (n < tup.fields.len) elem_idx = n;
|
|
} else |_| {
|
|
if (tup.names) |names| {
|
|
for (names, 0..) |nm, i| {
|
|
if (nm == field_name_id and i < tup.fields.len) {
|
|
elem_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (elem_idx) |idx| {
|
|
const elem_ty = tup.fields[idx];
|
|
const ptr = self.builder.structGepTyped(obj_ptr, @intCast(idx), self.module.types.ptrTo(elem_ty), obj_ty);
|
|
return .{ .ptr = ptr, .ty = elem_ty };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Vector lane: `.x`/`.y`/`.z`/`.w` (or colour aliases `.r`/`.g`/`.b`/`.a`)
|
|
// → lane 0/1/2/3 via the same vectorLaneIndex the read path uses. A
|
|
// non-lane field on a vector is a genuine miss (caller diagnoses).
|
|
if (type_info == .vector) {
|
|
const vidx = Lowering.vectorLaneIndex(field) orelse return null;
|
|
const elem_ty = type_info.vector.element;
|
|
const ptr = self.builder.structGepTyped(obj_ptr, vidx, self.module.types.ptrTo(elem_ty), obj_ty);
|
|
return .{ .ptr = ptr, .ty = elem_ty };
|
|
}
|
|
|
|
// Plain struct field.
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
const ptr = self.builder.structGepTyped(obj_ptr, @intCast(i), self.module.types.ptrTo(f.ty), obj_ty);
|
|
return .{ .ptr = ptr, .ty = f.ty };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get the pointer (alloca ref) for an lvalue expression, without loading.
|
|
pub fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref {
|
|
switch (node.data) {
|
|
.identifier => |id| {
|
|
const local = if (self.scope) |scope| scope.lookup(id.name) else null;
|
|
if (local) |binding| {
|
|
if (binding.is_alloca) {
|
|
// If the variable IS a pointer (e.g., p: *Vec2), load it
|
|
// to get the actual pointer value for GEP/store operations
|
|
if (!binding.ty.isBuiltin()) {
|
|
const info = self.module.types.get(binding.ty);
|
|
if (info == .pointer) {
|
|
return self.builder.load(binding.ref, binding.ty);
|
|
}
|
|
}
|
|
return binding.ref;
|
|
}
|
|
} else if (self.resolveGlobalRef(id.name, null)) |gi| {
|
|
// Module-global lvalue: address into the global's live storage
|
|
// so a downstream GEP/store targets the global itself, not a
|
|
// loaded copy. A pointer-typed global is loaded first to get
|
|
// the pointer value to GEP through (mirrors the local pointer
|
|
// case above); any other global yields its storage address.
|
|
if (!gi.ty.isBuiltin() and self.module.types.get(gi.ty) == .pointer) {
|
|
return self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
}
|
|
return self.builder.emit(.{ .global_addr = gi.id }, self.module.types.ptrTo(gi.ty));
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
var obj_ptr = self.lowerExprAsPtr(fa.object);
|
|
var obj_ty = self.inferExprType(fa.object);
|
|
// Auto-deref for chained pointer field access:
|
|
// When fa.object is a field_access or index_expr, lowerExprAsPtr returns
|
|
// a structGep/pointer to the slot. If the slot holds a pointer type,
|
|
// we need to load the pointer value before GEPing into the pointee struct.
|
|
// (Identifiers are already loaded by the identifier handler in lowerExprAsPtr.)
|
|
if (fa.object.data != .identifier and !obj_ty.isBuiltin()) {
|
|
const info = self.module.types.get(obj_ty);
|
|
if (info == .pointer) {
|
|
obj_ptr = self.builder.load(obj_ptr, obj_ty);
|
|
obj_ty = info.pointer.pointee;
|
|
}
|
|
}
|
|
// Resolve the field lvalue (struct / union direct / promoted
|
|
// anonymous-struct member / tuple element) via the shared
|
|
// resolver so address-of and the multi-target store path never
|
|
// disagree on the slot. No match → emit the read path's
|
|
// field-not-found diagnostic (lowerFieldAccessOnType →
|
|
// emitFieldError) instead of silently GEPing field 0 as .s64;
|
|
// that bogus pointer reaches LLVM emission as ptrTo(.unresolved)
|
|
// and panics.
|
|
if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| return r.ptr;
|
|
return self.emitFieldError(obj_ty, fa.field, node.span);
|
|
},
|
|
.index_expr => |ie| {
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
// For fixed-size arrays, use the alloca so GEP addresses the original memory
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const base = if (is_array)
|
|
(self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object))
|
|
else
|
|
self.lowerExpr(ie.object);
|
|
return self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty);
|
|
},
|
|
.deref_expr => |de| {
|
|
return self.lowerExpr(de.operand);
|
|
},
|
|
else => {},
|
|
}
|
|
// Fallback: lower as expression (may produce a value, not pointer)
|
|
return self.lowerExpr(node);
|
|
}
|
|
|
|
/// Store a value to a GEP, handling both plain and compound assignment.
|
|
pub fn storeOrCompound(self: *Lowering, gep: Ref, val: Ref, op: ast.Assignment.Op, ty: TypeId) void {
|
|
if (op == .assign) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != ty and val_ty != .void and ty != .void)
|
|
self.coerceToType(val, val_ty, ty)
|
|
else
|
|
val;
|
|
self.builder.store(gep, store_val);
|
|
} else {
|
|
const loaded = self.builder.load(gep, ty);
|
|
const result = self.emitCompoundOp(loaded, val, op, ty);
|
|
self.builder.store(gep, result);
|
|
}
|
|
}
|
|
|
|
pub fn emitCompoundOp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.Assignment.Op, ty: TypeId) Ref {
|
|
return switch (op) {
|
|
.add_assign => self.builder.add(lhs, rhs, ty),
|
|
.sub_assign => self.builder.sub(lhs, rhs, ty),
|
|
.mul_assign => self.builder.mul(lhs, rhs, ty),
|
|
.div_assign => self.builder.div(lhs, rhs, ty),
|
|
.mod_assign => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.and_assign => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.or_assign => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.xor_assign => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.shl_assign => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.shr_assign => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
else => self.emitError("compound_assign", null),
|
|
};
|
|
}
|
|
|
|
// ── Defer / cleanup ─────────────────────────────────────────────
|
|
|
|
pub fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void {
|
|
// Push deferred expression onto the stack — emitted at every block exit, LIFO.
|
|
self.defer_stack.append(self.alloc, .{ .body = ds.expr, .is_onfail = false }) catch {};
|
|
}
|
|
|
|
/// `onfail [e] BODY` (ERR E1.7) — cleanup that runs only when an error
|
|
/// leaves the enclosing block. Recorded on the shared cleanup stack;
|
|
/// emitted (interleaved with defers, reverse) at error exits by
|
|
/// `emitErrorCleanup`, and discarded — never run — on a success exit.
|
|
pub fn lowerOnFail(self: *Lowering, ofs: *const ast.OnFailStmt, span: ast.Span) void {
|
|
// `onfail` is only meaningful inside a failable function — a
|
|
// non-failable function never error-exits, so it could never fire.
|
|
const ret_ty = self.effectiveReturnType() orelse {
|
|
self.diagOnFailNotFailable(span);
|
|
return;
|
|
};
|
|
if (self.errorChannelOf(ret_ty) == null) {
|
|
self.diagOnFailNotFailable(span);
|
|
return;
|
|
}
|
|
self.defer_stack.append(self.alloc, .{ .body = ofs.body, .is_onfail = true, .binding = ofs.binding }) catch {};
|
|
}
|
|
|
|
pub fn diagOnFailNotFailable(self: *Lowering, span: ast.Span) void {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "`onfail` is only valid inside a failable function (a return type with `!` or `!Named`) — use `defer` for unconditional cleanup", .{});
|
|
}
|
|
}
|
|
|
|
/// Emit cleanups from saved_len..current in reverse (LIFO) order on a
|
|
/// SUCCESS exit: only `defer` entries run; `onfail` entries are skipped
|
|
/// (and discarded by the truncation). Truncates the stack to saved_len.
|
|
pub fn emitBlockDefers(self: *Lowering, saved_len: usize) void {
|
|
// Guard: if stack was already drained (e.g., by a return that emitted all defers)
|
|
if (saved_len > self.defer_stack.items.len) return;
|
|
if (self.currentBlockHasTerminator()) {
|
|
// Block already terminated (e.g., by return) — cleanups were already emitted
|
|
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
|
return;
|
|
}
|
|
const stack = self.defer_stack.items;
|
|
var i = stack.len;
|
|
while (i > saved_len) {
|
|
i -= 1;
|
|
if (!stack[i].is_onfail) self.lowerCleanupBody(stack[i].body);
|
|
}
|
|
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
|
}
|
|
|
|
/// Emit pending `defer` cleanups for a `break`/`continue` exit: everything
|
|
/// registered since the innermost loop's body began, in LIFO order. `onfail`
|
|
/// entries are skipped (a break is a success exit). The stack is NOT
|
|
/// truncated — the same entries still belong to the fall-through lowering
|
|
/// path after the branch that contains the break; the enclosing block scopes
|
|
/// truncate as usual.
|
|
pub fn emitLoopExitDefers(self: *Lowering) void {
|
|
const stack = self.defer_stack.items;
|
|
var i = stack.len;
|
|
while (i > self.loop_defer_base) {
|
|
i -= 1;
|
|
if (!stack[i].is_onfail) self.lowerCleanupBody(stack[i].body);
|
|
}
|
|
}
|
|
|
|
/// Run a `defer`/`onfail` cleanup body for its side effects (void context).
|
|
/// A braced body lowers as statements (NOT as a value) so a trailing-`;`
|
|
/// last expression is fine here — cleanup bodies never yield a value.
|
|
pub fn lowerCleanupBody(self: *Lowering, body: *const Node) void {
|
|
if (body.data == .block) self.lowerBlock(body) else _ = self.lowerExpr(body);
|
|
}
|
|
|
|
/// Emit cleanups from `base`..current in reverse order on an ERROR exit
|
|
/// (raise / try-propagation): BOTH `defer` and `onfail` entries run,
|
|
/// interleaved in reverse declaration order. `err_tag` is the in-flight
|
|
/// error tag, bound to each `onfail e`'s binding. Does not truncate — the
|
|
/// terminating `ret` + the unwinding block-scope `emitBlockDefers` (which
|
|
/// then see the terminator and skip) leave the stack consistent.
|
|
pub fn emitErrorCleanup(self: *Lowering, base: usize, err_tag: Ref) void {
|
|
if (base > self.defer_stack.items.len) return;
|
|
const tag_ty = self.builder.getRefType(err_tag);
|
|
const stack = self.defer_stack.items;
|
|
var i = stack.len;
|
|
while (i > base) {
|
|
i -= 1;
|
|
const entry = stack[i];
|
|
if (entry.is_onfail) {
|
|
if (entry.binding) |name| {
|
|
var ofscope = Scope.init(self.alloc, self.scope);
|
|
const saved = self.scope;
|
|
self.scope = &ofscope;
|
|
ofscope.put(name, .{ .ref = err_tag, .ty = tag_ty, .is_alloca = false });
|
|
self.lowerCleanupBody(entry.body);
|
|
self.scope = saved;
|
|
ofscope.deinit();
|
|
} else {
|
|
self.lowerCleanupBody(entry.body);
|
|
}
|
|
} else {
|
|
self.lowerCleanupBody(entry.body);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
|
// push Context.{...} { body } — allocates a fresh Context on the
|
|
// stack frame, rebinds the lowering's `current_ctx_ref` to it for
|
|
// the body's lexical scope, then restores. No global, no walk.
|
|
if (!self.implicit_ctx_enabled) {
|
|
_ = self.diagnoseMissingContext("`push Context.{...}`");
|
|
self.lowerBlock(ps.body);
|
|
return;
|
|
}
|
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
|
_ = self.diagnoseMissingContext("`push Context.{...}`");
|
|
self.lowerBlock(ps.body);
|
|
return;
|
|
};
|
|
const saved_ctx_ref = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref;
|
|
|
|
const saved_target = self.target_type;
|
|
self.target_type = ctx_ty;
|
|
const ctx_val = self.lowerExpr(ps.context_expr);
|
|
self.target_type = saved_target;
|
|
|
|
const slot = self.builder.alloca(ctx_ty);
|
|
self.builder.store(slot, ctx_val);
|
|
self.current_ctx_ref = slot;
|
|
|
|
self.lowerBlock(ps.body);
|
|
}
|
|
|
|
pub fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
|
// Evaluate all RHS values first, then assign to LHS targets
|
|
var vals = std.ArrayList(Ref).empty;
|
|
defer vals.deinit(self.alloc);
|
|
for (ma.values) |v| {
|
|
vals.append(self.alloc, self.lowerExpr(v)) catch unreachable;
|
|
}
|
|
|
|
for (ma.targets, 0..) |target, i| {
|
|
if (i >= vals.items.len) break;
|
|
const val = vals.items[i];
|
|
switch (target.data) {
|
|
.identifier => |id| {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void)
|
|
self.coerceToType(val, val_ty, binding.ty)
|
|
else
|
|
val;
|
|
self.builder.store(binding.ref, store_val);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.index_expr => |ie| {
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.ptrToArrayElem(obj_ty) orelse self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void)
|
|
self.coerceToType(val, val_ty, elem_ty)
|
|
else
|
|
val;
|
|
// For fixed-size arrays, use the alloca pointer directly
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null;
|
|
if (obj_alloca) |alloca_ref| {
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty);
|
|
self.builder.store(gep, store_val);
|
|
} else {
|
|
const obj = self.lowerExpr(ie.object);
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty);
|
|
self.builder.store(gep, store_val);
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
const obj_ptr = self.lowerExprAsPtr(fa.object);
|
|
const obj_ty = self.inferExprType(fa.object);
|
|
// Resolve the target field via the shared lvalue resolver —
|
|
// the same one address-of uses — so a missing field emits a
|
|
// diagnostic instead of defaulting to field 0 / field_ty
|
|
// .unresolved, which silently corrupted a neighbouring field
|
|
// (or panicked at LLVM emission).
|
|
if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != r.ty and val_ty != .void and r.ty != .void)
|
|
self.coerceToType(val, val_ty, r.ty)
|
|
else
|
|
val;
|
|
self.builder.store(r.ptr, store_val);
|
|
} else {
|
|
_ = self.emitFieldError(obj_ty, fa.field, target.span);
|
|
}
|
|
},
|
|
.deref_expr => |de| {
|
|
const ptr = self.lowerExpr(de.operand);
|
|
const pointee_ty = blk: {
|
|
const ptr_ty = self.inferExprType(de.operand);
|
|
if (!ptr_ty.isBuiltin()) {
|
|
const info = self.module.types.get(ptr_ty);
|
|
if (info == .pointer) break :blk info.pointer.pointee;
|
|
}
|
|
break :blk ptr_ty;
|
|
};
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
|
self.coerceToType(val, val_ty, pointee_ty)
|
|
else
|
|
val;
|
|
self.builder.store(ptr, store_val);
|
|
},
|
|
else => {
|
|
_ = self.emitError("multi_assign_target", target.span);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void {
|
|
// Lower the RHS expression (must produce a tuple)
|
|
const saved_fbv = self.force_block_value;
|
|
const saved_target = self.target_type;
|
|
self.force_block_value = true;
|
|
// Same as the unannotated var-decl path: the destructure declares new
|
|
// bindings, so the ambient target type must not type the RHS literals.
|
|
self.target_type = null;
|
|
const ref = self.lowerExpr(dd.value);
|
|
self.force_block_value = saved_fbv;
|
|
self.target_type = saved_target;
|
|
const ty = self.builder.getRefType(ref);
|
|
|
|
// Get tuple field info
|
|
if (ty.isBuiltin()) return;
|
|
const ti = self.module.types.get(ty);
|
|
if (ti != .tuple) return;
|
|
const tuple = ti.tuple;
|
|
if (dd.names.len > tuple.fields.len) return;
|
|
|
|
// E1.8 (discard rejection): when the RHS is a value-carrying failable,
|
|
// the error slot (always the LAST tuple field) cannot be dropped. It is
|
|
// dropped when the destructure omits it (fewer names than fields, so the
|
|
// trailing error slot is never reached) or binds it to `_`. The `try` /
|
|
// `catch` / `or value` consumer forms all strip the error channel (their
|
|
// result type is non-failable), so this fires only on a BARE failable
|
|
// destructure — exactly the case that would let an error vanish silently.
|
|
if (self.errorChannelOf(ty) != null) {
|
|
const err_dropped = dd.names.len < tuple.fields.len or
|
|
std.mem.eql(u8, dd.names[dd.names.len - 1], "_");
|
|
if (err_dropped) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, dd.value.span, "the error slot of a failable cannot be dropped — bind it (`v, err := …`) and handle it, or use `try` / `catch`", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract each field and bind to a new variable
|
|
for (dd.names, 0..) |name, i| {
|
|
if (std.mem.eql(u8, name, "_")) continue; // discard
|
|
const field_ty = tuple.fields[i];
|
|
const field_val = self.builder.emit(.{ .tuple_get = .{
|
|
.base = ref,
|
|
.field_index = @intCast(i),
|
|
.base_type = ty,
|
|
} }, field_ty);
|
|
const slot = self.builder.alloca(field_ty);
|
|
self.builder.store(slot, field_val);
|
|
if (self.scope) |scope| {
|
|
scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true });
|
|
}
|
|
}
|
|
|
|
// Destructuring a failable's result binds the error slot to a variable:
|
|
// the user now owns the error explicitly, so the trace is absorbed
|
|
// (ERR E3.2). A plain (non-failable) tuple destructure clears nothing.
|
|
if (self.errorChannelOf(ty) != null) self.emitTraceClear();
|
|
}
|