Note on news #2:
To give everyone a concrete idea of why this custom tcorefn (Typed CoreFn) makes building AOT backends drastically easier, I thought I’d share what the JSON output actually looks like under the hood.
It is important to note that tcorefn.json simply pushes further the idea of a desugared JSON that represents the PureScript code, by whitelisting additional info (not needed for JIT backends like JS).
Instead of just dumping source spans, the compiler now hands over the full memory layout (via dataDecls) and the deep type information at every AST node (via annotation.type).
Here are only 3 quick side-by-side examples of PureScript code and the resulting condensed JSON:
1. Data Declarations (no more guessing around ADTs)
In standard CoreFn, constructors appear as you go, forcing the backend to gather them. With tcorefn, the AOT backend knows the exact memory structure as soon as it reads the file.
PureScript:
data Maybe a = Nothing | Just a
tcorefn.json (at the root of the module):
"dataDecls": [
{
"typeName": "Maybe",
"constructors": [
{ "constructorName": "Nothing", "fieldTypes": [] },
{ "constructorName": "Just", "fieldTypes": [ { "TypeVar": { "name": "a" } } ] }
]
}
],
"decls": ...
The backend immediately sees the ADT structure. No need to crawl the AST to infer constructor arities or memory layouts.
2. Fully Typed AST Nodes (no more “Shadow Typing”)
In classic CoreFn, an AST node only holds a sourceSpan. With this format, every node knows exactly what type it handles.
PureScript:
-- implemented internally as: isJust = maybe false (const true)
isJust :: forall a. Maybe a -> Boolean
tcorefn.json (excerpt showing the root expression for isJust):
"expression": {
"type": "App",
"annotation": {
"sourceSpan": { "start": [279, 10], "end": [279, 34] },
// HERE: The full function signature is retained on the node
"type": {
"Func": {
"args": [ { "ADT": { "path": ["Data", "Maybe", "Maybe"], "args": [ { "TypeVar": { "name": "a" } } ] } } ],
"ret": "Boolean"
}
}
},
"abstraction": {
"type": "App",
"abstraction": { "type": "Var", "value": { "identifier": "maybe" } },
"argument": { "type": "Literal", "value": { "value": false } }
},
"argument": {
"type": "App",
"abstraction": { "type": "Var", "value": { "identifier": "const" } },
"argument": {
"type": "Literal",
"value": { "literalType": "BooleanLiteral", "value": true },
"annotation": {
// Primitive types are also explicitly attached to literals deep down
"type": "Boolean"
}
}
}
}
Every single AST node in decls has its exact type attached. You don’t need an external JIT-like type-checker or shadow structures in the backend to know if you’re dealing with a Boolean, an Int32, or a specific ADT.
3. Records and FFI (structural typing preserved)
One of the biggest challenges for AOT is knowing when you are manipulating a raw record/structure. This JSON makes it explicit.
PureScript:
foreign import identity :: forall a. { identity :: a } -> a
tcorefn.json (excerpt):
"abstraction": {
"type": "Var",
"value": { "identifier": "identity", "moduleName": ["Control", "Category"] },
"annotation": {
"meta": { "metaType": "IsForeign" },
"type": {
"Func": {
// The exact structure of the Record is preserved!
"args": [ { "Record": { "identity": { "TypeVar": { "name": "a" } } } } ],
"ret": { "TypeVar": { "name": "a" } }
}
}
}
}
When calling FFI or handling PureScript records, the backend receives the exact shape of the expected object. This makes generating C/Go structs (or anything similar, in other languages) drastically easier. And of course, all of this is recursive (e.g. "args": [ { "Record": { "identity": { "Record": ... } } } ])