ERR/E4.1b: #caller_location + Source_Location (+ namespaced default fix, comptime flush)
Finishes Phase E4. `process.exit` / `assert` now report the caller's location.
#caller_location + Source_Location:
- new `hash_caller_location` token (lexer) + a leaf `caller_location` AST node
(parser primary-expr; sema + lsp arms).
- `Source_Location :: struct { file; line; col; func }` in std.sx.
- expandCallDefaults rewrites a `#caller_location` param default to a marker
carrying the CALL site's span + source_file.
- lowerCallerLocation synthesizes the struct: file + line:col via
errors.SourceLoc.compute over the diagnostics file→source map, stamped with
the enclosing (caller) function name. inferExprType resolves it to
Source_Location. Explicitly forwarding a Source_Location through an inner
call preserves the outermost site.
namespaced default-param expansion (pre-existing crash): expandCallDefaults
bailed on field_access callees, so `mod.fn(args)` with an omitted defaulted
param passed too few args → LLVM "incorrect number of arguments". Now resolves
the namespace fd (by field / qualified name); method-on-value calls (where
`self` shifts the count) stay excluded. Prerequisite for process.exit/assert
(always called namespaced) taking `loc = #caller_location`.
comptime flush: interp.callForeign flushes the interpreter's buffered print
output before invoking any host symbol, so a comptime diagnostic emitted just
before a terminating `_exit` (process.exit at comptime) survives.
process.exit/assert take `loc: Source_Location = #caller_location`; assert
prints `ASSERTION FAILED at <file>:<line>: <msg>`. examples 250 (assert
file:line), 251 (caller-location + forwarding). The two ffi-objc *.ir
snapshots are regenerated — adding Source_Location to std.sx renumbers the
global string pool the type/field-name tables index (benign, identical IR).
This commit is contained in:
@@ -426,6 +426,15 @@ pub const Interpreter = struct {
|
||||
|
||||
fn callForeign(self: *Interpreter, func: *const inst_mod.Function, args: []const Value) InterpError!Value {
|
||||
const name = self.module.types.getString(func.name);
|
||||
|
||||
// A foreign call may not return (e.g. `process.exit` → `_exit`), which
|
||||
// would discard the interpreter's buffered `print` output (otherwise
|
||||
// flushed only after `#run` completes). Flush it first so comptime
|
||||
// diagnostics emitted just before a terminating call survive.
|
||||
if (self.output.items.len > 0) {
|
||||
_ = std.c.write(1, self.output.items.ptr, self.output.items.len);
|
||||
self.output.clearRetainingCapacity();
|
||||
}
|
||||
const symbol = (host_ffi.lookupSymbol(self.alloc, name) catch return bailDetail("comptime foreign call: dlsym error looking up symbol")) orelse {
|
||||
if (last_bail_detail == null) last_bail_detail = "comptime foreign call: symbol not found via dlsym (target-specific binding called at compile time?)";
|
||||
return error.CannotEvalComptime;
|
||||
|
||||
@@ -2657,6 +2657,7 @@ pub const Lowering = struct {
|
||||
|
||||
.try_expr => |te| self.lowerTry(te.operand, node.span),
|
||||
.catch_expr => |ce| self.lowerCatch(&ce, node.span),
|
||||
.caller_location => self.lowerCallerLocation(node),
|
||||
else => self.emitError("unknown_expr", node.span),
|
||||
};
|
||||
}
|
||||
@@ -11096,16 +11097,40 @@ pub const Lowering = struct {
|
||||
/// node with the defaults filled in. Returns null when no expansion is
|
||||
/// needed (callee unknown, all args provided, or no defaults available).
|
||||
fn expandCallDefaults(self: *Lowering, c: *const ast.Call) ?*ast.Call {
|
||||
if (c.callee.data != .identifier) return null;
|
||||
const id_name = c.callee.data.identifier.name;
|
||||
const eff_name = blk: {
|
||||
const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name;
|
||||
if (self.ufcs_alias_map.get(id_name)) |target| {
|
||||
break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
||||
const fd = blk: {
|
||||
switch (c.callee.data) {
|
||||
.identifier => |id| {
|
||||
const eff_name = blk2: {
|
||||
const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
|
||||
if (self.ufcs_alias_map.get(id.name)) |target| {
|
||||
break :blk2 if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
||||
}
|
||||
break :blk2 scoped;
|
||||
};
|
||||
break :blk self.fn_ast_map.get(eff_name) orelse return null;
|
||||
},
|
||||
// Namespace call `mod.fn(args)` — args map directly to params
|
||||
// (no `self` prepend), so default expansion is the same shape as
|
||||
// a bare call. A METHOD call `value.method(args)` prepends `self`
|
||||
// (arg/param counts are offset), so it's excluded: only treat the
|
||||
// receiver as a namespace when it isn't a value in scope.
|
||||
.field_access => |fa| {
|
||||
const obj_name: ?[]const u8 = switch (fa.object.data) {
|
||||
.identifier => |id| id.name,
|
||||
.type_expr => |te| te.name,
|
||||
else => null,
|
||||
};
|
||||
const name = obj_name orelse return null;
|
||||
if (self.scope) |scope| {
|
||||
if (scope.lookup(name) != null) return null; // method call on a value
|
||||
}
|
||||
if (self.global_names.contains(name)) return null;
|
||||
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ name, fa.field }) catch fa.field;
|
||||
break :blk self.fn_ast_map.get(qualified) orelse self.fn_ast_map.get(fa.field) orelse return null;
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
break :blk scoped;
|
||||
};
|
||||
const fd = self.fn_ast_map.get(eff_name) orelse return null;
|
||||
if (c.args.len >= fd.params.len) return null;
|
||||
var end: usize = c.args.len;
|
||||
while (end < fd.params.len) : (end += 1) {
|
||||
@@ -11117,7 +11142,17 @@ pub const Lowering = struct {
|
||||
for (c.args, 0..) |arg, i| new_args[i] = arg;
|
||||
var i: usize = c.args.len;
|
||||
while (i < end) : (i += 1) {
|
||||
new_args[i] = fd.params[i].default_expr.?;
|
||||
const def = fd.params[i].default_expr.?;
|
||||
// `#caller_location` resolves at the CALL site, not the callee's
|
||||
// signature: emit a fresh marker carrying the call's span + file so
|
||||
// lowering synthesizes the caller's `Source_Location` (ERR E4.1b).
|
||||
if (def.data == .caller_location) {
|
||||
const n = self.alloc.create(ast.Node) catch return null;
|
||||
n.* = .{ .span = c.callee.span, .data = .{ .caller_location = {} }, .source_file = c.callee.source_file };
|
||||
new_args[i] = n;
|
||||
} else {
|
||||
new_args[i] = def;
|
||||
}
|
||||
}
|
||||
const new_call = self.alloc.create(ast.Call) catch return null;
|
||||
new_call.* = .{ .callee = c.callee, .args = new_args };
|
||||
@@ -13863,6 +13898,7 @@ pub const Lowering = struct {
|
||||
if (op_ty == channel) break :blk .void;
|
||||
break :blk self.failableSuccessType(op_ty);
|
||||
},
|
||||
.caller_location => self.module.types.findByName(self.module.types.internString("Source_Location")) orelse .unresolved,
|
||||
.if_expr => |ie| {
|
||||
// If-else types as its branches' unified type. A `noreturn`
|
||||
// branch (one that diverges — `return` / `raise` / `break` /
|
||||
@@ -15574,6 +15610,46 @@ pub const Lowering = struct {
|
||||
/// callees, propagation from a value-carrying caller, and `try` inside an
|
||||
/// `or` chain need the error-channel tuple ABI / fallback routing — those
|
||||
/// land in E1.4b/E2, so we bail loudly here.
|
||||
/// Synthesize a `Source_Location` value for a `#caller_location` marker
|
||||
/// (ERR E4.1b). The node's `span`/`source_file` are the CALL site (rewritten
|
||||
/// by `expandCallDefaults`); resolve them to file / line:col against the
|
||||
/// source text and stamp the enclosing (caller) function name.
|
||||
fn lowerCallerLocation(self: *Lowering, node: *const Node) Ref {
|
||||
const sl_tid = self.module.types.findByName(self.module.types.internString("Source_Location")) orelse {
|
||||
if (self.diagnostics) |d| d.addFmt(.err, node.span, "`#caller_location` needs `Source_Location` (from std.sx) in scope", .{});
|
||||
return self.builder.constInt(0, .void);
|
||||
};
|
||||
const file = node.source_file orelse self.current_source_file orelse (self.main_file orelse "");
|
||||
const src = self.sourceForFile(file);
|
||||
const loc = errors.SourceLoc.compute(src, node.span.start);
|
||||
const func_name = self.currentFunctionName();
|
||||
var fields = [_]Ref{
|
||||
self.builder.constString(self.module.types.internString(file)),
|
||||
self.builder.constInt(@intCast(loc.line), .s32),
|
||||
self.builder.constInt(@intCast(loc.col), .s32),
|
||||
self.builder.constString(self.module.types.internString(func_name)),
|
||||
};
|
||||
return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &fields) catch unreachable } }, sl_tid);
|
||||
}
|
||||
|
||||
/// The source text for `file`, via the diagnostics' file→source map (which
|
||||
/// includes the main file). Empty if unavailable — line:col then degrade to
|
||||
/// 1:1 rather than crash.
|
||||
fn sourceForFile(self: *Lowering, file: []const u8) []const u8 {
|
||||
const diags = self.diagnostics orelse return "";
|
||||
if (diags.import_sources) |is| {
|
||||
if (is.get(file)) |s| return s;
|
||||
}
|
||||
return diags.source;
|
||||
}
|
||||
|
||||
/// Name of the function currently being lowered (the caller, at a
|
||||
/// `#caller_location` site), or "" outside any function.
|
||||
fn currentFunctionName(self: *Lowering) []const u8 {
|
||||
const fid = self.builder.func orelse return "";
|
||||
return self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name);
|
||||
}
|
||||
|
||||
fn lowerTry(self: *Lowering, operand: *const Node, span: ast.Span) Ref {
|
||||
// (1) `try` is legal only inside a failable function.
|
||||
const caller_ret = self.effectiveReturnType() orelse {
|
||||
|
||||
Reference in New Issue
Block a user