Skip to content

TypeDecl assigns a name to a Record or TaggedUnion BaseType, making it referenceable by name from fact type fields, list element_type positions, and other TypeDecl definitions. Named types allow complex structured types to be declared once and referenced by name throughout a contract.

TypeDecl is a DSL-layer convenience only. The elaborator resolves all named type references during Pass 3 (type environment construction) and inlines the full BaseType structure at every point of use during Pass 4 (AST materialization). TypeDecl entries do not appear in TenorInterchange output.

DSL Syntax

hcl
type <TypeId> {
  <field_name>: <BaseType>
  ...
}

Record TypeDecl

hcl
type Address {
  street: Text(max_length: 256)
  city:   Text(max_length: 128)
  state:  Text(max_length: 64)
  zip:    Text(max_length: 10)
}

TaggedUnion TypeDecl

hcl
type PaymentMethod {
  CreditCard:   Record(fields: { last_four: Text(max_length: 4), brand: Text(max_length: 20) })
  BankTransfer: Record(fields: { routing: Text(max_length: 9), account: Text(max_length: 4) })
  Wire:         Record(fields: { swift_code: Text(max_length: 11) })
}

Restriction

Named type aliases for scalar BaseTypes (Bool, Int, Decimal, Text, Enum, Date, DateTime, Money, Duration, List) are not permitted. Only Record and TaggedUnion benefit from naming.

hcl
// WRONG -- cannot alias scalar types
type Score = Int(min: 0, max: 100)

// CORRECT -- use the scalar type directly
fact credit_score {
  type:   Int(min: 0, max: 100)
  source: "credit_bureau.score"
}

Usage in Facts

A TypeDecl may be referenced anywhere a Record or TaggedUnion BaseType is expected:

hcl
type LineItem {
  id:          Text(max_length: 64)
  description: Text(max_length: 256)
  amount:      Money(currency: "USD")
  approved:    Bool
}

fact line_items {
  type:   List(element_type: LineItem, max: 100)
  source: "order_service.line_items"
}

fact shipping_address {
  type:   Address
  source: "order_service.shipping"
}

After elaboration, LineItem and Address are fully inlined. The interchange JSON contains the expanded Record structure, not a type reference.

Nested Type References

TypeDecl definitions may reference other TypeDecl definitions:

hcl
type Address {
  street: Text(max_length: 256)
  city:   Text(max_length: 128)
  state:  Text(max_length: 64)
  zip:    Text(max_length: 10)
}

type Customer {
  name:    Text(max_length: 200)
  email:   Text(max_length: 320)
  address: Address
}

fact customer_info {
  type:   Customer
  source: "crm.customer_details"
}

Customer references Address, which is resolved first. After Pass 3/4, the address field is inlined to its full Record structure.

Shared Type Libraries

A shared type library is a Tenor file containing only TypeDecl constructs and no other construct kinds. Type libraries enable cross-contract type reuse.

Library file (types/common.tenor):

hcl
type Address {
  street: Text(max_length: 256)
  city:   Text(max_length: 128)
  state:  Text(max_length: 64)
  zip:    Text(max_length: 10)
}

type Currency {
  code: Text(max_length: 3)
  name: Text(max_length: 64)
}

Contract importing the library:

hcl
import "types/common.tenor"

fact shipping_address {
  type:   Address
  source: "order_service.shipping"
}

type LineItem {
  id:      Text(max_length: 64)
  amount:  Money(currency: "USD")
  address: Address
}

Type Library Constraints

  • A type library file may not contain import declarations (no transitive imports).
  • A type library file may contain only TypeDecl constructs (no Fact, Entity, Rule, Persona, Operation, Flow).
  • Imported TypeDecl ids must not conflict with local TypeDecl ids or other imported TypeDecl ids.

Type Identity

Type identity is structural. After Pass 3/4 inlining, two types are identical if and only if their fully expanded BaseType structures are recursively equal, regardless of origin. A Record imported from a library and a Record declared locally are the same type if they have identical field names and field types.

Interchange Representation

TypeDecl has no interchange representation. Imported TypeDecl definitions are consumed during Pass 3 and inlined during Pass 4. The interchange output for a contract that imports shared types is identical to the interchange for a contract that declares those same types locally. No import reference, file path, or TypeDecl entry appears in the interchange bundle.

Full Working Example

A complete contract using TypeDecl for structured data:

hcl
// --- Shared types ---
type LineItem {
  sku:         Text(max_length: 32)
  description: Text(max_length: 256)
  quantity:    Int(min: 1, max: 10000)
  unit_price:  Money(currency: "USD")
  approved:    Bool
}

type ShippingInfo {
  carrier:          Text(max_length: 100)
  tracking_number:  Text(max_length: 64)
  estimated_days:   Duration(unit: "days", min: 1, max: 30)
}

// --- Personas ---
persona requestor
persona approver

// --- Entity ---
entity Requisition {
  states:  [draft, submitted, approved, rejected]
  initial: draft
  transitions: [
    (draft, submitted),
    (submitted, approved),
    (submitted, rejected)
  ]
}

// --- Facts using named types ---
fact line_items {
  type:   List(element_type: LineItem, max: 50)
  source: "procurement_service.line_items"
}

fact shipping_info {
  type:   ShippingInfo
  source: "logistics_service.shipping_details"
}

fact requisition_total {
  type:   Money(currency: "USD")
  source: "procurement_service.total"
}

// --- Rules ---
rule all_items_approved {
  stratum: 0
  when:    forall item in line_items . item.approved = true
  produce: verdict items_ok { payload: Bool = true }
}

rule budget_check {
  stratum: 0
  when:    requisition_total <= 50000.00
  produce: verdict within_budget { payload: Bool = true }
}

rule can_approve {
  stratum: 1
  when:    verdict_present(items_ok) and verdict_present(within_budget)
  produce: verdict approval_ready { payload: Bool = true }
}

// --- Operations ---
operation submit_requisition {
  personas: [requestor]
  require:  requisition_total > 0
  effects:  [Requisition: draft -> submitted]
  outcomes: [submitted]
}

operation approve_requisition {
  personas: [approver]
  require:  verdict_present(approval_ready)
  effects:  [Requisition: submitted -> approved]
  outcomes: [approved]
}

Constraints

  • TypeDecl ids are unique within a contract, including imported TypeDecl ids.
  • TypeDecl ids occupy a distinct namespace from other construct kinds. A TypeDecl named Foo does not conflict with an Entity named Foo.
  • The TypeDecl reference graph must be acyclic. If TypeDecl A contains a field of type TypeDecl B, and B contains a field of type A, this is a cycle. Cycle detection uses DFS. Violations: "TypeDecl cycle detected: A -> B -> C -> A".
  • A TypeDecl may only alias Record or TaggedUnion types. Scalar aliases are not permitted.

Common Mistakes

Aliasing scalar types. You cannot write type Score = Int(0, 100). Use scalar types directly in fact declarations.

Circular type references. type A { b: B } and type B { a: A } create a cycle that the elaborator rejects.

Expecting TypeDecl in interchange output. TypeDecl is resolved and inlined during elaboration. The interchange JSON contains expanded BaseType structures, not type names.

Confusing TypeDecl with entity or fact declarations. TypeDecl only creates reusable type aliases. It does not declare facts, entities, or any other construct.

How TypeDecl Connects to Other Constructs

  • Facts reference TypeDecl names in their type field (for Record/TaggedUnion types or List element types).
  • Other TypeDecls may reference TypeDecl names in field types (acyclic only).
  • Rules and operations use the expanded types in predicate expressions (field access on Record-typed facts, quantification over List-typed facts).
  • Interchange contains only expanded BaseType structures. TypeDecl is invisible after elaboration.

Static Analysis Properties

  • TypeDecl resolution occurs during elaboration (Pass 3/4) before any static analysis.
  • The expanded types feed into S7 (evaluation complexity bounds) for predicate expressions involving Record field access and List quantification.