TASTs for AOT backends (Go, Wasm, Rust, F# & C#...)

Quick update on TAST v3:

The first major milestone (structural optimization and minification of types) is already implemented.

Injecting deep structural types at every node in v2 led to massive JSON payloads. To solve this, I’ve introduced a typeTable at the root of tcorefn.json. Types are now deduplicated globally per module, and AST nodes simply reference them via a lightweight typeId.

The size reduction is massive, while preserving the exact same structural types.

Here is a simplified look at the new structure for a PureScript snippet like this:

Given:

isEven :: forall a. Integral a => a -> Boolean
isEven x = ...

When:

isEven 42

Then:

{
  // Types are globally interned and deduplicated
  "typeTable": [
    { "type": "Int" }, // typeId: 0
    { "type": "Boolean" }, // typeId: 1
    { "type": "Func", "args": [0], "ret": 1 } // typeId: 2 (Int -> Boolean)
  ],
  "decls": [
    {
      ...,
      "expression": {
        "type": "App",
        "annotation": { "type": 1 }, 
        "abstraction": {
          "type": "TypeApp",
          "annotation": { "type": 2 }, 
          "expression": { "type": "Var", "value": { "identifier": "isEven" } },
          "typeArgument": 0
        },
        "argument": { "type": "Literal", "value": 42 }
      }
    }
    // 0, 1 and 2 can be reused...
  ]
}

But compression isn’t the only benefit. Freeing up this “budget” allowed us to enrich the AST with a game-changing addition: the TypeApp node (as seen above). This lets us know exactly which types are invoked for a polymorphic function, which is incredibly useful for monomorphization. Without compression, this would have resulted in catastrophic file size inflation. Before that, I was forced to reinvent the wheel and use a custom unify in the backends…

Across a full project build (300+ modules), we maintain a compact 1.8x size ratio compared to the completely untyped upstream PureScript compiler (13MB vs 23MB).

Cheers

1 Like