Statifier.Compiler.Expressions (Statifier v2.0.0)

Copy Markdown View Source

The expression-compilation seam: compiles raw predicator source - a cond, an expr, or a <content> text body - into the single Machine.expr() sum type, on its own, before either of its two consumers (the transition pass's transitions, the executable-content pass's content/donedata) so neither lands a raw-string field and changes its type afterwards.

ADR-0014 item 1 commits to spans, not point positions: every compile entry point in this module calls a _with_spans/1 variant, never a _with_positions/1 one - compile/3 calls Predicator.compile_with_spans/1, and compile_program/3 calls Predicator.compile_program_with_spans/1. The program path's point-position stopgap is closed. Item 2 already settled the compiled shape - %Predicator.Compiled{} threads its own positions table alongside instructions, so there is no separate table for this module to carry or drop.

Summary

Types

Identifies the node a compiled expression belongs to, in the same ADR-0012 constraint-3 index space Statifier.Compiler.Error names an owner by: a transition's t_index, a content node's c_index, a final state's own index for its <donedata>, or a top-level <script>'s document-order position. Nothing calls compile/3 with a real index yet - the transition pass wires transitions, the executable-content pass wires content and donedata - so this phase's own tests exercise the type directly with placeholder indexes.

Functions

Compiles source into a Machine.expr().

Compiles a predicator statement program - a <script> body - into a Machine.program(), the sibling type Machine.program()'s typedoc describes: a program is never an arm of Machine.expr(), because Predicator.evaluate/3 rejects statement programs outright (deps/predicator/lib/predicator.ex:213-217), so this function is a parallel entry point rather than a third clause of compile/3.

Constant-folds a <data> element's child text into a {:static, value} Machine.expr(). Spec 5.3.2 is explicit that <data>'s children are "an in-line specification of the value of the data object", not an expression that reads the datamodel, and B.2.1 gives the ECMAScript datamodel a parse-then-fall-back-to-string ladder for exactly this reason: "if the content is a valid JSON... create the corresponding ECMAScript object... Otherwise the Processor MUST treat the content as a space-normalized string literal". This is the predicator analogue, one rung shorter than B.2.1's (predicator's own literal syntax stands in for JSON; there is no XML rung under ADR-0004)

Wraps a literal value with no expression to evaluate - the {:static, v} arm of Machine.expr() for a plain <content>text</content> body - there is no predicator source and nothing to compile.

Types

owner_ref()

@type owner_ref() ::
  {:transition, non_neg_integer()}
  | {:content, non_neg_integer()}
  | {:donedata, non_neg_integer()}
  | {:data, non_neg_integer()}
  | {:global_script, non_neg_integer()}
  | {:invoke, non_neg_integer(), non_neg_integer()}

Identifies the node a compiled expression belongs to, in the same ADR-0012 constraint-3 index space Statifier.Compiler.Error names an owner by: a transition's t_index, a content node's c_index, a final state's own index for its <donedata>, or a top-level <script>'s document-order position. Nothing calls compile/3 with a real index yet - the transition pass wires transitions, the executable-content pass wires content and donedata - so this phase's own tests exercise the type directly with placeholder indexes.

{:global_script, non_neg_integer()} (ADR-0026 Decision 7) is a top-level <script> compiled via compile_program/3 into Machine.global_scripts - it has no c_index, t_index, or state index to name it by, since it is not addressed through the block runner's contents tuple at all (Machine's own "why global_scripts is different" moduledoc section). Reusing {:content, c_index} for it would put a false index into the ADR-0012 index space, naming a block-runner slot that does not exist for this node; the integer here is instead the script's own position among document.scripts, in document order.

{:invoke, state_index, invoke_index} names one <invoke> element's own attributes and children (type/typeexpr, src/srcexpr, a <param>, a namelist entry, <content>) - state_index is the owning state, and invoke_index is that invocation's position in the state's own invoke list, mirroring {:donedata_param, state_index, param_index}'s shape (Statifier.Event.Cause.origin/0).

Functions

compile(source, owner, location)

@spec compile(
  source :: String.t(),
  owner :: owner_ref(),
  location :: Statifier.Parser.Location.t()
) ::
  {:ok, Statifier.Machine.expr()} | {:error, Statifier.Compiler.Error.t()}

Compiles source into a Machine.expr().

owner identifies the node source came from (for the error case only - a successful compile carries no owner, since nothing downstream needs one until evaluation fails). location is the document Location a failure is reported against: the attribute value span (attribute_locations[:cond] / [:expr]) when the author wrote the attribute, the owning node's own location otherwise - the caller's choice, not this function's.

On success, compile_with_spans/1's %Predicator.Compiled{} is stored whole - its positions table is a span table (ADR-0014 item 1), never reduced to point positions and never re-supplied via a :positions keyword (item 2: passing both to evaluate/3 raises).

On failure, compile_with_spans/1 returns {:error, %Predicator.Errors.ParseError{}} directly (verified against the installed predicator 8.0.0 dependency: deps/predicator/lib/predicator.ex:902-915's build_compiled_result/1 private clause, and deps/predicator/lib/predicator/errors/parse_error.ex). The struct carries the parser's bare :message with no location text appended, {line, column} in :position, and the failing token's extent in :span - present in every compile mode, not only _with_spans, since the span comes from the token stream rather than from the spans: option.

compile_program(source, owner, location)

@spec compile_program(
  source :: String.t(),
  owner :: owner_ref(),
  location :: Statifier.Parser.Location.t()
) :: {:ok, Statifier.Machine.program()} | {:error, Statifier.Compiler.Error.t()}

Compiles a predicator statement program - a <script> body - into a Machine.program(), the sibling type Machine.program()'s typedoc describes: a program is never an arm of Machine.expr(), because Predicator.evaluate/3 rejects statement programs outright (deps/predicator/lib/predicator.ex:213-217), so this function is a parallel entry point rather than a third clause of compile/3.

owner and location carry the same meaning compile/3's docs give them - the owning node for the error case, and the Location a failure is reported against.

On success, Predicator.compile_program_with_spans/1's %Predicator.Compiled{} is stored whole, exactly as compile/3 stores compile_with_spans/1's result. compiled.positions maps each instruction's 0-based index to the source span of the AST node that emitted it; the instruction that terminates a statement - store for an assignment, pop for a bare expression statement - carries that statement's own source extent (upstream's own words, deps/predicator/lib/predicator.ex:856-895's doc for compile_program_with_spans/1).

On failure, the error is the same %Predicator.Errors.ParseError{} shape compile/3 returns, handed straight through with no re-parse. A program parse failure still carries a :span: it comes from the token stream that compile_program_with_spans/1 shares with every other compile entry point (deps/predicator/lib/predicator.ex:902-915's build_compiled_result/1 private clause), not from the spans: option - so a span is present in every compile mode, because it comes from the token stream.

compile_program/3 used to compile with a point-position variant instead, a stopgap ADR-0014 item 1 sanctioned for exactly this case: "If cond wiring must begin before 4.0 ships, it threads the identical seam with compile_with_positions/1 / :positions as a stopgap, storing the table in the same field" - "the seam is the commitment, the width follows the pin." Predicator ~> 9.0 closed that stopgap by shipping compile_program_with_spans/1, and this function widened onto it.

inline_value(text)

@spec inline_value(text :: String.t()) :: Statifier.Machine.expr()

Constant-folds a <data> element's child text into a {:static, value} Machine.expr(). Spec 5.3.2 is explicit that <data>'s children are "an in-line specification of the value of the data object", not an expression that reads the datamodel, and B.2.1 gives the ECMAScript datamodel a parse-then-fall-back-to-string ladder for exactly this reason: "if the content is a valid JSON... create the corresponding ECMAScript object... Otherwise the Processor MUST treat the content as a space-normalized string literal". This is the predicator analogue, one rung shorter than B.2.1's (predicator's own literal syntax stands in for JSON; there is no XML rung under ADR-0004):

  1. trim text - String.trim/1's result doubles as this analogue's "space-normalized string literal", the fallback of step 5;
  2. compile the trimmed text with Predicator.compile_with_spans/1;
  3. on success, evaluate the compiled result at compile time, against an empty Predicator.Context.new(%{}, on_unbound: :error);
  4. on {:ok, value}, return {:static, value};
  5. on any failure at either step, return {:static, trimmed_text}.

The empty, on_unbound: :error context in step 3 is what makes the fold safe: <data id="x">hello</data> compiles to a load of hello, which fails against a context that holds nothing and errors rather than defaulting to :undefined - so it falls through to the string literal "hello" instead of silently becoming a datamodel read. [1, 2, 3] compiles and evaluates to the list [1, 2, 3] directly, with nothing to fall back to.

Consequently this function never returns a {:compiled, ...} arm: child content can never raise error.execution at binding time, which is why 5.3.2's "empty data element" failure clause never has a child-content case to apply to under this datamodel.

Statifier.EventData.coerce/1 runs a near-identical parse-or-string ladder, and the two are deliberately not merged: this function implements B.2.1 (datamodel initialization from <data> inline content) at compile time, because a <data> body is static by construction, while EventData.coerce/1 implements B.2.8.1 (_event.data population) at event time, because a <param expr>'s value is only known then. Same shape, different clause, different time - see that module's moduledoc.

static(value)

@spec static(value :: term()) :: Statifier.Machine.expr()

Wraps a literal value with no expression to evaluate - the {:static, v} arm of Machine.expr() for a plain <content>text</content> body - there is no predicator source and nothing to compile.