Version: 0.1.0-draft Date: 2025-12-17 Status: Draft SPDX-License-Identifier: PMPL-1.0-or-later
Ephapax (from Greek ἐφάπαξ, "once for all") is a programming language with a linear type system designed for safe memory management targeting WebAssembly.
- Memory Safety: Prevent use-after-free, double-free, and memory leaks
- No Runtime GC: All memory management resolved at compile time
- WASM-First: Optimised for WebAssembly's linear memory model
- Formal Foundation: Type system grounded in linear logic with mechanised proofs
- Rust: Ownership and borrowing concepts (simplified)
- Linear Haskell: Linear types and multiplicity annotations
- MLKit: Region-based memory management
- Cyclone: Safe C with regions and unique pointers
let let! fn if then else
region drop copy true false inl
inr case of match with end
+ - * / % -- Arithmetic
== != < > <= >= -- Comparison
&& || ! -- Logical
& -- Borrow
@ -- Region annotation
-> -- Function arrow
: -- Type annotation
identifier ::= [a-zA-Z_][a-zA-Z0-9_]*
type_var ::= '\'' [a-z]+
region_name ::= [a-z][a-z0-9_]*
integer ::= [0-9]+
float ::= [0-9]+ '.' [0-9]+
string ::= '"' [^"]* '"'
bool ::= 'true' | 'false'
unit ::= '()'
| Type | Description | Linearity |
|---|---|---|
() |
Unit type | Unrestricted |
Bool |
Boolean | Unrestricted |
I32 |
32-bit signed integer | Unrestricted |
I64 |
64-bit signed integer | Unrestricted |
F32 |
32-bit float | Unrestricted |
F64 |
64-bit float | Unrestricted |
String@r
A string allocated in region r. Strings are linear: they must be used exactly once.
T1 -> T2
Functions consume their linear arguments and produce a result.
(T1, T2)
Pairs. If either component is linear, the pair is linear.
T1 + T2
Discriminated unions. Linear if either variant is linear.
&T
A second-class borrow of type T. Borrows:
- Cannot be stored in data structures
- Cannot be returned from functions
- Cannot escape the scope where created
region r { T }
Type T scoped to region r.
x
Reference to a bound variable. Linear variables are consumed on use.
Standard let:
let x = e1 in e2
Linear let (explicit annotation):
let! x = e1 in e2
Allocation (in region):
String.new@r("hello")
Concatenation (consumes both):
String.concat(s1, s2)
Length (borrows):
String.len(&s)
Lambda:
fn(x: T) -> e
Application:
f(e)
Construction:
(e1, e2)
Projection:
e.0 -- first
e.1 -- second
Destructuring let:
let (x, y) = e1 in e2
Injection:
inl[T2](e) -- left
inr[T1](e) -- right
Case analysis:
case e of
inl(x) -> e1
inr(y) -> e2
end
if e1 then e2 else e3
Both branches must consume the same linear resources.
region r {
e
}
All allocations in e using @r are freed when the region exits.
Create borrow:
&e
The original value remains available; the borrow provides temporary read access.
Explicit drop:
drop(e)
Explicit copy (unrestricted types only):
copy(e)
R; Γ ⊢ e : T ⊣ Γ'
Where:
Ris the set of active regionsΓis the input typing contexteis the expressionTis the typeΓ'is the output context (tracking consumption)
A context Γ is a list of (x : T, used) triples.
Lookup: Γ(x) = T if (x : T, _) ∈ Γ
Mark used: Γ[x ↦ used] sets the used flag for x
Extend: Γ, x : T adds a fresh binding
T-Var-Lin (Linear variable use):
Γ(x) = T linear(T) ¬used(Γ, x)
─────────────────────────────────────
R; Γ ⊢ x : T ⊣ Γ[x ↦ used]
T-StringNew (String allocation):
r ∈ R
────────────────────────────────
R; Γ ⊢ String.new@r(s) : String@r ⊣ Γ
T-StringConcat (Concatenation):
R; Γ ⊢ e1 : String@r ⊣ Γ'
R; Γ' ⊢ e2 : String@r ⊣ Γ''
────────────────────────────────────────
R; Γ ⊢ String.concat(e1, e2) : String@r ⊣ Γ''
T-Region (Region scope):
r ∉ R R ∪ {r}; Γ ⊢ e : T ⊣ Γ' T does not mention r
──────────────────────────────────────────────────────────
R; Γ ⊢ region r { e } : T ⊣ Γ'
T-Borrow (Second-class borrow):
Γ(x) = T
────────────────────────────
R; Γ ⊢ &x : &T ⊣ Γ
Memory is a byte array indexed by 32-bit addresses. Strings are represented as (ptr, len) pairs.
A region is identified by a saved bump pointer. Region entry pushes the current bump pointer; region exit restores it, effectively freeing all allocations made within the region.
String Allocation:
⟨μ, R, ρ, String.new@r(s)⟩
──→ ⟨μ', R, ρ, loc⟩
where μ' = μ[loc ↦ s], loc fresh
String Concatenation:
⟨μ, R, ρ, String.concat(loc1, loc2)⟩
──→ ⟨μ', R, ρ, loc3⟩
where μ(loc1) = s1, μ(loc2) = s2
μ' = μ[loc1 ↦ ⊥][loc2 ↦ ⊥][loc3 ↦ s1++s2]
Region Exit:
⟨μ, r::R, ρ, region r { v }⟩
──→ ⟨free_region(μ, r), R, ρ, v⟩
If R; Γ ⊢ e : T ⊣ Γ' and e is not a value, then e can take a step.
If R; Γ ⊢ e : T ⊣ Γ' and e ──→ e', then R; Γ'' ⊢ e' : T ⊣ Γ' for some Γ''.
If R; Γ ⊢ e : T ⊣ Γ' and e ──→* v, then all linear bindings in Γ are marked used in Γ'.
- No use-after-free: Linear types ensure resources are not accessed after consumption
- No double-free: Linear types ensure resources are consumed at most once
- No leaks: Linear types ensure resources are consumed at least once
- No region escapes: Region-scoped types cannot outlive their region
String.new : ∀r. (data: &[u8]) -> String@r
String.len : ∀r. (&String@r) -> I32
String.concat : ∀r. (String@r, String@r) -> String@r
String.slice : ∀r. (&String@r, I32, I32) -> String@r
String.eq : ∀r. (&String@r, &String@r) -> Bool
File.open : ∀r. (&String@r) -> Result[File@r, Error]
File.read : ∀r. (&mut File@r, &mut [u8]) -> Result[I32, Error]
File.write : ∀r. (&mut File@r, &[u8]) -> Result[I32, Error]
File.close : ∀r. (File@r) -> Result[(), Error]
| Ephapax Type | WASM Representation |
|---|---|
() |
(no value) |
Bool |
i32 (0 or 1) |
I32 |
i32 |
I64 |
i64 |
F32 |
f32 |
F64 |
f64 |
String@r |
i32 (handle/pointer) |
(T1, T2) |
i32 (pointer to struct) |
T1 + T2 |
i32 (pointer to tagged) |
- Arguments passed on WASM stack
- Linear arguments consumed (caller loses access)
- Return value on WASM stack
[0x0000] Bump pointer (4 bytes)
[0x0004] Region stack pointer (4 bytes)
[0x0008] Region stack (256 bytes, 64 levels max)
[0x0108] Heap start
... bump-allocated data ...
type File : Linear
type File<Open> : Linear
type File<Closed> : Linear
fn close : File<Open> -> File<Closed>
type File<P : Permission>
fn read : &(1/2) File -> Bytes
fn write : &(1) File -> ()
type Chan<T> : Linear
fn send : (Chan<T>, T) -> ()
fn recv : Chan<T> -> T
module ::= decl*
decl ::= fn_decl | type_decl
fn_decl ::= 'fn' IDENT '(' params ')' ':' type '=' expr
type_decl ::= 'type' IDENT '=' type
params ::= (param (',' param)*)?
param ::= IDENT ':' type
type ::= base_type
| 'String' '@' IDENT
| type '->' type
| '(' type ',' type ')'
| type '+' type
| '&' type
| 'region' IDENT '{' type '}'
base_type ::= '()' | 'Bool' | 'I32' | 'I64' | 'F32' | 'F64'
expr ::= literal
| IDENT
| 'String.new' '@' IDENT '(' STRING ')'
| 'String.concat' '(' expr ',' expr ')'
| 'String.len' '(' expr ')'
| 'let' IDENT '=' expr 'in' expr
| 'let!' IDENT '=' expr 'in' expr
| 'fn' '(' IDENT ':' type ')' '->' expr
| expr '(' expr ')'
| '(' expr ',' expr ')'
| expr '.' ('0' | '1')
| 'inl' '[' type ']' '(' expr ')'
| 'inr' '[' type ']' '(' expr ')'
| 'case' expr 'of' 'inl' '(' IDENT ')' '->' expr
'inr' '(' IDENT ')' '->' expr 'end'
| 'if' expr 'then' expr 'else' expr
| 'region' IDENT '{' expr '}'
| '&' expr
| 'drop' '(' expr ')'
| 'copy' '(' expr ')'
literal ::= INTEGER | FLOAT | STRING | 'true' | 'false' | '()'- Wadler, P. (1990). Linear types can change the world!
- Tofte, M. & Talpin, J.P. (1997). Region-based memory management
- Walker, D. (2005). Substructural type systems (ATTAPL Chapter 1)
- Bernardy, J.P. et al. (2018). Linear Haskell: practical linearity in a higher-order polymorphic language
- Grossman, D. et al. (2002). Region-based memory management in Cyclone
End of Specification