Sprig generics (implemented contract)
Sole current generics reference. The website and SDK use this source.
This document describes the generic type system the stage-0 compiler implements. It is narrower than a general generics design and deliberately so.
Declaration
generic T:
class Box:
let value: Tgeneric T:wraps one declaration: a class, variant or function.- Blocks may declare any number of parameters:
generic K, V:. There is no arbitrary limit, and there is no separate single-parameter type system. - Duplicate names in one list (
generic T, T:) reportSPR-NAME-DUPLICATE. - Parameters are visible only inside the block; they never leak, and a name or type outside the block cannot refer to them (
SPR-NAME-UNRESOLVED).
Application is always explicit
let box = Box[Int](https://github.com/ColinHouse/Sprig/blob/main/docs/language/value=42) # generic class constructor
let value = identity[Int](https://github.com/ColinHouse/Sprig/blob/main/docs/language/42) # generic function
let some: Option[Int] = Option[Int].Some(value=1)
let none: Option[Int] = Option[Int].None- Every generic use site writes all
[Type]arguments. There is no inference:identity(42)andBox(value=42)reportSPR-TYPE-GENERIC-ARGS-REQUIRED;Entry[String]for a two-parameter declaration reportsSPR-TYPE-GENERIC-ARITY, as does a bareBoxin a type position or arguments on a non-generic type. Partial arguments are never inferred from context. - Nested applications are ordinary:
List[Option[Int]],Map[String, Box[Int]],Box[List[String?]]. - Generic types are invariant. No
out/in, wildcards or subtyping.
Type parameters have almost no abilities
Inside a generic declaration, T supports assignment, passing, returning and being placed in compatible generic containers. It has no operators, ordering or methods, and equality only when the enclosing function declares it: value + value is SPR-TYPE-OPERAND, and a == b is rejected unless the function starts with requires K: Equatable, in which case equality is checked with value equality on the boxed representation.
Capabilities are deliberately minimal:
| Capability | Status |
|---|---|
Equatable | implemented for <T> equality under requires X: Equatable |
Comparable | parsed, but not implemented (SPR-GENERIC-CONSTRAINT) |
Clauses must form a leading prefix of the function body. A late or nested clause reports SPR-GENERIC-CONSTRAINT and grants no capability.
Any requires clause naming a parameter that is not in scope reports SPR-NAME-UNRESOLVED.
Nullability rule (Practical Strict)
List[String?],Box[String?]andOption[String?]are legal.If the declaration itself applies
?to the parameter:spriggeneric T: class Box: let value: T?then
Box[String]is legal butBox[String?]is rejected withSPR-TYPE-GENERIC-NULLABLE. The compiler does not flattenT?or inventString??; the declaration already owns the nullable position. This applies recursively to written annotations, local/lambda types and nested generic applications, and is independent of declaration order.
Generic variants
Generic variants use the expanded payload form:
generic T:
variant Option:
Some:
value: T
None:Option[Int].Some(value=42) builds a value; Option[Int].None is a value, not a call. A match writes the unapplied case owner; do not put [Int] in a case:
func read(option: Option[Int]) -> Int:
match option:
case Option.Some as some:
return some.value
case Option.None:
return 0Exhaustive match works on instantiations and a new case still reports SPR-MATCH-NONEXHAUSTIVE for every match that misses it.
Relationship to indexing
values[index] remains ordinary indexing. The parser records a bracket payload that parses as type references as a candidate; the checker decides:
- if the base names a generic declaration, the bracket is a type application;
- otherwise a single plain name is indexing, as in the design kit.
One consequence: handler[index](https://github.com/ColinHouse/Sprig/blob/main/docs/language/arg) (index, then call the result) is not a valid form and is diagnosed; index-then-call was never usable with the current function-type model.
JVM lowering
Generics are erased and boxed in generated Java:
- a type parameter becomes
java.lang.Object; - generic classes and variants are emitted as raw classes;
- the compiler inserts boxing at generic argument positions and casts plus unboxing at generic result positions;
instanceofstays raw, so match exhaustiveness is unaffected.
Sprig types are preserved at the source level: Box[Int] is Int to Sprig even though the JVM sees a boxed value.
Imported Java generics
Explicit type arguments also apply to imported Java classes and methods (ArrayList[String], Host.method[String](https://github.com/ColinHouse/Sprig/blob/main/docs/language/value)). Concrete arguments are preserved in Sprig types and in sprig api metadata; class type variables resolve through the receiver and its inherited hierarchy. The profile is deliberately bounded: no wildcard syntax, no capture conversion and no Java generic inference; method type parameters require explicit arguments, and recursive/intersection bounds or generic arrays are rejected before codegen. Java reference results remain conservatively nullable. See JVM interop for arrays and collection adapters.
Not implemented
Generic inference, variance, Comparable and any user-defined capability, generic constraints on JVM types, and registry/publishing features. The manifest and entry-discovery model (sprig.toml, init, project, deps, run --bin), schema-5 lockfiles, local/Git and Apache Maven resolution are implemented; see the current project/dependency documentation.
