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.
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.
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.
`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.
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.
`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.
--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.
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
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
逃逸主报文由三段长从句压缩为「frame-local + 悬垂 + 修法」两句:
stack address escapes: `&x` is frame-local; using it at {where} hands out a dangling pointer
once this frame returns. Copy the value, or allocate on the heap (`new heap hlife ...`).
同步缩短 where 短语(return value / escaping callee|method parameter / constructor parameter stored
into a field / longer-lived field-element / module-level global)
其余冗长报文一并精简:willFree 失效、slife 手动 free/del、async slife、stack+hlife 矛盾、三处容量上限、
@life 标签实参缺失
测试只断言关键字 LifetimeError,不受文案影响(Test/*.py 已确认)。