Commit Graph

47 Commits

Author SHA1 Message Date
agra
5afbc65414 fix(backend): float != must be UNORDERED so nan != nan is true [F0.9]
emitCmpNe lowered float `!=` to `LLVMRealONE` (ordered not-equal), which
is false when either operand is NaN. That made `nan != nan` false in
native code — breaking the canonical `x != x` NaN test, making `!=`
non-complementary with `==` for NaN, and disagreeing with the interpreter.

Change the float predicate to `LLVMRealUNE` (unordered not-equal): true
if either operand is NaN OR they are unequal. For all non-NaN operands
`UNE` ≡ `ONE`, so only NaN-involving comparisons change (toward correct).
The integer predicate (`LLVMIntNE`) and `emitCmpEq` (`OEQ`) are unchanged,
so `nan == nan` stays false and `!=` is now the exact complement of `==`.

- Regression: examples/0150-types-float-ne-unordered-nan.sx (fails before,
  passes after; also pins #run/comptime == runtime agreement).
- specs.md: documents float comparison / NaN semantics (Operators).
- Resolves issue 0091 (issues/0091-float-ne-ordered-nan.md).
2026-06-04 17:04:41 +03:00
agra
04f46ef384 feat(lang): integer numeric-limit accessors (s64.max, u8.min, s3.max) [NL.1]
A field-like access on a builtin INTEGER type name folds to a compile-time
constant of the queried type, driven by (width, signedness) arithmetic:
  sN: min=-(2^(N-1)), max=2^(N-1)-1;  uN: min=0, max=2^N-1
for every width s1..s64 / u1..u64 (not just power-of-two), plus usize/isize.

- type_resolver.zig: extract the single width parser (parseWidthInt) reused by
  resolveNamed AND the new accessors (no second parser — issue-0083 class);
  add resolveBuiltinName / integerWidthSign / integerLimitBits / integerLimitFor.
- lower.zig: lowerNumericLimit intercept beside the error.X / Struct.CONST /
  pack-arity identifier-receiver intercepts; folds ints via constInt, emits a
  clean diagnostic for a non-numeric receiver (bool/string/void/Any/noreturn),
  falls through for floats (NL.2).
- expr_typer.zig: mirror the result type so inferExprType reports the queried type.
- program_index.zig: recognize the accessors in the comptime-int / array-dim path
  so [u8.max]T (255) / [s16.max]T (32767) work; [u64.max]T is rejected oversized.
- u64.max / usize.max stored as the all-ones bit pattern with TYPE u64 (i64 -1),
  asserted via union { u: u64; s: s64 } reinterpret.

Docs: specs.md numeric-limits subsection (formulas + result-type + u64 note);
readme.md language overview. Examples 0148 (positive) / 0149 (negative-receiver).
Unit tests for the value computation in type_resolver.test.zig.

Gate: zig build, zig build test (359/359), tests/run_examples.sh (416 ok, 0 failed).
2026-06-04 16:14:06 +03:00
agra
c01ece5483 docs(spec): make the count zero-rule context-dependent per consumer (0083)
The count description claimed every count must be "positive integral",
which is wrong: zero is context-dependent. Verified at HEAD — an array
dimension (`[0]s64`) and a generic value-param count (`Box(0)`, $N:u32)
both accept zero as a length-0 instantiation, while a `Vector` lane
count stays strictly positive (`Vector(0,f32)` rejected). Negatives are
rejected for array dims and unsigned value-params, but a signed
value-param accepts a negative; only the integral requirement (folds
4.0, rejects 4.5) is common to all three.

Split the count paragraph into per-consumer bullets stating the exact
range each accepts. Range-bound paragraph unchanged. Pin the zero
contrast with examples 0147 (array-dim + value-param zero accepted) and
1505 (Vector zero-lane rejected). No compiler-code change.
2026-06-04 15:32:48 +03:00
agra
0d29f2c286 docs(spec): split range bounds from counts; pin inline-for range semantics (0083)
specs.md lumped `inline for` / `for` range bounds in with counts (array
dimension, Vector lane count, generic value-param count) under the
count negative-rejection rule. A range bound is a range ENDPOINT, not a
count: negative endpoints are valid and an empty/inverted range runs zero
iterations. The compiler already implements this correctly (Agra ruling:
spec-text bug, no code change).

- specs.md: counts and range bounds are now described separately. Counts
  reject negatives; bounds accept any compile-time integer (negatives
  valid, integral floats fold) but still reject a non-integral float
  because the loop cursor must be an integer.
- examples/0612-comptime-inline-for-range-bounds.sx: `inline for -2..1`
  and `for -2..1` both sum -3; `inline for 0..(-2.0)` runs zero
  iterations (empty range). Runtime/comptime parity asserted.
- examples/1138-diagnostics-inline-for-non-integral-bound.sx: a
  non-integral float bound `inline for 0..4.5` is a clean diagnostic,
  exit 1 (must-be-integer still applies to bounds).

Count consumers (1132/1133/1134/1135) unchanged and green.
2026-06-04 15:17:33 +03:00
agra
e03c087e5a fix(ir): integral-float counts + range-checked value-param binds (0083)
Item 2 (Agra ruling): a compile-time INTEGRAL float (`4.0`, `N : f64 :
4.0`, `N :: 4.0`) used as an array dimension / Vector lane / generic
value-param count / `inline for` bound now folds to its integer at the
shared leaf — `program_index.floatToIntExact`, used by both the
`.float_literal` arm of `evalConstIntExpr` and `moduleConstInt`. All four
consumers route through the one evaluator, so `[4.0]s64` lays out the same
`[4]s64` uniformly; a non-integral (`4.5`) or negative value stays
rejected by the downstream `foldDimU32` gate. Pass-0 now pre-registers
float-valued module consts for forward-alias parity with int consts.

Item 1: a generic value-param bind (`Box($K: u32)`) never range-checked
the folded arg, so `Box(5_000_000_000)` compiled and ran. The bind now
range-checks against the param's declared type — a `u32` count through the
shared `foldDimU32` gate (making program_index's "single u32 gate for
value-param counts" doc true), any other integer type through the new
`program_index.intTypeRange` — and emits a clean "value N does not fit in
u32 parameter K" otherwise. The declared type is threaded via a new
`TemplateParam.value_type`.

Regressions: examples 0145 (integral-float array dim), 1504 (Vector lane),
0611 (inline-for bound), 0209 (value-param integral-float), 1132
(non-integral float dim rejected), 1133 (negative float dim rejected),
1134 (oversized u32 value-param rejected) + program_index float-fold unit
tests. Gate: zig build, zig build test, 406/0 run_examples.
2026-06-04 13:16:39 +03:00
agra
bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

Compiler:
- Parser records `Block.produces_value` (last stmt is a no-`;` trailing
  expression) + `Block.discarded_semi` (the `;` that discarded a value),
  via `expectSemicolonAfter`. A trailing expression before `}` may now
  omit its `;` (previously a parse error). Match-arm and else-arm bodies
  are built value-producing regardless of the arm `;` (arms are exempt —
  the `;` is an arm terminator).
- Lowering: `lowerBlockValue` / the block-expr path / `inferExprType`
  respect `produces_value`. A value-position block that discards its value
  is a hard error (`lowerValueBody` for function bodies; the value-context
  `.block` path for if/else branches, `catch` bodies, value bindings,
  match arms). Pure-failable `-> !` bodies (value rides the error channel)
  and a value-if whose branches are void are handled without false errors.
- `defer`/`onfail` cleanup bodies lower as statements (void), so a
  trailing `;` there is fine.

Migration (behavior-preserving — output unchanged):
- stdlib + ~210 examples: dropped the trailing `;` on value-position last
  expressions. `format` now ends with an explicit `#insert "return
  result;"` (it relied on `#insert`-as-block-value, which `;` discards).
- Two `main :: () -> s32` examples that relied on the old silent
  default-return got an explicit trailing `0`.
- Rejection snapshots 0412 / 1013 regenerated (their quoted source lines
  lost a `;`); the diagnostics themselves are unchanged.

Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041
(rejection); 3 parser unit tests. Filed issue 0066 (pre-existing
match-arm negated-literal phi-width quirk, surfaced not caused here).

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00
agra
e86e41b719 ERR/E5.3: specs.md §12 Error Handling (fold locked design into spec)
Add a top-level §12 Error Handling distilling the locked error design +
surface syntax: failable signatures (-> (T,!) / -> ! / multi-value),
named `error { }` + inferred `!` sets, raise/try/catch/or/onfail, the
path-marker rule, set widening, error.X as a value, discard rejection +
flow-check, closures-with-!, return traces, and the u32-last-slot ABI.

Renumber Grammar §12→§13 and Open Questions §13→§14 (insert sits after
§10.5, so §3/§10.5 — the only section numbers referenced from CLAUDE.md
— stay valid). Cross-link the `!` channel from the Keywords list,
Operator Precedence, Function Definition, and §11 Program Structure;
extend the §13 grammar with error_decl, raise_stmt, onfail_stmt, a
catch_expr tier, `try` in unary, and failable type productions.

Pure docs; no compiler change. Gates: build, test, run_examples (293/0).
2026-06-01 18:19:26 +03:00
agra
6b5edc77b4 lang: require ':' before a for-loop range cursor
The cursor clause now matches the collection form's ': (capture)' — 'for 0..N: (i)' instead of 'for 0..N (i)'. The colon is required when a cursor is present; the no-cursor form 'for 0..N { }' is unchanged. Updated examples/200, the pack-index doc comment, and the spec.
2026-05-31 10:57:21 +03:00
agra
d70a7084ff specs: document for-loop by-reference capture (for xs: (*elem))
Covers the *elem pointer binding: zero-copy pass to *T params, write-back via elem.*, value-position auto-deref, and pointer-subject match.
2026-05-31 10:32:05 +03:00
agra
8a875d354c lang F1 2.7: pack-as-value diagnostics (Phase 2 complete)
Using a bare pack name where a runtime value is required was silent garbage
(f(xs)/return xs produced a stray pointer). Now a clear, context-tailored
compile error: isPackName + diagPackAsValue, caught at lowerVarDecl (storage),
lowerReturn (return), lowerFor (iterate), and an identifier-arm catch-all for
call/other. Storage binds a placeholder so there is no cascade error.

Suggestions point at WORKING fixes -- materialize (..xs), or declare the slice
form ..xs: []P for runtime use. The plan category-B "spread ..xs" is broken
(spreading a comptime pack into a []Any param crashes the LLVM verifier; filed
issue 0053), so the diagnostics steer to the slice-of-protocol variadic instead.

Repurposed examples/162-pack-bare-args.sx (was an aspirational bare-$args->[]Any
auto-materialise, contradicting Decision 1) into the slice-form forward
(..args: []Any). examples/203 is the four-category negative test. specs.md "Pack
as value" updated. 238 examples + unit green.
2026-05-30 02:09:41 +03:00
agra
ab572359ae lang: slice-of-protocol variadic ..xs: []P erases each arg to the protocol
packVariadicCallArgs stored the raw concrete arg into a [N x P] array when the
element type was a protocol, so an 8-byte struct landed in a 16-byte {ctx,
vtable} slot -> garbage vtable -> Bus error on dispatch. Now, when the slice
element type is a protocol, each arg is xx-erased to the protocol value via
buildProtocolErasure (same impl-driven machinery as the xx cast). This makes
..xs: []P the runtime, protocol-erased counterpart to the comptime
heterogeneous pack ..xs: P (which stays comptime-only): xs[runtime_i].method()
now works in an ordinary loop.

specs.md: full variadic/pack form-comparison table (concrete-vs-erased,
comptime-vs-runtime). Regression: examples/202. Issue 0052 (FIXED). 237 green.
2026-05-30 01:50:29 +03:00
agra
27c88d4d26 lang F1: range-based for + inline-for unroll over packs
Add range loop syntax:
- runtime  for start..end (i) { }   counting loop, cursor optional, end exclusive
- comptime inline for start..end (i) { }   comptime-unrolled body

The inline form binds the cursor as an int_val comptime constant per
iteration, so xs[i] over a heterogeneous pack substitutes the concrete
per-position element -- the canonical's pack-iteration vehicle
(inline for 0..sources.len (i) { sources[i].addListener(...) }).

- AST: ForExpr.range_end, ForExpr.is_inline
- parser: parseForExpr range vs collection form; suppress_call flag so
  N (i) is not read as a call N(i) while parsing a range bound
- lower: lowerRuntimeRangeFor / lowerInlineRangeFor; evalComptimeInt;
  comptimeIndexOf extends pack-index resolution beyond int literals

Revises spec's inline for i in 0..N to the no-in, range-first, paren-cursor
form. Regression: examples/200-for-range.sx.
2026-05-29 21:36:17 +03:00
agra
934585ac74 lang 2.4: lock protocol-pack access semantics (interface-only)
Design decision: a protocol-constrained pack element is viewed THROUGH the
constraint protocol — only the protocol's interface (its methods, and the
projections xs.T / xs.value) is accessible, not arbitrary concrete members,
exactly like a constrained generic `T: Show`. So `xs[i].v` (a field on the
concrete IntBox, not declared on Show) is an error; the constraint is enforced
and bounds the body regardless of the concrete arg types at a call site.

The previous example 191 demonstrated `xs[i].v` — which only compiled because
the constraint is not yet enforced. Trimmed it to the protocol-agnostic part
that's correct today (per-shape binding + comptime `xs.len` across arities /
heterogeneous shapes); protocol-interface access + projection are the remaining
2.4 work. specs.md records the access rule.
2026-05-29 17:55:11 +03:00
agra
4c15fd55bb specs: add Variadic Heterogeneous Type Packs section
Specs the Feature 1 language surface: the three variadic forms
(`[]T` / `..$xs: []Type` / `..xs: Protocol`), the pack-ops table
(`xs.len`, `xs[i]`, `inline for` index + element forms, projection, and
the four spread targets — call args / tuple value / tuple type / closure
sig), position-driven pack projection with the same-name soft warning,
the tuple spread/projection parallels, N=0 semantics, the pack-as-value
diagnostic rule, tuple-based storage + the impl-driven `xx` requirement,
and the canonical Combined/map example. Cross-references from the Tuple
Types and Closure Type sections.
2026-05-29 12:03:51 +03:00
agra
952dc0e161 ffi: drop legacy name: ..T variadic syntax
Parser hard-rejects the legacy `name: ..T` form with a one-line
migration message pointing at the new `..name: []T` shape. The
leading-`..` form is the one the lowering paths
(`resolveParamType` / `packVariadicCallArgs`) treat as canonical
post-issue-0049; leaving both forms accepted invited the same
class of cross-module emit crashes any time a `..T`-form decl in
stdlib crossed an import boundary.

`specs.md` updated alongside: the Variadic Functions section now
documents `..name: []T` as the surface form, with notes on
homogeneous vs `[]Any` boxing and the `..` spread at call sites.
Inline references to `args: ..Any` in §7 and §8 refreshed.
2026-05-27 21:32:45 +03:00
agra
b710a0a42a lang: xx <lvalue> borrows the operand's storage instead of heap-copying
`xx <struct-typed local>` used to heap-copy the value through context.allocator.
The protocol value's `ctx` pointed at the heap copy; the original local was
left behind, untouched. Mutations through the protocol never reached the
original, and direct reads of the original never saw protocol mutations.
Two-fork bug, silent, easy to write by mistake.

New rule (Option 3 in the discussion):

- `xx <lvalue>` — identifier, field access, index expression, deref —
  borrows the operand's storage. No heap copy, no `free` needed.
- `xx <rvalue>` — struct literal, function-call result, arithmetic, etc. —
  heap-copies through context.allocator. Unchanged from today.
- `xx @ptr` and `xx <pointer-typed value>` — borrows the pointee. Unchanged.

Single switch in `buildProtocolErasure` ([lower.zig:10334](src/ir/lower.zig#L10334))
gated by a new `isLvalueExpr` helper ([lower.zig:10322](src/ir/lower.zig#L10322)).
Struct-typed operand: if the AST shape is identifier/field/index/deref,
emit `lowerExprAsPtr(operand_node)` and skip the heap-copy; otherwise
keep the alloca-store-heap_copy path.

specs.md §3 ownership table extended to three rows (rvalue, lvalue,
pointer) with examples and rationale per row.

Regressions:

- `examples/130-xx-value-routes-through-context-allocator.sx` — the
  Phase 1.1 witness for heap-copy-via-context-allocator. Previous shape
  (`xx <local-value>`) is now a borrow under Option 3 and no longer
  exercises the heap-copy path. Rewritten to use a struct literal
  (`xx ByValue.{...}`) which still heap-copies through context.allocator
  — Tracer.count = 1 as before.
- `examples/135-xx-lvalue-borrows.sx` — new test. Dereferences a
  TrackingAllocator into a stack value, does `xx tracker` inside a
  push Context, and asserts alloc_count/dealloc_count on the LOCAL go
  up. Under old semantics this would have stayed at 0 (heap copy got
  the increments, local stayed stale).

157/157 example tests pass; chess clean on macOS / iOS sim / Android
(`tools/verify-step.sh` ran green immediately before this work).
2026-05-25 15:23:13 +03:00
agra
72593db953 mem: List(T) mutations gain optional alloc: Allocator = context.allocator
The chess panel-text regression (text vanished after the first move on
macOS) had a single root cause: GlyphCache's entries List, hash table,
and shaped_buf grew through `context.allocator` — which during render
is the per-frame arena. On the next arena reset the backing died, and
subsequent glyph lookups read garbage / wrote into freshly-allocated
view-tree memory.

Fix is shaped as the user proposed: `List(T)`'s mutations take an
optional trailing `alloc: Allocator = context.allocator` argument. No
allocator stored on the container, no init ceremony, every existing
`list.append(item)` callsite keeps working unchanged. Long-lived
owners now write `list.append(item, self.parent_allocator)` and the
arena-leak bug becomes impossible to write accidentally.

Default-arg substitution previously only fired for identifier callees
(`expandCallDefaults` at lower.zig:7978). Extended to the generic
struct-method dispatch path (`list.append(...)` lands here) via a new
`appendDefaultArgs` helper that lowers fd.params[i].default_expr in
the caller's scope and appends to the lowered args slice.

Long-lived owners updated to capture `parent_allocator: Allocator` at
init and use it for every internal growth:

- GlyphCache (the chess bug) — entries, shaped_buf, hash_keys,
  hash_vals, atlas bitmap.
- DockInteraction — drops the existing `push Context` workaround in
  `ensure_capacity` for the explicit-arg form.
- StateStore — entries list + per-entry data buffer.
- Gles3Gpu, MetalGPU — shaders, buffers, textures (atlas-grow during
  render would otherwise leak resources into the frame arena).

Also kept: an operator-precedence fix in pipeline.sx
(`(self.frame_index & 1) == 0` instead of
`self.frame_index & 1 == 0`, which parses as
`self.frame_index & (1 == 0)` = always 0). That was a stealth
single-arena-only bug that masked the GlyphCache one for a long time.

Docs:
- specs.md §11 documents `param: T = expr` default parameter values.
  The parser already supported it — formalised in the spec now.
- current/CHECKPOINT-MEM.md logs the change.
- CLAUDE.md REJECTED PATTERNS gains a "Long-lived containers growing
  through context.allocator" section with the `parent_allocator`
  capture template and the list of existing examples to mirror.

155/155 example tests pass — zero-diff against snapshots since every
existing callsite still resolves to `context.allocator`.
2026-05-25 14:41:17 +03:00
agra
4c6c29b299 specs: §10.5 Bundling and Post-Link Callbacks
Documents the post-link callback model that the bundling-in-sx campaign
landed (Weeks 6 + 7):

  - Explicit opt-in via `BuildOptions.set_post_link_callback(fn)` or
    `set_post_link_module(name)` from a user `#run` block. No stdlib
    default; no implicit prelude. CLI `--bundle` / `--apk` auto-fallback
    to `post_link_module = "platform.bundle"` so existing CLI invocations
    keep working without an in-source registration.

  - `BuildOptions` surface: setters (link_flag / framework / output_path
    / wasm_shell / asset_dir / post_link_callback / post_link_module /
    bundle_path / bundle_id / codesign_identity / provisioning_profile /
    manifest_path / keystore_path) + accessors (binary_path / target_triple
    / is_macos / is_ios / is_ios_device / is_ios_simulator / is_android /
    framework / framework_path / jni_main / asset_dir families). Returned
    strings are "" when unset; counts are 0.

  - `fs.sx` / `process.sx` stdlib modules. Both work in two execution
    contexts: at runtime via the dynamic linker, and at #run / post-link
    via `src/ir/host_ffi.zig`'s dlsym(RTLD_DEFAULT) trampolines.

  - Per-target Apple `.app` flow: stage + Info.plist (macOS minimal vs
    iOS-shaped UIDeviceFamily/LSRequiresIPhoneOS/UIApplicationSceneManifest/
    DTPlatformName) + provisioning embed (iOS device) + Frameworks/ embed
    (iOS) + entitlements extraction (`security cms` + 3× `plutil`) +
    codesign with --entitlements when present.

  - Android `.apk` flow: SDK discovery → highest build-tools / platforms
    via `ls -1 | sort -V | tail -1` → stage lib/arm64-v8a/<libfoo.so> →
    manifest synth (NativeActivity vs `#jni_main` Activity) → javac + d8
    per `#jni_main` decl → aapt2 link → zip lib/dex/assets → zipalign →
    keytool debug keystore (first use) → apksigner sign.
2026-05-23 01:35:05 +03:00
agra
f9ecf9d00e iOS lock step keyboard + metal 2026-05-18 17:40:10 +03:00
agra
67e02a20a5 ... 2026-03-04 09:18:24 +02:00
agra
6c5672c7df trailing commas 2026-03-03 09:42:01 +02:00
agra
bbb5426777 sm 2026-03-02 21:00:55 +02:00
agra
566121c45a more forward declarations 2026-02-24 17:37:52 +02:00
agra
b98711a1d3 imports 2026-02-24 13:37:27 +02:00
agra
170e236764 vtables, protocol 2026-02-24 06:20:38 +02:00
agra
0cc7b69441 closures 2026-02-23 13:45:44 +02:00
agra
1cc67f9b5a optionals 2026-02-22 22:16:30 +02:00
agra
6f927361aa pipes 2026-02-20 13:28:38 +02:00
agra
1ecac79642 bit ops 2026-02-20 12:12:51 +02:00
agra
e0e655cd36 tuples 2026-02-19 01:26:04 +02:00
agra
fbf8a62362 comptime format 2026-02-18 18:57:51 +02:00
agra
4aff004118 http server 2026-02-17 19:49:01 +02:00
agra
4fd87309d9 http server 2026-02-17 16:57:12 +02:00
agra
c8ceceed0f ... 2026-02-16 01:58:30 +02:00
agra
58e2a5bdb1 multiple assign 2026-02-16 01:13:34 +02:00
agra
fb60818424 for arr: (it) {} 2026-02-16 00:55:03 +02:00
agra
e7d2abdf0c layout 2026-02-14 21:28:49 +02:00
agra
d61c6488f3 @ enum type 2026-02-14 14:52:39 +02:00
agra
fe7efeadb0 flags 2026-02-14 14:03:22 +02:00
agra
025b790411 enum, union 2026-02-14 13:17:22 +02:00
agra
4ff828fd1a sdl phase 2 2026-02-12 21:37:25 +02:00
agra
dab162bfe4 strings 2026-02-12 16:23:42 +02:00
agra
25e1372731 extend default to s64 2026-02-11 01:05:21 +02:00
agra
70435d3c85 pointers 2026-02-10 22:47:43 +02:00
agra
ef14144d49 ... 2026-02-10 21:03:56 +02:00
agra
3fde080092 quick sort 2026-02-10 18:58:04 +02:00
agra
55fc5790e4 so... jai :D 2026-02-09 18:07:41 +02:00