Commit ecc91f4
feat(vectorization): broaden tuple operators and unify on a per-position model 📐
Vectorization — applying operators element-wise across tuples like
`(1, 2) + (3, 4) → (4, 6)` — previously only fired for binary numeric
operations and used a uniform "one scalar overload covers every
position" model. This PR broadens the scope and refactors the dispatch
to a single principle:
> A vec call is a tuple of independent scalar dispatches, one per
> element position.
User-visible changes
- Unary, n-ary, and non-numeric operators all vec now. `-(1, 2, 3)`,
`("a", "b") ++ ("c", "d")`, `([1], [2]) ++ ([3], [4])`, `(1, 2) + 5`
all work where they previously errored.
- Heterogeneous tuples dispatch per position:
`([1,2,3], "foo") ++ ([4,5,6], "bar")` evaluates element 0 via
`++(List, List)` and element 1 via `++(String, String)`, yielding
`([1,2,3,4,5,6], "foobar")`.
- Chained operator calls keep precise types — the analyser no longer
widens `Tuple<Int, Int> - Tuple<Int, Int>` to `Any`.
- Compound assignment (`+=` etc.) on tuple lvalues type-checks
correctly.
- Mixed-element tuples with no overload at some position
(`(1, "a") + (2, "b")`) fail at compile time with the failing
position called out, instead of crashing mid-iteration at runtime.
- Element-call failures wrap with `"while vectorising '<name>' at
index N"` to preserve outer-call context.
- Vectorization is gated on operator syntax — `id((1, 2, 3))` still
returns the tuple verbatim and never element-wise calls `id`.
Implementation
- Parser: `Expression::Call` carries an `operator_form: bool` flag set
by the desugaring sites for infix and prefix-unary operators.
- Binding shape: every `Binding::Resolved` / `Binding::Dynamic` entry
is a `Candidate { var: ResolvedVar, vectorized: bool }`.
- Analyser: per-position resolution walks the scope chain once per
tuple position. `Binding::Resolved(vec)` only when every position
pins to the same scalar; otherwise `Binding::Dynamic` with the
union of per-position candidates. Result types are computed
element-by-element so heterogeneous-vec calls keep precise tuple
types.
- Runtime: `Callable::Vec { candidates, axis_len }` carries the full
scalar list. `dispatch_vec_call` resolves a scalar per element
position via the same `matches_value_args` lookup the scalar path
uses, so the analyser and runtime can't drift on what counts as
"applicable".
- Dead code removed: `BinaryOperator::supports_vectorization`,
`StaticType::supports_vectorization{,_with}`, `synthetic_vec_sig`,
`candidate_is_compat`, `static_vec_axis_len`, `vec_candidate_applies`,
and the old `try_vectorized_call` fallback path.
Design and docs
- Full RFC at `docs/design/vectorization.md`.
- User-facing behaviour documented in
`manual/src/reference/types/tuple.md`.
Tests
- 12 new functional `.ndc` programs covering unary vec, non-numeric
vec, list-element vec, mixed-numeric promotion, chained-precision,
compound-assignment aliasing, regular-call no-vec, exact-match
precision, heterogeneous vec, per-position no-overload error.
- 1 new regression program for the `Binding::Dynamic` misinference
the original PR caught.
- 5 new unit tests for the pure helpers.
Benchmarks (release-with-debug, 10 runs)
- AoC 2025/08 part1: 489ms master → 578ms branch (+18%).
- AoC 2025/08 part2: 816ms master → 885ms branch (+8%).
- Non-vec benches: ~20% faster on `enumerate_for_loop.ndc` and
`hof_pipeline.ndc` from precision recovery on operator chains;
others within ±4% noise.
The vec-loop slowdown is the cost of per-element correctness checking
(the old probe-first approach miscoupled mixed-element tuples). The
RFC's deferred compile-time unrolling of `Resolved(vec)` is the
natural follow-on perf PR.
Closes #145.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>1 parent cf6dac0 commit ecc91f4
27 files changed
Lines changed: 1300 additions & 213 deletions
File tree
- docs/design
- manual/src/reference/types
- ndc_analyser/src
- ndc_bin/src
- ndc_core/src
- ndc_lsp/src
- ndc_parser/src
- ndc_vm/src
- value
- tests/functional/programs
- 013_vector_math
- 900_bugs
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
Large diffs are not rendered by default.
0 commit comments