FS FormScript

Getting started

FormScript has no runtime to install for most uses: it is a specification and a reference implementation. This site is the fastest way to see it working — the playground runs the real implementation in your browser.

Your first form

A form is a form block with a version. Inside it are fields.

forms
form hello version = "1.0" {
  field name {
    type = string
    label = "Your name"
    required = true
  }
  field greeting {
    type = string
    value = "Hello, " + name
  }
  output {
    name: string
    greeting: string
  }
}

Three things are worth noticing.

The output block names types, not values. name: string says "the output document has a key name whose type is string" — the value comes from the field of the same name. That is what §4.33 means by an output schema.

A derived field is just a value. value = "Hello, " + name recomputes whenever name changes. The engine's dependency graph is built from the expression, not declared by hand.

required is a constraint, not a type. It says the field must hold a value; it does not say what that value is.

Running it locally

bash
git clone https://github.com/bazokhan/formscript.git
cd formscript
npm install
npm run verify:all

verify:all runs four gates in sequence: the specification's own consistency lint, the TypeScript build, ESLint, and the test suite.

The command line

bash
# Diagnose a document
npm run formscript -- lint examples/demo.forms

# Evaluate one and print its output document
npm run formscript -- run examples/demo.forms name=Ada age=36

# The parsed document, as JSON
npm run formscript -- ast examples/demo.forms

# The conformance suite of §18
npm run formscript -- conformance

# The error catalogue of §14
npm run formscript -- errors

# The conformance level of §15.1, with its evidence
npm run formscript -- level

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

The output document looks like this:

json
{ "name": "Ada", "age": "36", "greeting": "Hello, Ada" }

The numbers are JSON strings, and that is deliberate: the engine returns a JavaScript value tree rather than JSON text, and a JSON number cannot carry a canonical decimal without passing through binary floating point. §4.36 says a serialiser that cannot do that must emit a JSON string — so it does.

Using the packages directly

Every package is usable on its own. A linter needs three of them:

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

const source = readFileSync('form.forms', 'utf8');

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}`);
}

The reported list is the one to show a person: §2.5 orders the diagnostics by stage — lexical, syntactic, semantic, type, constraint — and shows the errors of the earliest stage that produced any, plus every warning. A document with a typo in a name does not also report every downstream consequence.

Where to go next