Three gaps kept `mod.f[T](x)` from compiling:
- get_func_name now unwraps a Subscript callee to its base name, so an
expression statement like `f[int](x)` / `mod.f[int](x)` reaches the
normal call path instead of failing name resolution. Generic class
constructors and type subscripts still return None, keeping their
dedicated paths.
- the generic-call dispatcher resolves `Attribute` bases inside the
subscript (`temp.array2list[int]`) to the defining module's SHA1, and
treats it as a module-qualified template lookup.
- inference understands pointer/array annotations around the type
parameter: `_annotation_refers_type_param` walks PtrT/ArrayT wrappers
and tuple slices, and inference peels as many pointer/array layers off
the argument as the parameter annotation wraps around T, so
`array2list(l, ...)` with `array: T*` and `l: []int` infers int.
Self-host: 110/110 with the new compiler building itself; GenericTest,
GenericTest3, GenericTest4, VectorGenericTest and ListTest pass.
A file opts into the strict NAFTAG lifetime mode with a leading
from __future__ import __cwa__
and only that file changes: every other module keeps the legacy @life
escape analysis, so existing code is untouched.
- detect_strict_cwa publishes the flag per file (reset before each
translation, also visible to the .vpi pass); the directive must be the
file's first statement, a module docstring may precede it
- the directive is not a module import: import translation returns early
and the .vpi writer skips it, so importers never look for `__future__`
- strict files reject @life/@willFree with LifetimeError; the OBS
call-site contracts are the only boundary syntax there
- Translator.StrictCWA carries the mode for the current file
- `x in (a,b,c)` / `[..]` / `{..}` lowers to an OR of equality tests (string
literals via strcmp) and `not in` negates it; literal containers used to
silently evaluate to false
- add VpiText (ParenEnd/ParamSpans/ParamCount/HasVararg/FindDefaultEq) and
drop the five hand-written copies of the parameter scanner; escape skips no
longer jump past the parameter-list end
- fill omitted arguments from recorded defaults before any zero-fill, and fail
fast when no default is recorded (memcpy keeps its intrinsic lowering)
- resolve package-qualified `extern def` calls without a SHA1 prefix:
explicit allow_star=1, rel-path module fallback, raw vpi bare-declaration
scan, and `extern def`/`export def` flags honoured during declare injection
- fix exact-size read_all usage in VpiStub.Load/Ensure and the vpi scanner
One-token annotation change across the call generators: str -> char*.
No logic changes; the same pass also rewrote literal "string" in comments,
which is why this is committed as a checkpoint rather than as behaviour.
- add VpiStub: fixed-layout blob written next to every .vpi (dual write at all
writers, Ensure() backfills a missing or stale companion on cache reuse)
- serve arity/signature queries for top-level functions from the blob, with a
silent fallback to the text scanner (methods, varargs, duplicates, corrupt file)
- prefer the injected declare over the text arity in the cross-module check
- CDefine/type text indexes and in-place SSA renumbering and bulk appends
- WIP: char* conversion in CallGenBase and CallGenLeafKit
--spend-file <path> instruments every compiled function with
vpf_enter(id, name) / vpf_leave(id); main additionally calls
vpf_install(path, level). includes/vprofile.vp keeps the tables (no
allocation on the hot path), times with QPC and writes the report from
atexit, so normal return and exit() both produce it.
--vpf 0 off | 1 call counts | 2 +per-function time (default) |
3 +caller/callee edges
*A report path is resolved against the output exe directory, so
../prof.txt and absolute paths behave as documented regardless of the
process cwd.
*Instrumented include objects use their own cache suffix
(.binary.vpf<N>, stacked with the asan/traceback suffixes) and that
cache is cleared once when profiling is enabled.
*StubMergerBuild force-adds the vprofile runtime to the reachable set;
FuncGenBase emits a declare on demand because the instrumented module
usually does not import vprofile.
*--repl is rejected: the fragment DLL is loaded at runtime and does not
link the runtime.
--build-self no longer keeps a whitelist: SelfBuild.CollectForwardedArgs
drops only --build-self / --project / --build-dir and forwards every
other argument verbatim, so a self-build can enable the same modes as a
normal build.
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
The cross-module return-type resolution derived only {sha} and the last segment from {sha}.Class.method, then queried the vpi cache and the dependency text.ll with no class name. Both key methods by class, so a class-level function without a return annotation (Scheduler._yield) missed both lookups and hit the fail-fast 'Cannot obtain the cross-module function return type precisely' path. Derive the middle segment and pass it as the class when it names a registered class; module functions ({sha}.submod.func) keep the bare-name lookup.
Injecting a dependency's module function deduped through a suffix match, so {other_sha}.init already in the module suppressed the bare @init declare of this module's export. Calls to tty.init(...) then suffix-matched the other module's init (1 parameter) and failed the arity check; the call-site module check also accepted a mangled symbol from another module whenever this module exported any function with that name.
- dedup a bare ExportT/State symbol by exact name only (VpiReader._inject_module_declare)
- accept a foreign-module binding only for bare symbols (the export/state form) or a re-export owner (CallGenMixin)
A suffix lookup for func() also matches {sha}.Class.func symbols. The no-self exception added for sched.Scheduler._yield() was too broad: it kept such a method hit for a module-qualified call like proc.current(), returning Process* where ProcessManager.current() -> int is meant (proc.current() is a module-level function). Keep the namespaced-function exception only when the receiver path itself names the class (mod.Class.func()); bare and module-qualified calls reject method-shaped suffix hits and resolve the module symbol through the vpi.
Two follow-ups to the cross-module signature check:
- A method without a return annotation is void, like a module-level `def`. The vpi
return-annotation cache recorded it as a hardcoded i32, so callers invented an i32 return
for a void method. It now records `void` for non-extern declarations (methods included).
- A class-level function declared without `self` (`Class.func()`) is a namespaced plain
function, not a method bound to a receiver. The module-qualified call path rejected such
symbols as method bindings, which then fell through to the external-C whitelist and failed
on a void return (`Scheduler._yield`).
- A pointer argument passed to an integer-typed parameter is converted at the call boundary
instead of failing the implicit pointer-to-integer rule. A `rawptr` value whose callee
parameter degraded to uint64 (stale/mis-typed stub) now reaches the callee with its address
instead of aborting with a CastError.
mlirlite.build_call composes the llvm.call function type from the argument values and the
caller-supplied return type; it never consults the callee. Declares are synthesized the same
way (_ensure_method_declare appends missing params from the caller's own argument values and
never compares return types), so nothing verified that a call site agreed with the definition.
Two modules could therefore emit incompatible calls for the same symbol, which only the MLIR
verifier rejected later:
llvm.func @MapHHDM(i64, i64, i64) (definition, void)
llvm.call @MapHHDM(i64, i64, i32) -> i32 (caller)
- after the declare/definition exists, conform every fixed argument to its parameter type and
emit the call with the symbol's return type (_conform_call_args / _call_symbol_ret_ty)
- fail fast when a cross-module call site resolves a return type the symbol disagrees with,
instead of emitting both and letting mlir-opt report an operand type mismatch
- record a module-level `def` without a return annotation as `void` in the vpi return-annotation
cache (previously a hardcoded i32, so callers invented an i32 return for void exports);
extern/state C declarations and methods keep the legacy default
`define GW_F = double(GW)` was registered as an integer constant: the DefineT
registration only took the float path when the initializer contained a float
literal, and the cast was unwrapped before the value was read. A float cast
produces a float even without a literal, so the define became an i16 640 and
define GW_F = double(GW)
define GH_F = double(GH)
aspect: double = GW_F / GH_F
folded with integer division to 1 instead of 16/9, which broke Gargantua's
projection (and any aspect/scale ratio derived the same way).
- decide the constant kind from the original initializer (a cast to a float
type, a float literal, or a reference to a float define) and evaluate it as
a double, keeping the f32/f64 choice from the annotation or `float(...)`
- resolve DefineT names inside float expressions, so `double(GW)` and
`double(A) / double(B)` evaluate at compile time
- .vpi generation writes such defines as float literals (`= 640.0`) instead of
running the integer evaluator, which also fixes a SIGFPE when that evaluator
hit `0 / 0` for a float expression; integer division/modulo now guard a zero
right operand instead of trapping the compiler
should_zext_widen decides the widening direction from the narrow side's AST
node, but the return and assignment coercions passed the statement node.
A statement has no type of its own, so the literal rule and the annotated
variable rule were skipped and the value fell through to sext:
`def f() -> uint64: return 0xDEADBEEF` produced 0xFFFFFFFFDEADBEEF and the
caller compared against 0xDEADBEEF and failed.
Unwrap Return/Assign/AnnAssign/AugAssign/Expr to their value expression
before the direction rules run, so a non-negative literal wider than
INT32_MAX zero-extends (0xDEADBEEF -> 0xDEADBEEF) and a negative one keeps
its sign (-1 -> 0xFFFFFFFFFFFFFFFF).
A uint16 value that is widened to int/uint32/uint64 must be zero-extended
(movzwl), and an int16 value must be sign-extended (movswl). coerce_to_type
picks the direction from the source AST node's declared signedness, which
requires the translator; several call sites omitted it and therefore fell
back to sext, so a big-endian uint16 such as 0xDCBC became 0xFFFF...DCBC
(numGlyphs 56508 read as -9028 and every glyph range collapsed to zero).
- pass trans at the assignment, annotated-assignment, augmented-assignment,
global-array initializer, index, element-store, constructor-argument,
conditional-expression and return coercion sites
- printf argument promotion now receives the argument expression and trans,
so uint16/uint8 promote with zext like any other widening
- print selects the format token and extension from the source signedness
(%u/%llu + zext for unsigned, %d/%lld + sext for signed) instead of a
hardcoded %d + sext for 8/16-bit values