TypeScript engine
@formalang/ts on npm. Runs in Node and in the browser; the workbench above runs it in a Web Worker.
Forma is an incubation project within WorldVM.
Skip to contentA typed Lisp for domain languages · pre-alpha
Forma is a small typed Lisp. You define domain keywords like entity or service as library code. The compiler checks programs that use them, reports errors at the line you wrote, and emits typed artifacts that other systems consume.
Your vocabulary. A checked core. One source instead of six files that drift.
(entity Employee {:name String :active Bool}) │ ├─ read lossless syntax tree ├─ expand macros → core forms ├─ infer types + effect sets ├─ elaborate descriptor checks slots ✓ └─ emit typed IR · Effect TS
↓ the real compiler, running in this page · try it
Edit real source. Watch typing removes a requirement, pauses on the compiler's red underline, and types the repair. Inferred contracts, diagnostics, typed IR, and Effect TypeScript all come from the source in this editor. Switch examples to explore inference and macro expansion.
00 The hidden model
Entities in JSON Schema. Endpoints in OpenAPI. Types in TypeScript. Failures nobody declared. The glue in a code generator nobody wants to touch. Every copy drifts, and when something breaks, the error points at generated code instead of the decision someone made. Each of those becomes a form.
"required": ["name", "department"]← an entityentity/employees: get: parameters: …← a queryqueryinterface Employee { … } // copy #3← a typetypethrow new Error("console down")← a failureerrorimport { print } from "./anywhere"← a capabilityservice{{#each fields}} // owner left← a generatorformExtension
The keyword is library code.
Adding one field to Employee means editing a JSON Schema, an OpenAPI spec, a TypeScript interface, and a Handlebars template whose author left two years ago.
entity is not built into the compiler. A prelude describes it with form: its slots, the name it binds, its result type, and the hook that constructs its output. Swap the prelude and you have a different language on the same checked core.
(form
(entity name fields {:keys [doc role id-pattern tier]})
"A named record of attributes."
:types
{
:name (Declares SchemaDecl)
:fields (Record Type)
:doc (Option String)
:role (Option String)
:id-pattern (Option String)
:tier (Option (Union :user :meta))}
:ir EntityDeclarationIR
{
:kind (if (= tier :meta) "MetaEntity" "Entity")
:name name
:fields (fields->ir name fields)
:doc doc
:role role
:idPattern id-pattern})(entity Department {:name String})
(entity Employee {:name String
:department (Option (Id Department))
:active Bool})
(query employee-directory
:from Employee
:where active
:select [name department])The elaboration hooks resolve Employee, check that :where is boolean, and project the selected fields into typed IR. The same prelude machinery could describe endpoints, policies, workflows, or UI instead. Open this example →
Contracts
The type says what the code can do.
A one-line change makes a helper print to the console. The diff looks harmless. Nobody can see that the function now depends on a service that can fail.
Operations infer Effect<A, E, R>. A reviewer reads one line instead of a call graph:
The contract survives code generation: requirements are lowered to Context services in the generated Effect TypeScript. Building with Effect? Read what Forma adds, and what it costs →
(error ConsoleUnavailable {:message String})
(service Console
(: print (-> String (Effect Unit [ConsoleUnavailable] []))))
(: log (-> String (Effect Unit [ConsoleUnavailable] [Console.print])))
(define log [message]
(do! [_ (Console.print message)]
(succeed nil)))String -> Effect<Unit, ErrorSet<ConsoleUnavailable>, RequirementSet<Console.print>>import { Context, Effect, Schema } from "effect";
export class ConsoleUnavailable extends Schema.TaggedError<ConsoleUnavailable>()("ConsoleUnavailable", {
message: Schema.String,
}) {}
export class Console extends Context.Service<
Console,
{
readonly print: (arg0: string) => Effect.Effect<void, ConsoleUnavailable>;
}
>()("Console") {}
export const log = (message: string): Effect.Effect<void, ConsoleUnavailable, Console> =>
Effect.gen(function* () {
const consoleService = yield* Console;
yield* consoleService.print(message);
return;
});Diagnostics
Forget a capability, and it won't compile.
tsc fails on line 412 of generated.ts. The mistake was a missing entry in a YAML file three directories away, and the error never mentions it.
Here the body calls Console.print, but the signature declares no requirements. The Effect checker rejects the operation and points at the call you wrote. Click the problem to jump to its source, then add Console.print to the signature, or press Reset, and generation resumes.
04 The ambition
The same program can be source, an outline, or a domain view. A form's descriptor already teaches the compiler its checks and the structural workbench its slots. The ambition is to make a new domain language feel as well supported as a built-in one.
preludes/workflow.lisp — one descriptor, many readers ┌────────────────────────────────────────────────────── │ (form step …) slots :system · :reads · :writes │ ├──▶ compiler slot checks · HM types · effect sets ├──▶ editor completions · slot placeholders · outline ├──▶ reviewer proposals as tree diffs, with consequences └──▶ artifacts typed IR · Effect TypeScript └────────────────────────────────────────────────────── (step background-check :system "Checkr" :reads [:identity] :writes [:check])
Bonus round
The structural workbench edits an onboarding workflow as rows and as source at the same time, with live values and types, descriptor placeholders, and assistant proposals you review as tree diffs. It pauses for approval before simulated capabilities run.
05 Alternatives
Each common approach covers some of this well. Forma's bet is covering all of it in one small language.
| New domain forms | Checking | Output | Trade-off | |
|---|---|---|---|---|
| YAML/JSON + a generator | No, the format is fixed | Schema validation | Whatever the templates emit | Errors surface in generated code |
| Config languages (CUE, Pkl, Dhall, Nickel) | Functions and schemas, not new forms | Strong, for data | JSON, YAML, and other data | No effect or capability tracking |
Racket #lang | Yes, full language construction | Whatever you build; Typed Racket is separate | Programs on the Racket runtime | Hard to embed in a TypeScript or browser stack |
| TypeScript builder DSLs | Functions and objects | TypeScript's types | Runtime objects in one host | The DSL is the host program, so it's hard to review, sandbox, or port |
| Forma | form descriptors in preludes | Hindley–Milner inference, descriptor validation, effect sets | Typed IR (application/vnd.forma.ir+json), Effect TypeScript | Pre-alpha, with one generated target so far |
06 Engine
One TypeScript engine runs behind a JSON host ABI in Node, the browser, and editor tooling, and stored fixtures pin its behavior.
@formalang/ts on npm. Runs in Node and in the browser; the workbench above runs it in a Web Worker.
@formalang/host wraps the engine in a JSON host ABI, with a source-built forma JSON CLI and daemon.
Fixtures and reviewed goldens pin parsing, typing, evaluation, and IR output, and CI checks the engine against them.
A language server plus CodeMirror and React components for diagnostics, hover, and structural editing.
source.lisp
│
@formalang/ts
node · browser
│
JSON host ABI
┌────────┼────────┐
cli language workbench
server
conformance fixtures ──▶ stored
goldens checked in CI07 Before you bet on it
working today ✓ reader · formatter · macros ✓ HM inference · effect inference ✓ canonical IR ✓ Effect TypeScript + Schema ✓ playground · language server ✓ conformance goldens not yet ○ published CLI ○ stable host ABI ○ consumer-prelude SDK ○ Rust / OCaml codegen
08 Examples
Edit the source and step through each compiler pass in your browser. Preview examples label any output that comes from a stored golden.
entity; the source elaborates into typed IR.Open example →Domain languages · 02The type says what code can doDrop a capability from the signature and watch the typechecker reject it.Open example →The core languageThe complete pipelineRead, expand, typecheck, eval, and a live JSON target for one program.Open example →Structural editingThe outline is the programEdit an onboarding workflow with live values and types, a source pane, and assistant proposals you can review.Open the workbench →Start with the entity example, then read how preludes and descriptors work.
09 Part of the WorldVM family
Forma is one thread in a family of open experiments in software that can describe, remember, coordinate, and explain itself. WorldVM is where they meet: a runtime for operational worlds of entities, rules, goals, and actions.