Typed RPC: one Endpoint, both sides type-checked (Servant, properly)#6
Merged
Conversation
The legacy LeanTea.Rpc is stringly-typed: Endpoint carries a
List String of param names and Handler := List String → IO String, so
nothing checks that the server decodes what the client sent or that the
client consumes what the server returns — the wire shape drifts
silently. This adds a typed layer where request/response types are part
of the endpoint and both sides are checked against them.
LeanTea.Rpc.Typed:
* Endpoint α β — the request and response types are in the type. One
declaration is the single source of truth.
* serve : Endpoint α β → (α → IO β) → Route — the handler is typed
(no List String, no manual parsing). Dispatch decodes the body to
α via FromJson (400 on failure) and encodes the β result. A wrong
or malformed request becomes a 400 automatically.
* Endpoint.clientFn — generates the browser JS
(fetch + JSON.stringify(req) + await r.json()) from the same
endpoint.
* Endpoint.stubDecl — generates the Lean type-check stub
(opaque apiFoo : Req → Async Resp) for the client checker.
LeanJs.TypeCheck — the client half. Since .leanjs is a separate DSL
with no type system (its Check pass does arity + record fields only),
we don't build a type checker into it — we emit the client to real
Lean and let Lean's elaborator check it. async def bodies emit as
do-blocks, await becomes ←, the async effect is modelled as an IO
alias purely so the types line up (we elaborate, never run). Fed a
prelude of the shared types + endpoint stubs, `lean` reports any call
site that disagrees with the server's types.
Result (examples/Tests/TypedRpcSpec.lean, 9/9): one
Endpoint SetCellReq SetCellResp drives a typed server (valid → 200,
malformed/wrong-shape → 400, unmatched → 404), the generated client JS,
and a client type-check where a correct .leanjs client passes and one
with a mistyped request field is rejected — checked against the same
SetCellReq/SetCellResp the server uses.
The legacy stringly LeanTea.Rpc stays for back-compat.
Type-check harness no longer re-declares the wire types as text (a
second definition that could drift). `TypeCheck.mkPrelude imports
stubs` builds a prelude that `import`s the module where the server's
types live, and `check` runs `lake env lean` so those imports resolve
against the project build. There is now exactly one definition of
`SetCellReq` / `SetCellResp`, shared by the server and the client
check.
The shared types move to `Tests.TypedRpcApi`; `TypedRpcSpec` imports
that module and passes `["Tests.TypedRpcApi"]` to `mkPrelude`. Spec
stays 9/9.
Docs:
* docs/05-rpc.md rewritten around `LeanTea.Rpc.Typed` — `Endpoint α β`,
typed server, and the both-sides compile-time check (the previous
text described an API that never shipped: a `handler` field on
`Endpoint`, `/api/_endpoints` discovery, `ToJsExpr`/`FromJsExpr`).
Legacy stringly `LeanTea.Rpc` documented as back-compat.
* README RPC section updated from "Servant-inspired / typed is
roadmap" to the shipped typed layer, with the compile-error example.
* Ch04 doc chapter gets a note pointing to the typed layer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the review: makes the API's input/output types checked on both the server and the client, from a single source of truth, so the wire shape can't drift (the actual point of the Servant-style claim).
The problem
Legacy
LeanTea.Rpcis stringly-typed:Endpointcarries aList Stringof param names andHandler := List String → IO String. The server hand-decodes (ps[0]?.bind String.toInt?), the client calls a generated untyped JS function. Nothing ties the two to a shared type — a request/response shape change compiles fine on both sides and breaks at runtime.What this adds
LeanTea.Rpc.Typed— types are part of the endpoint:serve : Endpoint α β → (α → IO β) → Route— the handler isα → IO β, noList String, no manual parsing. Dispatch decodes the body toαviaFromJson(400 on malformed/wrong-shape) and encodesβ.Endpoint.clientFn— generates the browser JS (fetch+JSON.stringify(req)+await r.json()) from the same endpoint.Endpoint.stubDecl— generates the Lean type-check stubopaque apiFoo : Req → Async Resp.LeanJs.TypeCheck— the client half..leanjsis a separate DSL with no type system (itsCheckdoes arity + record-field names only). Rather than build a type checker into it — reinventing what Lean has — we emit the client to real Lean and let Lean's elaborator check it:async def→do-block,await→←, the async effect modelled as anIOalias purely so the types line up (we elaborate, never run). Fed a prelude of the shared types + endpoint stubs,leanreports any call site that disagrees with the server's types.Proof (examples/Tests/TypedRpcSpec.lean — 9/9)
One
Endpoint SetCellReq SetCellRespdrives:{"ok":true,"value":"A1=42"}), malformed JSON → 400, wrong-shape → 400, unmatched → 404.leanjsclient passes; one with a mistyped request field (formla) is rejected — checked against the sameSetCellReq/SetCellRespapiSetCellgenerated, request serialized as JSONDesign note (why this shape)
The investigation confirmed
.leanjsisn't Lean-checked and the embeddedLeanTea.JsDSL is untypedExpr. The leverage was to reuse Lean's own elaborator for the client rather than hand-roll a type system — so the client and server share one definition and the check is Lean's real type checker, not an approximation.Scope / follow-ups (not in this PR)
import-ing the user's types module instead of the inline structure text the harness currently takes (removes the one remaining duplicate).lean's error line/col back to the.leanjssource (today it reports the emitted-harness position; the message itself — the offending field name — is already actionable).TypeCheck.checkinto the serve-time compile pipeline so a type error fails the build; migrate one example app (Sheet/Canvas) onto the typed layer..d.tsgeneration for external (non-Lean) JS/TS consumers.LeanTea.Rpckept for back-compat.Test plan
lake buildclean (178 targets)typed_rpc_spec9/9 (server dispatch + client type-check accept/reject + JS gen). Spawnsleanfor the client checks, same pattern asleanjs_spec(lean --run).