Commit Graph
1013 Commits
Author SHA1 Message Date
TermiNexus 2c65cea97f feat(lsp): parse own/parenthesized/nullable types and report implicit conversions
ParseType now understands `own T`, `(T)` and `T?`, so assignment checks reuse the existing int/pointer conversion codes (integer<->pointer, pointer mismatch). Value-typed targets reject pointer values, and duplicate nullable reporting is gone.
2026-11-03 19:49:04 +08:00
TermiNexus 295d78aa56 feat(lsp): report nullable flow and pointer/value mismatches
Returns, call arguments (from the callee signature) and constructor arguments (from __init__ or field order) now flag a nullable value flowing into a non-nullable slot, and a pointer value stored into a value-typed target is reported like the compiler does.
2026-11-03 19:34:27 +08:00
TermiNexus 3bc7afaf64 feat(obs): reject nullable constructor and variant payload arguments
OOP constructors read the nullability mask of their __init__ (self offset included), plain structs and renum variants read their field flags; generic variant payloads are read from the normalized type argument. A nullable value can no longer be packed into a non-nullable payload.
2026-11-03 18:26:50 +08:00
TermiNexus 65e03c823f feat(obs): reject nullable call arguments for non-nullable parameters
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.
2026-11-03 18:12:20 +08:00
TermiNexus b17de2bb65 feat(obs): reject nullable values in non-nullable bindings
In strict mode a `T?` binding or field may hold None, so assigning it to an `own T`, a plain pointer, a non-nullable field or a non-nullable return now fails instead of silently narrowing. Unwrap with `match` (or declare the target as `T?`). The language server publishes the same diagnostic for annotated/plain assignments.
2026-11-03 18:01:21 +08:00
TermiNexus 6073f60321 fix(own): read nullability through the own wrapper
`own (T?)` resolves to the nullable pointer, so a `None` initializer is valid there.
2026-11-03 17:21:35 +08:00
TermiNexus 1077830be9 fix(lsp): stop emitting string tokens
The TextMate grammar already colors strings, and a semantic `string` token makes VS Code classify the adjacent call parentheses as string content, so bracket pair colorization skips them.
2026-11-03 16:48:10 +08:00
TermiNexus d8884ff7dd feat(match): enforce nullable match exhaustiveness in the compiler
The language server reported a missing `None` arm while the compiler accepted the same match. Both paths now require `Some(...)` and the empty branch unless a wildcard covers them, and a wildcard arm without a `Some` arm runs for the non-null branch instead of falling through.
2026-11-03 16:31:41 +08:00
TermiNexus 273792e1be fix(lsp): leave None to the TextMate grammar
The semantic `class` token overrode the grammar and colored `None` green. Emitting nothing restores `constant.language.null`, which uses the same dark blue as `def`.
2026-11-03 16:22:38 +08:00
TermiNexus 0e5933ce09 feat(lsp): color None like a constructor
`None` is a value singleton, not a control keyword, so it now uses the same token type as `Some` instead of the `case`/`own` keyword color.
2026-11-03 16:18:10 +08:00
TermiNexus 7917992deb fix(match): reject a bare Nil pattern
A bare `Nil` parses as a capture, so a nullable match silently treated it as the null branch. Both the compiler and the language server now reject it and point at `None` (nullable `T?`) or `Nil()` (builtin `Option[T]`). Exhaustiveness reports `None` as the missing nullable branch, and the builtin generic type names no longer trigger the unknown-name warning.
2026-11-03 16:11:22 +08:00
TermiNexus 7289b027ec feat(lsp): unknown-name warnings, Nil validation and match exhaustiveness
`Nil` is rejected outside nullable/`Option` matches in both the compiler and the language server, non-exhaustive nullable matches report the missing branch names, and unresolved plain names are published as warnings. Diagnostics now honour ParseError.severity.
2026-11-03 16:00:13 +08:00
TermiNexus 2781ba7466 feat(lsp): flag direct dot access on nullable values
Attribute reads now check the base symbol detail (`T?` annotation) and emit the same error as the compiler when `?.` is missing. `?.` and pattern-bound non-null captures stay clean.
2026-11-03 15:41:14 +08:00
TermiNexus 1911f70818 feat(option): reject direct dot access on nullable values
A field annotated `own T?` / `T?` (and a local declared nullable) now fails fast on `base.field`: it may be None. Use `base?.field` or check against None first. FieldEntry carries a Nullable flag (appended), populated from the annotation during struct registration and inherited by flattened parent fields; both read and write attribute paths check it.
2026-11-03 15:30:16 +08:00
TermiNexus 98d4da7602 feat(option): allow case None on nullable pointers
None stays a keyword literal; the pattern parser already produced MatchSingleton, and the nullable-pointer match now treats it as the Nil branch. LSP colors None as a keyword.
2026-11-03 15:19:53 +08:00
TermiNexus c7ab075594 feat(option): match nullable pointers with Some(v)/Nil; align LSP captures
Compiler:
- `match p:` where p is a nullable pointer (`own U?` / `T?`) now treats
  `Some(v)` as the non-null branch (v is the pointer, so `v.field` works)
  and `Nil` / `Nil()` as the null branch. The subject's pointee must be a
  plain struct (a renum pointee keeps the tag-based renum path).
- Guards are rejected for this form (not implemented), and unsupported
  patterns fail fast.

LSP:
- `case Some(v)` captures are declared and bound to the match subject's
  class in a second pass, so `v.a` resolves and is colored as a property.
- `case Nil:` is no longer treated as a capture variable; `Some` / `Nil`
  get class/property semantic colors.

Probe: the reported ViperTestProject snippet compiles and prints
1 / 8 / 5 (v.a = 5); semantic tokens show Some=class, v=variable,
v.a property, Nil=property. Self-host 110/110 and the regression set
(EnumTest, ListTest, GenericTest/3/4, NewTest, NewLifetimeTest,
StructTest) pass.
2026-11-02 22:40:23 +08:00
TermiNexus c81ebaa7d4 feat(option): builtin Option uses the Nil variant
The lazily registered builtin template now declares Nil = 0 instead of None_.
2026-11-02 22:15:15 +08:00
TermiNexus 695c886ed0 fix(lsp): walk MatchClass sub-patterns as patterns so case captures bind
case Some(v): v is now declared and colored as a variable in the arm body.
2026-11-02 22:07:52 +08:00
TermiNexus 1f491c6dba feat(renum): generic Option[T] and Option pattern matching
- Generic renum specialization: `Option[int].Some(42)` / `Option[int].None_()`
  are intercepted before method dispatch (which would translate the
  subscript type argument as a value), specialize the renum template and
  construct the variant. `_ctor_call_agg_ty` recognizes the generic
  qualifier so construction materializes the renum value.
- Builtin Option: the generator registers `renum Option[T]` (None_ |
  Some(T)) lazily on first `Option[...]` use, with a reserved synthetic
  module id. The copy in includes/builtins.vp is skipped during Phase1
  template/variant registration so a user-defined `renum Option` (EnumTest)
  keeps precedence; constructor/annotation lookup prefers the current
  module's Option.
- Match: the renum class is derived from the concrete subject struct
  (authoritative for `Option[int]`) instead of only the annotation name.
  Both `case Option[int].Some(v)` and `case Some(v)` bind the payload;
  `None_()` works qualified or bare.

LSP: pattern captures (`case Some(v)`) are declared as variables.

Probe: builtin Option without importing builtins prints 42 for Some and
hits the None_ arm; EnumTest (user renum Option) passes; self-host
110/110; full regression set passes.
2026-11-02 22:05:09 +08:00
TermiNexus 51c0c7dae9 fix(option): base?.field yields the field value; resolve chained members in LSP
- A value field under `?.` now yields its value (zero when the base is
  null) instead of a pointer to the field. `print(_U3.n?.d)` prints the
  stored `d` (8), not an address. Pointer fields still yield the pointer
  (null on a null base), so `a?.next?.x` keeps working.
- LSP `_LocalMember` follows an intermediate field's declared type when a
  chain continues (`_U3.n.d` / `_U3.n?.d`), so `d` resolves and is colored
  as a property instead of being dropped as unknown.

Probe: the reported snippet prints 1 and 8; semantic tokens for
`print(_U3.n?.d)` include `_U3` (variable), `n` and `d` (property);
completion after `_U3.n?.` offers the U members. Self-host 110/110;
ListTest, GenericTest, GenericTest3/4, PtrOpProbe, HashLibTest, NewTest,
NewLifetimeTest, StructTest and EscapeOkTest pass.
2026-11-02 20:34:03 +08:00
TermiNexus faf1f17b22 feat(option): base?.field optional access and nullable own
Option access:
- `base?.field` evaluates to `Option[field]`: a null base yields null.
  A value field yields its address (`Option[int]` = `int*`); a pointer
  field is loaded only on the non-null branch and selected with null.
  Chaining (`n?.next?.a`) works through the pointer result.
- The AST `Attribute.optional` flag drives this in translate_attribute.

`own` nullable fix:
- `n: own U? = None` was rejected because the own-None check ignored the
  `?` wrapper. The check now skips nullable annotations, so nullable owned
  bindings accept None; non-null `own T = None` still fails.

LSP:
- member completion treats `n?.` as `n.` (base `n`, not `n?`), so fields
  and methods are offered after `?.`; semantic tokens and diagnostics
  already resolve through the optional attribute.

Probe: null `n?.a` prints the None branch; after `n = new U(5)`,
`q: Option[int] = n?.a; *q` = 5; `n?.next` null-checks; `U.__sizeof__()`
= 16; LSP completion after `n?.` offers `a`/`next` and diagnostics are
clean. Self-host 110/110; ListTest, GenericTest, GenericTest3/4,
PtrOpProbe, HashLibTest, NewTest, NewLifetimeTest, StructTest and
EscapeOkTest pass.
2026-11-02 20:24:34 +08:00
TermiNexus 3b8dc1377d feat(lsp): leave moved variables and their members uncolored
Semantic tokens now drop a variable token whose name was moved before that
line, and a property token whose base (`x` in `x.a`) is such a name. The
transfer statement itself keeps its colors (moves on the same line are
ignored for this filter), so only later uses turn plain/white.

With
  _U2: own U = _U
  _U3: own U = _U2
  print(_U2.a)
line 3 emits only `print`; `_U2` and `a` have no semantic token. The
move diagnostic still marks `_U2` separately.

Completion filtering is unchanged (`_` offers only `_U3`).
2026-11-02 20:09:54 +08:00
TermiNexus 6e6b94fdfb feat(lsp): hide moved names from completion, flag pointer-to-value writes
- Completion now computes the names moved before the cursor in the
  enclosing function and omits them, so after
  `_U2: own U = _U` typing `_` no longer offers `_U`.
- Diagnostics report a pointer/`own` value stored into a value-typed
  annotation (`_U3: U = _U2`) as an error, matching the compiler's
  CastError; pointer/own/optional targets stay clean.

Verified over textDocument/completion and textDocument/diagnostic:
`_` offers only `_U2`; `_U3: U = _U2` yields one severity-1 diagnostic on
`_U2`; `_U3: own U = _U2` yields none.
2026-11-02 20:02:46 +08:00
TermiNexus f31c1dbc78 feat(own): enforce move use-after-move and reject pointer-to-value transfer
Compiler:
- moving an owned value (`b: own T = a`) marks the source released; this
  now also applies outside OBS files when the target is declared `own`.
  Later reads/field access fail with a LifetimeError.
- transferring an owned pointer into a value-typed target
  (`_U3: U = _U2`) now fails with CastError before codegen. Previously it
  emitted a bitcast of the struct value to a pointer and died in mlir-opt.

LSP:
- track `own` declarations and moves per function and report
  "cannot use 'x': its value was moved" as an error diagnostic on later
  uses (severity 1, so editors paint it red).
- a rebinding clears the moved state.

Probe: `_U: own U = new U(1); _U2: own U = _U; print(_U2.a)` is clean;
`print(_U.a)` produces the move diagnostic and the compiler LifetimeError;
`_U3: U = _U2` produces the CastError. Self-host 110/110; ListTest,
GenericTest, GenericTest3/4, PtrOpProbe, HashLibTest, NewTest,
NewLifetimeTest, StructTest, EscapeOkTest and OopInheritTest pass.
2026-11-02 19:52:59 +08:00
TermiNexus 6fa98cf303 fix(lsp): resolve own/Box/Option types and color builtins
- Treat OwnT / OptionT / NullableT / Option / Box / BoxT / RawT as type
  wrappers in `_IsTypeWrapperName`, so `_U: own U` binds type U and
  `_U.a` resolves to the field (property color, hover, goto).
- Emit function semantic tokens for builtin calls (print, len, range,
  malloc, free, sizeof, ...), which have no declaration symbol; `print`
  was previously colored only by the grammar and lost it inside files the
  grammar could not scope.

Verified over a real semanticTokens/full request: `own` = keyword,
`print` = function, `_U.a` = property on the same line.
2026-11-02 19:01:40 +08:00
TermiNexus c95290f913 fix(lsp): color the own soft keyword
`own` is consumed by the annotation parser, so it never appears as a
symbol and the editor showed it uncolored. Emit a keyword semantic token
for the name `own`; identifiers/keywords otherwise stay with the TextMate
grammar.

This also unblocks symbol colors after an `own` annotation once the
server is rebuilt (the old binary could not parse `own` at all and lost
every later symbol).
2026-11-02 18:55:15 +08:00
TermiNexus c37b8dc52c feat(obs): drop default Box[T], add explicit own T ownership
Revert the implicit boxing pass: a bare `T` annotation is a raw value
again (`_U: U = new U(...)` fails with a pointer/value CastError), and
struct layouts are untouched.

`own T` is the explicit owned reference:
- resolves to one pointer layer (`Ptr(T)`); `own T?` stays nullable.
- a variable declared `own` is registered as an owned slot; a `new`
  initializer is always taken over (outside the NLL frame-confined
  predicate), so scope exit runs `__del__` + `free`.
- `new stack` is rejected for `own` (its storage dies with the frame).
- `own` bindings reject `None`; moves (`b: own T = a`) release the source
  slot.
- a bare identifier named `own` still works as a name/type.

`Box[T]`, `RawT[T]`, `Option[T]` / `T?` stay available as explicit forms.
LSP completion lists `own`.

Probe: `struct U { ..., n: own U }` resolves and `U.__sizeof__()` = 24;
`_U: own U = new U(1,2,3,4); _B: own U = _U` runs and `print(_U.a)` fails
with LifetimeError; `own U = None`, `own U = new stack U(...)` fail fast;
without `own` the old CastError returns; `own` works outside OBS too.
Self-host 110/110; ListTest, GenericTest, GenericTest3, GenericTest4,
PtrOpProbe, HashLibTest, NewTest, NewLifetimeTest, StructTest and
EscapeOkTest pass.
2026-11-02 18:26:00 +08:00
TermiNexus 1581aa0d84 feat(obs): default T to Box[T], explicit Box/RawT/Option, move analysis
Rename the per-file opt-in from `__cwa__` to `from __future__ import __obs__`
(internal StrictCWA becomes StrictOBS).

OBS type model:
- `Box[T]` / `BoxT[T]` is one owned indirection (`Ptr(T)`); a user generic
  class named Box keeps precedence.
- `RawT[T]` is the unboxed value type; primitives, arrays and pointers stay raw.
- `Option[T]` is the explicit spelling of the existing `T?` suffix
  (parser marker renamed to OptionT; NullableT still accepted) and resolves
  to a nullable pointer.
- Declaration annotations (locals, globals, parameters, returns) get an
  `AutoBoxT[...]` wrapper in OBS files before the vpi pass, so struct/class
  types become `Box[T]` while primitives stay raw; struct field declarations
  are left untouched, preserving the layout (`U.__sizeof__()` stays 16).
  Explicit `Box[...]`/`RawT[...]`/`Option[...]`/`T?` are not wrapped again.

Ownership:
- an OBS move (`b: T = a` where `a` owns a heap object) marks the source
  slot as released, so later uses (including `a.field`) fail fast; rebinding
  clears the state.

LSP: `Box`, `RawT` and `Option` are offered as builtin completions.

Probe: `_U: U = new U(1,2,3,4); _B: U = _U; print(U.__sizeof__())` prints 16 and
`print(_U.a)` fails with LifetimeError; Box[U]/Option[U]/U? declarations work;
`Box[U] = None` is rejected; `__cwa__` is rejected and `__obs__` accepted.
Self-host 110/110; ListTest, GenericTest, GenericTest3, GenericTest4,
PtrOpProbe, HashLibTest, NewTest, NewLifetimeTest, StructTest and
EscapeOkTest pass.
2026-11-02 18:10:34 +08:00
TermiNexus 8c72adde88 feat(gen): nullable T? types and non-null analysis in strict CWA files
A strict CWA file (`from __future__ import __cwa__`) now distinguishes
nullable from non-nullable types:

- `name: T?` resolves to `Ptr(T)` and records the slot as nullable
  (a per-function slot table, like the released-slot table).
- assigning `None` to a non-nullable declaration, plain assignment,
  annotated attribute/subscript target or return type fails fast with a
  TypeError; `T?` accepts it.
- class-name extraction unwraps `NullableT[...]`, so field lookup,
  method dispatch and `AnnotClassName` still see the class.

Non-strict files keep the previous behavior (`T* = None` is accepted);
`T?` also parses there as `Ptr(T)`, it just carries no enforcement.

Probe: strict `p: A? = None` / `p = new heap hlife A()` / `p = None`
works and null comparisons run; `p: A = None`, `p: A* = None`,
`q = None` on a non-nullable variable and `-> T*` + `return None` all
fail fast; `-> A?` and field `next: A?` work; the same `T* = None`
still compiles outside strict mode. Self-host 110/110; ListTest,
GenericTest3, PtrOpProbe, HashLibTest, NewTest, NewLifetimeTest,
StructTest and EscapeOkTest pass.
2026-11-02 15:47:36 +08:00
TermiNexus 5f981d7878 feat(gen): del obj reclaims a heap object (__del__ then free)
`del name` now destroys a class/struct instance:

- requires a pointer variable (T*) to a class/struct; value-typed T and
  non-struct types fail fast,
- rejects frame-allocated pointers (`new stack` / borrows) and repeat
  `del` of the same binding,
- calls `__del__` (MRO-aware, skipped for a null pointer) and then
  `free`s the object,
- cancels the automatic scope-exit release when the binding is a
  compiler-owned `slife` object, so the destructor and free still happen
  exactly once,
- marks the slot released: later reads, field access, subscript access
  and `&name` fail with a LifetimeError until the variable is rebound
  (rebinding clears the mark, as with willFree).

The released-variable diagnostic now names both causes (`del` or
`@life(willFree(...))`) and drops the obsolete `c.deref(p)` spelling.

Probe: hlife object prints `__del__` output once and exits 0; slife
object prints once (no second call at scope exit); field read/write and
plain use after `del` fail fast; rebound variable is usable again.
Self-host 110/110; ListTest, GenericTest3, PtrOpProbe, HashLibTest,
NewTest, NewLifetimeTest, StructTest and EscapeOkTest pass.
2026-11-02 15:15:20 +08:00
TermiNexus d9511a54ea fix(gen): keep new T() construction in the storage it allocated
For a plain struct with no field defaults and no constructor arguments,
the `A()` call path returned `zeroinitializer` directly instead of
constructing into the storage provided by `new`. Consequences for
`p: A* = new A()`:

- the malloc block was leaked,
- the struct value was materialized into an entry alloca by coercion,
  so `p` pointed at the frame,
- the slife scope-exit free then freed a stack address (heap corruption / 0xC0000374).

Reuse the constructor path whenever a `new` storage descriptor is
pending, so `_translate_struct_ctor` zero-initializes the heap block and
returns its pointer.

Also guard the compiler-owned `new ... slife` binding: the target slot
must be a pointer variable, otherwise a value-typed `T` target would make
the scope-exit free load the struct value and bitcast it to a pointer.

Probe: `new heap A()`, `new A()`, `new stack A()` and `new heap hlife A()`
on a field-only class all print the expected values and exit 0; the
generated IR frees the malloc pointer instead of a stack slot.
Self-host 110/110; ListTest, GenericTest3, PtrOpProbe and HashLibTest
pass; NewTest, NewLifetimeTest, StructTest, EscapeOkTest and
OopInheritTest pass. NewAstTest and GenericOopTest fail the same way on
the previous compiler (pre-existing).
2026-11-02 14:45:44 +08:00
TermiNexus 77e879ef18 fix(gen): correct two latent lookup bugs exposed by the new call order
- ExprGenOps called `_lookup_module_sha1` on ExprGenAttr, but the method
  lives on CallGenBase. With ExprGenOps now translated before the
  callee's stub/text.ll is available, the receiver lookup failed with
  "method return type cannot be obtained precisely":
  `cls=ExprGenAttr ... _lookup_module_sha1`. Use
  `self.Gen.CallGenBase._lookup_module_sha1`.
- VpiReader._lookup_vpi_ret_annot's generic-specialization fallback used
  `sha1_len` without declaring it. The new `list[int].set` return-type
  lookup reaches that fallback and failed with
  `NameError: undefined name 'sha1_len'`. Compute
  `sha1_len = strlen(src_sha1)` before the cache scan.

Self-host 110/110 (two generations); ListTest, GenericTest3, PtrOpProbe
and HashLibTest pass.
2026-11-02 13:48:28 +08:00
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 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 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