Skip to main content

Type Syntax Reference

This is a comprehensive quick reference for the Constellation type system. Use this as a lookup guide when implementing modules, debugging type errors, or working with the compiler.

Quick Type Lookup Table

Type CategoryConstellation SyntaxCTypeScala TypeExample Value
Primitives
StringStringCType.CStringString"hello"
IntegerIntCType.CIntLong42
FloatFloatCType.CFloatDouble3.14
BooleanBooleanCType.CBooleanBooleantrue
Collections
ListList<T>CType.CList(T)List[T][1, 2, 3]
MapMap<K, V>CType.CMap(K, V)Map[K, V]{"a": 1}
Structured
Record{ field: Type }CType.CProduct(Map)Case class{ name: "Alice" }
UnionA | BCType.CUnion(Map)-Tagged variant
OptionalOptional<T>CType.COptional(T)Option[T]Some(42) or None
Special
Nothing(implicit)CType.CString-[] (empty list)

Primitive Types

String

Constellation Syntax:

in text: String
in name: String

Runtime Type:

CType.CString

Runtime Value:

CValue.CString(value: String)

Scala Mapping:

  • Scala type: String
  • Type tag: given CTypeTag[String] => CType.CString
  • Injector: CValueInjector[String].inject("hello") => CValue.CString("hello")
  • Extractor: CValueExtractor[String].extract(CValue.CString("hello")) => IO("hello")

Examples:

# Input declaration
in message: String

# String literal
greeting = "Hello, World!"

# Module with String parameter
uppercase = Uppercase(message)

Common Operations:

  • Concatenation (via modules like Concat)
  • Transformation (via modules like Uppercase, Lowercase, Trim)
  • Pattern matching (via modules like Match, Replace)

Int

Constellation Syntax:

in count: Int
in age: Int

Runtime Type:

CType.CInt

Runtime Value:

CValue.CInt(value: Long)  // 64-bit signed integer

Scala Mapping:

  • Scala type: Long
  • Type tag: given CTypeTag[Long] => CType.CInt
  • Injector: CValueInjector[Long].inject(42L) => CValue.CInt(42)
  • Extractor: CValueExtractor[Long].extract(CValue.CInt(42)) => IO(42L)

Examples:

# Input declaration
in count: Int

# Integer literal
answer = 42

# Module with Int parameter
doubled = Double(count)

Important Notes:

  • No implicit conversion between Int and Float
  • No arithmetic operators in constellation-lang (use modules)
  • Literals are inferred as Int unless decimal point is present

Float

Constellation Syntax:

in ratio: Float
in temperature: Float

Runtime Type:

CType.CFloat

Runtime Value:

CValue.CFloat(value: Double)  // 64-bit double precision

Scala Mapping:

  • Scala type: Double
  • Type tag: given CTypeTag[Double] => CType.CFloat
  • Injector: CValueInjector[Double].inject(3.14) => CValue.CFloat(3.14)
  • Extractor: CValueExtractor[Double].extract(CValue.CFloat(3.14)) => IO(3.14)

Examples:

# Input declaration
in ratio: Float

# Float literal (must have decimal point)
pi = 3.14159

# Module with Float parameter
rounded = Round(pi)

Important Notes:

  • Decimal point required for float literals: 3.14 (not 3)
  • No implicit conversion from Int to Float
  • Use modules for math operations (no built-in operators)

Boolean

Constellation Syntax:

in enabled: Boolean
in active: Boolean

Runtime Type:

CType.CBoolean

Runtime Value:

CValue.CBoolean(value: Boolean)

Scala Mapping:

  • Scala type: Boolean
  • Type tag: given CTypeTag[Boolean] => CType.CBoolean
  • Injector: CValueInjector[Boolean].inject(true) => CValue.CBoolean(true)
  • Extractor: CValueExtractor[Boolean].extract(CValue.CBoolean(true)) => IO(true)

Examples:

# Input declaration
in enabled: Boolean

# Boolean literals
isActive = true
isDisabled = false

# Used in conditional expressions
result = if enabled
then Process(data)
else Skip()

# Used with guards
guarded = ExpensiveOp(data) when enabled

Important Notes:

  • Two literals only: true and false
  • Used in conditionals (if/else, branch, when)
  • No boolean operators in constellation-lang (use modules like And, Or, Not)

Collection Types

List

Constellation Syntax:

in items: List<String>
in numbers: List<Int>
in people: List<{ name: String, age: Int }>

Runtime Type:

CType.CList(valuesType: CType)

Runtime Value:

CValue.CList(
value: Vector[CValue],
subtype: CType
)

Scala Mapping:

  • Scala type: List[A] or Vector[A]
  • Type tag: given CTypeTag[List[A]] => CType.CList(A's CType)
  • Injector: CValueInjector[List[A]].inject(List(1, 2, 3))
  • Extractor: CValueExtractor[List[A]].extract(listValue) => IO(List(...))

Examples:

# Input declaration
in tags: List<String>

# List literal
numbers = [1, 2, 3, 4, 5]

# Empty list (type inferred from context)
empty = []

# List of records
type Person = { name: String, age: Int }
in people: List<Person>

Element-wise Operations on List:

When a list contains record elements, special operations apply to each element:

type Item = { id: String, price: Float }
in items: List<Item>
in context: { currency: String }

# Merge adds context to EACH item
enriched = items + context
# Type: List<{ id: String, price: Float, currency: String }>

# Projection selects fields from EACH item
selected = items[id]
# Type: List<{ id: String }>

# Field access extracts field from EACH item
ids = items.id
# Type: List<String>

Type Properties:

  • Homogeneous: All elements must have the same type
  • Covariant: If A <: B, then List<A> <: List<B>
  • Empty list: [] has type List<Nothing> (compatible with any List<T>)

Common Patterns:

# Filter list
filtered = Filter(items, x => x.active)

# Map over list
transformed = Map(items, x => x.name)

# Extract field from list of records
names = people.name # Type: List<String>

# Flatten nested lists
flat = Flatten(nestedList)

Map<K, V>

Constellation Syntax:

in cache: Map<String, Int>
in lookup: Map<Int, String>

Runtime Type:

CType.CMap(
keysType: CType,
valuesType: CType
)

Runtime Value:

CValue.CMap(
value: Vector[(CValue, CValue)],
keysType: CType,
valuesType: CType
)

Scala Mapping:

  • Scala type: Map[K, V]
  • Type tag: given CTypeTag[Map[K, V]] => CType.CMap(K's CType, V's CType)
  • Injector: CValueInjector[Map[K, V]].inject(Map("a" -> 1))
  • Extractor: CValueExtractor[Map[K, V]].extract(mapValue) => IO(Map(...))

Examples:

# Input declaration
in metadata: Map<String, Int>

# Map construction (via modules)
cache = BuildMap(keys, values)

# Map lookup
value = MapGet(cache, "key")

Type Properties:

  • Keys are invariant: Map<A, V> and Map<B, V> are unrelated even if A <: B
  • Values are covariant: If A <: B, then Map<K, A> <: Map<K, B>
  • Homogeneous: All keys same type, all values same type

Important Notes:

  • No map literals in constellation-lang (construct via modules)
  • Key and value types explicit in runtime representation
  • Use modules for operations like MapGet, MapPut, MapKeys, MapValues

Candidates (Legacy Alias)

Constellation Syntax:

in items: Candidates<{ id: String }>

Modern Equivalent:

in items: List<{ id: String }>

Important Notes:

  • Legacy alias: Candidates<T> is fully equivalent to List<T>
  • Use List<T> in new code for clarity
  • Backwards compatible: Existing code using Candidates will continue to work
  • Same runtime type: Both map to CType.CList

Structured Types

Record Types (Product Types)

Constellation Syntax:

# Inline record type
in person: { name: String, age: Int }

# Type alias
type Person = {
name: String,
age: Int,
email: String
}
in person: Person

Runtime Type:

CType.CProduct(structure: Map[String, CType])

Runtime Value:

CValue.CProduct(
value: Map[String, CValue],
structure: Map[String, CType]
)

Scala Mapping:

  • Scala type: Case class (via Scala 3 Mirrors)
  • Type tag: Automatically derived for case classes
  • Injector: Automatically derived for case classes
  • Extractor: Automatically derived for case classes

Examples:

# Type definition
type Person = {
name: String,
age: Int,
email: String
}

# Input with record type
in person: Person

# Record literal
alice = {
name: "Alice",
age: 30,
email: "alice@example.com"
}

# Field access
name = person.name # Type: String
age = person.age # Type: Int

# Projection (select subset of fields)
basic = person[name, age] # Type: { name: String, age: Int }

Nested Records:

type Address = {
street: String,
city: String,
country: String
}

type Person = {
name: String,
address: Address
}

in person: Person

# Nested field access
city = person.address.city # Type: String

Scala Case Class Mapping:

// Define case class
case class Person(name: String, age: Long, email: String)

// CTypeTag automatically derived
// CType.CProduct(Map(
// "name" -> CType.CString,
// "age" -> CType.CInt,
// "email" -> CType.CString
// ))

// Use in module
val module = ModuleBuilder
.metadata("ProcessPerson", "Processes a person", 1, 0)
.implementationPure[Person, String] { person =>
s"${person.name} is ${person.age} years old"
}
.build

Subtyping (Width + Depth):

Records use structural subtyping:

type PersonBase = { name: String }
type PersonWithAge = { name: String, age: Int }
type PersonFull = { name: String, age: Int, email: String }

# Wider records are subtypes of narrower records
# PersonFull <: PersonWithAge <: PersonBase

in detailed: PersonFull

# Valid: pass wider type where narrower expected
result = ProcessBase(detailed) # ProcessBase expects { name: String }

Width subtyping: A record with more fields is a subtype of one with fewer fields.

Depth subtyping: Field types must also be subtypes.


Union Types

Constellation Syntax:

# Simple union
type Result = String | Int

# Record variant union
type APIResponse = {
status: Int,
data: String
} | {
status: Int,
error: String
}

# Multi-way union
type Value = String | Int | Float | Boolean

Semantic Type:

SemanticType.SUnion(members: Set[SemanticType])

Runtime Type:

CType.CUnion(structure: Map[String, CType])

Runtime Value:

CValue.CUnion(
value: CValue,
structure: Map[String, CType],
tag: String // Discriminator
)

Examples:

# Define union type
type Result = {
success: Boolean,
data: String
} | {
success: Boolean,
error: String
}

# Input with union type
in response: Result

# Branch on union members (pattern matching)
output = branch
when HasField(response, "data") => response.data
when HasField(response, "error") => response.error
otherwise => "unknown"

Union Flattening:

Nested unions are automatically flattened:

type A = String | Int
type B = Float | Boolean
type C = A | B

# C is equivalent to: String | Int | Float | Boolean

Subtyping:

type Success = { data: String }
type Failure = { error: String }
type Response = Success | Failure

# Each member is a subtype of the union
# Success <: Response
# Failure <: Response

# Union is subtype if ALL members are subtypes
type BaseRecord = { id: String }
type RecordA = { id: String, name: String }
type RecordB = { id: String, count: Int }
type UnionAB = RecordA | RecordB

# UnionAB <: BaseRecord (both members have 'id' field)

Important Notes:

  • Union members unordered: Order doesn't matter, A | B equals B | A
  • Automatically flattened: (A | B) | C becomes A | B | C
  • Used for variant returns: Success/failure, different result types
  • Runtime requires tags: Each variant has a discriminator tag

Optional

Constellation Syntax:

in maybeValue: Optional<Int>
in optionalName: Optional<String>

Semantic Type:

SemanticType.SOptional(inner: SemanticType)

Runtime Type:

CType.COptional(innerType: CType)

Runtime Value:

// Present value
CValue.CSome(value: CValue, innerType: CType)

// Absent value
CValue.CNone(innerType: CType)

Scala Mapping:

  • Scala type: Option[T]
  • Type tag: given CTypeTag[Option[T]] => CType.COptional(T's CType)
  • Injector: Some(42) => CValue.CSome(...), None => CValue.CNone(...)
  • Extractor: CValue.CSome(...) => Some(...), CValue.CNone(...) => None

Examples:

# Input declaration
in maybeCount: Optional<Int>

# Coalesce operator (??) provides fallback
count = maybeCount ?? 0
# If maybeCount is Some(42), count = 42
# If maybeCount is None, count = 0

# Guard expression produces Optional
in shouldProcess: Boolean
in data: String
result = Process(data) when shouldProcess
# Type: Optional<Result>
# Only runs Process if shouldProcess is true

# Chain optional operations
final = result ?? defaultValue

Working with Optional in Scala Modules:

case class LookupInput(key: String)
case class LookupOutput(value: Option[String])

val lookup = ModuleBuilder
.metadata("Lookup", "Looks up a value", 1, 0)
.implementation[LookupInput, LookupOutput] { input => IO {
val cache = Map("foo" -> "bar")
LookupOutput(value = cache.get(input.key))
}}
.build

// Usage in constellation-lang:
/*
in key: String
result = Lookup({ key: key })
# result: { value: Optional<String> }

value = result.value ?? "not found"
# value: String (unwrapped with default)
*/

Type Properties:

  • Covariant: If A <: B, then Optional<A> <: Optional<B>
  • Two runtime variants: CSome (present) and CNone (absent)
  • Used with guards: expr when condition produces Optional<T>
  • Unwrapped with ??: opt ?? fallback extracts value or uses fallback

Type Syntax in Declarations

Input Declarations

# Primitive inputs
in text: String
in count: Int
in ratio: Float
in enabled: Boolean

# Collection inputs
in tags: List<String>
in cache: Map<String, Int>

# Record inputs
in person: { name: String, age: Int }

# Union inputs
in result: { success: Boolean, data: String } | { success: Boolean, error: String }

# Optional inputs
in maybeValue: Optional<Int>

# Type alias
type Person = { name: String, age: Int }
in person: Person

Output Declarations

# Simple output
out result

# Multiple outputs
out name
out age
out email

# Output with projection
out person.name
out person.age

# Output with optional coalesce
out maybeValue ?? 0

Type Aliases

# Simple alias
type UserId = String

# Record alias
type Person = {
name: String,
age: Int,
email: String
}

# Union alias
type Result = {
success: Boolean,
data: String
} | {
success: Boolean,
error: String
}

# Nested type alias
type Address = { street: String, city: String }
type Person = {
name: String,
address: Address
}

# Using aliases
in user: Person
result = ProcessPerson(user)

Function Parameters

# Function with primitive parameters
result = Uppercase(text: String) -> String

# Function with record parameters
result = GreetPerson(person: { name: String, age: Int }) -> { greeting: String }

# Function with multiple parameters
result = Concat(a: String, b: String) -> String

# Function with optional parameters
result = Lookup(key: String) -> { value: Optional<String> }

Type Literals and Constructors

Literal Expressions

# String literal
text = "hello" # Type: String

# Integer literal
count = 42 # Type: Int

# Float literal (requires decimal point)
ratio = 3.14 # Type: Float

# Boolean literals
yes = true # Type: Boolean
no = false # Type: Boolean

# List literal
numbers = [1, 2, 3] # Type: List<Int>
names = ["Alice", "Bob"] # Type: List<String>
empty = [] # Type: List<Nothing>

# Record literal
person = {
name: "Alice",
age: 30,
email: "alice@example.com"
}
# Type: { name: String, age: Int, email: String }

# Nested record literal
user = {
name: "Alice",
address: {
street: "123 Main St",
city: "Springfield"
}
}
# Type: { name: String, address: { street: String, city: String } }

Type Inference from Literals

# Type inferred from literal
x = 42 # Inferred: Int
name = "Alice" # Inferred: String
ratio = 3.14 # Inferred: Float

# Type inferred from list elements
numbers = [1, 2, 3] # Inferred: List<Int>
mixed = [1, 2.5] # ERROR: inconsistent types

# Empty list needs context
in numbers: List<Int>
result = if condition
then numbers
else [] # Inferred as List<Int> from context

Type Compatibility Quick Reference

Assignment Compatibility Matrix

Can I assign type X to type Y?

From \ ToStringIntFloatBooleanListMap<K,V>RecordOptionalUnion
StringYesNoNoNoNoNoNoNoIf member
IntNoYesNoNoNoNoNoNoIf member
FloatNoNoYesNoNoNoNoNoIf member
BooleanNoNoNoYesNoNoNoNoIf member
ListNoNoNoNoIf A <: TNoNoNoIf member
Map<K,A>NoNoNoNoNoIf K=K, A <: VNoNoIf member
RecordNoNoNoNoNoNoWidth+DepthNoIf member
OptionalNoNoNoNoNoNoNoIf A <: TIf member
UnionNoNoNoNoNoNoNoNoAll members
NothingYesYesYesYesYesYesYesYesYes

Legend:

  • Yes: Always compatible
  • No: Never compatible
  • If A <: T: Compatible if element/inner type is a subtype
  • Width+Depth: Compatible via structural subtyping (width and depth rules)
  • If member: Compatible if type is a member of the union
  • All members: Compatible if all union members are subtypes of target

Subtyping Rules Summary

Primitives:
- No subtyping between different primitives
- String, Int, Float, Boolean only subtypes of themselves

Collections:
- List<A> <: List<B> if A <: B (covariant)
- Map<K, A> <: Map<K, B> if A <: B (values covariant, keys invariant)
- Optional<A> <: Optional<B> if A <: B (covariant)

Records:
- Width: { a: A, b: B } <: { a: A } (more fields is subtype)
- Depth: Field types must be subtypes

Unions:
- A <: (A | B) (member is subtype of union)
- (A | B) <: C if A <: C and B <: C (union is subtype if all members are)

Nothing:
- Nothing <: T for all T (bottom type)

Type Conversion: SemanticType ↔ CType

SemanticType to CType (Compile-time → Runtime)

SemanticType.toCType(semanticType: SemanticType): CType

Mapping Table:

SemanticTypeCTypeNotes
SStringCType.CStringDirect mapping
SIntCType.CIntDirect mapping
SFloatCType.CFloatDirect mapping
SBooleanCType.CBooleanDirect mapping
SNothingCType.CStringBottom type, no runtime repr (String by convention)
SRecord(fields)CType.CProduct(fields)Maps to product type
SList(elem)CType.CList(elem)Direct mapping
SMap(k, v)CType.CMap(k, v)Direct mapping
SOptional(inner)CType.COptional(inner)Direct mapping
SFunction(...)ERRORFunctions don't exist at runtime
SUnion(members)CType.CUnion(tagMap)Union with tags
RowVar(id)ERRORMust be resolved during type checking
SOpenRecord(...)ERRORMust be closed during type checking

Example:

import io.constellation.lang.semantic.SemanticType.*

// Convert semantic type to runtime type
val semanticType = SRecord(Map(
"name" -> SString,
"age" -> SInt
))

val runtimeType = SemanticType.toCType(semanticType)
// => CType.CProduct(Map(
// "name" -> CType.CString,
// "age" -> CType.CInt
// ))

Types that CANNOT be converted:

  • SFunction - Functions are compile-time only
  • RowVar - Row variables must be resolved first
  • SOpenRecord - Open records must be closed first

CType to SemanticType (Runtime → Compile-time)

SemanticType.fromCType(cType: CType): SemanticType

Mapping Table:

CTypeSemanticTypeNotes
CType.CStringSStringDirect mapping
CType.CIntSIntDirect mapping
CType.CFloatSFloatDirect mapping
CType.CBooleanSBooleanDirect mapping
CType.CList(elem)SList(elem)Direct mapping
CType.CMap(k, v)SMap(k, v)Direct mapping
CType.CProduct(fields)SRecord(fields)Maps to record type
CType.CUnion(fields)SUnion(members)Union type
CType.COptional(inner)SOptional(inner)Direct mapping

Example:

import io.constellation.{CType, lang.semantic.SemanticType}

// Convert runtime type to semantic type
val runtimeType = CType.CProduct(Map(
"name" -> CType.CString,
"age" -> CType.CInt
))

val semanticType = SemanticType.fromCType(runtimeType)
// => SRecord(Map(
// "name" -> SString,
// "age" -> SInt
// ))

Scala Interop: Type Classes

CTypeTag - Compile-time Type Derivation

trait CTypeTag[A] {
def cType: CType
}

Given Instances:

// Primitives
given CTypeTag[String] => CType.CString
given CTypeTag[Long] => CType.CInt
given CTypeTag[Double] => CType.CFloat
given CTypeTag[Boolean] => CType.CBoolean

// Collections
given CTypeTag[List[A]] => CType.CList(A's CType)
given CTypeTag[Vector[A]] => CType.CList(A's CType)
given CTypeTag[Map[K, V]] => CType.CMap(K's CType, V's CType)
given CTypeTag[Option[A]] => CType.COptional(A's CType)

// Case classes (automatically derived via Scala 3 Mirrors)
case class Person(name: String, age: Long)
// CTypeTag[Person] => CType.CProduct(Map(
// "name" -> CType.CString,
// "age" -> CType.CInt
// ))

Usage:

import io.constellation.deriveType

// Derive CType from Scala type
val stringType = deriveType[String] // => CType.CString
val listType = deriveType[List[String]] // => CType.CList(CType.CString)

case class Point(x: Long, y: Long)
val pointType = deriveType[Point] // => CType.CProduct(...)

CValueInjector - Scala Value → CValue

trait CValueInjector[A] {
def inject(value: A): CValue
}

Given Instances:

// Primitives
CValueInjector[String].inject("hello") => CValue.CString("hello")
CValueInjector[Long].inject(42L) => CValue.CInt(42)
CValueInjector[Double].inject(3.14) => CValue.CFloat(3.14)
CValueInjector[Boolean].inject(true) => CValue.CBoolean(true)

// Collections
CValueInjector[List[Long]].inject(List(1, 2, 3))
=> CValue.CList(Vector(CValue.CInt(1), CValue.CInt(2), CValue.CInt(3)), CType.CInt)

CValueInjector[Map[String, Long]].inject(Map("a" -> 1))
=> CValue.CMap(Vector((CValue.CString("a"), CValue.CInt(1))), CType.CString, CType.CInt)

// Optional
CValueInjector[Option[Long]].inject(Some(42))
=> CValue.CSome(CValue.CInt(42), CType.CInt)

CValueInjector[Option[Long]].inject(None)
=> CValue.CNone(CType.CInt)

Example Usage in Modules:

case class Input(text: String, count: Long)
case class Output(result: String)

val module = ModuleBuilder
.metadata("MyModule", "Example", 1, 0)
.implementationPure[Input, Output] { input =>
// Input automatically extracted from CValue
// Output automatically injected to CValue
Output(result = input.text * input.count.toInt)
}
.build

CValueExtractor - CValue → Scala Value

trait CValueExtractor[A] {
def extract(data: CValue): IO[A]
}

Given Instances:

// Primitives
CValueExtractor[String].extract(CValue.CString("hello"))
=> IO("hello")

CValueExtractor[Long].extract(CValue.CInt(42))
=> IO(42L)

// Collections
CValueExtractor[List[Long]].extract(
CValue.CList(Vector(CValue.CInt(1), CValue.CInt(2)), CType.CInt)
) => IO(List(1L, 2L))

// Optional
CValueExtractor[Option[Long]].extract(CValue.CSome(CValue.CInt(42), CType.CInt))
=> IO(Some(42L))

CValueExtractor[Option[Long]].extract(CValue.CNone(CType.CInt))
=> IO(None)

// Type mismatch raises error
CValueExtractor[String].extract(CValue.CInt(42))
=> IO.raiseError(RuntimeException("Expected CValue.CString, but got CInt(42)"))

Common Type Patterns

Pattern 1: Simple Module with Primitives

// Scala
case class UppercaseInput(text: String)
case class UppercaseOutput(result: String)

val uppercase = ModuleBuilder
.metadata("Uppercase", "Converts text to uppercase", 1, 0)
.implementationPure[UppercaseInput, UppercaseOutput] { input =>
UppercaseOutput(result = input.text.toUpperCase)
}
.build
# constellation-lang
in text: String
result = Uppercase({ text: text })
out result.result

Pattern 2: List Processing

// Scala
case class FilterInput(items: List[String], pattern: String)
case class FilterOutput(filtered: List[String], count: Long)

val filterStrings = ModuleBuilder
.metadata("FilterStrings", "Filters strings by pattern", 1, 0)
.implementationPure[FilterInput, FilterOutput] { input =>
val filtered = input.items.filter(_.contains(input.pattern))
FilterOutput(filtered, filtered.size.toLong)
}
.build
# constellation-lang
in items: List<String>
in pattern: String

result = FilterStrings({ items: items, pattern: pattern })
out result.filtered
out result.count

Pattern 3: Optional Values

// Scala
case class LookupInput(key: String)
case class LookupOutput(value: Option[String])

val lookup = ModuleBuilder
.metadata("Lookup", "Looks up a value", 1, 0)
.implementation[LookupInput, LookupOutput] { input => IO {
val cache = Map("foo" -> "bar")
LookupOutput(value = cache.get(input.key))
}}
.build
# constellation-lang
in key: String
result = Lookup({ key: key })
value = result.value ?? "not found"
out value

Pattern 4: Nested Records

// Scala
case class Address(street: String, city: String, country: String)
case class Person(name: String, age: Long, address: Address)
case class Output(city: String)

val extractCity = ModuleBuilder
.metadata("ExtractCity", "Extracts city from person", 1, 0)
.implementationPure[Person, Output] { person =>
Output(city = person.address.city)
}
.build
# constellation-lang
type Address = {
street: String,
city: String,
country: String
}

type Person = {
name: String,
age: Int,
address: Address
}

in person: Person
result = ExtractCity(person)
out result.city

Pattern 5: Record with Structural Subtyping

// Scala - accepts any record with "name" field
case class WithName(name: String)
case class Output(greeting: String)

val greet = ModuleBuilder
.metadata("Greet", "Greets by name", 1, 0)
.implementationPure[WithName, Output] { input =>
Output(greeting = s"Hello, ${input.name}!")
}
.build
# constellation-lang
type PersonBase = { name: String }
type PersonFull = { name: String, age: Int, email: String }

in person: PersonFull

# Works because PersonFull <: PersonBase
result = Greet(person) # Greet expects { name: String }
out result.greeting

Pattern 6: List Element-wise Operations

type Item = { id: String, price: Float }
in items: List<Item>

# Add currency to each item
in currency: { currency: String }
enriched = items + currency
# Type: List<{ id: String, price: Float, currency: String }>

# Project id from each item
ids = items[id]
# Type: List<{ id: String }>

# Extract price from each item
prices = items.price
# Type: List<Float>

Pattern 7: Union for Result Types

// Scala - using separate case classes for union variants
case class Success(data: String)
case class Failure(error: String, code: Long)

// Module returns union via Either or ADT
// (union handling at constellation level, not Scala module level)
# constellation-lang
type Result = {
success: Boolean,
data: String
} | {
success: Boolean,
error: String
}

in response: Result

# Pattern match on union
output = branch
when HasField(response, "data") => response.data
when HasField(response, "error") => response.error
otherwise => "unknown"

Pattern 8: Guard for Conditional Execution

in shouldProcess: Boolean
in data: String

# Only run expensive operation if condition is true
result = ExpensiveOp(data) when shouldProcess
# Type: Optional<Result>

# Unwrap with fallback
final = result ?? { result: "skipped" }
out final.result

Type Error Quick Reference

Common Type Errors and Solutions

1. Type Mismatch

Error:

Type mismatch: expected Int, got String

Cause: Passing wrong type to module parameter.

Solution:

# Wrong
in count: String
result = Double(count) # Double expects Int

# Correct
in count: Int
result = Double(count)

2. Undefined Variable

Error:

Undefined variable: foo

Cause: Using variable before declaration.

Solution:

# Wrong
result = Process(foo)

# Correct
in foo: String
result = Process(foo)

3. Undefined Type

Error:

Undefined type: Person

Cause: Using type before definition.

Solution:

# Wrong
in person: Person

# Correct
type Person = { name: String, age: Int }
in person: Person

4. Invalid Field Access

Error:

Invalid field access: field 'email' not found. Available: name, age

Cause: Accessing non-existent field.

Solution:

# Wrong
type Person = { name: String, age: Int }
in person: Person
email = person.email

# Correct (add field to type)
type Person = { name: String, age: Int, email: String }
in person: Person
email = person.email

5. Invalid Projection

Error:

Invalid projection: field 'email' not found. Available: name, age

Cause: Projecting non-existent fields.

Solution:

# Wrong
in person: { name: String, age: Int }
subset = person[name, email]

# Correct
subset = person[name, age]

6. Incompatible Merge

Error:

Cannot merge types: String + Int

Cause: Using + with non-record types.

Solution:

# Wrong
in a: String
in b: Int
result = a + b

# Correct (merge is for records)
in a: { name: String }
in b: { age: Int }
result = a + b # Type: { name: String, age: Int }

7. Projection on Non-Record

Error:

Projection requires a record type, got String

Cause: Using projection syntax on non-record.

Solution:

# Wrong
in text: String
result = text[name]

# Correct
in person: { name: String, age: Int }
result = person[name]

8. Field Access on Non-Record

Error:

Field access requires a record type, got List<String>

Cause: Using dot notation on wrong type.

Solution:

# Wrong
in tags: List<String>
first = tags.name

# Correct (field access works on List<Record>)
in items: List<{ name: String }>
names = items.name # Type: List<String>

9. Coalesce on Non-Optional

Error:

Left side of ?? must be Optional, got Int

Cause: Using ?? with non-optional value.

Solution:

# Wrong
in value: Int
result = value ?? 0

# Correct
in value: Optional<Int>
result = value ?? 0

10. Module Name Case Mismatch

Error:

Function 'uppercase' not found. Did you mean 'Uppercase'?

Cause: Case-sensitive module name mismatch.

Solution:

// Scala
ModuleBuilder.metadata("Uppercase", ...).build
# Wrong
result = uppercase(text)

# Correct (exact case match)
result = Uppercase(text)

Advanced Type Features

Nothing Type (Bottom Type)

Properties:

  • Subtype of all types
  • Used for empty lists: [] has type List<Nothing>
  • Cannot be explicitly written
  • Has no runtime representation (uses CType.CString by convention)

Examples:

# Empty list compatible with any list type
in numbers: List<Int>

result = if condition
then numbers
else [] # Type: List<Nothing>, compatible with List<Int>

# Empty list can be assigned to any list variable
in strings: List<String>
empty = [] # Type: List<Nothing>, compatible with List<String>

Function Types (Compile-time Only)

Semantic Type:

SemanticType.SFunction(
paramTypes: List[SemanticType],
returnType: SemanticType
)

Properties:

  • Exists only during type checking
  • Used for lambda expressions
  • Cannot be converted to CType (runtime error if attempted)
  • Contravariant in parameters, covariant in return

Example:

# Lambda in Filter function
in items: List<{ id: String, active: Boolean }>

# Lambda parameter type inferred from context
filtered = Filter(items, item => item.active)
# ^^^^
# Type: (item: { id: String, active: Boolean }) => Boolean

Row Polymorphism (Open Records)

Semantic Types:

// Row variable
SemanticType.RowVar(id: Int)

// Open record
SemanticType.SOpenRecord(
fields: Map[String, SemanticType],
rowVar: RowVar
)

Properties:

  • Open records have "at least" specified fields
  • Row variable represents "rest" of fields
  • Used for row-polymorphic functions
  • Must be resolved before runtime (cannot convert to CType)

Example:

// Function accepting any record with "name" field
val openType = SOpenRecord(
Map("name" -> SString),
RowVar(1)
)
// Pretty prints as: { name: String | ρ1 }

// Matches:
// - { name: String }
// - { name: String, age: Int }
// - { name: String, age: Int, email: String }

Summary

Key Takeaways

  1. Two Type Systems:

    • CType/CValue: Runtime types and values (execution)
    • SemanticType: Compile-time types (type checking)
  2. Primitive Types:

    • String, Int, Float, Boolean
    • No implicit conversions between primitives
    • No subtyping between different primitives
  3. Collection Types:

    • List<T>: Homogeneous, covariant
    • Map<K, V>: Keys invariant, values covariant
    • Element-wise operations on List<Record>
  4. Structured Types:

    • Records: Structural subtyping (width + depth)
    • Unions: Represent multiple possible types
    • Optional: Nullable values, covariant
  5. Type Inference:

    • Bidirectional: bottom-up (synthesis) and top-down (checking)
    • Empty lists type as List<Nothing>
    • Lambda parameters inferred from context
  6. Subtyping:

    • Records: wider <: narrower (more fields is subtype)
    • Collections: covariant in element types (except Map keys)
    • Unions: member is subtype of union
    • Nothing: bottom type, subtype of all
  7. Scala Interop:

    • CTypeTag: Compile-time type derivation
    • CValueInjector: Scala value → CValue
    • CValueExtractor: CValue → Scala value
    • Automatic derivation for case classes
  8. Special Types:

    • Nothing: Bottom type, subtype of all
    • Function types: Compile-time only, no runtime representation
    • Open records: Row polymorphism, must be closed before runtime

  • Language types: website/docs/language/types.md
  • Type system foundations: website/docs/llm/foundations/type-system.md
  • Type algebra: website/docs/language/type-algebra.md
  • Module development: website/docs/llm/patterns/module-development.md

Quick Reference Cheat Sheet

PRIMITIVES:
String, Int, Float, Boolean
No implicit conversions, no subtyping

COLLECTIONS:
List<T> - covariant
Map<K, V> - keys invariant, values covariant

RECORDS:
{ field: Type }
Structural subtyping: wider <: narrower

UNIONS:
A | B | C
Member is subtype of union

OPTIONAL:
Optional<T>
Covariant, use with ?? operator

TYPE INFERENCE:
Bottom-up: literals → types
Top-down: context → expected types
Empty list: List<Nothing>

SCALA INTEROP:
deriveType[T] → CType
CValueInjector[T].inject → CValue
CValueExtractor[T].extract → IO[T]

SUBTYPING:
Nothing <: T (all types)
{ a: A, b: B } <: { a: A } (width)
List<A> <: List<B> (if A <: B)
A <: (A | B) (union membership)