Parasol’s core grammar
What Parasol reads before a dialect has said anything. A dialect cannot change this; it can only fill in the one hole marked below.
module = { directive } { statement } .
directive = "@use" string "."
| "@infix" operator number ( identifier | "=>" expression ) "."
| "@infixr" operator number ( identifier | "=>" expression ) "."
| "@prefix" operator ( identifier | "=>" expression ) "."
| "@syntax" identifier [ parameters | pattern ] "=>" expression "." .
parameters = "(" [ parameter { "," parameter } ] ")" .
parameter = identifier [ ":" kind ] .
pattern = { hole | identifier } .
hole = "<" identifier [ ":" kind ] ">" .
kind = "expression" | "name" | "literal" | "block" | "place" .
statement = include | expression [ "." ] .
include = "@include" string "." .
expression = infix [ ":=" expression ] .
infix = unary { operator unary } . (* precedence: declared *)
unary = operator unary | postfix . (* prefix: declared *)
postfix = primary { ":" identifier [ arguments ] } .
arguments = "(" [ expression { "," expression } ] ")" .
primary = integer | float | string | symbol
| form
| identifier [ "(" expression ")" ]
| group | array | dictionary | block .
form = identifier [ "(" [ expression { "," expression } ] ")" ] (* a call *)
| identifier { expression | identifier } . (* a pattern *)
group = "(" [ expression { "." expression } ] ")" .
array = "[" [ expression { "," expression } ] "]" .
dictionary = "#[" [ pair { "," pair } ] "]" .
pair = key "=" expression .
key = infix, stopping at a top-level "=" .
block = "{" [ parameters ] [ temporaries ] body "}" .
parameters = identifier { "," identifier } "|" .
temporaries = "|" [ identifier { "," identifier } ] "|" .
body = [ expression { "." expression } [ "." ] ] .
The holes are infix, unary and form. Which spellings are operators,
what they group into and how tightly, and which names are forms, comes from the
module’s own directives. Everything else on this page is the same for every
Parasol file there will ever be.
form is tried before prefix application, and only for a name the header
has already declared. A name that is not a form is whatever Solveig says it is,
so f(x) is x:f until some line above it says otherwise.
A pattern’s shape comes from its declaration, which is why form above
cannot say more than expressions and identifiers in some order: which
positions are holes and which are literal words is what @syntax settled. The
words in a pattern are not reserved – then is a form’s word in a file that
declared one and an ordinary name in every other, this one included.
A dictionary’s = is the separator and not an operator, and this is the one
place in Parasol where a context outranks a declaration. #[k = v] reads as a
pair in a module that declared = and in one that did not. Solveig settles the
same ambiguity by parsing a key at its sum level, which is below where =
lives; Parasol cannot copy that, because a dialect may declare = at any
precedence and lib/clike.psol puts it at 10.
The shadowing is confined to the top level of a key. One bracket down the declaration is back:
@infix = 40 equals.
#[(b = c) = d] is #[(b:equals(c)) = d]
A statement separator is a . between two, optional after the last — in a
file, in a block and in a group alike. That is Solveig’s rule and Parasol does
not have a second one.
Tokens
| name | [A-Za-z_][A-Za-z0-9_]* |
| integer | # then an optional - then digits, $ and hexadecimal digits, or % and binary digits |
| float | digits, optionally a . and more digits, optionally e/E with an optional sign and more digits |
| string | "…", with \" \\ \n \t \r and no other escape |
| symbol | ' and then a name |
| directive | @ and then a name |
| operator | one or more of + - * / < > = ! & ^ % ~ ? \, or ||, or a lone | — except a % immediately before 0 or 1, which begins a binary integer |
| comment | ; to the end of the line |
Everything but operator is Solveig’s own spelling in shape, and not quite
in full. POSTMORTEM.md 16 found nine differences and 0.11.0
closed five of them — the sign on an integer, hexadecimal, float exponents, and
the escape set, which Parasol had been more permissive about and was therefore
emitting Solveig that solas rejected.
Two remain, and neither is an oversight. @expr is refused on purpose. And
-3 cannot be had:
Solveig’s scanner gives the sign to the number outside a @expr region and
treats it as the operator inside one, and Parasol can have neither half — it has
no regions, and taking -3 as a literal would stop a -3 being a subtraction
in every dialect that declares -. A signed integer is safe for the reason
the lexer gives: nothing but an integer can begin with #.
The header is where the two are meant to differ, and nowhere else.
|, :, . and , are not operator characters and cannot become any. |
is what tells a block’s parameters from its body, and the rest are the core
syntax of a send, a statement and an argument list. A dialect gets the
characters that mean nothing until it says so — \ is in the list, and is what
a dialect may write for a bitwise or, though since 0.14.0 | itself is
available and is what this repository uses.
|| is a token even though | is not, taken by the lexer before the bar
and belonging to every dialect rather than to any declaration. It is two bars
and not a bar: a block looks for a lone bar in each of the three places it
allows one, so { a | b } and { x | | t | t } keep their single reading and
{ a || b } was an error before the token existed. The one thing it cost is
{ || … }, which used to be an empty list of temporaries; that is { | | … }
now. This is where the C-like dialect gets || without the lexer becoming
something a file can change.
Operator characters run together as far as they go. a<=b is one operator
<= and not < then =, which is what lets a dialect declare <= without <
having to stop existing. := is taken before any of this and is always itself.
What a dialect file is
A .psol holding directives and nothing else, reached with @use "name.psol".
and read into the header of whoever used it. A statement in one is an error.
A dialect provides syntax; Solveig’s own @include provides code. So there
is no third thing for a .psol to be, and a dialect that wants both ships a
.sol beside itself.
Looked for beside the file using it, then in each -I directory, then in
PARASOL_PATH — the order @include uses over in Solveig.
Read once. Two dialects that both use a third meet it once, so a diamond costs nothing and its declarations do not collide with themselves. A file still being read is a cycle, and is an error rather than a silent stop: the silent stop terminates and then reports the operators as undeclared, which is true and no help at all.
Nesting is limited to 64, the depth Solveig allows an @include.
What happens when two of them declare one spelling is in PARASOL.md, under When two dialects collide.
Patterns
A pattern begins with a word. Forced: a reader finds a form by seeing a name it knows, so a pattern beginning with a hole would put it back to guessing.
Two holes may sit in a row when the second one is a block. A block is a
primary, consumed only where an operand may start, so an expression always stops
at the { and the split is exactly where a reader would put it. Any other pair
is refused – not for ambiguity but for greed: given <a> <b> and f x + y
the first hole takes the sum and the second finds nothing.
Several forms may share a leading word, and are matched together:
@syntax if <c> then <a> => c:ifTrue({ a }).
@syntax if <c> then <a> else <b> => c:ifElse({ a }, { b }).
No backtracking, and none needed. A hole is parsed once and shared by every
candidate still standing, so two forms can only part company at a word – here,
after the second hole, where one has ended and the other wants else. Each step
either reads an expression or looks at one token.
Which holds because the declaration refuses any pair that would part company anywhere else. Two forms under one word whose first difference is a hole against a word cannot be told apart at all, and are an error at the second declaration rather than a rule at every use.
A name is either a call or patterns, never both.
Which shape a form should have
A pattern for something that reads as a step; a call for something that reads as an application. Not a style preference — the shapes behave differently, and choosing wrongly is silent.
A pattern’s trailing hole takes an expression, and an expression continues through sends and infix operators. So nothing written after such a form can apply to the form’s result:
@syntax at <s> => src:looksLike(s).
at "*" || at "/" is at ("*" || (at "/"))
A call ends at its closing parenthesis, so anything after it applies to what it answered:
@syntax at(s) => src:looksLike(s).
at("*") || at("/") is src:looksLike("*"):or({ src:looksLike("/") })
The test is whether the answer is used. store <s> slot <n> is a step whose
result nobody looks at, and its trailing hole ends at the . that ends the
statement. at(s) answers a boolean somebody tests. A form whose trailing part
is a word or a delimited hole has no such question — if <c> <t: block> ends
at the block.
Both programs in programs/ got this wrong once, which is why it is written
here rather than only in their notes.
What a hole will accept
<body: block>, and the same after a comma in the call shape. Five kinds, all
decided by looking at what was parsed:
expression |
anything, and what a hole means when it says nothing |
name |
a bare identifier |
literal |
an integer, a float, a string or a symbol |
block |
{ ... } |
place |
something that may be assigned to: a name, or a slot |
Saying nothing goes on meaning expression, and has to: a form written
before kinds existed must keep working.
A hole asks for what the template does not supply. while <t> do <b> puts
the braces on itself, so b takes an expression and a <b: block> there would
refuse the correct spelling. repeat <n> times <b: block> hands its hole
straight to Solveig’s repeat without wrapping, so that one has to ask.
Checked after the argument is expanded, so a hole filled by another form is checked against what that form became rather than against a use of it.
An operator’s template
An operator names either the message it becomes or, after =>, a template it
stands for. The template is the only way to declare an operator whose
right-hand side must not always be evaluated, a message receiving its argument
already evaluated.
The operands are left and right, and a prefix operand is operand. A
template binding one of those names is refused where it is written.
An operator with a template is a form, registered as one, so hygiene, the expansion trail and everything else arrive from the expander rather than from a second implementation.
What a form may reach
A form’s template is read under the header as it stood at its own line. It
may use the operators declared above it and the forms declared above it, and it
cannot see what comes after — inside the header as much as after it, and across
a @use as much as within one file.
That is not a restriction for tidiness. It is why expansion terminates. Expanding form N yields uses of forms below N, so the highest index strictly falls and no form can reach itself, however the declarations are arranged. There is no recursion to limit and no counter deciding when to give up.
A template may not bind a name that is already one of its parameters.
@syntax f(t) => { | t | t:add(#1) }.
The t inside is two things at once — the argument the caller passed, and the
block’s own temporary — and no rule about which wins is a rule anybody should
have to know. Refused at the declaration, where the author is.
Everything else a template binds is renamed at every expansion, to a name nothing in the module uses. See Hygiene in PARASOL.md for what that does and does not buy.
What settles a block
The leading | of a temporary list, exactly as in Solveig:
{ a | a }. ; -- one parameter
{ | a | a }. ; -- no parameters, one temporary
{ a | | t | t }. ; -- one parameter and one temporary
{ a:print }. ; -- neither: a body that happens to start with a name
This is the one place the reader looks ahead, and it looks ahead over names and commas only.
Precedence against the core
A send binds tighter than any operator, and an assignment looser than all of
them. Neither is negotiable, because neither is an operator: : and := are
core syntax and a dialect never sees them.
a := #1 + #2:negated is a := #1:add(#2:negated)
a := ~b + c is a := b:not:add(c)
So #3 + #4:negated is a sum whose second operand was negated, and a dialect
cannot make it anything else.