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.
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
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
# 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:
{ "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:
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
- The language — the whole language, section by section.
- Examples — complete documents.
- The packages — the full API of every package.