FS FormScript
Package@formscript/engine
Specification4.26, 4.27, 4.28, 4.35, 5.2, 5.5, 5.8, 6.1, 6.3, 7.11, 8.3, 8.5, 8.6, 8.7, 8.8, 8.9, 8.10, 9, 9.1, 9.2, 9.3, 9.4, 9.5, 11
Depends onast, decimal, errors, evaluator, scope, types, validation

@formscript/engine

The reactive evaluation engine of the FormScript Language Specification 1.0.0, §9.

The engine owns the runtime state of a form and evaluates it. A transaction is the only way the state changes, and the eight steps of Spec-9.6 are executed literally and in order:

  1. fix $now for the whole transaction (Spec-9.8),
  2. apply the inputs — content, touched flag, override source, relation holder — and abort with E-REL-02 when two members of one relation are assigned (Spec-8.12),
  3. take the transitive closure of the changed nodes over the dependency graph,
  4. topologically sort the dirty set, breaking ties by document order (Spec-9.9),
  5. evaluate the dirty nodes in one pass (Spec-9.7); an error value that reaches a non-field node aborts with E-EVAL-07,
  6. judge the validity of every field (Spec-10.13),
  7. project the output document through the output block (Spec-4.35),
  8. commit, then apply the effects (Spec-8.8).

Specification sections implemented

Section What this package implements
§9.1 The determinism contract: one snapshot sequence per (document, profile, environment, action sequence)
§9.2 The dependency graph, its node kinds and the cycle check (Spec-9.3, Spec-9.4)
§9.3 Transactions, dirty closure, topological order, single evaluation pass
§9.4 Derived fields, overridable fields (Spec-9.10, Spec-9.11)
§9.5 Structural propagation of a repeat (Spec-9.12, Spec-9.13)
§5.2, §5.5, §5.8 The three axes of a field and the read rule
§6.1 The error value
§4.35 The output projection
§7.11 Repeat element identity and reindexing
§8.3, §8.8, §8.9, §8.10 Applicability, effects, overridable fields, snapshots
§8.5, §8.6, §8.7 state declarations: the members of a state, the root scope they live in
§6.3 $action_name.status and $source_name.status
§4.26, §4.27, §4.28 The choice set of a field, declared or taken from a source

Validation itself lives in @formscript/validation; this package calls it and owns the transaction around it.

Public API

ts
class Engine {
  constructor(document: Document, options?: EngineOptions);

  readonly model: FormModel;
  readonly diagnostics: Diagnostic[];      // static diagnostics, including cycle detection

  get validity(): FormValidationResult;
  get output(): Record<string, unknown> | null;

  valueOf(path: readonly string[]): unknown;
  stateOf(path: readonly string[]): FieldRuntime | null;
  choiceSetOf(path: readonly string[]): ChoiceSet | null;

  applyInput(assignments: readonly Assignment[]): TransactionResult;
  mutateRepeat(path: readonly string[], mutation: RepeatMutation): TransactionResult;
  submit(): TransactionResult;

  exportSnapshot(): FormSnapshot;
  restore(snapshot: FormSnapshot): TransactionResult;
}

Also exported: buildFormModel, buildDependencyGraph, RuntimeState, pathOfExpression and serializeValue.

The graph

ts
function buildDependencyGraph(model: FormModel, options: GraphOptions): DependencyGraph;

interface DependencyGraph {
  readonly nodes: readonly GraphNode[];
  readonly nodeById: ReadonlyMap<string, GraphNode>;
  readonly reads: ReadonlyMap<string, readonly string[]>;
  readonly dependents: ReadonlyMap<string, readonly string[]>;
  findCycle(): readonly string[] | null;
  dirtyFrom(changed: Iterable<string>): ReadonlySet<string>;
  topologicalOrder(dirty: ReadonlySet<string>): readonly string[];
}

Node ids are stable and readable: value:<path>, applicability:<path>, derived:<path>:<property>, collection:<path>, relation:<name>:<path>, state:<name> and form:valid.

The host supplies what the engine cannot fetch

The engine is synchronous and performs no I/O (Spec-9.1). What only the host knows is passed through EngineOptions:

ts
interface EngineOptions {
  readonly actionStatus?: (name: string) => ActionStatus | undefined;
  readonly sourceStatus?: (name: string) => SourceStatus | undefined;
  readonly sourceValue?: (name: string) => unknown;
}

$action_name.status reads idle | pending | success | error (Spec-6.3, Spec-11.2) and $source_name.status reads idle | pending | ready | error (Spec-6.3, Spec-11.7); a declared action or source with no provider reads idle. sourceValue is the value a source produced, from which the choice set of a field that declares optionsFrom is materialised (Spec-4.26).

Judgement calls

  1. $form.valid observes the previous transaction's judgement. Step 5 (evaluation) precedes step 6 (validity) in Spec-9.6, so an expression evaluated in step 5 cannot see the validity computed in the same step. The engine therefore resolves $form.valid to the validity committed at the end of the last transaction, and the nodes that read it are refreshed in every transaction (they depend on the synthetic form:valid node, which is always dirty). The value is deterministic, which is what Spec-9.1 requires.
  2. The applicability of an entity depends on the applicability of its ancestors. Spec-9.3 defines an edge only for expression reads, but Spec-8.2 makes an entity applicable only if every enclosing entity is applicable. The engine adds that structural edge so the topological order is correct.
  3. A relation member that is not held is produced by a relation node. The graph is rebuilt whenever the held member changes, because the producer of a member depends on which member is held (Spec-8.12, Spec-8.13).
  4. A value-based constraint on an empty field is not evaluated by the engine's validity pass — that rule belongs to @formscript/validation and is described in its README.
  5. An unparsable input becomes unparsed content with the raw text retained (Spec-5.6). The coercion rules of Spec-5.3 are implemented for string, boolean, number, integer, enum, date, datetime, array and object; a value that does not fit the declared type is unparsed rather than an error.
  6. A reference into a repeat from outside the repeat's context reads null. The specification only defines indexed access (household.members[0].name, §6.3). Inside a repeat, the engine resolves a member to the element currently being evaluated; outside, the reference has no element and reads as absent.
  7. Nested repeats share one element set per innermost repeat. The specification does not define per-parent element sets, so the engine keeps one element count per repeat and uses the innermost repeat's index for the cell of a field.
  8. A repeat's value is the array of its element objects, each element holding the values of its members (Spec-9.3, the collection node).
  9. The output document omits an inapplicable field and emits an explicit null for an applicable but empty one (Spec-4.35). A field whose content is unparsed or failed blocks the output document entirely.
  10. Numbers are serialised in canonical decimal form as a string (Spec-4.36). The engine returns a plain object rather than JSON text, so a decimal is emitted as its canonical string — the form Spec-4.36 prescribes for a serialiser that cannot emit canonical decimals. An integer is emitted the same way, because Spec-4.36 covers it too.
  11. An effect is applied after the commit (Spec-8.8). A reset effect returns an overridable field to computed and a relation to its held member; the recomputation happens in the next transaction, because step 5 permits only one evaluation pass (Spec-9.7).
  12. The $now value comes from EngineOptions.now. The engine never reads the wall clock itself, so a run is reproducible (Spec-9.1, Spec-9.8).
  13. A state is a declaration, not an entity. It contributes no data level and never appears in the output document (Spec-7.1). Its name is declared in the form root scope, so a state that shadows a field name is E-REF-02 and the field keeps its place (Spec-8.7). Referencing the state evaluates its members in the current transaction; a state node in the graph keeps the consumers of a state ordered after the fields the state reads.
  14. The choice set of a field is materialised on demand. It comes from the field's options attribute or from the value of the source it names (Spec-4.26, Spec-4.27); a source that has not resolved leaves the set empty and marks it pending rather than invalid (Spec-11.7). A value outside the enum of the field is E-TYPE-04 and aborts the transaction (Spec-4.28).

Not implemented

  • action blocks are not executed. Their when guards are ordinary expressions and are evaluated like any other, but the action's remote effects belong to §11, which is outside §9.
  • source fetching is not implemented; the host supplies a source's status and its value through EngineOptions.

Public API

60 exported symbols.

ACTION_STATUSES re-export
ActionStatus type

The status of an action (Spec-6.3, Spec-11.2).

ancestorsOf re-export
applicabilityNodeId function
AppliedEffect interface
Assignment interface
attributeExpression re-export
attributeString function

The literal string of an attribute, when it has one.

buildDependencyGraph function

Builds the dependency graph of §9.2; an edge `X → Y` exists iff expression `Y` reads node `X` (Spec-9.3).

buildFormModel re-export
ChoiceOption interface

An option of a materialised choice set (Spec-4.26).

ChoiceSet interface

The choice set of a field, materialised from its `options` or from its source (Spec-4.26, Spec-4.27).

cloneRuntime function
collectionNodeId function
dataEntities re-export
DependencyGraph interface
derivedNodeId function
EffectModel interface
enclosingRepeat re-export
Engine class

The reactive evaluation engine of §9; the eight steps of Spec-9.6 run in order.

EngineOptions interface
entitiesOfKind re-export
EntityKind type
entityOfNode function

The entity a node belongs to.

FieldModel interface
FieldRuntime interface

The three stored axes of a field, plus its value and override source.

FORM_VALID_NODE_ID const

The node that holds `$form.valid` (Spec-10.17).

FormModel interface
FormSnapshot interface
GraphNode interface
GraphOptions interface
initialRuntime re-export
isActionStatus re-export
isSourceStatus re-export
literalBoolean re-export
NodeKind type
OptionModel interface

A declared option of a field's choice set (Spec-4.25).

OutputEntryModel interface
OverrideSource type
pathOfExpression function

The path a `path_ref` expression names, or `null`.

producerNodeIds function

The nodes that hold a field's value.

readPaths function

The declaration paths an expression reads.

readsFormValid function

Whether an expression reads `$form.valid`.

referencePath function

The path of a reference expression, or `null` when it is not a plain path.

RelationEquationModel interface
RelationModel interface
relationNodeId function
RepeatMutation type
resolveLongest function

The longest prefix of a path that names a declaration (Spec-6.20).

RuntimeState re-export
serializeValue function

The canonical decimal form of a number (Spec-4.36, Spec-5.33).

SnapshotField interface
SOURCE_STATUSES re-export
SourceStatus type

The status of a source (Spec-6.3, Spec-11.7).

StateMemberModel interface

A member of a `state` block (Spec-8.5).

StateModel interface

A `state` declaration; it contributes no data level (Spec-7.1, Spec-8.5).

stateNodeId function

The node that holds a `state`'s members (Spec-8.5).

stateReads function

The states an expression reads (Spec-8.7).

TransactionResult interface
valueNodeId function