Makes the F0.4 fixes exhaustive across every resolution / nesting path.
0083 — named-const array dimension, stateless paths. Attempt 1 fixed the
stateful resolver (direct local decls, struct fields, params, returns) but the
binding-free registration-time resolver (`type_bridge`, used for type aliases
`Arr :: [N]T` and inline union/enum field types) still resolved a named dim with
a silent `else 0`, so `Arr :: [N]s64; a : Arr` and `union { a: [N]s64 }` were
still miscompiled (garbage / bus error). Thread the module-global const table
(`ProgramIndex.module_const_map`) into `type_bridge` alongside the alias map, so
`StatelessInner.resolveArrayLen` resolves a named module-const dim to the same
length everywhere. The remaining unresolvable case (a computed/comptime dim on
the binding-free path, which the stateful path hard-errors) now bails LOUDLY
instead of fabricating a 0 length.
0085 — nested slice-literal elements. `lowerArrayLiteral` lowered each element
with the element type as target but appended the raw value. A nested `.[...]`
element at a slice element type (`[][]s64`) still lowers to an aggregate array
`[N]T`, so the outer aggregate held raw arrays where slice {ptr,len} headers
were expected — indexing the inner slice read a garbage pointer and segfaulted.
After lowering each element, coerce a same-element array to the slice element
type via the existing `array_to_slice` op. The coercion recurses with the
nesting, so `[][]T` and deeper materialize at every level — local-bound AND
direct-call-argument forms.
Regressions (fail-before/pass-after demonstrated on the pre-fix compiler):
examples/0140-types-named-const-array-dim.sx — extended with type-alias,
nested [N][M]T, and union-field named dims (s64 / string / struct elems)
examples/0142-types-nested-slice-literal-elements.sx — [][]s64 + [][]string,
local-bound vs direct-arg
src/ir/type_bridge.test.zig — named-const dim resolves to literal length
Gate: zig build, zig build test, bash tests/run_examples.sh (388 passed).
Issues 0083 and 0085 marked RESOLVED.
438 lines
23 KiB
Zig
438 lines
23 KiB
Zig
const std = @import("std");
|
|
const ast = @import("../ast.zig");
|
|
const types = @import("types.zig");
|
|
const type_bridge = @import("type_bridge.zig");
|
|
const lower = @import("lower.zig");
|
|
const inst = @import("inst.zig");
|
|
|
|
const Node = ast.Node;
|
|
const TypeId = types.TypeId;
|
|
const FuncId = inst.FuncId;
|
|
const BuiltinId = inst.BuiltinId;
|
|
const Lowering = lower.Lowering;
|
|
|
|
/// The classification of a call expression: which dispatch path lowering will
|
|
/// take, the IR type the call evaluates to, and the properties (selected
|
|
/// target, enum variant, receiver / `__sx_ctx` prepend, default-arg expansion)
|
|
/// that path implies.
|
|
///
|
|
/// `plan(c)` is the single point that recognises a call form; `resultType(c)`
|
|
/// is the thin "just the type" projection (`plan(c).return_type`). This step
|
|
/// (A3.2 convergence sub-step 2) builds the plan object and routes typing
|
|
/// through it; `lowerCall` still owns its own dispatch and is rerouted onto
|
|
/// the plan in sub-step 3.
|
|
pub const CallPlan = struct {
|
|
kind: Kind,
|
|
return_type: TypeId,
|
|
target: Target = .none,
|
|
/// Enum / tagged-union variant tag, for the construction kinds.
|
|
variant: ?u32 = null,
|
|
/// Lowering prepends the receiver as arg 0 (UFCS / instance-method forms).
|
|
prepends_receiver: bool = false,
|
|
/// Lowering prepends the implicit `__sx_ctx` as arg 0.
|
|
prepends_ctx: bool = false,
|
|
/// The caller omits trailing positional args the callee provides defaults
|
|
/// for, so lowering splices them in (`expandCallDefaults` / `appendDefaultArgs`).
|
|
expands_defaults: bool = false,
|
|
|
|
pub const Kind = enum {
|
|
builtin,
|
|
reflection,
|
|
generic_fn,
|
|
/// A plain free function — resolved (`target.func`) or known only by
|
|
/// AST and lowered lazily (`target.named`).
|
|
direct_fn,
|
|
closure,
|
|
fn_pointer,
|
|
protocol_dispatch,
|
|
struct_method,
|
|
/// Free-function UFCS: `recv.fn(args)` → `fn(recv, args)`, where `fn`
|
|
/// is a plain free function and `recv` is a value (not a namespace /
|
|
/// type prefix). Distinct from `namespace_fn` precisely because the
|
|
/// receiver IS prepended (`prepends_receiver`).
|
|
free_fn_ufcs,
|
|
foreign_instance,
|
|
foreign_static,
|
|
/// `pkg.fn(args)` — the receiver is a namespace / module prefix, NOT a
|
|
/// value, so nothing is prepended.
|
|
namespace_fn,
|
|
enum_construct,
|
|
enum_shorthand,
|
|
unresolved,
|
|
};
|
|
|
|
/// What `plan` selected. The active arm is disambiguated by `kind`:
|
|
/// e.g. a `.named` under `.reflection` is a builtin name, under
|
|
/// `.direct_fn` a lazily-lowered fn, under `.closure` a binding.
|
|
pub const Target = union(enum) {
|
|
none,
|
|
builtin: BuiltinId,
|
|
/// A resolved (lowered) free / method / namespace function.
|
|
func: FuncId,
|
|
/// A callee carried by name — reflection builtin, generic / lazy fn,
|
|
/// closure / fn-pointer binding, or a not-yet-lowered namespace fn.
|
|
named: []const u8,
|
|
/// Protocol method, by index in the protocol's method table.
|
|
protocol_method: u32,
|
|
/// Foreign-class method (Obj-C / JNI), with its static-ness.
|
|
foreign_method: struct { name: []const u8, is_static: bool },
|
|
/// Enum / tagged-union type under construction.
|
|
constructed: TypeId,
|
|
};
|
|
};
|
|
|
|
/// Call result typing (architecture phase A3.2), extracted from
|
|
/// `Lowering.inferExprType`'s call arm. Discovers the IR type a call
|
|
/// expression evaluates to — across builtins / reflection builtins, generic
|
|
/// and plain free functions (lowered or lazy via `fn_ast_map`), closure /
|
|
/// function-typed locals, protocol dispatch, foreign-class instance/static
|
|
/// methods, struct (UFCS) methods, qualified namespace calls, and
|
|
/// enum/tagged-union construction.
|
|
///
|
|
/// A `*Lowering` facade (Principle 5, like `ExprTyper` / `PackResolver`): call
|
|
/// typing reads live lexical-scope / target-type state and the function /
|
|
/// foreign-class / protocol resolver helpers, so it borrows `*Lowering` rather
|
|
/// than re-threading every field.
|
|
pub const CallResolver = struct {
|
|
l: *Lowering,
|
|
|
|
/// Infer the IR type a call expression evaluates to (without lowering it).
|
|
pub fn resultType(self: CallResolver, c: *const ast.Call) TypeId {
|
|
return self.plan(c).return_type;
|
|
}
|
|
|
|
/// Classify a call: pick the dispatch kind / target / variant and derive
|
|
/// the result type and prepend / default-expansion properties. The single
|
|
/// source of truth for "what kind of call is this?".
|
|
pub fn plan(self: CallResolver, c: *const ast.Call) CallPlan {
|
|
if (c.callee.data == .identifier) {
|
|
const bare_name = c.callee.data.identifier.name;
|
|
// Resolve local function name (bare → mangled) and UFCS aliases
|
|
const name = blk: {
|
|
const scoped = if (self.l.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
|
if (self.l.program_index.ufcs_alias_map.get(bare_name)) |target| {
|
|
break :blk if (self.l.scope) |scope| scope.lookupFn(target) orelse target else target;
|
|
}
|
|
break :blk scoped;
|
|
};
|
|
if (Lowering.resolveBuiltin(bare_name)) |bid| {
|
|
const rt: TypeId = switch (bid) {
|
|
.sqrt, .sin, .cos, .floor => blk: {
|
|
if (c.args.len > 0) {
|
|
const arg_ty = self.l.inferExprType(c.args[0]);
|
|
if (arg_ty == .f32) break :blk TypeId.f32;
|
|
}
|
|
break :blk TypeId.f64;
|
|
},
|
|
.size_of, .align_of => .s64,
|
|
.cast => if (c.args.len > 0) self.l.resolveTypeArg(c.args[0]) else .unresolved,
|
|
else => .unresolved,
|
|
};
|
|
return .{ .kind = .builtin, .return_type = rt, .target = .{ .builtin = bid } };
|
|
}
|
|
// Reflection builtins live outside `resolveBuiltin`'s table (their
|
|
// lowering goes through `tryLowerReflectionCall`, not the
|
|
// `BuiltinId` dispatch). Recognize them here so pack-fn callers
|
|
// mangle their results with the right tag.
|
|
if (std.mem.eql(u8, bare_name, "type_name")) return refl(bare_name, .string);
|
|
if (std.mem.eql(u8, bare_name, "type_eq")) return refl(bare_name, .bool);
|
|
if (std.mem.eql(u8, bare_name, "has_impl")) return refl(bare_name, .bool);
|
|
if (std.mem.eql(u8, bare_name, "field_count")) return refl(bare_name, .s64);
|
|
if (std.mem.eql(u8, bare_name, "field_index")) return refl(bare_name, .s64);
|
|
if (std.mem.eql(u8, bare_name, "field_name")) return refl(bare_name, .string);
|
|
if (std.mem.eql(u8, bare_name, "error_tag_name")) return refl(bare_name, .string);
|
|
if (std.mem.eql(u8, bare_name, "is_comptime")) return refl(bare_name, .bool);
|
|
if (std.mem.eql(u8, bare_name, "__interp_print_frames")) return refl(bare_name, .void);
|
|
if (std.mem.eql(u8, bare_name, "__trace_resolve_frame"))
|
|
return refl(bare_name, self.l.module.types.findByName(self.l.module.types.internString("Frame")) orelse .unresolved);
|
|
if (std.mem.eql(u8, bare_name, "is_flags")) return refl(bare_name, .bool);
|
|
if (std.mem.eql(u8, bare_name, "type_of")) return refl(bare_name, .any);
|
|
if (std.mem.eql(u8, bare_name, "field_value")) return refl(bare_name, .any);
|
|
// Generic function — infer return type via type bindings.
|
|
if (self.l.program_index.fn_ast_map.get(name)) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
return .{
|
|
.kind = .generic_fn,
|
|
.return_type = self.l.genericResolver().inferGenericReturnType(fd, c),
|
|
.target = .{ .named = name },
|
|
.expands_defaults = defaultsFor(fd, c.args.len),
|
|
};
|
|
}
|
|
}
|
|
// Declared (lowered) function — return type from its signature.
|
|
if (self.l.resolveFuncByName(name)) |fid| {
|
|
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
|
return .{
|
|
.kind = .direct_fn,
|
|
.return_type = func.ret,
|
|
.target = .{ .func = fid },
|
|
.prepends_ctx = func.has_implicit_ctx,
|
|
.expands_defaults = if (self.l.program_index.fn_ast_map.get(name)) |fd| defaultsFor(fd, c.args.len) else false,
|
|
};
|
|
}
|
|
// Not lowered yet (lazy lowering): take the return type from the
|
|
// declared AST. A void/return-less fn is void — not an
|
|
// `.unresolved` guess.
|
|
if (self.l.program_index.fn_ast_map.get(name)) |fd| {
|
|
return .{
|
|
.kind = .direct_fn,
|
|
.return_type = if (fd.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .named = name },
|
|
.expands_defaults = defaultsFor(fd, c.args.len),
|
|
};
|
|
}
|
|
// Local closure- / function-typed binding (e.g. a `cb: Closure(...)
|
|
// -> R` or bare `cb: (T) -> R` parameter) — extract its declared
|
|
// return type so `try` / `catch` on the call see the (possibly
|
|
// failable) result.
|
|
if (self.l.scope) |scope| {
|
|
if (scope.lookup(bare_name)) |binding| {
|
|
if (!binding.ty.isBuiltin()) {
|
|
const ti = self.l.module.types.get(binding.ty);
|
|
if (ti == .closure) return .{
|
|
.kind = .closure,
|
|
.return_type = ti.closure.ret,
|
|
.target = .{ .named = bare_name },
|
|
.prepends_ctx = self.l.implicit_ctx_enabled,
|
|
};
|
|
if (ti == .function) return .{
|
|
.kind = .fn_pointer,
|
|
.return_type = ti.function.ret,
|
|
.target = .{ .named = bare_name },
|
|
.prepends_ctx = self.l.implicit_ctx_enabled and ti.function.call_conv != .c,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
} else if (c.callee.data == .field_access) {
|
|
const cfa = c.callee.data.field_access;
|
|
const recv_ty = self.l.inferExprType(cfa.object);
|
|
// Receiver is a protocol type → protocol method dispatch.
|
|
if (self.l.getProtocolInfo(recv_ty)) |proto_info| {
|
|
for (proto_info.methods, 0..) |m, mi| {
|
|
if (std.mem.eql(u8, m.name, cfa.field)) return .{
|
|
.kind = .protocol_dispatch,
|
|
.return_type = m.ret_type,
|
|
.target = .{ .protocol_method = @intCast(mi) },
|
|
.prepends_receiver = true,
|
|
};
|
|
}
|
|
}
|
|
// Foreign-class instance method: look up the method's declared
|
|
// return type so chained calls (e.g.
|
|
// `UIWindow.alloc().initWithWindowScene(scene)`) resolve.
|
|
{
|
|
var recv_inner = recv_ty;
|
|
if (!recv_inner.isBuiltin()) {
|
|
const ri = self.l.module.types.get(recv_inner);
|
|
if (ri == .pointer) recv_inner = ri.pointer.pointee;
|
|
}
|
|
if (!recv_inner.isBuiltin()) {
|
|
const inner_info = self.l.module.types.get(recv_inner);
|
|
if (inner_info == .@"struct") {
|
|
const sn = self.l.module.types.getString(inner_info.@"struct".name);
|
|
if (self.l.program_index.foreign_class_map.get(sn)) |fcd| {
|
|
for (fcd.members) |m| switch (m) {
|
|
.method => |md| if (!md.is_static and std.mem.eql(u8, md.name, cfa.field)) {
|
|
return .{
|
|
.kind = .foreign_instance,
|
|
.return_type = self.l.resolveForeignMethodReturnType(fcd, md),
|
|
.target = .{ .foreign_method = .{ .name = md.name, .is_static = false } },
|
|
.prepends_receiver = true,
|
|
};
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Instance method call: obj.method(args) → StructName.method.
|
|
{
|
|
var obj_ty = recv_ty;
|
|
if (!obj_ty.isBuiltin()) {
|
|
const oi = self.l.module.types.get(obj_ty);
|
|
if (oi == .pointer) obj_ty = oi.pointer.pointee;
|
|
}
|
|
if (!obj_ty.isBuiltin()) {
|
|
const oi = self.l.module.types.get(obj_ty);
|
|
if (oi == .@"struct") {
|
|
const struct_name = self.l.module.types.getString(oi.@"struct".name);
|
|
const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ struct_name, cfa.field }) catch cfa.field;
|
|
// Generic #compiler method dispatch — return type from declaration.
|
|
if (self.l.program_index.fn_ast_map.get(qualified)) |method_fd| {
|
|
if (method_fd.body.data == .compiler_expr) {
|
|
return .{
|
|
.kind = .struct_method,
|
|
.return_type = if (method_fd.return_type) |rt| type_bridge.resolveAstType(rt, &self.l.module.types, &self.l.program_index.type_alias_map, &self.l.program_index.module_const_map) else .void,
|
|
.target = .{ .named = qualified },
|
|
.prepends_receiver = true,
|
|
.expands_defaults = defaultsFor(method_fd, c.args.len + 1),
|
|
};
|
|
}
|
|
}
|
|
if (self.l.resolveFuncByName(qualified)) |fid| {
|
|
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
|
return .{
|
|
.kind = .struct_method,
|
|
.return_type = func.ret,
|
|
.target = .{ .func = fid },
|
|
.prepends_receiver = true,
|
|
.prepends_ctx = func.has_implicit_ctx,
|
|
.expands_defaults = if (self.l.program_index.fn_ast_map.get(qualified)) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Free-function UFCS: `recv.fn(args)` → `fn(recv, args)`. lowerCall
|
|
// reaches this only when the receiver is a VALUE (the
|
|
// `is_namespace == false` path), in which case it prepends the
|
|
// receiver and fixes up a `*T` first param. Mirror that boundary so
|
|
// the plan carries `prepends_receiver`, distinct from a true
|
|
// namespace call (`pkg.fn()`), which must NOT prepend.
|
|
if (self.objectIsValue(cfa.object)) {
|
|
if (self.l.resolveFuncByName(cfa.field)) |fid| {
|
|
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
|
return .{
|
|
.kind = .free_fn_ufcs,
|
|
.return_type = func.ret,
|
|
.target = .{ .func = fid },
|
|
.prepends_receiver = true,
|
|
.prepends_ctx = func.has_implicit_ctx,
|
|
.expands_defaults = if (self.l.program_index.fn_ast_map.get(cfa.field)) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
|
};
|
|
}
|
|
if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
|
|
return .{
|
|
.kind = .free_fn_ufcs,
|
|
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .named = cfa.field },
|
|
.prepends_receiver = true,
|
|
.expands_defaults = defaultsFor(bfd, c.args.len + 1),
|
|
};
|
|
}
|
|
}
|
|
// Type.variant(args) — qualified construction; foreign static; or a
|
|
// qualified namespace function. Reached for namespace / type
|
|
// prefixes (and inert for value receivers handled above).
|
|
const type_name = switch (cfa.object.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => null,
|
|
};
|
|
if (type_name) |tn| {
|
|
// Foreign-class static method: `Alias.static_method(args)`.
|
|
if (self.l.program_index.foreign_class_map.get(tn)) |fcd| {
|
|
for (fcd.members) |m| switch (m) {
|
|
.method => |md| if (md.is_static and std.mem.eql(u8, md.name, cfa.field)) {
|
|
return .{
|
|
.kind = .foreign_static,
|
|
.return_type = self.l.resolveForeignMethodReturnType(fcd, md),
|
|
.target = .{ .foreign_method = .{ .name = md.name, .is_static = true } },
|
|
};
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
const type_name_id = self.l.module.types.internString(tn);
|
|
if (self.l.module.types.findByName(type_name_id)) |ty| {
|
|
const ti = self.l.module.types.get(ty);
|
|
if (ti == .tagged_union or ti == .@"enum") return .{
|
|
.kind = .enum_construct,
|
|
.return_type = ty,
|
|
.target = .{ .constructed = ty },
|
|
.variant = self.l.resolveVariantIndex(ty, cfa.field),
|
|
};
|
|
}
|
|
// Qualified function call. `resolveFuncByName` only finds
|
|
// ALREADY-LOWERED functions; namespace imports are typically
|
|
// lowered lazily on demand, so a fresh `pkg.hello()` call site
|
|
// may resolve through `fn_ast_map` first. Without this, the
|
|
// call's return type silently falls through to `.unresolved`
|
|
// and any pack-fn caller (e.g. `print("{}\n", pkg.hello())`)
|
|
// mangles the arg, mis-tagging the actual string in the Any box.
|
|
const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ tn, cfa.field }) catch cfa.field;
|
|
if (self.l.resolveFuncByName(qualified)) |fid| {
|
|
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
|
return .{
|
|
.kind = .namespace_fn,
|
|
.return_type = func.ret,
|
|
.target = .{ .func = fid },
|
|
.prepends_ctx = func.has_implicit_ctx,
|
|
.expands_defaults = if (self.l.program_index.fn_ast_map.get(qualified)) |fd| defaultsFor(fd, c.args.len) else false,
|
|
};
|
|
}
|
|
if (self.l.program_index.fn_ast_map.get(qualified)) |qfd| {
|
|
return .{
|
|
.kind = .namespace_fn,
|
|
.return_type = if (qfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .named = qualified },
|
|
.expands_defaults = defaultsFor(qfd, c.args.len),
|
|
};
|
|
}
|
|
// Namespace aliases sometimes register the function under its
|
|
// bare name (matches `lowerCall`'s effective-name resolution).
|
|
if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
|
|
return .{
|
|
.kind = .namespace_fn,
|
|
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
|
.target = .{ .named = cfa.field },
|
|
.expands_defaults = defaultsFor(bfd, c.args.len),
|
|
};
|
|
}
|
|
}
|
|
} else if (c.callee.data == .enum_literal) {
|
|
// .Variant(args) — dot-shorthand construction. Result type is
|
|
// whatever target type is in scope; absent one it stays unresolved.
|
|
const rt = self.l.target_type orelse .unresolved;
|
|
var variant: ?u32 = null;
|
|
if (self.l.target_type) |tgt| {
|
|
if (!tgt.isBuiltin()) {
|
|
const ti = self.l.module.types.get(tgt);
|
|
if (ti == .tagged_union or ti == .@"enum")
|
|
variant = self.l.resolveVariantIndex(tgt, c.callee.data.enum_literal.name);
|
|
}
|
|
}
|
|
return .{
|
|
.kind = .enum_shorthand,
|
|
.return_type = rt,
|
|
.target = if (variant != null) .{ .constructed = rt } else .none,
|
|
.variant = variant,
|
|
};
|
|
}
|
|
return .{ .kind = .unresolved, .return_type = .unresolved };
|
|
}
|
|
|
|
fn refl(name: []const u8, rt: TypeId) CallPlan {
|
|
return .{ .kind = .reflection, .return_type = rt, .target = .{ .named = name } };
|
|
}
|
|
|
|
/// True when a field-access receiver is a value (so `recv.fn(...)` is a
|
|
/// method / UFCS call), false when it is a bare namespace / type prefix
|
|
/// (so `pkg.fn(...)` is a namespace call). This is exactly the negation of
|
|
/// `lowerCall`'s `is_namespace`: a non-identifier object is always a value;
|
|
/// an identifier / type_expr is a value iff it names a local or a global.
|
|
/// `pub` so `lowerCall` sources its namespace/value boundary here rather
|
|
/// than re-deriving it — one definition, shared by typing and lowering.
|
|
pub fn objectIsValue(self: CallResolver, obj: *const Node) bool {
|
|
const obj_name: []const u8 = switch (obj.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => return true,
|
|
};
|
|
if (self.l.scope) |scope| {
|
|
if (scope.lookup(obj_name) != null) return true;
|
|
}
|
|
return self.l.program_index.global_names.contains(obj_name);
|
|
}
|
|
|
|
/// True when a call supplying `supplied` leading params (user args plus a
|
|
/// prepended receiver for methods) omits a trailing param the callee
|
|
/// defaults — i.e. lowering will splice that default in.
|
|
fn defaultsFor(fd: *const ast.FnDecl, supplied: usize) bool {
|
|
if (supplied >= fd.params.len) return false;
|
|
return fd.params[supplied].default_expr != null;
|
|
}
|
|
};
|