Loom Language Specification v0.1
Status: Design document. Not yet implemented. Version: 0.1 Author: Jorge Ghiringhelli Date: 2025
Table of Contents
- Design Philosophy
- Syntax
- Type System
- Effect System
- Module System
- Contracts
- Compilation Pipeline
- The GS Connection
- What Loom Is Not
- Status and Next Steps
1. Design Philosophy
Loom is a programming language designed for AI generation and human review, not human typing. It compiles to LLVM IR and from there to native binaries, WASM, and hardware accelerator kernels. Every design decision flows from five principles.
Principle 1: Intent-Preserving
The language captures what a system should do, not how memory is laid out. There is no manual memory model, no stack discipline encoded in syntax, no pointer arithmetic. Those are compiler concerns. A Loom function says: “given this input, produce this output, with these effects.” It does not say how the values travel through registers.
Principle 2: Effect-Transparent
Every function that touches the outside world declares it in its type signature. Purity is the default. Effects are explicit, tracked through call chains, and verified by the compiler. Calling a NetworkCall function from a Pure context is a compile error, not a runtime surprise.
Principle 3: Contract-First
Preconditions and postconditions are part of the function signature. They are not comments, not documentation strings, not assertions scattered in the body. The compiler reads them. The type checker enforces them where it can. The runtime checks them where static verification is undecidable. A missing precondition that would allow an invalid state is a compile error.
Principle 4: Refinement-Typed
Types encode invariants, not just shapes. Email is not String. PositiveInt is not Int. NonEmptyList<T> is not List<T>. Refinements are predicates attached to base types. Where the predicate is decidable at compile time, it is enforced there. Where it is not, it is enforced at the first point of construction.
Principle 5: No Ceremony
No null. No exceptions. No undefined behavior. No implicit coercions. No overloaded operators with surprising semantics. Absence is Option<T>. Failure is Result<T, E>. Branching over variants is match, and the compiler refuses to accept non-exhaustive matches. Every construct has one meaning.
2. Syntax
Loom syntax is designed to be readable by humans reviewing AI-generated code. It borrows structural ideas from ML and Rust while removing historical accidents. It is not an S-expression language. Significant whitespace is not used for block structure; blocks use end terminators.
2.1 Lexical Conventions
- Keywords:
module,fn,type,enum,let,match,with,require,ensure,import,spec,provides,effect,where,end - Identifiers:
snake_casefor values and functions,PascalCasefor types and modules - Comments:
--for line comments,{- -}for block comments - String literals: double-quoted, UTF-8
- Numeric literals:
42,3.14,0xFF,1_000_000
2.2 Module Declaration
Every file is a module. The module header is mandatory.
module UserService
spec "Manages user lifecycle: creation, lookup, deactivation."
provides
create_user : CreateUserRequest -> Effect<[DatabaseWrite, Log], Result<User, UserError>>
find_user : UserId -> Effect<[DatabaseRead], Option<User>>
deactivate : UserId -> Effect<[DatabaseWrite, Log], Result<Unit, UserError>>
requires
user_repository : UserRepository
logger : Logger
end
The spec field is a single sentence. The compiler enforces this. The provides block lists the full typed signature of every exported operation. The requires block lists dependencies as typed interfaces, never as concrete implementations.
2.3 Function Definitions
fn create_user :: CreateUserRequest -> Effect<[DatabaseWrite, Log], Result<User, UserError>>
require: request.email is Email
require: request.password.length >= 8
ensure: result is Ok implies result.value.email == request.email
with user_repository, logger
let normalized = normalize_email(request.email)
let hashed = hash_password(request.password)
let candidate = User.new(normalized, hashed)
match user_repository.insert(candidate)
Ok(saved) -> log_then_return(logger, "user.created", saved)
Err(e) -> Err(UserError.from_repository_error(e))
end
end
The :: separator reads “has type.” The effect list [DatabaseWrite, Log] lists every effect the function may perform. The with clause names the injected dependencies the function uses. The body is a sequence of let bindings followed by an expression.
2.4 Type Definitions
Product types (records):
type User =
id : UserId
email : Email
password : HashedPassword
active : Bool
created : Timestamp
end
Sum types (enums with data):
enum UserError =
| EmailAlreadyExists of Email
| InvalidPassword
| UserNotFound of UserId
| RepositoryFailure of String
end
Refined types:
type Email = String where is_valid_email
type UserId = UUID
type PositiveInt = Int where (> 0)
type NonEmptyStr = String where (.length > 0)
type HashedPassword = Bytes where (.length == 60) -- bcrypt output
The where clause attaches a predicate to a base type. The predicate must be a pure boolean expression. The compiler checks it statically where possible. Where static checking is undecidable, the compiler inserts a checked assertion at construction points.
2.5 Let Bindings and Immutability
All bindings are immutable. There is no reassignment.
let x = 42
let name = "Jorge"
let users = find_all_users()
Mutation of data structures is expressed through returning new values. The LLVM backend is responsible for optimizing in-place updates where safe.
2.6 Match Expressions
match is exhaustive. The compiler rejects patterns that fail to cover all variants.
match result
Ok(user) -> user.email
Err(UserNotFound) -> "unknown"
Err(e) -> log_error(e) then "error"
end
Patterns support destructuring, guards, and nested matches.
match payment
Authorized(amount) where amount > 0 -> process(amount)
Authorized(_) -> Err(PaymentError.ZeroAmount)
Declined(reason) -> Err(PaymentError.Declined(reason))
Pending -> Err(PaymentError.NotReady)
end
2.7 Complete Example: Pure Calculation Module
module PricingEngine
spec "Computes order totals, discounts, and tax. No side effects."
provides
compute_total : OrderLineList -> DiscountCode -> TaxRate -> Result<OrderTotal, PricingError>
apply_discount : Price -> DiscountCode -> Result<Price, PricingError>
requires
end -- no dependencies: this module is Pure
type OrderLine =
sku : ProductSku
quantity : PositiveInt
unit_price : Price
end
type OrderTotal =
subtotal : Price
discount : Price
tax : Price
total : Price
end
fn compute_total :: OrderLineList -> DiscountCode -> TaxRate -> Result<OrderTotal, PricingError>
require: lines.length > 0
require: tax_rate >= 0.0 and tax_rate <= 1.0
ensure: result is Ok implies result.value.total == result.value.subtotal - result.value.discount + result.value.tax
let subtotal = sum_lines(lines)
match apply_discount(subtotal, discount_code)
Ok(discounted) ->
let tax = discounted * tax_rate
let total = discounted + tax
Ok(OrderTotal.new(subtotal, subtotal - discounted, tax, total))
Err(e) -> Err(e)
end
end
fn sum_lines :: OrderLineList -> Price
require: lines.length > 0
ensure: result > 0
lines |> map(fn line -> line.quantity * line.unit_price end) |> fold(0, (+))
end
2.8 Complete Example: Payment Processor Stub
module PaymentGateway
spec "Submits charges to the payment provider and returns authorization results."
provides
authorize : ChargeRequest -> Effect<[NetworkCall, Log], Result<Authorization, PaymentError>>
capture : AuthorizationId -> Effect<[NetworkCall, Log], Result<Receipt, PaymentError>>
refund : ReceiptId -> Amount -> Effect<[NetworkCall, Log], Result<Refund, PaymentError>>
requires
http_client : HttpClient
logger : Logger
end
type ChargeRequest =
amount : PositiveInt -- amount in smallest currency unit (cents)
currency : CurrencyCode
card_token : PaymentToken
idempotency : IdempotencyKey
end
enum PaymentError =
| InsufficientFunds
| CardDeclined of String
| NetworkTimeout
| InvalidToken
| ProviderError of Int of String -- status code, message
end
fn authorize :: ChargeRequest -> Effect<[NetworkCall, Log], Result<Authorization, PaymentError>>
require: request.amount > 0
ensure: result is Ok implies result.value.amount == request.amount
with http_client, logger
let payload = serialize_charge(request)
match http_client.post("/v1/authorize", payload)
Ok(response) ->
logger.info("payment.authorized", [("idempotency", request.idempotency)])
parse_authorization(response)
Err(timeout) -> Err(PaymentError.NetworkTimeout)
Err(e) -> Err(PaymentError.ProviderError(e.status, e.body))
end
end
3. Type System
3.1 Base Types
| Type | Description |
|---|---|
Int | 64-bit signed integer |
Float | 64-bit IEEE 754 double |
String | UTF-8 encoded text |
Bool | True or False |
Bytes | Byte array |
Timestamp | UTC timestamp, microsecond precision |
UUID | 128-bit universally unique identifier |
Unit | The type of functions that return nothing |
3.2 Composite Types
Product types (records): Named fields, all fields required, no inheritance.
type Point = x : Float, y : Float end
Sum types (enums): Exhaustive variants, each may carry data.
enum Shape =
| Circle of Float -- radius
| Rectangle of Float of Float -- width, height
| Triangle of Point of Point of Point
end
Tuples: Unnamed, positionally accessed, fixed arity.
let pair : (String, Int) = ("hello", 42)
let first = pair.0
let second = pair.1
3.3 Parameterized Types
Option<T> -- Some(value) or None
Result<T, E> -- Ok(value) or Err(error)
List<T> -- homogeneous ordered sequence
Map<K, V> -- key-value store, keys must be comparable
Set<T> -- deduplicated collection, T must be comparable
These are part of the standard library. They are not special syntax; they are regular type constructors.
3.4 Refined Types
A refined type is a base type with a predicate.
type Email = String where is_valid_email
type PositiveInt = Int where (> 0)
type Probability = Float where (>= 0.0 and <= 1.0)
type NonEmptyList<T> = List<T> where (.length > 0)
type BoundedStr<N> = String where (.length <= N)
Refinement predicates must be pure boolean functions or inline pure expressions. The type checker attempts static verification via SMT solving. Where it cannot decide statically (e.g., string validation requires runtime regex evaluation), it inserts a checked construction:
-- This is a compile error if the type checker can prove it false statically.
-- Otherwise, it generates a runtime check at this construction point.
let email : Email = Email.from("jorge@example.com")
Email.from is generated by the compiler for every refined type. It returns Result<Email, RefinementError> and can never be bypassed.
3.5 No Subtyping
There is no inheritance. There is no subtype relationship between any two types. Composition is the only mechanism for sharing structure. Interface satisfaction is structural (duck-typed at the module boundary): a module satisfies a requires interface if it provides all the required operations with compatible types.
4. Effect System
The effect system is the structural heart of Loom. It is not a library or an annotation layer. It is part of the type system, enforced by the compiler, and propagated through every function call chain.
4.1 Effect Declarations
Effects are declared in function signatures using the Effect<[effects], ReturnType> type.
fn read_user :: UserId -> Effect<[DatabaseRead], Option<User>>
fn write_log :: String -> Effect<[Log], Unit>
fn current_time :: Unit -> Effect<[Clock], Timestamp>
fn generate_id :: Unit -> Effect<[Random], UUID>
fn pure_add :: Int -> Int -> Int -- no Effect wrapper: this is Pure
A function with no Effect wrapper is Pure by default. The compiler verifies this: any attempt to call an effectful function from a pure function is a type error.
4.2 Built-in Effects
| Effect | Meaning |
|---|---|
Pure | No side effects. Default when Effect is omitted. |
DatabaseRead | Reads from a persistent database |
DatabaseWrite | Writes to a persistent database |
NetworkCall | Makes an outbound network request |
FileRead | Reads from the filesystem |
FileWrite | Writes to the filesystem |
Clock | Reads the current time (non-deterministic without this) |
Random | Generates random values |
Log | Writes to a structured log output |
Clock and Random are effects because their output is non-deterministic. Marking them as effects means that any function using them is, by type, non-deterministic, and testing infrastructure can inject controlled implementations.
4.3 Effect Propagation
Effects propagate upward through call chains automatically.
fn get_or_create_user :: Email -> Effect<[DatabaseRead, DatabaseWrite, Log], Result<User, UserError>>
-- calling find_user introduces DatabaseRead
-- calling create_user introduces DatabaseWrite and Log
-- the union of all called effects appears in this signature
match find_user_by_email(email)
Some(user) -> Ok(user)
None -> create_user(CreateUserRequest.new(email))
end
end
If the declared effects do not match the union of effects of all called functions, the compiler emits an error. You cannot under-declare effects.
4.4 Effect Polymorphism
Higher-order functions can be parameterized over effects.
fn map_result :: Result<A, E> -> (A -> Effect<[fx], B>) -> Effect<[fx], Result<B, E>>
Here fx is an effect variable. The function is effect-polymorphic: it propagates whatever effects the callback f requires. This allows generic combinators to work with both pure and effectful callbacks without specialization.
4.5 Effect Containment Example
-- COMPILE ERROR: pure function cannot call effectful function
fn compute_discount :: Price -> Price
let now = current_time() -- current_time has Effect<[Clock], Timestamp>
-- Error: Clock effect escapes Pure context
Price.from(now.unix_seconds)
end
-- CORRECT: declare the Clock effect
fn compute_time_based_discount :: Price -> Effect<[Clock], Price>
let now = current_time()
if is_happy_hour(now) then Price.from(price * 0.8) else price
end
4.6 Testing Implications
Because Clock and Random are effects, test code can inject deterministic implementations through the module’s requires interface. There is no need for mocking frameworks. The effect system creates the seam automatically.
5. Module System
5.1 Module as GS Boundary
Each Loom module is a Generative Specification boundary. The module header encodes the GS spec, provides, and requires properties directly. This is not convention; it is enforced by the parser. A file without a valid module header does not parse.
module OrderService
spec "Processes customer orders from submission to fulfillment."
provides
submit_order : SubmitOrderRequest -> Effect<[DatabaseWrite, NetworkCall, Log], Result<Order, OrderError>>
cancel_order : OrderId -> Reason -> Effect<[DatabaseWrite, Log], Result<Unit, OrderError>>
get_order : OrderId -> Effect<[DatabaseRead], Option<Order>>
requires
order_repository : OrderRepository
payment_gateway : PaymentGateway
inventory_service : InventoryPort
logger : Logger
end
5.2 Dependency Inversion by Construction
The requires block names typed interfaces, never concrete modules. The concrete implementations are wired at the composition root. There is no import of a concrete implementation module inside a service module. The type checker enforces this: you can only use identifiers from your own module’s scope and your declared requires interfaces.
5.3 Circular Import Prevention
The module dependency graph is checked at compile time for cycles. A circular import is a compile error. This is not a lint warning; the type checker cannot proceed with cyclic dependencies.
5.4 Spec Generation
The compiler can emit a GS spec stub from a module header:
loom spec UserService.loom --output docs/specs/UserService.md
This generates a structured spec document with the module purpose, operations table, dependency table, and effect inventory. The stub is a starting point; the human fills in invariants, constraints, and domain notes.
6. Contracts
6.1 Preconditions and Postconditions
require: and ensure: are first-class constructs. They appear at the top of a function body, before any let bindings.
fn divide :: Float -> Float -> Result<Float, ArithmeticError>
require: divisor != 0.0
ensure: result is Ok implies result.value * divisor ~= dividend -- within floating-point tolerance
if divisor == 0.0 then Err(ArithmeticError.DivisionByZero)
else Ok(dividend / divisor)
end
end
6.2 Static vs. Dynamic Verification
The contract verifier operates in two passes:
Static pass: Contracts involving only the function’s type signature, refined type predicates, and pure boolean expressions on literals are checked at compile time via SMT solving. If the precondition is provably violated at a call site, the compiler emits an error at the call site.
-- COMPILE ERROR at this call site:
-- Precondition `divisor != 0.0` is statically falsifiable: divisor is the literal 0.0
let result = divide(100.0, 0.0)
Dynamic pass: Contracts involving refined types with runtime predicates, or values whose range cannot be determined statically, are compiled to checked assertions. In debug builds, violations raise a ContractViolationError with the violated condition, function name, and source location. In release builds, contracts can be elided with --release-contracts=zero-cost after the developer has provided proofs for all contract obligations.
6.3 Contracts and Refined Types Interacting
fn index_of :: List<T> -> Int -> Effect<[Pure], Result<T, BoundsError>>
require: index >= 0
require: index < list.length
ensure: result is Ok
Ok(list.unsafe_get(index)) -- safe because preconditions guarantee bounds
end
When both preconditions are in scope as verified, the compiler can verify that unsafe_get is actually safe and elides the bounds check from the output. Contracts are the mechanism through which safe high-level operations justify their unsafe lowerings.
6.4 Invariants on Types
Type-level invariants are expressed as contracts on the constructor:
type PositiveInt = Int where (> 0)
-- The compiler generates:
fn PositiveInt.from :: Int -> Result<PositiveInt, RefinementError>
require: value > 0
Ok(PositiveInt.unsafe_wrap(value))
end
There is no way to construct a PositiveInt that violates the invariant. unsafe_wrap is a compiler-internal construct, not accessible in user code.
7. Compilation Pipeline
source.loom
|
| [Lexer + Parser]
v
Loom AST
|
| [Name resolution, module binding]
v
Resolved AST
|
| [Type checker]
| - Infers and checks all types
| - Resolves refined type predicates
| - Emits type errors
v
Typed AST
|
| [Effect checker]
| - Verifies effect declarations match call graph
| - Checks pure contexts for effect leakage
| - Emits effect errors
v
Effect-checked AST
|
| [Contract verifier]
| - Static pass: SMT solving for decidable contracts
| - Marks remaining contracts for dynamic insertion
| - Emits static contract violation errors
v
Contract-annotated AST
|
| [Lowering to Loom IR]
| - Monomorphization of parameterized types
| - Closure conversion
| - Contract insertion (dynamic checks)
v
Loom IR
|
| [Optimizer]
| - Dead code elimination
| - Inlining
| - Provably safe contract elision
v
Optimized Loom IR
|
| [LLVM backend]
| - Loom IR to LLVM IR translation
v
LLVM IR
|
| [LLVM optimizer + codegen]
v
+--------+----------+----------+
| | | |
Native WASM PTX (GPU) TPU kernel
binary
7.1 Parser
Standard recursive descent. The grammar is LL(2). Module header parsing is a distinct phase that completes before the body is parsed, enabling incremental compilation and parallel module processing.
7.2 Type Checker
Hindley-Milner type inference extended with refinement types and effect rows. Type variables are resolved before the effect checker runs. All types are monomorphic at IR generation.
7.3 Effect Checker
Walks the effect-checked call graph. For each function, computes the transitive effect set from its callees. Compares against the declared effect set. Reports missing or undeclared effects. Effect variables in polymorphic functions are instantiated during this pass.
7.4 Contract Verifier
Uses an embedded SMT solver (Z3 via API) for the static pass. Pure predicate expressions on integer and boolean types are translated to SMT formulae. String and custom type predicates are marked for dynamic insertion.
7.5 LLVM Backend
Loom IR maps closely to LLVM IR. Product types become LLVM structs. Sum types become tagged unions. Effect annotations have no runtime representation; they are erased after verification. match expressions become LLVM switch instructions with exhaustiveness guaranteed by the frontend. Contracts in release-mode-with-proof are elided entirely.
7.6 Verification Property
By the time source reaches LLVM IR, the following properties have been mechanically verified:
- All types are consistent (type safety)
- All effects are declared (effect safety)
- All decidable contracts hold at call sites (partial correctness)
- All pattern matches are exhaustive (no runtime match failure)
- No null dereferences (no null in the language)
- No unhandled exceptions (no exceptions in the language)
The LLVM IR produced by Loom carries fewer undefined behaviors than hand-written C or Rust, because the verification has been completed before codegen.
8. The GS Connection
Loom does not exist in isolation. It is the execution layer of the Generative Specification stack.
8.1 The Full Stack
Natural language intent (human)
|
v
Generative Specification
(CLAUDE.md, spec files, ForgeCraft validation)
|
v
Loom source
(AI-generated against GS spec)
|
v
Verified Loom IR
(type-checked, effect-checked, contract-verified)
|
v
LLVM IR
|
v
Native binary / WASM / TPU kernel
8.2 GS Properties Encoded in Loom
The GS white paper defines four properties that a well-governed AI-generated artifact should satisfy: Bounded, Typed, Testable, and Verifiable.
Bounded: The Loom module system enforces this mechanically. Every module has a single-sentence spec, a named provides surface, and a named requires interface list. The compiler rejects modules without these. There is no way to generate an unbounded module.
Typed: The Loom type system, including refinement types, encodes the data contracts from the GS spec directly in the language. A GS spec that says “email must be a valid email address” becomes type Email = String where is_valid_email in Loom. The spec is the type.
Testable: The effect system makes all non-determinism explicit. Every dependency is injected. Every external call is declared. Test doubles are natural replacements: a test wires a different implementation into requires. No mocking framework is needed.
Verifiable: The contract system encodes GS postconditions as ensure: clauses. In a Loom world, “does this satisfy the spec?” is not answered by a human reading a rubric. It is answered by the compiler running the contract verifier.
8.3 ForgeCraft Integration
ForgeCraft’s setup_project command will be extended to generate a Loom module stub from a GS spec:
forgecraft generate-loom --spec docs/specs/UserService.md --output src/UserService.loom
The generated stub includes the module header with spec, provides, and requires filled in from the GS spec, plus empty function bodies with require: and ensure: stubs derived from the spec’s invariants section. The AI fills in the implementation against the stub. The compiler verifies the result.
This closes the loop: the specification drives generation, and generation produces something the compiler can verify against the specification.
9. What Loom Is Not
Not a replacement for existing languages today
Loom has no ecosystem, no package registry, no production runtime, and no standard library beyond core types. It cannot be used to build production software today. It is a language design that targets future AI-native development workflows.
Not designed for humans to write manually at scale
Loom is review-friendly: a human can read Loom source and understand what it does. It is not write-friendly in the way Python or TypeScript are, because it was not designed for that. The expected authorship pattern is: humans write GS specs, AI generates Loom, humans review and approve Loom, compiler verifies Loom. Writing Loom by hand is possible but not the primary use case.
Not a research language for academic proof systems only
Loom must compile to real hardware. LLVM output to native binaries and WASM is a requirement, not a stretch goal. The effect system and contract system are designed to be practical, not theoretically complete. Decidability is traded for ergonomics where necessary.
Not a DSL
Loom is general-purpose. It has modules, parameterized types, higher-order functions, and an effect system that handles arbitrary external I/O. It is not specialized to any domain. A Loom program can, in principle, implement any computation.
Not attempting to replace human judgment
The humans in the Loom workflow operate at the specification layer: they write GS specs, they review AI-generated Loom, they approve deployments. Loom automates the translation from spec to verified code. It does not automate the decision about what the spec should say. That remains a human responsibility.
10. Status and Next Steps
v0.1 is a design document. No implementation exists. The following steps are required to produce a working prototype.
10.1 Formal Grammar
Produce a complete BNF or EBNF grammar for Loom. The grammar should be unambiguous and LL(2) parseable. Test the grammar against the examples in this document.
Deliverable: docs/loom/grammar.ebnf
10.2 Reference Type Checker
Implement the type checker and effect checker in Haskell or Rust. Haskell is preferred for the initial prototype due to its algebraic data type support for representing Loom ASTs. The reference implementation does not need to produce executable output; it needs to accept valid programs and reject invalid ones with useful error messages.
Deliverable: loom-tc/ repository with parser, type checker, effect checker
10.3 Small Standard Library
Define the core standard library: base types, Option<T>, Result<T, E>, List<T>, Map<K, V>, basic string operations, basic arithmetic, basic IO types. The standard library is itself written in Loom where possible; IO primitives are written as builtin effect declarations.
Deliverable: loom-stdlib/core/
10.4 LLVM Backend Prototype
Implement the Loom IR to LLVM IR translation. The initial target is x86-64 native binaries and WASM. GPU and TPU targets are deferred. Use the LLVM C API or inkwell (Rust LLVM bindings) for IR construction.
Deliverable: loom-llvm/ with working compilation of the examples in Section 2
10.5 ForgeCraft Integration
Extend ForgeCraft’s setup_project command to emit a Loom module stub from a GS spec file. This requires defining the mapping from GS spec properties to Loom module declarations.
Deliverable: forgecraft generate-loom command, documented in ForgeCraft README
10.6 Validation Corpus
Produce a corpus of 20-30 non-trivial Loom programs covering: pure computation, database-backed services, network clients, mixed-effect orchestration. Use these programs to validate the type checker, effect checker, and LLVM backend against expected behavior.
Deliverable: loom-corpus/ with programs and expected type-check and compile outcomes
Loom v0.1 specification. Subject to revision as the prototype progresses.