Each `-> &T` / `-> &mut T` function now records which parameter it borrows (a parameter index, `self`, or unknown) in a return-borrow registry, keyed by symbol and by `Class.method`. A reference initialized from a call registers only the summarized argument (the receiver for methods), and `return f(...)` checks only that argument; callees without a summary keep the conservative "all borrowed arguments" fallback. Borrowing a compiler-owned (`own`) local cannot escape the frame any more: `_OwnerOutlivesFrame` rejects `own`/slife bindings and only accepts reference parameters, `self`, statics, and non-owned heap pointers, so `return id(&ownLocal)` and `return refBinding` are rejected while `return id(&heapPtr)` still passes. The LSP mirrors the owned-local rule.
Reference returns are checked structurally instead of relying on the frame tag, so `return &x` and `return x` for a by-value parameter are rejected while a reference parameter, `self`, and a call still pass. Borrows created by a call result now register the borrowed arguments, so the owner cannot be deleted while the reference lives. `own` rejects `new memgr(...)` storage (pool memory is not freed by `free`) and raw-pointer adoption; `own` arguments reject memgr too. Overwriting an `own` field releases the value it held (not during construction), and `del` of an `own` field is rejected. Reference parameters registered under their source name as well, so passing a borrow to an `&T` parameter no longer trips the stack-escape check. The LSP mirrors the new return and adoption rules.
Borrows carry a kind: while a value is mutably borrowed no new borrow starts, a mutable borrow is rejected while read-only borrows are alive, and a `&mut T` slot only accepts `&mut value`. Reference fields record their path (`B.n`) as the borrower, so `del` is blocked by nested and self borrows and released on `del`/reassign of the root. The table grows to 1024 rows and fails fast instead of silently dropping rows, and each function clears its rows at entry. `Box[T]`/`BoxT[T]` count as ownership spellings; own fields are released at scope exit (not only on `del`), for forward-declared and self-referential types too, behind a null guard. A move is only allowed into an `own` slot. Reference results use borrow elision (a reference parameter or `self`) instead of the `@life` advice, reference parameters no longer trip the stack-escape check, and `&` on an owned pointer yields the heap pointer (LIFE_HEAP).
`own` adds a pointer layer in the resolver; own parameters are released by the callee and callers move owned arguments (borrows are rejected); returning a local own binding transfers it; own fields are released when the containing object is deleted; reference-field assignments register a borrow so the owner cannot be del-ed while the object lives.
Compiled callees register a per-parameter nullable mask (self included for methods, specializations included); cross-module callees fall back to their vpi param annotations. A nullable binding/field argument now fails instead of silently narrowing.
Module-level functions and class methods each carried their own copy of "resolve the
return type", "create the parameter allocas" and "emit the implicit epilogue". The copies
drifted: an unannotated method was still typed i32 by the class-method body path while its
declaration said void, so the epilogue emitted `llvm.return 0 : i32` inside a void function
and the MLIR verifier rejected it ("'llvm.return' op expected no operands", Scheduler._yield).
- FuncGenBase.resolve_func_ret_ty: the single return-type policy (annotation -> storage
marker -> __init__/__new__ hooks -> unannotated = procedure void -> fail-fast). Used by
the forward-declare pass, the body pass, class methods and generic specializations
- FuncGenBase.param_llvm_ty: the single parameter mapping (CVoid skipped, bare class ->
Ptr(struct), ArrayT -> element pointer), used by every signature and alloca site
- FuncGenBase.emit_func_epilogue: the single implicit-return implementation (procedure ->
ret void, unreachable merge, fail-fast for an annotated fall-through), shared by module
functions, methods and nested functions
- FuncGenBase.translate_func_body: the shared body pipeline (scope entry, self/parameter
storage, *args va_list, try/except out-params, pre-scan, capture preparation, slife,
statements, epilogue, state restore). ClassGenVTable._translate_method now only handles
method naming, vtable/overload registration and add_param
- the same policy reaches cross-module and generic declares: unannotated vpi functions and
methods declare void, generic specialization declares use the shared resolver, and the
list.append call sites emit a void call
- method parameters now carry the const / volatile / pointee-volatile / pure markers that
the module-level path always had
- FuncGenMixin: the two dry-run helpers that return ints are annotated, as the language
now requires (an unannotated def is a procedure)
Verified: 41 Test projects pass (MethodProcTest, CrossModuleSigTest, RawPtrParamTest,
AstTest, ListTest, OopInheritTest, ...), ViperC self-hosts with the new compiler, and the
6 tests that still fail reproduce identically with the previous compiler.
Unannotated functions were typed by scanning the body, which inferred i32, so falling off
the end emitted `llvm.return 0 : i32` inside a function declared void. The MLIR verifier
rejects that ("expected no operands"), which is what broke Scheduler._yield.
- an unannotated `def` is void in both the forward declare and the body, so the implicit
epilogue emits `ret void`
- translate_return treats a procedure (no annotation, `-> None`, or a void-typed function) as
void: the expression is evaluated for its side effects and `ret void` is emitted. Bare
`return`, `return f()` where f is void, and `return <None value>` are legal and never carry
a value
- body_has_value_return accepts `return f()` and `return <None define>` in a procedure
- `define X = None` is a named None: registered as the string sentinel None, materialized as
a None value at use sites, and exported through the vpi