StatifierBlocks.Compiler.Chart (StatifierBlocks v0.29.0)

Copy Markdown View Source

The Chart stage: run the generated SCXML through statifier's own pipeline and route every finding back to a block (ADR-0004 decisions 9 and 10).

This package ships no reachability analysis, no transition-target check, no id-uniqueness check and no expression well-formedness check. Statifier's validator already runs those against a conformance corpus with a regression ratchet behind it (st-ADR-0006), and a second semantic validator written against block documents would be a second implementation of the same rules that must agree with the first - every disagreement surfacing as a document the editor accepts and the engine then rejects. So: generate the SCXML, run it through Statifier.compile/2, and map the findings back through provenance. Adding a chart-semantic check here is a change to ADR-0004 decision 9.

How the routing works

Upstream findings carry no element reference. Every diagnostic in the pipeline - Statifier.Parser.ParseError, Statifier.Lowering.Error, Statifier.Validator.Error, Statifier.Validator.Warning, Statifier.Compiler.Error - is {reason, message, location}, where location is a %Statifier.Parser.Location{} over the source bytes. So the route is location.start_offset through StatifierBlocks.Provenance.owner_at/2, and decision 5's totality is what makes it a total function rather than one with an :unmapped arm.

Upstream's message survives verbatim, and its reason tag becomes the finding's code. Upstream's document-order sort survives as document order over blocks, which StatifierBlocks.Compiler applies to every stage's findings alike.

The fault split (decision 9)

Two kinds of chart-stage finding, distinguished by exactly one thing - whether the owning span carries a config key:

  • Structural findings are bugs in this package or in a host's block type, never the author's doing. {:unresolved_target, id}, {:initial_not_descendant, id, parent}, a malformed namespace: an author cannot express any of these, because the block vocabulary has no way to name them. Their owning span has config_key: nil, and fault: :package is the actionable part - "this cannot be fixed here" is the only honest message.
  • Content findings are the author's, and they carry a config key. An :expression config field is a predicator source string passed verbatim into a cond; if it does not parse, upstream returns {:expression_compile_error, owner_ref, source, parse_error} located at the attribute value - a span this package recorded with the config key the value came from. That is squarely the author's typo, and the config key is what lets an editor put the error on the field they typed into rather than on the block as a whole.

An upstream error of either kind fails the compile; a warning does not, and rides on StatifierBlocks.Compiled.

The sub-expression span (decision 9's last refinement)

A content finding routes to a field. Decision 9 goes one step further: the predicator parse error carries a span within the expression string, so an editor can underline the offending sub-expression inside the field rather than the whole field. That span is composed here and lands on the finding as config_value_span.

The composition is three steps, and the middle one is upstream's:

  1. Upstream reports the failure against the attribute value's span in the generated SCXML (Statifier.Compiler.Error's location), and carries predicator's own {line, column} span over the expression string it compiled.
  2. Statifier.Parser.Location.resolve_span/4 composes the two into an absolute span over the generated document, walking the raw bytes and the entity-expanded string in lockstep so a > earlier in the expression cannot shift the answer.
  3. That absolute span is run backwards through the same correspondence the provenance map is built from: subtract the attribute value's own start, and unescape the prefix, which turns generated-document bytes back into bytes of the value the author typed.

Step 3's unescape is not decoration. A cond is emitted through StatifierBlocks.Compiler.Serializer's XML escaping, so the canonical amount > > 5000 reaches the document as amount > > 5000, where the offending second > sits at byte 12 rather than at byte 9 - one entity reference ahead of it is already enough to move it. The offsets this field carries are into the author's value, so they are the ones an editor can use without knowing that a serializer exists.

When a finding gets one

All four have to hold, and the field is nil otherwise:

  • the finding's owning span carries a config key - the same test the fault split above turns on, and the reason a canonicalised attribute (core.wait's delay, whose bytes the block type rebuilt rather than passed through) is never annotated and so never offered an in-value offset;
  • the diagnostic is an :expression_compile_error, the only upstream reason that carries an expression string at all;
  • predicator supplied a span for the failure (ParseError's :span is optional, and nil on an error built through new/3);
  • the resolved span lies inside the attribute value's own span - the guard that keeps a degraded resolution honest rather than turning it into an offset into somebody else's bytes.

resolve_span/4 degrades rather than raising: an expression whose expanded text does not describe the raw slice resolves to the value's whole span, which arrives here as "underline the entire field" - the same answer a consumer would have reached with no span at all, which is why it is passed through rather than discarded.

Why "carries a config key" is enough

The first bullet reads "carries a config key", where decision 9's annotation rule means "was written verbatim from that config value". Those coincide because the annotation is only ever left on a verbatim value: a block type that composes an attribute leaves it unannotated, so its bytes have no config key and a finding inside them is the package's. core.subchart was the one exception - it annotated a cond it builds from an outcome name with the config key outcomes - and that annotation was dropped rather than this criterion widened.

Summary

Functions

Compiles scxml upstream and maps what comes back.

Functions

validate(scxml, provenance, document)

Compiles scxml upstream and maps what comes back.

{:ok, warnings} when the chart is valid - upstream's warnings (st-ADR-0033 made Machine.warnings/1 their only surfacing seam), mapped the same way errors are. {:error, findings} otherwise.