Remove Cranelift JIT compiler and expose the existing C backend as the compilation target. Generated C code can be compiled with GCC/Clang. Changes: - Remove cranelift-* dependencies from Cargo.toml - Delete src/compiler.rs (565 lines of Cranelift code) - Add compile_to_c() function with -o and --run flags - Fix C backend name mangling (main -> main_lux) to avoid conflicts - Update CLI help text and documentation Usage: lux compile <file.lux> # Output C to stdout lux compile <file.lux> -o out.c # Write to file lux compile <file.lux> --run # Compile and execute C backend supports: functions, basic types, operators, if/then/else, records, enums, Console.print. Future work: closures, lists, patterns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
749 lines
16 KiB
Markdown
749 lines
16 KiB
Markdown
# Lux Language Skills & Implementation Plan
|
|
|
|
## Table of Contents
|
|
|
|
1. [Effect System](#1-effect-system)
|
|
2. [Schema Evolution](#2-schema-evolution)
|
|
3. [Behavioral Types](#3-behavioral-types)
|
|
4. [Type System Foundation](#4-type-system-foundation)
|
|
5. [Implementation Roadmap](#5-implementation-roadmap)
|
|
|
|
---
|
|
|
|
## 1. Effect System
|
|
|
|
### Overview
|
|
|
|
Effects make side effects explicit, trackable, and testable. Every function declares what it can do. Handlers interpret effects at runtime.
|
|
|
|
### Core Concepts
|
|
|
|
#### Effect Declarations
|
|
|
|
```lux
|
|
effect Http {
|
|
fn get(url: String): Response
|
|
fn post(url: String, body: Bytes): Response
|
|
}
|
|
|
|
effect Database {
|
|
fn query<T>(q: Query<T>): List<T>
|
|
fn execute(q: Command): Int
|
|
}
|
|
|
|
effect Logger {
|
|
fn log(level: Level, message: String): Unit
|
|
}
|
|
|
|
effect Async {
|
|
fn await<T>(future: Future<T>): T
|
|
fn spawn<T>(action: fn(): T): Future<T>
|
|
}
|
|
```
|
|
|
|
#### Effect Signatures
|
|
|
|
Functions declare their effects after `with`:
|
|
|
|
```lux
|
|
fn fetchUsers(): List<User> with {Database, Logger} =
|
|
Logger.log(Info, "Fetching users")
|
|
Database.query(selectAllUsers)
|
|
|
|
-- Multiple effects compose naturally
|
|
fn syncUsers(source: Url): Int with {Http, Database, Logger} =
|
|
let users = Http.get(source) |> parseUsers
|
|
Logger.log(Info, "Syncing {users.len} users")
|
|
users |> List.map(upsertUser) |> List.sum
|
|
```
|
|
|
|
#### Effect Handlers
|
|
|
|
Handlers provide implementations:
|
|
|
|
```lux
|
|
handler realHttp: Http {
|
|
fn get(url) = httpClientGet(url)
|
|
fn post(url, body) = httpClientPost(url, body)
|
|
}
|
|
|
|
handler mockHttp(responses: Map<String, Response>): Http {
|
|
fn get(url) = responses.get(url).unwrapOr(Response.notFound)
|
|
fn post(url, _) = responses.get(url).unwrapOr(Response.notFound)
|
|
}
|
|
|
|
handler postgresDb(conn: Connection): Database {
|
|
fn query(q) = conn.execute(q.toSql) |> parseRows
|
|
fn execute(q) = conn.execute(q.toSql)
|
|
}
|
|
```
|
|
|
|
#### Running Effects
|
|
|
|
```lux
|
|
fn main(): Unit with {Console} =
|
|
let users = run fetchUsers() with {
|
|
Database = postgresDb(openConnection()),
|
|
Logger = consoleLogger
|
|
}
|
|
Console.print("Found {users.len} users")
|
|
```
|
|
|
|
### Effect Features
|
|
|
|
#### Effect Polymorphism
|
|
|
|
Write code generic over effects:
|
|
|
|
```lux
|
|
fn withRetry<E, T>(action: fn(): T with E, attempts: Int): T with E =
|
|
match attempts {
|
|
0 => panic("Retry exhausted"),
|
|
n => try action() catch _ => withRetry(action, n - 1)
|
|
}
|
|
```
|
|
|
|
#### Effect Constraints
|
|
|
|
Require specific effects:
|
|
|
|
```lux
|
|
fn transactional<T>(action: fn(): T with {Database}): T with {Database} =
|
|
Database.execute(begin)
|
|
let result = try action() catch e => {
|
|
Database.execute(rollback)
|
|
throw e
|
|
}
|
|
Database.execute(commit)
|
|
result
|
|
```
|
|
|
|
#### Effect Inference
|
|
|
|
Effects can be inferred within function bodies, but signatures must be explicit:
|
|
|
|
```lux
|
|
fn helper() with {Logger} = -- explicit signature
|
|
let x = compute() -- effect-free, inferred
|
|
Logger.log(Debug, "x = {x}") -- Logger effect used
|
|
x
|
|
```
|
|
|
|
#### Built-in Effects
|
|
|
|
```lux
|
|
effect Fail {
|
|
fn fail<T>(error: Error): T -- early return / exceptions
|
|
}
|
|
|
|
effect State<S> {
|
|
fn get(): S
|
|
fn put(s: S): Unit
|
|
fn modify(f: fn(S): S): Unit
|
|
}
|
|
|
|
effect Reader<R> {
|
|
fn ask(): R
|
|
}
|
|
|
|
effect Random {
|
|
fn int(range: Range<Int>): Int
|
|
fn float(): Float
|
|
fn shuffle<T>(list: List<T>): List<T>
|
|
}
|
|
|
|
effect Time {
|
|
fn now(): Instant
|
|
fn sleep(duration: Duration): Unit
|
|
}
|
|
```
|
|
|
|
### Effect Semantics
|
|
|
|
- **Lexical handling**: Effects are handled at `run` boundaries
|
|
- **Order independence**: Multiple effects can be handled in any order
|
|
- **Resumable**: Handlers can resume computations (algebraic effect style)
|
|
- **Zero-cost goal**: Handlers inline when statically known
|
|
|
|
---
|
|
|
|
## 2. Schema Evolution
|
|
|
|
### Overview
|
|
|
|
Types change over time. Data persists. Lux tracks type versions and ensures compatibility at compile time.
|
|
|
|
### Core Concepts
|
|
|
|
#### Versioned Types
|
|
|
|
```lux
|
|
type User @v1 {
|
|
name: String,
|
|
email: String
|
|
}
|
|
|
|
type User @v2 {
|
|
name: String,
|
|
email: String,
|
|
createdAt: Timestamp
|
|
}
|
|
```
|
|
|
|
#### Compatibility Rules
|
|
|
|
**Auto-compatible changes** (no migration needed):
|
|
- Adding optional fields
|
|
- Adding fields with defaults
|
|
- Widening numeric types (Int32 -> Int64)
|
|
- Adding enum variants (for extensible enums)
|
|
|
|
**Breaking changes** (require explicit migration):
|
|
- Removing fields
|
|
- Renaming fields
|
|
- Changing field types
|
|
- Removing enum variants
|
|
|
|
```lux
|
|
type User @v3 {
|
|
fullName: String, -- renamed from 'name'
|
|
email: String,
|
|
createdAt: Timestamp,
|
|
|
|
-- Explicit migration required
|
|
from @v2 = {
|
|
fullName: v2.name,
|
|
email: v2.email,
|
|
createdAt: v2.createdAt
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Migration Chains
|
|
|
|
Migrations compose automatically:
|
|
|
|
```lux
|
|
-- Reading @v1 data as @v3:
|
|
-- 1. @v1 -> @v2 (auto: createdAt gets default)
|
|
-- 2. @v2 -> @v3 (explicit: name -> fullName)
|
|
|
|
fn loadLegacyUser(data: Bytes): User @v3 =
|
|
Codec.decode<User>(data) -- handles any version
|
|
```
|
|
|
|
#### Version Constraints
|
|
|
|
```lux
|
|
-- Accept any version >= @v2
|
|
fn processUser(user: User @v2+): Unit = ...
|
|
|
|
-- Accept exactly @v3
|
|
fn processUserV3(user: User @v3): Unit = ...
|
|
|
|
-- Return latest version
|
|
fn createUser(name: String): User @latest = ...
|
|
```
|
|
|
|
### Schema Features
|
|
|
|
#### Serialization
|
|
|
|
```lux
|
|
-- Encode with version tag
|
|
let bytes = Codec.encode(user) -- includes version marker
|
|
|
|
-- Decode to specific version (migrates if needed)
|
|
let user: User @v3 = Codec.decode(bytes)
|
|
|
|
-- Decode to any compatible version
|
|
let user: User @v2+ = Codec.decode(bytes)
|
|
```
|
|
|
|
#### Database Integration
|
|
|
|
```lux
|
|
table users: User @v3 {
|
|
primaryKey: id,
|
|
index: [email]
|
|
}
|
|
|
|
-- Compiler generates migration SQL when version changes
|
|
-- Or errors if migration is ambiguous
|
|
```
|
|
|
|
#### API Versioning
|
|
|
|
```lux
|
|
endpoint getUser: GET "/users/{id}" -> User @v2
|
|
|
|
-- Later: update endpoint
|
|
endpoint getUser: GET "/users/{id}" -> User @v3
|
|
-- Compiler: "Breaking change for clients expecting @v2"
|
|
-- Must either:
|
|
-- 1. Keep old endpoint as getUser_v2
|
|
-- 2. Prove @v3 is wire-compatible with @v2
|
|
```
|
|
|
|
#### Compatibility Checking
|
|
|
|
```lux
|
|
-- Compile-time compatibility proof
|
|
assert User @v2 compatibleWith User @v1 -- passes
|
|
assert User @v3 compatibleWith User @v1 -- fails: breaking change
|
|
|
|
-- Generate compatibility report
|
|
lux schema diff User @v1 User @v3
|
|
-- Output:
|
|
-- - 'name' renamed to 'fullName' (breaking)
|
|
-- - 'createdAt' added with default (compatible)
|
|
```
|
|
|
|
### Schema Semantics
|
|
|
|
- **Versions are types**: `User @v1` and `User @v2` are distinct types
|
|
- **Migrations are functions**: `from @v1` is a function `@v1 -> @v2`
|
|
- **Compatibility is decidable**: Compiler checks all rules statically
|
|
- **Wire format is stable**: Version tag + canonical encoding
|
|
|
|
---
|
|
|
|
## 3. Behavioral Types
|
|
|
|
### Overview
|
|
|
|
Properties beyond input/output types. Express and verify behavioral guarantees like purity, totality, idempotency.
|
|
|
|
### Core Concepts
|
|
|
|
#### Built-in Properties
|
|
|
|
```lux
|
|
-- Purity: no effects
|
|
fn add(a: Int, b: Int): Int
|
|
is pure
|
|
= a + b
|
|
|
|
-- Totality: always terminates, no exceptions
|
|
fn safeDiv(a: Int, b: Int): Option<Int>
|
|
is total
|
|
= if b == 0 then None else Some(a / b)
|
|
|
|
-- Idempotency: f(f(x)) == f(x)
|
|
fn normalize(s: String): String
|
|
is idempotent
|
|
= s.trim.lowercase
|
|
|
|
-- Determinism: same inputs -> same outputs
|
|
fn hash(data: Bytes): Hash
|
|
is deterministic
|
|
```
|
|
|
|
#### Refinement Types
|
|
|
|
```lux
|
|
type PositiveInt = Int where self > 0
|
|
type NonEmptyList<T> = List<T> where self.len > 0
|
|
type Email = String where self.matches(emailRegex)
|
|
|
|
fn head<T>(list: NonEmptyList<T>): T
|
|
is total -- can't fail: list is non-empty
|
|
= list.unsafeHead
|
|
|
|
fn sqrt(n: PositiveInt): Float
|
|
is total -- can't fail: n is positive
|
|
```
|
|
|
|
#### Output Refinements
|
|
|
|
```lux
|
|
fn sort<T: Ord>(list: List<T>): List<T>
|
|
is pure,
|
|
is total,
|
|
where result.len == list.len,
|
|
where result.isSorted,
|
|
where result.isPermutationOf(list)
|
|
|
|
fn filter<T>(list: List<T>, pred: fn(T): Bool): List<T>
|
|
is pure,
|
|
is total,
|
|
where result.len <= list.len,
|
|
where result.all(pred)
|
|
```
|
|
|
|
#### Property Requirements
|
|
|
|
```lux
|
|
-- Require properties from function arguments
|
|
fn retry<F, T>(action: F, times: Int): Result<T, Error>
|
|
where F: fn(): T with {Fail},
|
|
where F is idempotent -- enforced at call site!
|
|
= ...
|
|
|
|
fn memoize<F, A, B>(f: F): fn(A): B with {Cache}
|
|
where F: fn(A): B,
|
|
where F is pure,
|
|
where F is deterministic
|
|
= ...
|
|
|
|
fn parallelize<F, T>(actions: List<F>): List<T> with {Async}
|
|
where F: fn(): T,
|
|
where F is commutative -- order-independent
|
|
= ...
|
|
```
|
|
|
|
### Verification Levels
|
|
|
|
#### Level 1: Compiler-Proven
|
|
|
|
Simple properties proven automatically:
|
|
|
|
```lux
|
|
fn double(x: Int): Int
|
|
is pure -- proven: no effects
|
|
= x * 2
|
|
|
|
fn always42(): Int
|
|
is total, -- proven: no recursion, no failure
|
|
is deterministic -- proven: no effects
|
|
= 42
|
|
```
|
|
|
|
#### Level 2: SMT-Backed
|
|
|
|
Refinements checked by SMT solver:
|
|
|
|
```lux
|
|
fn clamp(x: Int, lo: Int, hi: Int): Int
|
|
where lo <= hi,
|
|
where result >= lo,
|
|
where result <= hi
|
|
= if x < lo then lo else if x > hi then hi else x
|
|
-- SMT proves postconditions hold
|
|
```
|
|
|
|
#### Level 3: Property-Tested
|
|
|
|
Complex properties generate tests:
|
|
|
|
```lux
|
|
fn sort<T: Ord>(list: List<T>): List<T>
|
|
where result.isPermutationOf(list) -- too complex for SMT
|
|
-- Compiler generates: forall lists, sort(list).isPermutationOf(list)
|
|
-- Runs as property-based test
|
|
```
|
|
|
|
#### Level 4: Assumed
|
|
|
|
Escape hatch for unverifiable properties:
|
|
|
|
```lux
|
|
fn externalSort<T: Ord>(list: List<T>): List<T>
|
|
assume is idempotent -- trust me (FFI, etc.)
|
|
= ffiSort(list)
|
|
```
|
|
|
|
### Property Propagation
|
|
|
|
Properties flow through composition:
|
|
|
|
```lux
|
|
fn f(x: Int): Int is pure = x + 1
|
|
fn g(x: Int): Int is pure = x * 2
|
|
|
|
fn h(x: Int): Int is pure = f(g(x)) -- inferred pure
|
|
```
|
|
|
|
```lux
|
|
fn f(x: Int): Int is idempotent = x.abs
|
|
fn g(x: Int): Int is idempotent = x.abs -- same function
|
|
|
|
-- Composition of idempotent functions is idempotent IF they're the same
|
|
-- or if one is a fixpoint of the other. Otherwise, not guaranteed.
|
|
fn h(x: Int): Int = f(g(x)) -- NOT automatically idempotent
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Type System Foundation
|
|
|
|
### Core Types
|
|
|
|
```lux
|
|
-- Primitives
|
|
Int, Int8, Int16, Int32, Int64
|
|
UInt, UInt8, UInt16, UInt32, UInt64
|
|
Float, Float32, Float64
|
|
Bool
|
|
Char
|
|
String
|
|
|
|
-- Collections
|
|
List<T>
|
|
Set<T>
|
|
Map<K, V>
|
|
Array<T> -- fixed size
|
|
|
|
-- Optionality
|
|
Option<T> = None | Some(T)
|
|
Result<T, E> = Err(E) | Ok(T)
|
|
|
|
-- Tuples
|
|
(A, B), (A, B, C), ...
|
|
|
|
-- Records
|
|
{ name: String, age: Int }
|
|
|
|
-- Functions
|
|
fn(A): B
|
|
fn(A, B): C
|
|
fn(A): B with {Effects}
|
|
```
|
|
|
|
### Algebraic Data Types
|
|
|
|
```lux
|
|
type Color = Red | Green | Blue
|
|
|
|
type Tree<T> =
|
|
| Leaf(T)
|
|
| Node(Tree<T>, Tree<T>)
|
|
|
|
type Result<T, E> =
|
|
| Ok(T)
|
|
| Err(E)
|
|
```
|
|
|
|
### Pattern Matching
|
|
|
|
```lux
|
|
fn describe(color: Color): String =
|
|
match color {
|
|
Red => "red",
|
|
Green => "green",
|
|
Blue => "blue"
|
|
}
|
|
|
|
fn sum(tree: Tree<Int>): Int =
|
|
match tree {
|
|
Leaf(n) => n,
|
|
Node(left, right) => sum(left) + sum(right)
|
|
}
|
|
```
|
|
|
|
### Type Classes / Traits
|
|
|
|
```lux
|
|
trait Eq {
|
|
fn eq(self, other: Self): Bool
|
|
}
|
|
|
|
trait Ord: Eq {
|
|
fn cmp(self, other: Self): Ordering
|
|
}
|
|
|
|
trait Show {
|
|
fn show(self): String
|
|
}
|
|
|
|
impl Eq for Int {
|
|
fn eq(self, other) = intEq(self, other)
|
|
}
|
|
```
|
|
|
|
### Row Polymorphism
|
|
|
|
```lux
|
|
-- Extensible records
|
|
fn getName(r: { name: String, ..rest }): String = r.name
|
|
|
|
-- Works with any record containing 'name'
|
|
getName({ name: "Alice", age: 30 })
|
|
getName({ name: "Bob", email: "bob@example.com" })
|
|
|
|
-- Extensible variants
|
|
type HttpError = { NotFound | Timeout | ..rest }
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Implementation Roadmap
|
|
|
|
### Phase 0: Foundation ✅ COMPLETE
|
|
|
|
**Goal**: Minimal viable compiler
|
|
|
|
- [x] Lexer and parser for core syntax
|
|
- [x] AST representation
|
|
- [x] Basic type checker (Hindley-Milner inference)
|
|
- [x] Interpreter for testing semantics
|
|
- [x] REPL with history, completions, syntax highlighting
|
|
|
|
**Deliverable**: Can type-check and interpret pure functional programs ✅
|
|
|
|
```lux
|
|
fn fib(n: Int): Int =
|
|
if n <= 1 then n else fib(n-1) + fib(n-2)
|
|
```
|
|
|
|
### Phase 1: Effect System ✅ COMPLETE
|
|
|
|
**Goal**: First-class algebraic effects
|
|
|
|
- [x] Effect declarations
|
|
- [x] Effect signatures on functions
|
|
- [x] Handler definitions
|
|
- [x] `run ... with` syntax
|
|
- [x] Effect inference within function bodies
|
|
- [x] Effect polymorphism
|
|
- [x] Built-in effects (Console, Fail, State, Random, File, Http, Time, Process, Test)
|
|
|
|
**Deliverable**: Can define, handle, and compose effects ✅
|
|
|
|
```lux
|
|
effect Console { fn print(s: String): Unit }
|
|
|
|
fn greet(name: String): Unit with {Console} =
|
|
Console.print("Hello, {name}!")
|
|
|
|
fn main() =
|
|
run greet("World") with { Console = stdoutConsole }
|
|
```
|
|
|
|
### Phase 2: Code Generation ⚠️ PARTIAL
|
|
|
|
**Goal**: Compile to a real target
|
|
|
|
- [x] C backend (basic functions, Console.print)
|
|
- [ ] C backend extensions (closures, lists, pattern variables)
|
|
- [ ] JavaScript backend
|
|
- [ ] WASM backend
|
|
- [ ] Optimization passes
|
|
|
|
**Deliverable**: Compiled programs that run natively or in browser
|
|
|
|
**Current**: `lux compile <file>` compiles to C code. Compile and run with `--run` flag.
|
|
|
|
### Phase 3: Schema Evolution ⚠️ PARTIAL
|
|
|
|
**Goal**: Versioned types with migrations
|
|
|
|
- [x] Version annotations on types (`@v1`, `@v2`)
|
|
- [x] Migration syntax (`from @v1 = ...`)
|
|
- [x] Version constraints (`@v2+`, `@latest`)
|
|
- [x] Runtime versioned values
|
|
- [x] Schema registry & compatibility checking
|
|
- [ ] Type system integration (versions partially ignored in typechecker)
|
|
- [ ] Auto-migration generation
|
|
- [ ] Codec generation
|
|
|
|
**Deliverable**: Types with automatic serialization and migration
|
|
|
|
```lux
|
|
type Config @v1 { host: String }
|
|
type Config @v2 { host: String, port: Int, from @v1 = { port: 8080, ..v1 } }
|
|
|
|
let cfg: Config @v2 = Codec.decode(legacyBytes)
|
|
```
|
|
|
|
### Phase 4: Behavioral Types ⚠️ PARTIAL
|
|
|
|
**Goal**: Property specifications and verification
|
|
|
|
- [x] Property syntax (`is pure`, `is total`, `is idempotent`, etc.)
|
|
- [x] Built-in properties parsing
|
|
- [x] Pure function checking (no effects)
|
|
- [ ] Total function verification
|
|
- [ ] Idempotent verification
|
|
- [ ] Deterministic verification
|
|
- [ ] Where clause enforcement
|
|
- [ ] SMT solver integration (Z3)
|
|
- [ ] Property-based test generation
|
|
- [ ] `assume` escape hatch
|
|
|
|
**Deliverable**: Compile-time verification of behavioral properties
|
|
|
|
```lux
|
|
fn abs(x: Int): Int
|
|
is pure,
|
|
is total,
|
|
where result >= 0
|
|
= if x < 0 then -x else x
|
|
```
|
|
|
|
### Phase 5: Ecosystem ✅ MOSTLY COMPLETE
|
|
|
|
**Goal**: Usable for real projects
|
|
|
|
- [x] Standard library (List, String, Option, Result, Math, Json modules)
|
|
- [x] LSP server (diagnostics, hover, completions)
|
|
- [x] REPL with history, tab completion, syntax highlighting
|
|
- [x] Formatter
|
|
- [x] Watch mode / hot reload
|
|
- [x] Basic debugger
|
|
- [x] Module system (imports, exports, aliases)
|
|
- [ ] Package manager (manifest parsing exists, not fully functional)
|
|
- [ ] Documentation generator
|
|
- [ ] Profiler
|
|
|
|
### Phase 6: Advanced Features
|
|
|
|
**Goal**: Full language vision
|
|
|
|
- [x] HTTP client effect
|
|
- [x] HTTP server effect
|
|
- [x] File system effect
|
|
- [x] Process/shell effect
|
|
- [x] Test effect (native testing framework)
|
|
- [ ] Database/SQL effect
|
|
- [ ] Incremental computation
|
|
- [ ] Distributed effects (location-aware)
|
|
- [ ] Proof assistant mode
|
|
|
|
---
|
|
|
|
## Resolved Design Decisions
|
|
|
|
### Syntax (Decided)
|
|
|
|
- [x] Braces for blocks, not significant whitespace
|
|
- [x] Effect syntax: `with {E1, E2}`
|
|
- [x] Version syntax: `@v1`, `@v2+`, `@latest`
|
|
|
|
### Semantics (Decided)
|
|
|
|
- [x] Deep handlers (algebraic effect style with resume)
|
|
- [x] Structural compatibility for records
|
|
- [x] Best-effort property verification (gradual)
|
|
|
|
### Pragmatics (In Progress)
|
|
|
|
- Primary target: Tree-walking interpreter (default), C backend for compilation
|
|
- FFI: Shell commands via Process effect
|
|
- [ ] JavaScript backend for browser use
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
### Effect Systems
|
|
- Koka language (Daan Leijen)
|
|
- Eff language (Matija Pretnar)
|
|
- "Algebraic Effects for Functional Programming" (Daan Leijen)
|
|
- Frank language (Sam Lindley)
|
|
|
|
### Schema Evolution
|
|
- Protocol Buffers / Protobuf
|
|
- Apache Avro
|
|
- "Schema Evolution in Heterogeneous Data Environments"
|
|
|
|
### Behavioral Types
|
|
- Liquid Haskell (refinement types)
|
|
- F* (dependent types + effects)
|
|
- Dafny (verification)
|
|
- "Refinement Types for Haskell" (Vazou et al.)
|
|
|
|
### General
|
|
- "Types and Programming Languages" (Pierce)
|
|
- "Practical Foundations for Programming Languages" (Harper)
|