Every normative statement of the FormScript Language Specification 1.0.0,
with the text that binds an implementation.
Chapter 1 — Purpose and scope
Spec-1.1
Within a chapter, statement numbers are assigned densely and in
order of appearance: the first statement of chapter N is Spec-N.1, the
second is Spec-N.2, and so on. A statement's number is therefore a stable
handle for citation, not a mnemonic for its content, and a number is never
reused after a statement is withdrawn. A statement's number does not change
when it moves within its chapter; moving a statement between chapters
renumbers it.
Spec-1.2
An implementation that fails any MUST is non-conforming. An
implementation that does nothing more than parse documents and report errors is
conformant at Level 0 (§15.1).
Chapter 2 — Design goals and the static-error commitment
Spec-2.1
A conforming implementation MUST implement all five stages.
A Stage-3 (parse + semantic analysis) implementation with no renderer is
Level 0 conformant.
Spec-2.2
Every error MUST carry a stable machine-readable code from the
catalogue (§14), a human-readable message, and — when the error originates in
source text — a location of line:column. Error code values are stable
within a major version; message text is not stable and MUST NOT be matched on.
Spec-2.3G3
An implementation MUST detect and report, before any
evaluation, all errors in the following list:
Syntax errors
Undefined identifier references
Duplicate identifier declarations in the same scope
Static type errors, where the type system is strong enough to prove them
Dependency cycles among derived values
Constraints that are statically contradictory (e.g. min = 10, max = 5)
Unknown properties or block names
Over-budget expressions (§6.6)
Unbounded iteration (§6.6)
Missing required properties (§14.3, E-PROP-07)
Output paths that do not resolve to a declared field (§14.3, E-OUT-01)
Comparison of a value with an absence literal using == or !=
(§5.5, E-TYPE-07)
Presentation-plane values used in data positions (Spec-12.9, E-TYPE-08)
Relations with no acyclic causality assignment, and malformed relations
(§8.8, E-REL-01, E-REL-03); overridable without value
(E-CONFLICT-04)
Fields that provably have no satisfiable value under their own type and
constraints (§10.6, analysis tier A1, E-SAT-01)
An unknown or unsupported profile (§5.6, E-PROF-01)
This list is normative and exhaustive for the "must be caught at compile time"
claim. A runtime that defers any of these to evaluation time is non-conforming.
Spec-2.4
References below the $ namespace ($env, $user, $form) are
resolved by the runtime and MUST NOT be checked statically. Their contents are
not known until the transaction runs, so a reference to one is never a
compile-time error (Spec-6.23).
Spec-2.5
A document MAY produce more than one error. An implementation
MUST report them in stage order — lexical, then syntactic, then semantic,
then static-type, then constraint — and MUST report every error of the
earliest stage that produced any. The stage of a diagnostic is the class of its
code in the catalogue of §14, and the stage order is the order of that
catalogue's sections: lexical (§14.1), syntactic (§14.2), semantic (§14.3),
static-type (§14.4), constraint (§14.5). Within one stage, errors MUST be
reported in document order (Spec-9.9). An error of a later stage MUST NOT be
reported while an earlier stage produced at least one error; it is
suppressed, not discarded, and MUST be reported once the earlier stage's
errors are resolved. A warning (§14.7) is reported whenever it is produced: it
neither suppresses an error nor is suppressed by one.
Spec-2.6
The diagnostic list for a document is complete for the earliest
non-empty stage: it contains every error of that stage and no error of any
later stage. No error of the earliest non-empty stage is ever suppressed. A
document with a lexical error and a semantic error reports the lexical error
and suppresses the semantic error; it does not report only the lexical error,
and it does not report the semantic error alongside it.
Spec-2.7
A conformance-suite row that names a single expected code
(§18.2) MUST produce that code as the first error it reports. A row MAY
produce further errors of the same stage after it — the §18.2 rows are
fragments, and the row that declares options and optionsFrom together also
names a source the fragment never declares, so that reference is reported too —
but the named code is the row's primary diagnostic (Spec-18.2).
Chapter 3 — Lexical structure
Spec-3.1
Source text MUST be UTF-8, with or without a byte-order mark.
Line terminators are LF (U+000A) and CRLF (U+000D U+000A). A lone CR is
an error.
Spec-3.2
A conforming implementation MUST NOT attach meaning to leading or
trailing whitespace, and MUST NOT use indentation to delimit blocks.
Spec-3.3
A /* appearing inside a line comment is not a block comment.
A # or // inside a block comment is not a line comment.
Spec-3.4
Identifiers are case-sensitive. Email and email are distinct
identifiers and MAY both exist.
Spec-3.5
Identifiers MUST NOT begin with two underscores. The prefix __
is reserved for implementation-internal use.
Spec-3.6
Identifiers MUST NOT contain a hyphen.
Spec-3.7
Case sensitivity is retained despite the cost, because form
definitions routinely mix conventions when merged, and a document that silently
coalesces email and Email is worse than one that errors.
Spec-3.8
A hard keyword MUST NOT be used as the name of a field, group,
repeat, section, choice, state, or page block, nor as an import
alias.
Spec-3.9
A soft keyword MAY be used as a declaration name or an attribute
name. A parser MUST resolve the soft keyword by context per §4.14, and MUST
prefer the identifier interpretation where both are possible.
Spec-3.10
Single-quoted strings are NOT supported. One string syntax
eliminates an entire class of quote-nesting bugs in the error-message and i18n
grammars.
Spec-3.11
A date or datetime literal with no timezone offset MUST be
interpreted as UTC. Local-time interpretation is a known source of off-by-one
bugs across platforms and is prohibited.
Spec-3.12
A duration literal MUST be interpreted in UTC-relative calendar
terms. 30d is 30 days; 6mo is 6 calendar months; 2y is 2 calendar years.
A duration MUST NOT be interpreted as a fixed number of seconds, because month
and year lengths vary.
Spec-3.13
Where a duration unit has a one-letter and a two-letter form
(m minutes, mo months), the lexer MUST use the longest match. 6mo is six
months; 6m is six minutes.
Spec-3.14
A lexer MUST reject a character that is not the start of a
terminal in this list, a keyword, an identifier, or a literal.
Spec-3.15
The multi-character tokens ??, ->, ==, !=, <=, >=,
//, /*, */ MUST be lexed as single tokens. The lexer MUST use
longest-match. The two-character tokens && and || are not tokens and MUST
be rejected (Spec-6.52).
Chapter 4 — Syntactic structure
Spec-4.1
A document MUST contain exactly one form definition. A document
MAY contain any number of include, import, and namespace declarations
before the form definition.
Spec-4.2
An include directive names a file to be merged into the current
document at the point of inclusion. include "path" as alias prefixes every
top-level name declared by the included file with alias.. include "path"
imports every name without a prefix.
Spec-4.3
An import directive names an extension namespace — one
declared in the same document or one registered with the runtime (Spec-16.8) — and
MUST be followed by as <alias>. The alias is a prefix under which the
namespace's declarations are visible.
Spec-4.4
A namespace block declares the contents of an extension
namespace. A namespace block MAY appear only at the top level of a document
(not nested inside a form block). Its contents are visible only to documents
that import it.
Spec-4.5
A relation declares mutually dependent input fields together
with one equation per member (§8.8). Inside a relation block the grammar is
the one above, not the general block grammar: the first statement MUST be a
hold clause, and every other statement is an equation whose left side is a
field path. A relation is a declaration, not an entity (Spec-7.1).
Spec-4.6
A separator is optional. A trailing separator before } is
permitted and is parsed as belonging to the preceding statement. Two
consecutive separators are a syntax error (E-SYN-03).
Spec-4.7
Attributes MAY appear in any order within a block, with exactly one
exception: type and options are typed attributes and are constrained to
their own value grammar (§4.4). Every other attribute's value is an
expression.
Spec-4.8
A page MUST be named with a string_literal, because a page's
name is a route identifier and is referenced from navigation and from state
declarations. Every other block kind MUST be named with an identifier. A
string literal used as the name of any other block kind is E-SYN-01.
Spec-4.9
field, group, repeat, section, choice, state, and
page are block-kind keywords. They are always recognised by position — a
block is always kind name { } — and so they do not conflict with a
declaration of the same name. Per Spec-3.9 they MAY name a declaration.
Spec-4.10
type and options MUST NOT be used as ordinary attribute
names. type always introduces a type_ref; options always introduces an
options_def. Both are soft keywords resolved by the attribute-name position
(§4.14).
Spec-4.11
The match subject is evaluated once per transaction, before any
arm is considered. Arms are tested in document order. The first arm whose
pattern matches is executed; its body MUST be a single statement or a block.
If no pattern matches, the default arm is executed if present, and otherwise
the match statement has no effect.
Spec-4.12
A bare literal pattern matches when the subject is equal to
that literal, using the equality rules of §6.2. A default arm matches when
no other arm does, and MUST be the last arm if present.
Spec-4.13
A bound is always a range. string(2..10) is a lower and an
upper limit; string(..10) is upper-limit only; number(0..) is lower-limit
only. A bound with neither limit (..) is unbounded and triggers the
cost rule of §6.6 when the type is used as an iterated collection.
Spec-4.14
An array type without an of clause is an array of any: its
element type is unconstrained. Because an unbounded array is an
unbounded-iteration risk (§6.6), an untyped array is only legal where it is
never iterated.
Spec-4.15
The upper limit of an array type bounds the number of
elements. The bound on an element type bounds the element. These are
independent:
text
array(0..200) of string(1..200)
^^^^^^^^^^^ ^^^^^^^^^^^^^
200 elements each 1..200 characters
Spec-4.16
An object type MAY declare its members. An object type
without members is opaque: expressions may read any property of it and receive
null for an absent one.
Spec-4.17
A duration literal MUST use either the short form
(30d, 6mo, 2y) or the ISO 8601 form (P30D, P1Y2M3D, PT2H30M).
The short form and the ISO form MAY be mixed within a document, but a single
literal MUST NOT mix them. In the ISO form the date components MUST appear in
the order YMWD and the time components in the order HMS,
separated by T; each part MUST carry at least one component, so P alone and
PT alone are not durations.
Spec-4.18
The escape sequences of a string literal are \"\\\/\n\t\r\b\f and \u{XXXX}, where XXXX is one to six hexadecimal
digits. A backslash followed by anything else is E-LEX-02 (invalid escape
sequence). A string literal is terminated by the first unescaped double quote;
there is no way to embed a raw double quote.
Spec-4.19
Precedence, from loosest to tightest: ?: → ?? → or →
and → equality → relational (including in, is between, and the absence
tests) → additive → multiplicative → unary → postfix. Every production in the
chain is left-associative except the ternary, which is right-associative.
Spec-4.20the `and` of `between`
between_expr is a production of its
own. A parser that has read additive "is" "between" MUST consume the
following additive "and" additive as part of the between form; a further
and is the ordinary conjunction. Two tokens of lookahead are sufficient: the
token after is distinguishes between from an absence test, and the token
after that distinguishes the two. a and b is between 1 and 2 therefore
parses as a and (b is between 1 and 2), and
x is between 1 and 2 and y parses as (x is between 1 and 2) and y.
Spec-4.21
in is a hard keyword. A reference to a field is written
identifier for a field in scope, or a.b.c for a path. The * selector
(a.*) denotes "every member of the object or collection a" and is valid
only as the right-hand side of a path.
Spec-4.22
FormScript has no lambdas. A function that takes a
collection and a function argument does not exist. Comprehensions are the only
higher-order construct. A map, filter, or reduce that appears in a
builtin list with a function argument is not a FormScript builtin (§6.4).
Spec-4.23
A comprehension is distinguished from a call to a builtin of the
same name by its first argument: if a comprehension head keyword is followed by
(identifierin, the construct is a comprehension, and identifier in is
a binding, not a membership test. Because the binding is not an
expression, the two forms are unambiguous with two tokens of lookahead:
text
count(x in items, x) → comprehension (binds x over items)
count(items) → builtin function
Spec-4.24
options constrains a field's value to one of the option
keys. The declared type of the field MUST be string or enum.
Spec-4.25
An option written key = "label" has a static label. An option
written key -> expression has a computed label. The two forms MAY be mixed
in one options_def.
Spec-4.26
optionsFrom = <source name> binds a field's choice set to a
source declared in the same document (§11.2). The source's declared type
MUST be array whose element type is object { value: string, label: string };
a source of any other declared type is E-TYPE-02.
Spec-4.27
A field that declares optionsFrom MUST declare type as
string or enum. A field MUST NOT declare both options and optionsFrom;
doing so is E-CONFLICT-06. A field that declares neither has an empty choice
set and accepts any value of its declared type.
Spec-4.28
When the field's declared type is enum, every value the
source yields MUST be one of the enum's declared option keys; a value that is
not is E-TYPE-04 at runtime. A source that yields no values leaves the field
with an empty choice set; it is not an error.
Spec-4.29
A warn prefix declares the constraint's severity as
warning rather than error (§10.5). A constraint with no warn prefix has
severity error.
Spec-4.30
path_ref is used only in validation constraints and refers to a
field path. It is not an expression form; in expression position the same
syntax is built from selector (§4.6).
Spec-4.31
An action or source block is a named declaration, not an
entity (§7). Its body is an ordinary block whose attributes are the properties
listed in §8.7.
Spec-4.32
A source block MAY declare a type attribute describing the
shape of the fetched value. A source without a declared type yields
untyped data, and any expression reading a member of it has type any and
cannot be statically verified against a field's declared type. A source whose
result is iterated MUST declare a bounded type (§6.6).
Spec-4.33
The output block declares a projection of the form's data:
which fields are submitted, and under what type. Every output_path MUST
resolve to a declared field. A field not listed in output is not submitted.
Spec-4.34
The declared type of an output entry MUST match the declared
type of the field it names (§5.2). An output entry whose path names a field
inside a section or page MUST use the field's own path, because
section and page do not introduce a data level (Spec-7.14).
Spec-4.35absence on the wire
The output document distinguishes the two
absences. An applicable field whose content is empty is emitted as an
explicit JSON null ("asked, not answered"). An inapplicable field is
omitted: its key is absent ("not asked"). A consumer can therefore tell the
two apart without any side channel. A field whose content is unparsed or
failed blocks submission (§10.4) and never reaches an output document.
Spec-4.36numbers on the wire
A number or an integer value MUST be
serialised as a JSON string carrying the canonical decimal form (Spec-5.32),
and MUST NOT pass through binary floating point. The string MUST be the
canonical decimal form and nothing else. There is no second wire form: a JSON
number cannot carry a decimal128 value without loss, so a runtime that could
emit one would emit a different output document from a runtime that could not,
and Spec-15.6 would have nothing to say.
Spec-4.37
A text entry's value is either a single string (the default
locale's text) or a map of locale tags to strings. In the map form, the
reserved key "default" denotes the default-locale text. A key MUST NOT
appear twice within one text block; a duplicate is E-SYN-08.
Spec-4.38
A text block MAY appear at the top level of a form block or
inside any entity block. Text blocks at different levels are merged into one
catalogue; a duplicate key across levels is E-SYN-08.
Spec-4.39
Where associativity matters, the production order in §4.6 defines
it.
Spec-4.40
Trailing commas are permitted in options_def, primary's
array form, and source's params block.
Spec-4.41
value and default are mutually exclusive on the same field
(§7.1). Declaring both is E-CONFLICT-01.
Spec-4.42
A field with neither value nor default is an input field
(§7.1). A field with value is a derived field.
Spec-4.43
Where a soft keyword could begin either an attribute or a block,
the parser MUST look at the token after the keyword to choose. A = means
attribute; { or an identifier or a string literal means block. A conforming
parser MUST NOT require the author to disambiguate by other means.
Spec-4.44
Inside a validate block, the constraint keywords required,
accepted, min, max, minLength, maxLength, pattern, email, phone,
url, unique, notIn, in, multipleOf, equal, notEqual, cross,
custom, and the severity keyword warn are recognised as keywords. when is
recognised as a guard only immediately after a complete constraint.
Chapter 5 — Types, coercion and the profile
Spec-5.1
integer is a subtype of number, and string is a subtype of
display (Spec-12.9). No other type is a subtype of any other type, and display
is not a subtype of string.
Spec-5.2
Every field's type is static and MUST be known at compile time.
A runtime MUST reject a document that computes a type dynamically.
Spec-5.3
The distinguished error value (§6.1) has no static type. It is
assignable to any type, propagates through every operator (§6.2), and gives any
field that receives it the content failed (§5.2).
Spec-5.4
empty means: null, or a string of length 0, or an empty array,
or an empty choice. A number equal to 0 and a boolean equal to false are
NOT empty.
Spec-5.5
required is violated if and only if the field is applicable and
its content is empty. A field holding 0 or false satisfies required; a
form that treats a legitimate 0 as a missing answer is a defect. A boolean
toggle that has never been assigned has content empty and reads null
(Spec-5.8); a toggle that must read false when unticked MUST declare
default = false.
Spec-5.6
unparsed content is retained, not discarded. The runtime MUST
keep the raw input so the user does not lose typing on a validation failure.
An unparsed field reads as null (unknown) to other expressions (Spec-5.8)
and is itself a type finding (E-EVAL-05), so a typing error in one field never
poisons its dependants with the error value.
Spec-5.7Touched
Touched becomes true on the first assignment by a
person or by an action's input mapping. Defaults, value expressions, and
relation equations never set it. It stays true until the form is reset, and
it is retained with the field's content in the shadow store (§8.2). Touched
exists to answer one question — may a violation be shown yet? (Spec-10.4) —
and affects no data-plane result.
Spec-5.8Read rule
What an expression sees when it reads a field is a
function of two axes:
Applicability
Content
An expression reads
inapplicable
any
inapplicable
applicable
empty, scalar-typed, input field
null (unknown)
applicable
empty, produced by a derived field's value expression
the absence the expression produced (null or inapplicable)
applicable
empty, array/repeat-typed
[]
applicable
value
the value
applicable
unparsed
null (unknown)
applicable
failed
the error value
Spec-5.9
Validity is derived, not stored (§5.2.1). Two consequences
follow. First, "required but empty" and "untouched" are different facts, and a
renderer can ask each independently. Second, a derived value is never "invalid"
in the sense of a stored flag; it is failed (a content fact) and judged
invalid (a derived fact).
Spec-5.10
A constraint binds when its guard — absent guard meaning the
literal true — evaluates to exactly true (Spec-8.2). A constraint whose
guard evaluates to false does not bind and is not evaluated. A constraint
whose guard is unknown or inapplicable is deferred: it does not bind, it is
not evaluated, and it is not reported as either passing or failing.
Spec-5.11
The derivation is:
text
validity(F) =
not_applicable if F is inapplicable (§8.2)
invalid if any binding constraint of F fails
pending if F is applicable, no binding constraint fails, and at
least one of F's constraints is deferred
valid otherwise
Spec-5.12
A deferred constraint MUST NOT be reported as a violation and
MUST NOT be reported as satisfied. A runtime MUST report deferred constraints
in the transaction's diagnostic output under the code E-VAL-05.
Spec-5.13
A pending field does not block submission (Spec-10.13). A
deferred constraint becomes binding when its guard becomes exactly true, at
which point the field's validity is recomputed in the same transaction
(Spec-9.6). A runtime MUST NOT treat pending as valid in any data-plane
result: $form.valid is true only if no field is invalid (§10.4).
Spec-5.14
Touched does not affect validity. It affects only whether a
violation may be shown (Spec-10.4). Validity is never stored and is never
pending for a field that is not applicable.
Spec-5.15derived content
A derived field's content is set from the
result of its value expression:
the error value → content failed (§5.6);
null → content empty, and the field reads as null;
inapplicable → content empty, and the field reads as inapplicable;
any other value → content value.
The distinction between the two absences is preserved by the read rule of
Spec-5.8, which distinguishes a derived field's empty content from an
input field's. This is what lets a derived boolean say "not yet known"
instead of guessing false.
Spec-5.16
Coercion is permitted in exactly four cases and forbidden
everywhere else:
From
To
Rule
integer
number
Widening; always exact
string
number
Only if the string fully matches -?\d+(\.\d+)?
number
integer
Only if exactly representable and finite
string
date/datetime
Only if it matches the declared format exactly
Spec-5.17
All other conversions are compile-time errors. E-TYPE-01 is
the code when an operator's operands or a builtin's argument types are not
acceptable; E-TYPE-02 is the code when a field's value or default
expression does not match the field's declared type; and E-TYPE-03 is the
code when a builtin's argument has no permitted conversion to the parameter's
type. In particular there is no "5" + 1 == "51"; concatenating a number to a
string requires explicit toString().
Spec-5.18
A coercion that fails produces the error value, not an exception.
A runtime MUST NOT throw on user input.
Spec-5.19
Implementations MUST implement at minimum: type of every literal,
type of every field, arithmetic operand types, comparison operand
compatibility, and the arity and argument types of every built-in function.
Spec-5.20
Implementations MAY implement full bidirectional inference over
comprehensions. Implementations that do not MUST defer those errors to
evaluation time, and MUST report them as runtime errors, not silently.
Spec-5.21
A value read from a source without a declared type has type
any. A value expression of type any assigned to a field of a declared
type MUST be checked at runtime (E-TYPE-02).
Spec-5.22
null and inapplicable are distinct first-class values.
isUnknown(inapplicable) is false. Neither equals any value under ==,
including itself (Spec-6.8).
Spec-5.23propagation and dominance
A strict operator or builtin
(Spec-6.8) applied to an absent operand yields an absent result. When operands
of both kinds are present, inapplicable dominates null. The reason is
monotonicity: learning an unknown can change a result, but nothing the user
types can make an inapplicable question apply, so the more final fact wins.
The error value dominates both (Spec-6.9).
Spec-5.24testing for absence
Absence is tested only with the predicates
isUnknown, isInapplicable, isAbsent, and isKnown (§6.4). Because == is
strict, x == null is itself null whenever x is, so it can never answer the
question it appears to ask. A comparison of any operand with an absence
literal by == or != is therefore a compile-time error (E-TYPE-07) whose
message suggests the predicate. The literals remain legal as values
(if (c) null else x).
Spec-5.25
Every type admits both absences and the error value. The type
system does not track absence statically (there is no T?); the analysis of
§10.6 reasons about it.
Spec-5.26
A document MAY declare profile = "<id>" in its form header
(§4.1). A document without one is read under the default profile of this
specification, core-2026.1. A runtime MUST support every profile it claims,
MUST publish the profiles it supports, and MUST reject a document naming any
other with E-PROF-01.
Spec-5.27G1/G2 restated
For a fixed document, profile, sequence of user
actions, environment values (Spec-5.41), and transaction timestamps, every
conforming runtime produces identical data-plane results: field content,
touched flags, applicability, validity and its codes, relation and override
state, and output documents. Text shown to a person is the presentation plane
(Spec-12.9) and is outside this guarantee.
Spec-5.28
Under core-2026.1, integer is a signed 64-bit integer. Every
integer operation is exact; a result outside the range yields the error value
(E-EVAL-06). number is IEEE 754-2008 decimal128: 34 significant decimal
digits, and an adjusted exponent — the exponent of the leading significant
digit — in the range −6143…+6144 inclusive. Addition, subtraction,
multiplication, and division are correctly rounded under round-half-even to 34
digits; a result whose adjusted exponent falls outside that range yields the
error value (E-EVAL-06). Thus 0.1 + 0.2 == 0.3 is true, and binary
floating point appears nowhere in the data plane.
Spec-5.29subnormals are not produced
The range of Spec-5.28 is the
normal range of decimal128. IEEE 754-2008 also permits subnormal results
whose adjusted exponent reaches down to −6176; this profile does not produce
them. A result that would require a subnormal is an underflow and yields the
error value (E-EVAL-06), exactly as a result beyond the exponent range does.
An implementation MUST NOT silently round such a result to zero.
Spec-5.30
Arithmetic with a number operand yields number. / always
yields number (7 / 2 is 3.5; use floor(a / b) for integer division).
% is the truncated remainder, a − b × trunc(a / b), computed exactly.
== and the ordering operators compare numeric value, so 2.0 == 2.
A numeric literal denotes an exact decimal value, and its kind fixes its type
and its domain. An int_literal (§4.5) has type integer and MUST denote a
value in that type's domain — a signed 64-bit integer (Spec-5.28). A
dec_literal has type number and MUST denote a value exactly representable
in that type's domain (Spec-5.28). A literal outside the domain of its own
kind is a lexical error (E-LEX-06); a literal whose kind's domain admits it
is never a lexical error, whatever its value.
Spec-5.31
round(n [, places [, mode]]): places defaults to 0; mode is
"half_up" (the default; ties away from zero) or "half_even".
round(2.5) is 3, round(-2.5) is -3, round(2.5, 0, "half_even") is
2, and round(1.005, 2) is 1.01. ceil and floor are exact.
avg(arr) is sum(arr) / count(arr) in number arithmetic; avg([]) is the
error value (division by zero).
Spec-5.32canonical decimal form
The canonical decimal form of a
number or an integer is the single text by which that number is written,
and is defined as follows. Let d be the number's significant digits with
trailing zeros removed, and let a be its adjusted exponent — the exponent of
its leading digit.
Zero is written 0. The sign of zero is not distinguished.
If −6 ≤ a ≤ 20 the form is positional: an optional leading -, then
the digits of d with the zeros that d dropped restored on the right, a
. placed so that the units digit falls last, and no trailing zeros in the
fractional part.
Otherwise the form is scientific: an optional leading -, then the first
digit of d, then a . and the remaining digits of d when d has more
than one digit, then E, then + or -, then the digits of a with no
leading zeros.
The form carries no + sign on a non-negative number, no leading zero before
the point, and no trailing point. Examples: 0, 1, -1, 0.1, 123.45,
1E+21, 1.5E+3, 1E-7.
The zeros that d dropped are restored by rule 2 and absent from rule 3, so a
value has exactly one text: 100 is written 100 and not 1E+2, and 5.0 is
written 5. An integer is always written without a fractional part, because
its normalised exponent is never negative.
Spec-5.33where the canonical form is used
A number or an integer
MUST be written in canonical decimal form wherever it is serialised: in an
output document (Spec-4.36), in a state snapshot (Spec-8.26), and as the
result of the toString builtin (§6.4). The toString builtin applied to a
number or an integer yields the canonical decimal form and nothing else.
Two conforming runtimes MUST produce byte-identical text for the same number.
Spec-5.34
A string is a sequence of Unicode scalar values (never a lone
surrogate). Every string that enters the data plane — literals, user input,
source results, $ values — is normalised to NFC on entry. Equality is
scalar-by-scalar equality of NFC forms, so "é" (U+00E9) equals
"e\u{0301}".
Spec-5.35
Length is the number of Unicode scalar values of the NFC form.
len, minLength, maxLength, substring, indexOf, and the bounds of
string(a..b) all count scalar values. graphemeLen(s) counts extended
grapheme clusters (UAX #29) for authors who want user-perceived length. The
default is the scalar count because grapheme segmentation depends on larger,
more frequently revised tables.
Spec-5.36
Ordering of strings (<, sort, minOf, maxOf) is by scalar
value of the NFC form. Locale-aware collation is a presentation concern and is
not available to data-plane expressions.
Spec-5.37
lower and upper use the Unicode default full case mappings
with no locale tailoring (the Turkish dotless i rules are not applied).
trim removes characters with the Unicode White_Space property at both ends.
slugify(s) is: NFKD, remove combining marks, lower, replace each maximal run
of characters outside [a-z0-9] with a single -, strip leading and trailing
-.
Spec-5.38
The calendar is proleptic Gregorian. Every datetime is an
instant normalised to UTC; a date is a calendar date. In
dateAdd(d, n, unit), unit is one of "s", "m", "h", "d", "w",
"mo", "y" (the duration-literal suffixes of §3.6); months and years are
added first and the day is clamped to the last valid day, so
dateAdd(2026-01-31, 1, "mo") is 2026-02-28. dateDiff(a, b, unit) counts
whole units from a to b, truncated toward zero. A 29 February anniversary
is reached on 1 March in non-leap years — a convention, chosen and stated so
that two runtimes cannot differ.
Spec-5.39
core-2026.1 pins Unicode 16.0 (normalisation, case mapping,
grapheme and regular-expression property data) for the data plane, and CLDR
46 for the presentation plane (Spec-12.9). A runtime whose platform libraries
implement other versions MUST ship the pinned tables or declare that it cannot
support the profile.
Spec-5.40
A new profile MAY be published in a MINOR release (§16.1). A
profile is immutable once published; behaviour that must change ships as a new
profile identifier.
Spec-5.41
$env, $user, and other $ values are typed inputs of the
determinism contract, as is the transaction timestamp $now. They are the
only way a data-plane result can depend on anything outside the document and
the user's actions. A conformance harness supplies them explicitly.
Spec-5.42
A profile pins the static-analysis thresholds as well as the
primitives. MAX_EXPR_COST and MAX_DOC_COST (§6.6) are properties of the
profile, not of the runtime. A conforming runtime MUST apply the profile's
values and MUST NOT raise them: the set of documents a runtime accepts is part
of its observable behaviour, and two runtimes that accept different documents
are not interchangeable even when they agree on every document they both
accept. A runtime that wishes to accept documents beyond a profile's
thresholds MUST publish a new profile (Spec-5.40).
Spec-5.43
The profile also pins the diagnostic order of §2.5: the
order in which a document's errors are reported is part of the profile, so
that two runtimes produce the same diagnostic list for the same document, in
the same order, with the same codes.
Spec-5.44profile identifiers are immutable
A published profile
identifier MUST NOT be reused for different content. Before a profile's first
publication its identifier and its content may be re-cut freely; after
publication, any change to its content — including its cost thresholds and its
diagnostic order — MUST ship under a new identifier (Spec-5.40).
Spec-5.45the profile of this specification
The conformance suite of
§18 is bound to the profile core-2026.1 as published with this
specification: MAX_EXPR_COST = 100,000, MAX_DOC_COST = 1,000,000, Unicode
16.0, CLDR 46, decimal128 with round-half-even, and the diagnostic order of
§2.5. A row of §18 whose expected result depends on the profile states so.
Chapter 6 — Expressions
Spec-6.1G4
Expressions are total and pure.
Total — every expression terminates and yields a value. There are no loops,
no recursion, and no unbounded iteration.
Pure — expressions have no side effects. Evaluating an expression cannot
modify form state, perform I/O, or read a clock other than now (§6.4).
Spec-6.2
An operation that cannot produce a meaningful result — division
by zero, a regular expression that fails to compile at runtime, an out-of-range
conversion — yields the error value rather than raising. The error value
propagates through every operator.
Spec-6.3
When the error value reaches a field's value, that field's content
becomes failed (§5.2) and the runtime MUST report E-EVAL-03 (or the
matching E-EVAL-* code) against that field. When the error value reaches the
top level of a transaction without materialising into a field — an
applicability (when) expression, for example — the transaction MUST report
the error and MUST NOT commit (E-EVAL-07).
Spec-6.4
The ONLY permitted non-determinism is now (§6.6), which reads
the evaluation timestamp fixed for the current transaction (§9.3). now MUST
be stable within a transaction.
Spec-6.5
== on array and object is structural and order-sensitive for
arrays: it is the conjunction (Spec-6.12) of the length test and the
element-wise ==. A comparison containing an absent element is absent unless
some pair is definitely unequal.
Spec-6.6
+ on a string and a number is a compile-time error, not a
runtime error. This is the most common bug in loosely-typed form languages and
FormScript refuses to permit it.
Spec-6.7G1
and and or are symmetric: a and b and b and a always
produce the same value (Spec-6.12), so evaluation order is unobservable. An
implementation MAY evaluate either operand first, and MAY skip an operand that
cannot change the result; it MUST NOT let that choice change any result. The
static cost counts both operands (§6.6). x != 0 and 10 / x > 1 is safe at
x = 0 because F absorbs the error from the right operand.
Spec-6.8absence propagation
An operator or builtin is strict unless
this specification says otherwise. For a strict operation:
If any operand is E, the result is E (but see absorption in Spec-6.12
and Spec-6.10).
Otherwise, if any operand is absent, the result is absent: I if any
operand is I, else N (Spec-5.23).
Otherwise the operation is evaluated normally.
Arithmetic, comparison, ==/!=, in, and every builtin not listed in
Spec-6.28 are strict. and, or, !, ? :, ??, and the absence-aware
builtins are not.
Spec-6.9
The error value takes precedence over absence in every strict
operation (E + N is E). In and/or it is absorbed by F/T
respectively and otherwise dominates absence (Spec-6.12).
Spec-6.10G1
c ? a : b — and its identical statement form
if (c) a else b — is: a if c is T; b if c is F; the absence c
itself if c is N or I (the result is not a branch); E if c is
E. Only the selected branch is evaluated, and MUST NOT be evaluated otherwise,
even when the other branch would produce E.
Spec-6.11`??`
a ?? b is a, unless a is absent (N or I), an
empty string, or an empty array, in which case it is b; if a is E, the
result is E. ?? is the one operator that turns absence into a value
silently, which is its purpose; use it deliberately. 0 ?? "d" is 0 and
false ?? "d" is false.
Spec-6.12three-valued logic
and, or, and ! are Kleene
strong three-valued logic, extended so that the two absences are the
"undetermined" value and E is absorbed by the dominant value. Stated as
rules:
a and b is F if either operand is F; otherwise E if either is E;
otherwise I if either is I; otherwise N if either is N; otherwise T.
a or b is T if either operand is T; otherwise E if either is E;
otherwise I if either is I; otherwise N if either is N; otherwise F.
!a is F for T, T for F, and a itself for N, I, and E.
As tables (row is the left operand, column the right):
and
T
F
N
I
E
T
T
F
N
I
E
F
F
F
F
F
F
N
N
F
N
I
E
I
I
F
I
I
E
E
E
F
E
E
E
or
T
F
N
I
E
T
T
T
T
T
T
F
T
F
N
I
E
N
T
N
N
I
E
I
T
I
I
I
E
E
T
E
E
E
E
Both operators are commutative and associative; T is the identity of and
and F of or; De Morgan's laws hold. The law of the excluded middle holds
for known operands only: x or !x is N when x is N. That is the
correct law for an unknown, and the reason an absent condition can no longer
satisfy both a rule and its negation.
Spec-6.13`is between`
x is between a and b is exactly
x >= a and x <= b under Spec-6.12. With x unknown the result is N; with
x = 10, a = 20, b = null it is F, because one comparison is definitely
false.
Spec-6.14`in`
x in [e1, …, en] is (x == e1) or … or (x == en)
under Spec-6.12, and F for an empty list. If the right-hand side is itself
absent, the result is that absence.
Spec-6.15the absence tests
These are the operator forms of the
predicates of Spec-6.28 and have exactly the same semantics:
x is known is true iff x is a value — neither absent nor the error
value. It is false for null and for inapplicable, and it is the error
value for the error value.
x is unknown is true iff x is null; it is false for inapplicable
and for a value.
x is absent is true iff x is null or inapplicable; it is false
for a value.
x is inapplicable is true iff x is inapplicable; it is false for
null and for a value.
x is empty is true iff x is absent, or is a string of length 0, or is
an empty array — the semantics of isEmpty (Spec-6.28).
Each propagates the error value, exactly like the builtin it mirrors.
Spec-6.16three different questions
"Known", "absent" and "empty" are
three different questions, and the language answers all three:
Question
Operator
null
inapplicable
""
[]
0
false
Do I have a value?
x is known
no
no
yes
yes
yes
yes
Is my value missing?
x is absent
yes
yes
no
no
no
no
Is my value nothing?
x is empty
yes
yes
yes
yes
no
no
0 and false are values, never empty (Spec-5.4). An empty string is
known and empty but not absent: the question was asked and answered with
nothing.
Spec-6.17`??` is empty-aware; the absence tests are not
?? treats an
empty string and an empty array as grounds for falling through to its right
operand (Spec-6.11). is known does not: an empty string is a known value.
The two are answering different questions, and an author who wants
"fall through if empty" writes ??; an author who wants "has this been
answered at all" writes is known.
Spec-6.18masked errors
When the error value is absorbed by a dominant
operand in and or or — F and E is F, T or E is T — the value of
the expression is unchanged, and the runtime MUST NOT materialise the absorbed
error into a field as failed content, because it was not the result of the
expression. The runtime MUST, however, record the absorption and report it as
warning W-EVAL-01 against the location of the expression.
Spec-6.19
Absorption affects diagnostics only. It MUST NOT affect the
value of any expression, and therefore MUST NOT affect the determinism
contract (Spec-9.1) or the cross-platform conformance test (Spec-15.6).
Spec-6.20
Reference resolution walks the lexical scope chain: current block,
then enclosing blocks, then form root, then the environment ($env).
Spec-6.21
An unqualified reference that resolves to nothing is a
compile-time error E-REF-01. FormScript does not support dynamic name
lookup; it is exactly the kind of magic that G3 forbids.
Spec-6.22
$now and all $-prefixed builtins are read-only.
Spec-6.23
A $-path that does not resolve yields null (§6.3). A runtime
MUST report every unresolved $-path it evaluated as the warning W-REF-01,
naming the path, so that a typo is visible rather than silent. The warning MUST
NOT change the data-plane value, which stays absent. The runtime MUST provide
$env.locale; MAY provide $env.api, $env.user.*, and other
environment-specific values.
Spec-6.24
self denotes the subject value of the context in which
the expression appears:
Context
self is
inside a validate block
the current value of the field being validated
inside a field's value expression
the field's own current value
inside a field's default expression
the field's own current value (usually null at first evaluation)
inside a repeat element's expression
the value of the enclosing repeat element
anywhere else
the error value
Spec-6.25
self is a hard keyword and MUST NOT be used as a declaration
name.
Spec-6.26
Within a repeat, self refers to the current element of that
repeat. Inside a comprehension over that repeat, the bound identifier (or
$this) refers to the current element of the comprehension's own iteration.
Spec-6.27
Builtins MUST be side-effect free and total. A builtin that
would otherwise fail (e.g. substring with a negative index) MUST return the
error value or null as documented for that builtin, and MUST NOT raise.
Spec-6.28builtins and absence
Builtins are strict (Spec-6.8) except
the following, which are absence-aware: isEmpty, isUnknown,
isInapplicable, isAbsent, isKnown, typeOf, coalesce, ifNull, iif,
orDefault, filledFields, isActive, shadow, isOverridden, count
(one-argument form), and the presentation builtins, which format an absent
argument as the empty string. isEmpty(x) is true for an absent value, "",
and []. isUnknown(x) is true only for null; isInapplicable(x) only for
inapplicable; isAbsent(x) for either; isKnown(x) is true for a value
that is neither absent nor the error value. The predicates and isEmptypropagate the error value (isKnown(E) is E, not false), so a failure
is never mistaken for a missing answer.
Spec-6.29
Implementations MUST support every builtin listed here to reach
Level 2 conformance (§15.1). Additional builtins MAY be added; a document
using a non-standard builtin MUST be rejected by a conforming implementation
unless it declares an extension (§16.2).
Spec-6.30no lambdas
No builtin takes a function argument. map,
filter, reduce, count, sum, any, and all are available in
comprehension form only, except for count(arr) and sum(arr), which
also have a one-argument builtin form (Spec-4.23).
Spec-6.31deprecated builtins
ifNull(a, b) and iif(c, a, b) are
deprecated: ifNull is exactly a ?? b (Spec-6.11) and iif is exactly
c ? a : b (Spec-6.10). A runtime MUST implement them, MUST NOT remove them
within a major version, and MUST emit warning W-BUILTIN-01 when a document
uses them. A runtime MAY implement coalesce as the variadic form of ?? and
orDefault as the ?? that also catches the error value; these two are
recommended over ifNull and iif and MUST be used in the specification's
own examples.
Spec-6.32
pmt(rate, periods, principal) returns the level payment for a
fully amortising loan. With r = rate, n = periods, and P = principal:
text
r == 0 → P / n
r != 0 → P * r / (1 - (1 + r)^(-n))
Spec-6.33
The power in pmt is defined for integer exponents only, by
left-to-right binary square-and-multiply in number arithmetic with each
product correctly rounded (Spec-5.28); x^(-n) is 1 / x^n. Fixing the sequence of
roundings is what lets two runtimes agree on every digit of a payment.
Spec-6.34
age(d) returns the number of whole calendar years between the
date d and the current transaction date, in UTC. A d in the future yields a
negative value. A 29 February birth date attains its
anniversary on 1 March in non-leap years (Spec-5.38).
Spec-6.35
now() returns the transaction timestamp and is identical to
$now. today() returns the UTC date of the transaction timestamp.
Spec-6.36
t(key, args…) resolves a text key against the current locale
(§12.2) and formats it with the supplied arguments. It is the only function
that produces user-facing text from a catalogue key. It returns a value of type
display (Spec-12.9) and is, with formatDate and formatMoney, one of the three
presentation builtins.
Spec-6.37
filledFields() returns an array of the paths of all active
fields whose content is not empty (§5.2), in document order.
Spec-6.38
isActive(path) returns whether the field at path is currently
active (§8.2). shadow(path) returns the retained value of a field that is
currently inactive, or null if it has none (§8.2).
Spec-6.39
The matches(s, regex) builtin and the pattern constraint use
the same regular-expression engine, which MUST have linear-time matching
semantics (no backtracking, no backreferences, no lookaround). A pattern
that the engine cannot compile is a compile-time error (E-BOUND-04).
Spec-6.40regular-expression dialect
The shared engine implements the
syntax of RE2 (linear-time, no backreferences, no lookaround) restricted to:
literals, ., character classes, the Perl classes \d \w \s (ASCII only),
\p{…} for Unicode General_Category and Script properties of the pinned
Unicode version, anchors ^ $, alternation, groups, and the quantifiers
* + ? {m,n} with their lazy forms. Matching runs over the NFC scalar values
of the subject (Spec-5.34). matches(s, re) is true if re matches anywhere
in s (anchor it to require a full match), so "abc" matches "^a" is true.
Spec-6.41
$this inside a comprehension refers to the bound element. The
bound identifier is scoped to the comprehension body only. reduce takes
three arguments: the bound identifier, the iterated collection, the initial
value, and the body.
Spec-6.42
A collection-typed field (array, repeat) that is applicable
and empty reads as the empty collection [] (Spec-5.8), so a comprehension
over it yields the identity of its aggregate: sum is 0, count is 0,
any is false, all is true, and map and filter are []. This is
honest — an empty list really has zero elements — and is what keeps conditional
logic free of null guards.
Spec-6.43
A comprehension over an inapplicable collection yields
inapplicable, and over an unknown collection (for example a source that
has not resolved) yields null. An excluded list is not an empty list, and an
unloaded list is not an empty list; neither silently becomes [].
Spec-6.44comprehension bodies
Each element's body is evaluated under
§6.2. map collects the bodies, absent ones included. filter and
count(x in xs, p) keep or count an element only when p is exactly true.
any and all fold the bodies with three-valued or and and. sum and
reduce are strict: one absent body makes the result absent (Spec-6.8), so a
total is unknown while any row it covers is unanswered. Write
ifNull(item.balance, 0) to say "count an unanswered row as zero" explicitly.
Spec-6.45G8
Every expression has a statically computable cost, in
abstract cost units:
Construct
Cost
Literal
0
Field reference
1
Binary operator
1 + cost(left) + cost(right)
Function call
1 + Σ cost(args)
Array literal
length
Comprehension
cost(body) × maxIterations
Every other node
1 + Σ cost(children)
Spec-6.46
maxIterations for a comprehension is the declared maximum size
of the iterated collection:
Iterated expression
maxIterations
a repeat
its max attribute (§8.6)
a field of type array(bound)
the bound's upper limit (§4.4)
a source
the upper limit of its declared type (§4.9)
a field of type object
the bound of the member's type
anything else
unbounded
Spec-6.47
A comprehension whose iterated collection is unbounded MUST be
rejected at compile time with E-COST-01. A repeat used as an iterated
collection MUST therefore declare max (§8.6), and an iterated array or
source MUST declare a bounded type (§4.4, §4.9).
Spec-6.48
An expression whose static cost exceeds MAX_EXPR_COST MUST be
rejected at compile time with E-COST-01. MAX_EXPR_COST is pinned by the
profile (Spec-5.42); the default value of core-2026.1 is 100,000.
Spec-6.49
(document budget). The sum of the static costs of all distinct
expressions in a document MUST NOT exceed MAX_DOC_COST. MAX_DOC_COST is
pinned by the profile (Spec-5.42); the default value of core-2026.1 is
1,000,000. A document exceeding it MUST be rejected at compile time with
E-COST-01.
Spec-6.50the multiplication cliff
Because the cost of a comprehension
is cost(body) × maxIterations, the per-expression ceiling imposes an
implicit ceiling on the size of a repeat: a repeat with max = 1000 and a
body of cost 12 costs 12,000, while the same repeat with max = 2000 costs
24,000. Authors of
large field-collection forms MUST be able to express a thousand-row repeat. A
profile MUST set MAX_EXPR_COST high enough that the multiplication never
becomes an accidental limit on the domain, and a spec revision that lowers it
below the level at which a documented form shape becomes illegal is a MAJOR
change.
Spec-6.51runtime budget
A runtime MUST enforce a runtime cost budget
per evaluation transaction, defaulting to max(100,000, document_total_cost)
units, halting with E-COST-02 if exceeded. The static limit exists to catch
mistakes at authoring time; the runtime limit exists to bound adversarial
documents that a checker was bypassed on.
Spec-6.52G4
A conforming implementation MUST reject the symbolic forms
&& and || with E-LEX-03. They are not part of the language; accepting
them as aliases would make two spellings of every condition, and the second
spelling is the one that drifts out of style guides.
Spec-6.53
The static cost of a relation (§8.8) is the maximum, over its
members, of the cost of the equations retained when that member is held,
because only one holder is active in any transaction. Applicability (when)
expressions are ordinary expressions and are costed as such.
Chapter 7 — Forms, groups, repeats and choices
Spec-7.1
state and relation are not entities. A state is a named
state declaration (§8.3) and a relation a declaration tying existing fields
together (§8.8); neither contributes data of its own. text, output,
action, source, validate, and namespace are likewise not entities; they
are block forms (§4.2) that may appear within a document.
Spec-7.2
The block-kind keywords field, group, repeat, section,
choice, state, and page name these seven block forms. Per Spec-3.9, they
may also be used as declaration names of other entities.
Spec-7.3
A field MAY declare value (computed) or default (initial,
user-editable), never both. A derived field MAY additionally declare
overridable = true (§8.9).
Spec-7.4
A field with value = expr is a derived field. Its value is
recomputed whenever any referenced value changes. User input to a derived field
MUST be rejected by the runtime (§9.4) unless the field is overridable
(§8.9).
Spec-7.5
A field with neither value nor default is an input field.
Its state is owned by the user.
Spec-7.6
A field's type is required. A field without a type is
E-PROP-07 (missing required property).
Spec-7.7
A group MAY be when-gated. A hidden group's fields are
inapplicable: they read as inapplicable, contribute nothing to output, and
none of their constraints run (§8.2). This is the correct behaviour: a form must not require a home
address from a user who has no fixed home.
Spec-7.8
min and max bounds are normative. A runtime MUST enforce
max and MUST NOT permit a user to add beyond it.
Spec-7.9
A repeat MUST declare max. A repeat without max is
E-PROP-07. This is required because the repeat's max is the iteration bound
used by the cost model (§6.6).
Spec-7.10
Every repeat MUST have a stable identity strategy. The
default is index-based with splice-aware reindexing. Implementations MAY offer
key = <path> for UUID-based identity, but MUST document the difference, since
index identity and key identity differ observably when rows are reordered or
deleted from the middle.
Spec-7.11
Deleting element i reindexes all subsequent elements in the
same transaction. Computed values and validation state for affected elements
MUST be recomputed within that transaction, not deferred to the next user
action. Getting this wrong is a visible bug: delete row 1 of 3 and row 2's
computed total silently becomes row 1's.
Spec-7.12
A choice renders as a radio group by default and as a select
element when the enum has more than 6 members — a renderer MAY deviate, and MUST
document the threshold.
Spec-7.13
A choice is semantically a field whose type is enum. The
two forms exist because a choice is the one field type whose rendering is
negotiated, and a separate entity lets a renderer and a validator treat it
without a special case on type. A choice MUST declare type, and that
type MUST be an enum type; the enum's members are the choice's options and
their labels. A choice MUST NOT declare options or optionsFrom: the
option set of a choice is the enum's member list, which is closed and written
down in the document. A form that needs a remote option set declares a field
with optionsFrom (§4.7), and one that must render as a radio group adds
render = "radio".
Spec-7.14
section and page are presentation-only. They MUST NOT appear
in output data and MUST NOT introduce a data level. Neither is a path prefix:
a field inside section address is addressed by its own name, relative to the
nearest data-bearing ancestor (Spec-6.20).
Spec-7.15
A page MAY declare when, making it skippable. A runtime MUST
skip an unsatisfied page without blocking navigation past it, and MUST NOT count
its fields toward the form's required-field set.
Chapter 8 — The property table
Spec-8.1G5
FormScript has exactly one visibility gate: when.
Dynamic requiredness is required = <expr>, dynamic readonlyness is
readonly = <expr>. All three are ordinary boolean-valued attributes evaluated
by the same engine, with the same dependency tracking, in the same order.
Spec-8.2definite activation
An entity is applicable if and only if
its own when evaluates to exactly true and every enclosing entity is
applicable. In every other case — when is false, unknown, or
inapplicable — it is inapplicable. (A when that yields the error value
is not a quiet case: the transaction fails, Spec-6.3.) This is the
definite-activation rule: a thing switches on only when it is definitely on.
The same rule governs dynamic required and readonly (they bind only when
exactly true) and constraint guards (Spec-10.9).
Spec-8.3
An inapplicable entity:
is not rendered
has no constraints evaluated; its validity is not_applicable (§10.4)
is omitted from output (Spec-4.35)
reads as inapplicable in every expression (Spec-5.8)
retains its content and Touched flag in a shadow store, for restoration if it
becomes applicable again
Spec-8.4
The shadow store is reachable through the shadow(path) builtin
(Spec-6.38) and through no other means; shadow returns the retained value if
the retained content was a value, and null otherwise. The shadow-store rule
is deliberate, and the read rule matters as much as the retention rule:
preserving a hidden field's value means a user who toggles a condition back and
forth does not lose their input, which matches how users understand "hide"
(not "delete"). But a hidden field that still drives derived values makes those
values depend on data the user can no longer see or change, which is worse
than losing the input. Hence an inapplicable field reads as inapplicable, and
its retained value is reachable only through an explicit shadow() call.
Spec-8.5
state declares a named snapshot of runtime state. It is the
supported way to implement save-and-resume without a runtime-specific API. The
body of a state block contains attributes whose values are expressions; a
runtime MUST expose the resulting map under the state's name (e.g. draft).
Spec-8.6
A state block's members MAY reference the form's fields and the
builtins of §6.4. A state block does not declare a data model and MUST NOT
appear in output.
Spec-8.7scope
A state name is a declaration in the form root scope
and is visible to every expression in the form, exactly as a field name is
(Spec-6.20). Referencing a state name evaluates its members in the current
transaction and yields an object. A state name MUST NOT shadow a field
name; a collision is E-REF-02.
Spec-8.8
An effect attribute MAY appear only on a form block or as the
sole statement of a match arm. An effect MUST take effect after the
transaction commits.
Spec-8.9
A renderer that ignores layout hints is still conformant. A
renderer that honours them MUST honour span consistently with columns.
Spec-8.10
min and max on a repeat are normative and are enforced by
the runtime. max is required (Spec-7.9) because it is the iteration bound
used by the cost model.
Spec-8.11
A relation names its members — input fields — and gives one
equation per member, saying how to compute that member from the others and from
any outside fields. Every member MUST be an input field (no value, not
overridable), a field MUST belong to at most one relation, every member MUST
have exactly one equation, each equation's type MUST match its member's type,
and hold MUST name a member. Any violation is E-REL-03.
Spec-8.12held member
Each relation has a runtime state, its held
member, initially the hold member. The held member is treated as an input:
its content is whatever the person (or its default) put there. Every other
member's content is computed from its equation. A transaction that assigns a
member by input first sets the held member to it. Assigning two or more members
of one relation in a single transaction is rejected with E-REL-02 and the
transaction does not commit.
Spec-8.13causality, checked statically
For every member h of a
relation, consider the dependency graph in which h's equation is removed
(h is an input), the equations of the other members are present, and every
other relation is fully cut (all of its equations removed, its members
treated as inputs). That graph MUST be acyclic. In addition, the quotient graph
obtained by contracting each relation to a single node MUST be acyclic. A
violation is E-REL-01, reported with the member and the cycle. The check is
polynomial — one graph per member — and is what makes the otherwise-cyclic
declaration safe: in any one transaction exactly one holder is active, and the
graph for that holder is a DAG.
Spec-8.14
Computed members are never Touched (Spec-5.7) and are always
editable. Editing a computed member makes it the held member (Spec-8.12), which
is how a person takes over from the computation.
Spec-8.15
Constraints apply to every member whether held or computed. A
computed gross above its type bound is a violation like any other.
Spec-8.16
The held member is part of the state snapshot (Spec-9.1).
Spec-8.17
The language does not require the equations to be inverses of one
another; if they are not, values will differ depending on the holder.
Spec-8.18inconsistency is reported
A runtime claiming Level 1 or
above MUST report W-REL-01 when it can prove that two equations of one
relation are not mutually consistent. "Can prove" means one of:
the tier-A2 narrowing of §10.6 shows that, for some value in a member's
declared domain, the two equations disagree; or
both equations are affine in the two members — each of the form
c₀ + c₁·a + c₂·b over the profile's decimal arithmetic — and substituting
one into the other does not yield the identity on the members' declared
domains.
A runtime that cannot decide MUST NOT report W-REL-01. The obligation is
absolute for a runtime that implements tier A2 and conditional only on tier A2
being implemented at all, which a runtime declares under Spec-15.1.
Spec-8.19the warning is not an error
W-REL-01 does not prevent
compilation and does not block submission. A relation whose equations are not
inverses is legal: the form is then deterministic — the same input sequence
always yields the same result — but not arithmetically coherent, and the
warning is how the author learns that.
Spec-8.20
overridable = true on a field that declares value = expr
gives it a source: computed or manual. The source is initially computed.
An input assignment sets it to manual, stores the input as the content, and
sets Touched. While manual, the result of expr is ignored. The effect
reset = <path> (or the runtime API) returns the field to computed.
Spec-8.21
overridable takes a boolean literal. It without value is
E-CONFLICT-04.
Spec-8.22
isOverridden(path) is true iff the field's source is
manual. The source is part of the state snapshot, and the output carries the
effective content.
Spec-8.23
An overridable field keeps its expression edges in the dependency
graph even while manual. This is conservative: cycle detection (Spec-9.4) is
unaffected by the source.
Spec-8.24
A runtime MUST be able to export a form's live state as a
snapshot, and MUST be able to import a snapshot and restore the form to
the state it describes. Save-and-resume is the reason state exists
(Spec-8.5), and an export format that is not specified is not save-and-resume.
Spec-8.25
A snapshot MUST serialise, for every field: its path, its
content, its Touched flag, its applicability, its override source
(Spec-8.20), and — for every relation — its held member (Spec-8.16). The
snapshot MUST also record the current page, the document's version
(§16.2), and the profile identifier (§5.6) under which it was taken.
Spec-8.26
A snapshot MUST be encoded as a single JSON object whose
serialisation is byte-stable across conforming runtimes, so that a snapshot
taken on one platform restores identically on another. The encoding of a
number is the canonical decimal form of Spec-4.36; the encoding of a
date is ISO 8601; the encoding of a duration is the ISO 8601 form of
Spec-4.17.
Spec-8.27
A runtime MUST reject a snapshot whose document version or
profile it does not implement, with E-VER-01, rather than restoring it
partially.
Spec-8.28
Restoring a snapshot is a transaction. The restored state MUST
satisfy the same invariants as any other transaction's result, and the
runtime MUST recompute validity in the restored transaction.
Spec-8.29
The required property is sugar, not a second mechanism:
required = true is exactly validate { required };
required = false is exactly validate { required when false } — a
constraint whose guard is never satisfied, and which therefore never binds;
required = <expression> is exactly validate { required when <expression> }.
A runtime MUST treat the two spellings identically. The property exists
because static requiredness is common and reads better on one line; it is not
a different rule. In particular required = false never makes a field
required, and it is the spelling to use when the requirement is carried by
accepted instead (Spec-8.31).
Spec-8.30
A field MUST NOT declare both the required property and a
required constraint. Doing so is E-CONFLICT-05: the two would say the same
thing twice, and two rules that must agree are a place where they will not.
Spec-8.31
The accepted constraint (§10.2.1) is unaffected by this rule.
A field MAY declare required = false together with accepted, and
required = false with accepted is the normal spelling for an affirmation
that is required to be true rather than merely answered.
Chapter 9 — The engine
Spec-9.1G1
For a fixed document D, profile P (§5.6), environment
E (Spec-5.41), and a fixed sequence of user actions A, the resulting
sequence of state snapshots is uniquely determined. A snapshot contains, for
every field, its content, Touched flag, and applicability; the held member of
every relation; the source of every overridable field; the status of every
action; and the current page.
Spec-9.2
D, P, E, and A fully determine every data-plane value.
There is no implementation-defined ordering, no dependence on hash-map
iteration order, no dependence on wall-clock time except through $now, and no
dependence on locale in the data plane (Spec-12.9).
Spec-9.3
The runtime maintains a directed graph whose nodes are:
Value nodes — the content of each input field
Applicability nodes — the result of each entity's when
Derived nodes — the result of each value, required, readonly,
options, constraint-guard, and validate expression, and each relation
equation
Collection nodes — the element set of each repeat
An edge X → Y exists iff expression Y reads node X. A read of a path
P is a read of P's value node and also of the applicability nodes of P
and of every entity enclosing P, because what an expression sees depends on
both (Spec-5.8).
Spec-9.4
The graph MUST be acyclic, subject to the relation rule of
Spec-8.13. A cycle is a compile-time error E-CYCLE-01, reported with the full
cycle path:
The graph is static up to one finite choice: which
member of each relation is held. Spec-8.13 requires every such choice to yield
an acyclic graph, and that is checked at compile time. Applicability never adds
or removes an edge: an inapplicable field changes the value a reader sees
(inapplicable), not the set of things it depends on. Within a transaction
the order is therefore fixed, and a single topological pass reaches the fixed
point (Spec-9.7).
Spec-9.6
Evaluation happens in transactions. A transaction is created by
any of: user input, action completion, repeat mutation, or explicit runtime
API call.
Spec-9.7
Because the graph is static and acyclic (Spec-9.5), step 5
computes a fixed point in a single pass. A runtime MUST NOT iterate step 5 to
convergence.
Spec-9.8
$now MUST return the same value for every evaluation within a
transaction. Reading the clock per-expression would make created_at differ
from updated_at for reasons the user cannot see.
Spec-9.9
Tie-breaking in step 4 MUST be by document order — the order in
which nodes appear in the source file. Two implementations that break ties
differently would violate G1.
Spec-9.10
A field with value = expr that is not overridable MUST ignore
user input; relation members (§8.8) and overridable fields (§8.9) accept input
by definition. A runtime MUST
either reject the write and report a non-blocking notice, or (in a UI shell)
present the field as read-only. Silently accepting input that is immediately
overwritten is non-conforming.
Spec-9.11
Derived fields MUST NOT be used as the target of action input
mapping. This prevents an action result from fighting the expression.
Spec-9.12
Changes to the shape of a repeat (add, remove, reorder) MUST
recompute, in a single transaction: the repeat's element count, every derived
value whose expression iterates that repeat, and the validation state of every
element. This is the Spec-7.11 requirement stated as an engine rule.
Spec-9.13
A runtime MUST NOT evaluate a comprehension over a repeat
whose element set is mid-mutation. Structural changes are atomic.
Chapter 10 — Validation
Spec-10.1
blur (default for most fields) — validate when the field loses
focus. Minimises interruption; the user has finished with the field.
Spec-10.2
change — validate on every keystroke. Correct for fields where
immediate feedback is cheap and useful: checkbox, radio, select, number with a
live range indicator.
Spec-10.3
submit — validate only on submit. Correct for expensive or
password fields.
Spec-10.4
A field MUST NOT display a validation error before the user has
interacted with it, unless the value came from a default that the runtime can
prove invalid. Showing "this field is required" on a pristine form is a defect.
Spec-10.5
Constraints accumulate. Multiple failing constraints on one field
MUST all be reported, not just the first, so the user can fix everything in one
pass.
Spec-10.6
custom = expr is the escape hatch. The expression MUST return
true (valid) or a string (error message) or null (valid). Returning a
string is what makes custom errors i18n-friendly (§12.3).
Spec-10.7
A custom constraint is evaluated in the field's own context, so
self denotes the field's value (§6.3.1).
Spec-10.8
The required constraint on a boolean field is satisfied by
false. It means "must have been assigned", not "must be true". A form whose
consent must be affirmative MUST use the accepted constraint.
Spec-10.9
The accepted constraint binds when its guard is exactly
true and fails unless the field's content is exactly the boolean true.
An unassigned boolean, an empty boolean, and a false boolean all fail it.
Unlike required, accepted is not satisfiable by un-ticking the control.
Spec-10.10
A failure of accepted is reported as E-VAL-04. The message
MUST be localisable (Spec-12.7) and MUST distinguish "not agreed" from "not
answered".
Spec-10.11
A required constraint on a boolean field MUST be reported
at compile time as warning W-VAL-01, whose message suggests accepted.
A runtime MUST NOT report W-VAL-01 for a boolean field that also declares
accepted.
Spec-10.12
Cross-field constraints participate in the same transaction as
the fields they read, so a form cannot be submitted in an inconsistent state
between two field updates.
Spec-10.13
A form is submittable iff every active, required field is valid
and every active constraint passes.
Spec-10.14strict forms
A form that declares strict = true is
submittable only if every active field is valid. A pending field (Spec-5.13)
blocks submission under strict, because its deferred constraint may yet
fail. A form without strict — the default — is submittable when no field is
invalid; a pending field does not block, and its deferred constraint is
reported as E-VAL-05 so that the deferral is visible rather than silent.
Spec-10.15
strict is a property of the form, not of a field, and it
affects only the submittability judgement of Spec-10.13. It MUST NOT affect any
data-plane result, the content of any field, or the value of any constraint.
Spec-10.16
A runtime MUST report the first failing field in document order,
not in click order or dependency order. Users read top to bottom.
Spec-10.17
The expression $form.valid evaluates to the submittability
judgement of Spec-10.13 computed without the strict adjustment of
Spec-10.14. strict governs whether the form MAY be submitted; it MUST NOT
change the value of $form.valid, which is a data-plane result (Spec-10.15).
Spec-10.18
Every validation message MUST carry a severity. Default is
error for all constraints in §4.8. Warnings are declared with a warn
prefix: warn max = 400 (credit-bureau guidance style, e.g. "this is above
typical").
Spec-10.19
An implementation MUST implement tier A1 and SHOULD implement
tier A2. Tier A1 is part of Level 0 conformance (§15.1). An implementation
that does not implement tier A2 MUST declare that fact (Spec-15.1) and MUST
NOT report tier-A2 diagnostics.
Spec-10.20
No tier may use a general constraint solver. No tier may
search, backtrack, or enumerate values. Every conclusion MUST be derivable
from the rules of this section, so that two conforming implementations reach
the same conclusion on the same document — a compile-time behaviour that
differs between runtimes is a conformance failure, exactly as a data-plane
behaviour that differs is (Spec-15.6).
Spec-10.21
Tier A1 reasons about one field's declared type and its
unguarded constraints, and MUST report E-SAT-01 when the intersection
of their domains is provably empty.
The domain of each construct is:
Construct
Domain
type = string(lo..hi)
strings of length lo..hi (scalars, NFC)
type = number(lo..hi)
the closed decimal interval [lo, hi] of the profile (Spec-5.28)
type = integer(lo..hi)
the closed integer interval [lo, hi]
type = enum(…)
the finite set of declared option keys
type = array(lo..hi) of T
arrays of length lo..hi with elements in T
type = object { … }
objects whose members are in the member domains
type = boolean
{true, false}
type = date / datetime / duration / file / signature / location / geo
unconstrained within the type's domain
required
the domain minus the empty content (Spec-5.4)
min / max
the numeric or date interval intersected with the domain
minLength / maxLength
the length interval intersected with the domain
multipleOf = k
the domain, with a divisibility constraint on numbers
pattern = re
the domain intersected with the language of re
options
the domain intersected with the option key set
in / notIn
the domain intersected with (resp. reduced by) the listed set
Spec-10.22
Tier A1 MUST report the more specific code where one applies:
E-BOUND-01 when the declared min exceeds the declared max;
E-BOUND-02 when the declared max exceeds the type's upper bound;
E-BOUND-03 when the declared min exceeds the type's upper bound;
E-BOUND-05 when a repeat's min exceeds its max. Only when none of
those applies does tier A1 report E-SAT-01.
Spec-10.23
The emptiness tests tier A1 MUST perform are:
Interval intersection — for number, integer, date, and length
bounds. Exact over the profile's decimal domain (Spec-5.28).
Finite-set intersection — for enum, options, in, notIn.
Regular-language intersection with a length range — for pattern against
a length-bounded string domain. The length range of a regular language is
computable for the RE2 dialect (Spec-6.40); the intersection of the
language with the length range is decidable. Where the string domain is
length-unbounded, a pattern constraint can only make the field
unsatisfiable if the language itself is empty, which is decidable.
Spec-10.24
Tier A1 MUST NOT report a field unsatisfiable unless the
intersection is provably empty. A construct whose domain tier A1 cannot
express — a pattern over an unbounded string domain whose emptiness is not
decidable, a custom constraint, a constraint whose guard is not true — is
undetermined at tier A1 and MUST NOT be reported.
Spec-10.25
A field whose constraints are all guarded is satisfiable at
tier A1: a guard that is not exactly true means the constraint does not
bind (Spec-5.10), and a constraint that does not bind cannot make a field
unsatisfiable.
Spec-10.26
Tier A2 reasons about constraints whose guard or target depends
on another field, and MUST consider, for each field F:
the guards of F's own constraints, narrowed by the tier-A1 domains of the
fields the guards read;
the relations F participates in (§8.8), whose equations relate F's
members.
Spec-10.27
Tier A2 MUST NOT use the $ namespace, a source whose type
is undeclared or any (Spec-5.21), or any expression whose domain tier A2
cannot bound. A field whose satisfiability depends on such an expression is
undetermined and MUST NOT be reported.
Spec-10.28
Tier A2 MUST report:
E-SAT-02 — two constraints on the same field have provably disjoint
domains. This is the cross-constraint form of E-SAT-01 and is reported in
preference to it when both apply.
E-SAT-03 — a contradiction that spans two fields: a field's constraint
and another field's constraint together have no joint solution (for example
equal = b on a and notEqual = a on b).
W-SAT-01 — a field is provably dead: its when is the literal
false, so the field can never be applicable and its constraints can never
run. This is a warning, not an error, because an extension or a later
revision may reinstate the field.
Spec-10.29
Tier A2 MUST terminate and MUST be bounded by the size of the
dependency graph (§9.2). Narrowing MUST proceed in document order and MUST
visit each node at most once per field; a cycle in the graph is already
E-CYCLE-01 and MUST NOT be entered by tier A2.
Chapter 11 — Sources, actions and offline behaviour
Spec-11.1
An action declaration describes a request. It is data, not code.
The runtime owns transport, auth, and serialisation. The legacy editor's
onSuccess(showSuccessSnackbar) form — a directive naming an unregistered
function — is removed; effects are handled by match over action status, which
is typed and analysable.
Spec-11.2
An action has a status: idle | pending | success | error.
Status is a normal derived value, so a form can branch on it:
retry MUST use exponential backoff with jitter and MUST cap at
the declared value. An action MUST NOT be retried on a 4xx response.
Spec-11.4
The body of an action is $form.output — the projected output
document (§4.10) — unless the action declares its own body.
Spec-11.5
A source is a lazily-evaluated, cached remote value. It MUST NOT
be fetched until something references it.
Spec-11.6
cache values: "none", "5m", "1h", "24h", "7d", or an
explicit duration.
Spec-11.7
Source resolution is asynchronous. A field whose options come
from a source — that is, a field that declares optionsFrom (§4.7) — MUST
render in a pending state until resolution, and MUST handle failure without
trapping the user. An implementation that cannot render a pending state is
non-conforming at Level 3 (§15.1).
Spec-11.8
A source that is iterated MUST declare a bounded type
(Spec-6.47).
Spec-11.9
A runtime operating offline MUST preserve unsent submissions in a
durable queue and MUST replay them on reconnect, in submission order.
Spec-11.10
An action declares a conflict policy for replay via the
conflict attribute: retry, skip, or fail. When the attribute is absent
the policy is skip, and the action is unmarked: a runtime MUST record that
the policy was not chosen explicitly, so that it can be reported. The default
MUST NOT be retry. A replayed request that carries a payment instruction
would send it twice, so re-sending MUST be an explicit, chosen behaviour.
Spec-11.11
A source that cannot be reached MUST NOT be queued for later
replay. The offline queue of Spec-11.9 preserves submissions, which are
writes; a source is a read. A source that fails to resolve leaves the fields
that depend on it without a choice set, and the failure is reported as
E-IO-01. A field whose source failed is not in the state of a field whose
source yielded no values (§4.7): the first carries the failure, the second
does not.
Chapter 12 — Internationalization
Spec-12.1G7
User-facing text MUST be externalizable: a conforming
runtime MUST be able to resolve every user-facing string in a document to a
locale-specific message, and the text block is the mechanism by which
translations are supplied.
Spec-12.2
Authors SHOULD place user-facing strings in text blocks and
reference them with t("key"). A string literal used directly as a
user-facing value is permitted — the language does not force indirection on
every label — but a Level 0 linter MUST report it as W-TEXT-01.
Spec-12.3
The text block's entries form a single catalogue. Entries
across all text blocks in a document are merged; a key MUST NOT appear twice
in the merged catalogue (E-SYN-08).
Spec-12.4
t("key") resolves against the current locale with this
fallback chain:
Any user-facing text MAY contain ICU MessageFormat
placeholders. The plural and select forms are REQUIRED at Level 2:
text
"files.selected": "{count, plural,
=0 {No files selected}
one {One file selected}
other {# files selected}}"
"greeting": "{gender, select, female {She} male {He} other {They}} applied."
Spec-12.6
Runtime implementations MUST use the host platform's CLDR plural
rules, not a hardcoded two-form one/other table. Languages with three or six
plural categories — Arabic, Welsh, Russian, Polish — are the reason this is
normative.
Spec-12.7
Error messages MUST be localisable, including validation errors
produced by built-in constraints. A runtime MUST be able to render
"Field must contain at least {min} characters" in any supported locale.
Spec-12.8
Dates, times, numbers, and currency MUST be formatted through the
host locale's CLDR data. A hardcoded MM/DD/YYYY is a conformance violation on
submit.
Spec-12.9G11
A data-plane result MUST NOT depend on the presentation
plane, and the presentation plane MUST NOT feed the data plane. The display
type marks a presentation-plane value. In particular:
The three presentation builtins — t, formatDate and formatMoney — are
the only functions that produce a display value (Spec-6.36).
A display-typed expression MUST NOT be used in a data position. The
data positions are: a field's value or default; an output entry's
value; the operand of an ordering comparison; the operand of -, *, / or
%; and one side of an == or != whose other side is not itself
display. Such a use is E-TYPE-08, and it is reported in preference to the
more general E-TYPE-02.
A data-plane result MUST NOT depend on the locale. Text shown to a person MAY
differ between locales; the data MUST NOT.
Chapter 13 — Accessibility
Spec-13.1G6
A Level 2 implementation MUST satisfy:
Requirement
Source
Rationale
Every input has a programmatically associated label
WCAG 3.3.2 (A)
Screen readers announce unlabelled inputs as "edit text"
Errors identified in text, not by colour alone
WCAG 1.4.1 (A)
~8% of men have colour vision deficiency
Errors described with a suggestion for correction
WCAG 3.3.3 (AA)
"Invalid" does not help; "Use YYYY-MM-DD" does
An error summary on submit, focus moved to it
WCAG 3.3.1 (A)
The established accessible pattern
Inline errors linked via aria-describedby
WCAG 1.3.1 (A)
Associates message with control
Focus visible at 2px minimum contrast
WCAG 2.4.11 (AA)
Keyboard and low-vision users
Target size ≥ 24×24 CSS px
WCAG 2.5.8 (AA)
Motor impairment
placeholder never substitutes for label
WCAG 3.3.2 (A)
Placeholder text vanishes on input
Change-of-context announced
WCAG 3.2.2 (A)
Page auto-navigation on select is disorienting
Spec-13.2
A placeholder MUST NOT be the only user-facing text for a
field. It disappears the moment the user types, taking the instruction with it.
Spec-13.3
A runtime MUST render label and MUST render placeholder only
as supplementary information.
Spec-13.4
On a failed submit, a conforming runtime MUST:
Render an error summary at the top of the form listing every failing field
Move keyboard focus to that summary
Announce it to assistive technology (e.g. role="alert", or a live region)
Render each summary entry as a link to its field
Render the same message inline at the field
Spec-13.5
Re-failing submission MUST return focus to the summary, not
scroll silently. This is the single most commonly-missed accessibility
requirement in form implementations.
Spec-13.6
group and repeat MUST be exposed to assistive technology as
labelled regions (<fieldset>/<legend> or equivalent). A repeated block
MUST expose its item count to the user.
Chapter 14 — The error catalogue
Spec-14.1
code and location are stable and MUST be emitted.
message is for humans and MUST NOT be parsed by tooling.
Spec-14.2
Implementations MUST emit context.suggestions for E-REF-01
using edit distance, and context.near_misses for E-PROP-01. A raw
"unknown identifier" with no suggestion is a diagnosability failure under G5.
Spec-14.3
A missing required property MUST be reported as E-PROP-07, not
as E-PROP-01. E-PROP-01 means "this property does not exist"; E-PROP-07
means "this property is required and is absent". The two are different
diagnoses and a linter that reports them identically is not useful.
Chapter 15 — Rendering and conformance levels
Spec-15.1
An implementation MUST declare its level. It MUST NOT claim a
level whose requirements it does not meet.
Spec-15.2
A renderer MUST map every intent it receives. An unmapped intent is
a conformance failure, and the runtime MUST surface it as a warning rather than
silently dropping the field.
Spec-15.3
A renderer MAY map an intent to a different widget, provided the
mapping preserves the intent's accessibility, validation, and keyboard
semantics. A renderer MUST NOT map RenderChoice to a control that cannot be
operated by keyboard alone.
Spec-15.4
A Level 2+ renderer MUST:
Maintain a labelled control for every active field
Expose field state to assistive technology per §13
Never render a value in a way that omits its label
Handle the pending source state without trapping focus
Preserve field identity across re-renders (so focus is not lost mid-typing)
Spec-15.5
A renderer MUST preserve input focus and cursor position across a
state propagation. A runtime that re-renders the whole form on every keystroke
and loses focus is non-conforming.
Spec-15.6
For a fixed document and a fixed script of transactions, two
conforming runtimes MUST produce identical: field values, validity states,
validation error codes, and output documents.
Chapter 16 — Versioning and extensions
Spec-16.1
The language is versioned MAJOR.MINOR.PATCH with SemVer
semantics.
Spec-16.2
A document declares its target with form name version = "1.0".
A runtime MUST reject a document whose MAJOR version it does not implement, with
E-VER-01.
Spec-16.3
Within a major version, MINOR releases MAY add:
new builtins (§6.4)
new field properties
new constraint kinds
new render intents
Spec-16.4
A document using a feature newer than the runtime's MINOR version
MUST be rejected, not ignored. Ignoring an unknown validate constraint means
a form silently stops enforcing a rule the author believed was active.
Spec-16.5
Extension blocks MUST be namespaced. Unnamespaced unknown syntax
MUST be an error.
Spec-16.6
An implementation MUST ignore unknown properties inside a known
extension namespace, and MUST report unknown blocks in that namespace. This
split is what allows a newer extension document to degrade usefully on an older
runtime instead of failing wholesale.
Spec-16.7
A namespace block's contents are visible only to documents that
import that namespace. A runtime that does not implement a namespace MUST
reject a document that imports it, with E-EXT-01.
Spec-16.8resolution
import "id" as alias resolves id against, in
order: (1) the namespace blocks declared in the same document, then (2) the
runtime's extension registry. The first match wins. If neither resolves, the
import is E-EXT-01. A document MUST NOT import two namespaces under the same
alias; that is E-EXT-02.
Spec-16.9contribution
An imported namespace contributes to the form
exactly as core vocabulary does:
a field declared in the namespace is a field of the form, addressable as
alias.name, and participates in the data model, in validity, and in output
if it is listed in the output block;
a constraint declared in the namespace binds under the rules of Spec-5.10 and
contributes to validity like any other constraint; a failure is reported as
E-VAL-02, with the constraint kind and the extension identifier in the
error's context;
a property declared in the namespace is available on the entities the
extension declares it for.
Spec-16.10versioning
A namespace block MAY declare
version = "<string>". A registry namespace carries the version the runtime
implements. A document that requires a version the runtime does not implement
is rejected with E-EXT-03; it MUST NOT be silently downgraded, because a
downgraded extension is a rule that does not mean what the author wrote.
Spec-16.11extensions are part of the form's identity
An imported
namespace participates in the document's identity for the purposes of §16.2
versioning and Spec-8.27 snapshots: a snapshot taken under one set of imported
namespaces MUST NOT be restored under a different set.
Chapter 17 — A worked example
Spec-17.1
A page with when = false MUST be skipped without blocking
navigation, and its fields MUST NOT contribute to form validity (Spec-7.15). The
verify page is skipped for technical support, and the user is never blocked
by a code they were never sent.
Chapter 18 — The conformance suite
Spec-18.1
The complete §17.2 document MUST parse, pass semantic analysis,
and produce a single well-defined output document for a given input sequence.
A conforming implementation MUST ship this document as a test fixture.
Spec-18.2
A conforming implementation MUST pass §18.1–§18.4 against the
same test vectors. The rows in §18.1 and §18.4 are complete documents and MUST
be parsed as written; the rows in §18.2 are single-statement fragments and
MUST be rejected with the stated code. The stated code MUST be the first
error reported for the row. An implementation MAY report further errors of the
same stage after it, but the stated code is the row's primary diagnostic.
Spec-18.3
An implementation MUST treat the §18.2 rows as errors of the
stated kind, not merely as rejections. A parser that rejects type = strng
with a generic "parse error" has not implemented §14; the code is the
interface. The stated code has to be the first error, so that a caller can
act on it without having to know which of the reported errors matters.
Spec-18.4
The reference parser for §3 and §4 is a grammar-level
artefact: it implements §3 and §4 only, and rejects the
syntactic and lexical rows of §18.2. The semantic rows of §18.2 — undefined
references, type errors, cycles, cost overruns, missing required properties,
unresolvable output paths — require the static analyser of §2.3 and are not
claimed by the reference parser. A conforming implementation MUST implement
both halves.
Chapter 19 — Migration from the legacy form format
Spec-19.1
A migration tool MUST be able to detect the legacy
computed(from …) pattern and MUST emit a diagnostic rather than a guess,
because the original expression is not recoverable from the dependency list.
Spec-19.2
A migration tool MUST emit a diagnostic when a legacy identifier
contains a hyphen, because FormScript 1.0 identifiers may not (§3.6). The tool MUST
suggest an underscore-separated equivalent and MUST check the result for
collisions.
This is the honest answer: The legacy editor's computed(from price, quantity)
recorded that total depends on price and quantity, but there was no syntax
anywhere in the nine parts for what total equals. The language's central
feature was unexpressible. Any automated migration must therefore surface these
as manual work.
Chapter 20 — Reserved words
Spec-20.1
The hard keyword not is the word form of the !
operator: not x is exactly !x, with the same three-valued semantics
(Spec-6.12). A document MAY use either spelling. not is reserved because
and and or are reserved and it would be incoherent to reserve two of the
three words.
Spec-20.2
This section is the single normative list of reserved words.
§3.5 shows a summary for the reader and MUST agree with this section.
Spec-20.3
A hard keyword MUST NOT be used as a declaration name — that
is, as the identifier of a field, group, repeat, section, page,
choice, or state block, nor as an import alias.
Spec-20.4
A soft keyword MAY be used as a declaration name, an attribute
name, and a field reference. The parser resolves the keyword interpretation
by context, per §4.14.
Spec-20.5
The following names are reserved for future use in version 2.x,
so that 3.0 can adopt them without a break: break, case, catch, for,
then, while, with.