That being said, we could absolutely enrich TAST to include these human intentions as well! The compiler actually holds the full SourceType (which includes ForAll quantifiers and typeclass constraints), before I deliberately strip them out in simplifyType to generate the structural tcorefn for AOT compilation. If needed for LLM context, it would be quite trivial to expose a TAST++ that serializes this high-level constraint information directly into the JSON.
We would add ann.sourceType (conceptually similar to a sourceMap) alongside the simplified type. For example, a function like foo :: forall a. Show a => a -> String would look like this (simplified):
"ann": {
"sourceSpan": { ... },
"type": {
"Func": {
"args": [
{ "Record": { "show": { "Func": { "args": [{ "TypeVar": "a" }], "ret": "String" } } } },
{ "TypeVar": "a" }
],
"ret": "String"
}
// ^ Perfect for AOT: simplified and desugared (constraints are explicitly dictionary arguments)
},
"sourceType": {
"ForAll": {
"identifier": "a",
"type": {
"ConstrainedType": {
"constraint": { "className": ["Prim", "Show"] },
"type": {
"Func": {
"args": [ { "TypeVar": "a" } ],
"ret": { "TypeConstructor": ["Prim", "String"] }
}
}
}
}
}
// ^ Perfect for Tooling/LLM: preserves the exact human intention `Show a => a -> String`
}
}
Regarding the human coder, the CST is for what’s explicitly written, and the TAST is for what’s (explicitly and) implicitly written (made explicit). A LLM can be interested in both, depending on the subject.
Since this might be off-topic for WASM (perhaps), feel free to open an issue in my Purescript fork. We’ll put our thoughts there!
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
Edit (the platform allows a maximum of 3 consecutive posts):
Hey @katsujukou & @harryprayiv,
As promised, I took some time to experiment with the Wasm backend to see if injecting the TAST metadata could help solve some of the optimization ceilings. I’ve put together a very simple, minimalist, hacky Proof of Concept (POC) on a fork, and the results are honestly quite good.
To test it, I targeted the “unboxing” heuristic. Currently, the backend uses counts.boxed <= 1 to guess if an Int can be safely unboxed as an I32. However, if that Int is used in a polymorphic context (even inside a dead or cold branch), the solver conservatively assigns it a Bx (Boxed) representation globally. This penalizes the hot path with multiple unnecessary allocations.
By propagating the ExprType metadata from my tcorefn fork all the way down to the MIR NonRec bindings, the compiler no longer has to guess. It strictly knows the variable is an Int. More importantly, by combining this absolute truth with a quick loop analysis (inLoop), we can forcefully assign Ti32 in hot paths while still falling back to the memory-saving heuristic for purely flat/linear code!
I wrote a small polyInt benchmark (a tight recursive loop of 6.4 million iterations holding an accumulator, but with a dead branch forcing polymorphic usage) to measure the impact:
Benchmark results (polyInt - 6.4M iterations):
-
Without TAST (current
counts.boxed heuristic): ~12.9ms (suffers from 1 allocation per loop iteration)
-
With TAST (forced
I32 fast-path): ~3.1ms (0 allocations in the hot path)
This structural proof yields a ~3.5x performance boost in scenarios that previously confused the heuristic solver.
Please note: this fork is not a clean, mergeable PR right now (it’s not nix-ified, the pattern matching updates across the MIR passes are a bit rough, etc.). It’s strictly a minimalist POC designed to prove the viability and the massive potential of having strict type information available for AOT compilation.
I just focused on one obvious improvement, but there are definitely many more things we could explore with this TAST approach:
-
Zero-cost Typeclass Dictionaries: Statically resolving properties at compile time and eliminating boxed dictionary records completely (mapping them to native Wasm structs with unboxed fields).
-
Fully Unboxed ADTs: Using the
dataDecls from the TAST to strictly map memory layouts and avoid boxing ADT payloads.
-
Eager Uncurrying: Knowing the exact arity of functions everywhere to avoid
$Clo allocations altogether.
- etc.
Also, this TAST-guided unboxing approach may be preferable to aggressive monomorphization for WebAssembly. It allows us to reap the benefits of native primitive speeds locally, without duplicating polymorphic functions, which keeps the final .wasm binaries lightweight.
If you’re curious, you can check out the POC commit here.
If this direction is promising for the future of purs-wasm, I’d be happy to discuss how we could make this structural approach cleaner and more robust together. Or, if you prefer, I would be more than happy to simply pass the torch and let you evolve independently with this approach, which I will have had the pleasure of bringing to your attention!
Cheers 