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
@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
@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.
@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.
@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):
- trim
text-String.trim/1's result doubles as this analogue's "space-normalized string literal", the fallback of step 5; - compile the trimmed text with
Predicator.compile_with_spans/1; - on success, evaluate the compiled result at compile time, against
an empty
Predicator.Context.new(%{}, on_unbound: :error); - on
{:ok, value}, return{:static, value}; - 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.
@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.