The language¶
This page describes Rhema as the compiler implements it, construct by construct. The tutorial introduces the same material in order of use; the grammar at the end summarises the syntax.
Files¶
A Rhema file is a sequence of declarations, in any order, encoded as UTF-8
and named with the extension .rhm. A file does one or more of three
things:
- declares questions: a start symbol, a prompt and an answer, at the top
level (the question
main) or inquestionblocks; - declares sets: whole assignments built from questions;
- provides productions and attributes for other files to
includeorimport.
A file with neither questions nor sets is a library: it can be checked,
included and imported, but not run. polynomial_core.rhm and
matrix_core.rhm in the standard library are libraries.
Lexical structure¶
Comments run from # to the end of the line.
Identifiers are a letter or underscore followed by letters, digits and
underscores. By convention nonterminals start with a capital letter
(E, Q) and attributes, randoms and lets do not; the language does not
require it.
Integers are decimal digits. A minus sign is an operator, except where a
declaration takes a signed integer: the value in attribute, starting
values in question blocks, and the draws of a set.
Strings are written in double quotes and may span lines; the line breaks
are part of the string. The only escapes are \" (a quote) and \\ (a
backslash). Any other backslash is kept, so LaTeX is written directly:
"\cdot" and "\\cdot" are the same string.
Keywords are reserved:
answer as attribute boolean context copy import include left
let nonassoc on precedence prompt question random require right
set split start synth weight when width and or not
Inside a set block, the words part, of, any, in, spread, low,
allowed, distinct, balanced and ordered have a meaning, but they are
not reserved and may be used as names elsewhere.
Attributes¶
Inherited attributes¶
attribute NAME = INTEGER [copy | split]
declares an attribute whose value travels down the tree. INTEGER is its
value at the root of a question, and must not be negative. The distribution
says how a node passes it to its children:
copy(the default)- every child that is a symbol of the grammar receives the node's value;
split- the node's value is divided between those children, every division into non-negative whole numbers being equally likely.
Leaves (generators and quoted words) receive nothing. A child that is a
module symbol (alias.F) counts as a symbol of the grammar.
The starting value can be changed from outside, with --set NAME=VALUE on
the command line, an INTEGER in a question block, or a draw in a set.
Synthesized attributes¶
synth NAME
synth NAME on SYMBOL
declares an attribute whose value travels up the tree, computed at a node
from its children. on SYMBOL restricts it to one nonterminal; without it,
every nonterminal must compute it. The same name may be declared on several
symbols, but not both with and without on.
Every production for a symbol must compute every synthesized attribute that
symbol carries. A generator leaf carries one attribute, value, the number
it drew; a quoted word carries value, its text.
Two names are reserved for the start symbol: pg_context, the WeBWorK
context of each question, and pg_width, the width of its answer blanks.
When the start symbol computes them, they override the file's context and
width for that question.
Productions¶
LHS -> SYMBOL ... [when EXPR] [weight INTEGER] { EQUATION ... }
A production says that the nonterminal LHS may be made of the symbols on
the right. The braces are required, even when empty.
Each symbol on the right is one of:
| Written | Meaning |
|---|---|
E |
a nonterminal, if the file has productions for it; otherwise a terminal that stands for its name |
+ - * / % ^ |
a terminal that stands for the operator |
"text" |
a terminal that stands for the text |
integer_between(1, 9) |
a generator: a leaf that draws a number (see builtins) |
calc.F or calc.F(cost = cost - 1) |
a nonterminal of an imported module, with starting values for its inherited attributes |
$1, $2, ... refer to these symbols, counting from 1.
when EXPR is the production's guard. The production may be chosen only
when it is true. It is evaluated before the children exist, so it may use
inherited attributes and question-level values, but not $n.
weight INTEGER (a positive integer, 1 by default) is the production's
relative chance of being chosen among those whose guards hold.
Equations¶
The body holds equations and requirements, separated by line breaks or commas:
NAME = EXPR, whereNAMEis inherited- An update, applied before the children are built: the value is added
to the attribute. It must not make the budget negative, and a positive
constant is refused, because an update spends a budget.
cost = -2spends 2. NAME = EXPR, whereNAMEis synthesized- Computes the attribute, after the children are built. An equation may use an attribute computed earlier in the same body by its bare name.
require EXPR- A condition checked after the children are built and the equations above it are computed. When it is false, the node is built again.
Updates are applied in the order written; equations and requirements are evaluated in the order written.
Expressions¶
| Form | Meaning |
|---|---|
12, "text" |
a number, a string |
NAME |
an inherited attribute of the node, a random or let, or an attribute computed earlier in the same production |
$2.NAME |
an attribute of the second child |
$2 |
the second child itself; only as an argument of infix and prefix |
S.NAME |
an attribute of the start symbol S; only in prompt and answer |
alias.NAME |
a random or let of an imported module |
f(e, ...) |
a builtin function |
-e, not e |
negation |
e + e, -, *, /, %, ^ |
arithmetic on numbers |
e ++ e |
joining text |
e = e, !=, <, <=, >, >= |
comparison |
e and e, e or e |
logic |
Numbers are exact rationals: 1 / 3 is one third, not 0.333. Comparisons
of text compare the strings; = and != on truth values are the
biconditional and exclusive or.
Operators bind, from loosest to tightest: or; and; not; the
comparisons (which do not chain); ++ (right-associative); + -;
* / %; ^ (right-associative); unary -. Brackets group.
Every value has a type: number, text, truth, formula, polynomial, equation, Boolean formula, matrix, set, vector or complex number. Types are checked when a value is used; a function given the wrong type stops generation with a message naming both.
Question-level values¶
random NAME = boolean
random NAME = GENERATOR(INTEGER, ...)
let NAME = EXPR
A random is drawn once per question, before the tree is built, and has the
same value wherever it is used. Its arguments must be integer literals.
A let is computed once per question, after the randoms, from randoms and
earlier lets. It may call any builtin function but may not refer to the
tree.
Precedence and rendering¶
precedence LEVEL left|right|nonassoc OPERATOR ...
gives operators a binding strength (higher binds tighter) and an
associativity. A production that has an operator among its symbols takes
that operator's level. The builtins infix($1, "+", $3) and
prefix("\neg", $2) write their operands, adding brackets exactly where the
levels and associativity require them. The operator's text need not be the
symbol: E -> E * E may be written with "\cdot".
Prompt, answer, context, width¶
prompt STRING
answer EXPR
context STRING
width INTEGER
The prompt is PGML with these replacements:
| Written | Becomes |
|---|---|
{S.NAME} |
an attribute of the start symbol S |
{NAME} |
a random or let |
{alias.NAME} |
a random or let of an imported module |
{answer S.NAME}, {answer NAME} |
an answer blank graded against that value |
{{, }} |
literal braces |
answer EXPR adds one blank after the prompt; it may use S.NAME and module
values. A question has either answer or {answer ...} blanks, not both.
context names the WeBWorK context (default Numeric); width sets the
width of every blank.
Questions¶
question NAME {
start SYMBOL
prompt STRING
[answer EXPR]
[context STRING]
[width INTEGER]
[ATTRIBUTE = INTEGER] ...
}
declares a named question over the file's productions. The top-level
start, prompt, answer, context and width form the question main.
A block without context or width takes the file's. ATTRIBUTE = INTEGER
starts an inherited attribute at that value for this question. A file of
blocks only has no main; its first block is its default question.
main cannot be used as a block's name.
include and import¶
include "FILE.rhm"
import "FILE.rhm" as ALIAS
include merges the other file's declarations into this one before
checking. Productions, attributes, randoms, lets and precedences add up;
identical declarations in both files are kept once. Of the declarations a
file makes once (start, prompt, answer, context, width), the
including file's own win. Among inherited attributes, the including file's
declaration wins, then the first included one.
import loads the other file as a separate module, checked on its own. Its
nonterminals, randoms, lets and questions are reached as ALIAS.NAME; its
synthesized attributes are read through $n as usual; its precedences and
productions are not visible. A module is loaded once per run however many
files import it, and its randoms are drawn once per question. A module's own
imports are private to it. A module subtree is atomic to the importer's
infix: no brackets are added around it.
Both look for the file beside the file that names it, then in -I
directories, RHEMA_PATH and the installed library. A file that includes or
imports itself, directly or through others, is refused.
Sets¶
set NAME {
part "HEADING"
COUNT of QUESTION(DRAW, ...)
COUNT of any(QUESTION(DRAW, ...), ...)
distinct
balanced
ordered
}
QUESTION is main, a question of this file, or ALIAS.NAME. Each DRAW
starts one inherited attribute of that question:
| Draw | Meaning |
|---|---|
cost = 6 |
every question starts at 6 |
cost in {8, 10, 12} |
values taken in turn, each once before any repeats |
cost ~ 6 [spread 2] [low 2] [allowed {4, 6}] |
drawn around the mean and adjusted so the entry's mean is exactly 6 (or as close as allowed permits); spread defaults to 2, low to 0 |
part lines group the entries that follow under one number. distinct
refuses a question of the same type and tree shape as an earlier one;
balanced refuses a third equal coded answer in a row; ordered sorts each
part by total budget.
How a question is generated¶
- The paper's variant is derived from the assignment's name, and from the student's when one is given; it fixes every random choice below.
- The randoms of the file and of every module it imports are drawn; then the lets are computed in order.
- From the start symbol, with the inherited attributes at their starting
values, each node is built:
- the productions for the symbol whose guards hold, and whose updates
leave every budget non-negative, are the candidates; if there are none,
generation stops with
no rule for 'E' fits {...}; - one candidate is chosen in proportion to its weight;
- its updates are applied and the inherited attributes distributed to the children, which are built in turn;
- its equations and requirements are evaluated in order. A failed requirement builds the node again, with fresh choices, up to 40 times; after that the failure passes to the node above, which tries again in the same way. A question that still fails is started afresh, up to 200 times, before generation gives up and names the requirement that failed most often.
- the productions for the symbol whose guards hold, and whose updates
leave every budget non-negative, are the candidates; if there are none,
generation stops with
- The prompt, blanks and context are read from the finished tree.
When a failure could not have gone differently, because no random choice was involved, it is reported at once instead of retried.
What the checker guarantees¶
rhema check accepts a file only if:
- it parses, and every name it uses is declared: symbols, attributes, randoms, lets, builtins, aliases, questions, modules' names;
- every production computes every synthesized attribute its symbol carries, and an update is never a positive constant;
- guards and updates use only what exists before the children are built; equations and requirements use children only within range and only attributes those children carry;
- every builtin and generator is called with the right number of arguments, and random draws have literal arguments;
- every nonterminal can finish (some production for it uses only symbols that can finish), and every cycle of productions spends an inherited attribute, so no derivation can go round for ever without spending;
- prompts, answers, questions and sets refer to things that exist, with the right kinds of blank;
- no file includes or imports itself.
What the checker cannot know is left to generation, and reported clearly when it happens: a budget no production can spend, a requirement that cannot be met, a value of the wrong type for a function.
Grammar¶
The syntax in EBNF. { X } means zero or more, [ X ] optional, |
alternatives; quoted text is literal, capitalised names are tokens.
file = { declaration } ;
declaration = "attribute" IDENT "=" sint [ "copy" | "split" ]
| "random" IDENT "=" ( "boolean" | IDENT "(" [ exprs ] ")" )
| "let" IDENT "=" expr
| "synth" IDENT [ "on" IDENT ]
| "start" IDENT
| "context" STRING
| "precedence" INT ( "left" | "right" | "nonassoc" ) operator { operator }
| "prompt" STRING
| "answer" expr
| "width" INT
| "include" STRING
| "import" STRING "as" IDENT
| "question" IDENT "{" { qitem } "}"
| "set" IDENT "{" { sitem } "}"
| production ;
production = IDENT "->" symbol { symbol } [ "when" expr ] [ "weight" INT ]
"{" { equation [ "," ] } "}" ;
symbol = IDENT | IDENT "(" [ exprs ] ")"
| IDENT "." IDENT [ "(" [ IDENT "=" expr { "," IDENT "=" expr } ] ")" ]
| STRING | "+" | "-" | "*" | "/" | "%" | "^" ;
equation = IDENT "=" expr | "require" expr ;
qitem = "start" IDENT | "context" STRING | "prompt" STRING
| "answer" expr | "width" INT | IDENT "=" sint ;
sitem = "part" [ STRING ] | "distinct" | "balanced" | "ordered"
| INT "of" choice ;
choice = squestion | "any" "(" squestion { "," squestion } ")" ;
squestion = [ IDENT "." ] IDENT [ "(" [ draw { "," draw } ] ")" ] ;
draw = IDENT "=" sint
| IDENT "in" "{" sint { "," sint } "}"
| IDENT "~" sint { "spread" sint | "low" sint | "allowed" "{" sint { "," sint } "}" } ;
expr = INT | STRING | IDENT | IDENT "." IDENT | "$" INT | "$" INT "." IDENT
| IDENT "(" [ exprs ] ")" | "(" expr ")"
| "-" expr | "not" expr | expr binop expr ;
exprs = expr { "," expr } ;
binop = "or" | "and" | "=" | "!=" | "<" | "<=" | ">" | ">="
| "++" | "+" | "-" | "*" | "/" | "%" | "^" ;
operator = "+" | "-" | "*" | "/" | "%" | "^" | STRING ;
sint = [ "-" ] INT ;