This document explains the type lifting transformation - how MeTTaIL systematically derives a typed calculus from an untyped Graph-Structured Lambda Theory (GSLT). This transformation is central to MeTTaIL's approach to semantic type checking.
Target audience: Compiler engineers implementing type systems
Prerequisites: Read 03-gph-enriched-lawvere.md for GSLT fundamentals.
T(G) = G -- shapes stay shapes
T(A x B) = T(A) x T(B) -- products lift pointwise
T(A -> B) = T(A) x (T(A) -> T(B)) -- exponentials add type param
// Pseudocode representation
struct Sort { name: String, is_generating: bool }
struct Morphism {
name: String,
inputs: Vec<Arity>,
output: Sort,
}
enum Arity {
Sort(Sort),
Product(Box<Arity>, Box<Arity>),
Exponential(Box<Arity>, Box<Arity>), // A -> B
}
struct TypeLiftedMorphism {
original: Morphism,
lifted_name: String, // e.g., "!!" for "!"
lifted_inputs: Vec<Arity>,
lifted_output: Sort,
extra_factors: Vec<Sort>, // from duplication rule
}
Following Wells & Stay's "Behavior in Higher-Order Languages", we can view the type lifting transformation through a more unified lens. A lambda theory presentation T consists of four components:
\Gamma \vdash f(x⃗) : B$p : \text{Pr}, q : \text{Pr} \vdash p \rightsquigarrow q$ for rewriting)| GSLT (Current) | Lambda Theory (New) | Description |
|---|---|---|
shapes P, N, R | T_type = {Pr, Nm} | Base types / generating shapes |
fn sym f: A -> B | $T_\text{oper}: a : A \vdash f(a) : B$ | Operation / function symbol |
rewrite r: A -> R | $T_\text{prop}: p,q : \text{Pr} \vdash p \rightsquigarrow q$ | Rewrite proposition |
equation e | $T_\text{ent}: \Gamma \| \Phi \vdash t \rightsquigarrow u$ | Entailment / inference rule |
T_type = {Pr, Nm}
T_oper = {
⊢ 0 : Pr
p₁ : Pr, p₂ : Pr ⊢ p₁ | p₂ : Pr
n : Nm, p : Pr ⊢ out(n, p) : Pr
n : Nm, λx.q : [Nm → Pr] ⊢ in(n, λx.q) : Pr
p : Pr ⊢ @p : Nm
n : Nm ⊢ *n : Pr
}
T_prop = {(p : Pr, q : Pr ⊢ p ⇝ q)}
T_ent = {
n : Nm, p : Pr, λx.q : [Nm → Pr] | ⊤ ⊢ out(n,p) | in(n,λx.q) ⇝ q[@p/x]
p₁,p₂,q : Pr | (p₁ ⇝ p₂) ⊢ p₁ | q ⇝ p₂ | q
p : Pr | ⊤ ⊢ *(@p) ⇝ p
}
This format provides:
The type lifting transformation described below can be seen as constructing a fibered category SubT → T where:
\varphi$ ↣ $\Gamma$\{x : A \mid \varphi\}$ arise naturallyGiven a GSLT describing an untyped calculus, we want to systematically produce a typed version where:
f has a "type-level" counterpart (conventionally ff)MeTTa's operational semantics (see 01-metta-operational-semantics.md) can be formalized as a GSLT. Type lifting transforms this into a typed theory that captures MeTTa's reduction behavior at the type level.
| MeTTa Concept | Untyped | Type-Lifted |
|---|---|---|
| Query operation | query: Term x KB -> State | queryquery: T(Term) x T(KB) -> T(State) |
| List construction | cons: Term x List -> List | conscons: T(Term) x T(List) -> T(List) |
| Multiset insertion | insert: Term x MSet -> MSet | insertinsert: T(Term) x T(MSet) -> T(MSet) |
Every function symbol f gets a "shadow" function symbol (type-lifted version):
Original: f: inputs -> output
Type-lifted: ff: T(inputs) -> T(output)
Where T(-) is a transformation on arities. The type ff(...) represents the
structural type of a term built with f(...).
T(G) = G (for any generating shape G)
Meaning: Types of things of shape G are also of shape G.
Why: A type of a process is still a process (structurally). Types describe the "shape" of what could be in a position.
Examples across calculi:
| Calculus | Original | Type-lifted |
|---|---|---|
| lambda | App: P x P -> P | AppApp: P x P -> P |
| SKI | S: 1 -> P | SS: 1 -> P |
| RHO | *: N -> P | **: N -> P |
| Ambient | open: N -> M | openopen: N -> M |
| MeTTa | atom: Atom -> Term | atomatom: Atom -> Term |
T(A x B) = T(A) x T(B)
Meaning: Products transform component-wise.
Why: The type of a pair is a pair of types.
Examples across calculi:
| Calculus | Original | Type-lifted |
|---|---|---|
| lambda | App: P x P -> P | AppApp: P x P -> P |
| RHO | \|: P x P -> P | \|\|: P x P -> P |
| RHO | !: N x P -> P | !!: N x P -> P |
| Ambient | []: N x P -> P | [][]: N x P -> P |
| MeTTa | cons: Term x List -> List | conscons: Term x List -> List |
T(A -> B) = T(A) x (T(A) -> T(B))
Meaning: An exponential becomes a product of:
T(A)T(A) -> T(B)Why: To type a binding construct, we need:
This enables dependent types - the type of the body can depend on what's bound.
Examples across calculi:
| Calculus | Original | Type-lifted | Extra factor |
|---|---|---|---|
| lambda | Lam: (P -> P) -> P | LamLam: P x (P -> P) -> P | Type of bound variable |
| RHO | ?: N x (N -> P) -> P | ??: N x N x (N -> P) -> P | Type of received name |
| Ambient | nu: (N -> P) -> P | nunu: N x (N -> P) -> P | Type of restricted name |
Let's trace through the transformation for RHO's receive operator:
Original: ?: N x (N -> P) -> P
N x (N -> P)T(N x (N -> P))= T(N) x T(N -> P) (Rule 2)= N x T(N -> P) (Rule 1)= N x (T(N) x (T(N) -> T(P))) (Rule 3)= N x (N x (N -> P)) (Rule 1)= N x N x (N -> P) (associativity)Result: ??: N x N x (N -> P) -> P
Interpretation: The type of a receive ?(x, lambda y.body) is ??(X, Y, lambda y.B) where:
X is the type of the channel xY is the type of names the continuation expectslambda y.B is the type of the body as a function of the received name's typeThe type lifting rules can be understood through the meta-context formalism
(Definition 1 in Wells & Stay's paper). A meta-context $\langle -\rangle : T \to T$ is an
endofunctor on the type category generated by:
- \times X$ (product on the left)[X \to -]$ (exponential)The type lifting transformation T(-) corresponds to:
| Input Arity A | T(A) as Meta-Context | Interpretation |
|---|---|---|
| Generating shape G | G (identity) | Types stay in same shape |
| Product A₁ × A₂ | T(A₁) × T(A₂) | Product of lifted types |
| Arrow A₁ → A₂ | T(A₁) × [T(A₁) → T(A₂)] | Type + dependent type function |
Key insight: The exponential rule T(A → B) = T(A) × [T(A) → T(B)] captures
that typing a binder requires both:
This structure is what enables dependent types to emerge naturally from operational semantics.
Rewrite operations in the meta-context formalism have the form:
∏⟨Γᵢ⟩ᵢ | ⋀⟨pᵢ ⇝ qᵢ⟩ᵢ ⊢ f(p⃗) ⇝ f(q⃗)
This says: "given contexts $\Gamma _{i}$ and rewrite hypotheses pᵢ ⇝ qᵢ, the operation f
applied to the sources rewrites to f applied to the targets."
The basic type lifting rules aren't quite sufficient. We need an additional rule for handling duplicated inputs in reduction sources.
Consider RHO's comm reduction:
comm: N x (N -> P) x P -> R
src(comm(x, K, Q)) = |(?(x, K), !(x, Q))
Notice that x appears twice in the source:
?(x, K) - the channel being listened on!(x, Q) - the channel being sent onThis duplication affects typing because the types of ! and ? need to "know"
they're on the same channel.
Duplication Rule: If an input a_j: A_j to a base reduction appears in
multiple function symbols in the source, extend each of those function symbols'
type-lifted version by A_j.
Step 1: Identify the base reduction
comm: N x (N -> P) x P -> R
Inputs: x: N, K: N -> P, Q: P
Step 2: Examine the source
src(comm(x, K, Q)) = |(?(x, K), !(x, Q))
Step 3: Find duplications
x appears in ?(x, K) - input to ?x appears in !(x, Q) - input to !So x is duplicated between ? and !.
Step 4: Extend type-lifted symbols
Without duplication rule:
!: N x P -> P becomes !!: N x P -> P?: N x (N -> P) -> P becomes ??: N x N x (N -> P) -> PWith duplication rule (add x N for the channel):
!!: N x P -> P becomes !!: N x P x N -> P??: N x N x (N -> P) -> P becomes ??: N x N x (N -> P) x N -> PInterpretation:
!!(A, B, x) - type of send on channel x sending something of type B??(A, B, lambda y.C, x) - type of receive on channel x expecting type BThe extra x N ensures the types know which channel is involved.
lambda-calculus: No duplication in beta rule
src(beta(K, N)) = App(Lam(K), N)
Each input appears once, so no extension needed.
SKI: Duplication in S combinator
src(sigma3(x, y, z)) = App(S2(x, y), z)
tgt(sigma3(x, y, z)) = App(App(x, z), App(y, z))
z appears twice on the RHS, affecting the types.
Ambient: Multiple duplications
src(in_red(n, m, Q, R, S)) = |([](n, |(.(in(m), Q), R)), [](m, S))
Both n and m appear in multiple places.
Equations in the untyped GSLT lift to equations in the typed GSLT.
If f(...) = g(...) is an equation in the untyped GSLT, then ff(...) = gg(...)
is an equation in the typed GSLT (with appropriate lifted arities).
Parallel composition properties:
| Untyped | Typed |
|---|---|
\|(P, Q) = \|(Q, P) | \|\|(T, U) = \|\|(U, T) |
\|(\|(P, Q), R) = \|(P, \|(Q, R)) | \|\|(\|\|(T, U), V) = \|\|(T, \|\|(U, V)) |
\|(P, 0) = P | \|\|(T, 00) = T |
Quote/unquote:
| Untyped | Typed |
|---|---|
@(*x) = x | @@(**A) = A |
The source and target equations lift to srcsrc and tgttgt:
Untyped comm:
src(comm(x, K, Q)) = |(?(x, K), !(x, Q))
tgt(comm(x, K, Q)) = ev(K, @(Q))
Typed comm:
srcsrc(commcomm(A, B, lambda y.C, x)) = ||(?(A, B, lambda y.C, x), !!(A, **(B), x))
tgttgt(commcomm(A, B, lambda y.C, x)) = C
Note:
commcomm is the type of a comm reductionsrcsrc gives the type of the sourcetgttgt gives the type of the targetC (the body type), possibly with substitutionThe most interesting part of type lifting is the derivation of modal types from reduction structure.
In a reduction source like:
src(comm(x, K, Q)) = |(?(x, K), !(x, Q))
Each subtree of the AST can be viewed in its surrounding context:
|
/ \
?(x,K) !(x,Q)
/ \ / \
x K x Q
Subtrees:
Q in context |(?(x,K), !(x, []))?(x,K) in context |([], !(x,Q))!(x,Q) in context |(?(x,K), [])Each subtree's position gives rise to a possibility modal type.
The type A: <K[-]>B means:
"A term of type A, when placed in context K, possibly reduces to something of type B"
For RHO's comm rule:
Q: <|(?(x,K), !(x,[]))>C means "Q, when sent on channel x, enables reduction
to continuation body C"Independent modal types (_i): Only track the target type
ctxposs_i(T) -- possibly reduces to type T
ctxcomm_i(...) -- in comm context, reduces to type T
Dependent modal types (_d): Also track the target term
ctxposs_d(T, t) -- possibly reduces to term t of type T
ctxcomm_d(..., t) -- in comm context, reduces to term t
When to use each:
From the comm reduction, we derive:
| Modal Type | Meaning |
|---|---|
ctxrecv_i(T, U, V, W, X) | Process type that, when juxtaposed with receive type ??(T, U, V, W), possibly reduces to type X |
ctxsend_i(T, U, V, W) | Process type that, when juxtaposed with send type !!(T, U, V), possibly reduces to type W |
ctxcomm_i(T, U, V, W, X) | Process type that, when sent and received, possibly reduces to type X |
ctxposs_i(T) | General possibility: process that possibly reduces to type T |
Plus dependent versions ctxrecv_d, ctxsend_d, ctxcomm_d, ctxposs_d.
These modal types satisfy equations relating contexts to general possibility:
||(ctxrecv_i(T, U, V, W, X), ??(T, U, V, W)) = ctxposs_i(X)
||(!!(T, ctxcomm_i(T, U, V, W, X), W), ??(T, U, V, W)) = ctxposs_i(X)
||(ctxsend_i(T, U, V, W), !!(T, U, V)) = ctxposs_i(W)
Reading the first equation: A process with receive-context modal type, in parallel with a matching receive, has general possibility type.
Untyped GSLT:
App: P x P -> P
Lam: (P -> P) -> P
beta: (P -> P) x P -> R
Apply transformation:
T(P x P) = P x P -> AppApp: P x P -> PT((P -> P)) = P x (P -> P) -> LamLam: P x (P -> P) -> PT((P -> P) x P) = T(P -> P) x T(P) = (P x (P -> P)) x P -> betabeta: P x (P -> P) x P -> RCheck for duplication in beta:
src(beta(K, N)) = App(Lam(K), N)
K appears once (in Lam(K))N appears once (as second arg to App)No duplication, so no extension needed.
Typed GSLT:
AppApp: P x P -> P
LamLam: P x (P -> P) -> P
betabeta: P x (P -> P) x P -> R
Modal types: From the beta reduction:
<App(Lam([]), N)>B - abstraction in application context<App(Lam(K), [])>B - argument in application contextThese give rise to arrow types! The context <App(Lam([]), N)>B is essentially
saying "a function that, applied to N, reduces to B".
The S combinator creates interesting duplication:
sigma3: P x P x P -> R
src(sigma3(x, y, z)) = App(S2(x, y), z)
tgt(sigma3(x, y, z)) = App(App(x, z), App(y, z))
z appears twice in the target (in App(x, z) and App(y, z)).
This means:
x has a modal type dependent on zy has a modal type dependent on z similarlyThe result: S has type that looks like (C -> B -> A) -> (C -> B) -> C -> A.
Type-lifted symbols (with duplication):
00: 1 -> P
||: P x P -> P
!!: N x P x N -> P -- extra N for channel
??: N x N x (N -> P) x N -> P -- extra N for channel
**: N -> P
@@: P -> N
srcsrc: R -> P
tgttgt: R -> P
commcomm: N x N x (N -> P) x N -> R
Modal types:
ctxrecv_i: N x N x (N -> P) x N x P -> P
ctxrecv_d: N x N x (N -> P) x N x P x P -> P
ctxsend_i: N x P x N x P -> P
ctxsend_d: N x P x N x P x P -> P
ctxcomm_i: N x N x (N -> P) x N x P -> P
ctxcomm_d: N x N x (N -> P) x N x P x P -> P
ctxposs_i: P -> P
ctxposs_d: P x P -> P
Based on the MeTTa state machine model from 01-metta-operational-semantics.md:
Untyped operations:
state: Term x KB x MSet x MSet -> State
query: Term -> R (as part of state transition)
cons: Term x List -> List
insert: Term x MSet -> MSet
Type-lifted operations:
statestate: Term x KB x MSet x MSet -> State -- all generating sorts
queryquery: Term -> R
conscons: Term x List -> List
insertinsert: Term x MSet -> MSet
Multiset equations lift:
insert(x, insert(y, m)) = insert(y, insert(x, m))
becomes:
insertinsert(X, insertinsert(Y, M)) = insertinsert(Y, insertinsert(X, M))
| Transformation | lambda | SKI | RHO | Ambient | MeTTa |
|---|---|---|---|---|---|
| Rule 1 (shapes) | App, Lam | App, S, K, I, S1, S2, K1 | All | All | All |
| Rule 2 (products) | App | App | |, !, [], etc. | |, [], ., etc. | cons, insert, state |
| Rule 3 (exponentials) | Lam | None | ? | nu | Pattern bindings |
| Duplication | None | S combinator (z) | comm (channel x) | in, out (n, m) | Query matches |
| Modal types | head context | head context | par, comm, exec | ambient, par, in/out/open | Query, chain, transform |
def type_lift_gslt(gslt: GSLT) -> TypedGSLT:
result = TypedGSLT()
# 1. Copy generating sorts
for sort in gslt.generating_sorts:
result.add_sort(sort)
# 2. Transform each morphism
for morph in gslt.morphisms:
lifted = lift_morphism(morph)
result.add_morphism(lifted)
# 3. Find duplications in reductions
duplications = find_duplications(gslt.reductions)
# 4. Extend morphisms per duplication rule
for (morph, extra_factors) in duplications:
result.extend_morphism(morph, extra_factors)
# 5. Lift equations
for eq in gslt.equations:
result.add_equation(lift_equation(eq))
# 6. Extract modal types from reduction sources
for reduction in gslt.reductions:
modals = extract_modal_types(reduction)
for modal in modals:
result.add_morphism(modal)
return result
def lift_arity(arity: Arity) -> Arity:
"""Apply T(-) to an arity"""
match arity:
case Sort(s):
return Sort(s) # Rule 1
case Product(a, b):
return Product(lift_arity(a), lift_arity(b)) # Rule 2
case Exponential(a, b):
# Rule 3: T(A -> B) = T(A) x (T(A) -> T(B))
ta = lift_arity(a)
tb = lift_arity(b)
return Product(ta, Exponential(ta, tb))
def lift_morphism(morph: Morphism) -> TypeLiftedMorphism:
lifted_inputs = [lift_arity(inp) for inp in morph.inputs]
return TypeLiftedMorphism(
original=morph,
lifted_name=morph.name + morph.name, # ff convention
lifted_inputs=lifted_inputs,
lifted_output=morph.output, # sorts stay same
extra_factors=[]
)
def find_duplications(reductions: List[Reduction]) -> List[Tuple[Morphism, List[Sort]]]:
"""
For each reduction, find which inputs appear multiple times in the source.
Return list of (morphism, extra_factors) pairs.
"""
result = []
for red in reductions:
# Parse source expression
source_ast = parse(red.source_equation)
# Count occurrences of each input variable
input_counts = count_variable_occurrences(source_ast, red.inputs)
# Find duplicated inputs
duplicated = [inp for inp, count in input_counts.items() if count > 1]
# For each morphism in source that uses a duplicated input
for morph in morphisms_in_ast(source_ast):
used_duplicates = [inp for inp in duplicated if uses_input(morph, inp)]
if used_duplicates:
extra_factors = [inp.sort for inp in used_duplicates]
result.append((morph, extra_factors))
return result
def extract_modal_types(reduction: Reduction) -> List[ModalType]:
"""
Extract context-based modal types from reduction source.
Each subtree position generates independent and dependent modal types.
"""
result = []
source_ast = parse(reduction.source_equation)
# Walk AST, creating context for each subtree
for subtree, context in subtrees_with_contexts(source_ast):
# Independent modal type: tracks result type only
independent = ModalType(
name=f"ctx{context.name}_i",
params=context.type_params + [subtree.type],
result=Sort("P"), # process type
meaning=f"possibly reduces to type in context {context}"
)
result.append(independent)
# Dependent modal type: tracks result type and term
dependent = ModalType(
name=f"ctx{context.name}_d",
params=context.type_params + [subtree.type, Sort("P")],
result=Sort("P"),
meaning=f"possibly reduces to specific term of type in context {context}"
)
result.append(dependent)
return result
T(A) for the same A always gives the
same resultThe type lifting transformation is implemented in MeTTaIL's Scala prototype. Key files:
T(-) rulesSee 01-mettail-scala-prototype.md for details.
When translating between calculi (e.g., encoding lambda-calculus in RHO, or encoding MeTTa in a core calculus), we need formal criteria for when a translation correctly preserves operational behavior.
An encoding $[[-]] : S \to T$ from source calculus S to target calculus T must satisfy:
[[\Gamma \times \Delta ]] = [[\Gamma ]] \times [[\Delta ]]$[[\varphi [\sigma ]]] = [[\varphi ]][[\sigma ]]$ (substitution commutes with encoding)[[[A → B]]] = [[[A] → [[B]]]]Pr_S maps to $\langle \text{Pr}_T\rangle$ for some meta-context $\langle -\rangle$p \rightsquigarrow q \vdash [[p]] \to *_\tau [[q]]$ (source rewrites become silent paths)p \approx q \vdash [[p]] \approx [[q]]$p ≈⃗ [[p]] (source is behaviorally equivalent to its encoding)When implementing a translation pass:
The standard encoding of lambda-calculus in RHO:
[[x]] = *x
[[λx.M]] = for(u, λv. v!(u, [[M]])) -- for fresh name u
[[M N]] = (νu)( [[M]] | u?(λx.x, [[N]]) )
This encoding:
To verify an encoding preserves behavior:
The theoretical concepts map to the existing Scala implementation as follows:
| Theoretical Concept | Scala Location |
|---|---|
| Lambda theory presentation | BasePres in TheoryEnv.scala |
| Base types (T_type) | IdCat case class |
| Products (×) | ProdCat(left, right) |
| Exponentials (→) | ArrowCat(dom, cod) |
| Function declarations | FnDecl in ModuleProcessor.scala |
| Rewrite declarations | RewriteDecl type |
| Type-lifting transformation | Hypercube.liftTypes() method |
| Pipeline orchestration | Pipeline.scala pass architecture |
| Theory environment | TheoryEnv.scala context management |
| BNFC syntax rendering | BNFCRenderer.scala |
| Binder handling | DesugarBinds.scala |
// Arities (categories in the lambda theory)
sealed trait Cat
case class IdCat(name: String) extends Cat // T_type elements
case class ProdCat(left: Cat, right: Cat) extends Cat // Products
case class ArrowCat(dom: Cat, cod: Cat) extends Cat // Exponentials
// Function declarations (T_oper elements)
case class FnDecl(name: String, inputs: List[Cat], output: Cat)
// Rewrite declarations (T_ent elements)
case class RewriteDecl(name: String, source: Term, target: Term)
The type lifting transformation:
T(G) = G - shapes stay shapesT(A x B) = T(A) x T(B) - products lift pointwiseT(A -> B) = T(A) x (T(A) -> T(B)) - exponentials add a type parameterThis mechanical transformation turns operational semantics into type structure.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |