Lavash.Rx.Transpiler (Lavash v0.4.0-rc.3)

Copy Markdown View Source

Translates Elixir expressions to JavaScript.

This module handles the core transpilation of Elixir AST to JavaScript code, enabling optimistic client-side evaluation of reactive expressions.

Supported Constructs

Literals

  • Strings, numbers, booleans, nil, atoms
  • Lists and maps

Operators

  • Comparison: ==, !=, >, <, >=, <=
  • Logical: &&, ||, and, or, not, !
  • Arithmetic: +, -, *, /
  • String: <> (concatenation)
  • List: ++ (concatenation), in (membership)
  • Pipe: |>

Control Flow

  • if cond, do: x, else: y → ternary operator

State References

  • @variablestate.variable
  • @nested.fieldstate.nested.field

String Functions

Enum Functions

Map Functions

  • Map.get/2,3

Utility Functions

Usage

iex> Lavash.Transpiler.to_js("@count + 1")
"(state.count + 1)"

iex> Lavash.Transpiler.to_js("if @active, do: \"on\", else: \"off\"")
"(state.active ? \"on\" : \"off\")"

Validation

Use validate/1 to check if an expression can be fully transpiled:

iex> Lavash.Transpiler.validate("@count + 1")
:ok

iex> Lavash.Transpiler.validate("Ash.read!(Product)")
{:error, "Ash.read!"}

Summary

Functions

Translates an Elixir AST to JavaScript.

Emit a JS object property access for an Elixir field name.

Emit a JS object literal key for an Elixir field name.

Translates an Elixir expression string to JavaScript.

Checks if an AST can be transpiled to JavaScript.

Transpiles a run function body to JavaScript statements.

Validates that an Elixir expression can be transpiled to JavaScript.

Validates an AST for transpilability.

Functions

ast_to_js(ast)

@spec ast_to_js(Macro.t()) :: String.t()

Translates an Elixir AST to JavaScript.

This is the lower-level function that works directly with AST. Use to_js/1 for string input.

js_field_access(target, field)

@spec js_field_access(String.t(), String.t() | atom()) :: String.t()

Emit a JS object property access for an Elixir field name.

Elixir allows ? and ! in atom names (idiomatic for boolean predicates and bang-mutating helpers). JS identifiers don't, so state.is_admin? is a syntax error. Field names that aren't valid JS identifiers get rewritten to bracket access with a string key: state["is_admin?"]. Safe names stay as dot access.

Used everywhere a state field becomes a JS property reference — the rx transpiler's @field handling, optimistic action set deltas, and the calc/derive emitter.

js_field_key(field)

@spec js_field_key(String.t() | atom()) :: String.t()

Emit a JS object literal key for an Elixir field name.

Same rule as js_field_access/2: safe names emit as bare keys ({is_admin: true}); names with ?/!/leading-digit/etc. wrap as quoted string keys ({"is_admin?": true}).

to_js(code)

@spec to_js(String.t()) :: String.t()

Translates an Elixir expression string to JavaScript.

Returns the JavaScript code as a string. Untranspilable expressions are converted to undefined with a comment indicating the issue.

Examples

iex> Lavash.Transpiler.to_js("@count")
"state.count"

iex> Lavash.Transpiler.to_js("length(@items)")
"(state.items.length)"

iex> Lavash.Transpiler.to_js("if @a, do: 1, else: 2")
"(state.a ? 1 : 2)"

transpilable?(ast)

@spec transpilable?(Macro.t()) :: boolean()

Checks if an AST can be transpiled to JavaScript.

Returns true if transpilable, false otherwise. Use validate/1 or validate_ast/1 to get the specific error.

transpile_run_body(ast)

@spec transpile_run_body(Macro.t()) :: {[String.t()], String.t()}

Transpiles a run function body to JavaScript statements.

This handles the run [:reads], fn assigns -> ... end pattern, where:

  • assigns.field accesses → state.field
  • x = expr bindings → const x = expr;
  • assign(assigns, :field, value) → adds to return object
  • Piped assigns with multiple assign/3 calls → combined return object

Returns a tuple of {statements, return_expr} where statements is a list of JS statements (const declarations) and return_expr is the return object.

Examples

# Simple case
iex> body = quote do
...>   discount = assigns.subtotal * assigns.discount_rate
...>   assigns |> assign(:discount_amount, discount) |> assign(:total, assigns.subtotal - discount)
...> end
iex> Lavash.Rx.Transpiler.transpile_run_body(body)
{["const discount = (state.subtotal * state.discount_rate);"],
 "{discount_amount: discount, total: (state.subtotal - discount)}"}

validate(source)

@spec validate(String.t()) :: :ok | {:error, String.t()}

Validates that an Elixir expression can be transpiled to JavaScript.

Returns :ok if the expression is fully transpilable, or {:error, description} if it contains unsupported constructs.

Examples

iex> Lavash.Rx.Transpiler.validate("length(@tags)")
:ok

iex> Lavash.Rx.Transpiler.validate("Ash.read!(Product)")
{:error, "Ash.read!"}

validate_ast(ast)

@spec validate_ast(Macro.t()) :: :ok | {:error, String.t()}

Validates an AST for transpilability.

Returns :ok if transpilable, {:error, reason} otherwise.