docs: PLAN-RACE (race over M:1 tasks) + file issue 0196
Plan the `race` async deliverable (roadmap A1): a structured first-wins over the M:1 `Task` layer, returning a comptime-synthesized tagged-union mirroring the input named tuple's labels. Identifies the one net-new compiler primitive needed — `pointee($P: Type) -> Type` (project `*Task(A)` → `A`); everything else (tuple reflection, union synthesis, the suspending runtime) is already in place. Issue 0196: a named-tuple type ALIAS (`NT :: Tuple(a: i64, b: bool)`) loses its structure — field access and reflection both fail on the alias, while the inline and `$T`-type-parameter forms work. Not on the race critical path (race reflects a tuple type parameter), filed for tracking.
This commit is contained in:
73
current/PLAN-RACE.md
Normal file
73
current/PLAN-RACE.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# PLAN-RACE — Stream B2/A1: `race` over the M:1 fiber scheduler
|
||||||
|
|
||||||
|
Carved from the async roadmap ([../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
|
||||||
|
§4.5, §4.6, §7 step 3). The headline A1 feature still missing after Stream B1: **`race`** — start N
|
||||||
|
async tasks, return when the FIRST completes, and **structurally** cancel + join the losers before
|
||||||
|
returning. The result is a synthesized tagged-union mirroring the input named tuple's labels.
|
||||||
|
|
||||||
|
```sx
|
||||||
|
fa := s.go(() -> A => read_a(conn)); // *Task(A)
|
||||||
|
fb := s.go(() -> B => read_b(conn)); // *Task(B)
|
||||||
|
winner := s.race((a: fa, b: fb)); // RaceResult = enum { a: A; b: B }
|
||||||
|
if winner == {
|
||||||
|
case .a: (v) { handle_a(v); } // v : A (fb cancelled + joined)
|
||||||
|
case .b: (v) { handle_b(v); } // v : B (fa cancelled + joined)
|
||||||
|
}
|
||||||
|
// positional form: s.race((fa, fb)) → tags ._0 / ._1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design decisions (grounded against the tree, 2026-06-26)
|
||||||
|
|
||||||
|
- **Built over the M:1 `Task` layer, NOT `context.io`/`Future`.** The suspending async is
|
||||||
|
`sched.go`/`wait`/`cancel` over `*Task($R)` (B1.4a); `context.io.async`→`Future` is the BLOCKING
|
||||||
|
impl (workers run inline → racing is meaningless there). The roadmap's "Future" maps to our
|
||||||
|
`*Task`. `race` is a `Scheduler`/`Task` UFCS function in `library/modules/std/sched.sx`.
|
||||||
|
- **The result is a comptime-synthesized nominal tagged-union** (`RaceResult`), one variant per input
|
||||||
|
tuple element: variant NAME = the tuple label (positional → `_0`/`_1`), payload = the task's result
|
||||||
|
type. Synthesis uses the proven `declare`/`define`/`make_enum` + `field_count`/`field_name`/
|
||||||
|
`field_type` reflection (examples 0619–0623, 0646). The input arrives as an inferred type PARAMETER
|
||||||
|
`$T` (a named tuple of `*Task(..)`), which reflects correctly (issue 0195 fixed; the tuple-*alias*
|
||||||
|
gap is issue 0196, NOT on this path).
|
||||||
|
- **Cancellation rides the existing cooperative `Task.cancel`** (sets the flag + `.canceled` state).
|
||||||
|
`race` cancels every loser, then `wait`s each (joins) so no loser fiber outlives the `race` call —
|
||||||
|
structured. Reuses `suspend_self`/`wake`; no new scheduler machinery.
|
||||||
|
|
||||||
|
## The one net-new compiler primitive (step 1)
|
||||||
|
|
||||||
|
**`pointee($P: Type) -> Type #builtin`** — given a pointer type `*X`, return `X`. This is the only
|
||||||
|
missing reflection capability: `race` must project each tuple element `*Task(A)` to its result type
|
||||||
|
`A`, and there is currently NO way to get a pointer's target type at comptime (`field_count(*X)`=0,
|
||||||
|
`type_info` has no pointer variant). With it the projection is pure sx:
|
||||||
|
```sx
|
||||||
|
TaskResult :: ($P: Type) -> Type { // P = *Task(A) → A
|
||||||
|
return field_type(pointee(P), 0); // pointee → Task(A); field 0 = `value: A`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Small + generally useful (reflection is currently complete for aggregates but blind to pointer
|
||||||
|
targets). Mirror `field_type`'s `#builtin` plumbing (`src/ir/lower/call.zig` + `src/ir/calls.zig`),
|
||||||
|
backed by the pointer TypeInfo's pointee TypeId (`src/ir/types.zig`). Lock with a comptime example.
|
||||||
|
|
||||||
|
## Steps (each: implement → lock with an example → `zig build test` green → both platforms)
|
||||||
|
|
||||||
|
1. **`pointee` reflection builtin.** Add `pointee($P: Type) -> Type` (core.sx + compiler). Example:
|
||||||
|
`pointee(*i64)` = `i64`, `field_type(pointee(*Task(i64)), 0)` = the task value type. (worker+review)
|
||||||
|
2. **`RaceResult($T) -> Type` synthesis.** Type-fn: reflect the named-tuple `$T` of `*Task(..)`,
|
||||||
|
project each element via `TaskResult`, mint the tagged-union (labels → variants). Comptime-only
|
||||||
|
example asserting the minted type's `field_count`/`field_name` match the input tuple.
|
||||||
|
3. **`Task.Value` projection + result construction.** Confirm a winner's value can be boxed into the
|
||||||
|
minted variant by label/index (uses the existing variant-construction path).
|
||||||
|
4. **Runtime `race(tasks: $T) -> RaceResult(T)`.** Suspend the caller until the first task is
|
||||||
|
`.ready`; build the winner variant; then cancel + `wait`-join every loser before returning.
|
||||||
|
Single-winner (first by completion order; FIFO tiebreak). Example: 2 tasks, deterministic winner
|
||||||
|
via `sleep` ordering (like 1817), asserting the loser is cancelled + joined.
|
||||||
|
5. **Positional tuple form** (`._0`/`._1`) + edge cases (already-ready task → immediate, single-task
|
||||||
|
race, all-cancelled). Examples.
|
||||||
|
6. **Validate** every new example byte-identical on aarch64-macOS host AND aarch64-linux container;
|
||||||
|
full `zig build test` green; adversarially review each step.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- **Step 0 (prereq) DONE:** issue 0195 (tuple/array/vector field reflection) fixed + committed
|
||||||
|
(`8ac6c573`). Tuple reflection works on inline + `$T`-param forms. Issue 0196 (tuple *alias*) filed,
|
||||||
|
not on the critical path.
|
||||||
|
- **NEXT: Step 1 — the `pointee` builtin.**
|
||||||
71
issues/0196-named-tuple-alias-loses-structure.md
Normal file
71
issues/0196-named-tuple-alias-loses-structure.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Issue 0196 — a named-tuple type alias (`NT :: Tuple(a: i64, b: bool)`) loses its structure
|
||||||
|
|
||||||
|
Status: **OPEN.** Found while building `race` (does NOT block it — `race` reflects a tuple type
|
||||||
|
PARAMETER `$T`, which works; this is the narrower *named alias* path). Filed so the inconsistency is
|
||||||
|
tracked.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Binding a named tuple type to a `::` alias and then using the alias drops the tuple's field
|
||||||
|
structure. Both field access and comptime reflection fail on the alias, even though the identical
|
||||||
|
**inline** type and a **type-parameter** `$T` bound to the same type both work.
|
||||||
|
|
||||||
|
| form | `field_count` / `field_name` | field access `x.a` |
|
||||||
|
|---|---|---|
|
||||||
|
| inline `Tuple(a: i64, b: bool)` | ✓ works (`fc=2`, `name0=a`) | n/a |
|
||||||
|
| type param `($T: Type)` ← `Tuple(a: i64, b: bool)` | ✓ works (`fc=2`, `name0=a`) | n/a |
|
||||||
|
| **alias `NT :: Tuple(a: i64, b: bool)`** | ✗ `error: unresolved type: 'NT'` | ✗ `error: field 'a' not found on type 'NT'` |
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
NT :: Tuple(a: i64, b: bool);
|
||||||
|
|
||||||
|
main :: () -> i32 {
|
||||||
|
// (1) field access through the alias fails:
|
||||||
|
x : NT = .(a = 1, b = true);
|
||||||
|
print("x.a={} x.b={}\n", x.a, x.b); // error: field 'a' not found on type 'NT'
|
||||||
|
|
||||||
|
// (2) reflection through the alias fails:
|
||||||
|
print("fc={}\n", field_count(NT)); // error: unresolved type: 'NT'
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrast — both of these work today:
|
||||||
|
```sx
|
||||||
|
// inline (see examples/comptime/0646-comptime-field-reflect-tuple-array.sx):
|
||||||
|
field_count(Tuple(a: i64, b: bool)); // 2
|
||||||
|
field_name(Tuple(a: i64, b: bool), 0); // "a"
|
||||||
|
|
||||||
|
// type parameter:
|
||||||
|
count :: ($T: Type) -> i64 { return field_count(T); }
|
||||||
|
count(Tuple(a: i64, b: bool)); // 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes / investigation prompt
|
||||||
|
|
||||||
|
> A `::` alias of a named tuple type (`NT :: Tuple(a: i64, b: bool)`) doesn't behave like the inline
|
||||||
|
> named tuple: `x : NT` then `x.a` reports "field 'a' not found on type 'NT'", and `field_count(NT)` /
|
||||||
|
> `field_name(NT, i)` report "unresolved type: 'NT'". The inline form and a generic `$T` type
|
||||||
|
> parameter bound to the same `Tuple(...)` both work, so the named-tuple TypeId is correct — the alias
|
||||||
|
> binding is where the structure (or the resolvability) is lost.
|
||||||
|
>
|
||||||
|
> First determine whether this is intended (tuples are structural / non-nominal per specs.md §3.5, so
|
||||||
|
> perhaps a `::` alias is meant to be spelled differently, e.g. a type-fn `NT :: () -> Type { ... }`)
|
||||||
|
> or a genuine bug in how a `::` type-alias binds a structural tuple type. If it's a bug: the alias
|
||||||
|
> likely registers a forward/opaque type entry that never resolves to the underlying `TupleInfo`
|
||||||
|
> (hence both "unresolved type" in reflection and "field not found" in member access). Check the
|
||||||
|
> type-alias decl lowering path (where `Name :: <type-expr>` binds) and whether a structural tuple
|
||||||
|
> type-expr is resolved + carried vs. left as an unresolved nominal placeholder.
|
||||||
|
>
|
||||||
|
> Also check the POSITIONAL case (`PT :: Tuple(i64, bool)` → `x : PT = .(1, true)`, `x.0`,
|
||||||
|
> `field_count(PT)`) to see whether the breakage is named-tuple-specific or all tuple aliases.
|
||||||
|
|
||||||
|
## Why it doesn't block `race`
|
||||||
|
|
||||||
|
`race` is `RaceResult :: ($T: Type) -> Type` over a tuple type **parameter** (the named tuple arrives
|
||||||
|
as the inferred `$T` of the `race(tasks)` call), and reflection on a `$T` tuple parameter works
|
||||||
|
(verified). The alias form is a convenience that `race` does not depend on.
|
||||||
Reference in New Issue
Block a user