Five adversarial reviews of the issue-0160 fix surfaced three more bugs in the touched optional-chain / optional-coercion code; all fixed here: 1. A COLD generic-instance getter through `?.` (`?*Vec(i64)` `.getter`, never called directly first) panicked with "unresolved type reached LLVM emission": a cold instance method is absent from resolveFuncByName, so the getter's return type resolved to .unresolved → a ?unresolved merge type. lowerOptionalChain and getterReturnTypeOnDeref now warm the monomorph (ensureGenericInstanceMethodLowered) before querying its return type. (The 0907 test passed only by luck — List(i64) is warmed by stdlib use; 0907 now also exercises a cold user generic.) 2. A real-field read through a `?*T` chain (`op?.field`, op: ?*T) reinterpreted the pointer bits as the field (silent garbage) — the some-branch real-field path didn't load through the pointer. It now derefs `?*T` before the field access. (Pre-existing — the else-branch predates 0160 — but it's the same function and a silent miscompile, so fixed here.) 3. `?[]T = array` skipped the array→slice promotion (corrupt .len/.ptr): the lowerVarDecl optional arm wrapped the raw array. It now coerces the value to the optional's child type (array→slice) before wrapping. Regression examples 0906/0907 extended to cover all three. Distinct PRE-EXISTING bugs the reviews surfaced in untouched subsystems are filed as issues 0161 (struct-literal vs scalar), 0162 (#run returning an optional aggregate), 0163 (untagged-union payload-binding match).
3218 lines
157 KiB
Zig
3218 lines
157 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 type_bridge = @import("../type_bridge.zig");
|
||
const unescape = @import("../../unescape.zig");
|
||
const errors = @import("../../errors.zig");
|
||
const TypeResolver = @import("../type_resolver.zig").TypeResolver;
|
||
|
||
const TypeId = types.TypeId;
|
||
const StringId = types.StringId;
|
||
const Ref = inst_mod.Ref;
|
||
const FuncId = inst_mod.FuncId;
|
||
const Function = inst_mod.Function;
|
||
const Module = mod_mod.Module;
|
||
|
||
|
||
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;
|
||
|
||
pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref {
|
||
// Check for tagged enum construction: .Variant.{ payload_fields }
|
||
// This happens when type_expr is an enum_literal and target_type is a union
|
||
if (sl.type_expr) |te| {
|
||
if (te.data == .enum_literal) {
|
||
const variant_name = te.data.enum_literal.name;
|
||
const union_ty = self.target_type orelse .unresolved;
|
||
if (!union_ty.isBuiltin()) {
|
||
const union_info = self.module.types.get(union_ty);
|
||
if (union_info == .tagged_union) {
|
||
return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// `.{ name = ... }` against a tagged-union target_type. Reject:
|
||
// the only valid construction forms are `.variant(payload)` and
|
||
// `.variant.{ field, ... }`. Falling through would lower the
|
||
// user's values straight into the `(tag, payload_bytes)` slot
|
||
// pair and emit IR that LLVM later rejects.
|
||
if (sl.type_expr == null and sl.struct_name == null) {
|
||
const tu_ty = self.target_type orelse .unresolved;
|
||
if (!tu_ty.isBuiltin()) {
|
||
const tu_info = self.module.types.get(tu_ty);
|
||
if (tu_info == .tagged_union) {
|
||
if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) {
|
||
const first_name = sl.field_inits[0].name.?;
|
||
if (self.diagnostics) |diags| {
|
||
const ty_name = self.formatTypeName(tu_ty);
|
||
if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) {
|
||
diags.addFmt(
|
||
.err,
|
||
span,
|
||
"cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`",
|
||
.{ ty_name, first_name, first_name, first_name },
|
||
);
|
||
} else {
|
||
self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span);
|
||
}
|
||
}
|
||
return self.builder.enumInit(0, Ref.none, tu_ty);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const ty: TypeId = if (sl.struct_name) |name|
|
||
// Source-aware (E2): a bare struct-literal type name resolves to the
|
||
// querying source's OWN same-name author, not the global `findByName`
|
||
// first-match — so `Box.{...}` in module B builds B's `Box`, never a
|
||
// flat-imported A's. `.undeclared`/`.pending` keep the empty-struct
|
||
// stub (byte-identical to the legacy `findByName orelse intern`);
|
||
// `.ambiguous`/`.not_visible` surface their loud diagnostic + poison.
|
||
self.resolveNominalLeaf(name, false, span)
|
||
else if (sl.type_expr) |te|
|
||
// Generic struct literal: Pair(i32).{ ... } — resolve type from type_expr
|
||
self.resolveTypeWithBindings(te)
|
||
else self.target_type orelse .unresolved;
|
||
|
||
// Plain (untagged) union target: build by writing each named member into a
|
||
// union-sized slot. `getStructFields` returns empty for a union, so the
|
||
// generic struct path below would emit a malformed `structInit` whose
|
||
// overlapping zero-fill clobbers the named member (issue 0158). Tagged
|
||
// unions were already handled above.
|
||
if (!ty.isBuiltin() and self.module.types.get(ty) == .@"union") {
|
||
return self.lowerUnionLiteral(sl, ty, span);
|
||
}
|
||
|
||
// A bare struct literal against an optional target `?T` builds the INNER
|
||
// `T` and wraps it once. Without this `ty` is the optional itself, so the
|
||
// literal is lowered into the optional's `{payload, has_value}` layout and
|
||
// then re-wrapped — corrupting the value (a `?T` arg silently reads as
|
||
// null) or failing LLVM verification on the double wrap (issue 0160).
|
||
// Only the bare `.{ ... }` form reaches here with an optional `ty` (a named
|
||
// / generic literal resolves `ty` from its own type, never the target).
|
||
if (!ty.isBuiltin() and self.module.types.get(ty) == .optional) {
|
||
const child = self.module.types.get(ty).optional.child;
|
||
// Build the inner `T` (targeting it so nested literals resolve), then
|
||
// wrap to `?T`. Building into `ty` (the optional) directly would fill
|
||
// its {payload, has_value} layout and corrupt the value / fail LLVM
|
||
// verification. Wrapping the raw struct_init SSA aggregate ALSO mislays
|
||
// a multi-field payload, so round-trip through memory first — the wrap
|
||
// then sees a loaded value, the same shape the working `T -> ?T` value
|
||
// coercion wraps. Returning a fully-built `?T` makes EVERY caller
|
||
// context correct, including array/struct-literal element slots that
|
||
// don't re-coerce (issue 0160).
|
||
const saved_tt = self.target_type;
|
||
self.target_type = child;
|
||
const inner = self.lowerStructLiteral(sl, span);
|
||
self.target_type = saved_tt;
|
||
const slot = self.builder.alloca(child);
|
||
self.builder.store(slot, inner);
|
||
const reloaded = self.builder.load(slot, child);
|
||
return self.coerceToType(reloaded, child, ty);
|
||
}
|
||
|
||
// Get struct field types for coercion and ordering
|
||
const struct_fields = self.getStructFields(ty);
|
||
|
||
// Look up field defaults from AST
|
||
const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: {
|
||
const ti = self.module.types.get(ty);
|
||
break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null);
|
||
} else @as(?[]const u8, null);
|
||
const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn|
|
||
(self.struct_defaults_map.get(sn) orelse &.{})
|
||
else
|
||
&.{};
|
||
|
||
// Check if any field_init has a name (named literal)
|
||
const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null;
|
||
|
||
if (has_names and struct_fields.len > 0) {
|
||
// Named literal: reorder fields to match struct declaration order
|
||
// First, lower all field values in source order (to preserve evaluation order)
|
||
var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty;
|
||
defer lowered.deinit(self.alloc);
|
||
for (sl.field_inits) |fi| {
|
||
const saved_tt = self.target_type;
|
||
// Set target_type to the field's declared type so array literals
|
||
// know if the target is a vector, etc.
|
||
if (fi.name) |fname| {
|
||
for (struct_fields) |sf| {
|
||
if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) {
|
||
self.target_type = sf.ty;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
const val = self.lowerExpr(fi.value);
|
||
self.target_type = saved_tt;
|
||
lowered.append(self.alloc, .{
|
||
.val = val,
|
||
.name = fi.name orelse "",
|
||
.node = fi.value,
|
||
}) catch unreachable;
|
||
}
|
||
|
||
// Build fields in declaration order
|
||
var fields = std.ArrayList(Ref).empty;
|
||
defer fields.deinit(self.alloc);
|
||
for (struct_fields, 0..) |sf, fi| {
|
||
const sf_name = self.module.types.getString(sf.name);
|
||
// Find the matching lowered value
|
||
var found = false;
|
||
for (lowered.items) |l| {
|
||
if (std.mem.eql(u8, l.name, sf_name)) {
|
||
var val = l.val;
|
||
const src_ty = self.builder.getRefType(val);
|
||
val = self.coerceToType(val, src_ty, sf.ty);
|
||
fields.append(self.alloc, val) catch unreachable;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
// Field not specified — use default if available, else zero
|
||
if (fi < field_defaults.len) {
|
||
if (field_defaults[fi]) |default_expr| {
|
||
// Coerce the default to the field type at the IR
|
||
// level (the implicit narrowing rule) so a float
|
||
// default folds/errors here instead of being
|
||
// silently bit-coerced by the backend.
|
||
fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable;
|
||
} else {
|
||
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
||
}
|
||
} else {
|
||
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
||
}
|
||
}
|
||
}
|
||
|
||
const result = self.builder.structInit(fields.items, ty);
|
||
if (sl.init_block) |ib| {
|
||
return self.lowerInitBlock(result, ty, ib);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// Positional literal: use source order
|
||
var fields = std.ArrayList(Ref).empty;
|
||
defer fields.deinit(self.alloc);
|
||
|
||
for (sl.field_inits, 0..) |fi, i| {
|
||
var val = self.lowerExpr(fi.value);
|
||
// Coerce field value to match struct field type
|
||
if (i < struct_fields.len) {
|
||
const src_ty = self.inferExprType(fi.value);
|
||
val = self.coerceToType(val, src_ty, struct_fields[i].ty);
|
||
}
|
||
fields.append(self.alloc, val) catch unreachable;
|
||
}
|
||
|
||
// Pad missing fields with defaults or zeroes
|
||
if (fields.items.len < struct_fields.len) {
|
||
for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| {
|
||
if (fi < field_defaults.len) {
|
||
if (field_defaults[fi]) |default_expr| {
|
||
fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable;
|
||
continue;
|
||
}
|
||
}
|
||
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
||
}
|
||
}
|
||
|
||
const result = self.builder.structInit(fields.items, ty);
|
||
|
||
// Lower init block if present
|
||
if (sl.init_block) |ib| {
|
||
return self.lowerInitBlock(result, ty, ib);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// Lower an init block: store struct value to alloca, bind `self`, execute block, reload.
|
||
pub fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref {
|
||
// Store struct value to a temporary alloca
|
||
const ptr_ty = self.module.types.ptrTo(ty);
|
||
const slot = self.builder.alloca(ty);
|
||
self.builder.store(slot, struct_val);
|
||
|
||
// Create a nested scope with `self` bound to the alloca pointer
|
||
var init_scope = Scope.init(self.alloc, self.scope);
|
||
defer init_scope.deinit();
|
||
const saved_scope = self.scope;
|
||
self.scope = &init_scope;
|
||
|
||
// `self` is the pointer to the struct (not an alloca itself — it IS the pointer value)
|
||
init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false });
|
||
|
||
// Lower the init block body
|
||
self.lowerBlock(ib);
|
||
|
||
// Restore scope
|
||
self.scope = saved_scope;
|
||
|
||
// Load and return the (possibly modified) struct value
|
||
return self.builder.load(slot, ty);
|
||
}
|
||
|
||
/// Get the field list for a struct TypeId, or empty if not a struct.
|
||
pub fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field {
|
||
if (ty.isBuiltin()) return &.{};
|
||
var resolved = ty;
|
||
const info = self.module.types.get(resolved);
|
||
// Dereference pointer types to get to the underlying struct
|
||
if (info == .pointer) {
|
||
resolved = info.pointer.pointee;
|
||
if (resolved.isBuiltin()) return &.{};
|
||
const inner = self.module.types.get(resolved);
|
||
return switch (inner) {
|
||
.@"struct" => |s| s.fields,
|
||
else => &.{},
|
||
};
|
||
}
|
||
return switch (info) {
|
||
.@"struct" => |s| s.fields,
|
||
else => &.{},
|
||
};
|
||
}
|
||
|
||
/// If a method's first param expects a pointer (*T) but we're passing T by value,
|
||
/// swap the first arg with the alloca address (implicit address-of).
|
||
pub fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void {
|
||
// Skip the implicit __sx_ctx param when inspecting the receiver slot.
|
||
const skip: usize = if (func.has_implicit_ctx) 1 else 0;
|
||
if (func.params.len <= skip) return;
|
||
const first_param_ty = func.params[skip].ty;
|
||
// Check if first param expects a pointer
|
||
if (!first_param_ty.isBuiltin()) {
|
||
const pi = self.module.types.get(first_param_ty);
|
||
if (pi == .pointer) {
|
||
// If obj is already a pointer type, it's already correct (no addr_of needed)
|
||
if (!obj_ty.isBuiltin()) {
|
||
const oi = self.module.types.get(obj_ty);
|
||
if (oi == .pointer) return; // already a pointer
|
||
}
|
||
// Method expects *T — pass the address of the receiver (value type in alloca)
|
||
if (obj_node.data == .identifier) {
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(obj_node.data.identifier.name)) |binding| {
|
||
if (binding.is_alloca) {
|
||
const ptr_ty = self.module.types.ptrTo(binding.ty);
|
||
method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Compound lvalue receiver: obj.field.method() / arr[i].method() /
|
||
// (*p).method() → take the lvalue's real address so mutations
|
||
// through *T are visible on the original storage (not a throwaway
|
||
// copy). Mirrors the explicit-arg path in call.zig.
|
||
//
|
||
// Exclude a comptime-pack index (`xs[i]` where `xs` is a pack): a
|
||
// pack has no runtime storage to address — its element is materialized
|
||
// at comptime and can't be mutated in place — so it must keep flowing
|
||
// through the general alloca+store-of-value path below.
|
||
const is_pack_index = obj_node.data == .index_expr and
|
||
obj_node.data.index_expr.object.data == .identifier and
|
||
self.isPackName(obj_node.data.index_expr.object.data.identifier.name);
|
||
if (!is_pack_index and (obj_node.data == .field_access or obj_node.data == .index_expr or obj_node.data == .deref_expr)) {
|
||
// `lowerExprAsPtr` yields the lvalue's address, typed either as
|
||
// `*T` already (index/deref) or as the pointee `T` (a field
|
||
// "place" ref). Normalize to `*T`: if it's already the pointer
|
||
// type, pass it directly; if it's the pointee value type, wrap
|
||
// with addr_of (a no-op in LLVM) to set the IR type to *T,
|
||
// preventing coerceCallArgs from doing a spurious alloca+store.
|
||
const ptr_ty = self.module.types.ptrTo(obj_ty);
|
||
const place = self.lowerExprAsPtr(obj_node);
|
||
const place_ty = self.builder.getRefType(place);
|
||
if (place_ty == ptr_ty) {
|
||
method_args.items[0] = place;
|
||
} else {
|
||
method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = place } }, ptr_ty);
|
||
}
|
||
return;
|
||
}
|
||
// General case: alloca+store the value and pass the alloca pointer
|
||
{
|
||
const slot = self.builder.alloca(obj_ty);
|
||
self.builder.store(slot, method_args.items[0]);
|
||
method_args.items[0] = slot;
|
||
}
|
||
} else {
|
||
// Method expects a value `T` but the receiver is a `*T` (e.g. a
|
||
// `for xs: (*x)` by-ref capture) — deref to pass the value.
|
||
if (!obj_ty.isBuiltin()) {
|
||
const oi = self.module.types.get(obj_ty);
|
||
if (oi == .pointer and oi.pointer.pointee == first_param_ty) {
|
||
method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types.
|
||
pub fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 {
|
||
if (ty.isBuiltin()) {
|
||
// Map builtin types to their names for method resolution (e.g., i64.eq)
|
||
return builtinTypeName(ty);
|
||
}
|
||
var resolved = ty;
|
||
const info = self.module.types.get(resolved);
|
||
if (info == .pointer) {
|
||
resolved = info.pointer.pointee;
|
||
if (resolved.isBuiltin()) return builtinTypeName(resolved);
|
||
}
|
||
const ri = self.module.types.get(resolved);
|
||
return switch (ri) {
|
||
.@"struct" => |s| self.module.types.getString(s.name),
|
||
else => null,
|
||
};
|
||
}
|
||
|
||
pub fn builtinTypeName(ty: TypeId) ?[]const u8 {
|
||
return switch (ty) {
|
||
.i8 => "i8",
|
||
.i16 => "i16",
|
||
.i32 => "i32",
|
||
.i64 => "i64",
|
||
.u8 => "u8",
|
||
.u16 => "u16",
|
||
.u32 => "u32",
|
||
.u64 => "u64",
|
||
.f32 => "f32",
|
||
.f64 => "f64",
|
||
.bool => "bool",
|
||
.string => "string",
|
||
else => null,
|
||
};
|
||
}
|
||
|
||
/// Resolve the type of a named field on a given type.
|
||
pub fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId {
|
||
if (std.mem.eql(u8, field, "len")) return .i64;
|
||
if (std.mem.eql(u8, field, "ptr")) {
|
||
const elem_ty = self.getElementType(ty);
|
||
return self.module.types.manyPtrTo(elem_ty);
|
||
}
|
||
const field_name_id = self.module.types.internString(field);
|
||
// Check union fields + promoted fields
|
||
if (!ty.isBuiltin()) {
|
||
const info = self.module.types.get(ty);
|
||
const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) {
|
||
.@"union" => |u| u.fields,
|
||
.tagged_union => |u| u.fields,
|
||
else => null,
|
||
};
|
||
if (u_fields) |ufields| {
|
||
for (ufields) |f| {
|
||
if (f.name == field_name_id) return f.ty;
|
||
// Check promoted fields from anonymous struct variants
|
||
if (!f.ty.isBuiltin()) {
|
||
const fi = self.module.types.get(f.ty);
|
||
if (fi == .@"struct") {
|
||
for (fi.@"struct".fields) |sf| {
|
||
if (sf.name == field_name_id) return sf.ty;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Check tuple fields
|
||
if (!ty.isBuiltin()) {
|
||
const ti = self.module.types.get(ty);
|
||
if (ti == .tuple) {
|
||
const tuple = ti.tuple;
|
||
// Try named fields
|
||
if (tuple.names) |names| {
|
||
for (names, 0..) |name_id, i| {
|
||
if (name_id == field_name_id) return tuple.fields[i];
|
||
}
|
||
}
|
||
// Try numeric index
|
||
const idx = std.fmt.parseInt(usize, field, 10) catch {
|
||
return .unresolved;
|
||
};
|
||
if (idx < tuple.fields.len) return tuple.fields[idx];
|
||
return .unresolved;
|
||
}
|
||
}
|
||
const struct_fields = self.getStructFields(ty);
|
||
for (struct_fields) |f| {
|
||
if (f.name == field_name_id) return f.ty;
|
||
}
|
||
return .unresolved;
|
||
}
|
||
|
||
pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref {
|
||
// `inline for xs (x)` element capture as the receiver: re-enter with the
|
||
// synthesized `xs[<i>]` as the object, so every pack-element rule below
|
||
// (interface-only constraint check, projection, substitution) sees the
|
||
// canonical `xs[i].<field>` shape.
|
||
if (fa.object.data == .identifier) {
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(fa.object.data.identifier.name)) |binding| {
|
||
if (binding.pack_elem) |elem| {
|
||
var patched = fa.*;
|
||
patched.object = elem;
|
||
return self.lowerFieldAccess(&patched, span);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// `error.X` — an error-tag literal. The `error` keyword in expression
|
||
// position parses as identifier "error" (E0.2), so `error.X` is a
|
||
// field access we intercept here. `error` is reserved, so this is
|
||
// unambiguous (no struct/pack can be named `error`).
|
||
if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) {
|
||
return self.lowerErrorTagLiteral(fa.field, span);
|
||
}
|
||
|
||
// Namespace-alias stripping in value position. The target module's
|
||
// declarations register under their bare names, so `alias.Member`
|
||
// re-enters as `Member` (`r.LIMIT`, and `r.Color` as the receiver of
|
||
// `r.Color.green`); `alias.Type.field` re-enters as `Type.field`.
|
||
if (self.namespaceRootedMember(fa.object)) |inner| {
|
||
const root = fa.object.data.field_access.object.data.identifier.name;
|
||
if (self.namespaceAliasTarget(root, span)) |target| {
|
||
// Resolve the inner name as a TYPE in the target's context
|
||
// (the alias edge authorizes the reach).
|
||
const saved_src = self.current_source_file;
|
||
self.setCurrentSourceFile(target.target_module_path);
|
||
const ty = self.resolveNominalLeaf(inner, false, span);
|
||
self.setCurrentSourceFile(saved_src);
|
||
if (ty != .unresolved and !ty.isBuiltin()) {
|
||
const info = self.module.types.get(ty);
|
||
if (info == .@"enum" or info == .tagged_union) {
|
||
// `alias.Enum.variant` — a typed enum literal.
|
||
const synth = self.alloc.create(Node) catch null;
|
||
if (synth) |n| {
|
||
n.* = .{ .span = span, .data = .{ .enum_literal = .{ .name = fa.field } } };
|
||
const saved_tt = self.target_type;
|
||
self.target_type = ty;
|
||
const ref = self.lowerExpr(n);
|
||
self.target_type = saved_tt;
|
||
return ref;
|
||
}
|
||
}
|
||
}
|
||
// `alias.Type.member` (struct constants etc.) — strip the alias;
|
||
// the type's members register under the bare type name globally.
|
||
const synth = self.alloc.create(Node) catch null;
|
||
if (synth) |n| {
|
||
n.* = .{ .span = fa.object.span, .data = .{ .identifier = .{ .name = inner } } };
|
||
const stripped = ast.FieldAccess{ .object = n, .field = fa.field, .is_optional = fa.is_optional };
|
||
return self.lowerFieldAccess(&stripped, span);
|
||
}
|
||
}
|
||
}
|
||
if (fa.object.data == .identifier) {
|
||
const oname = fa.object.data.identifier.name;
|
||
const shadowed = if (self.scope) |s| s.lookup(oname) != null else false;
|
||
if (!shadowed and !self.program_index.global_names.contains(oname)) {
|
||
if (self.namespaceAliasTarget(oname, span)) |target| {
|
||
const synth = self.alloc.create(Node) catch null;
|
||
if (synth) |n| {
|
||
n.* = .{ .span = span, .data = .{ .identifier = .{ .name = fa.field } } };
|
||
// Lower in the TARGET module's context: the alias edge
|
||
// authorizes the member, so the bare-visibility gate must
|
||
// judge it as the target's own name, not the caller's.
|
||
const saved_src = self.current_source_file;
|
||
self.setCurrentSourceFile(target.target_module_path);
|
||
const ref = self.lowerExpr(n);
|
||
self.setCurrentSourceFile(saved_src);
|
||
return ref;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Bare `Enum.variant` — a qualified enum literal. When the object is a type
|
||
// NAME resolving to an enum / tagged-union (not shadowed by a value binding /
|
||
// global value) and `field` is a PAYLOADLESS variant, construct it like the
|
||
// leading-dot `.variant` in a typed context. Mirrors the `alias.Enum.variant`
|
||
// namespace path above. Restricted to payloadless variants so a payload-
|
||
// carrying `Ev.a(5)` still flows through the call path (which supplies the
|
||
// payload) rather than being hijacked into a zero-arg `.a` here.
|
||
if (fa.object.data == .identifier) {
|
||
const oname = fa.object.data.identifier.name;
|
||
const shadowed = if (self.scope) |s| s.lookup(oname) != null else false;
|
||
if (!shadowed and !self.program_index.global_names.contains(oname)) {
|
||
if (self.module.types.findByName(self.module.types.internString(oname))) |ty| {
|
||
if (!ty.isBuiltin() and self.isPayloadlessVariant(ty, fa.field)) {
|
||
const synth = self.alloc.create(Node) catch null;
|
||
if (synth) |n| {
|
||
n.* = .{ .span = span, .data = .{ .enum_literal = .{ .name = fa.field } } };
|
||
const saved_tt = self.target_type;
|
||
self.target_type = ty;
|
||
const ref = self.lowerExpr(n);
|
||
self.target_type = saved_tt;
|
||
return ref;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pack-arity intercept: `<pack_name>.len` in a pack-fn mono's
|
||
// body resolves to the comptime-known N. The mono doesn't
|
||
// materialise the `[]Any` slice that the inline path used, so
|
||
// `args` isn't in scope as a value.
|
||
if (self.pack_param_count) |ppc| {
|
||
if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) {
|
||
if (ppc.get(fa.object.data.identifier.name)) |n| {
|
||
return self.builder.constInt(@as(i64, @intCast(n)), .i64);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pack value projection: `xs.<m>` where `<m>` is a (zero-arg) method of
|
||
// the pack's constraint protocol projects it over every element →
|
||
// a tuple `(xs[0].<m>(), …, xs[N-1].<m>())`. (`xs.len` handled above.)
|
||
if (self.pack_constraint) |pcon| {
|
||
if (fa.object.data == .identifier) {
|
||
if (pcon.get(fa.object.data.identifier.name)) |proto| {
|
||
if (self.lookupProtocolField(proto, fa.field) != null) {
|
||
return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Interface-only enforcement (Decision): a member access on a
|
||
// constrained pack element `xs[i].<m>` may only name a method of the
|
||
// constraint protocol — not an arbitrary concrete field. Checked here,
|
||
// on the `xs[i]` (index_expr) base, BEFORE substitution erases the
|
||
// "constrained to P" context. Protocol method CALLS go through the call
|
||
// path; a method name passes this check (it's in the protocol).
|
||
if (self.pack_constraint) |pcon| {
|
||
if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) {
|
||
const base_name = fa.object.data.index_expr.object.data.identifier.name;
|
||
if (pcon.get(base_name)) |proto| {
|
||
if (self.lookupProtocolField(proto, fa.field) == null) {
|
||
if (self.diagnostics) |diags| {
|
||
diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto });
|
||
}
|
||
return self.builder.constInt(0, .void);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check for struct constant access: Struct.CONST
|
||
if (fa.object.data == .identifier) {
|
||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field;
|
||
if (self.struct_const_map.get(qualified)) |info| {
|
||
return self.lowerStructConstant(info);
|
||
}
|
||
}
|
||
|
||
// Numeric-limit accessor: `<IntType>.min` / `.max` folds to a comptime
|
||
// const of the queried type (sibling of the identifier-receiver
|
||
// intercepts above). Placed AFTER `Struct.CONST` so a user const named
|
||
// `min`/`max` wins on its own struct; a builtin type name can never
|
||
// name a user struct (reserved), so they never collide.
|
||
if (self.lowerNumericLimit(fa, span)) |ref| return ref;
|
||
|
||
// M1.3 — `obj.class` on any Obj-C-class pointer lowers to
|
||
// `object_getClass(obj)`. Sugar; the receiver is opaque so
|
||
// we don't auto-deref. Returns `Class` (alias for *void;
|
||
// typed Class(T) parameterization is M1.1.b).
|
||
if (std.mem.eql(u8, fa.field, "class")) {
|
||
const expr_ty = self.inferExprType(fa.object);
|
||
if (self.objc().isObjcClassPointer(expr_ty)) {
|
||
const obj_ref = self.lowerExpr(fa.object);
|
||
const ptr_void = self.module.types.ptrTo(.void);
|
||
const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void);
|
||
const args = self.alloc.alloc(Ref, 1) catch unreachable;
|
||
args[0] = obj_ref;
|
||
return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void);
|
||
}
|
||
}
|
||
|
||
// M2.2 — `obj.field` where `field` is declared with `#property`
|
||
// on a runtime Obj-C class lowers as `[obj field]` (the synthesized
|
||
// getter). Receiver stays opaque — no auto-deref.
|
||
if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| {
|
||
return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span);
|
||
}
|
||
|
||
// M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class
|
||
// pointer for a plain instance field (NOT a #property) lowers as
|
||
// `object_getIvar(obj, load(__<Cls>_state_ivar))` + struct_gep on
|
||
// the state struct + load. The receiver is the opaque Obj-C id
|
||
// (matching Apple's `self` semantics); the state lives in the
|
||
// hidden `__sx_state` ivar.
|
||
if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| {
|
||
return self.lowerObjcDefinedStateFieldRead(fa.object, info);
|
||
}
|
||
|
||
// `#get` property accessor: `obj.field` where `field` is a `#get` method
|
||
// dispatches as a no-paren method call (`obj.field()`). Detected via type
|
||
// info only (no lowering) so the receiver is not evaluated twice — the
|
||
// synthesized call re-lowers `fa.object` and handles the receiver
|
||
// address-of + any generic binding itself.
|
||
{
|
||
var recv_ty = self.inferExprType(fa.object);
|
||
if (!recv_ty.isBuiltin()) {
|
||
const di = self.module.types.get(recv_ty);
|
||
if (di == .pointer) recv_ty = di.pointer.pointee;
|
||
}
|
||
if (self.getAccessorFor(recv_ty, fa.field) != null) {
|
||
// For an explicit-deref receiver `(*p).getter`, dispatch on the
|
||
// inner pointer `p` (`p.getter`, auto-deref) — semantically identical
|
||
// and it takes the working receiver path (the synthesized call on a
|
||
// `.deref_expr` receiver otherwise mis-lowers the `*self` address).
|
||
var recv_fa = fa.*;
|
||
if (fa.object.data == .deref_expr) recv_fa.object = fa.object.data.deref_expr.operand;
|
||
const callee_node = Node{ .data = .{ .field_access = recv_fa }, .span = span };
|
||
const syn_call = ast.Call{ .callee = @constCast(&callee_node), .args = &.{} };
|
||
return self.lowerCall(&syn_call);
|
||
}
|
||
}
|
||
|
||
var obj = self.lowerExpr(fa.object);
|
||
var obj_ty = self.inferExprType(fa.object);
|
||
|
||
// Auto-deref: if the object is a pointer to a struct, load through it
|
||
if (!obj_ty.isBuiltin()) {
|
||
const ptr_info = self.module.types.get(obj_ty);
|
||
if (ptr_info == .pointer) {
|
||
const pointee = ptr_info.pointer.pointee;
|
||
obj = self.builder.load(obj, pointee);
|
||
obj_ty = pointee;
|
||
}
|
||
}
|
||
|
||
// Special fields on slices/strings (NOT structs with .len/.ptr fields)
|
||
if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) {
|
||
// Only use length/data_ptr for slice, string, array, vector types
|
||
const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: {
|
||
const info = self.module.types.get(obj_ty);
|
||
break :blk info == .slice or info == .array or info == .vector;
|
||
} else false);
|
||
|
||
if (is_special) {
|
||
if (std.mem.eql(u8, fa.field, "len")) {
|
||
return self.builder.emit(.{ .length = .{ .operand = obj } }, .i64);
|
||
}
|
||
{
|
||
const elem_ty = self.getElementType(obj_ty);
|
||
const mp_ty = self.module.types.manyPtrTo(elem_ty);
|
||
return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Optional chaining: p?.field
|
||
if (fa.is_optional) {
|
||
return self.lowerOptionalChain(obj, fa, span);
|
||
}
|
||
|
||
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
|
||
}
|
||
|
||
/// True when an `.identifier` receiver text resolves to an in-scope VALUE
|
||
/// binding rather than a builtin type. A backtick raw identifier (F0.6) can
|
||
/// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``);
|
||
/// such a value is reachable through the same three sources the ordinary
|
||
/// identifier field-access path consults (see `expr_typer` `.identifier`
|
||
/// arm): lexical `scope`, program `global_names`, and module value
|
||
/// constants `module_const_map`. The numeric-limit intercept must defer to
|
||
/// ordinary field access whenever ANY of the three binds the name, so a
|
||
/// raw value field read is never hijacked into a numeric-limit fold
|
||
/// (locals, globals, and module-consts alike). A single helper used
|
||
/// by both lowering and inference keeps the two resolvers in lockstep
|
||
/// (two-resolver defect class).
|
||
pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool {
|
||
if (self.scope) |scope| {
|
||
if (scope.lookup(name) != null) return true;
|
||
}
|
||
if (self.program_index.global_names.get(name) != null) return true;
|
||
if (self.program_index.module_const_map.get(name) != null) return true;
|
||
return false;
|
||
}
|
||
|
||
/// Numeric-limit accessor intercept (`<Type>.min`/`.max`/`.epsilon`/
|
||
/// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` /
|
||
/// `Struct.CONST` / pack-arity identifier-receiver intercepts in
|
||
/// `lowerFieldAccess`. Folds the limit to a comptime const of the queried
|
||
/// type via the shared `TypeResolver` logic (no second computor) + the
|
||
/// existing `constInt` / `constFloat` const paths:
|
||
/// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`);
|
||
/// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/
|
||
/// `.nan` → `constFloat` (via `floatLimitFor`).
|
||
/// Returns null when the field is not a limit accessor, or the receiver is not
|
||
/// a builtin type (a user struct → ordinary field lowering reports
|
||
/// field-not-found). Two clean diagnostics (then a placeholder, so lowering
|
||
/// finishes and `hasErrors()` aborts the build):
|
||
/// - a FLOAT-only accessor on an integer type (`i32.epsilon`, `u8.inf`);
|
||
/// - any accessor on a builtin NON-numeric receiver
|
||
/// (`bool`/`string`/`void`/`Any`/`noreturn`).
|
||
pub fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref {
|
||
const name = switch (fa.object.data) {
|
||
.identifier => |id| id.name,
|
||
.type_expr => |te| te.name,
|
||
else => return null,
|
||
};
|
||
if (!TypeResolver.isLimitField(fa.field)) return null;
|
||
const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null;
|
||
|
||
// A backtick raw identifier (F0.6) can bind a value whose spelling
|
||
// shadows a builtin type name (`` `f64 := … ``). Field access on that
|
||
// value is an ordinary field read, not a numeric-limit fold — defer to
|
||
// the normal field-access path when the receiver identifier resolves to
|
||
// a value binding through any of scope / globals / module consts
|
||
//. A `.type_expr` receiver is unambiguously a type
|
||
// and can never be value-shadowed.
|
||
if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null;
|
||
|
||
if (TypeResolver.integerLimitFor(name, fa.field)) |value| {
|
||
return self.builder.constInt(value, ty);
|
||
}
|
||
if (TypeResolver.floatLimitFor(name, fa.field)) |value| {
|
||
return self.builder.constFloat(value, ty);
|
||
}
|
||
// The field is a limit accessor, but it does not apply to this type.
|
||
if (self.diagnostics) |d| {
|
||
if (TypeResolver.integerWidthSign(name) != null) {
|
||
// Integer receiver + a float-only accessor.
|
||
d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field });
|
||
} else {
|
||
// Non-numeric builtin receiver (bool/string/void/Any/noreturn).
|
||
d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field });
|
||
}
|
||
}
|
||
return self.emitPlaceholder(fa.field);
|
||
}
|
||
|
||
/// Lower a struct-level constant value (e.g., Phys.GRAVITY).
|
||
pub fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref {
|
||
const val_node = info.value;
|
||
return switch (val_node.data) {
|
||
.int_literal => |lit| blk: {
|
||
if (info.ty) |t| self.checkIntLiteralFits(lit.value, t, val_node.span);
|
||
break :blk self.builder.constInt(lit.value, info.ty orelse .i64);
|
||
},
|
||
.float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64),
|
||
.bool_literal => |lit| self.builder.constBool(lit.value),
|
||
.string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)),
|
||
else => self.lowerExpr(val_node),
|
||
};
|
||
}
|
||
|
||
/// Lower optional chaining: `p?.field` where p is ?T
|
||
/// Produces ?FieldType: some(unwrap(p).field) if p has value, else null
|
||
/// If FieldType is already optional (?U), flattens to ?U (no double wrapping)
|
||
pub fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref {
|
||
const obj_ty = self.inferExprType(fa.object);
|
||
// Get the inner (non-optional) type
|
||
const inner_ty = if (!obj_ty.isBuiltin()) blk: {
|
||
const info = self.module.types.get(obj_ty);
|
||
break :blk if (info == .optional) info.optional.child else obj_ty;
|
||
} else obj_ty;
|
||
|
||
// `#get` accessor through `?.`: if the unwrapped (and pointer-deref'd)
|
||
// receiver has a getter for this field, the some-branch dispatches the
|
||
// getter instead of a struct-field read. A synthetic receiver local (typed
|
||
// `inner_ty`) lets the existing getter intercept in `lowerFieldAccess` do
|
||
// the deref / address-of; we bind it type-only here for the return-type
|
||
// query, then fill its ref in the some-branch (issue 0160).
|
||
var deref_inner = inner_ty;
|
||
if (!deref_inner.isBuiltin() and self.module.types.get(deref_inner) == .pointer)
|
||
deref_inner = self.module.types.get(deref_inner).pointer.pointee;
|
||
const getter_recv: ?[]const u8 = if (self.scope != null and self.getAccessorFor(deref_inner, fa.field) != null) blk: {
|
||
var buf: [40]u8 = undefined;
|
||
const nm = std.fmt.bufPrint(&buf, "$oc_recv_{d}", .{self.block_counter}) catch "$oc_recv";
|
||
self.block_counter += 1;
|
||
const owned = self.alloc.dupe(u8, nm) catch break :blk null;
|
||
// Type-only binding for the return-type query: type it as a POINTER to
|
||
// the struct so inference routes through the (working) pointer-deref
|
||
// getter path for both `?T` and `?*T`. The some-branch re-binds it to
|
||
// the actual unwrapped receiver before lowering.
|
||
self.scope.?.put(owned, .{ .ref = Ref.none, .ty = self.module.types.ptrTo(deref_inner), .is_alloca = false });
|
||
break :blk owned;
|
||
} else null;
|
||
const read_node: ?*Node = if (getter_recv) |nm| blk: {
|
||
const id = self.alloc.create(Node) catch break :blk null;
|
||
id.* = .{ .span = span, .data = .{ .identifier = .{ .name = nm } } };
|
||
const rn = self.alloc.create(Node) catch break :blk null;
|
||
rn.* = .{ .span = span, .data = .{ .field_access = .{ .object = id, .field = fa.field } } };
|
||
break :blk rn;
|
||
} else null;
|
||
|
||
// Warm a generic-instance getter's monomorph BEFORE typing the chain: a
|
||
// cold instance method is absent from `resolveFuncByName`, so `resultType`
|
||
// would resolve the getter to `.unresolved` → a `?unresolved` merge type
|
||
// that panics at LLVM emission. Lowering it now binds its type parameter so
|
||
// both the type query and the some-branch call see the concrete signature.
|
||
if (getter_recv != null) {
|
||
const tn = self.formatTypeName(deref_inner);
|
||
if (self.genericInstanceMethod(tn, fa.field)) |gm| _ = self.ensureGenericInstanceMethodLowered(gm);
|
||
}
|
||
|
||
// Get the field type on the inner type (the getter's return type, if any).
|
||
const field_ty = if (read_node) |rn| self.inferExprType(rn) else self.resolveFieldType(inner_ty, fa.field);
|
||
// If field is already optional, flatten (don't double-wrap)
|
||
const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false;
|
||
const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty);
|
||
|
||
// Check if optional has value
|
||
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool);
|
||
|
||
// Create blocks
|
||
const some_bb = self.freshBlock("chain.some");
|
||
const none_bb = self.freshBlock("chain.none");
|
||
const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty});
|
||
|
||
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
|
||
|
||
// Some: unwrap, access field (already ?FieldType if flattened, else wrap)
|
||
self.builder.switchToBlock(some_bb);
|
||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty);
|
||
const field_val = if (read_node) |rn| blk: {
|
||
// Re-bind the synthetic receiver to the unwrapped value, then dispatch
|
||
// the getter through the normal field-access intercept. A `?*T` unwraps
|
||
// to the pointer receiver directly; a `?T` unwraps to a value that the
|
||
// getter (`self: *T`) needs to address, so materialize it into an alloca
|
||
// and bind it like an ordinary `T` local (is_alloca).
|
||
if (!inner_ty.isBuiltin() and self.module.types.get(inner_ty) == .pointer) {
|
||
self.scope.?.put(getter_recv.?, .{ .ref = unwrapped, .ty = inner_ty, .is_alloca = false });
|
||
} else {
|
||
// Materialize the value and bind the alloca POINTER as a `*T` value
|
||
// (not an is_alloca `T`), so the receiver path is identical to the
|
||
// `?*T` case — a plain pointer receiver the getter intercept derefs.
|
||
const slot = self.builder.alloca(inner_ty);
|
||
self.builder.store(slot, unwrapped);
|
||
self.scope.?.put(getter_recv.?, .{ .ref = slot, .ty = self.module.types.ptrTo(inner_ty), .is_alloca = false });
|
||
}
|
||
break :blk self.lowerExpr(rn);
|
||
} else blk: {
|
||
// Real-field read. For a `?*T` the unwrapped value is the POINTER, so
|
||
// load through it before the struct-field access — `lowerFieldAccessOnType`
|
||
// does not auto-deref, and a `structGet` on the raw pointer would read
|
||
// the pointer bits as the field (silent garbage). A `?T` value optional
|
||
// accesses the unwrapped value directly.
|
||
var fobj = unwrapped;
|
||
var fty = inner_ty;
|
||
if (!fty.isBuiltin() and self.module.types.get(fty) == .pointer) {
|
||
const pointee = self.module.types.get(fty).pointer.pointee;
|
||
fobj = self.builder.load(unwrapped, pointee);
|
||
fty = pointee;
|
||
}
|
||
break :blk self.lowerFieldAccessOnType(fobj, fty, fa.field, span);
|
||
};
|
||
const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty);
|
||
self.builder.br(merge_bb, &.{some_result});
|
||
|
||
// None: produce null optional
|
||
self.builder.switchToBlock(none_bb);
|
||
const none_result = self.builder.constNull(result_ty);
|
||
self.builder.br(merge_bb, &.{none_result});
|
||
|
||
// Merge
|
||
self.builder.switchToBlock(merge_bb);
|
||
return self.builder.blockParam(merge_bb, 0, result_ty);
|
||
}
|
||
|
||
/// Field access on a known type (shared by regular field access and optional chaining)
|
||
/// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour
|
||
/// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any
|
||
/// other field name so the read path (`lowerFieldAccessOnType`) and the
|
||
/// write path (`lowerAssignment`) share one resolver and reject a
|
||
/// non-lane field identically.
|
||
pub fn vectorLaneIndex(field: []const u8) ?u32 {
|
||
if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0;
|
||
if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1;
|
||
if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2;
|
||
if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3;
|
||
return null;
|
||
}
|
||
|
||
/// A `#get` property accessor for `obj_ty.field`, or null. A `#get` method is a
|
||
/// normal method (registered `Type.method`) marked `is_get`; it is reachable via
|
||
/// no-paren field syntax. Handles a generic-struct instance (`List(i64).len`)
|
||
/// and a plain struct (`Foo.bar`). `ty` must be the dereferenced (non-pointer)
|
||
/// receiver type.
|
||
pub fn getAccessorFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const ast.FnDecl {
|
||
if (ty.isBuiltin()) return null;
|
||
// A REAL field of this name wins over a same-name `#get` (a getter must not
|
||
// shadow stored data on the read path). If the struct genuinely declares the
|
||
// field, this is not a property access.
|
||
const field_id = self.module.types.internString(field);
|
||
for (self.getStructFields(ty)) |f| {
|
||
if (f.name == field_id) return null;
|
||
}
|
||
// Generic instance: genericInstanceMethod is keyed by the instance name
|
||
// (e.g. "List(i64)"), which is what formatTypeName produces.
|
||
const tn = self.formatTypeName(ty);
|
||
if (self.genericInstanceMethod(tn, field)) |m| {
|
||
return if (m.fd.is_get) m.fd else null;
|
||
}
|
||
// Plain struct: methods are registered "StructName.method" in fn_ast_map.
|
||
const info = self.module.types.get(ty);
|
||
if (info == .@"struct") {
|
||
const sname = self.module.types.getString(info.@"struct".name);
|
||
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, field }) catch return null;
|
||
if (self.program_index.fn_ast_map.get(q)) |fd| {
|
||
return if (fd.is_get) fd else null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// A `#set` property accessor for `obj_ty.field`, or null — the WRITE
|
||
/// counterpart of `getAccessorFor`. A `#set` is registered/dispatched under its
|
||
/// effective `field$set` name (so a same-name `#get` keeps the plain `field`),
|
||
/// and a REAL field of the same name wins over it (parallels the `#get` rule).
|
||
/// `ty` must be the dereferenced (non-pointer) receiver type.
|
||
/// The return type of a `#get` accessor named `field` on `deref_ty` (a
|
||
/// dereferenced struct type), or null when there is no such getter (or no scope
|
||
/// to resolve through). Resolves the type the SAME way a real read does — via a
|
||
/// synthetic `*deref_ty` receiver local routed through inference — so a generic
|
||
/// instance getter (`List(T).len`) binds its type parameter exactly as the call
|
||
/// path would. Shared by `lowerOptionalChain` and the optional-chain inference
|
||
/// in `expr_typer`, so `obj?.getter` types identically to how it lowers.
|
||
pub fn getterReturnTypeOnDeref(self: *Lowering, deref_ty: TypeId, field: []const u8) ?TypeId {
|
||
if (deref_ty.isBuiltin()) return null;
|
||
if (self.getAccessorFor(deref_ty, field) == null) return null;
|
||
const s = self.scope orelse return null;
|
||
// Warm a generic-instance getter's monomorph so its return type resolves
|
||
// (cold instance methods are absent from `resolveFuncByName` → `.unresolved`).
|
||
if (self.genericInstanceMethod(self.formatTypeName(deref_ty), field)) |gm|
|
||
_ = self.ensureGenericInstanceMethodLowered(gm);
|
||
var buf: [40]u8 = undefined;
|
||
const nm = std.fmt.bufPrint(&buf, "$oc_ty_{d}", .{self.block_counter}) catch "$oc_ty";
|
||
self.block_counter += 1;
|
||
const owned = self.alloc.dupe(u8, nm) catch return null;
|
||
s.put(owned, .{ .ref = Ref.none, .ty = self.module.types.ptrTo(deref_ty), .is_alloca = false });
|
||
const id = self.alloc.create(Node) catch return null;
|
||
id.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = owned } } };
|
||
const rn = self.alloc.create(Node) catch return null;
|
||
rn.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .field_access = .{ .object = id, .field = field } } };
|
||
return self.inferExprType(rn);
|
||
}
|
||
|
||
pub fn getSetterFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const ast.FnDecl {
|
||
if (ty.isBuiltin()) return null;
|
||
// A REAL field of this name wins over a same-name `#set` (a setter must not
|
||
// shadow stored data on the write path).
|
||
const field_id = self.module.types.internString(field);
|
||
for (self.getStructFields(ty)) |f| {
|
||
if (f.name == field_id) return null;
|
||
}
|
||
const eff = std.fmt.allocPrint(self.alloc, "{s}" ++ Lowering.setter_eff_suffix, .{field}) catch return null;
|
||
// Generic instance: keyed by the instance name (e.g. "List(i64)").
|
||
const tn = self.formatTypeName(ty);
|
||
if (self.genericInstanceMethod(tn, eff)) |m| {
|
||
return if (m.fd.is_set) m.fd else null;
|
||
}
|
||
// Plain struct: the setter stub is registered "StructName.field$set".
|
||
const info = self.module.types.get(ty);
|
||
if (info == .@"struct") {
|
||
const sname = self.module.types.getString(info.@"struct".name);
|
||
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, eff }) catch return null;
|
||
if (self.program_index.fn_ast_map.get(q)) |fd| {
|
||
return if (fd.is_set) fd else null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
pub fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
|
||
const field_name_id = self.module.types.internString(field);
|
||
|
||
// Check if it's a union type
|
||
if (!obj_ty.isBuiltin()) {
|
||
const info = self.module.types.get(obj_ty);
|
||
switch (info) {
|
||
.tagged_union => |u| {
|
||
// .tag → extract the enum tag value with the correct tag type
|
||
if (std.mem.eql(u8, field, "tag")) {
|
||
return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type);
|
||
}
|
||
// Tagged union — use enum_payload
|
||
for (u.fields, 0..) |f, i| {
|
||
if (f.name == field_name_id) {
|
||
return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty);
|
||
}
|
||
}
|
||
// Check promoted fields from anonymous struct variants
|
||
for (u.fields) |f| {
|
||
if (!f.ty.isBuiltin()) {
|
||
const field_info = self.module.types.get(f.ty);
|
||
if (field_info == .@"struct") {
|
||
for (field_info.@"struct".fields, 0..) |sf, si| {
|
||
if (sf.name == field_name_id) {
|
||
const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty);
|
||
return self.builder.structGet(reinterpreted, @intCast(si), sf.ty);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
.@"union" => |u| {
|
||
// Untagged union — use union_get to reinterpret bytes
|
||
for (u.fields, 0..) |f, i| {
|
||
if (f.name == field_name_id) {
|
||
return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty);
|
||
}
|
||
}
|
||
// Check promoted fields from anonymous struct variants
|
||
for (u.fields) |f| {
|
||
if (!f.ty.isBuiltin()) {
|
||
const field_info = self.module.types.get(f.ty);
|
||
if (field_info == .@"struct") {
|
||
for (field_info.@"struct".fields, 0..) |sf, si| {
|
||
if (sf.name == field_name_id) {
|
||
const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty);
|
||
return self.builder.structGet(reinterpreted, @intCast(si), sf.ty);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
|
||
// Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) →
|
||
// lane 0/1/2/3. Shares lane-index resolution with the write path
|
||
// (lowerAssignment) via vectorLaneIndex; a non-lane field falls
|
||
// through to the field-not-found error below.
|
||
if (!obj_ty.isBuiltin()) {
|
||
const vinfo = self.module.types.get(obj_ty);
|
||
if (vinfo == .vector) {
|
||
if (Lowering.vectorLaneIndex(field)) |vidx| {
|
||
return self.builder.structGet(obj, vidx, vinfo.vector.element);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Closure field access: .fn_ptr → field 0, .env → field 1
|
||
if (!obj_ty.isBuiltin()) {
|
||
const cinfo = self.module.types.get(obj_ty);
|
||
if (cinfo == .closure) {
|
||
if (std.mem.eql(u8, field, "fn_ptr")) {
|
||
const fn_ptr_ty = self.module.types.ptrTo(.void);
|
||
return self.builder.structGet(obj, 0, fn_ptr_ty);
|
||
} else if (std.mem.eql(u8, field, "env")) {
|
||
const env_ty = self.module.types.ptrTo(.void);
|
||
return self.builder.structGet(obj, 1, env_ty);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Tuple field access: .0, .1, etc. or named fields
|
||
if (!obj_ty.isBuiltin()) {
|
||
const tinfo = self.module.types.get(obj_ty);
|
||
if (tinfo == .tuple) {
|
||
const tuple = tinfo.tuple;
|
||
// Try named fields first
|
||
if (tuple.names) |names| {
|
||
for (names, 0..) |name_id, i| {
|
||
if (name_id == field_name_id) {
|
||
return self.builder.structGet(obj, @intCast(i), tuple.fields[i]);
|
||
}
|
||
}
|
||
}
|
||
// Try numeric index (e.g., "0", "1")
|
||
const idx = std.fmt.parseInt(u32, field, 10) catch {
|
||
return self.emitFieldError(obj_ty, field, span);
|
||
};
|
||
if (idx < tuple.fields.len) {
|
||
return self.builder.structGet(obj, idx, tuple.fields[idx]);
|
||
}
|
||
return self.emitFieldError(obj_ty, field, span);
|
||
}
|
||
}
|
||
|
||
// Resolve struct field index and type
|
||
const struct_fields = self.getStructFields(obj_ty);
|
||
for (struct_fields, 0..) |f, i| {
|
||
if (f.name == field_name_id) {
|
||
return self.builder.structGet(obj, @intCast(i), f.ty);
|
||
}
|
||
}
|
||
|
||
return self.emitFieldError(obj_ty, field, span);
|
||
}
|
||
|
||
pub fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref {
|
||
var target = self.target_type orelse .unresolved;
|
||
|
||
// An OPTIONAL destination types the literal by its CHILD: `.x` flowing
|
||
// into a `?E` slot must produce an `E` for the coercion layer to wrap
|
||
// (`.optional_wrap`). Resolving against the optional itself fell into
|
||
// resolveVariantValue's non-enum fallback — variant 0, mis-typed as
|
||
// the optional (issue 0098).
|
||
while (!target.isBuiltin()) {
|
||
const info = self.module.types.get(target);
|
||
if (info != .optional) break;
|
||
target = info.optional.child;
|
||
}
|
||
|
||
const cs = self.builder.current_span;
|
||
const span = ast.Span{ .start = cs.start, .end = cs.end };
|
||
|
||
// The destination must be a known enum / tagged union that carries the
|
||
// named variant — every other shape used to lower to a silent 0.
|
||
if (target == .unresolved) {
|
||
// Cascade guard: an unresolved destination usually means the slot's
|
||
// TYPE already failed to resolve and was diagnosed (not-visible /
|
||
// ambiguous); a second error on the same line is noise.
|
||
if (self.diagnostics) |d| {
|
||
if (!d.hasErrors()) {
|
||
d.addFmt(.err, span, "enum literal '.{s}' has no destination type to resolve against", .{el.name});
|
||
}
|
||
}
|
||
return self.builder.enumInit(0, Ref.none, target);
|
||
}
|
||
var known_variant = false;
|
||
if (!target.isBuiltin()) {
|
||
const info = self.module.types.get(target);
|
||
const name_id = self.module.types.internString(el.name);
|
||
switch (info) {
|
||
.@"enum" => |e| {
|
||
for (e.variants) |v| {
|
||
if (v == name_id) {
|
||
known_variant = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!known_variant) self.emitBadEnumVariant(target, e, el.name, span);
|
||
},
|
||
.tagged_union => |u| {
|
||
for (u.fields) |f| {
|
||
if (f.name == name_id) {
|
||
known_variant = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!known_variant) self.emitBadVariant(target, u, el.name, span);
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
if (!known_variant) {
|
||
if (self.diagnostics) |d| {
|
||
const builtin_or_non_enum = target.isBuiltin() or switch (self.module.types.get(target)) {
|
||
.@"enum", .tagged_union => false,
|
||
else => true,
|
||
};
|
||
if (builtin_or_non_enum) {
|
||
d.addFmt(.err, span, "enum literal '.{s}' cannot type itself from non-enum destination '{s}'", .{ el.name, self.formatTypeName(target) });
|
||
}
|
||
}
|
||
return self.builder.enumInit(0, Ref.none, target);
|
||
}
|
||
|
||
const tag = self.resolveVariantValue(target, el.name);
|
||
return self.builder.enumInit(tag, Ref.none, target);
|
||
}
|
||
|
||
/// Is `field` a PAYLOADLESS variant of enum/tagged-union `ty`? A plain `.@"enum"`
|
||
/// variant is always payloadless; a `tagged_union` variant is payloadless iff its
|
||
/// payload is `void`. Used by `lowerFieldAccess` to recognise a bare
|
||
/// `Enum.variant` qualified literal (payload-carrying variants stay on the call
|
||
/// path, which supplies the payload). False for any non-enum type / unknown field.
|
||
pub fn isPayloadlessVariant(self: *Lowering, ty: TypeId, field: []const u8) bool {
|
||
return switch (self.module.types.get(ty)) {
|
||
.@"enum" => |e| blk: {
|
||
for (e.variants) |v| if (std.mem.eql(u8, self.module.types.getString(v), field)) break :blk true;
|
||
break :blk false;
|
||
},
|
||
.tagged_union => |u| blk: {
|
||
for (u.fields) |f| if (std.mem.eql(u8, self.module.types.getString(f.name), field)) break :blk (f.ty == .void);
|
||
break :blk false;
|
||
},
|
||
else => false,
|
||
};
|
||
}
|
||
|
||
/// The enum twin of `emitBadVariant`: an unknown variant of a plain enum,
|
||
/// with the legal variants listed.
|
||
pub fn emitBadEnumVariant(
|
||
self: *Lowering,
|
||
enum_ty: TypeId,
|
||
enum_info: types.TypeInfo.EnumInfo,
|
||
variant_name: []const u8,
|
||
span: ast.Span,
|
||
) void {
|
||
const diags = self.diagnostics orelse return;
|
||
const ty_name = self.formatTypeName(enum_ty);
|
||
var list: std.ArrayList(u8) = .empty;
|
||
for (enum_info.variants, 0..) |v, i| {
|
||
if (i > 0) list.appendSlice(self.alloc, ", ") catch return;
|
||
list.appendSlice(self.alloc, self.module.types.getString(v)) catch return;
|
||
}
|
||
diags.addFmt(
|
||
.err,
|
||
span,
|
||
"'{s}' is not a variant of '{s}' (variants are: {s})",
|
||
.{ variant_name, ty_name, list.items },
|
||
);
|
||
}
|
||
|
||
/// Lower an `error.X` tag literal to its global tag id (a `u32`). When the
|
||
/// destination context (`target_type`) is a named error set, the value is
|
||
/// typed as that set and `X`'s membership is validated; otherwise the value
|
||
/// is the raw `u32` global tag id (per the spec's context rule).
|
||
pub fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref {
|
||
const tag_id = self.module.types.internTag(tag_name);
|
||
if (self.target_type) |t| {
|
||
if (!t.isBuiltin()) {
|
||
const info = self.module.types.get(t);
|
||
if (info == .error_set) {
|
||
// The bare-`!` inferred placeholder (reserved name "!") accepts
|
||
// any tag — its members aren't known until the whole-program SCC
|
||
// pass (E1.4) folds in every raised tag. Skip membership for it.
|
||
if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) {
|
||
var in_set = false;
|
||
for (info.error_set.tags) |member| {
|
||
if (member == tag_id) {
|
||
in_set = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!in_set) {
|
||
if (self.diagnostics) |diags| {
|
||
diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) });
|
||
}
|
||
}
|
||
}
|
||
return self.builder.constInt(@as(i64, @intCast(tag_id)), t);
|
||
}
|
||
}
|
||
}
|
||
return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32);
|
||
}
|
||
|
||
/// Lower a tagged enum construction: .Variant.{ field_inits }
|
||
/// The struct literal provides the payload fields; we wrap them in an enum_init.
|
||
pub fn lowerTaggedEnumLiteral(
|
||
self: *Lowering,
|
||
sl: *const ast.StructLiteral,
|
||
variant_name: []const u8,
|
||
union_ty: TypeId,
|
||
union_info: types.TypeInfo.TaggedUnionInfo,
|
||
span: ast.Span,
|
||
) Ref {
|
||
if (self.findTaggedVariant(union_info, variant_name) == null) {
|
||
self.emitBadVariant(union_ty, union_info, variant_name, span);
|
||
return self.builder.enumInit(0, Ref.none, union_ty);
|
||
}
|
||
|
||
const tag = self.resolveVariantValue(union_ty, variant_name);
|
||
const name_id = self.module.types.internString(variant_name);
|
||
|
||
// Find the payload type for this variant
|
||
var payload_ty: TypeId = .void;
|
||
for (union_info.fields) |f| {
|
||
if (f.name == name_id) {
|
||
payload_ty = f.ty;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (payload_ty == .void or sl.field_inits.len == 0) {
|
||
// No payload or no fields — just tag
|
||
return self.builder.enumInit(tag, Ref.none, union_ty);
|
||
}
|
||
|
||
// Lower the payload as a struct init of the payload type
|
||
const saved_tt = self.target_type;
|
||
self.target_type = payload_ty;
|
||
const payload_fields = self.getStructFields(payload_ty);
|
||
|
||
var fields = std.ArrayList(Ref).empty;
|
||
defer fields.deinit(self.alloc);
|
||
|
||
for (sl.field_inits, 0..) |fi, i| {
|
||
if (i < payload_fields.len) {
|
||
const saved_inner = self.target_type;
|
||
self.target_type = payload_fields[i].ty;
|
||
var val = self.lowerExpr(fi.value);
|
||
self.target_type = saved_inner;
|
||
const src_ty = self.inferExprType(fi.value);
|
||
val = self.coerceToType(val, src_ty, payload_fields[i].ty);
|
||
fields.append(self.alloc, val) catch unreachable;
|
||
} else {
|
||
fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable;
|
||
}
|
||
}
|
||
|
||
// Pad missing payload fields with zeroes
|
||
if (fields.items.len < payload_fields.len) {
|
||
for (payload_fields[fields.items.len..]) |sf| {
|
||
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
||
}
|
||
}
|
||
|
||
const payload = self.builder.structInit(fields.items, payload_ty);
|
||
self.target_type = saved_tt;
|
||
|
||
return self.builder.enumInit(tag, payload, union_ty);
|
||
}
|
||
|
||
pub fn findTaggedVariant(
|
||
self: *Lowering,
|
||
union_info: types.TypeInfo.TaggedUnionInfo,
|
||
variant_name: []const u8,
|
||
) ?usize {
|
||
const name_id = self.module.types.internString(variant_name);
|
||
for (union_info.fields, 0..) |f, i| {
|
||
if (f.name == name_id) return i;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
pub fn emitBadVariant(
|
||
self: *Lowering,
|
||
union_ty: TypeId,
|
||
union_info: types.TypeInfo.TaggedUnionInfo,
|
||
variant_name: []const u8,
|
||
span: ast.Span,
|
||
) void {
|
||
const diags = self.diagnostics orelse return;
|
||
const ty_name = self.formatTypeName(union_ty);
|
||
var list: std.ArrayList(u8) = .empty;
|
||
for (union_info.fields, 0..) |f, i| {
|
||
if (i > 0) list.appendSlice(self.alloc, ", ") catch return;
|
||
list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return;
|
||
}
|
||
diags.addFmt(
|
||
.err,
|
||
span,
|
||
"'{s}' is not a variant of '{s}' (variants are: {s})",
|
||
.{ variant_name, ty_name, list.items },
|
||
);
|
||
}
|
||
|
||
/// Resolve a variant name to its runtime value (flags: power-of-2, regular: index).
|
||
pub fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 {
|
||
if (ty.isBuiltin()) return 0;
|
||
const info = self.module.types.get(ty);
|
||
const name_id = self.module.types.internString(variant_name);
|
||
switch (info) {
|
||
.@"enum" => |e| {
|
||
for (e.variants, 0..) |v, i| {
|
||
if (v == name_id) {
|
||
if (e.explicit_values) |vals| {
|
||
if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i])));
|
||
}
|
||
return @intCast(i);
|
||
}
|
||
}
|
||
},
|
||
.tagged_union => |u| {
|
||
for (u.fields, 0..) |f, i| {
|
||
if (f.name == name_id) {
|
||
if (u.explicit_tag_values) |vals| {
|
||
if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i])));
|
||
}
|
||
return @intCast(i);
|
||
}
|
||
}
|
||
},
|
||
else => {},
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/// Resolve a variant name to its tag index within an enum or union type.
|
||
pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 {
|
||
if (ty.isBuiltin()) return 0;
|
||
const info = self.module.types.get(ty);
|
||
const name_id = self.module.types.internString(variant_name);
|
||
switch (info) {
|
||
.tagged_union => |u| {
|
||
for (u.fields, 0..) |f, i| {
|
||
if (f.name == name_id) return @intCast(i);
|
||
}
|
||
},
|
||
.@"enum" => |e| {
|
||
for (e.variants, 0..) |v, i| {
|
||
if (v == name_id) return @intCast(i);
|
||
}
|
||
},
|
||
else => {},
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
pub fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref {
|
||
var elems = std.ArrayList(Ref).empty;
|
||
defer elems.deinit(self.alloc);
|
||
|
||
// Determine element type: explicit type_expr > target_type > inference
|
||
var elem_ty: TypeId = .unresolved;
|
||
var from_target = false;
|
||
var is_vector = false;
|
||
|
||
// First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3])
|
||
if (al.type_expr) |te| {
|
||
const resolved = self.resolveArrayLiteralType(te);
|
||
if (resolved != .unresolved) {
|
||
if (!resolved.isBuiltin()) {
|
||
const info = self.module.types.get(resolved);
|
||
switch (info) {
|
||
.array => |a| {
|
||
elem_ty = a.element;
|
||
from_target = true;
|
||
},
|
||
.vector => |v| {
|
||
elem_ty = v.element;
|
||
from_target = true;
|
||
is_vector = true;
|
||
},
|
||
.slice => |s| {
|
||
elem_ty = s.element;
|
||
from_target = true;
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!from_target) {
|
||
if (self.target_type) |tt| {
|
||
if (!tt.isBuiltin()) {
|
||
const info = self.module.types.get(tt);
|
||
switch (info) {
|
||
.array => |a| {
|
||
elem_ty = a.element;
|
||
from_target = true;
|
||
},
|
||
.slice => |s| {
|
||
elem_ty = s.element;
|
||
from_target = true;
|
||
},
|
||
.vector => |v| {
|
||
elem_ty = v.element;
|
||
from_target = true;
|
||
is_vector = true;
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!from_target and al.elements.len > 0) {
|
||
const inferred = self.inferExprType(al.elements[0]);
|
||
if (inferred != .void) elem_ty = inferred;
|
||
}
|
||
|
||
for (al.elements) |elem| {
|
||
const old_tt = self.target_type;
|
||
self.target_type = elem_ty;
|
||
var val = self.lowerExpr(elem);
|
||
self.target_type = old_tt;
|
||
// A nested `.[...]` element at a slice element type lowers to an
|
||
// aggregate array `[N]U` (lowerArrayLiteral always yields an array
|
||
// value); materialize it into a `[]U` slice so the element is a real
|
||
// {ptr,len} header rather than a raw array the callee would read its
|
||
// header off of. This per-element coercion recurses with
|
||
// the literal nesting, so `[][]T` and deeper coerce at every level.
|
||
if (!elem_ty.isBuiltin()) {
|
||
const ei = self.module.types.get(elem_ty);
|
||
if (ei == .slice) {
|
||
const val_ty = self.builder.getRefType(val);
|
||
if (!val_ty.isBuiltin()) {
|
||
const vi = self.module.types.get(val_ty);
|
||
if (vi == .array and vi.array.element == ei.slice.element) {
|
||
val = self.coerceToType(val, val_ty, elem_ty);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
elems.append(self.alloc, val) catch unreachable;
|
||
}
|
||
|
||
const result_ty = if (is_vector)
|
||
self.module.types.vectorOf(elem_ty, @intCast(al.elements.len))
|
||
else
|
||
self.module.types.arrayOf(elem_ty, @intCast(al.elements.len));
|
||
return self.builder.structInit(elems.items, result_ty);
|
||
}
|
||
|
||
/// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]).
|
||
/// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr.
|
||
pub fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId {
|
||
switch (te.data) {
|
||
.call => |cl| {
|
||
// Vector(3, f32) or Module.Vector(3, f32)
|
||
const callee_name = switch (cl.callee.data) {
|
||
.identifier => |id| id.name,
|
||
.field_access => |fa| fa.field,
|
||
else => return .unresolved,
|
||
};
|
||
if (std.mem.eql(u8, callee_name, "Vector")) {
|
||
if (cl.args.len == 2) {
|
||
const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved;
|
||
const elem = self.resolveTypeWithBindings(cl.args[1]);
|
||
return self.module.types.vectorOf(elem, length);
|
||
}
|
||
}
|
||
// Generic-struct typed-literal head (`Box(i64).[...]`): route
|
||
// through the single layout choke-point (CP-1). A qualified head
|
||
// `a.Box(i64).[...]` selects a's OWN template via the namespace edge
|
||
// (Counter-1: was the global last-wins map); a bare head selects the
|
||
// single bare-VISIBLE author.
|
||
if (headNameOfCallee(cl.callee)) |hn| {
|
||
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) {
|
||
.template => |t| return self.instantiateGenericStruct(&t, cl.args),
|
||
.poisoned => return .unresolved,
|
||
.not_generic => {},
|
||
}
|
||
}
|
||
return .unresolved;
|
||
},
|
||
.parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span),
|
||
.identifier => |id| {
|
||
// E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type
|
||
// name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is
|
||
// not bare-visible (consistent with annotations / 0763); ≥2 direct
|
||
// flat same-name authors are ambiguous (loud diagnostic, consistent
|
||
// with the leaf / 0755); a single source-keyed author resolves to
|
||
// ITS TypeId instead of a global `findByName` first-/last-wins pick.
|
||
switch (self.headTypeGate(id.name, te.span)) {
|
||
.ambiguous, .not_visible => return .unresolved,
|
||
.resolved => |tid| return tid,
|
||
.proceed => {},
|
||
}
|
||
const name_id = self.module.types.internString(id.name);
|
||
return self.module.types.findByName(name_id) orelse .unresolved;
|
||
},
|
||
.type_expr => |inner| {
|
||
if (self.headTypeLeak(inner.name, te.span)) return .unresolved;
|
||
return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
||
},
|
||
.field_access => |fa| {
|
||
// Module.Type — try to resolve the field as a type name
|
||
const name_id = self.module.types.internString(fa.field);
|
||
return self.module.types.findByName(name_id) orelse .unresolved;
|
||
},
|
||
else => return .unresolved,
|
||
}
|
||
}
|
||
|
||
pub fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref {
|
||
// Pack-arg substitution: `args[<int_literal>]` inside a body
|
||
// whose enclosing comptime call bound `args` as a pack name.
|
||
// Lowering the i-th call-site arg directly gives the concrete
|
||
// call-arg type — bypasses the `[]Any` slice boxing that would
|
||
// otherwise lose the type. Non-literal indices fall through to
|
||
// the standard slice indexing path.
|
||
if (self.packArgNodeAt(ie)) |arg_node| {
|
||
return self.lowerExpr(arg_node);
|
||
}
|
||
// Out-of-bounds pack indexing: object IS a pack name + index
|
||
// IS a comptime int literal but exceeds the pack arity. Emit
|
||
// a focused diagnostic so the user gets "pack index 2 out of
|
||
// bounds" instead of the generic "unresolved 'args'" that the
|
||
// fall-through scope-lookup would produce.
|
||
if (self.diagPackIndexOOB(ie)) {
|
||
return self.builder.constInt(0, .i64);
|
||
}
|
||
// Runtime index into a comptime-only pack (Decision 1): a pack has no
|
||
// runtime representation, so the index must be a compile-time constant.
|
||
// A runtime index is a hard error — clearer than the "unresolved
|
||
// '<pack>'" the slice-index fall-through would otherwise produce.
|
||
if (self.pack_param_count) |ppc| {
|
||
if (ie.object.data == .identifier) {
|
||
const pname = ie.object.data.identifier.name;
|
||
if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) {
|
||
if (self.diagnostics) |diags| {
|
||
diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname});
|
||
}
|
||
return self.builder.constInt(0, .i64);
|
||
}
|
||
}
|
||
}
|
||
// Infer element type from the object's slice/array type
|
||
const obj_ty = self.inferExprType(ie.object);
|
||
// Array with addressable storage: GEP the element in place + load,
|
||
// never `index_get` on the loaded array VALUE — that realizes as
|
||
// copy-whole-array-to-temp per read (the general-expression sibling
|
||
// of 0110's `lowerFor` fix), and on a 64K+ array the whole-aggregate
|
||
// load/store ops segfault LLVM's SelectionDAG (issue 0124). The
|
||
// object must not be lowered as a value on this path or the dead
|
||
// whole-array load still reaches the DAG.
|
||
if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array) {
|
||
if (self.getExprAlloca(ie.object)) |storage| {
|
||
const idx = self.lowerExpr(ie.index);
|
||
const elem_ty = self.getElementType(obj_ty);
|
||
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = storage, .rhs = idx } }, self.module.types.ptrTo(elem_ty));
|
||
return self.builder.load(gep, elem_ty);
|
||
}
|
||
}
|
||
const obj = self.lowerExpr(ie.object);
|
||
const idx = self.lowerExpr(ie.index);
|
||
// `*[N]T` receiver auto-derefs (issue 0117): `obj` IS the pointer
|
||
// value — GEP the pointee array and load the element.
|
||
if (self.ptrToArrayElem(obj_ty)) |elem| {
|
||
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, self.module.types.ptrTo(elem));
|
||
return self.builder.load(gep, elem);
|
||
}
|
||
const elem_ty = self.getElementType(obj_ty);
|
||
return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty);
|
||
}
|
||
|
||
pub fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref {
|
||
const obj = self.lowerExpr(se.object);
|
||
const obj_ty = self.inferExprType(se.object);
|
||
var lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .i64);
|
||
if (se.start_exclusive) lo = self.builder.add(lo, self.builder.constInt(1, .i64), .i64);
|
||
// Open-ended `hi`: for a fixed-size array the length is a compile-time
|
||
// constant — emit it directly rather than a runtime `.length` op. Runtime
|
||
// codegen folds the identical constant for an array (`emitLength`), so the
|
||
// result is unchanged; the win is the comptime interp, which can't
|
||
// disambiguate a 2-element array from a `{ptr,len}` fat pointer by Value
|
||
// shape and so would misread a `.length` op on an array.
|
||
var hi = if (se.end) |e|
|
||
self.lowerExpr(e)
|
||
else if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array)
|
||
self.builder.constInt(@intCast(self.module.types.get(obj_ty).array.length), .i64)
|
||
else if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .many_pointer) blk: {
|
||
// A many-pointer `[*]T` carries no length, so an open-ended slice
|
||
// `mp[lo..]` has no upper bound to resolve — a `.length` op on it would
|
||
// yield a garbage length (issue 0159). Require an explicit `hi`.
|
||
if (self.diagnostics) |d|
|
||
d.addFmt(.err, se.object.span, "slicing a many-pointer `[*]T` requires an explicit upper bound (`mp[lo..hi]`) — it has no length", .{});
|
||
break :blk self.builder.constInt(0, .i64);
|
||
} else
|
||
self.builder.emit(.{ .length = .{ .operand = obj } }, .i64);
|
||
if (se.end_inclusive) hi = self.builder.add(hi, self.builder.constInt(1, .i64), .i64);
|
||
// Subslice of string stays string (same {ptr, i64} layout, correct type category)
|
||
if (obj_ty == .string) {
|
||
return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi, .base_ty = obj_ty } }, .string);
|
||
}
|
||
const elem_ty = self.getElementType(obj_ty);
|
||
const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8);
|
||
return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi, .base_ty = obj_ty } }, slice_ty);
|
||
}
|
||
|
||
pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref {
|
||
var elems = std.ArrayList(Ref).empty;
|
||
defer elems.deinit(self.alloc);
|
||
var field_type_ids = std.ArrayList(TypeId).empty;
|
||
defer field_type_ids.deinit(self.alloc);
|
||
var name_ids = std.ArrayList(types.StringId).empty;
|
||
defer name_ids.deinit(self.alloc);
|
||
var has_names = false;
|
||
|
||
// A tuple_init's element values must match its field types exactly
|
||
// (LLVM `insertvalue` does no implicit conversion). When a contextual
|
||
// target tuple of matching arity is in scope (annotation, assignment
|
||
// LHS, call/return slot), its field types drive element lowering so an
|
||
// ambient scalar `target_type` (e.g. the enclosing fn's int return
|
||
// type) can't narrow an element below its field width. Otherwise each
|
||
// element's type is inferred independently.
|
||
// A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields,
|
||
// so element-count ≠ field-count and a contextual target tuple can't be
|
||
// aligned by index — infer field types from the expanded refs instead.
|
||
var has_spread = false;
|
||
for (tl.elements) |elem| {
|
||
if (elem.value.data == .spread_expr) has_spread = true;
|
||
}
|
||
|
||
// Contextual target tuple field types. Without a spread we require
|
||
// exact arity (existing behavior); with a spread we index positionally
|
||
// by output position (so `(..sources)` into a `(VL(T0), …)` field coerces
|
||
// / erases each spliced element to its slot's type).
|
||
var target_fields: ?[]const TypeId = null;
|
||
if (self.target_type) |tt| {
|
||
if (!tt.isBuiltin()) {
|
||
const tinfo = self.module.types.get(tt);
|
||
if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) {
|
||
target_fields = tinfo.tuple.fields;
|
||
}
|
||
}
|
||
}
|
||
|
||
const saved_target = self.target_type;
|
||
var out_idx: usize = 0;
|
||
for (tl.elements) |elem| {
|
||
// Pack-spread element → splice its per-element values as fields.
|
||
if (elem.value.data == .spread_expr) {
|
||
const sp_operand = elem.value.data.spread_expr.operand;
|
||
if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| {
|
||
defer self.alloc.free(refs);
|
||
// Element AST nodes (for protocol-erasure lvalue/name fallback)
|
||
// when the spread is a bare pack name.
|
||
const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null)
|
||
self.pack_arg_nodes.?.get(sp_operand.data.identifier.name)
|
||
else
|
||
null;
|
||
for (refs, 0..) |r, ri| {
|
||
var val = r;
|
||
var vty = self.builder.getRefType(r);
|
||
if (target_fields) |tf| {
|
||
if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) {
|
||
const want = tf[out_idx];
|
||
const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value;
|
||
val = self.coerceOrErase(r, vty, want, node);
|
||
vty = want;
|
||
}
|
||
}
|
||
elems.append(self.alloc, val) catch unreachable;
|
||
field_type_ids.append(self.alloc, vty) catch unreachable;
|
||
name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable;
|
||
out_idx += 1;
|
||
}
|
||
continue;
|
||
}
|
||
// Not a pack spread (e.g. tuple-value spread) — not yet handled.
|
||
_ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic
|
||
continue;
|
||
}
|
||
const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value);
|
||
self.target_type = field_ty;
|
||
var val = self.lowerExpr(elem.value);
|
||
self.target_type = saved_target;
|
||
const val_ty = self.builder.getRefType(val);
|
||
if (val_ty != field_ty and val_ty != .void) {
|
||
val = self.coerceToType(val, val_ty, field_ty);
|
||
}
|
||
elems.append(self.alloc, val) catch unreachable;
|
||
field_type_ids.append(self.alloc, field_ty) catch unreachable;
|
||
if (elem.name) |name| {
|
||
name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable;
|
||
has_names = true;
|
||
} else {
|
||
name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable;
|
||
}
|
||
out_idx += 1;
|
||
}
|
||
|
||
// Reuse the contextual target tuple type when it drove lowering so the
|
||
// value's type identity (incl. field names) matches the destination
|
||
// slot; otherwise build the tuple type from the inferred fields.
|
||
const tuple_ty = if (target_fields != null and self.target_type != null)
|
||
self.target_type.?
|
||
else
|
||
self.module.types.intern(.{ .tuple = .{
|
||
.fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable,
|
||
.names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null,
|
||
} });
|
||
|
||
const owned = self.alloc.dupe(Ref, elems.items) catch unreachable;
|
||
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty);
|
||
}
|
||
|
||
pub fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref {
|
||
const ptr = self.lowerExpr(de.operand);
|
||
// Resolve pointee type from the pointer type.
|
||
const ptr_ty = self.inferExprType(de.operand);
|
||
if (!ptr_ty.isBuiltin()) {
|
||
const info = self.module.types.get(ptr_ty);
|
||
if (info == .pointer) {
|
||
return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee);
|
||
}
|
||
}
|
||
// Operand isn't a pointer — `.*` is invalid. Diagnose here instead of
|
||
// emitting a `.deref` with an `.unresolved` result type, which would
|
||
// otherwise slip through to emit_llvm's "unresolved type reached LLVM
|
||
// emission" panic with no source location.
|
||
if (self.diagnostics) |d| {
|
||
d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)});
|
||
}
|
||
return ptr;
|
||
}
|
||
|
||
pub fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref {
|
||
const val = self.lowerExpr(fu.operand);
|
||
const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand));
|
||
return self.builder.optionalUnwrap(val, inner_ty);
|
||
}
|
||
|
||
pub fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref {
|
||
const lhs = self.lowerExpr(nc.lhs);
|
||
const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs));
|
||
|
||
// Short-circuit: only evaluate RHS if LHS is null.
|
||
// IMPORTANT: optional_unwrap must be in the "has value" branch,
|
||
// not before the condBr — the interpreter errors on unwrapping null.
|
||
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool);
|
||
|
||
const then_bb = self.freshBlock("nc.has");
|
||
const rhs_bb = self.freshBlock("nc.rhs");
|
||
const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty});
|
||
|
||
// If has value, go to then_bb to unwrap; else go to rhs_bb
|
||
self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{});
|
||
|
||
// Then block: unwrap LHS and branch to merge
|
||
self.builder.switchToBlock(then_bb);
|
||
const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty);
|
||
self.builder.br(merge_bb, &.{unwrapped});
|
||
|
||
// RHS block: evaluate fallback and branch to merge
|
||
self.builder.switchToBlock(rhs_bb);
|
||
var rhs = self.lowerExpr(nc.rhs);
|
||
const rhs_ty = self.builder.getRefType(rhs);
|
||
if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) {
|
||
rhs = self.coerceToType(rhs, rhs_ty, inner_ty);
|
||
}
|
||
self.builder.br(merge_bb, &.{rhs});
|
||
|
||
// Continue at merge
|
||
self.builder.switchToBlock(merge_bb);
|
||
return self.builder.blockParam(merge_bb, 0, inner_ty);
|
||
}
|
||
|
||
pub fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId {
|
||
if (!ty.isBuiltin()) {
|
||
const info = self.module.types.get(ty);
|
||
if (info == .optional) return info.optional.child;
|
||
}
|
||
return .unresolved;
|
||
}
|
||
|
||
// ── Core expression dispatch ───────────────────────────────────
|
||
|
||
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 .i64;
|
||
} else .i64;
|
||
self.checkIntLiteralFits(lit.value, ty, node.span);
|
||
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| {
|
||
// `inline for xs (x)` element capture — lower the
|
||
// synthesized `xs[<i>]` it aliases.
|
||
if (binding.pack_elem) |elem| break :blk self.lowerExpr(elem);
|
||
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, .i64),
|
||
.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) — source-aware (issue 0115):
|
||
// the global registry is last-wins across modules, so select the
|
||
// AUTHOR first and emit ITS global, never an unrelated module's
|
||
// same-named one.
|
||
if (self.program_index.global_names.get(id.name)) |gi| {
|
||
switch (self.selectGlobalAuthor(id.name)) {
|
||
.resolved => |g| break :blk self.builder.emit(.{ .global_get = g.id }, g.ty),
|
||
.not_a_global => {},
|
||
.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);
|
||
},
|
||
.not_visible => {
|
||
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);
|
||
},
|
||
.untracked => break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty),
|
||
}
|
||
}
|
||
// Check module-level value constants (e.g. AF_INET :i32: 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, 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),
|
||
// Own const author with no materialized value (unsupported
|
||
// shape, e.g. an array const) — fall through; the tail of
|
||
// identifier lowering diagnoses it as unresolved.
|
||
.own_opaque => {},
|
||
.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;
|
||
// An own fn whose name a flat-merge collision dropped from the
|
||
// global decl list (first-wins) has no `fn_ast_map` entry but IS
|
||
// a raw-facts author — the author selection inside this arm
|
||
// serves it, so admit it through the gate.
|
||
const fn_author_only = !self.program_index.fn_ast_map.contains(eff_fn_name) and
|
||
std.mem.eql(u8, eff_fn_name, id.name) and
|
||
(if (self.scope) |scope| scope.lookup(id.name) == null else true) and
|
||
self.current_source_file != null and
|
||
self.selectPlainCallableAuthor(id.name, self.current_source_file.?) == .func;
|
||
if (self.program_index.fn_ast_map.contains(eff_fn_name) or fn_author_only) {
|
||
// 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: a bare function name in a `Type` (`.type_value`)
|
||
// slot is its FUNCTION TYPE — `const_type(() -> R)` — so it prints
|
||
// / reflects as the real function type, not a func-ref. For a
|
||
// genuine `Any` param the old behavior is kept (a formatted
|
||
// type-name string boxed as Any).
|
||
if (self.target_type == .any or self.target_type == .type_value) {
|
||
const fd_any: ?*const ast.FnDecl = self.program_index.fn_ast_map.get(eff_fn_name) orelse fd_blk: {
|
||
switch (self.selectPlainCallableAuthor(id.name, self.current_source_file.?)) {
|
||
.func => |sf| break :fd_blk sf.decl,
|
||
else => break :fd_blk null,
|
||
}
|
||
};
|
||
if (fd_any) |fd| {
|
||
if (self.target_type == .type_value) {
|
||
var param_ids = std.ArrayList(TypeId).empty;
|
||
defer param_ids.deinit(self.alloc);
|
||
for (fd.params) |p| param_ids.append(self.alloc, self.resolveParamType(&p)) catch {};
|
||
const fn_tid = self.module.types.functionType(param_ids.items, self.resolveReturnType(fd));
|
||
break :blk self.builder.constType(fn_tid);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
// 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
|
||
// abi(.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) "abi(.c)" else "default sx convention";
|
||
const have_cc = if (func_cc == .c) "abi(.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 }, .i64);
|
||
}
|
||
}
|
||
// 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.ptrToArrayElem(obj_ty) orelse 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);
|
||
}
|
||
// A non-storage value binding — a scalar `::` constant
|
||
// folded to its value (`is_alloca == false`, not a
|
||
// ref-capture pointer or pack-element alias). It has NO
|
||
// address: array/struct consts get real storage (reached
|
||
// via `resolveGlobalRef` below), but a scalar const does
|
||
// not. Taking `@const` would otherwise fall through to the
|
||
// generic `addr_of` arm and reinterpret the folded value
|
||
// as a pointer — `inttoptr (i64 <value> to ptr)`, a wild
|
||
// pointer that segfaults on deref and emits invalid stores
|
||
// for asm `-> @const` (issue 0138). Diagnose loudly.
|
||
if (!binding.is_ref_capture and binding.pack_elem == null) {
|
||
if (self.diagnostics) |d|
|
||
d.addFmt(.err, node.span, "cannot take the address of constant '{s}' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)", .{id_name});
|
||
break :blk self.emitPlaceholder("addr_of_const");
|
||
}
|
||
}
|
||
}
|
||
// address_of(global) → emit global_addr (pointer to global, not load)
|
||
if (self.resolveGlobalRef(id_name, node.span)) |gi| {
|
||
const ptr_ty = self.module.types.ptrTo(gi.ty);
|
||
break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty);
|
||
}
|
||
// A module-scope scalar `::` constant (not in lexical `scope`, and
|
||
// not a storage-backed array/struct const — those resolve above).
|
||
// Same defect as the local case: without storage, `@FORTY` would
|
||
// become `inttoptr (i64 <value> to ptr)`. Diagnose (issue 0138).
|
||
if (self.program_index.module_const_map.get(id_name) != null) {
|
||
if (self.diagnostics) |d|
|
||
d.addFmt(.err, node.span, "cannot take the address of constant '{s}' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)", .{id_name});
|
||
break :blk self.emitPlaceholder("addr_of_const");
|
||
}
|
||
}
|
||
// Fold a negated integer literal into one constant: `-128` must
|
||
// range-check as -128, not as an out-of-range +128 intermediate.
|
||
if (uop.op == .negate and uop.operand.data == .int_literal) {
|
||
const lit = uop.operand.data.int_literal;
|
||
const v = -%lit.value;
|
||
if (self.target_type) |tt| {
|
||
if (tt == .f32 or tt == .f64) {
|
||
break :blk self.builder.constFloat(@floatFromInt(v), tt);
|
||
}
|
||
}
|
||
const nty = if (self.target_type) |tt| (if (self.isIntEx(tt)) tt else TypeId.i64) else TypeId.i64;
|
||
self.checkIntLiteralFits(v, nty, node.span);
|
||
break :blk self.builder.constInt(v, nty);
|
||
}
|
||
// An explicit `xx` cast requests the conversion, truncation
|
||
// included — literal operands skip the fits-check.
|
||
const saved_fit = self.suppress_int_fit_check;
|
||
if (uop.op == .xx) self.suppress_int_fit_check = true;
|
||
const operand = self.lowerExpr(uop.operand);
|
||
self.suppress_int_fit_check = saved_fit;
|
||
break :blk switch (uop.op) {
|
||
.negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
||
// `!` is LOGICAL not. Only a real bool may go through the
|
||
// bitwise `bool_not` (i1); an integer-backed operand — an
|
||
// error binding (u32 tag), a plain integer — lowers as the
|
||
// truthiness complement `operand == 0`: a bitwise not of a
|
||
// nonzero tag stays nonzero, so `if !e` held even on a set
|
||
// error (issue 0129). Anything else is diagnosed.
|
||
.not => blk2: {
|
||
const oty = self.inferExprType(uop.operand);
|
||
if (oty == .bool) {
|
||
break :blk2 self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool);
|
||
}
|
||
const int_like = self.isIntEx(oty) or
|
||
(!oty.isBuiltin() and self.module.types.get(oty) == .error_set);
|
||
if (int_like) {
|
||
const zero = self.builder.constInt(0, oty);
|
||
break :blk2 self.builder.emit(.{ .cmp_eq = .{ .lhs = operand, .rhs = zero } }, .bool);
|
||
}
|
||
if (self.diagnostics) |d| {
|
||
d.addFmt(.err, node.span, "'!' needs a bool, integer, or error operand; got '{s}'", .{self.formatTypeName(oty)});
|
||
}
|
||
break :blk2 self.builder.constBool(false);
|
||
},
|
||
.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(node.span),
|
||
.continue_expr => self.lowerContinue(node.span),
|
||
.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. i2, 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);
|
||
},
|
||
|
||
// Compound type literals (`*T`, `[]T`, `[*]T`, `?T`, `[N]T`, fn types)
|
||
// in expression position are first-class `Type` values, exactly like
|
||
// the named form above (`t : Type = *i64;` ↔ `t : Type = f64;`). Also
|
||
// the path a static `cast(*i64) v` type argument takes — call args are
|
||
// lowered before the cast handler inspects the AST (issue 0118).
|
||
.pointer_type_expr,
|
||
.many_pointer_type_expr,
|
||
.slice_type_expr,
|
||
.optional_type_expr,
|
||
.array_type_expr,
|
||
.function_type_expr,
|
||
=> blk: {
|
||
const ty = self.resolveTypeWithBindings(node);
|
||
// The resolver diagnosed any unresolved leaf; don't mint a Type
|
||
// value around the failure sentinel.
|
||
if (ty == .unresolved) break :blk self.emitError("unknown_expr", node.span);
|
||
break :blk self.builder.constType(ty);
|
||
},
|
||
|
||
.try_expr => |te| self.lowerTry(te.operand, node.span),
|
||
.catch_expr => |ce| self.lowerCatch(&ce, node.span),
|
||
.caller_location => self.lowerCallerLocation(node),
|
||
.asm_expr => |ae| self.lowerAsmExpr(&ae, node.span),
|
||
else => self.emitError("unknown_expr", node.span),
|
||
};
|
||
}
|
||
|
||
/// The single register a constraint pins, or null for a register-class /
|
||
/// memory constraint. Strips a leading `=`/`+` (output / read-write marker),
|
||
/// then returns the `{reg}` body. `"={eax}"` → `eax`, `"+{rax}"` → `rax`,
|
||
/// `"{rdi}"` → `rdi`; `"=r"` / `"r"` / `"=m"` → null.
|
||
fn pinnedRegister(constraint: []const u8) ?[]const u8 {
|
||
var c = constraint;
|
||
if (c.len > 0 and (c[0] == '=' or c[0] == '+')) c = c[1..];
|
||
if (c.len >= 2 and c[0] == '{' and c[c.len - 1] == '}') return c[1 .. c.len - 1];
|
||
return null;
|
||
}
|
||
|
||
/// The asm expression's result type from its `out_value` operands (design
|
||
/// §II.5): 0 → `void`; 1 → that operand's type; N → a tuple `(T1,…,Tn)`, named
|
||
/// by each operand's effective name (explicit `[name]` else the `{reg}` pin;
|
||
/// `.empty` for an anonymous field). Returns `.unresolved` if any output type is
|
||
/// unresolvable (the resolver already diagnosed). Shared by `lowerAsmExpr` and
|
||
/// `ExprTyper.inferType` so a `return asm`, a `:=` binding, and a `q, r := asm`
|
||
/// destructure all agree on the type.
|
||
pub fn asmResultType(self: *Lowering, ae: *const ast.AsmExpr) TypeId {
|
||
var fields = std.ArrayList(TypeId).empty;
|
||
defer fields.deinit(self.alloc);
|
||
var names = std.ArrayList(types.StringId).empty;
|
||
defer names.deinit(self.alloc);
|
||
var has_names = false;
|
||
for (ae.operands) |op| {
|
||
if (op.role != .out_value) continue;
|
||
const fty = self.resolveTypeWithBindings(op.payload);
|
||
if (fty == .unresolved) return .unresolved;
|
||
fields.append(self.alloc, fty) catch unreachable;
|
||
const eff = op.name orelse (pinnedRegister(op.constraint) orelse "");
|
||
if (eff.len != 0) has_names = true;
|
||
names.append(self.alloc, if (eff.len == 0) types.StringId.empty else self.module.types.internString(eff)) catch unreachable;
|
||
}
|
||
if (fields.items.len == 0) return .void;
|
||
if (fields.items.len == 1) return fields.items[0];
|
||
return self.module.types.intern(.{ .tuple = .{
|
||
.fields = self.alloc.dupe(TypeId, fields.items) catch unreachable,
|
||
.names = if (has_names) self.alloc.dupe(types.StringId, names.items) catch unreachable else null,
|
||
} });
|
||
}
|
||
|
||
/// Inline assembly lowering. Phase B (partial): validate the asm shape in the
|
||
/// compile path with specific named diagnostics, THEN bail on the not-yet-
|
||
/// implemented codegen so the user sees the real problem first (the IR op +
|
||
/// LLVM emit land in Phases C–E; result-type derivation + the auto-naming rule
|
||
/// move to the expression typer once lowering produces a real value). Always
|
||
/// returns a placeholder Ref so `hasErrors()` aborts the build on whichever
|
||
/// diagnostic fired (CLAUDE.md no-silent-arm).
|
||
pub fn lowerAsmExpr(self: *Lowering, ae: *const ast.AsmExpr, span: ast.Span) Ref {
|
||
const diags = self.diagnostics orelse return self.emitPlaceholder("inline_asm");
|
||
|
||
// (1) The template must be a compile-time-known string (a `"..."` literal or
|
||
// a `#string` heredoc), not a runtime expression.
|
||
const template_is_string = switch (ae.template.data) {
|
||
.string_literal => true,
|
||
else => false,
|
||
};
|
||
if (!template_is_string) {
|
||
diags.addFmt(.err, ae.template.span, "asm template must be a compile-time-known string", .{});
|
||
return self.emitPlaceholder("inline_asm");
|
||
}
|
||
|
||
// (2) Operand-name validation (design §II.5 auto-naming rule). For each
|
||
// explicit `[name]`:
|
||
// - reject the ECHO form `[eax] "={eax}"` — a label identical to the
|
||
// register its own constraint pins carries no information (the operand
|
||
// is already auto-named after that register); and
|
||
// - reject DUPLICATE names — `%[name]` / the result field would be
|
||
// ambiguous.
|
||
for (ae.operands, 0..) |op, i| {
|
||
const name = op.name orelse continue;
|
||
if (pinnedRegister(op.constraint)) |reg| {
|
||
if (std.mem.eql(u8, name, reg)) {
|
||
diags.addFmt(.err, span, "redundant asm operand name `{s}` — it already names the pinned register; drop the `[{s}]`", .{ name, name });
|
||
return self.emitPlaceholder("inline_asm");
|
||
}
|
||
}
|
||
for (ae.operands[0..i]) |prev| {
|
||
const pname = prev.name orelse continue;
|
||
if (std.mem.eql(u8, name, pname)) {
|
||
diags.addFmt(.err, span, "duplicate asm operand name `{s}`", .{name});
|
||
return self.emitPlaceholder("inline_asm");
|
||
}
|
||
}
|
||
}
|
||
|
||
// (3) An asm with no value outputs yields no result, so it must be
|
||
// `volatile` — otherwise its effects could be deleted. Mirrors Zig's rule.
|
||
var n_outputs: usize = 0;
|
||
for (ae.operands) |op| {
|
||
if (op.role == .out_value) n_outputs += 1;
|
||
}
|
||
if (n_outputs == 0 and !ae.is_volatile) {
|
||
diags.addFmt(.err, span, "asm expression with no outputs must be marked `volatile`", .{});
|
||
return self.emitPlaceholder("inline_asm");
|
||
}
|
||
|
||
// (4) Every `%[name]` in the template must name an operand (effective name:
|
||
// explicit `[name]` or auto-derived register). Caught here so emit's
|
||
// template rewriter never sees an unknown reference. §II.6.
|
||
{
|
||
const tmpl = ae.template.data.string_literal.raw;
|
||
var i: usize = 0;
|
||
while (i < tmpl.len) : (i += 1) {
|
||
if (tmpl[i] != '%' or i + 1 >= tmpl.len) continue;
|
||
const nxt = tmpl[i + 1];
|
||
if (nxt == '%' or nxt == '=') {
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if (nxt != '[') continue;
|
||
const close = std.mem.indexOfScalarPos(u8, tmpl, i + 2, ']') orelse {
|
||
diags.addFmt(.err, span, "unterminated `%[` in asm template", .{});
|
||
return self.emitPlaceholder("inline_asm");
|
||
};
|
||
var ref_name = tmpl[i + 2 .. close];
|
||
if (std.mem.indexOfScalar(u8, ref_name, ':')) |colon| ref_name = ref_name[0..colon];
|
||
var found = false;
|
||
for (ae.operands) |op| {
|
||
const eff = op.name orelse (pinnedRegister(op.constraint) orelse "");
|
||
if (eff.len != 0 and std.mem.eql(u8, eff, ref_name)) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
diags.addFmt(.err, span, "asm template references `%[{s}]` but no operand is named `{s}`", .{ ref_name, ref_name });
|
||
return self.emitPlaceholder("inline_asm");
|
||
}
|
||
i = close;
|
||
}
|
||
}
|
||
|
||
// ── Build the IR op. Result type from the out_value operands (0→void,
|
||
// 1→T, N→named tuple). N outputs → LLVM returns a struct {T1,…,Tn}, which
|
||
// is exactly sx's tuple representation, so emit needs no special case. ──
|
||
const result_ty = self.asmResultType(ae);
|
||
if (result_ty == .unresolved) return self.emitPlaceholder("inline_asm");
|
||
|
||
// IR operands, in source order (= `%N` index space + LLVM operand order).
|
||
const ir_ops = self.alloc.alloc(inst_mod.InlineAsm.AsmOperand, ae.operands.len) catch unreachable;
|
||
for (ae.operands, 0..) |op, i| {
|
||
// Effective name (design §II.5): explicit `[name]`, else auto-derived
|
||
// from a `{reg}` pin, else anonymous (`.empty`).
|
||
const eff_name: []const u8 = op.name orelse (pinnedRegister(op.constraint) orelse "");
|
||
var operand_ref: Ref = Ref.none;
|
||
var out_ty: TypeId = .void;
|
||
switch (op.role) {
|
||
// Inputs (incl. symbol operands `"s"` — a function/global whose
|
||
// mangled name the template emits, e.g. a direct `bl %[fn]`). A
|
||
// symbol RHS (a function name) lowers to its address (`ptr @fn`);
|
||
// emit passes it with its own type so the backend prints the symbol.
|
||
.input => operand_ref = self.lowerExpr(op.payload),
|
||
.out_value => out_ty = self.resolveTypeWithBindings(op.payload),
|
||
.out_place => {
|
||
// Read-write (`+`) outputs tie an input to the output and seed
|
||
// it with the place's loaded value; indirect-memory (`=*m`)
|
||
// outputs pass the place address as a pointer arg and the asm
|
||
// writes through it — both handled in `emitInlineAsm`.
|
||
// `@place` lowers to its address (a pointer); the asm result is
|
||
// stored through it. The stored type is the pointee.
|
||
operand_ref = self.lowerExpr(op.payload);
|
||
const pty = self.inferExprType(op.payload);
|
||
out_ty = if (!pty.isBuiltin()) blk: {
|
||
const info = self.module.types.get(pty);
|
||
break :blk if (info == .pointer) info.pointer.pointee else .unresolved;
|
||
} else .unresolved;
|
||
if (out_ty == .unresolved) {
|
||
diags.addFmt(.err, span, "asm `-> @place` output target must be an addressable place", .{});
|
||
return self.emitPlaceholder("inline_asm");
|
||
}
|
||
},
|
||
}
|
||
ir_ops[i] = .{
|
||
.role = switch (op.role) {
|
||
.out_value => .out_value,
|
||
.out_place => .out_place,
|
||
.input => .input,
|
||
},
|
||
.name = if (eff_name.len == 0) types.StringId.empty else self.module.types.internString(eff_name),
|
||
.constraint = self.module.types.internString(op.constraint),
|
||
.operand = operand_ref,
|
||
.out_ty = out_ty,
|
||
};
|
||
}
|
||
|
||
const ir_clobbers = self.alloc.alloc(types.StringId, ae.clobbers.len) catch unreachable;
|
||
for (ae.clobbers, 0..) |cl, i| {
|
||
ir_clobbers[i] = self.module.types.internString(cl);
|
||
}
|
||
|
||
// Template text RAW — no sx escape processing (matches `#string` literal
|
||
// bytes; the `%[name]`/`%%`/`$` rewrite happens at emit). §II.11.
|
||
const template_text = ae.template.data.string_literal.raw;
|
||
|
||
return self.builder.emit(.{ .inline_asm = .{
|
||
.template = self.module.types.internString(template_text),
|
||
.operands = ir_ops,
|
||
.clobbers = ir_clobbers,
|
||
.has_side_effects = ae.is_volatile,
|
||
} }, result_ty);
|
||
}
|
||
|
||
/// 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 (`i64`, `*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 == i64` 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
|
||
// `.i64`) 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 } }, .i64);
|
||
const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .i64);
|
||
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 (`i64 * 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.
|
||
// `i64 + string`, `i64 < string`, `i64 & 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");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Comparison operand promotion. Arithmetic arms below carry the promoted
|
||
// common type `ty` on the result op, so the LLVM emitter re-matches the
|
||
// operands against it (`matchBinOpTypes`). Comparisons carry `.bool`
|
||
// instead, so `emitCmp`/`emitCmpOrdered` only see the raw operand LLVM
|
||
// types — and those only reconcile int↔int width (SExt/ZExt). A mixed
|
||
// int-vs-float compare (`xx i < t`, i:i32 t:f32) or a two-float-width
|
||
// compare (`f64 >= f32`) reaches the emitter with mismatched operands and
|
||
// fails LLVM verification (issue 0146). Coerce each operand up to the
|
||
// promoted common type HERE — `coerceToType` emits the SIToFP / FPExt /
|
||
// width-ext — so the operands are already type-equal when the cmp is built.
|
||
// Restricted to float `ty`: an int↔int compare is handled by the emitter,
|
||
// and a non-numeric `ty` (struct/string/enum) has its own cmp path.
|
||
switch (bop.op) {
|
||
.eq, .neq, .lt, .lte, .gt, .gte => {
|
||
if (Lowering.isFloat(ty)) {
|
||
const lhs_ir = self.builder.getRefType(lhs);
|
||
if (lhs_ir != ty and (Lowering.isFloat(lhs_ir) or self.isIntEx(lhs_ir))) {
|
||
lhs = self.coerceToType(lhs, lhs_ir, ty);
|
||
}
|
||
const rhs_ir = self.builder.getRefType(rhs);
|
||
if (rhs_ir != ty and (Lowering.isFloat(rhs_ir) or self.isIntEx(rhs_ir))) {
|
||
rhs = self.coerceToType(rhs, rhs_ir, ty);
|
||
}
|
||
}
|
||
},
|
||
else => {},
|
||
}
|
||
|
||
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, .i64),
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ── Chained comparison ──────────────────────────────────────────
|
||
|
||
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),
|
||
};
|
||
}
|