Skip to content

Add codegen, bind, and splice — AST-to-JavaScript generation#2

Merged
dannote merged 2 commits into
masterfrom
codegen
Apr 21, 2026
Merged

Add codegen, bind, and splice — AST-to-JavaScript generation#2
dannote merged 2 commits into
masterfrom
codegen

Conversation

@dannote
Copy link
Copy Markdown
Member

@dannote dannote commented Apr 20, 2026

Adds the ability to generate JavaScript source from AST maps, closing the parse↔codegen roundtrip. Three new functions:

codegen/1 — AST → JavaScript

Rust NIF that reads BEAM terms directly (no serde intermediary), reconstructs OXC's arena-allocated AST via AstBuilder, and runs oxc_codegen for proper operator precedence, formatting, and semicolons.

ast = %{type: :program, body: [
  %{type: :function_declaration, id: %{type: :identifier, name: "add"},
    params: [%{type: :identifier, name: "a"}, %{type: :identifier, name: "b"}],
    body: %{type: :block_statement, body: [
      %{type: :return_statement, argument: %{type: :binary_expression,
        operator: "+", left: %{type: :identifier, name: "a"},
        right: %{type: :identifier, name: "b"}}}
    ]}}
]}

OXC.codegen!(ast)
# "function add(a, b) {\n\treturn a + b;\n}\n"

Roundtrips with parse:

{:ok, ast} = OXC.parse("const x = 1 + 2", "test.js")
OXC.codegen!(ast)
# "const x = 1 + 2;\n"

bind/2 — substitute $placeholders

Replaces $name identifiers in a parsed AST. Supports renaming, literals (including recursive maps/lists → JS objects/arrays), parsed expressions, and raw AST nodes.

OXC.parse!("const $name = $init", "t.js")
|> OXC.bind(
  name: "config",
  init: {:literal, %{port: 3000, host: "localhost", tags: ["web", "api"]}}
)
|> OXC.codegen!()
Binding value Result
"count" Rename identifier
{:literal, 42} Literal (number, string, bool, nil)
{:literal, %{a: 1}} JS object expression (recursive)
{:literal, [1, 2]} JS array expression (recursive)
{:expr, "ref(0)"} Parsed JS expression
%{type: ...} Raw AST node

splice/3 — expand $placeholder into multiple nodes

Replaces a single $placeholder statement, object property, or array element with a list of nodes. Strings are auto-parsed as JS.

template = ~s|import { z } from "zod";\nexport const $schema = z.object({$fields});\n$actions\n|

OXC.parse!(template, "t.ts")
|> OXC.bind(schema: "userSchema")
|> OXC.splice(:fields, ["id: z.string().uuid()", "name: z.string()"])
|> OXC.splice(:actions, [
  ~s|export function listUsers() { return fetch("/api/users"); }|,
  ~s|export function createUser(d) { return fetch("/api/users", { method: "POST" }); }|
])
|> OXC.codegen!()

Works in three contexts:

  • Statements: $actions in a block/body → list of statements
  • Properties: {$fields} in an object → list of properties
  • Elements: [$items] in an array → list of elements

Passing [] removes the placeholder entirely.

Implementation

  • native/oxc_ex_nif/src/codegen.rs — 1069-line Rust NIF that converts BEAM terms → OXC AST using AstBuilderoxc_codegen. Reads terms directly via Rustler's Term::map_get with pre-declared atoms, no serialization overhead.
  • bind/2 and splice/3 are pure Elixir using the existing postwalk/2.
  • 37 new tests covering codegen roundtrips, bind variants, splice contexts.
  • README updated with codegen, bind, splice, and lint documentation.

dannote added 2 commits April 20, 2026 20:48
- codegen: AST map → JavaScript source via OXC's Codegen
- bind: substitute $placeholders in parsed AST
- Rust NIF reads BEAM terms directly (no serde roundtrip)
- Supports all ESTree node types: statements, expressions,
  declarations, patterns, modules, classes, template literals
…EADME

bind/2 now supports:
- {:expr, "ref(0)"} — parse JS expression
- {:literal, %{...}} — recursive map → JS object
- {:literal, [...]} — recursive list → JS array

splice/3 replaces $placeholder statements/properties/elements:
- Statement-level: $actions → list of statements
- Property-level: {$fields} → list of object properties
- Element-level: [$items] → list of array elements
- Accepts strings (auto-parsed) or raw AST nodes

README updated with codegen, bind, splice, and lint sections.
@dannote dannote merged commit 4ad490c into master Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant