Specification

Phase 0, Section 3: Subtyping and Coercion

Phase 0: Foundation — establishing the formal core calculus. Section 3 of 5: Subtyping and Coercion.


1. What this section commits to

When two types are the same under the type-checker even though they are not literally equal: the subtyping relation. When the user (or elaborator) must insert an explicit operation to bridge two types: the coercion relation. The line between these is the central question for this section.

The Sections 1 and 2 grammar and operation typing are not expected to change here. If subtyping forces a change to either, that's evidence the foundation is wrong rather than that subtyping needs special-casing.


2. Subtyping vs. coercion: the conceptual distinction

Two types T₁ and T₂ may be related in three ways:

Relation Meaning Runtime cost
Equal (T₁ ≡ T₂) Same type up to alpha-renaming and refinement-set canonicalization None
Subtype (T₁ <: T₂) Every value of T₁ is also a value of T₂; the value flows without modification None
Coercible (T₁ ⇝ T₂) A value of T₁ can be transformed into a value of T₂ via an explicit operation Possibly real (cast: data conversion; broadcast: data replication; reshape: zero-cost view)

Phase 0 commits to a sharp line:

Subtyping affects only the refinement axis. Dtype, shape structure, and rank are all invariant. Differences along those axes require coercions (cast, broadcast, reshape, transpose) — explicit operations in the IR with explicit lowering.

The rationale is operational. A subtype relationship has no runtime effect; the value just gets viewed at a different type. A coercion is a real operation: cast performs FP rounding, broadcast replicates data (or sets up a view), reshape may or may not produce a new tensor. The IR should make the distinction visible because the lowering and the cost are different.


3. Subtyping rules

The judgment is Γ ⊢ T₁ <: T₂ — "in context Γ, every value of T₁ is a value of T₂."

3.1 Reflexivity

   Γ ⊢ T type
─────────────────  (refl)
  Γ ⊢ T <: T

Every well-formed type is a subtype of itself. Standard.

3.2 Transitivity

  Γ ⊢ T₁ <: T₂      Γ ⊢ T₂ <: T₃
─────────────────────────────────────  (trans)
        Γ ⊢ T₁ <: T₃

Subtyping composes. The implementation derives transitivity rather than checking it explicitly; it falls out of the structural rules below.

3.3 Tensor subtyping (the load-bearing rule)

  Γ ⊢ s₁ ≡ s₂
  Γ; R₁ ⊨ R₂
─────────────────────────────────────────────────  (sub-tensor)
  Γ ⊢ Tensor[dt, s₁, R₁] <: Tensor[dt, s₂, R₂]

Three premises:

The intuition: stronger refinements produce smaller value sets. A tensor known to satisfy B ≥ 100 can be used wherever a tensor satisfying B ≥ 1 is expected, because the first constraint implies the second. The subtype is the more constrained one.

3.4 Function subtyping

  Γ ⊢ T₂ <: T₁    Γ ⊢ U₁ <: U₂
─────────────────────────────────  (sub-fn)
   Γ ⊢ T₁ → U₁ <: T₂ → U₂

Contravariant in the input, covariant in the output. Standard. A function is a subtype if it accepts a wider set of inputs (less constrained refinements) and produces a narrower set of outputs (more constrained refinements).

3.5 No width subtyping

Refinement sets are not records. There is no [p₁, p₂, p₃] <: [p₁, p₂] rule based on length. Width-style subtyping falls out of implication: R₁ ⇒ R₂ if R₁ includes (or implies) every predicate of R₂, regardless of cardinality.

This means:


4. The subsumption rule

  Γ ⊢ e : T₁    Γ ⊢ T₁ <: T₂
─────────────────────────────────  (subsumption)
        Γ ⊢ e : T₂

Standard. Any expression at a subtype can be used where a supertype is expected. This is what makes subtyping implicit — no syntactic marker, the value just flows.

The subsumption rule is admissible, not declared as a separate operation. The type-checker applies it during synthesis-vs-checking (bidirectional type-checking, Section 5 implementation): when checking that an expression has an annotated type, the checker synthesizes the expression's type and verifies subtyping against the annotation.


5. Subtyping algorithm

Given Γ ⊢ T₁ <: T₂?, the implementation:

  1. Structural decomposition. If T₁ and T₂ are different shapes (one tensor, one function), reject. If both are functions, recurse on inputs (contravariantly) and outputs (covariantly). If both are tensors, proceed to (2).

  2. Dtype check. Compare dtypes for syntactic equality. If different, reject. (Cast is required.)

  3. Shape check. For each dimension, build the SMT obligation s₁[i] = s₂[i] and discharge under Γ. If any dimension fails, reject.

  4. Refinement implication. Build the SMT obligation R₁ ⇒ R₂ (i.e., assert Γ ∧ R₁ ∧ ¬R₂, check satisfiability). If unsat, the implication holds, accept. If sat, the counterexample is the failing refinement and is reported. If unknown (timeout), reject.

The total cost is dominated by SMT calls. The implementation should:

Section 5 (implementation) elaborates these.


6. Coercions in the core IR

Coercions are operations, not subtyping. Each appears in the IR explicitly. From Section 2:

Coercion Operation Cost
Dtype change cast[dt₂] FP rounding / integer reinterpretation
Shape replication broadcast[s'] Data replication or view; lowering decides
Shape rearrangement transpose[π] Usually a stride change; sometimes a copy
Volume-preserving shape change reshape[s'] Usually zero-cost view; sometimes a copy
Subset selection slice[axis, a, b] View into existing tensor
Concatenation concat[axis] Allocates new tensor

The user (or elaborator) inserts these explicitly. The core IR does not "automatically broadcast" or "automatically cast." This was committed in Section 2 §11 for broadcasting and §5 for casting; this section ratifies the broader principle.

The reason is consistency with the lowering story. Each coercion has a specific MLIR linalg lowering and a specific cost. Inserting them explicitly means the IR makes the cost visible and the lowering is unambiguous.

6.1 Coercion vs. subtyping in practice

Two cases that look similar but lie on opposite sides of the line:

(* Subtyping: the value flows; no operation. *)
let f (x : Tensor[F32, [B, S], [B ≥ 1]]) : Tensor[F32, [B, S], [B ≥ 0]] =
  x  (* B ≥ 1 implies B ≥ 0; subsumption applies. *)

(* Coercion: explicit operation in IR. *)
let g (x : Tensor[F32, [B, S, D]]) : Tensor[F32, [B, S, D, 1]] =
  reshape[[B, S, D, 1]] x
  (* The trailing 1 changes the shape structure. Not subtyping. *)

The first compiles to no IR operation — f is the identity at runtime. The second compiles to a reshape IR node, which lowers to (most likely) a stride manipulation but is still a visible operation.


7. The subtyping algorithm and bidirectional checking

The full type-checking strategy for Phase 0 is bidirectional:

Subsumption appears at the synthesis-to-checking boundary:

  Γ ⊢ e ⇒ T₁    Γ ⊢ T₁ <: T₂
─────────────────────────────────  (check-via-sub)
        Γ ⊢ e ⇐ T₂

This is the only place the type-checker invokes the subtyping algorithm. Internal nodes use synthesis. Boundary checks use subtyping.

This commits us to bidirectional type-checking as the algorithmic strategy. It is well-understood, plays well with refinement systems, and produces good error messages (the failure point is always a synthesis-vs-checking mismatch at a known location).


8. Examples

Concrete cases to test understanding.

8.1 Refinement strengthening

T₁ = Tensor[F32, [B, S, D], [B ≥ 16, D mod 8 = 0]]
T₂ = Tensor[F32, [B, S, D], [B ≥ 1]]

T₁ <: T₂  ✓

T₁'s refinements imply T₂'s. A function expecting T₂ accepts T₁.

8.2 Refinement weakening (rejected)

T₁ = Tensor[F32, [B, S, D], [B ≥ 1]]
T₂ = Tensor[F32, [B, S, D], [B ≥ 16]]

T₁ <: T₂  ✗  (counterexample: B = 1)

B ≥ 1 does not imply B ≥ 16. The user gets a counterexample assignment.

8.3 Dtype mismatch (rejected, requires cast)

T₁ = Tensor[BF16, [B, S], []]
T₂ = Tensor[F32, [B, S], []]

T₁ <: T₂  ✗  (dtype invariant)

To convert: cast[F32] x : Tensor[F32, [B, S], []]

The dtype rejection is structural; the user inserts cast. Note that the cast operation's MLIR lowering may or may not be lossless; that's a numerical concern (Phase 6) not a type-system one.

8.4 Function subtyping

F₁ = Tensor[F32, [B, S], [B ≥ 1]]   → Tensor[F32, [B, S], [B ≥ 16]]
F₂ = Tensor[F32, [B, S], [B ≥ 16]]  → Tensor[F32, [B, S], [B ≥ 1]]

F₁ <: F₂  ✓

F₁ accepts a wider input set (B ≥ 1 is weaker than B ≥ 16) and produces a narrower output set (B ≥ 16 is stronger than B ≥ 1). It can be substituted for F₂ anywhere F₂ is expected.

8.5 Shape rank difference (rejected)

T₁ = Tensor[F32, [B, S], []]
T₂ = Tensor[F32, [B, S, 1], []]

T₁ <: T₂  ✗  (rank invariant)

To convert: reshape[[B, S, 1]] x  or  unsqueeze[axis=2] x

Different ranks are different types. The user inserts a reshape (or, if Phase 7's surface syntax adds unsqueeze, that desugars to reshape).

8.6 Refinement set canonicalization (handled implicitly)

T₁ = Tensor[F32, [B], [B ≥ 1, B ≥ 0]]
T₂ = Tensor[F32, [B], [B ≥ 1]]

T₁ <: T₂  ✓
T₂ <: T₁  ✓

B ≥ 0 is implied by B ≥ 1, so the two refinement sets are logically equivalent. SMT discharges both directions. The implementation will canonicalize for efficiency, but the rules are correct without it.


9. Implicit dtype promotion: rejected

A tempting feature that this section commits to not having: implicit dtype promotion. In NumPy, int_array + float_array automatically promotes the int to float. In Python, int + float returns float. In C, int + double promotes via integer promotions.

Phase 0 rejects this. Dtype mismatch in elementwise binary operations is a type error, full stop. The user calls cast explicitly.

Rationale:

Same argument as broadcasting in Section 2 §11: the core IR is the contract; surface ergonomics is a separate concern.


10. Resolved open issues

11. New open issues

  1. Algorithmic subtyping for refinements containing free shape variables. When Γ ⊢ T₁ <: T₂ and both types contain free shape variables (e.g., a polymorphic function's signature), the SMT discharge may ∀-quantify those variables. Z3 handles ∀-Presburger but may be slow. Engineering mitigation is recorded at the end of Section 5 §11 (structural unification before implication when needed).

  2. Mutual recursion and subtyping coinduction. Functions calling each other through subtyping create a fixed-point obligation. Engineering approach: greatest fixed-point subtyping with cycle detection — end of Section 5 §11. Not user-visible in Phase 0.

  3. Subtyping for effect rows. Effects (Phase 2) will introduce row types: Tensor[F32, s, R] !{Random, State}. Subtyping over effect rows is its own design question — typically row subtyping where extra effects in the smaller row are permitted. Deferred to Phase 2.

  4. Negative refinements. A predicate like ¬(B = 0) is in the grammar but creates SMT performance issues (negation over disjunction explodes). Phase 0 semantics: correctness does not depend on a particular normal form. Implementation: canonicalize negation and align with the global SMT timeout policy — end of Section 5 §11.


12. Decisions, summarized

# Decision Rationale
3.1 Subtyping affects only the refinement axis; dtype, shape, rank are invariant Sharp line between subtyping (no runtime cost) and coercion (operation in IR)
3.2 Tensor subtyping: dtype equal, shape equal, refinement implication via SMT Liquid-Haskell-style refinement subtyping, decidable via Z3
3.3 Function subtyping: contravariant input, covariant output Standard
3.4 No width subtyping for refinement sets; semantics handled by SMT Canonicalization is an implementation concern, not a correctness one
3.5 Subsumption rule is admissible; type-checking is bidirectional Standard for refinement type systems; good error locality
3.6 Coercions are explicit IR operations: cast, broadcast, transpose, reshape, slice, concat Lowering and cost are visible in the IR
3.7 No implicit dtype promotion in core IR Surface syntax can sugar; core IR stays unambiguous

13. Section 4 preview

Section 4 commits to (see that section for the authoritative definitions):

Sections 1, 2, and 3 are not expected to change in Section 4. The semantics is a model for the typing rules already committed; if the model can't accommodate them, the rules are wrong.


End of Phase 0, Section 3.