Replace the two near-identical VPI annotation line scanners with one marker scanner parameterized by annotation kind. Source-only change: 91 added, 123 deleted (net -32 lines).
Suppress DEBUG output at the default INFO level, share the VPI pre-registration walker, and share the .vpi read path between the annotation cache loaders. Source-only change: 53 added, 81 deleted (net -28 lines).
Drop unreachable struct/type helpers, the legacy wrapper-main and build pipeline paths, debug-only scaffolding, unused logging helpers/constants, and the no-op LSP signature map. Add the audit report for the remaining duplication and logging work.
Element stores used to be a raw inline store, so a list filled with
`l[i] = array[i]` kept `__count__` at 0: `len()` was 0 and sorting/
iteration loops did nothing even though the raw slots held the data.
- `l[i] = v` and `l.set(i, v)` now call the real `list.set`
(`_emit_list_set_call`), the same route `append` already uses for
capacity/reallocation;
- `_list.set` grows the data buffer when `i >= capacity` (double, or
`i + 1` for a far index), copies the live prefix, and sets
`__count__ = i + 1` when `i >= count`; writing an existing index does
not change the count;
- remove the duplicated inline set logic from the subscript path and
both list-builtin branches.
Probe: `a[2] = 9` -> len 3; `a[20] = 7` -> len 21 and a[20] = 7;
`b[1] = "x"` -> len 2; the reporting project now sorts and prints
1 2 5 5 6 9; self-host 110/110; ListTest, GenericTest3, PtrOpProbe and
HashLibTest pass.
`for i in list[str]` binds i as i8*; a following `for i in range(3)`
reused that slot, so the integer index/comparison was written through an
i8* slot. With opaque pointers the store was accepted and the renderer
then saw a pointer-typed constant 3/6
(`[MLIR-FAIL] unsupported constant operand form`); without the renderer
check it silently corrupted the value.
- range loops accept any existing integer slot (the first width wins,
as before) but reject non-integer slots;
- list / array / pointer / dict / OOP iteration require the exact
element type, compared structurally (pointer depth + unwrapped base
type, because opaque pointers print alike);
- a mismatch fails fast with TypeError naming the old and new types and
suggesting a different variable name.
Probe: list[int] then range still works (prints 10/20/30, 10, 10);
list[str] then range now reports `loop variable 'i' is already declared
as '!llvm.ptr'; cannot reuse it as 'i32'`; the reporting project
compiles and runs; self-host 110/110; ListTest, GenericTest3, PtrOpProbe
and HashLibTest pass.
`for v in list[T]` allocated the loop variable as i8* and loaded each
element through i8*, so integer elements were treated as pointers. A
later reuse of the same name, e.g.
for i in nums: ... # i becomes i8*
for i in range(6): ... # stores/compares 6 in the i8* slot
then materialized a pointer-typed constant 6 and failed the renderer
with `[MLIR-FAIL] unsupported constant operand form (type '!llvm.ptr',
value '6')`.
Resolve the element type from the list specialization
(`_list_elem_type_from_name`: list[int] -> i32, list[str] -> i8*) and use
it for both the loop-variable alloca and the element load; fail fast when
the specialization name cannot be resolved instead of silently loading a
pointer.
Probe: `for v in list[int]` prints 10/20/30 and sums 60; the reporting
project compiles and runs without the MLIR failure; self-host 110/110;
ListTest, GenericTest3, PtrOpProbe and HashLibTest pass.
A pointer chain that reaches a class is a reference to the object, not an
array of objects. `arr: &list[int]*` (inside `arr` is `list[int]**`) used
to be treated as raw pointer arithmetic: `len(arr)` read the pointer slot,
`arr[i]` indexed it, and `arr.append(x)` passed the slot as self -- all
producing garbage or a runtime crash.
- add `_oop_self_ptr`: peel extra pointer levels down to Ptr(Struct);
- `len()` normalizes the receiver after resolving the class, so list
inline `__count__` and class `__len__` receive the object pointer;
- subscript reads dispatch list / class `__getitem__` with the peeled
receiver; raw pointer traversal remains the fallback when the chain
does not reach a class (or the class has no `__getitem__`);
- the list builtin method branch and `_translate_method_call` peel the
receiver, so `arr.append(x)` works through a reference;
- `is_list_subscript` unwraps every level for the assignment path, so
`arr[i] = v` and tuple swaps on elements write the real list; value-type
list variables keep their old path.
Probe: `&list[int]*` len/read/write/swap/append and `&Box*` custom
`__len__`/`__getitem__` all produce the expected values; self-host
110/110; ListTest, GenericTest3, PtrOpProbe and HashLibTest pass.
A tuple/list literal on the right of a multi-target assignment used to
return no value (`assignment rhs is None ... [kind=51]`). It is now
staged and assigned target by target:
- every RHS element is evaluated left to right and parked in its own
temporary slot, so a swap always reads the old values;
- each target is then written by the ordinary single-target path, which
keeps const checks, coercion, attribute/field stores, array and list
element stores (`__setitem__`) and ownership handling unchanged;
- the temporary's life tag is copied from the staged expression, so
pointer assignments keep their existing frame/heap lifetime;
- starred elements/targets and compiler-owned `new ... slife` objects
fail fast (they cannot be staged without changing their semantics).
Probe: ints, array elements (`a[i], a[i+1] = a[i+1], a[i]`) and struct
fields (`p.x, p.y = p.y, p.x`) all swap correctly (exit 2304);
self-host 110/110; PtrOpProbe, HashLibTest, GenericTest3 and ListTest
still pass.
`f(a: &T)` lowers to `T*` and takes the argument's address at the call
site. The old rule passed any pointer argument through unchanged, so
`bubble_sort(nums)` with `arr: &list[int]*` (T = list[int]*) sent
`list[int]*` where `list[int]**` was declared and failed with a pointer
level 1 -> 2 CastError.
- _apply_auto_addr_arg compares the argument's structural pointer depth
with the parameter type instead of "is a pointer". For a Name whose
own alloca already has the parameter depth, the alloca is passed
directly: the `&obj` fold on an object-pointer variable drops one
level, which is wrong for `&T` with a pointer T.
- translate_call_arg falls back to the definition-side vpi annotation
when the same-module table misses, so a cross-module callee that has
not been body-translated yet still gets its &T modes.
- _coerce_args_to_def_params uses the injected declare's parameter type
when the vpi text cannot resolve it (e.g. a generic class name), and
passes that type to the auto-address transform.
- a null literal passed to `&T` keeps passing through (no lvalue).
Probe: `bubble_sort(nums)` sorts through `&list[int]*`; `bump(x)` and
`bump(p)` with `p: int*` both increment x (value addressed, pointer
passed through). Self-host 110/110; GenericTest3, ListTest, PtrOpProbe
pass; EscapeTest still reports its expected LifetimeError.
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
ViperC is now self-hosted, so its own source can use `in (...)`/`[...]`/`{...}`.
The vpi scanner compared raw byte values (0x22/0x27/0x28/...) and now reads
if ch in ('"', '\''): quote = ch
elif ch in ('(', '[', '{'): depth += 1
which is the same lowering the compiler emits for user code (OR of equality
tests, string literals through strcmp).
- `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.
__vpcbuild__/ and __vpcbuild_*/ covered only two spellings; builds also create
__vpcbuild3__, __vpcbuild6__ and similar, so collapse them into one glob.
- 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.
Raw-IR modules (`__llvmir__` / `__mlir__`) were the last common case that still needed
child processes: llvm-link merged the `.raw.ll` sidecar into the translated module, then
`opt -passes=always-inline,globaldce` inlined the raw bodies. Both now run inside the DLL:
- new `ircore_mlir_to_obj_raw(mlir, len, raw_ir, raw_ir_len, ...)`: the sidecar is parsed
into the same LLVM context, merged with LLVMLinkModules2, then run through
always-inline,globaldce via LLVMRunPasses (IRCORE_FLAG_INLINE covers the `.inline`
marker case, which has no sidecar)
- the worker reads the sidecar itself with stdlib buffers (MemBuddy is not thread-safe),
so `.ll.pre`, `.ll.link` and `.ll.inline` intermediates are gone as well
- LLVMLinkModules2 is exported by libLLVM-22.dll but no longer declared in the llvm-c
headers we build against, so it is declared by hand (stable C ABI)
- PhaseB falls back to the legacy flow only for --phase2command now
Verified: RawIRTest runs through ircore with no `.mlir.opt`/`.ll.pre`/`.ll.link`/
`.ll.inline` leftovers and prints the same results as the legacy build; the FFI suite
gained a raw-merge check (the sidecar symbol is undefined without it, linked and inlined
away with it); TestFBProject (also uses __llvmir__) and 12 representative projects pass.
--traceback only needed one thing from llc (the per-module part): -frame-pointer=all. The
codegen flags are now a parameter of the FFI instead of a reason to fall back:
- ircore_mlir_to_obj_opts / ircore_llvmir_to_obj_opts take IRCORE_FLAG_* bits:
ASan instrumentation and frame pointers
- frame pointers are applied by adding the "frame-pointer"="all" function attribute to
every definition (what clang emits for -fno-omit-frame-pointer; the backend honours it
and it overrides the target default that llc's flag would have set), after ASan ran so
its generated helpers are covered too
- PhaseB calls the opts entry point with flags = asan|traceback, so neither mode starts a
child process anymore; the legacy command flow is unchanged for --phaseb-legacy,
--phase2command and raw-IR modules
Verified: a --traceback build has no .mlir.opt leftovers, the object carries the same rbp
prologues as `llc -frame-pointer=all`, the <exe>.stackmap is produced (24616 bytes, same
as the legacy build) and the executable runs; --asan still reports the same heap overflow;
12 representative Test projects pass with the deployed compiler.
--asan used to force the legacy PhaseB because the instrumentation step was a separate
`opt -passes=forceattrs,asan -force-attribute=sanitize_address -asan-globals=false` child
process. ircore.dll now exposes ircore_mlir_to_obj_asan / ircore_llvmir_to_obj_asan, which
run the same passes through LLVM's C pass builder (LLVMRunPasses) between MLIR lowering and
codegen, so ASan builds also avoid the three child processes.
- the two cl::opt values are parsed once (std::call_once) before the first pass run
- PhaseB loads the asan entry point together with the others and calls it when _asan_mode
is set; the legacy `opt` path stays for --phaseb-legacy and is unchanged
- --traceback (llc -frame-pointer=all) and raw-IR modules (llvm-link/opt) still take the
legacy flow
Verified on a real VKernel module: the ircore-built object and the `opt`-instrumented one
reference the same ASan runtime symbols (69 __asan_report / 4 asan.module_ctor), and a
deliberate heap overflow is reported identically by both builds.
PhaseB spawned three child processes per module (mlir-opt, mlir-translate, llc), and the
process startup dominated the cost: a 0.3 KB module took ~170 ms, of which ~150 ms was
just starting the tools; the 59-module VKernel set cost 18.4 s CPU (3.3 s wall on 16
workers).
ircore.dll (IRCore) does MLIR parse + verify + LLVM IR translation + codegen in a single
call. PhaseB now uses it from the worker threads:
- loaded at runtime with LoadLibraryA/GetProcAddress, not through the import table, so the
compiler still starts when the DLL is missing; one retry points the loader at clang64's
bin directory because ircore.dll needs that exact LLVM build
- falls back to the legacy flow automatically (with a warning) when the DLL cannot load
- `--phaseb-legacy` forces the legacy backend; the old command path is kept intact
(compile_ll_to_obj / init_llc_task / _llc_task_worker / run_llc_parallel)
- ASan instrumentation, raw-IR merging (llvm-link/opt), --traceback frame pointers and
--phase2command keep using the external tools
- new failure stage LLC_STAGE_IRCORE so a failure is attributed to the FFI call
Measured: 59 VKernel modules 18.4 s -> 4.8 s CPU, 3.3 s -> 1.6 s wall; AstTest
--rebuild-includes 13.6 s -> 11.3 s; BuddyTest 4.8 s -> 2.9 s; ViperC self-host 125 s ->
119 s (PhaseA dominates it); 41 Test projects pass, plus the 5 pre-existing failures.
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)
LookupSignDefaultsByName subtracted one receiver slot from every class-level def, but the vpi keeps the declared parameter list: a @staticmethod's first parameter is a real argument. Scheduler.exit_process(pid: int) therefore reported 0 parameters and the call-site arity check rejected exit_process(p.pid). Subtract the receiver only when the first parameter is actually named self/cls.
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
Include .obj files are cached under <includes>.binary_mlir/<sha>.obj and keyed by the
module's own source sha1 only, while the object body embeds the sha1-prefixed symbols of
every module it calls. After a dependency (or the compiler) changed, the cached object was
still reused, so the link either failed with an undefined symbol for a module sha1 that no
longer existed, or ran code compiled against the previous module graph. Restoring a source
file to a previously built revision therefore surfaced old codegen, which looks like a
compiler regression.
Store a stamp in the cache directory holding the running compiler's build id (exe size and
last write time) plus the sorted sha1 set of every include module; App sources are excluded
so an App-only edit keeps the cache. A mismatch deletes the cached objects and the build
recompiles them from the current sources with the current 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
Adds LspSigCache: per-file signature cache under <project>/.viperc-lsp-cache, keyed by content hash+size, invalidated on didChange/didSave; resolves from-import, module-attribute and wildcard calls without building the workspace index. Extends LspTypes with list/array/pointer structural checks, array/string initializer lengths and list-literal element checks; adds return-type consistency, augmented-assignment narrowing, constant folding for unary/binop and string/list subscripts. Diagnostics use a per-line occurrence bucket, a dedicated cache work pool and statement/expression budgets so large include files stay responsive. gate13 covers the new rules.
Adds a conservative type model (LspTypes) and checks for annotated declarations, plain assignments to annotated names, and same-file function calls: implicit narrowing (including the compiler's constant bit-range exemption), bool narrowing, double->float, int<->ptr, float<->ptr, scalar->aggregate, str mismatch, plus argument count/type checks. Signatures are parsed from the existing symbol detail; cross-file calls are intentionally skipped to keep publish latency low. Diagnostics run in a scratch arena sized for analysis, with a 256-diagnostic cap; gate13 covers the new rules and the repo corpus stays clean.
Adds the first semantic rule: a function with a non-void return annotation that can reach the end without return/raise is reported at the return annotation. If/else must both exit; bare return is accepted (compiler parity); ... stubs, extern declarations and .vpi files are skipped. The annotation span is recovered from the header so int*/[]int/const int* are underlined completely. Semantic checks run only on syntactically clean files to avoid cascades. New probe --gate13 covers 13 cases plus a corpus check.
initializeOptions.disablePullDiagnostics now hides diagnosticProvider and keeps push diagnostics, matching the extension setting. gate2 verifies push and pull both clear the overflow diagnostic after the literal is fixed, and that the disable option falls back to push.
\rom mod import name as alias\ 原来只为别名生成 occurrence,原始名 name 没有 token。现在原始名和别名各生成一个 occurrence(按源码顺序),两者都解析到被导入的声明;\import a.b as c\ 仍只标注别名,模块路径保持 namespace。gate10 增加原始名/别名/使用处的 token 与定义断言。