refactor(B8.2): move expr core + operators to lower/expr.zig
Verbatim relocation of the 8-method core expression cluster (lowerExpr dispatch, ref-capture pointee, binary ops, tuple ops/lex-compare/ membership, chained comparison, emitCmp) appended to src/ir/lower/expr.zig. 8 aliases on Lowering keep all call sites unchanged. Method pub-flips: isArithOperand, isBitwiseOperand, isOrderingOperand, lowerLambda, binOpSymbol. expr.zig reaches arithResultType, exprIsFailable, binOpSymbol via Lowering-namespace alias consts. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn.
This commit is contained in:
978
src/ir/lower.zig
978
src/ir/lower.zig
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,9 @@ const Builder = mod_mod.Builder;
|
||||
const lower = @import("../lower.zig");
|
||||
const Lowering = lower.Lowering;
|
||||
const Scope = lower.Scope;
|
||||
const binOpSymbol = Lowering.binOpSymbol;
|
||||
const arithResultType = Lowering.arithResultType;
|
||||
const exprIsFailable = Lowering.exprIsFailable;
|
||||
const headNameOfCallee = Lowering.headNameOfCallee;
|
||||
const StructConstInfo = Lowering.StructConstInfo;
|
||||
|
||||
@@ -1422,3 +1425,961 @@ pub fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId {
|
||||
}
|
||||
|
||||
// ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─
|
||||
|
||||
pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
||||
// Stamp this node's source span onto the instructions it emits (ERR
|
||||
// E3.0 — feeds DWARF line-info + comptime frame resolution). Save/
|
||||
// restore so a parent's later emits keep the parent's span after a
|
||||
// child lowers. Skip the empty default so synthetic nodes don't reset
|
||||
// a meaningful enclosing span to offset 0.
|
||||
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 };
|
||||
// A node carrying an explicit `source_file` is one spliced into a body
|
||||
// from another module — a substituted caller comptime-`$`-arg (stamped
|
||||
// at the `cpn` build site in lowerComptimeCall / monomorphizePackFn).
|
||||
// Resolve its bare names in THAT module's visibility context, overriding
|
||||
// the body's defining-module pin, then restore so sibling callee nodes
|
||||
// keep the enclosing context. Ordinary expression nodes never carry a
|
||||
// `source_file`, so this is a no-op on the hot path.
|
||||
const restore_source = node.source_file != null;
|
||||
const saved_source = self.current_source_file;
|
||||
if (node.source_file) |sf| self.setCurrentSourceFile(sf);
|
||||
defer if (restore_source) self.setCurrentSourceFile(saved_source);
|
||||
return switch (node.data) {
|
||||
// Bare `$<pack>` in expression position → an `[]Type` slice
|
||||
// value where each element is a `const_type(arg_types[i])`.
|
||||
// Per `Type → .any` mapping in type_bridge, the IR slice
|
||||
// type is `[]Any`; the interp stores raw `.type_tag` Values
|
||||
// (NOT Any-boxed) so `args[i]` reads back as a Type value
|
||||
// directly. Step 4 final slice — lets builder fns walk the
|
||||
// whole pack at interp time.
|
||||
.comptime_pack_ref => |cpr| blk: {
|
||||
// `$<name>` is overloaded in expression position:
|
||||
// - Inside a pack-fn mono (or a `tryPackImplMatch`
|
||||
// impl mono), `name` is a pack binding → slice of
|
||||
// element types (`[]Type` lowered as `[]Any`).
|
||||
// - Inside an impl mono whose impl pattern bound a
|
||||
// single-type generic (`$R: Type` in
|
||||
// `Closure(..$args) -> $R`), `name` is in
|
||||
// `type_bindings` → single `const_type(R)` value.
|
||||
// Pack arg types are checked first (the slice form),
|
||||
// then pack_bindings (the impl-mono mirror), then
|
||||
// type_bindings (single-type binding); only if all
|
||||
// miss is it a real "outside an active binding" error.
|
||||
if (self.pack_arg_types) |pat| {
|
||||
if (pat.get(cpr.pack_name)) |arg_tys| {
|
||||
break :blk self.buildPackSliceValue(arg_tys);
|
||||
}
|
||||
}
|
||||
if (self.pack_bindings) |pb| {
|
||||
if (pb.get(cpr.pack_name)) |arg_tys| {
|
||||
break :blk self.buildPackSliceValue(arg_tys);
|
||||
}
|
||||
}
|
||||
if (self.type_bindings) |tb| {
|
||||
if (tb.get(cpr.pack_name)) |ty| {
|
||||
break :blk self.builder.constType(ty);
|
||||
}
|
||||
}
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name});
|
||||
}
|
||||
break :blk self.builder.constNull(self.module.types.sliceOf(.any));
|
||||
},
|
||||
// Pack-index in expression position: `$<pack>[<lit>]` →
|
||||
// `const_type(arg_types[index])`. Yields a comptime-only
|
||||
// Type value (`Value.type_tag(TypeId)` in the interp).
|
||||
// OOB / no-active-pack-binding → focused diagnostic; the
|
||||
// emitted Ref is a const_type(.void) placeholder so the
|
||||
// verifier downstream catches misuse rather than silently
|
||||
// succeeding with .void.
|
||||
.pack_index_type_expr => |pi| blk: {
|
||||
if (self.pack_arg_types) |pat| {
|
||||
if (pat.get(pi.pack_name)) |arg_tys| {
|
||||
if (pi.index < arg_tys.len) {
|
||||
break :blk self.builder.constType(arg_tys[pi.index]);
|
||||
}
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{
|
||||
pi.pack_name, pi.index, pi.pack_name, arg_tys.len,
|
||||
if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"),
|
||||
});
|
||||
}
|
||||
break :blk self.builder.constType(.void);
|
||||
}
|
||||
}
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{
|
||||
pi.pack_name, pi.index,
|
||||
});
|
||||
}
|
||||
break :blk self.builder.constType(.void);
|
||||
},
|
||||
.int_literal => |lit| {
|
||||
// If target is a float type, emit as float literal
|
||||
if (self.target_type) |tt| {
|
||||
if (tt == .f32 or tt == .f64) {
|
||||
return self.builder.constFloat(@floatFromInt(lit.value), tt);
|
||||
}
|
||||
}
|
||||
const ty = if (self.target_type) |tt| blk: {
|
||||
break :blk if (self.isIntEx(tt)) tt else .s64;
|
||||
} else .s64;
|
||||
return self.builder.constInt(lit.value, ty);
|
||||
},
|
||||
.float_literal => |lit| {
|
||||
const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64;
|
||||
return self.builder.constFloat(lit.value, fty);
|
||||
},
|
||||
.bool_literal => |lit| self.builder.constBool(lit.value),
|
||||
.string_literal => |lit| blk: {
|
||||
const str = if (lit.is_raw)
|
||||
lit.raw
|
||||
else
|
||||
unescape.unescapeString(self.alloc, lit.raw) catch lit.raw;
|
||||
const sid = self.module.types.internString(str);
|
||||
break :blk self.builder.constString(sid);
|
||||
},
|
||||
// A bare `null` / `---` with no surrounding type expectation is a
|
||||
// legitimate typeless literal, not a failed lookup: `.void` is its
|
||||
// intentional default (emitConstNull/emitConstUndef handle void as
|
||||
// null-ptr / undef-i64). Not a candidate for the `.unresolved` tripwire.
|
||||
.null_literal => self.builder.constNull(self.target_type orelse .void),
|
||||
.undef_literal => self.builder.constUndef(self.target_type orelse .void),
|
||||
|
||||
.identifier => |id| blk: {
|
||||
// A bare pack name in value position has no runtime
|
||||
// representation (Decision 1). Projections (`xs.len`, `xs[i]`,
|
||||
// `xs.value`) are field/index nodes handled elsewhere, so a bare
|
||||
// `xs` reaching here is always a pack-as-value misuse.
|
||||
if (self.isPackName(id.name)) {
|
||||
break :blk self.diagPackAsValue(id.name, node.span, .generic);
|
||||
}
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(id.name)) |binding| {
|
||||
if (binding.is_alloca) {
|
||||
break :blk self.builder.load(binding.ref, binding.ty);
|
||||
}
|
||||
break :blk binding.ref;
|
||||
}
|
||||
}
|
||||
// Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals
|
||||
if (self.comptime_constants.get(id.name)) |cv| {
|
||||
switch (cv) {
|
||||
.int_val => |iv| break :blk self.builder.constInt(iv, .s64),
|
||||
.enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty),
|
||||
}
|
||||
}
|
||||
// `context` resolves to a load through the lowering's
|
||||
// current `__sx_ctx` pointer. Every sx function (and
|
||||
// every `push Context.{...}` body) sets `current_ctx_ref`
|
||||
// to a `*Context` it owns, so this is one indirection.
|
||||
if (std.mem.eql(u8, id.name, "context")) {
|
||||
if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) {
|
||||
break :blk self.diagnoseMissingContext("the `context` identifier");
|
||||
}
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
break :blk self.diagnoseMissingContext("the `context` identifier");
|
||||
};
|
||||
break :blk self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||
}
|
||||
// Check globals (#run constants)
|
||||
if (self.program_index.global_names.get(id.name)) |gi| {
|
||||
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
||||
}
|
||||
// Check module-level value constants (e.g. AF_INET :s32: 2)
|
||||
if (self.program_index.module_const_map.get(id.name)) |ci_global| {
|
||||
if (!self.isNameVisible(id.name)) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name});
|
||||
break :blk self.emitError(id.name, node.span);
|
||||
}
|
||||
// F2: emit the SOURCE-AWARE author's value (own-wins), not the
|
||||
// global last-wins `ci_global`. ≥2 flat-visible same-name const
|
||||
// authors → a loud ambiguity (issue 0105 / 0760), never a silent
|
||||
// pick. `.none` after a visible name is the registration-only
|
||||
// author (no per-source partition) — emit its global value.
|
||||
switch (self.selectModuleConst(id.name)) {
|
||||
.resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source),
|
||||
.ambiguous => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name});
|
||||
break :blk self.emitPlaceholder(id.name);
|
||||
},
|
||||
.none => break :blk self.emitModuleConst(ci_global, null),
|
||||
}
|
||||
}
|
||||
// Check if it's a function name — produce function pointer reference
|
||||
// Resolve mangled name for block-local functions
|
||||
const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
|
||||
if (self.program_index.fn_ast_map.contains(eff_fn_name)) {
|
||||
// Visibility check only for user-typed bare names (id.name
|
||||
// == eff_fn_name) without a UFCS alias. Mangled local-
|
||||
// scope names and UFCS rewrites are compiler indirections
|
||||
// and stay exempt.
|
||||
if (std.mem.eql(u8, eff_fn_name, id.name) and
|
||||
self.program_index.ufcs_alias_map.get(id.name) == null and
|
||||
!self.isNameVisible(eff_fn_name))
|
||||
{
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name});
|
||||
break :blk self.emitError(eff_fn_name, node.span);
|
||||
}
|
||||
// Type-as-value: if target is Any (Type variable), produce a type name string
|
||||
if (self.target_type == .any) {
|
||||
const fd = self.program_index.fn_ast_map.get(eff_fn_name).?;
|
||||
const fn_type_str = self.formatFnTypeString(fd);
|
||||
const sid = self.module.types.internString(fn_type_str);
|
||||
const str = self.builder.constString(sid);
|
||||
break :blk self.builder.boxAny(str, .string);
|
||||
}
|
||||
// fix-0102d site 2: taking a bare same-name fn as a VALUE
|
||||
// (func_ref, fn-ptr / closure coercion) must capture the
|
||||
// RESOLVED author's FuncId for a genuine flat collision, not
|
||||
// the first-wins winner's. Plain bare name only; `.ambiguous`
|
||||
// → loud diagnostic; `.none` → existing first-wins path. The
|
||||
// winner is lazily lowered ONLY on `.none` — a rerouted value
|
||||
// never uses the winner, so its body must not be lowered.
|
||||
const value_fid: ?FuncId = blk_fv: {
|
||||
if (std.mem.eql(u8, eff_fn_name, id.name) and
|
||||
self.program_index.ufcs_alias_map.get(id.name) == null and
|
||||
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
switch (self.selectPlainCallableAuthor(id.name, caller_file)) {
|
||||
.func => |sf| {
|
||||
var selected = sf;
|
||||
break :blk_fv self.selectedFuncId(&selected, id.name);
|
||||
},
|
||||
.ambiguous => {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name});
|
||||
break :blk self.emitError(id.name, node.span);
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!self.lowered_functions.contains(eff_fn_name)) {
|
||||
self.lazyLowerFunction(eff_fn_name);
|
||||
}
|
||||
break :blk_fv self.resolveFuncByName(eff_fn_name);
|
||||
};
|
||||
if (value_fid) |fid| {
|
||||
// Auto-promote bare function → closure when target_type is closure
|
||||
if (self.target_type) |tt| {
|
||||
if (!tt.isBuiltin()) {
|
||||
const tt_info = self.module.types.get(tt);
|
||||
if (tt_info == .closure) {
|
||||
const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure);
|
||||
break :blk self.builder.closureCreate(tramp_id, Ref.none, tt);
|
||||
}
|
||||
// Coercing a bare fn name to a fn-pointer
|
||||
// type — the call_conv must match. A
|
||||
// default-conv sx fn assigned to a
|
||||
// callconv(.c) slot (e.g. passed to
|
||||
// pthread_create) would otherwise crash at
|
||||
// runtime when the C caller doesn't supply
|
||||
// the implicit __sx_ctx arg.
|
||||
if (tt_info == .function) {
|
||||
const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv;
|
||||
if (func_cc != tt_info.function.call_conv) {
|
||||
if (self.diagnostics) |d| {
|
||||
const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention";
|
||||
const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention";
|
||||
d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc });
|
||||
}
|
||||
break :blk self.emitPlaceholder(eff_fn_name);
|
||||
}
|
||||
}
|
||||
// NOTE: `xx <sx_fn> : *void` (e.g.
|
||||
// `class_addMethod(_, _, xx my_imp, _)`)
|
||||
// is intentionally NOT diagnosed here.
|
||||
// Manually-constructed Closure values
|
||||
// legitimately store default-conv sx fns
|
||||
// into a `*void` slot for sx-side dispatch
|
||||
// through the closure trampoline ABI. The
|
||||
// compiler can't distinguish C-side vs
|
||||
// sx-side use from the cast alone.
|
||||
// examples/50-smoke.sx has both shapes.
|
||||
}
|
||||
}
|
||||
break :blk self.builder.emit(.{ .func_ref = fid }, .s64);
|
||||
}
|
||||
}
|
||||
// Type-as-value: a name that resolves to a TypeId
|
||||
// (primitive, alias, registered struct/enum/union,
|
||||
// generic-struct instantiation) evaluates to a
|
||||
// `const_type` in expression position. Works for
|
||||
// direct assignment to a `Type`-typed slot
|
||||
// (`x: Type = Vec4`), comparison (`x == Vec4`), and
|
||||
// pack-arg / Any context (boxing happens at the
|
||||
// consumer).
|
||||
// E4 single-hop visibility + ambiguity gate: a bare type name used
|
||||
// as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over
|
||||
// 2+ flat hops is not bare-visible (consistent with annotations /
|
||||
// 0763); ≥2 direct flat same-name authors are ambiguous (loud
|
||||
// diagnostic, 0755/0767). A single source-keyed author — including
|
||||
// the querying source's OWN author over a same-name flat import
|
||||
// (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name
|
||||
// author a global `findByName` would pick. A value name / generic
|
||||
// param / undeclared name → `.proceed`, falling through below.
|
||||
const ty = blk_ty: {
|
||||
switch (self.headTypeGate(id.name, node.span)) {
|
||||
.ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name),
|
||||
.resolved => |tid| break :blk_ty tid,
|
||||
.proceed => {},
|
||||
}
|
||||
if (self.type_bindings) |tb| {
|
||||
if (tb.get(id.name)) |t| break :blk_ty t;
|
||||
}
|
||||
if (self.program_index.type_alias_map.get(id.name)) |t| break :blk_ty t;
|
||||
if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t;
|
||||
const name_id = self.module.types.internString(id.name);
|
||||
if (self.module.types.findByName(name_id)) |t| break :blk_ty t;
|
||||
break :blk_ty TypeId.void;
|
||||
};
|
||||
if (ty != .void) {
|
||||
break :blk self.builder.constType(ty);
|
||||
}
|
||||
// Unknown identifier
|
||||
break :blk self.emitError(id.name, node.span);
|
||||
},
|
||||
|
||||
.binary_op => |bop| self.lowerBinaryOp(&bop),
|
||||
|
||||
.unary_op => |uop| blk: {
|
||||
// `xx <pack>` with a slice target materializes the comptime
|
||||
// pack into a runtime `[]elem` (issue 0053). Must run before the
|
||||
// operand is lowered (a bare pack name otherwise hits the
|
||||
// pack-as-value error).
|
||||
if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) {
|
||||
const pname = uop.operand.data.identifier.name;
|
||||
if (self.target_type) |tt| {
|
||||
if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) {
|
||||
break :blk self.lowerPackToSlice(pname, tt);
|
||||
}
|
||||
}
|
||||
break :blk self.diagPackAsValue(pname, node.span, .generic);
|
||||
}
|
||||
// address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of
|
||||
if (uop.op == .address_of and uop.operand.data == .index_expr) {
|
||||
const ie = &uop.operand.data.index_expr;
|
||||
const idx = self.lowerExpr(ie.index);
|
||||
const obj_ty = self.inferExprType(ie.object);
|
||||
const elem_ty = self.getElementType(obj_ty);
|
||||
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
||||
// For array targets, use the storage pointer (alloca for a
|
||||
// local, global_addr for a module global) so the resulting
|
||||
// pointer is into live storage, not a loaded copy.
|
||||
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);
|
||||
break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty);
|
||||
}
|
||||
// address_of(field_access) → use lowerExprAsPtr for GEP chain
|
||||
// Handles all cases: pointer-based, index-based, nested field access
|
||||
if (uop.op == .address_of and uop.operand.data == .field_access) {
|
||||
const inner_ty = self.inferExprType(uop.operand);
|
||||
const ptr_ty = self.module.types.ptrTo(inner_ty);
|
||||
const ptr = self.lowerExprAsPtr(uop.operand);
|
||||
break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty);
|
||||
}
|
||||
// address_of(identifier) → return alloca directly (pointer to variable)
|
||||
if (uop.op == .address_of and uop.operand.data == .identifier) {
|
||||
const id_name = uop.operand.data.identifier.name;
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(id_name)) |binding| {
|
||||
if (binding.is_alloca) {
|
||||
const ptr_ty = self.module.types.ptrTo(binding.ty);
|
||||
break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
// address_of(global) → emit global_addr (pointer to global, not load)
|
||||
if (self.program_index.global_names.get(id_name)) |gi| {
|
||||
const ptr_ty = self.module.types.ptrTo(gi.ty);
|
||||
break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty);
|
||||
}
|
||||
}
|
||||
const operand = self.lowerExpr(uop.operand);
|
||||
break :blk switch (uop.op) {
|
||||
.negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
||||
.not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool),
|
||||
.bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
||||
.xx => self.lowerXX(operand, uop.operand),
|
||||
.address_of => blk2: {
|
||||
const inner_ty = self.inferExprType(uop.operand);
|
||||
const ptr_ty = self.module.types.ptrTo(inner_ty);
|
||||
break :blk2 self.builder.emit(.{ .addr_of = .{ .operand = operand } }, ptr_ty);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
.if_expr => |ie| self.lowerIfExpr(&ie),
|
||||
.match_expr => |me| self.lowerMatch(&me),
|
||||
.while_expr => |we| self.lowerWhile(&we),
|
||||
.for_expr => |fe| self.lowerFor(&fe),
|
||||
.break_expr => self.lowerBreak(),
|
||||
.continue_expr => self.lowerContinue(),
|
||||
.call => |c| self.lowerCall(&c),
|
||||
.ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic),
|
||||
.field_access => |fa| self.lowerFieldAccess(&fa, node.span),
|
||||
.struct_literal => |sl| self.lowerStructLiteral(&sl, node.span),
|
||||
.array_literal => |al| self.lowerArrayLiteral(&al),
|
||||
.index_expr => |ie| self.lowerIndexExpr(&ie),
|
||||
.slice_expr => |se| self.lowerSliceExpr(&se),
|
||||
.lambda => |lam| self.lowerLambda(&lam),
|
||||
.force_unwrap => |fu| self.lowerForceUnwrap(&fu),
|
||||
.null_coalesce => |nc| self.lowerNullCoalesce(&nc),
|
||||
.deref_expr => |de| self.lowerDerefExpr(&de),
|
||||
.enum_literal => |el| self.lowerEnumLiteral(&el),
|
||||
.comptime_expr => |ct| self.lowerInlineComptime(ct.expr),
|
||||
.insert_expr => |ins| blk: {
|
||||
break :blk self.lowerInsertExprValue(ins.expr);
|
||||
},
|
||||
.tuple_literal => |tl| self.lowerTupleLiteral(&tl),
|
||||
.spread_expr => self.emitError("spread_expr", node.span),
|
||||
.chained_comparison => |cc| self.lowerChainedComparison(&cc),
|
||||
|
||||
// `#jni_env(env) { body }` in expression position — the block's
|
||||
// value becomes the env-scope's value. Save→set→body-value→restore.
|
||||
.jni_env_block => |eb| blk: {
|
||||
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;
|
||||
const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void);
|
||||
_ = 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);
|
||||
break :blk value;
|
||||
},
|
||||
|
||||
// Statements that can appear in expression position
|
||||
.block => |blk| 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();
|
||||
}
|
||||
// This block sits in value position (lowerExpr is reached only
|
||||
// for value contexts — statement blocks go through lowerBlock).
|
||||
// If its last expression's value is discarded by a `;`, the
|
||||
// surrounding expression has no value to use: report it.
|
||||
if (!blk.produces_value and blk.discarded_semi != null) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, blk.discarded_semi.?, "this block is used as a value but its last expression's value is discarded by this `;` — drop the `;`", .{});
|
||||
}
|
||||
}
|
||||
// A block in expression position yields its last statement's
|
||||
// value only when it produces one (no trailing `;`); otherwise
|
||||
// it runs as statements and evaluates to void.
|
||||
if (blk.produces_value and blk.stmts.len > 0) {
|
||||
for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| {
|
||||
self.lowerStmt(stmt);
|
||||
}
|
||||
break :blk self.tryLowerAsExpr(blk.stmts[blk.stmts.len - 1]) orelse
|
||||
self.builder.constInt(0, .void);
|
||||
}
|
||||
for (blk.stmts) |stmt| {
|
||||
self.lowerStmt(stmt);
|
||||
}
|
||||
break :blk self.builder.constInt(0, .void);
|
||||
},
|
||||
|
||||
// type_expr can appear as a variable reference when the name collides
|
||||
// with a builtin type name (e.g. s2, u8). Check scope first.
|
||||
.type_expr => |te| blk: {
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(te.name)) |binding| {
|
||||
if (binding.is_alloca) {
|
||||
break :blk self.builder.load(binding.ref, binding.ty);
|
||||
}
|
||||
break :blk binding.ref;
|
||||
}
|
||||
}
|
||||
if (self.program_index.global_names.get(te.name)) |gi| {
|
||||
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
||||
}
|
||||
// Type literal in expression position → first-class
|
||||
// `const_type` Value (i64 = TypeId.index()). Makes
|
||||
// `t : Type = f64;` store a real TypeId; lets
|
||||
// `t == f64` icmp at runtime against the same TypeId.
|
||||
if (self.isKnownTypeName(te.name)) {
|
||||
const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||||
break :blk self.builder.constType(ty);
|
||||
}
|
||||
break :blk self.emitError(te.name, node.span);
|
||||
},
|
||||
|
||||
.try_expr => |te| self.lowerTry(te.operand, node.span),
|
||||
.catch_expr => |ce| self.lowerCatch(&ce, node.span),
|
||||
.caller_location => self.lowerCallerLocation(node),
|
||||
else => self.emitError("unknown_expr", node.span),
|
||||
};
|
||||
}
|
||||
|
||||
/// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns
|
||||
/// the element (pointee) type so a value-position use can auto-deref it.
|
||||
pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId {
|
||||
if (node.data != .identifier) return null;
|
||||
const scope = self.scope orelse return null;
|
||||
const binding = scope.lookup(node.data.identifier.name) orelse return null;
|
||||
if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null;
|
||||
const info = self.module.types.get(binding.ty);
|
||||
return if (info == .pointer) info.pointer.pointee else null;
|
||||
}
|
||||
|
||||
pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
||||
// Short-circuit: `a and b` → if a then b else false
|
||||
if (bop.op == .and_op) {
|
||||
const lhs = self.lowerExpr(bop.lhs);
|
||||
const rhs_bb = self.freshBlock("and.rhs");
|
||||
const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool});
|
||||
const false_val = self.builder.constBool(false);
|
||||
self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val});
|
||||
self.builder.switchToBlock(rhs_bb);
|
||||
const rhs = self.lowerExpr(bop.rhs);
|
||||
self.builder.br(merge_bb, &.{rhs});
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return self.builder.blockParam(merge_bb, 0, .bool);
|
||||
}
|
||||
// Short-circuit: `a or b` → if a then true else b
|
||||
if (bop.op == .or_op) {
|
||||
// A failable `or` (value-terminator or chain) routes to the error-
|
||||
// handling lowering, not the optional/boolean unwrap below. Detected
|
||||
// structurally (a `try`-chain's value type is non-failable `T`, so a
|
||||
// type-only `exprIsFailable(lhs)` would miss nested chains).
|
||||
if (self.orIsFailableChain(bop)) {
|
||||
return self.lowerFailableOr(bop);
|
||||
}
|
||||
const lhs = self.lowerExpr(bop.lhs);
|
||||
const rhs_bb = self.freshBlock("or.rhs");
|
||||
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
||||
const true_val = self.builder.constBool(true);
|
||||
self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{});
|
||||
self.builder.switchToBlock(rhs_bb);
|
||||
const rhs = self.lowerExpr(bop.rhs);
|
||||
self.builder.br(merge_bb, &.{rhs});
|
||||
self.builder.switchToBlock(merge_bb);
|
||||
return self.builder.blockParam(merge_bb, 0, .bool);
|
||||
}
|
||||
|
||||
// Type-literal comparison fold: when both sides are type-shaped
|
||||
// AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to
|
||||
// a static TypeId at lower time (`type_of(x)` for any
|
||||
// statically-typed `x`), resolve each and emit a `const_bool`.
|
||||
// Same semantic as `type_eq(A, B)` but using the standard `==`
|
||||
// operator — the user's intuition. Without the fold, both
|
||||
// sides lower as `const_type` undef-i64 and the runtime icmp
|
||||
// returns garbage.
|
||||
if (bop.op == .eq or bop.op == .neq) {
|
||||
if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) {
|
||||
const lhs_ty = self.resolveTypeArg(bop.lhs);
|
||||
const rhs_ty = self.resolveTypeArg(bop.rhs);
|
||||
const eq_result = lhs_ty == rhs_ty;
|
||||
return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result);
|
||||
}
|
||||
}
|
||||
|
||||
// Any-shaped `==` (e.g. `t == s64` where `t: Type`): both
|
||||
// operands are 16-byte `{tag, value}` aggregates. LLVM
|
||||
// doesn't accept `icmp` on aggregates directly. Decompose
|
||||
// via `unbox_any` (which extracts the value field at
|
||||
// `.s64`) and compare the i64s. Tag fields are stable
|
||||
// across compilations of the same source so value-only
|
||||
// identity is enough.
|
||||
if (bop.op == .eq or bop.op == .neq) {
|
||||
const lhs_ty = self.inferExprType(bop.lhs);
|
||||
const rhs_ty = self.inferExprType(bop.rhs);
|
||||
if (lhs_ty == .any and rhs_ty == .any) {
|
||||
const lhs = self.lowerExpr(bop.lhs);
|
||||
const rhs = self.lowerExpr(bop.rhs);
|
||||
const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64);
|
||||
const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64);
|
||||
if (bop.op == .eq) {
|
||||
return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool);
|
||||
} else {
|
||||
return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: optional == null / optional != null
|
||||
if (bop.op == .eq or bop.op == .neq) {
|
||||
const lhs_is_null = bop.lhs.data == .null_literal;
|
||||
const rhs_is_null = bop.rhs.data == .null_literal;
|
||||
if (lhs_is_null or rhs_is_null) {
|
||||
const opt_node = if (rhs_is_null) bop.lhs else bop.rhs;
|
||||
const opt_ty = self.inferExprType(opt_node);
|
||||
if (!opt_ty.isBuiltin()) {
|
||||
const info = self.module.types.get(opt_ty);
|
||||
if (info == .optional) {
|
||||
const opt_val = self.lowerExpr(opt_node);
|
||||
const has = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool);
|
||||
// == null → !has_value, != null → has_value
|
||||
return if (bop.op == .eq) self.builder.emit(.{ .bool_not = .{ .operand = has } }, .bool) else has;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error-set equality: an error-set value compares only with an
|
||||
// `error.X` tag literal or another error-set value. Comparing to a raw
|
||||
// integer is a type error (coerce with `xx`). `e == error.X` resolves
|
||||
// X against e's set and validates membership.
|
||||
if (bop.op == .eq or bop.op == .neq) {
|
||||
if (self.tryLowerErrorSetEquality(bop)) |result| return result;
|
||||
}
|
||||
|
||||
// Set target_type for null literals to match the other operand's type.
|
||||
// This ensures null gets the same LLVM type as the value being compared.
|
||||
if (bop.op == .eq or bop.op == .neq) {
|
||||
const null_on_rhs = bop.rhs.data == .null_literal;
|
||||
const null_on_lhs = bop.lhs.data == .null_literal;
|
||||
if (null_on_rhs or null_on_lhs) {
|
||||
var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs);
|
||||
// Lower the non-null side first when its type isn't statically
|
||||
// inferable, and take the null's type from the lowered value —
|
||||
// never a guess.
|
||||
var pre_lowered: ?Ref = null;
|
||||
if (other_ty == .unresolved) {
|
||||
pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs);
|
||||
other_ty = self.builder.getRefType(pre_lowered.?);
|
||||
}
|
||||
if (other_ty != .void and other_ty != .unresolved) {
|
||||
const saved_tt = self.target_type;
|
||||
self.target_type = other_ty;
|
||||
const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?;
|
||||
const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?;
|
||||
self.target_type = saved_tt;
|
||||
const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } };
|
||||
return self.builder.emit(cmp_op, .bool);
|
||||
}
|
||||
}
|
||||
}
|
||||
var lhs = self.lowerExpr(bop.lhs);
|
||||
// A `for xs: (*x)` capture is a pointer; in a value position (here, an
|
||||
// operand) it auto-derefs to the element.
|
||||
const lhs_ref_pointee = self.refCapturePointee(bop.lhs);
|
||||
if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p);
|
||||
// Set target_type from LHS so enum literals on RHS resolve correctly.
|
||||
// When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use
|
||||
// the lowered operand's concrete type rather than a guess.
|
||||
const lhs_ty = blk: {
|
||||
if (lhs_ref_pointee) |p| break :blk p;
|
||||
const it = self.inferExprType(bop.lhs);
|
||||
break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it;
|
||||
};
|
||||
const saved_tt = self.target_type;
|
||||
if (lhs_ty != .void) {
|
||||
if (!lhs_ty.isBuiltin()) {
|
||||
const lhs_info = self.module.types.get(lhs_ty);
|
||||
if (lhs_info == .@"enum" or lhs_info == .@"union" or lhs_info == .tagged_union) {
|
||||
self.target_type = lhs_ty;
|
||||
}
|
||||
} else if (lhs_ty == .f32 or lhs_ty == .f64) {
|
||||
self.target_type = lhs_ty;
|
||||
}
|
||||
}
|
||||
var rhs = self.lowerExpr(bop.rhs);
|
||||
const rhs_ref_pointee = self.refCapturePointee(bop.rhs);
|
||||
if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p);
|
||||
self.target_type = saved_tt;
|
||||
// Result type follows the shared promotion rule: an int LHS with a
|
||||
// float RHS promotes to the float (`s64 * f32` → `f32`); vectors /
|
||||
// structs keep the LHS type. `inferExprType` reuses the same helper
|
||||
// so static typing agrees with the value produced here.
|
||||
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
||||
var ty = arithResultType(lhs_ty, rhs_inferred);
|
||||
|
||||
// Auto-unwrap optional operands for arithmetic/comparison
|
||||
if (!ty.isBuiltin()) {
|
||||
const info = self.module.types.get(ty);
|
||||
if (info == .optional) {
|
||||
ty = info.optional.child;
|
||||
lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty);
|
||||
}
|
||||
}
|
||||
const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
||||
if (!rhs_ty.isBuiltin()) {
|
||||
const rhs_info = self.module.types.get(rhs_ty);
|
||||
if (rhs_info == .optional) {
|
||||
rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child);
|
||||
}
|
||||
}
|
||||
|
||||
// String comparison: use str_eq/str_ne (memcmp-based) instead of pointer comparison
|
||||
if (ty == .string and (bop.op == .eq or bop.op == .neq)) {
|
||||
return if (bop.op == .eq)
|
||||
self.builder.emit(.{ .str_eq = .{ .lhs = lhs, .rhs = rhs } }, .bool)
|
||||
else
|
||||
self.builder.emit(.{ .str_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool);
|
||||
}
|
||||
|
||||
// Tuple operators
|
||||
if (!ty.isBuiltin()) {
|
||||
const lhs_info = self.module.types.get(ty);
|
||||
if (lhs_info == .tuple) {
|
||||
return self.lowerTupleOp(bop, lhs, rhs, ty);
|
||||
}
|
||||
}
|
||||
// Tuple membership: value in (tuple)
|
||||
if (bop.op == .in_op) {
|
||||
const rhs_ty_raw = self.inferExprType(bop.rhs);
|
||||
if (!rhs_ty_raw.isBuiltin()) {
|
||||
const rhs_info_raw = self.module.types.get(rhs_ty_raw);
|
||||
if (rhs_info_raw == .tuple) {
|
||||
return self.lowerTupleMembership(lhs, rhs, rhs_info_raw.tuple);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reject scalar ops on incompatible operand types (e.g.
|
||||
// `s64 + string`, `s64 < string`, `s64 & string`). The result type
|
||||
// `ty` is derived from the LHS, so without this the op lowers as
|
||||
// `<op> : <lhs>` and either reinterprets the RHS bytes (arithmetic
|
||||
// / bitwise → garbage) or feeds mismatched LLVM types to `icmp`
|
||||
// (ordering → verifier failure).
|
||||
{
|
||||
const group: enum { none, arith, ordering, bitwise } = switch (bop.op) {
|
||||
.add, .sub, .mul, .div, .mod => .arith,
|
||||
.lt, .lte, .gt, .gte => .ordering,
|
||||
.bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise,
|
||||
else => .none,
|
||||
};
|
||||
if (group != .none) {
|
||||
const eff_rhs_ty = blk: {
|
||||
if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs);
|
||||
if (!rhs_ty.isBuiltin()) {
|
||||
const ri = self.module.types.get(rhs_ty);
|
||||
if (ri == .optional) break :blk ri.optional.child;
|
||||
}
|
||||
break :blk rhs_ty;
|
||||
};
|
||||
const ok = switch (group) {
|
||||
.arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty),
|
||||
.ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty),
|
||||
.bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty),
|
||||
.none => true,
|
||||
};
|
||||
if (!ok) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{
|
||||
binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty),
|
||||
});
|
||||
}
|
||||
return self.emitPlaceholder("operand-type-mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return switch (bop.op) {
|
||||
.add => self.builder.add(lhs, rhs, ty),
|
||||
.sub => self.builder.sub(lhs, rhs, ty),
|
||||
.mul => self.builder.mul(lhs, rhs, ty),
|
||||
.div => self.builder.div(lhs, rhs, ty),
|
||||
.mod => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
||||
.eq => self.builder.cmpEq(lhs, rhs),
|
||||
.neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.lt => self.builder.cmpLt(lhs, rhs),
|
||||
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.gt => self.builder.cmpGt(lhs, rhs),
|
||||
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.and_op => self.builder.emit(.{ .bool_and = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.or_op => self.builder.emit(.{ .bool_or = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.bit_and => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
||||
.bit_or => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
||||
.bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
||||
.shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
||||
.shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
||||
.in_op => self.emitError("in_op", bop.lhs.span),
|
||||
};
|
||||
}
|
||||
|
||||
/// Handle tuple binary ops: concat (+), repeat (*), comparison (==, !=, <, <=, >, >=)
|
||||
pub fn lowerTupleOp(self: *Lowering, bop: *const ast.BinaryOp, lhs: Ref, rhs: Ref, lhs_ty: TypeId) Ref {
|
||||
const lhs_info = self.module.types.get(lhs_ty);
|
||||
const lhs_fields = lhs_info.tuple.fields;
|
||||
|
||||
switch (bop.op) {
|
||||
.add => {
|
||||
// Tuple concatenation: (a, b) + (c, d) → (a, b, c, d)
|
||||
const rhs_ty = self.inferExprType(bop.rhs);
|
||||
const rhs_fields = if (!rhs_ty.isBuiltin()) blk: {
|
||||
const ri = self.module.types.get(rhs_ty);
|
||||
break :blk if (ri == .tuple) ri.tuple.fields else &[_]TypeId{};
|
||||
} else &[_]TypeId{};
|
||||
|
||||
var all_fields = std.ArrayList(TypeId).empty;
|
||||
defer all_fields.deinit(self.alloc);
|
||||
var all_vals = std.ArrayList(Ref).empty;
|
||||
defer all_vals.deinit(self.alloc);
|
||||
|
||||
for (lhs_fields, 0..) |f, i| {
|
||||
all_fields.append(self.alloc, f) catch unreachable;
|
||||
all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable;
|
||||
}
|
||||
for (rhs_fields, 0..) |f, i| {
|
||||
all_fields.append(self.alloc, f) catch unreachable;
|
||||
all_vals.append(self.alloc, self.builder.structGet(rhs, @intCast(i), f)) catch unreachable;
|
||||
}
|
||||
|
||||
const result_ty = self.module.types.intern(.{ .tuple = .{
|
||||
.fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable,
|
||||
.names = null,
|
||||
} });
|
||||
const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable;
|
||||
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty);
|
||||
},
|
||||
.mul => {
|
||||
// Tuple repeat: (a, b) * 3 → (a, b, a, b, a, b)
|
||||
const count: usize = switch (bop.rhs.data) {
|
||||
.int_literal => |il| @intCast(@as(u64, @bitCast(il.value))),
|
||||
else => 1,
|
||||
};
|
||||
|
||||
var all_fields = std.ArrayList(TypeId).empty;
|
||||
defer all_fields.deinit(self.alloc);
|
||||
var all_vals = std.ArrayList(Ref).empty;
|
||||
defer all_vals.deinit(self.alloc);
|
||||
|
||||
for (0..count) |_| {
|
||||
for (lhs_fields, 0..) |f, i| {
|
||||
all_fields.append(self.alloc, f) catch unreachable;
|
||||
all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
const result_ty = self.module.types.intern(.{ .tuple = .{
|
||||
.fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable,
|
||||
.names = null,
|
||||
} });
|
||||
const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable;
|
||||
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty);
|
||||
},
|
||||
.eq, .neq => {
|
||||
// Element-wise equality (or single-element tuple vs scalar)
|
||||
const rhs_is_tuple = blk: {
|
||||
const rt = self.inferExprType(bop.rhs);
|
||||
if (!rt.isBuiltin()) {
|
||||
break :blk self.module.types.get(rt) == .tuple;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
if (!rhs_is_tuple and lhs_fields.len == 1) {
|
||||
// Single-element tuple vs scalar: unwrap and compare
|
||||
const lf = self.builder.structGet(lhs, 0, lhs_fields[0]);
|
||||
const eq = self.builder.cmpEq(lf, rhs);
|
||||
return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = eq } }, .bool) else eq;
|
||||
}
|
||||
var result = self.builder.constBool(true);
|
||||
for (lhs_fields, 0..) |f, i| {
|
||||
const lf = self.builder.structGet(lhs, @intCast(i), f);
|
||||
const rf = self.builder.structGet(rhs, @intCast(i), f);
|
||||
const eq = self.builder.cmpEq(lf, rf);
|
||||
result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = eq } }, .bool);
|
||||
}
|
||||
return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = result } }, .bool) else result;
|
||||
},
|
||||
.lt, .lte, .gt, .gte => {
|
||||
// Lexicographic comparison
|
||||
return self.lowerTupleLexCompare(bop.op, lhs, rhs, lhs_fields);
|
||||
},
|
||||
else => return self.builder.constInt(0, .s64),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lowerTupleLexCompare(self: *Lowering, op: ast.BinaryOp.Op, lhs: Ref, rhs: Ref, fields: []const TypeId) Ref {
|
||||
// Lexicographic comparison using boolean logic.
|
||||
// (a0,a1) < (b0,b1) = (a0 < b0) || (a0 == b0 && a1 < b1)
|
||||
// (a0,a1) <= (b0,b1) = (a0 < b0) || (a0 == b0 && a1 <= b1)
|
||||
if (fields.len == 0) return self.builder.constBool(op == .lte or op == .gte);
|
||||
|
||||
const n = fields.len;
|
||||
// Start with the last field using the actual op
|
||||
const lf_last = self.builder.structGet(lhs, @intCast(n - 1), fields[n - 1]);
|
||||
const rf_last = self.builder.structGet(rhs, @intCast(n - 1), fields[n - 1]);
|
||||
var result = switch (op) {
|
||||
.lt => self.builder.cmpLt(lf_last, rf_last),
|
||||
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lf_last, .rhs = rf_last } }, .bool),
|
||||
.gt => self.builder.cmpGt(lf_last, rf_last),
|
||||
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lf_last, .rhs = rf_last } }, .bool),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// Work backwards: result = (a[i] < b[i]) || (a[i] == b[i] && result)
|
||||
if (n > 1) {
|
||||
var i: usize = n - 1;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const lf = self.builder.structGet(lhs, @intCast(i), fields[i]);
|
||||
const rf = self.builder.structGet(rhs, @intCast(i), fields[i]);
|
||||
const strict = if (op == .lt or op == .lte) self.builder.cmpLt(lf, rf) else self.builder.cmpGt(lf, rf);
|
||||
const eq = self.builder.cmpEq(lf, rf);
|
||||
const eq_and_rest = self.builder.emit(.{ .bool_and = .{ .lhs = eq, .rhs = result } }, .bool);
|
||||
result = self.builder.emit(.{ .bool_or = .{ .lhs = strict, .rhs = eq_and_rest } }, .bool);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn lowerTupleMembership(self: *Lowering, value: Ref, tuple: Ref, tuple_info: anytype) Ref {
|
||||
// value in (a, b, c) → value == a || value == b || value == c
|
||||
var result = self.builder.constBool(false);
|
||||
for (tuple_info.fields, 0..) |f, i| {
|
||||
const elem = self.builder.structGet(tuple, @intCast(i), f);
|
||||
const eq = self.builder.cmpEq(value, elem);
|
||||
result = self.builder.emit(.{ .bool_or = .{ .lhs = result, .rhs = eq } }, .bool);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Control flow ────────────────────────────────────────────────
|
||||
|
||||
pub fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref {
|
||||
// a < b < c → (a < b) and (b < c)
|
||||
// Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice.
|
||||
if (cc.operands.len < 2 or cc.ops.len == 0) {
|
||||
return self.builder.constBool(true);
|
||||
}
|
||||
|
||||
var refs = std.ArrayList(Ref).empty;
|
||||
defer refs.deinit(self.alloc);
|
||||
for (cc.operands) |op| {
|
||||
refs.append(self.alloc, self.lowerExpr(op)) catch unreachable;
|
||||
}
|
||||
|
||||
var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]);
|
||||
|
||||
var i: usize = 1;
|
||||
while (i < cc.ops.len) : (i += 1) {
|
||||
const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]);
|
||||
result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref {
|
||||
return switch (op) {
|
||||
.eq => self.builder.cmpEq(lhs, rhs),
|
||||
.neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.lt => self.builder.cmpLt(lhs, rhs),
|
||||
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
.gt => self.builder.cmpGt(lhs, rhs),
|
||||
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
||||
else => self.builder.constBool(false),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Defer/Push/MultiAssign ──────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user