formal grammar for shlcpp. EBNF | is alternation, { x } is zero or more,
[ x ] is optional, ( x ) is grouping. terminals are quoted or CAPS.
this describes the language, not the implementation. the parser does an indentation pre-pass then parses per line, but the grammar below is what it accepts.
NEWLINE ::= '\n' (* statement terminator *)
INDENT ::= (* emitted when indentation increases *)
DEDENT ::= (* emitted when indentation decreases *)
COMMENT ::= '#' { any char except '\n' }
| '/*' { any char } '*/' (* shortest match *)
NUMBER ::= digits [ '.' digits ] [ exponent ] (* one type: 64-bit float *)
| '0' ( 'x' | 'X' | 'b' | 'B' | 'o' | 'O' ) alnum { alnum }
exponent ::= ( 'e' | 'E' ) [ '+' | '-' ] digits
digits ::= digit { digit }
letter ::= 'A'..'Z' | 'a'..'z'
digit ::= '0'..'9'
alnum ::= letter | digit
STRING ::= '"' { string_char } '"'
| '\'' { string_char } '\''
string_char ::= escape | any char except the delimiting quote and '\'
escape ::= '\n' | '\t' | '\\' | '\"' | "\'" | ...
IDENT ::= ( letter | '_' ) { letter | digit | '_' }
keyword ::= (* any word from the keyword list *)
the lexer drops blank lines and comment-only lines before parsing.
keywords (natural word first, programmer alias after):
'to' | 'can' | 'def' function definition
'give' | 'return' return
'say' | 'show' | 'print' print line (all three identical)
'esay' print line to stderr
'thing' | 'class' | 'structure'
'make' | 'new' instantiation
'has' field declaration
'extends' inheritance
'yes' | 'true' boolean true
'no' | 'false' boolean false
'null' null literal
'if' | 'elif' | 'else'
'while' | 'until' | 'repeat' | 'times' | 'forever' | 'for' | 'in' | 'each'
'stop' | 'skip' | 'pass'
'unless' | 'check' | 'when' | 'otherwise' | 'do'
'try' | 'catch' | 'always' | 'finally'
'error' | 'throw'
'and' | 'or' | 'not'
'spawn' | 'await' | 'parallel'
'import' | 'from' | 'as' | 'use'
'self' | 'super' | 'del' | 'is' | 'be'
'at' | 'least' | 'exactly' | 'less' | 'more' | 'than'
'matches' | 'fn' | 'lambda'
'the' | 'let' | 'please' (* noise words, skipped by the lexer *)
'finally' is a lexer alias for 'always', 'throw' for 'error'. 'unless', 'when' and 'do' are reserved but have no productions yet.
program ::= stmt_list EOF
stmt_list ::= { stmt }
stmt ::= simple_stmt ( NEWLINE | EOF ) | compound_stmt
block ::= NEWLINE INDENT stmt { stmt } DEDENT
header ::= [ ':' ] (* trailing colon optional *)
simple_stmt ::= expr_stmt
| 'give' [ expr ] (* 'return' *)
| say_stmt
| 'check' expr
| 'stop' | 'skip' | 'pass'
| 'error' expr (* 'throw' *)
| del_stmt
| import_stmt
expr_stmt ::= assign_expr (* assignment is statement-level *)
say_stmt ::= say_kw { style_word } expr
say_kw ::= 'say' | 'show' | 'print' | 'esay'
style_word ::= 'in' | 'red' | 'green' | 'blue' | 'yellow'
| 'cyan' | 'magenta' | 'white' | 'black'
| 'bold' | 'italic' | 'underline'
target ::= postfix_expr (* a bare IDENT or 'self', or a postfix
chain ending in index or property;
never a call or literal *)
del_stmt ::= 'del' target
import_stmt ::= 'import' IDENT { '.' IDENT } [ 'as' IDENT ]
| 'from' IDENT { '.' IDENT } 'import' ( '*' | IDENT { ',' IDENT } )
| 'use' STRING
compound_stmt ::= if_stmt | while_stmt | until_stmt | repeat_stmt
| forever_stmt | for_stmt | func_def | class_def
| try_stmt | match_stmt | parallel_stmt
if_stmt ::= 'if' expr header block
{ 'elif' expr header block }
[ 'else' header block ]
while_stmt ::= 'while' expr header block
until_stmt ::= 'until' expr header block
repeat_stmt ::= 'repeat' expr 'times' header block
forever_stmt ::= 'forever' header block
for_stmt ::= 'for' [ 'each' ] IDENT [ ',' IDENT ] 'in' expr header block
func_def ::= ( 'to' | 'can' | 'def' ) IDENT '(' [ params ] ')' header block
params ::= param { ',' param } [ ',' ]
param ::= IDENT [ '=' expr ]
class_def ::= ( 'thing' | 'class' | 'structure' ) IDENT
[ 'extends' IDENT ] header
NEWLINE INDENT class_member { class_member } DEDENT
class_member ::= 'has' IDENT [ '=' expr ] NEWLINE
| func_def
| 'pass' NEWLINE
try_stmt ::= 'try' header block
( 'catch' IDENT header block [ 'always' header block ]
| 'always' header block )
match_stmt ::= 'matches' expr header NEWLINE INDENT
{ 'is' expr header block }
[ 'otherwise' header block ]
DEDENT
parallel_stmt ::= 'parallel' header block
precedence, low to high. all levels left-associative except assignment and '**', which are right-associative.
expr ::= or_expr
assign_expr ::= target assign_op assign_expr | or_expr
assign_op ::= '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | 'be'
| '//=' | '**=' | '<<=' | '>>='
(* right-assoc; compound ops desugar to x = x op v;
'be' is the alias in 'let x be 10' *)
or_expr ::= and_expr { 'or' and_expr }
and_expr ::= not_expr { 'and' not_expr }
not_expr ::= 'not' not_expr | bitor_expr
bitor_expr ::= bitxor_expr { '|' bitxor_expr }
bitxor_expr ::= bitand_expr { '^' bitand_expr }
bitand_expr ::= equality_expr { '&' equality_expr }
equality_expr ::= compare_expr { ( '==' | '!=' ) compare_expr }
compare_expr ::= shift_expr { comp_op shift_expr | is_tail }
comp_op ::= '<' | '<=' | '>' | '>=' | 'in' | 'not' 'in'
is_tail ::= 'is' ( 'at' 'least' shift_expr
| 'exactly' shift_expr
| 'less' 'than' shift_expr
| 'more' 'than' shift_expr
| 'not' 'in' shift_expr
| 'in' shift_expr
| 'not' shift_expr
| shift_expr ) (* bare 'is' means '==' *)
shift_expr ::= add_expr { ( '<<' | '>>' ) add_expr }
add_expr ::= mul_expr { ( '+' | '-' ) mul_expr }
mul_expr ::= pow_expr { ( '*' | '/' | '//' | '%' ) pow_expr }
pow_expr ::= unary_expr [ '**' pow_expr ] (* right-assoc *)
unary_expr ::= ( '-' | '~' | 'await' | 'spawn' ) unary_expr | postfix_expr
postfix_expr ::= primary { postfix_op }
postfix_op ::= '(' [ args ] ')' (* call *)
| '[' expr ']' (* index *)
| '[' [ expr ] ':' [ expr ] [ ':' [ expr ] ] ']'
| '.' ( IDENT | keyword ) (* property; keywords work as names *)
primary ::= NUMBER | STRING | 'yes' | 'no' | 'null'
| IDENT | 'self'
| '(' assign_expr ')' (* parens opt back into assignment *)
| list_lit | dict_lit | lambda
| 'make' IDENT '(' [ args ] ')' (* 'new' *)
| 'super' '(' [ args ] ')' (* parent init, subclass only *)
| comprehension
list_lit ::= '[' [ expr { ',' expr } [ ',' ] ] ']'
dict_lit ::= '{' [ expr ':' expr { ',' expr ':' expr } [ ',' ] ] '}'
lambda ::= ( 'fn' | 'lambda' ) [ IDENT { ',' IDENT } ] '=>' expr
args ::= arg { ',' arg } [ ',' ]
arg ::= IDENT '=' expr | expr
comprehension ::= '[' expr 'for' IDENT 'in' expr [ 'if' expr ] ']'
(* loop variable is a single IDENT; 'for k, v' doesn't work here *)
notes:
istails: bareisis==,is notis!=,is inisin, the rest are the obvious comparison. alternatives are tried in order, sois notalways means!=.even,odd,prime,positiveandnegativeare ordinary identifiers; there are noistrait forms.**binds tightest of the binary ops and is right-associative:2**3**2is2**(3**2)= 512. unary binds tighter than**, so-2**2is(-2)**2= 4, unlike python.- comparisons are left-associative with no python-style chaining:
1 < x < 10parses as(1 < x) < 10. - assignment is right-associative and returns the value. it is statement-level
only, plus explicit parens
(x = 5). notbinds tighter thanand/orbut looser than comparisons.-not xdoesn't parse; writenot -xor-(not x).- newlines are ignored inside
(),[]and{}. - a
[literal is a comprehension iffforfollows the first expression. supercan only invoke the parent init; an overriding method can't call the parent's version (the runtime rejectssuper.method()).x.f(a)parses as property access followed by a call; the parser special-cases it into a method call.keywordin postfix_op is any reserved word:s.matches(re),obj.errorand friends parse.assign_exprtriestarget assign_opfirst, falling back toor_expr.- the lexer validates digits per radix (
0xhex,0bbinary,0ooctal). - a missing final NEWLINE is tolerated; EOF ends the last statement.
make Dog("rex")instantiates;initruns automatically and returns self.say,show,printare identical;esayis the stderr variant. Style words (say in red bold "hi") are consumed before the expression;say "hi" in redparses as the comparison("hi" in red), not styling. Style words shadow identifiers in say position:say redcannot print a variable namedred.
places where the implementation disagrees with the grammar above.
notbinds at unary level, tighter than comparisons.-not xparses (as-(not x)).- assignment is accepted in any expression position, not just statements.
awaitandspawntake a full expression operand.- a bare
tryblock with nocatchoralwaysis accepted.