Check Array(T, N: Comptime(Int)) sizes #67

Closed
opened 2026-09-16 20:52:45 +00:00 by jacksonopp · 2 comments
Owner

Follow-up to #66 (and relates to #55/#56).

#66 taught the parser/AST that a generic type argument can be either a type (GenericArg::Type) or a bare comptime value (GenericArg::Value), specifically so Array(Int, 3) parses — the array's size sitting in type-argument position alongside its element type. That PR is parser/AST-level only; no checker semantics were built.

This issue is the checker-side follow-up, once array literals (#56) are underway:

  • TyKind::Array needs a size field (or equivalent), so Array(Int, 3) and Array(Int, 5) are genuinely different types — done in #71/#72: no dedicated TyKind::Array; sizes live in TyKind::Instantiated { args: [.., InstantiationArg::Value(_)] }, and check_array_literal_different_sizes_are_distinct_tys covers it.
  • The checker needs to understand Comptime(Int)-kinded generic parameters generally — resolving a GenericArg::Value against that parameter, and rejecting a GenericArg::Type (or vice versa) where the kind doesn't match. — done in #71 (GenericParamKind mismatch diagnostics).
  • [3]Int[1, 2, 3]-style array literals need their declared size checked against the literal's actual element count and, once size is type-level, against whatever Array(Int, N) the literal is being assigned/passed into. — both done. ArraySizeMismatch/ArrayTypeMismatch/InvalidArraySize cover the literal-vs-itself case (check_array_size, array_members_contains_mismatch). The assigned/passed-into case is done via a new expected: Option<Ty<'db>> threaded through check_let → check_expr → check_array_literal: a bare [1, 2, 3] now adopts a sized Array(Int, N) target instead of always inferring List(Int), checking both element count and element type against it. Covered by check_let_sized_array_literal_matches_same_size_array_annotation, check_let_sized_array_literal_against_different_size_annotation_reports_mismatch, check_let_bare_array_literal_matches_same_size_array_annotation, and check_let_bare_array_literal_against_mismatched_size_annotation_reports_mismatch.
  • Decide whether Comptime(Int)-kinded params can be arbitrary comptime-evaluated expressions (Array(Int, computeSize())) or literals only, for a first pass. — literals only, for now. This isn't a fresh decision so much as the ceiling already set by #66/#71: GenericArg::Value holds a Spanned<Literal> and InstantiationArg::Value holds a Literal, not an arbitrary Expr; check_array_size explicitly skips non-literal sizes since Scope only tracks types, not values. Supporting arbitrary comptime expressions is real, separate work — split out to a new issue.

Closing this out — all four bullets are resolved (the last one by scoping it out rather than building it). Arbitrary comptime-evaluated sizes are tracked in a new issue.

Follow-up to #66 (and relates to #55/#56). #66 taught the parser/AST that a generic type argument can be either a type (`GenericArg::Type`) or a bare comptime value (`GenericArg::Value`), specifically so `Array(Int, 3)` parses — the array's size sitting in type-argument position alongside its element type. That PR is parser/AST-level only; no checker semantics were built. This issue is the checker-side follow-up, once array literals (#56) are underway: - [x] ~~`TyKind::Array` needs a size field (or equivalent), so `Array(Int, 3)` and `Array(Int, 5)` are genuinely different types~~ — done in #71/#72: no dedicated `TyKind::Array`; sizes live in `TyKind::Instantiated { args: [.., InstantiationArg::Value(_)] }`, and `check_array_literal_different_sizes_are_distinct_tys` covers it. - [x] ~~The checker needs to understand `Comptime(Int)`-kinded generic parameters generally — resolving a `GenericArg::Value` against that parameter, and rejecting a `GenericArg::Type` (or vice versa) where the kind doesn't match.~~ — done in #71 (`GenericParamKind` mismatch diagnostics). - [x] ~~`[3]Int[1, 2, 3]`-style array literals need their declared size checked against the literal's actual element count *and*, once size is type-level, against whatever `Array(Int, N)` the literal is being assigned/passed into.~~ — both done. `ArraySizeMismatch`/`ArrayTypeMismatch`/`InvalidArraySize` cover the literal-vs-itself case (`check_array_size`, `array_members_contains_mismatch`). The assigned/passed-into case is done via a new `expected: Option<Ty<'db>>` threaded through `check_let` → `check_expr` → `check_array_literal`: a bare `[1, 2, 3]` now adopts a sized `Array(Int, N)` target instead of always inferring `List(Int)`, checking both element count and element type against it. Covered by `check_let_sized_array_literal_matches_same_size_array_annotation`, `check_let_sized_array_literal_against_different_size_annotation_reports_mismatch`, `check_let_bare_array_literal_matches_same_size_array_annotation`, and `check_let_bare_array_literal_against_mismatched_size_annotation_reports_mismatch`. - [x] ~~Decide whether `Comptime(Int)`-kinded params can be arbitrary comptime-evaluated expressions (`Array(Int, computeSize())`) or literals only, for a first pass.~~ — **literals only**, for now. This isn't a fresh decision so much as the ceiling already set by #66/#71: `GenericArg::Value` holds a `Spanned<Literal>` and `InstantiationArg::Value` holds a `Literal`, not an arbitrary `Expr`; `check_array_size` explicitly skips non-literal sizes since `Scope` only tracks types, not values. Supporting arbitrary comptime expressions is real, separate work — split out to a new issue. Closing this out — all four bullets are resolved (the last one by scoping it out rather than building it). Arbitrary comptime-evaluated sizes are tracked in a new issue.
jacksonopp added this to the twill project 2026-09-16 20:56:52 +00:00
Author
Owner

Repointing the Comptime(Int)/generic-instantiation dependency: #69 got scoped down to just List(T) for its first pass, with Array/Map (and the Comptime(_) value-slot machinery this issue needs) split out to #71. This issue's blocked-on dependency is #71, not #69.

Repointing the Comptime(Int)/generic-instantiation dependency: #69 got scoped down to just List(T) for its first pass, with Array/Map (and the Comptime(_) value-slot machinery this issue needs) split out to #71. This issue's blocked-on dependency is #71, not #69.
Author
Owner

Confirmed via TDD while starting #72: check_let never threads a let's annotation down as an expected type into its value's own checking — it resolves the annotation and the value independently (check_type_expr / check_expr), then just compares the two Tys for equality. That means a bare literal ([1, 2, 3], no [N]T[...]/[]T[...] self-annotation) always infers itself as List(T) via check_array_literal's (None, None) branch, regardless of any surrounding annotation.

Tested all three container kinds with check_let:

  • let bar: List(Int) = [1, 2, 3] — passes (bare literal's own List(Int) default happens to match the annotation).
  • let baz: Map(Int, Int) = [1: 2, 3: 4, 5: 6] — passes, same reason.
  • let foo: Array(Int, 3) = [1, 2, 3] — fails, reports a false ValueAnnotationTypeMismatch (expected Array(Int, 3), found List(Int)) — because the bare RHS never gets a chance to be checked against Array(Int, 3), only independently of it.

So List/Map passing isn't evidence that expected-type propagation exists — it's coincidence, since their bare-literal default happens to equal the annotated type. Array is just the first case where the two diverge and the gap becomes visible. Worth folding into this issue's scope alongside the declared-size-vs-literal-count check already listed here: check_array_literal's bare branch (and really check_let/check_expr generally) needs some notion of an expected type propagated in from the annotation, not just independent bottom-up inference compared after the fact.

Confirmed via TDD while starting #72: `check_let` never threads a `let`'s annotation down as an expected type into its value's own checking — it resolves the annotation and the value independently (`check_type_expr` / `check_expr`), then just compares the two `Ty`s for equality. That means a *bare* literal (`[1, 2, 3]`, no `[N]T[...]`/`[]T[...]` self-annotation) always infers itself as `List(T)` via `check_array_literal`'s `(None, None)` branch, regardless of any surrounding annotation. Tested all three container kinds with `check_let`: - `let bar: List(Int) = [1, 2, 3]` — passes (bare literal's own `List(Int)` default happens to match the annotation). - `let baz: Map(Int, Int) = [1: 2, 3: 4, 5: 6]` — passes, same reason. - `let foo: Array(Int, 3) = [1, 2, 3]` — **fails**, reports a false `ValueAnnotationTypeMismatch` (expected `Array(Int, 3)`, found `List(Int)`) — because the bare RHS never gets a chance to be checked *against* `Array(Int, 3)`, only independently of it. So `List`/`Map` passing isn't evidence that expected-type propagation exists — it's coincidence, since their bare-literal default happens to equal the annotated type. `Array` is just the first case where the two diverge and the gap becomes visible. Worth folding into this issue's scope alongside the declared-size-vs-literal-count check already listed here: `check_array_literal`'s bare branch (and really `check_let`/`check_expr` generally) needs some notion of an expected type propagated in from the annotation, not just independent bottom-up inference compared after the fact.
jacksonopp removed this from the twill project 2026-10-04 02:28:22 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jacksonopp/twill#67
No description provided.