Skip to content

Commit ecc91f4

Browse files
timfennisclaude
andcommitted
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

docs/design/vectorization.md

Lines changed: 370 additions & 0 deletions
Large diffs are not rendered by default.

manual/src/reference/types/tuple.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,29 @@ assert_eq(b, (1,2,3,4,5));
4343
## Operators
4444

4545
{{#include ../../snippets/list-operators.md}}
46+
47+
## Vectorization
48+
49+
Operators broadcast element-wise over tuples. Both arguments must be tuples
50+
of the same length, or one side may be a scalar that broadcasts:
51+
52+
```ndc
53+
assert_eq((1, 2) + (3, 4), (4, 6));
54+
assert_eq(-(1, 2, 3), (-1, -2, -3));
55+
assert_eq((1, 2) + 5, (6, 7));
56+
assert_eq(("a", "b") ++ ("c", "d"), ("ac", "bd"));
57+
```
58+
59+
Vectorization only kicks in for operator syntax (`a + b`, `-x`,
60+
`a ++ b`, etc.). Regular function calls never broadcast, so
61+
`f((1, 2, 3))` passes the whole tuple to `f` and does not call `f`
62+
once per element.
63+
64+
Mixed-element tuples or length mismatches error rather than silently
65+
producing wrong results:
66+
67+
```ndc
68+
(1, 2, 3) + (4, 5) // ERROR: no overload matches
69+
(1, "a") + (2, "b") // ERROR: no overload accepts both pairs
70+
```
71+

ndc_analyser/src/analyser.rs

Lines changed: 235 additions & 38 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)