SELSimple Expression Language
GitHub

SEL conformance suite

These files are normative. An implementation is correct when it passes all of them; when two implementations disagree, this suite and spec/ decide which is wrong, not either implementation.

The format is plain line-oriented text rather than JSON, so that a future port — Python, Lisp, anything — needs no parser other than the one it is already writing. Setup is written in SEL, which removes the need for a second data format entirely.


File format

### name: text.left.basic
--- setup
name = "Za\u{17C}\u{F3}\u{142}\u{107}"
--- source
LEFT(name, 3)
--- expect
text "Zaż"
===

A line is a marker if it starts with ### , starts with --- , or is exactly ===. Everything else is content, taken verbatim.

A section's content is its lines joined with \n, with leading and trailing runs of space, tab, CR and LF removed — those four characters and no others. That sentence is normative for the five readers, and it is fussier than it looks. Each host has a trim function to hand and no two of them strip the same set: JavaScript's String.prototype.trim() removes ECMA-262's WhiteSpace, which includes U+FEFF and every Unicode Zs; Python's str.strip() removes Unicode whitespace but not U+FEFF; PHP's trim() adds NUL and a vertical tab; C++ and Common Lisp were already spelling out the four.

Reaching for the native one is therefore a way to run a different program in one host than in another, invisibly. lex.space.bom-is-not-whitespace is the case that found it: four hosts raised E_SYNTAX at the byte-order mark and JavaScript answered TRUE, because its reader had deleted the BOM before the lexer ever saw it. The lexers had agreed all along. The same trap is why tools/README.md is normative about the corpus format's trailing newline.

The four are SEL's own whitespace, which is the only set that can be right here: a reader that strips something the language does not is asserting that two different programs are the same one.

Marker Meaning
### name: <id> starts a case; <id> must be unique across the whole suite
--- note prose, ignored by the runner
--- setup SEL source evaluated first against an empty context; the resulting context is then given to --- source. Optional.
--- source the SEL source under test. Required.
--- expect one expectation line. Required.
=== ends the case

Blank lines and lines beginning with % outside any case are ignored, so files can carry headers and section breaks. Inside a section every line is content, including ones beginning with # — those are SEL comments, not file comments.

An error raised by --- setup is a suite bug, not a test failure, and the runner reports it separately.


Expectation forms

One per case.

Form Asserts
text "…" kind TEXT with exactly these code points
num 0.3333333333 kind TEXT with exactly this content — sugar for text, unquoted, for readability
bin 00ff10 kind BIN with exactly these bytes, lower-case hex
bool TRUE / bool FALSE kind BOOL
none kind NONE with no children
tree … full structure — see below
error E_CODE that code, position not checked
error E_CODE at 3:11 that code at that 1-based line and column

Quoted text in text "…" uses a fixed, tiny escape set handled by the runner itself — \\, \", \n, \t, \r, \uXXXX — deliberately not SEL's lexer. The suite must not validate the lexer with the lexer.

The tree dump

tree compares against a canonical dump that every implementation must emit byte for byte. Agreement on the dump is itself part of what is being tested.

dump(v)  = scalar(v) + children(v)

scalar:    NONE -> "-"
           TEXT -> "t" + quoted
           BIN  -> "b" + lower-case hex
           BOOL -> "TRUE" | "FALSE"

children:  none    -> ""
           n keys  -> "{" key "=" dump(child) { ", " key "=" dump(child) } "}"

Keys are quoted with the same escape set as text. Children appear in insertion order — order is normative, so a dump mismatch caused purely by ordering is a real failure.

(1, 2)              ->  -{"1"=t"1", "2"=t"2"}
A=1; A[2]="x"; A    ->  t"1"{"2"=t"x"}

Running

node js/bin/conformance.mjs                              [file…]
php  php/bin/conformance                                 [file…]
cpp/build/conformance                                    [file…]
lisp/bin/conformance                                     [file…]
PYTHONPATH=$PWD/python python3 python/bin/conformance.py [file…]

With no arguments each runs every conformance/*.selt. All exit non-zero on any failure and print, for each, the case name, the expectation, and what was actually produced. tools/check.sh runs the lot; tools/impls.sh is the roster they come from.

The files

01–10 are grouped by language feature. Two more are grouped by failure instead, because misuse is a surface of its own and organising it by feature scattered it into a dozen places where nobody could see what was missing:

File Holds Naming
11-arity.selt every function, one argument below its minimum and one above its maximum arity.<function>.too-few / .too-many
12-misuse.selt wrong types, wrong values, wrong shapes — grouped by the Args accessor that rejects the argument mis.<accessor-or-area>.<detail>

Both pin at line:col on every case. For 11 that is the call's position; for 12 it is the argument's, which is the innermost-failure promise in spec/errors.md being held to. A new built-in lands with its two arity cases and its type cases in the same change as its implementation.

Adding cases

A new built-in lands together with its cases in the same change. When the two hosts disagree, add the minimal case that reproduces it before fixing either one — that case is the durable part of the fix.

Before adding a case, check that its source is not already in the suite under another name: names are checked for uniqueness by the runner, sources are not, and a duplicate pair survived in the suite for exactly that reason.

View this page's Markdown on GitHub