Commit Graph
1004 Commits
Author SHA1 Message Date
TermiNexus 9e0729b855 refactor: share VPI escape and willFree scanners
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).
2026-11-02 13:29:29 +08:00
TermiNexus 38a521fcd2 refactor: filter debug logs and deduplicate VPI loaders
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).
2026-11-02 13:24:30 +08:00
TermiNexus 634321a12b refactor: consolidate duplicate ViperC helpers
Merge repeated list lowering, directory scanning, SHA1-store writes, VPI lookup keys, type-parameter splitting, DefineT width normalization, and LSP helpers. Source-only change: 235 added, 793 deleted (net -558 lines).
2026-11-02 13:08:50 +08:00
TermiNexus 5b68732a94 refactor: remove dead ViperC compiler code
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.
2026-11-02 12:46:53 +08:00
TermiNexus 5a0d61a163 feat(gen): l[i] = v extends the list to include i
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.
2026-11-02 11:34:48 +08:00
TermiNexus 10071cb58e fix(gen): reject reusing a loop variable with a different type
`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.
2026-11-02 11:21:19 +08:00
TermiNexus 91d300d45f fix(gen): list iteration loads the real element type
`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.
2026-11-02 11:10:20 +08:00
TermiNexus 49683fc881 fix(gen): OOP __len__/__getitem__/methods beat pointer arithmetic
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.
2026-11-02 10:50:35 +08:00
TermiNexus 83d309509b feat(gen): tuple-literal simultaneous assignment (a, b = b, a)
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.
2026-11-02 10:29:05 +08:00
TermiNexus 94d8c0d151 fix(gen): keep &T auto-address correct when T is itself a pointer
`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.
2026-11-02 09:46:31 +08:00
TermiNexus f57a7d962c feat(gen): generic calls with explicit args and pointer-parameter inference
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.
2026-11-02 09:07:32 +08:00
TermiNexus 40e3388dc3 feat(gen): per-file strict CWA mode via from __future__ import __cwa__
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
2026-11-01 19:03:56 +08:00
TermiNexus 63f3e053b2 refactor(stub): use literal-container membership in the vpi scanner
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).
2026-11-01 00:16:04 +08:00
TermiNexus 83d15289ef feat(vpc): literal-container membership, one vpi scanner, extern/default fixes
- `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
2026-11-01 00:05:44 +08:00
TermiNexus 84f11a69e7 refactor(callgen): annotate string values as char* (mechanical pass)
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.
2026-10-31 16:50:55 +08:00
TermiNexus cd78edd702 chore: ignore the prebuilt libs directory
libs/libLLVM-22.dll is a 155 MB downloadable artifact; keeping it out of the
index keeps clones and diffs small.
2026-10-31 16:30:32 +08:00
TermiNexus 759c810cb2 chore: ignore every __vpcbuild*__ build directory
__vpcbuild__/ and __vpcbuild_*/ covered only two spellings; builds also create
__vpcbuild3__, __vpcbuild6__ and similar, so collapse them into one glob.
2026-10-31 16:28:10 +08:00
TermiNexus 4bf04c2452 feat(vpc): binary .vpb stub companion and str->char* migration in call gen
- 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
2026-10-31 16:25:18 +08:00
TermiNexus 451d89b39a feat(vpf): default to level 3 (counts + time + edges) when --spend-file is given 2026-10-30 22:26:02 +08:00
TermiNexus 91e188c539 feat(vpc): vprofile instrumentation (--spend-file/--vpf) + verbatim --build-self forwarding
--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.
2026-10-30 22:24:01 +08:00
TermiNexus c9d33bc202 build(phaseb): move llvm-link + opt for raw-IR modules into ircore
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.
2026-10-30 20:49:13 +08:00
TermiNexus f8368d5e47 build(phaseb): run --traceback through ircore too
--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.
2026-10-30 20:33:39 +08:00
TermiNexus eba9e9b6cc build(phaseb): run ASan instrumentation in-process too
--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.
2026-10-30 20:09:00 +08:00
TermiNexus ef4582dcfc build(phaseb): compile modules in-process through ircore.dll
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.
2026-10-30 18:42:55 +08:00
TermiNexus eda8e4465f refactor(codegen): one body pipeline for module functions and class methods
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.
2026-10-29 19:37:55 +08:00
TermiNexus 3b3cd42d1c fix(codegen): a procedure never emits a value return
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
2026-10-29 18:13:06 +08:00
TermiNexus ffe31cc6e3 fix(call): pass the class segment when resolving a cross-module method return type
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.
2026-10-29 17:48:59 +08:00
TermiNexus f5c5e068ae fix(vpi): keep the bare ExportT declare when another module has the same name
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)
2026-10-29 17:33:31 +08:00
TermiNexus 825fef8f41 fix(vpi): count a static method's first parameter as an argument
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.
2026-10-29 17:19:00 +08:00
TermiNexus 8effc94827 fix(call): bind mod.func() to the module function, not a same-named static method
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.
2026-10-29 17:10:17 +08:00
TermiNexus 1f067aa58f fix(call): accept void methods and rawptr arguments at the call boundary
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.
2026-10-29 16:43:35 +08:00
TermiNexus 4c34e396a4 fix(call): align cross-module calls with the callee symbol's signature
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
2026-10-29 15:53:43 +08:00
TermiNexus a5284f9ce5 fix(codegen): keep float DefineT initializers floating
`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
2026-10-29 02:28:55 +08:00
TermiNexus 52f41c2553 fix(build): invalidate cached include objects when the include set or compiler changes
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.
2026-10-29 00:41:05 +08:00
TermiNexus d5d05ec11e fix(codegen): widen from the value expression, not the enclosing statement
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).
2026-10-28 23:15:07 +08:00
TermiNexus 19f9c53a5f fix(codegen): zero-extend unsigned 16-bit widening in every coercion path
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
2026-10-28 23:06:17 +08:00
TermiNexus 1adc61e3c2 chore: ignore LSP signature cache 2026-10-28 22:36:25 +08:00
TermiNexus 1968748ea5 feat(lsp): cross-file signatures, returns, augassign and richer types
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.
2026-10-28 22:36:07 +08:00
TermiNexus 79d53e691c fix(codegen): keep module-level DefineT initializers 2026-10-28 21:45:38 +08:00
TermiNexus fde6571a3b feat(lsp): 类型赋值/调用/隐式转换诊断对齐编译器
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.
2026-10-28 18:56:14 +08:00
TermiNexus 9ff5224a36 feat(lsp): flag non-void functions that fall off the end
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.
2026-10-28 15:53:13 +08:00
TermiNexus ea53f11027 fix(lsp): honor disablePullDiagnostics and add stale-overflow regression
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.
2026-10-28 15:22:01 +08:00
TermiNexus 7072acea82 test(lsp): 覆盖修复字面量后溢出诊断清除
gate2 在 push 与 pull 两条路径上都验证:把超长字面量改成 1 后诊断必须清空,防止词法错误状态串到下一次解析。
2026-10-28 15:19:08 +08:00
TermiNexus 86cced94dc fix(lsp): 拉取诊断客户端不再重复收到 push 诊断
initialize 记录 textDocument.diagnostic 能力;客户端支持 pull 时不再发送 publishDiagnostics,避免 Problems 面板重复。gate2 增加溢出诊断断言:仅 1 条、range 覆盖完整字面量、错误恢复后函数语义 token 仍在,并验证 pull 客户端不会同时收到 push。
2026-10-28 15:07:15 +08:00
TermiNexus 9d298b5ae9 feat(types): 宽度事实表共享给编译器与 LSP
新增 lib/types/IntFacts.vp:显式宽度范围检查、最小可存宽度、最小二幂宽度统一到一张表。ExprGenConst 与 define_min_width 改用它;LSP hover 的位宽推荐/后缀适配判定也改用它,任意 _iN/_uN 后缀可显示,但推荐宽度仍只给二幂值。gate10 增加 128_u8、1_u100、128_u7 与数字分隔符 hover 断言;计划文档记录 Pylance 式诊断等级等决策。
2026-10-28 14:42:25 +08:00
TermiNexus d1f84371f8 feat(lsp): hover 支持字面量、模块路径与位宽分析
新增 LspHover:目标 hover 保留原有签名/文档,模块补充工作区相对路径;光标在数字/字符串字面量上时显示十进制、十六进制、二进制、最小位宽、最小二幂整型宽度(u8..u128 / i8..i128)以及 f16/f32/f64 的精确性;True/False/None 也有 hover。十六进制与十进制改为手写 nibble/逐位输出,避开运行库变参格式化对宽值的问题。gate10 增加模块/整数/hex/bin/浮点/字符串 hover 断言。
2026-10-28 13:37:40 +08:00
TermiNexus 6baad4a6e6 fix(lsp): 别名导入的原始名字也高亮可跳转
\rom mod import name as alias\ 原来只为别名生成 occurrence,原始名 name 没有 token。现在原始名和别名各生成一个 occurrence(按源码顺序),两者都解析到被导入的声明;\import a.b as c\ 仍只标注别名,模块路径保持 namespace。gate10 增加原始名/别名/使用处的 token 与定义断言。
2026-10-28 12:53:44 +08:00
TermiNexus c87f1b6f3c fix(lsp): 通配导入链式重导出可解析
from A import * 的符号没有本地声明,C.b 查 C 的成员失败后原来直接返回;现在沿目标文件的 wildcard 链继续解析(带 hop 上限),模块属性与裸名字两条路径都覆盖。gate10 增加三级通配重导出 token/跳转断言(使用大小写不冲突的夹具名)。
2026-10-28 12:39:28 +08:00
TermiNexus 8e86eea159 fix(codegen): keep unary constant folding within the operand width 2026-10-27 21:33:48 +08:00
TermiNexus e63fc34057 fix(codegen): reject out-of-range field defaults at declaration 2026-10-27 21:23:01 +08:00