FS FormScript

Tools and runtimes

You have reached the end of the language. This section is about the things that run it.

The command line

bash
npm run formscript -- lint examples/demo.forms
npm run formscript -- run examples/demo.forms name=Ada age=36
npm run formscript -- ast examples/demo.forms
npm run formscript -- conformance
npm run formscript -- errors
npm run formscript -- level
npm run formscript -- spec-lint
Command What it does
lint <file…> read a document and print every diagnostic with its place
ast <file> print the parsed document as JSON
run <file> [p=v …] evaluate a form and print its output document
conformance [spec.md] run the conformance suite of §18
spec-lint [spec.md] check the specification document itself
errors list the error catalogue
level the conformance level, with its evidence

Two options matter:

  • --all — report every diagnostic, not only the earliest stage's errors;
  • --json — print the output document as indented JSON.

lint exits non-zero when a document has errors, so it drops straight into a CI pipeline.

The playground

The playground runs the reference implementation in your browser: the same lexer, parser, analyser and engine the conformance suite runs, compiled to JavaScript and loaded as a native module.

It is not a mock-up. The diagnostics carry their codes, the stage order decides which of them you are shown, and the output document is the one the specification describes.

The packages

Every part of the pipeline is a package you can use on its own:

Package What it does
@formscript/lexer text to tokens
@formscript/parser tokens to an AST
@formscript/ast the AST, and how to walk it
@formscript/analyzer the AST to a semantic model, with diagnostics
@formscript/scope names, scopes and the declarations in them
@formscript/types the type system
@formscript/decimal exact decimals
@formscript/evaluator expressions, and the builtins
@formscript/engine the reactive model, and the output document
@formscript/validation constraints, and satisfiability
@formscript/render the render intents
@formscript/render-dom intents to a DOM
@formscript/render-html intents to HTML
@formscript/runtime the browser runtime
@formscript/a11y the accessibility contract
@formscript/i18n locales and formatting
@formscript/cli the command line
@formscript/conformance the conformance suite
@formscript/errors the error catalogue
@formscript/property the property tests

A linter needs three of them:

ts
import { tokenize } from '@formscript/lexer';
import { parse } from '@formscript/parser';
import { analyze } from '@formscript/analyzer';

const lexed = tokenize(source, { file: 'form.forms' });
if (lexed.diagnostics.length > 0) {
  for (const diagnostic of lexed.diagnostics) {
    console.error(`${diagnostic.code} ${diagnostic.message}`);
  }
  process.exit(1);
}

const parsed = parse(lexed.tokens, { file: 'form.forms' });
if (parsed.document === null) process.exit(1);

const result = analyze(parsed.document, { file: 'form.forms' });
for (const diagnostic of result.reported) {
  console.error(`${diagnostic.code} ${diagnostic.message}`);
}

reported is the list to show a person: it is the errors of the earliest stage that produced any, plus every warning.

The full API of every package is in the packages reference.

A second runtime

FormScript is written down so precisely that a second implementation can be checked against it rather than admired. There is one: a Kotlin implementation of the same specification, in a separate repository, written without looking at the TypeScript one.

Both are held to the same three things:

  • the conformance suite of §18 — rows read out of the specification itself;
  • the corpus — real questionnaires published by health organisations, converted and run through both runtimes;
  • an oracle — a handful of forms whose expected output is written out by hand from the specification, with the statement that gives each answer cited.

The oracle is the one that finds things. It is not the first runtime's answers, so it can disagree with them — and it did: it found a repeat that grew an element without re-reading the guards of the entities it held, a dependency graph that ordered a group's composition before the group's own guard, and a dotted path that two halves of the runtime read two different ways.

Those were defects in the reference implementation, found by asking the specification what the answer was. Where the specification was silent, it was amended, in public, with the amendment written down.

The corpus

The corpus is the other half of the check. It is real forms — published by health organisations — converted into FormScript and run through the engine.

the corpus page reports how many forms there are, how many converted cleanly, what could not be carried over and why, and what the engine produced for the rest. A second corpus does the same for XForms.

Where to go from here