Skip to content
Topaz6.0 · Before release
Download

Search documentation

Specification 6.0 · Not released · Tested product 5.21.3

Use arrow keys to choose a result. Press Enter to open.

Type an API name, diagnostic number, rule, or project topic.

    Language · chapter 07 · 38 rules

    Records, enums and newtypes

    Declare data types, construct their values, and check record updates and generic construction.

    Scope and ownership

    Link

    Edition: Topaz 5.21 language mode with the SELF engine. The source revision is af88afbc1d99a8c0220b5984f9d74f895f8156f2.

    This page covers nominal declarations, their fields and variants, construction, record updates, and declaration type parameters. General type compatibility belongs to Types. Protocols and implementations owns conformance, derives requirements, and method resolution. Receiver APIs and pattern matching have their own references.

    Declaration syntax

    Link
    EBNF
    1. RecordDeclaration =
    2. "record" Identifier NominalTypeParameters? DerivesClause?
    3. "{" (RecordField ("," RecordField)* ","?)? "}" ;
    4. RecordField = Identifier ":" Type ("=" Expression)? ;
    5. EnumDeclaration =
    6. "enum" Identifier NominalTypeParameters? DerivesClause?
    7. "{" (Variant ("," Variant)* ","?)? "}" ;
    8. Variant = Identifier ("(" Type ("," Type)* ")")? ;
    9. NewtypeDeclaration =
    10. "newtype" Identifier NominalTypeParameters? DerivesClause? "=" Type ;
    11. NominalTypeParameters =
    12. "<" Identifier ("," Identifier)* ">" ;
    13. DerivesClause =
    14. "derives" (Identifier ("," Identifier)*
    15. | FunctionTypeParameters ConditionalConformance ("," ConditionalConformance)*) ;
    16. ConditionalConformance = Identifier "<" Type ">" ;

    The excerpt uses Identifier, Type, and Expression from their respective language definitions. Export placement and declaration contexts are covered separately.

    Source evidence
    Ruletopaz.rule.structures.records.declaration-fields

    Named and typed record fields

    A record declaration gives each field a name and a type.

    Why

    Named fields give each part of a record one identity. Declared types let construction, access, and updates share the same checks.

    Source evidence

    Enum variants and payload types

    An enum declaration lists named variants with optional positional payload types.

    Why

    A variant names one alternative, and its payload carries the data needed for that alternative. Declared payload types make matching and construction agree.

    Source evidence

    A newtype base type

    A newtype declaration names one base type after =.

    Why

    A newtype gives one existing value type a distinct identity. One base type preserves that wrapper relationship without introducing another record shape.

    Source evidence
    Ruletopaz.rule.structures.records.derives-placement

    Derives clause placement

    A nominal derives clause follows the declaration name and type parameters. It precedes the body of a record or enum, or = and the base type of a newtype.

    Why

    Placing derivation beside the type name makes generated protocol behavior visible before the representation. Records, enums, and newtypes use the same clause position after the name and type parameters.

    Source evidence

    Newtype derivation

    A newtype declaration may request the same derived protocols as a record or enum. Its base type supplies the member capability checks. Derivation is explicit; the base type does not implicitly give the newtype a protocol conformance. The clause precedes = and accepts the same conditional generic binder.

    Why

    A newtype has a distinct nominal identity even when its representation is an existing type. Explicit derivation lets it opt into the same protocol contracts as records and enums. Inspecting the base type uses the existing capability rules without introducing implicit conformance inheritance or a wrapper-specific prohibition.

    Source evidence

    Record fields and construction

    Link

    Construct a record and supply a spread value

    Link
    example.tpzConstruct a record and supply a spread value
    1. record Point { x: int, y: int = 0 }
    2. let point = Point { x: 1 }
    3. let moved = Point { ...point, y: 2 }
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    Declaring a field default

    A record field default is an expression written after its type and =.

    Why

    The field type states the required value shape, and the following initializer supplies its default. This matches the type and initializer order used by bindings.

    Source evidence

    Repeated field declarations

    Declaring the same field name twice in one record produces TPZ5008.

    Why

    Each field name must identify one stored value and one declared type. Duplicate declarations would make access and construction ambiguous.

    Source evidence

    Required construction fields

    A named record construction without a spread produces TPZ5004 when it omits a field with no default.

    Why

    A constructed record must contain a value for every field. Requiring fields without defaults prevents a later read from encountering uninitialized data.

    Source evidence

    Omitting a defaulted field

    A named record construction may omit a field that has a declared default.

    Why

    A declaration supplies the shared default for each omitted field. Call sites can use that policy without repeating the initializer expression.

    Source evidence

    Repeated construction fields

    Supplying the same field twice in a named record construction produces TPZ5008.

    Why

    A field must receive one explicit initializer. Rejecting duplicates avoids choosing a value by overwrite order and evaluating a discarded initializer.

    Source evidence

    Undeclared construction fields

    Supplying a field absent from the declared record produces TPZ5006.

    Why

    Rejecting undeclared names catches misspellings at construction. It also keeps every stored field within the record type that later access checks use.

    Source evidence

    Construction field types

    A named construction checks each uniquely supplied field against its resolved declared type after substituting record type arguments.

    Why

    Reads rely on the declared field type. Checking supplied values after generic substitution preserves that guarantee for each record instantiation.

    Source evidence

    Evaluation of omitted record fields

    Each construction evaluates defaults for omitted fields after its explicit initializers, in field declaration order. A supplied field or a spread supplies the field instead. Defaults refer to names in their declaration environment.

    Why

    A fresh evaluation gives each construction its own default values. The declaration environment keeps defaults independent of names at the call site, and a fixed order makes effects predictable.

    Source evidence

    Declared types also constrain defaults

    A record field default must satisfy the declared field type. A string default for an int field is a type error.

    Why

    A default can become the field value without an explicit initializer at a call site. Checking it against the declared type preserves the same requirement for both paths.

    Source evidence

    Record literals and updates

    Link
    EBNF
    1. NominalRecordConstruction =
    2. Identifier "{" RecordContents? "}" ;
    3. RecordContents =
    4. (RecordSpread ("," FieldInitializers)? | FieldInitializers) ","? ;
    5. RecordSpread = "..." Expression ;
    6. FieldInitializers = FieldInitializer ("," FieldInitializer)* ;
    7. FieldInitializer = FieldName ":" Expression ;
    8. StructuralRecordLiteral =
    9. "{" FieldInitializers ","? "}" ;
    10. RecordValueUpdate =
    11. Identifier "{" FieldInitializers ","? "}" ;

    This excerpt shows nonempty updates with identifier bases. Types covers structural compatibility.

    Source evidence

    The name in a record construction

    The name in a named record construction denotes a declared record.

    Why

    Resolving the head as a record type gives construction its field declarations and nominal identity. It does not depend on an unrelated value with that spelling.

    Source evidence

    Record spread placement

    A spread in a named record construction must appear first and occur at most once.

    Why

    One initial spread gives construction a single base value. Explicit fields can then replace named values without competing spreads or an implicit overwrite order.

    Source evidence

    The type of a record spread

    A named record construction checks the spread value against the constructed record type.

    Why

    The copied fields must belong to the record being constructed. Requiring the same instantiated record type prevents incompatible fields from entering through a spread.

    Source evidence

    Spreads on a value update

    A spread on a record value update produces TPZ5001.

    Why

    A value update already names the record to copy. Another spread would introduce a second base and a choice of overwrite order.

    Source evidence

    Structural record literals

    A structural record literal encloses at least one named field initializer in braces and has no nominal record name.

    Why

    Field names and values can describe a record without assigning a separate nominal identity. This supports small grouped values whose shape is their type.

    Source evidence
    Ruletopaz.rule.structures.records.empty-braces

    Empty braces in expressions

    In an expression, empty braces form a block.

    Why

    Empty braces in an expression denote an empty block. Keeping that reading makes empty bodies consistent and avoids selecting between a block and a record from context.

    Source evidence
    Ruletopaz.rule.structures.source.record-update-order

    Record update evaluation and copied fields

    A record update evaluates its base once before explicit field initializers. Initializers run in source order. A spread supplies omitted fields without reevaluating their defaults. The result has new record storage and retains the field values from the base.

    Why

    Evaluating the base once avoids duplicating its effects. Explicit fields then replace named values while omitted fields retain the copied values.

    Source evidence

    Structural record value updates

    Link

    Update a structural record value

    Link
    example.tpzUpdate a structural record value
    1. let point = { x: 1, y: 2 }
    2. let moved = point { y: 3 }
    3. let updated: int = moved.y
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    The base of a structural update

    In a structural record update value { field: expression }, value denotes an existing structural record.

    Why

    An update needs an existing value to supply fields that are not replaced. Resolving the head in the value namespace identifies that source directly.

    Source evidence

    Structural update field types

    For a field supplied once, a structural record update checks its value against the base record field type.

    Why

    Replacing a field must preserve the type used by existing readers. Checking against the base field type prevents an update from invalidating those readers.

    Source evidence

    Structural update result type

    A structural record value update has the type of its base record.

    Why

    An update changes field values while preserving the record shape. Keeping the base type lets the result serve the same uses as the original value.

    Source evidence

    Update a nominal record through its declaration

    Value-update syntax requires a structural record. Update a nominal record with its declared name and a spread, as in Point { ...point, x: 2 }.

    Why

    Nominal updates use named construction with a leading spread. Rejecting a value-headed update keeps one update form for a nominal record.

    Source evidence

    Repeated structural update fields

    Each explicit field name in a structural record update must occur once. Repeating a field is rejected with TPZ5008 even when an earlier initializer has an incompatible type. A field copied from the base may be replaced by one explicit initializer.

    Why

    Each explicit field must select one value. Rejecting repeated names prevents initializer order from choosing a winner or hiding an incompatible earlier value.

    Source evidence

    Enum variants and construction

    Link

    Construct variants with and without payloads

    Link
    example.tpzConstruct variants with and without payloads
    1. enum Reply { Waiting, Done(int, string) }
    2. let waiting: Reply = Reply.Waiting()
    3. let done: Reply = Reply.Done(2, "ready")
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    See Pattern matching for variant patterns, exhaustiveness, and unreachable cases. Functions and generics covers argument binding.

    Ruletopaz.rule.structures.records.enum-variant-value

    Variants without payloads

    Calling a qualified enum constructor without payloads, such as Choice.Empty(), produces a value of the enum type.

    Why

    A bare nullary variant and a call with no arguments construct the same variant without a payload. Parentheses preserve that construction intent and use the same argument-list shape as variants with payloads and constructor patterns.

    Source evidence

    Variants with payloads

    A payload-bearing enum variant is a constructor function from its substituted payload types to the enum type.

    Why

    A payload variant needs values before it can exist. Giving its constructor a function type reuses ordinary argument checking and function passing.

    Source evidence

    Variant names and prelude constructors

    Enum variants may be named None, Some, Ok, or Err. Construct them through the enum name, such as Reply.None() or Reply.Ok(value). The prelude Ok(value) constructs a Result value. An unqualified constructor pattern such as None() resolves through the normalized scrutinee type. Prelude spelling does not take priority. For a union, collect the constructor declarations with that head before checking payload arity. One declaration is required. Multiple instantiations of that same declaration project the union of the corresponding payload types. Distinct declarations with the same head are ambiguous even when their payload shapes agree. Narrow with an existing typed pattern and match the narrowed value. Ambiguity is a TPZ5001 pattern-type error. Wrong payload arity remains TPZ5004. Constructor patterns use unqualified heads.

    Why

    A constructor belongs to its defining nominal type. A prelude spelling must not reserve the same field-like name inside every enum. Resolving a pattern to one constructor keeps unrelated nominal types distinct. If a union introduces a second constructor with that name, requiring type narrowing prevents an existing arm from silently acquiring new cases. The ordinary typed pattern supplies that narrowing without another constructor notation.

    Source evidence

    Enum declarations and payload types

    An enum declaration may have no variants. Nominal type names are predeclared, so enum payloads may refer to their own enum or another declared enum, including through Array. This admits the declaration; it does not create a value for an enum with no finite constructor path. Payloads follow the same union type rules as other type positions. A union payload may contain enum types.

    Why

    Nominal declarations establish type identities before values are constructed. Empty and recursive types can therefore be declared without requiring a finite value. Payloads compose the same type expressions used elsewhere. An enum member in a union must not require an extra wrapper or a different variant structure.

    Source evidence

    Unique enum variant declarations

    Each variant name must be unique within its enum. The retained checker diagnoses repeated declarations with TPZ5008; current SELF appends them without that guard. The intended rejection remains a product expectation and the missing SELF check is an implementation limitation.

    Why

    A variant name must select one alternative within its enum. Reusing the name would make construction and pattern matching ambiguous.

    Source evidence

    Accept an enum type in a union payload

    Link
    example.tpzAccept an enum type in a union payloadOpen in Playground
    1. enum Inner { Leaf }
    2. enum Outer { Wrapped(Inner | int) }
    Expected outputexit 0
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Source evidence

    Use distinct variants for distinct payload cases

    Link
    example.tpzUse distinct variants for distinct payload casesOpen in Playground
    1. enum Inner { Leaf }
    2. enum Outer { Wrapped(Inner), Number(int) }
    3. let value: Outer = Outer.Wrapped(Inner.Leaf())
    Expected outputexit 0
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Source evidence

    Construct and match both union payload alternatives

    Link
    example.tpzConstruct and match both union payload alternatives
    1. import std.io { writeLine }
    2. enum Inner { Leaf }
    3. enum Outer { Wrapped(Inner | int) }
    4. function describe(value: Outer) -> string {
    5. match (value) {
    6. case Wrapped(payload) => match (payload) {
    7. case number: int => "{number}"
    8. case _ => "leaf"
    9. }
    10. }
    11. }
    12. writeLine(describe(Outer.Wrapped(Inner.Leaf())))
    13. writeLine(describe(Outer.Wrapped(7)))
    Expected outputexit 0
    leaf
    7
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. leaf
    2. 7
    Source evidence

    Enum variants and prelude constructors retain separate identities

    Link
    example.tpzEnum variants and prelude constructors retain separate identities
    1. import std.io { writeLine }
    2. enum Reply { None, Some(int), Ok(string), Err(bool) }
    3. function describe(value: Reply) -> string {
    4. match (value) {
    5. case None() => "enum none"
    6. case Some(number) => "enum some {number}"
    7. case Ok(text) => "enum ok {text}"
    8. case Err(flag) => if (flag) { "enum error" } else { "enum false" }
    9. }
    10. }
    11. function option(value: Option<int>) -> string {
    12. match (value) {
    13. case None() => "option none"
    14. case Some(number) => "option some {number}"
    15. }
    16. }
    17. function result(value: Result<int, string>) -> string {
    18. match (value) {
    19. case Ok(number) => "result ok {number}"
    20. case Err(text) => "result error {text}"
    21. }
    22. }
    23. writeLine(describe(Reply.None()))
    24. writeLine(describe(Reply.Some(4)))
    25. writeLine(describe(Reply.Ok("yes")))
    26. writeLine(describe(Reply.Err(true)))
    27. writeLine(option(None()))
    28. writeLine(option(Some(5)))
    29. writeLine(result(Ok(6)))
    30. writeLine(result(Err("no")))
    Expected outputexit 0
    enum none
    enum some 4
    enum ok yes
    enum error
    option none
    option some 5
    result ok 6
    result error no
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. enum none
    2. enum some 4
    3. enum ok yes
    4. enum error
    5. option none
    6. option some 5
    7. result ok 6
    8. result error no
    Source evidence

    A unique constructor head projects its declared payload from a union

    Link
    example.tpzA unique constructor head projects its declared payload from a union
    1. import std.io { writeLine }
    2. enum Left { Number(int) }
    3. enum Right { Text(string) }
    4. function describe(value: Left | Right) -> string {
    5. match (value) {
    6. case Number(number) => "{number + 1}"
    7. case Text(text) => text
    8. }
    9. }
    10. writeLine(describe(Left.Number(4)))
    11. writeLine(describe(Right.Text("right")))
    Expected outputexit 0
    5
    right
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 5
    2. right
    Source evidence

    A typed pattern selects one nominal owner before matching a shared variant

    Link
    example.tpzA typed pattern selects one nominal owner before matching a shared variant
    1. import std.io { writeLine }
    2. enum Left { Shared(int) }
    3. enum Right { Shared(string) }
    4. function describe(value: Left | Right) -> string {
    5. match (value) {
    6. case left: Left => match (left) { case Shared(number) => "left {number}" }
    7. case right: Right => match (right) { case Shared(text) => "right {text}" }
    8. }
    9. }
    10. writeLine(describe(Left.Shared(3)))
    11. writeLine(describe(Right.Shared("R")))
    Expected outputexit 0
    left 3
    right R
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. left 3
    2. right R
    Source evidence

    Typed narrowing distinguishes an enum Ok from a Result Ok

    Link
    example.tpzTyped narrowing distinguishes an enum Ok from a Result Ok
    1. import std.io { writeLine }
    2. enum Reply { Ok(int) }
    3. function describe(value: Reply | Result<int, string>) -> string {
    4. match (value) {
    5. case reply: Reply => match (reply) { case Ok(number) => "reply {number}" }
    6. case result: Result<int, string> => match (result) {
    7. case Ok(number) => "result {number}"
    8. case Err(text) => text
    9. }
    10. }
    11. }
    12. let result: Result<int, string> = Ok(8)
    13. writeLine(describe(Reply.Ok(7)))
    14. writeLine(describe(result))
    Expected outputexit 0
    reply 7
    result 8
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. reply 7
    2. result 8
    Source evidence

    Reject a shared bare constructor head before inspecting its payload

    Link
    example.tpzReject a shared bare constructor head before inspecting its payloadOpen in Playground
    1. enum Left { Shared(int) }
    2. enum Right { Shared(int) }
    3. function describe(value: Left | Right) -> int {
    4. match (value) {
    5. case Shared(number) => number
    6. case _ => 0
    7. }
    8. }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5001 structures-ambiguous-constructor-pattern at example.tpz:5:10
    Source evidence

    A prelude-spelled enum variant retains its declared payload arity

    Link
    example.tpzA prelude-spelled enum variant retains its declared payload arityOpen in Playground
    1. enum Reply { Ok(int, int) }
    2. function total(value: Reply) -> int {
    3. match (value) { case Ok(number) => number }
    4. }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5004 structures-enum-pattern-arity at example.tpz:3:24
    Source evidence

    Newtype construction

    Link

    Wrap one base value

    Link
    example.tpzWrap one base value
    1. newtype UserId = int
    2. let user: UserId = UserId(7)
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    Newtype constructor argument type

    A newtype constructor checks its single positional argument against the resolved base type and returns the newtype.

    Why

    The constructor creates a distinct nominal value from its declared base representation. Checking that representation prevents the wrapper from hiding an incompatible value.

    Source evidence

    Invalid newtype arguments

    A newtype call with an argument count other than one or a spread argument produces TPZ5004. Named arguments follow the ordinary call argument rule and are rejected during parsing with TPZ2001.

    Why

    A newtype wraps one value and declares no field name for it. One positional argument expresses that relationship without invented labels or argument expansion.

    Source evidence

    Derive the existing protocols for a newtype

    Link
    example.tpzDerive the existing protocols for a newtypeOpen in Playground
    1. newtype Item derives Eq, Order, Show, JSON = int
    2. function jsonIdentity<T: JSON>(value: T) -> T { value }
    3. let value = Item(7)
    4. let same: bool = Eq.equals(value, value)
    5. let order: int = Order.compare(value, Item(8))
    6. let shown: string = Show.show(value)
    7. let retained: Item = jsonIdentity(value)
    Expected outputexit 0
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Source evidence

    Call derived Eq and Show on a newtype

    Link
    example.tpzCall derived Eq and Show on a newtype
    1. import std.io { writeLine }
    2. newtype Item derives Eq, Show = int
    3. let value = Item(7)
    4. writeLine("{Eq.equals(value, Item(7))}")
    5. writeLine("{Eq.equals(value, Item(8))}")
    6. writeLine(Show.show(value))
    Expected outputexit 0
    true
    false
    Item(7)
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. true
    2. false
    3. Item(7)
    Source evidence

    Use the conditional derive binder on a newtype

    Link
    example.tpzUse the conditional derive binder on a newtypeOpen in Playground
    1. newtype Box<T> derives<T: Eq> Eq<Box<T>> = T
    2. let left: Box<int> = Box(3)
    3. let right: Box<int> = Box(3)
    4. let same: bool = Eq.equals(left, right)
    Expected outputexit 0
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Source evidence

    Require an explicit newtype conformance

    Link
    example.tpzRequire an explicit newtype conformanceOpen in Playground
    1. newtype Item = int
    2. let value = Item(7)
    3. let same = Eq.equals(value, value)
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5522 structures-no-visible-eq-conformance at example.tpz:3:22
    Source evidence

    Declaration type parameters

    Link

    Give a generic record an expected type

    Link
    example.tpzGive a generic record an expected type
    1. record Box<T> { value: T }
    2. let box: Box<int> = Box { value: 3 }
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    Nominal type parameter lists

    Record, enum, and newtype type parameters are comma-separated names inside angle brackets after the declaration name.

    Why

    Records, enums, and newtypes introduce type parameters in the same position and with the same delimiters. Readers do not need a separate binder notation for each declaration.

    Source evidence

    Expected types in generic record construction

    Why

    Records participate in the same nominal construction model as enums and newtypes.

    Source evidence

    Supply the complete nominal type through the expected context

    Construct a generic record, enum, or newtype in a context that supplies the same nominal declaration and all its type arguments. The expected arguments may themselves be enclosing generic parameters. Field or payload values alone do not supply omitted nominal arguments; a missing matching context is a type-argument arity error (TPZ5022). The declaration origin is part of that match; the current record-context retargeting defect is an implementation limitation.

    Why

    A complete expected nominal type determines every argument, including arguments with no field or payload evidence. Matching its declaration keeps unrelated nominal types distinct.

    Source evidence

    Nested construction follows explicit expected-type propagation

    A binding annotation, a declared argument type, a declared return or block-tail type, and parentheses can carry a complete nominal expectation to construction. Once a record is instantiated, its substituted declared field types provide expectations for nested constructions. A generic newtype uses the substituted base type from its matching expected newtype. Collection nesting follows the collection contextual-checking rule and preserves the matching nominal identity requirement.

    Why

    An instantiated field already has a substituted type. Passing it to nested construction lets that type information remain useful through grouping, calls, and returns.

    Source evidence

    Constructor type arguments come from nominal context

    Explicit call type-argument lists belong to generic call targets. Nominal constructors do not contribute declaration parameters to that call-parameter list: a newtype or enum constructor call with explicit type arguments is rejected as a non-generic call (TPZ5512). Put the full nominal type in the surrounding annotation or other expected context instead. Record construction keeps its brace syntax; this rule makes no spacing-dependent promise about parser lookahead.

    Why

    Nominal construction obtains its type arguments from the expected nominal type. Keeping that rule across constructors avoids a second instantiation path through call syntax.

    Source evidence

    Construction examples and repairs

    Link

    Missing a required field

    Link
    example.tpzMissing a required fieldOpen in Playground
    1. record Point { x: int, y: int }
    2. let point = Point { x: 1 }
    Recorded outputexit 1
    No stdout.
    error[TPZ5004]: record `Point` is missing field `y`
     --> example.tpz:2:13
      |
    2 | let point = Point { x: 1 }
      |             ^^^^^^^^^^^^^^
    
    example.tpz: 1 type diagnostic
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5004 record Point is missing field y at example.tpz:2:13
    Observed assertions · passed · 5.21.3

    Exit code 1.

    No stdout.

    Observed stderr
    1. error[TPZ5004]: record `Point` is missing field `y`
    2. --> example.tpz:2:13
    3. |
    4. 2 | let point = Point { x: 1 }
    5. | ^^^^^^^^^^^^^^
    6. example.tpz: 1 type diagnostic
    • Passed · expected nonzero; observed 1
    • Passed · stdout matches the authored expectation
    • Passed · checking TPZ5004; cause record `Point` is missing field `y`; primary location asserted; additional diagnostics allow
    Source evidence

    Supply the required field

    Link
    example.tpzSupply the required field
    1. record Point { x: int, y: int }
    2. let point = Point { x: 1, y: 2 }
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    A repeated field declaration

    Link
    example.tpzA repeated field declarationOpen in Playground
    1. record Point {
    2. x: int,
    3. x: int,
    4. }
    Recorded outputexit 1
    No stdout.
    error[TPZ5008]: record field `x` is declared twice in `Point`
     --> example.tpz:3:3
      |
    3 |   x: int,
      |   ^^^^^^
    
    example.tpz: 1 type diagnostic
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5008 record field x is declared twice in Point at example.tpz:3:3
    Observed assertions · passed · 5.21.3

    Exit code 1.

    No stdout.

    Observed stderr
    1. error[TPZ5008]: record field `x` is declared twice in `Point`
    2. --> example.tpz:3:3
    3. |
    4. 3 | x: int,
    5. | ^^^^^^
    6. example.tpz: 1 type diagnostic
    • Passed · expected nonzero; observed 1
    • Passed · stdout matches the authored expectation
    • Passed · checking TPZ5008; cause record field `x` is declared twice in `Point`; primary location asserted; additional diagnostics allow
    Source evidence

    Give each declared field its own name

    Link
    example.tpzGive each declared field its own name
    1. record Point {
    2. x: int,
    3. y: int,
    4. }
    5. let point = Point { x: 1, y: 2 }
    Recorded outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Observed assertions · passed · 5.21.3

    Exit code 0.

    Observed stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)

    No stderr.

    • Passed · expected 0; observed 0
    • Passed · stdout matches the authored expectation
    • Passed · stderr matches the authored expectation
    Source evidence

    An extra newtype constructor argument

    Link
    example.tpzAn extra newtype constructor argumentOpen in Playground
    1. newtype UserId = int
    2. let user = UserId(7, 8)
    Recorded outputexit 1
    No stdout.
    error[TPZ5004]: newtype `UserId` constructor takes 1 argument (the `int` to wrap), found 2
     --> example.tpz:2:12
      |
    2 | let user = UserId(7, 8)
      |            ^^^^^^^^^^^^
    
    example.tpz: 1 type diagnostic
    
    $ topaz check example.tpz · recorded with topaz 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5004 newtype UserId constructor takes 1 argument at example.tpz:2:12
    Observed assertions · passed · 5.21.3

    Exit code 1.

    No stdout.

    Observed stderr
    1. error[TPZ5004]: newtype `UserId` constructor takes 1 argument (the `int` to wrap), found 2
    2. --> example.tpz:2:12
    3. |
    4. 2 | let user = UserId(7, 8)
    5. | ^^^^^^^^^^^^
    6. example.tpz: 1 type diagnostic
    • Passed · expected nonzero; observed 1
    • Passed · stdout matches the authored expectation
    • Passed · checking TPZ5004; cause newtype `UserId` constructor takes 1 argument; primary location asserted; additional diagnostics allow
    Source evidence

    Defaults run for each omitted field

    Link
    example.tpzDefaults run for each omitted field
    1. import std.io
    2. function fallback() -> int {
    3. io.writeLine("default")
    4. 7
    5. }
    6. record Sample { value: int = fallback() }
    7. let a = Sample {}
    8. let b = Sample { value: 9 }
    9. let c = Sample {}
    10. io.writeLine("{a.value}:{b.value}:{c.value}")
    Expected outputexit 0
    default
    default
    7:9:7
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. default
    2. default
    3. 7:9:7
    Source evidence

    Reject a default of the wrong field type

    Link
    example.tpzReject a default of the wrong field typeOpen in Playground
    1. record BadDefault { value: int = "bad" }
    2. let value = BadDefault {}
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5001 structures-default-type-mismatch at example.tpz:1:34
    Source evidence

    Use a default of the declared field type

    Link
    example.tpzUse a default of the declared field type
    1. record BadDefault { value: int = 7 }
    2. let value = BadDefault {}
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    A spread precedes source-ordered replacements

    Link
    example.tpzA spread precedes source-ordered replacements
    1. import std.io
    2. function mark(label: string, value: int) -> int {
    3. io.writeLine(label)
    4. value
    5. }
    6. record Pair { x: int = mark("default-x", 1), y: int = mark("default-y", 2) }
    7. function seed() -> Pair {
    8. io.writeLine("seed")
    9. Pair { x: 10, y: 20 }
    10. }
    11. let updated = Pair { ...seed(), y: mark("y", 7), x: mark("x", 8) }
    12. let point = { x: 1, y: 2 }
    13. let moved = point { y: mark("structural-y", 3) }
    14. io.writeLine("{updated.x}:{updated.y}:{point.y}:{moved.y}")
    Expected outputexit 0
    seed
    y
    x
    structural-y
    8:7:2:3
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. seed
    2. y
    3. x
    4. structural-y
    5. 8:7:2:3
    Source evidence

    Reject value-update syntax on a nominal record

    Link
    example.tpzReject value-update syntax on a nominal recordOpen in Playground
    1. record Point { x: int }
    2. let point = Point { x: 1 }
    3. let moved = point { x: 2 }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5001 structures-nominal-value-update at example.tpz:3:13
    Source evidence

    Use a named construction with a spread

    Link
    example.tpzUse a named construction with a spread
    1. record Point { x: int }
    2. let point = Point { x: 1 }
    3. let moved = Point { ...point, x: 2 }
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    Check an empty enum and a finite recursive enum construction

    Link
    example.tpzCheck an empty enum and a finite recursive enum construction
    1. enum Empty {}
    2. enum Tree { Leaf(int), Branch(Array<Tree>) }
    3. let leaf: Tree = Tree.Leaf(3)
    4. let tree: Tree = Tree.Branch([leaf])
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    Reject a repeated variant in one enum

    Link
    example.tpzReject a repeated variant in one enumOpen in Playground
    1. enum Choice { A, A }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5008 structures-duplicate-enum-variant at example.tpz:1:18
    Source evidence

    Check distinct enum variants

    Link
    example.tpzCheck distinct enum variants
    1. enum Choice { A, B }
    2. let value: Choice = Choice.B()
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    Annotations, arguments, returns and substituted record fields

    Link
    example.tpzAnnotations, arguments, returns and substituted record fields
    1. record Box<T> { value: T }
    2. newtype Wrapped<T> = T
    3. function make<T>(value: T) -> Box<T> { Box { value: value } }
    4. function wrap<T>(value: T) -> Wrapped<T> { Wrapped(value) }
    5. function consume(value: Box<int>) -> int { value.value }
    6. let nested: Box<Box<int>> = Box { value: Box { value: 3 } }
    7. let argument = consume(Box { value: 2 })
    8. let made: Box<string> = make("seven")
    9. let wrapped: Wrapped<int> = Wrapped(6)
    10. let genericWrapped: Wrapped<string> = wrap("value")
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    Fields do not infer a generic record specialization

    Link
    example.tpzFields do not infer a generic record specializationOpen in Playground
    1. record Box<T> { value: T }
    2. let box = Box { value: 1 }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5022 structures-generic-record-context at example.tpz:2:11
    Source evidence

    An annotation supplies the record specialization

    Link
    example.tpzAn annotation supplies the record specialization
    1. record Box<T> { value: T }
    2. let box: Box<int> = Box { value: 1 }
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    A payload does not infer a generic newtype specialization

    Link
    example.tpzA payload does not infer a generic newtype specializationOpen in Playground
    1. newtype Wrapped<T> = T
    2. let wrapped = Wrapped(1)
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5022 structures-generic-newtype-context at example.tpz:2:15
    Source evidence

    An annotation supplies the newtype specialization

    Link
    example.tpzAn annotation supplies the newtype specialization
    1. newtype Wrapped<T> = T
    2. let wrapped: Wrapped<int> = Wrapped(1)
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    Nominal parameters are not generic call parameters

    Link
    example.tpzNominal parameters are not generic call parametersOpen in Playground
    1. newtype Wrapped<T> = T
    2. let wrapped: Wrapped<int> = Wrapped<int>(1)
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5512 structures-nominal-call-type-arguments at example.tpz:2:29
    Source evidence

    Put newtype arguments in the annotation

    Link
    example.tpzPut newtype arguments in the annotation
    1. newtype Wrapped<T> = T
    2. let wrapped: Wrapped<int> = Wrapped(1)
    Expected outputexit 0
    example.tpz: types-ok (1 module)
    example.tpz: resolve-ok (1 module)
    
    $ topaz check example.tpz · target product 5.21.3
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (1 module)
    2. example.tpz: resolve-ok (1 module)
    Source evidence

    A foreign expectation cannot change the constructor declaration

    Link
    example.tpzA foreign expectation cannot change the constructor declarationOpen in Playground
    1. import models { Box as RemoteBox }
    2. record Box<T> { value: T }
    3. let wrong: RemoteBox<int> = Box { value: 1 }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz · target product 5.21.3
    models.tpzA foreign expectation cannot change the constructor declarationOpen in Playground
    1. export record Box<T> { value: T }
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5022 structures-generic-record-context at example.tpz:3:29

    Source-derived candidate: the checker can retarget the constructor before the origin guard. The intended TPZ5022 expectation is retained; no product execution receipt has been fabricated.

    Source evidence

    Use the constructor whose declaration matches the expectation

    Link
    example.tpzUse the constructor whose declaration matches the expectationOpen in Playground
    1. import models { Box as RemoteBox }
    2. record Box<T> { value: T }
    3. let wrong: RemoteBox<int> = RemoteBox { value: 1 }
    Expected outputexit 0
    example.tpz: types-ok (2 modules)
    example.tpz: resolve-ok (2 modules)
    
    $ topaz check example.tpz · target product 5.21.3
    models.tpzUse the constructor whose declaration matches the expectationOpen in Playground
    1. export record Box<T> { value: T }
    Expected result

    Expected result

    Checking accepts the program. Exit code 0.

    Expected stdout
    1. example.tpz: types-ok (2 modules)
    2. example.tpz: resolve-ok (2 modules)
    Source evidence

    A structural update cannot repeat a field

    Link
    example.tpzA structural update cannot repeat a fieldOpen in Playground
    1. let point = { x: 1 }
    2. let moved = point {
    3. x: 2,
    4. x: 3,
    5. }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5008 duplicate-record-field-initializer at example.tpz:4:3
    Source evidence

    A later initializer cannot hide an incompatible earlier value

    Link
    example.tpzA later initializer cannot hide an incompatible earlier valueOpen in Playground
    1. let point = { x: 1 }
    2. let moved = point {
    3. x: "bad",
    4. x: 2,
    5. }
    Expected outputNonzero exit
    No stdout.
    $ topaz check example.tpz --format json · target product 5.21.3
    Expected result

    Expected result

    Rejection during check. Nonzero exit.

    • TPZ5008 duplicate-record-field-initializer at example.tpz:4:3
    Source evidence

    One explicit initializer can replace a copied field

    Link
    example.tpzOne explicit initializer can replace a copied field
    1. import std.io
    2. record Point { x: int }
    3. let original = { x: 1 }
    4. let updated = original { x: 2 }
    5. let named = Point { x: 3 }
    6. let copied = Point { ...named, x: 4 }
    7. io.writeLine("{updated.x}:{original.x}:{copied.x}:{named.x}")
    Expected outputexit 0
    2:1:4:3
    
    $ topaz run example.tpz · target product 5.21.3
    Expected result

    Expected result

    Execution completes. Exit code 0.

    Expected stdout
    1. 2:1:4:3
    Source evidence

    Related diagnostics

    Link

    Wrong argument count

    Redeclaration

    Unknown member or field

    TPZ5001

    TPZ5022

    Explicit type arguments are not allowed here

    Missing protocol conformance

    Unexpected token

    Menu

    Download