spec_to_rest
Spec language foundations

Type system

Edit on GitHub

Primitives, compounds, entity and relation types, refinements, expression types, and compatibility

Last updated:

Primitive types

TypeValuesDefaultSQLJSON
StringUnicode text""TEXT or VARCHAR(n)string
Intarbitrary-precision integers0INTEGER or BIGINTinteger
FloatIEEE 754 double0.0DOUBLE PRECISIONnumber
Booltrue, falsefalseBOOLEANboolean
DateTimeUTC timestampepochTIMESTAMPTZstring (ISO 8601)
Durationa span of time0BIGINT (seconds)integer
UUIDa UUIDnoneUUID or TEXTstring

Underneath, the verifier's type system has four scalar types: Bool, Int, Real, and String. DateTime and Duration are integers (an epoch and a count of seconds), and UUID is a string. No primitive has subtypes, and any of them refines into a named, constrained type with a where clause.

Compound types

Set[T] is an unordered collection of unique values; Seq[T] is ordered and may repeat; Map[K, V] maps keys to values; Option[T] is a value or none.

tags:           Set[String]
login_attempts: Seq[LoginAttempt]
settings:       Map[String, String]
description:    Option[String]

A Set[T] becomes a JSON array, with uniqueness enforced in the application layer, backed by either a JSON column or a junction table. A Seq[T] keeps an ordering column. An Option[T] is a nullable column.

Entity types and tables

Each entity maps to a database table:

Entity featureDatabase mapping
Entity nametable name, pluralized and snake_case
Primitive fieldcolumn
Option[T] fieldnullable column
Set[T] of a primitiveJSON array column or junction table
Set[T] of an entityjunction table
Entity-typed fieldforeign-key column
where on a fieldCHECK constraint
Entity invariantCHECK constraint where SQL can express it, else app validation

For example,

entity Todo {
  id: Int where value > 0
  title: String where len(value) >= 1 and len(value) <= 200
  status: Status
  tags: Set[String]
}

generates:

CREATE TABLE todos (
  id INTEGER PRIMARY KEY CHECK (id > 0),
  title VARCHAR(200) NOT NULL CHECK (length(title) >= 1),
  status VARCHAR(20) NOT NULL CHECK (status IN ('TODO','IN_PROGRESS','DONE','ARCHIVED')),
  tags JSONB DEFAULT '[]'
);

Relation types and foreign keys

A relation type A -> mult B relates A and B with a multiplicity:

RelationDatabase mapping
A -> one Bcolumn on A: b_id INTEGER NOT NULL REFERENCES b(id)
A -> lone Bnullable column on A: b_id INTEGER REFERENCES b(id)
A -> some Bjunction table with an at-least-one constraint (trigger or app)
A -> set Bjunction table a_b(a_id REFERENCES a, b_id REFERENCES b)

A state relation maps the same way:

state {
  store: ShortCode -> lone LongURL
}

Whether that becomes one table or two joined by a foreign key depends on whether ShortCode and LongURL are entities or value types.

Parametric types

Set[T], Map[K, V], Seq[T], and Option[T] are the only parametric types, and there are no user-defined generics. Keeping the set closed means every type has a clear database and JSON mapping.

Refinement types

A where clause refines a base type with a predicate that must always hold. Inside the clause, value is the instance being constrained.

type ShortCode  = String where len(value) >= 6 and len(value) <= 10
                             and value matches /^[a-zA-Z0-9]+$/
type Money      = Int where value >= 0
type Percentage = Float where value >= 0.0 and value <= 100.0
type Email      = String where value matches /^[^@]+@[^@]+\.[^@]+$/

A refinement compiles to a CHECK constraint, request-body validation, the matching OpenAPI schema constraint, and a runtime check. It is not a separate type from its base: ShortCode is a String with extra constraints, so anything that takes a String takes a ShortCode. The reverse needs the constraint proven or checked, since an arbitrary String is not a valid ShortCode.

The type of an expression

Declarations carry explicit types, and each expression then has a determined type.

ExpressionType
integer literalInt
float literalFloat
string literalString
boolean literalBool
a field referencethe field's declared type
entity.fieldthat field's type
store[key]the relation's target type
dom(rel)a Set of the source type
ran(rel)a Set of the target type
#collectionInt
a + b on IntInt
a + b on setsthe same set type
a and bBool
x in SBool
pre(field), field'the field's type
if c then a else bthe branches' shared type (numeric branches widen)
a quantifierBool

Subtyping and compatibility

The type layer keeps a few compatibility rules rather than a full subtype lattice. A numeric value widens: an Int is accepted where a Float is expected, never the reverse, since the verifier joins Int and Real to Real. A refined type stands in for its base, because it is that base with a constraint. A bare value is accepted where an Option[T] is expected, wrapped in some. Enums are distinct types that do not widen into one another, and collections are invariant in their element type, so a Set[Child] is not a Set[Parent].

Entity inheritance is structural. entity Child extends Parent gives Child all of Parent's fields and invariants and lets it add more, but the type layer treats each entity as its own type, so substituting a Child for a Parent is not something it checks.

Type checking is partial today. The check command runs a narrow structural lint, L01, that catches a literal used at the wrong type; a full type checker is future work.

On this page