sml

SML { ❄ }

Downloads Questions, bug reports, or just to say hi: sal@mail.swebase.cn 中文 EN

Chapter 10: Complete Reference to Features

The design principle of SML is “from minimalism to richness, with customizable functions” - the basic nine piece set is enabled by default; Complex abilities are disabled by default and can be explicitly enabled using @feature enable when needed.

This chapter is an authoritative reference for each feature: opening methods, syntax, error messages, and relationships with other features.

⚠️ Scope of this chapter (added after measurement, 2026-09-19): the @feature enable/disable gating mechanism itself currently exists only on Rust (reference implementation) and JS — C / C++ / Lua treat @feature … as an illegal directive (E-PARSE-005, measured; their legal directives are only contract / is / version), so their capability set is fixed. Individual features are also unevenly implemented (e.g. typed-block is Rust + JS only); see the table below and the audit table in §5.2.2. In documents shared across implementations, use forms all five understand (such as @is) and do not rely on @feature.

Implementation coverage (measured on real binaries, 2026-09-19)

Every cell is a behaviour obtained by running real binaries (Rust smltools, the C dumper, a purpose-built C++ runner, luajit, JS parseSafe) — not a guess from reading source. ? means not measured this round (it does not mean “unsupported”).

Feature Rust JS C C++ Lua
@feature gating mechanism ✅ ✅ ❌ E-PARSE-005 ❌ E-PARSE-005 ❌ E-PARSE-005
bareword-string ✅ ✅ ✅ ✅ ✅
include (single file) ✅ ✅ ※ ✅ ✅ ✅
env ($env.X) ✅ ✅ ✅ ✅ ✅
contract (@contract / @is) ✅ ✅ ✅ ✅ ✅
fragment (@base {} + &base) ✅ spliced ✅ ⚠️ not spliced (keeps an &base key) ❌ E-PARSE-005 ❌ E-PARSE-001
top-level-array ✅ ❌ E-PARSE-003 ✅ ❌ E-PARSE-006 ❌ E-PARSE-006
namespace (include … as ns) ✅ ✅ ✅ ✅ ❌ E-FEATURE-001
implicit-ns ✅ ✅ ? ? ?
multi-include (include "a", "b") ✅ ✅ ✅ ✅ ❌ E-FEATURE-001
glob-include ("inc/*.sml") ✅ ⚑ ✅ ❌ E-INCLUDE-001 ? ❌ E-FEATURE-001
regex-include (re:"…") ✅ ✅ (restricted subset) ⚑ ? ❌ ❌
ext-rewrite ✅ ? ? ? ?
when (@when) ✅ ❌ E-PARSE-005 ❌ E-PARSE-005 ❌ E-PARSE-005 ❌ E-PARSE-005
for (@for) ✅ ❌ E-PARSE-005 ❌ E-PARSE-005 ❌ E-PARSE-005 ⚠️ to be re-checked
typed-block ✅ ✅ ❌ silent (__type metadata only) ❌ same as C ❌ even the metadata is lost

※ JS resolves include from a host-supplied file map: parse(text, { files }) — JS itself never touches the filesystem (designed for editors / servers; files is { "relative/path": "content" }). Without files you get E-INCLUDE-001. ⚑ glob-include / regex-include status on both ends (measured 2026-09-19): ① the JS side now has both (it used to report E-INCLUDE-001 for glob and E-LEX-004 for re:"…"); ② built from the current source, Rust and JS now agree on the same 15 inputs (directory prefixes, anchors, error codes, zero-hit cases); ③ there is an older record of “Rust’s release binary did not expand glob” (os error 123) — if the binary you have still behaves that way it is simply older than the source (the glob branch has been there all along); rebuild it; ④ Rust’s regex-include had a Windows-only defect: a \. in the pattern was treated as a path separator (re:"widget_.*\.sml" was split into dir=widget_.* ⇒ “directory read failed”) — fixed (the split now uses SML’s /); ⑤ JS’s re: is the same restricted subset: groups (), alternation |, {n,m} and \d are treated as literal characters on both ends (the engine only has . [...] * + ? ^ $). (preserved note about the release binary follows)

⚑ Rust’s glob-include did not expand in that older round (the literal path inc/*.sml was opened ⇒ os error 123), although the source does contain a glob branch (rust/sml-include/src/parse.rs:367-377) ⇒ the release binary in use is suspected to be out of sync with the sources (needs a rebuild + re-measure). ⚠️ Lua’s for needs re-checking: it “accepted” @for without the feature while rejecting @feature — confirm whether it really expands or just swallows the token as an ordinary key.

Three conclusions for anyone writing docs:

  1. @when / @for exist only in Rust today (the other four all return E-PARSE-005) — do not put them in examples that claim to be cross-implementation.
  2. top-level-array is Rust + C only; namespace / multi-include are missing only in Lua (and Lua fails loudly with a code, not silently).
  3. C / C++ / Lua have no @feature (E-PARSE-005), so “trim capabilities with @feature” simply does not apply to them; they support namespace / multi-include etc. unconditionally.

Lexical-level differences (measured 2026-09-19, again from real binaries)

The same round swept lexical units too (20 cases × 5 implementations; the verdict for each case is “did the case’s key x survive”, which is what catches silent data corruption — “does it parse at all” cannot see it):

Case family Result
Leading BOM (U+FEFF) ⚠️ all five used to glue the BOM onto the first key name (Rust printed "\uFEFFx": 1, JS {"\uFEFFx":1}) ⇒ fixed on all five this round (see below)
Block comments /* */, _* *_, multi-line, containing a /* mention, at EOF without newline ✅ all five agree
Strings: single-line / multi-line / escapes / containing # // /* ✅ all five agree
Triple-quoted multi-line string ❌ Lua only: the key is dropped (not fixed; registered)
CRLF line endings, three line-comment styles, @version with trailing comment ✅ all five agree
@feature … # trailing comment ❌ Lua swallows the next line (not fixed; registered); C reports E-PARSE-005 (no @feature, see the table above)

How the BOM was fixed (one place per implementation, all treated as file-level normalisation): Rust in sml-parse::scan::strip_version (the single funnel of every text entry) plus sml-include::expand_file_tokens (child files); JS in parse(); C in sml_parse plus the child-file read in resolve_includes; C++ in Parser::parse plus the include child-file read; Lua in Sml.load plus the include child-file read. Each implementation gained a test that asserts the key is still x — asserting “it parses” would not catch this (without the fix it still “parses fine”). ⚠️ Worth noting: smltools’ YAML / XML importers already stripped the BOM (yaml.rs:93, xml.rs:63); only the native SML path was missing it — the same job treated two ways.

10.1 How to turn on/off features

At the beginning of the file, use the @feature command:

@version v1
@feature enable glob regex
@feature disable ext-rewrite

-enable name list: separated by spaces.

-disable name list: Same as above.

-It can appear multiple times and take effect by overlapping in the order of appearance.

-Write after @version and before other content.

After activation, the corresponding capabilities can be used within this file (or included subfiles); Different files can be declared independently.

10.2 Built in feature list

include (default enabled)

The most basic “file insertion” ability.

Item Value
Status Default enabled, unable to disable
Grammar include "path.sml"
Function Insert the content of the target file as it is
Path benchmark The directory containing the file itself
Nesting limit 32 layers (over limit error)
Circular reference Error (not silent)

Error codes: include.circular, include.depth-exceeded, include.not-found.

namespace (default enabled)

as namespace, pointwise path, macro/contract isolation.

Item Value
Status Default On
Grammar include "x" as a.b.c
Function Package content into a { b { c { ... } } } nested blocks
Macro Isolation Yes - @name/@contract is also isolated by namespace
External reference syntax a.b.c.MacroName (qualified name)
Internal reference syntax MacroName (local name, parser automatically adds prefix)

Error codes: ns.invalid-path, ns.duplicate-symbol, ns.unresolved-prefix.

implicit-ns (default enabled)

include without extension is automatically used as a namespace.

Item Value
Status Default On
Grammar include "ui" Equivalent include "ui.sml" as ui
Implicit namespace name file name (without extension)
Explicit as Coverage Explicit as foo takes precedence over implicit

Close it: @feature disable implicit-ns - Afterwards, include "ui" must have an extension.

multi (default off)

Include multiple targets and import aliases at once.

Item Value
Status Default Off
Open @feature enable multi
Grammar include "a.sml", "b.sml" as y, "c"
Alias format import ui.buttons, admin.panel
Same name conflict Error (not silent)

Error codes: multi.dup-name, multi.empty.

glob (default off)

* is compatible with multiple files in the matching directory.

Item Value
Status Default Off
Open @feature enable glob
Grammar include "widgets/*.sml"
Wildcard * (one character, not crossing /), ? (single character)
Sort Dictionary order determined, consistent across platforms
Implicit as Applicable - include "widgets/*" ⇒ as widgets

Error code: glob.not-found (if 0 matches) glob.malformed.

regex (default off)

re://.../ prefix triggers regular matching.

Item Value
Status Default Off
Open @feature enable regex
Grammar include "re:^v[0-9]+\\.sml$" as versions
Regular subset . * + ? ^ $ [a-z], \\. \\d \\w
Performance Handwritten recursive backtracking, O (n · m) sufficient file name short string
Attention Use \\. as the path delimiter (\ also needs to be double written in md)

Complete regularization features (lookahead, backref) not supported - deliberately kept simple, for complex matching, please use shell universal+glob.

ext-rewrite (default off)

Parse files with any suffix as .sml.

Item Value
Status Default Off
Open @feature enable ext-rewrite
Grammar include "*.json" -> .sml, include "conf" -> .sml
Typical usage Rewrite .json/.yaml/.conf and process with SML parser
Risk Incorrect parsing of binary files as SMLs can cause stack explosion; Suggest adding globe restrictions

contract (default enabled)

@contract/@is verification and backfilling.

Item Value
Status Default On
Grammar @contract Name [loose|strict] { field: type [default v] [min n] [max n] [enum(a,b)] [?] [required] }
Reference type str int num bool array[T] enum(...) or another contract name
Strictness default strictness; loose allows undeclared fields; strict makes strictness explicit (same as default, for readability)
Nested Infinite; Recursive contract detects loops when referenced
Wrong position Accurate to rows and columns

Error codes: contract.required-missing, contract.type-mismatch, contract.enum-invalid, contract.out-of-range, contract.unknown-field, contract.recursive.

env (default enabled)

$env.VAR environment variable injection.

Item Value
Status Default On
Grammar $env.VAR_NAME
Missing behavior Replace with empty string (no error reported)
Type Always a string (numbers should also be quoted)
Escape _ . - is available in the name; The first character must be a letter or _
Nested $env cannot write $env anymore

Error code: env.bad-name.

escape (default enabled)

Escaping within a quoted string.

Item Value
Status Default On
Support \n \t \r \\ \" \' \0 \u{XXXX} \uXXXX
Not supported Octal \077, hexadecimal naked \x41 (to avoid ambiguity)
Scope Quotation string only; Naked words do not escape

fragment (default enabled)

@name/&name fragment inheritance.

Item Value
Status Default On
Grammar Define @name { ... }, Reference key: &name
Scope Follow namespace (when @feature enable namespace)
Conflict Same Scope Duplicate @name Error

top-array (default enabled)

The top level of the file allows for arrays (not objects).

Item Value
Status Default On
Grammar [{...} {...} ...] (top layer is array)
Typical scenario Configuration items are sequential tables (such as monitoring rules, routing tables)

bareword-str (default enabled)

Naked words are automatically recognized as strings.

Item Value
Status Default On
Close @feature disable bareword-str - all strings must be quoted afterwards
Trigger When version v.strict-strings() is set

when (default off)

Parse-time static conditional pruning: based on $env.*, decide at parse stage whether the next field/block is kept.

Item Value
Status Default Off
Enable @feature enable when
Syntax @when $env.NAME [==|!= "value"] (no operator = truthiness test)
Scope The next field/block immediately (cleared after consumption)
Condition form Closed set: only $env.NAME truthiness, or $env.NAME ==/!= literal
Dependency The $env.* left-hand side requires the env feature to be on
@version v1
@feature enable when

@when $env.ENV == "prod"
log_level: warn        # only appears in prod

@when $env.DEBUG       # truthiness: non-empty and not "0"/"false"
verbose: true

Design trade-off: no general expressions (&&/||/()/function calls). Supporting them would require an evaluator plus sandbox, timeout and IO-restriction guards, conflicting with “SML is a pure data format”. Split compound conditions into multiple @when, or generate per-environment files with an external build tool.

for (default off)

Parse-time bounded loop unrolling: substitute each item of the finite list after in into the loop body, producing an array.

Item Value
Status Default Off
Enable @feature enable for
Syntax key: @for VAR in a b c { ... }
Loop variable Read-only binding, referenced inside the body via ${VAR} text interpolation
List The finite enumeration after in (naked word or quoted string); no while, no recursion
Interpolation position ${VAR} is only allowed in value position (e.g. name: "${h}"); key position is not supported and raises a parse error
Scope In nested @for, the inner level sees the outer binding (same name shadowed by inner)
@version v1
@feature enable for

hosts: @for h in web api db {
  name: "${h}"
  port: 8080
}
# ⇒ hosts: [ {name:"web",port:8080}, {name:"api",...}, {name:"db",...} ]

Combination trap @when × @for:

  • When outer @when is written before the @for field, a false condition means the whole hosts field disappears (not an empty array, the key is deleted) — “@when filters the field, not a single iteration”.
  • When @when is written inside the @for body, it applies to the next field within the block and is evaluated per-element. The two semantics differ — keep them straight.

Why no general loops: bounded for ... in enumeration belongs to LOOP (primitive recursive) languages — it cannot compute Ackermann, thus is not Turing-complete; once while/recursion is introduced, sandbox and resource quotas become mandatory, which conflicts with “SML is a pure data format”.

${VAR} is NOT supported in key position: the {/} inside ${VAR} are treated as block boundaries by the lexer, which would split the key and silently produce garbage. Loop-variable interpolation only accepts value position (name: "${h}"); for “dynamic keys”, use an array instead: hosts: @for h in a b c { name: "${h}", ... } and index by name on the consumer side. Writing ${VAR} in a key position raises an explicit error (fail-fast), not garbage.

A complete runnable example is in examples/for_when.sml (@when + @for + nested @for + combination trap, demonstrated in one file).

10.3 Compatibility Matrix

Features ↓→ with namespace with multi with globe with regex
include ✅ Direct combination ✅ ✅ ✅
namespace — ✅ ✅ ✅
multi ✅ — ⚠️ See Note 1 ⚠️
glob ✅ (Implicit as) ⚠️ — ❌ Exclusive see Note 2
regex ✅ ⚠️ ❌ Mutual exclusion -
ext-rewrite ✅ ✅ ✅ ✅
contract ✅ (Reference contract with restricted name) — — —

Note 1: The “comma separated” syntax of include "a, b" as y is used together with multi, and a single target in the list cannot contain , (even inside quotation marks - commas inside quotation marks are considered literal).

Note 2: include "re:^.*\\.sml$" has already covered all. sml, there is no need to use include "*.sml" again. Simultaneously using what may be interpreted as “glob priority” or “regex priority” may result in different cross implementation behaviors - SML specifies explicit prefix priority: re: follows regex; *.sml takes the globe.

typed-block (default off; Rust ✅ + JS ✅, C / C++ / Lua ❌)

Block-level type annotation: put the contract name in front of the block name (Metrics metrics { }), equivalent to writing @is Metrics on the block’s first line. No new tokens — it is exactly the same shape as the existing bare-block form type [name] { } and only takes effect when the first word is an already-defined contract name. See §5.2.2.

Block-level annotation also supports multi-contract mixin: put several contract names in front of the block name (HasHost HasPort api { } — from the first word, every consecutive word that hits the contract table is a contract; the first miss is the block name), equivalent to @is HasHost, HasPort inside the block. The single-contract form (Metrics metrics { }) is byte-for-byte unchanged; only ≥ 2 contract words trigger the mixin, and a same-field conflict reports E-CONTRACT-016. See §5.4.1 Mixin.

@feature enable typed-block
Item Value
Default off — without it the form degrades to a plain bare block: the contract is not applied and no error is raised (a silent divergence)
Depends on contract (the contract layer itself, default on)
Implementations (audited 2026-09-19) Rust ✅ (Feature::TypedBlock in rust/sml-feature, gated in rust/sml-parse/src/parser.rs) · JS ✅ (js/sml.mjs) · ❌ C / C++ / Lua (no such feature name in their sources)

⚠️ On C / C++ / Lua the form degrades to a plain bare block (extra __type/__name metadata) — the contract is neither validated nor default-filled, and no error is raised: a silent divergence (measured with C’s dumper).

⚠️⚠️ Even more important: the @feature gating mechanism itself currently exists only on Rust + JS — C / C++ / Lua treat @feature … as an illegal directive and report E-PARSE-005 (“legal directives: contract / is / version”; measured on C, same code path in C++ / Lua). So this chapter’s “trim capabilities with @feature” narrative does not apply to those three (their feature set is fixed). ⇒ In documents shared across implementations prefer @is ContractName and do not rely on @feature.

10.4 Implementation Layer of Features (Architecture Tips)

SML parser runs according to “feature bitmask”:

FeatureSet = (include | namespace | implicit-ns | contract | env | escape
              | fragment | top-array | bareword-str
              | multi | glob | regex | ext-rewrite
              | when | for | typed-block)

-Core layer (default enabled) 9 bits default=1 (all basics except when/for).

-Entering the hierarchy (default off) multi | glob | regex | ext-rewrite | when | for 6 bits default=0.

-The parser directly rejects the corresponding syntax for unopened features (parsing error, not silently skipping).

when/for belong to the “parse-time directive” family, gated by the Rust-side cargo feature when at compile time; the document-level when/for feature switches control whether the syntax is allowed at runtime. Compile-time and runtime are decoupled: even if the cargo feature is off at build time, other features are unaffected.

This is the implementation foundation of ‘customizable functionality, don’t fall into the same path as YAML’.

10.5 Performance and Portability

Features Impact on parsing time Cross platform differences
include O (total file size) Path delimiter normalization (/) ↔ \)
namespace Extremely small (path compiled once) None
multi Linear superposition None
glob O (n) file enumeration hidden files (.foo) default not included
regex O (n · m) short string None
ext-rewrite Size of file to be rewritten Content encoding assumed UTF-8
contract Linear with field number Recursive contract needs to be memoize

10.6 Future Features (Roadmap)

The following items are all unimplemented, roadmap previews only; each is annotated with its current substitute / mitigation so readers neither misuse them nor reinvent the wheel:

Idea Status Today / superseded by
@import once (dedupe include so a file is not pulled in twice) Unimplemented Diamond includes are already legal (the chain-stack cycle check only blocks true cycles, E-INCLUDE-002), and total expansions are capped by E-LIMIT-003 — dedupe is not required
feature from "another.sml" (inherit feature settings from another file) Unimplemented No direct substitute; each file declares its own @feature at the top
with contract=loose (block-level relaxation overriding file-level) Unimplemented Substitute — write loose / strict on the @contract itself (contract-level, not block-level override)
? ternary shorthand (a ? b : c at value position) Unimplemented ? is currently the optional field marker; there is no ternary at value position

10.7 Give it a try with your hands

Write an features.sml for your project:

@version v1
@feature enable glob multi
@feature disable ext-rewrite

include "modules/*.sml" as modules
import modules.auth, modules.billing

Use commands such as sml check features.sml (refer to ch07 Multilingual) to run parsing and contract verification.

→ Appendix: Comparison and Investigation

Hands on practice

After reading this chapter, directly modify SML in the editor below and click “Run” to immediately see the parsing results or validation errors - having output is necessary for efficient learning.

✍ Hands-on exercise Use include to load multiple subfiles into their respective namespaces: include "auth.sml" as auth and include "billing.sml" as billing. Each subfile uses @is Module and includes a name field. Verify that both the auth and billing namespaces are correctly loaded.
💡 Hint: Include... as auth/as billing loads different files into isolated namespaces to avoid field conflicts.
Virtual files (referenced by include)
auth.sml
@contract Module { name: str }
@is Module
name: auth
billing.sml
@contract Module { name: str }
@is Module
name: billing
✍ Self-check quiz: Chapter 10 Self testing: Feature Reference Score 0 / 4
Q1. Which feature should be used to enable the include feature in Glob configuration?
Q2. Judgment: Without declaring @feature, all SML basic syntax (key value/block/array/contract) is still available.
Q3. Which of the following belongs to the expansion capability of feature control?
Q4. What are the common mistakes in writing @feature (such as spelling a wrong name)?