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.
- 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.
- 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.
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.
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.
`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.
- 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.
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.
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.
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.
- `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
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
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