mem: delete .heap_alloc/.heap_free IR ops + the silent libc-malloc escape
allocViaContext used to fall back to a direct `.heap_alloc` (libc
malloc) when `Context` wasn't registered — i.e. when the program
didn't import std.sx. That was a silent escape hatch: a program could
appear to allocate fine without a `Context`, sidestepping protocol
dispatch entirely. Same shape as the matchContextAllocCall trap we
removed, just in a different code path.
Now: every site that needs `Context` emits a clear diagnostic when
the type isn't in scope, pointing the user at the required import.
- `allocViaContext`: the three fallback branches (no implicit_ctx, no
Context type, malformed Context struct) all call the new
`diagnoseMissingContext("heap allocation")` and return a
placeholder. Codegen no longer emits libc malloc as the silent
no-import path.
- `lowerPush`: the no-Context branches used to silently drop the
push and just lower the body. Now diagnose first, then lower
(keeping the body's other diagnostics flowing).
- `lowerIdentifier` for "context": used to silently fall through to
`global_names.get("context")` (which would emit an unresolved
identifier with no actionable hint). Now diagnose with the
required-import message.
With every consumer gone, the `.heap_alloc` and `.heap_free` IR ops
are deleted entirely:
- `inst.zig`: drop the Op variants.
- `interp.zig`: drop the execInst arms.
- `emit_llvm.zig`: drop the arms (the `getOrDeclareMalloc/Free`
helpers stay — they're still used by the foreign-decl path for
user-level `malloc`/`free` foreign bindings).
- `print.zig`: drop the printers + the isVoidOp arm.
- `emit_llvm.test.zig`: drop the unit test (op no longer exists).
155/155 example tests pass. Unit tests green. Chess green on macOS /
iOS sim / Android. A program that doesn't import std.sx and tries to
use `context.allocator.alloc` or `push Context.{}` or the `context`
identifier now gets a real error:
error: heap allocation requires the Context type — add
`#import "modules/std.sx";` (or a module that imports it)
Closes the last silent allocation-protocol escape.
This commit is contained in:
@@ -1917,13 +1917,14 @@ pub const Lowering = struct {
|
||||
// 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 (self.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) {
|
||||
if (self.current_ctx_ref != Ref.none) {
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
break :blk self.emitError("context", node.span);
|
||||
};
|
||||
break :blk self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||
if (std.mem.eql(u8, id.name, "context")) {
|
||||
if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) {
|
||||
break :blk self.diagnoseMissingContext("the `context` identifier");
|
||||
}
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
break :blk self.diagnoseMissingContext("the `context` identifier");
|
||||
};
|
||||
break :blk self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||
}
|
||||
// Check globals (#run constants)
|
||||
if (self.global_names.get(id.name)) |gi| {
|
||||
@@ -5489,6 +5490,19 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a diagnostic for code that needs `Context` (allocator
|
||||
/// protocol, `push Context.{...}`, the `context` identifier) when
|
||||
/// the program hasn't registered the type — i.e. doesn't transitively
|
||||
/// import `modules/std.sx`. Returns a placeholder Ref so the lowering
|
||||
/// can keep going and surface any additional errors.
|
||||
fn diagnoseMissingContext(self: *Lowering, what: []const u8) Ref {
|
||||
if (self.diagnostics) |d| {
|
||||
const span = ast.Span{ .start = 0, .end = 0 };
|
||||
d.addFmt(.err, span, "{s} requires the Context type — add `#import \"modules/std.sx\";` (or a module that imports it)", .{what});
|
||||
}
|
||||
return self.emitPlaceholder("missing-context");
|
||||
}
|
||||
|
||||
/// Emit `context.allocator.alloc(size)` dispatch — used by internal
|
||||
/// compiler-driven heap copies (e.g. the `xx value` protocol-erasure
|
||||
/// path in `buildProtocolValue`). Routes through whatever allocator is
|
||||
@@ -5496,21 +5510,21 @@ pub const Lowering = struct {
|
||||
/// `push Context.{ allocator = my_alloc, ... }` actually backs every
|
||||
/// allocation including the ones the compiler inserts.
|
||||
///
|
||||
/// Falls back to `.heap_alloc` (libc malloc) only when the `context`
|
||||
/// global hasn't been registered (programs that don't `#import
|
||||
/// "modules/std.sx"`). All standard sx code imports std.sx via
|
||||
/// allocators.sx, so the fallback exists strictly for the bootstrapping
|
||||
/// edge case.
|
||||
/// If `Context` isn't registered (the program doesn't import std.sx),
|
||||
/// emits a diagnostic and returns a placeholder. We deliberately do
|
||||
/// NOT fall back to a direct libc malloc — that was the silent escape
|
||||
/// hatch that bit us through the implicit-context refactor (see the
|
||||
/// "Silent unimplemented arms" REJECTED PATTERN in CLAUDE.md).
|
||||
fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref {
|
||||
if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
return self.diagnoseMissingContext("heap allocation");
|
||||
}
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
return self.diagnoseMissingContext("heap allocation");
|
||||
};
|
||||
const ctx_ty_info = self.module.types.get(ctx_ty);
|
||||
if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
return self.diagnoseMissingContext("heap allocation");
|
||||
}
|
||||
const allocator_ty = ctx_ty_info.@"struct".fields[0].ty;
|
||||
const ctx = self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||
@@ -6210,10 +6224,12 @@ pub const Lowering = struct {
|
||||
// stack frame, rebinds the lowering's `current_ctx_ref` to it for
|
||||
// the body's lexical scope, then restores. No global, no walk.
|
||||
if (!self.implicit_ctx_enabled) {
|
||||
_ = self.diagnoseMissingContext("`push Context.{...}`");
|
||||
self.lowerBlock(ps.body);
|
||||
return;
|
||||
}
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
_ = self.diagnoseMissingContext("`push Context.{...}`");
|
||||
self.lowerBlock(ps.body);
|
||||
return;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user