STLC Core Surface
This example builds a small source language over the existing HOAS checker. The surface adds syntax and source positions; the kernel still owns typing and normalization.
Define the surface
defineSurface maps each source constructor to a HOAS expression.
The core STLC fragment has Pi types, lambdas, annotations, and
applications.
surface = S.defineSurface {
name = "STLC";
description = "Small typed lambda-calculus surface example.";
constructors = {
lam = {
tag = "stlc.lam";
handler = { depth, h, elaborate, hoas, ... }:
elaborate depth (hoas.lam h.name h.domain h.body);
};
app = {
tag = "stlc.app";
handler = { depth, h, elaborate, hoas, ... }:
elaborate depth (hoas.app h.fn h.arg);
};
};
};
Smart constructors
Users construct source terms with small Nix functions. The values are
surface nodes until check elaborates them into HOAS.
arrow = domain: codomain: pi "_" domain (_: codomain);
lam = name: domain: body: surface.mk "lam" { inherit name domain body; };
ann = term: type: surface.mk "ann" { inherit term type; };
app = fn: arg: surface.mk "app" { inherit fn arg; };
idNatTy = arrow H.nat H.nat;
idNat = lam "x" H.nat (x: var x);
idNatAnn = ann idNat idNatTy;
Expected-type holes
holeLam omits a lambda domain. The handler reads the expected type,
solves a surface implicit metavariable with the Pi domain, and then
elaborates an ordinary typed lambda.
holeLambdaFromExpected = { context, name, body, hoas ? H }:
let
implicit = context.implicitMeta {
type = { ctx = C.emptyCtx; ty = V.vU V.vLevelZero; };
label = "stlc.lambda-domain";
};
expectedType = context.expectedType or null;
domain = if expectedType == null then null else piDomain expectedType;
in
if expectedType == null
then S.unsolvedImplicitError { metas = [{ id = implicit.id; }]; }
else if domain == null
then expectedFunctionError context
else {
term = hoas.lam name domain body;
solvedState = M.solveMeta implicit.id (E.eval [ ] (hoas.elab domain)) implicit.state;
};
Implicit insertion
Plicity-style implicit binders are carried as sidecars on Pi and lambda nodes. During application, the elaborator inserts fresh metas for implicit arguments and solves them from explicit arguments or the expected residual type.
polyIdTy = implicitPi "A" universe0 (A: arrow A A);
polyId = implicitLam "A" universe0 (A: lam "x" A (x: var x));
polyIdAnn = ann polyId polyIdTy;
stlcPolyIdAppliedSolvesImplicit =
let
position = { path = [ "poly" "id" "zero" ]; };
r = check (app polyIdAnn H.zero) H.nat position;
in
!(r ? error) && r.tag == "app";