Solver shell — language & syntax reference
This is the authoritative reference for the solver shell command language. It
defines what the user types (the surface syntax), how that is normalised (the
canonical form), and how the canonical form executes (semantics), driving
the lexer → parser → interpreter pipeline that runs every command block.
1. Overview
The shell reads one command block at a time and executes it, producing a
single integer exit code (rcode). A command block is a tree of statements
composed with the sequencing operators ; && ||, optionally grouped with
{ }, and optionally wrapped in a loop header that runs the whole block once
per value of a list.
Every statement has the same shape once normalised:
<guard>: <target> = <evaluation>;
- guard —
True, or a boolean expression that decides whether the statement runs. The&&/||operators lower to guards overrcode. - target —
_(discard) or a variable name to which the evaluation's value is assigned. - evaluation — a command invocation or a value expression; it yields a value
and sets
rcode.
The whole language is sugar over a list of these guarded statements.
2. Lexical elements
-
sequence separator —
;or newline -
and-operator —
&& -
or-operator —
|| -
group —
{…} -
loop header —
loop…: -
assignment —
= -
discard target —
_ -
variable reference —
{name}(also{name.attr}and{name:spec};{{/}}for a literal brace) -
word / command — bare token,
shlex-quoted ('…',"…") -
comment —
#to end of line (outside quotes) -
A newline inside an unclosed
{ }continues the block (the reader counts brace depth); otherwise a newline ends the block. A newline is otherwise equivalent to;. -
Whitespace is insignificant except inside quotes and as a token separator.
-
Quoting follows
shlex: single quotes are literal, double quotes group, backslash escapes. Tokenisation of a command's arguments usesshlex.split.
Variable references
A {name} (or {name.attr}, {name:spec}) reference is expanded by the
interpreter as it evaluates a statement. It reads the value from the
store and, when the name is a callable, invokes it first
(without any arguments), so the substituted value is recomputed on every
reference; an attribute path is then resolved on that value with
getattr, segment by segment ({problem.difficulty} → the difficulty field
of the workspace Problem). How the resulting value is rendered depends on
context:
- command argument — rendered with
shlex.quote(str(value))(orformat(value, spec)when a spec is given), i.e. as one shell-safe token, so a value containing spaces stays a single argument. - expression — handed to the safe evaluator (§4): a literal-safe value
(number, string, list of those, …) is inlined as its
reprso it parses as a literal, while any other object (e.g.{problem}, aProblem) is bound by reference and resolved during evaluation rather than stringified.
Also:
- A non-group
{…}is always a variable reference — never a set/dict literal — so it must be a complete{name}/{name.attr}/{name:spec}with a §3-valid name ([a-z][a-z0-9_]*). Anything else ({0problem},{problem[0:1]},{1,2,3}) is a lex-time syntax error. To index or slice a list, the brackets go outside the braces —{problems}[10:12],{solved}[0], not{problems[0]}(see §4). {name.attr}is attribute access on the variable's value; the path may chain ({name.attr.attr}). Each segment follows the same[a-z][a-z0-9_]*convention as a name — so private/dunder attributes are not reachable — and anything else ({problem.0},{problem._fields}) is a lex-time syntax error. A missing attribute raises at substitution time and aborts the statement (reported, not fatal), like an undefined{name}.{name:spec}applies Python's format mini-language:echo {problem.number:04d}. A spec may follow an attribute path —{problem.title:>30}— and formats the final value.- A literal brace must be doubled:
{{→{,}}→}. - Substitution is not quote-aware: a
{name}inside quotes is still expanded. - An undefined
{name}raises and aborts the statement (reported, not fatal).
3. Names and variables
Variable storage, the reserved names, and the two write channels are defined by
variables.py; this section only states the syntactic rules.
- User variable name:
[a-z][a-z0-9_]*(lowercase, leading letter, no dash — so a name never collides with a command name likeclaude-apior with subtractiona-b). It may not be a reserved name or a registered command name. - Special (reserved) variables — five seeded by
variables.py, the rest registered with@variableby the module that computes them, so which exist depends onpackages/solver/solver/modules.csvexactly as the command set does. None may be assigned to.
| name | type | meaning | written by |
|---|---|---|---|
accounts |
list[roster.Account] | the collaborator accounts on the roster | computed, on each reference |
articles |
list[Article] | every writable blog — the tag pages and the curated ones | computed, on each reference |
config |
Config | the global configuration singleton | nothing — fixed |
key_requests |
list[Thread] | the key-authorization requests waiting to be granted | computed, on each reference |
last |
int | number of the last solved problem | computed, on each reference |
loop |
Any | the current loop value; None outside a loop | the shell |
next |
int | number of the next unsolved problem | computed, on each reference |
open_prs |
list[PullRequest] | the open pull requests waiting for review | computed, on each reference |
problem |
Problem | None | the workspace problem, as an object | the shell |
problems |
list[Problem] | every known problem | computed, on each reference |
random |
int | number of a random unsolved problem | computed, on each reference |
rcode |
int | exit code of the most recently run evaluation | the shell |
reserved |
list[str] | sorted list of every reserved name | nothing — fixed |
solution_files |
list[str] | the current problem's solution files | computed, on each reference |
solved |
list[Problem] | the solved problems | computed, on each reference |
tags |
list[Tag] | the central tag vocabulary | computed, on each reference |
threads |
list[Thread] | this caller's messages, newest first | computed, on each reference |
unsolved |
list[Problem] | the unsolved problems | computed, on each reference |
A computed special is re-evaluated on each {…} reference, so {random} yields a
fresh pick, {next} reflects current progress, and {open_prs} the queue as it is now.
Two references in one block are therefore two reads — put an expensive one in a loop
header, which is evaluated once, rather than in the body.
last / next / random are bare problem numbers (handy as command
arguments: init {next}), while the rest hold objects — use an attribute path to reach
a field, e.g. {problem.number}, {loop.title}, {loop.branch}.
A computed special may also name a menu: Ask(choices='open_prs') offers the same list
loop {open_prs}: iterates (see docs/developer-guide.md §3.11), so the two can never
disagree about what is on it.
- assign vs set (see
variables.py): a username = exprstatement assigns through the assign channel, which rejects every reserved name. The shell updates the settable specials (problem,rcode, andloopvia theloopconstruct) through the separate set channel.
4. Statements
A statement is one of:
- Command — a registered command name followed by arguments:
init {next},eval,stack && reset. Returns the command'sintexit code. - Assignment —
name = evaluation: evaluates the right-hand side and stores its value undername. The right-hand side may be either kind of evaluation — an expression (x = {problem.number} + 1) or a command, in which case the stored value is the command's integer exit code (x = init 42). - Bare expression — a value expression with no command and no
=:{problem.number} > 100,[1, 2, 3]. Its value setsrcodeby truthiness (see §6).
Evaluations: command vs expression
Within an evaluation the interpreter dispatches on the first token (after {}
substitution): if it names a registered command, the evaluation is a command
invocation; otherwise it is a value expression evaluated by the safe
evaluator. A safe expression supports only literals, names already substituted to
literals, comparisons, boolean/unary/binary operators, list and tuple literals
({…} is a variable reference, not a set/dict literal — §2), indexing/slicing
({problems}[6:10:2], {solved}[0]), and calls to a fixed set of pure builtins
(len, min, max, sum, sorted, abs, round, any, all, range,
int/str/bool/list/tuple/set/dict/frozenset, …) where the callee is
a bare name — no attribute access, lambdas, calls to substituted values, or
surviving names. (Attribute access exists only inside a {…} reference —
{problem.title} — resolved at substitution time, §2; {problem}.title in an
expression is still an error.)
5. Composition and precedence
Binding from tightest to loosest:
{ }grouping&&(and)||(or);/ newline (sequence)
So a && b || c ; d parses as (((a && b) || c) ; d). Use { } to override:
a && { b ; c } guards the whole { b ; c } group on a succeeding.
Lowering operators to guards
&& and || are sugar; the lexer/parser lower them to guarded statements. The
guard of each operand after the first is built from the preceding operand's
rcode:
a ; b → True: a then True: b
a && b → True: a then {rcode} == 0: b
a || b → True: a then {rcode} != 0: b
The exit code is referenced as the variable {rcode} (not a bare rcode), so
the parser resolves it through the same {} substitution as any other variable.
A statement whose guard is false is skipped and does not change rcode,
so a skipped operand's predecessor code flows on to the next guard (e.g.
false && a || b runs b).
6. Exit code (rcode) semantics
Every executed evaluation sets rcode:
- Command:
rcode= the command's returnedint. - Expression:
rcode=0if the value is truthy, else1. (The value itself is what an assignment stores;rcodeis only the truthiness.) - Skipped statement (guard false):
rcodeunchanged. - Block / group:
rcodeis that of its last executed statement.
solver "<block>" exits the process with the block's final rcode, so blocks
can gate shell-level pipelines: solver "init 42 && eval && stack".
7. Loops
A command block may be prefixed with a loop header:
loop <list>: <block>
<list>is an expression (subject to{}substitution) that evaluates to an iterable of values — a variable holding a list, e.g.loop {problems}:, a literal, e.g.loop [1, 2, 3]:, or a slice of either, e.g.loop {problems}[6:10:2]:. The elements may be any objects (ints,Problems, …). Only a plain sequence (list,tuple,set,range) spreads into per-iteration values; any other value — an int, a str, a singleProblem(loop {problems}[503]:) — is looped over once, as itself.- The block runs once per element, with the special
loopbound to the current element for the duration of that iteration ({loop}in the body resolves to it; an element's fields are reached with an attribute path, e.g.{loop.number}). The variable shares its spelling with the keyword: bareloop …:at the start of a block is the header,{loop}is the value. - With no header (or an empty list) the block runs once with
loop=None. - The header
:may be omitted when the body boundary is unambiguous — the body is a{ … }group (loop {problems} { init {loop.number} }) or starts on a new line — the lexer inserts the missing:(auto-fix). An inline body on the same line still requires the:(loop [1, 2] echo hiis an error: without it the list expression cannot be told apart from the body). - After the loop,
loopis reset toNoneandrcodeis that of the last executed statement. - Loops do not nest. A
loopheader may only prefix a top-level block; there is a single loop variableloop, so aloopappearing inside another block is a syntax error. (The grammar enforces this — aloop-headeris reachable only fromblock, never from within agrouporstatement.)
Flow-control words
These keywords (classified as Flow, not commands) raise control-flow signals
handled by the run loop:
break— stop the loop; resume after it.continue— skip to the next loop element.exit— end the shell session (or thesolver "<block>"invocation).
On-error shortcuts
A trailing || continue or || break is not lowered to an ordinary
guarded statement; it folds into the preceding statement's on-error action:
cmd || continue— ifcmdfails (rcode != 0), continue the loop.cmd || break— ifcmdfails, break out of the loop.
The default on-error action is ignore. The shortcut applies to the whole
left-hand operand, so cmd1 && cmd2 || break breaks when the cmd1 && cmd2
group fails. exit is not an on-error action — cmd || exit is an ordinary
|| chain whose right operand is the exit flow-control statement.
8. Canonical form
The lexer normalises any block to this shape — a loop header (None when
absent) over a brace group of guarded statements, with nested groups where a
guard applies to more than one statement:
<None | list>:
{
<guard>: <target> = <evaluation> [|| continue | break];
<guard>: { <nested canonical group> } [|| continue | break];
...
}
where guard ∈ { True, {rcode} == 0, {rcode} != 0, <user expression> },
target ∈ { _, <name> }, and the optional || continue / || break clause
carries the on-error action (absent ⇒ ignore).
Parser output
The parser turns the canonical form into
(loop, [Statement(guard, target, evaluation, on_error), …])
loop— the loop list, classified (aVariable/Literal) orNone.guard,evaluation— each classified intoVariable(a lone{name}),Command(first token is a known command),Flow(continue/break/exit),Literal(any other value expression), orBlock(a nested group — theevaluationrecurses into another statement list).on_error—IGNORE/CONTINUE/BREAK.
Variable substitution of {…} is performed by the interpreter, not the
parser; the parser only records where variables and commands occur.
Worked examples
cmd1
─────────────────────────────
None:
{
True: _ = cmd1;
}
cmd1; cmd2
─────────────────────────────
None:
{
True: _ = cmd1;
True: _ = cmd2;
}
cmd1 && cmd2
─────────────────────────────
None:
{
True: _ = cmd1;
{rcode} == 0: _ = cmd2;
}
cmd1 || cmd2
─────────────────────────────
None:
{
True: _ = cmd1;
{rcode} != 0: _ = cmd2;
}
x = {problem.number} + 1
─────────────────────────────
None:
{
True: x = {problem.number} + 1;
}
loop {problems}: init {loop.number} && eval
─────────────────────────────
{problems}:
{
True: _ = init {loop.number};
{rcode} == 0: _ = eval;
}
9. Grammar (EBNF, surface syntax)
block = [ loop-header ] sequence ;
loop-header = "loop" [ expression ] ":" ;
(* a missing ":" is auto-fixed before
a "{…}" body group or newline, §7 *)
sequence = or-expr { ( ";" | NEWLINE ) or-expr } [ ";" | NEWLINE ] ;
or-expr = and-expr { "||" and-expr } ;
and-expr = primary { "&&" primary } ;
primary = group | statement ;
group = "{" sequence "}" ;
statement = assignment | command | expression ;
assignment = name "=" evaluation ;
command = command-name { argument } ;
evaluation = command | expression ;
expression = (* safe-eval subset: literals, comparisons, bool/unary/binary
operators, list/tuple literals, indexing/slicing,
calls to safe builtins, substituted names *) ;
name = lowercase-letter { lowercase-letter | digit | "_" } ;
var-ref = "{" name { "." name } [ ":" format-spec ] "}" ;
(* expanded by the interpreter, §2 *)
argument = word ; (* shlex-tokenised, {}-substituted *)