Skip to content

Construct fixed size Vec with a list literal - #96

Merged
sdiehl merged 2 commits into
sdiehl:mainfrom
benjohnbarnes:fix/shape-indexed-literal
Oct 11, 2026
Merged

sdiehl merged 2 commits into
sdiehl:mainfrom
benjohnbarnes:fix/shape-indexed-literal

Conversation

@benjohnbarnes

@benjohnbarnes benjohnbarnes commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Constructing Vec with a list literal

This is just an initial proposal, not a finished patch. It is offered so the idea can be validated before anything is polished. It type-checks and has a test suite, but some points are Outstanding at the end.

The problem

Vec(a, n : Nat) exists and dimensions unify by equality, but nothing can build a vector longer than
one. MkVec is opaque and pinned hidden by stdlib_invariant_constructors_are_hidden; vempty and
vsingle pin lengths 0 and 1; vmap and vzip preserve or demand equality; and a length-changing
vcons needs n + 1, which dimension arithmetic declines. The index is therefore habitable in
principle and unreachable in practice.

The change

A list literal in a Vec position takes its length from the literal's own element count. Nothing new is
spelled — these are ordinary list literals, and the position's type is what gives each one its length.

These compile.

fn takes3(v : Vec(Int, 3)) : Int = 0
fn fixed() : Int = takes3([1, 2, 3])            -- the count is the length

fn takes0(v : Vec(Int, 0)) : Int = 0
fn empty() : Int = takes0([])                   -- and this is the empty vector

fn first(v : Vec(Int, n)) : Int = vhead(v)      -- a variable length takes any count
fn any_length() : Int = first([1]) + first([1, 2]) + first([1, 2, 3])

fn same(v : Vec(a, n)) : Vec(a, n) = vmap(\(x) -> x, v)   -- and a variable element type
fn kept() : Vec(Int, 2) = same([1, 2])

fn count(v : Vec(a, n)) : Int = 0
fn open_element() : Int = count([])   -- compiles: the count solves n, and nothing needs to solve a

These do not compile.

fn takes3(v : Vec(Int, 3)) : Int = 0
fn takes2(v : Vec(Int, 2)) : Int = 0
fn from_list() : List(Int) = [1, 2, 3]

fn wrong_count() : Int = takes3([1, 2])                -- expected length 3, but got length 2
fn a_list() : Int = takes3(from_list())                -- expected Vec(Int, 3), got List(Int)
fn bound_first() : Int =
  let v = [1, 2, 3]
  takes3(v)                                            -- v is a List(Int) before the position is known
fn rigid(v : Vec(Int, n)) : Vec(Int, n) = [1, 2, 3]    -- expected n, got 3
fn mixed() : Int = takes2([1, true])                   -- expected Int, got Bool

fn shown(v : Vec(a, n)) : String = show(vto_list(v))
-- an unsolved element type is fine until something needs an instance of it, and then the
-- message names it: cannot discharge constraint Show(a); add `given Show(a)` to the
-- enclosing function

And these, because the rule is Vec's alone. A user's own type of the same shape — and even one that
spells the same name — is not offered the form:

type Grid(a, n : Nat) = MkGrid(List(a))
fn takes(v : Grid(Int, 3)) : Int = 0
fn user_type() : Int = takes([1, 2, 3])   -- expected Grid(Int, 3), got List(Int)

A size written in the literal still does not parse, which is the alternative this proposal does not
take:

fn sized_literal() : Vec(Int, 3) = [1, 2, 3; 3]   -- unexpected token
fn sized_type(v : [Int; 3]) : Int = 0             -- unexpected token
fn unboxed_type(v : #[Int, 3]) : Int = 0          -- unexpected '[', expected '(', '{'

Why Vec, and what a generalisation could be

Primarily because it is the only type where the check is real. MkVec is opaque and pinned hidden, so
outside its module a literal is the only route to a value — which makes the count the only thing holding
the length honest. The predicate therefore names Data.Vec.Vec, matched on its qualified name in
wired.rs beside the compiler's other standard-library hooks.

Potentially Prism could in future have a more general feature to allow other types to construct from a list (and refuse this at compile time if the list is not a valid length). Swift has ExpressibleByArrayLiteral for this sort of thing. A generalisation like this would hopefully then be additive.

What it touches

One arm in check; one predicate, an identity test on Vec; one per-node fact for elaboration
(NodeRes::ShapeIndexed, following the unboxed_field precedent); and one wrapper in the existing
Expr::List elaboration arm, since the representation is a single List(a) field. Nothing reaches
Core
— dimensions are erased before it, so there is no representation or codegen change.

Tests

tests/language/shape_indexed_literal.rs, wired into tests/frontend.rs: sixteen cases — lengths 0,
1, 2, 3 and 12; a length clash asserted on its wording; a variable length solved from the count;
a rigid length refused; the element lane adopted; both directions of the list/vector boundary; a
user-declared type of the same shape being refused rather than offered the form; and a literal bound
before its position being inferred as a List rather than a vector.

Two of them are runtime cases, and they exist because vto_list destructures the vector's own
constructor: a bare list would fail to match rather than quietly resemble one. That is the property a
checking-only change would get wrong, so it is a committed test rather than a probe.

Outstanding

Nothing below is outstanding any more. Each is kept as the record of what it was, and the branch
carries the fix.

  • docs/src/stdlib/data-vec.md is regenerated rather than hand-edited: prism docs --stdlib --check --out docs/src/stdlib is green on this branch with LLVM 22, and it reported data-vec.md
    as the only page that had gone stale. prism docs --stdlib --test is green with it — 566 doctests
    and 538 expectations, none failed.
  • The suite runs by its committed target. cargo test --test frontend shape_indexed is green on
    this branch — sixteen cases, the two that reach the vector's own constructor included.
  • The double shape_indexed_ctor call is gone: the rule is decided before the match, so the
    predicate is computed once rather than tested by a guard and re-run by the body.

Not part of this

  • Dynamic containers. Bytes and Tensor already have list constructors; making a literal build
    those is a class plus instances plus elaboration, is not about the size at all, and would be
    unchecked by nature.
  • Arithmetic on dimensions. vcons and append stay declined. A literal can write a longer vector
    than the present constructors can; it cannot compute one.

@benjohnbarnes benjohnbarnes changed the title A list literal constructs a shape-indexed type Construct fixed size Vec with a list literal Oct 9, 2026
@benjohnbarnes
benjohnbarnes force-pushed the fix/shape-indexed-literal branch 2 times, most recently from 754a9a3 to ec765c5 Compare October 9, 2026 11:53
A list literal in a Vec position takes its length from the literal's own element count, so takes3([1, 2, 3]) fills a Vec(Int, 3) and [] is the empty vector. A count that disagrees with a written dimension is refused naming both lengths, and a count meeting a variable dimension solves it, so one function serves every arity. Nothing new is spelled: the spelling is the ordinary list literal, and the expected type is what gives it its length, as it already does when a bare 1 adopts a Float context.

The rule is Data.Vec.Vec and nothing else, and that is deliberate. A dimension is erased before Core, so no criterion can tell a Nat that measures a container from one that means something else, and a predicate keyed on a declaration's shape would offer the form to any type with a Nat in it, whatever its author meant by it. Vec is the one type where the offer is worth making, because MkVec is opaque and pinned hidden by stdlib_invariant_constructors_are_hidden: outside its module a literal is the only route to a value, which makes the count the only thing holding the length honest. A user's own type of the same shape keeps its constructor visible, so nothing is checked there either way and the form would only appear to promise something.

What it does not do. Growth stays declined: a literal can write a longer vector than the present constructors can, but it cannot compute one. And a size written in the literal itself is still a syntax error, which is the alternative this does not take.

Outstanding. docs/src/stdlib/data-vec.md is generated from Data/Vec.pr, whose prose this changes, and regenerating it needs the native toolchain. And tests/frontend.rs reaches prism::build, so the fifteen cases in tests/language/shape_indexed_literal.rs were run as a temporary standalone target rather than by the committed one; a checkout with LLVM runs them in place.
@benjohnbarnes
benjohnbarnes force-pushed the fix/shape-indexed-literal branch from ec765c5 to 67debf9 Compare October 9, 2026 11:55
Comment thread docs/src/spec.md Resolved
Comment thread tests/language/shape_indexed_literal.rs Resolved
Comment thread src/wired.rs
/// Those are the only declarations of their name, so a bare name is enough for
/// them; a user is free to declare a `Vec` of their own, and the list-literal
/// rule that reads this must not catch it.
pub(crate) const TY_VEC: &str = "Data.Vec.Vec";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me have a think about this, not sure I'm in love with having qualified literals as wired in types. I think this probably belongs in the desugaring pass.

benjohnbarnes added a commit to benjohnbarnes/prism that referenced this pull request Oct 9, 2026
Review on sdiehl#96 asked for less exposition in the language spec. The
paragraph goes rather than shrinks: the section is about the kinds, and
how a vector is written is described where the form is defined, in
Data/Vec.pr, which is what the library docs are generated from. Nothing
else in the spec changes, so the branch no longer touches spec.md at all.

The fifteen cases spell their prism programs as multiline literals in
place of escaped one-liners, which is how the two runtime cases were
already written. Every embedded program is byte-for-byte what it was, so
nothing about what is checked changes.
@sdiehl

sdiehl commented Oct 10, 2026

Copy link
Copy Markdown
Owner

The lean fuzzer seems like some upstream network timeout unrelated to this PR.

The scoreboard issue can be fixed with just scoreboard and then commit the updated version.

@benjohnbarnes
benjohnbarnes force-pushed the fix/shape-indexed-literal branch 3 times, most recently from 36b995d to ed9d11e Compare October 11, 2026 13:03
Stephen read the proposal and replied. Two of his points were decisions. The
checker is the right home for the rule: he had suggested desugaring, thought
about it, and withdrew that, because desugaring runs before types exist and
cannot pin a length anywhere — there is no expression-level ascription to emit,
and no ordinary function returns `Vec(a, 3)` for an arbitrary count without
dimension arithmetic. And the name-matched version may land first, provided it
says it is a stepping stone toward a conformance a type opts into, as Swift's
`ExpressibleByArrayLiteral` is, with `Vec` as its first instance. Here it says
so, in `wired.rs`.

The rest were things to do. The extension of the language spec is gone rather
than trimmed, since the section it sat in is about the kinds. The test programs
are multiline literals in place of escaped one-liners. The rule is decided once,
before the match, instead of tested by a guard and re-run by the body. A
constructor with no registered tag is an internal invariant rather than a silent
tag `0`. And the comments come down to what the code needs — the module prose with
them: a list literal may be passed where a `Vec` is expected, and a list value
cannot, since a list has no length in its type.

The thing that is not apparent is the inference order, and it is pinned rather
than noted. The rule needs the expected type at the literal's own site, so
`takes3([1, 2, 3])` works while `let v = [1, 2, 3]` and then `takes3(v)` infers a
`List` and is refused as a `List` against `Vec` mismatch. A test holds it, and
`Data/Vec.pr` says it, which is what he asked for.

`docs/src/stdlib/data-vec.md` is regenerated from that prose, since the page is
generated: `prism docs --stdlib --out docs/src/stdlib` changed it and no other
page of the 95, which is the one `--check` had named. `Check stdlib docs` is green
here with LLVM 22, both halves of it.
@benjohnbarnes
benjohnbarnes force-pushed the fix/shape-indexed-literal branch from ed9d11e to 9a54e9d Compare October 11, 2026 13:11
@sdiehl
sdiehl marked this pull request as ready for review October 11, 2026 22:42
@sdiehl
sdiehl merged commit 21e79d2 into sdiehl:main Oct 11, 2026
52 checks passed
@benjohnbarnes
benjohnbarnes deleted the fix/shape-indexed-literal branch October 13, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants