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:
agra
2026-05-25 12:49:26 +03:00
parent 8e21cc5f73
commit b263704664
6 changed files with 31 additions and 100 deletions

View File

@@ -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;
};