Eskiueskiu v0.7.0

Eskiu Grammar

A formal grammar for Eskiu, reflecting the actual recursive-descent parser (parser/parser.cpp) and lexer (lexer/lexer.cpp). It is the authoritative syntax reference; the prose in spec.md explains semantics.

Notation (EBNF):

Form Meaning
x? optional
x* zero or more
x+ one or more
x (',' x)* comma-separated list
a \| b alternation
'tok' literal token
UPPER a token class (terminal); see Lexical
lower a non-terminal

The grammar is presented top-down: lexical structure, then the preprocessor, then declarations, types, statements, and expressions (lowest precedence first).


Lexical structure

Tokens are produced after the preprocessor pass. Whitespace and comments separate tokens and are otherwise insignificant.

IDENT      = [A-Za-z_] [A-Za-z0-9_]*
INT_LIT    = [0-9]+  |  '0x' [0-9A-Fa-f]+
FLOAT_LIT  = [0-9]+ '.' [0-9]+
STRING_LIT = '"' ( escape | not('"') )* '"'        // adjacent literals concatenate: "a" "b" == "ab"
CHAR_LIT   = "'" ( escape | not("'") ) "'"
escape     = '\' ( 'n' | 't' | 'r' | 'f' | 'v' | '\' | '"' | "'" | '0'    // string and char share the set
                 | 'x' HEX HEX? )                                        // raw byte, 1-2 hex digits

Comments: // … end-of-line and /* … */ (block comments do not nest).

Keywords (reserved)

let  const  volatile  static  async  await  escaping  must_use
int int8 int16 int32 int64  uint uint8 uint16 uint32 uint64
float double bool char string void
struct packed union interface enum fn
if else while do for in switch match case default break continue return
import extern intrinsic  sizeof alloc_with free_closure
thread_create thread_join  asm  try catch finally throw  defer errdefer
null true false

type is not reserved: it is a contextual keyword recognized only in the type-alias form type NAME = ….

Operators and punctuation

+  -  *  /  %                          arithmetic
++  --                                 increment / decrement (prefix or postfix)
=  +=  -=  *=  /=  %=  &=  |=  ^=  <<=  >>=   assignment (compound forms desugar)
==  !=  <  >  <=  >=                   comparison
&&  ||  !                             logical
&  |  ^  ~  <<  >>                     bitwise
( ) { } [ ]  ;  ,  .                  delimiters
..   ...   :   ->   ?                  range, ellipsis, colon, arrow, error-propagation

Preprocessor

A text pass run before tokenizing. Directives occupy a whole line; skipped or directive lines become blank lines so source line numbers are preserved.

directive =
    '#define' IDENT macro-body
  | '#define' IDENT '(' (IDENT (',' IDENT)*)? ')' macro-body    // function-like
  | '#undef'  IDENT
  | '#ifdef'  IDENT  |  '#ifndef' IDENT  |  '#else'  |  '#endif'
  | '#pragma' …                          // passed through to the compiler (e.g. pack)
  | '#error'  text                       // aborts compilation on an active branch
  | '#!' …                               // shebang: ignored (see __FILE__/__LINE__ ref)

Predefined macros: __FILE__, __LINE__, a host-OS macro (__APPLE__/__linux__), and __ESKIU_FREESTANDING__ under --freestanding.


Program

program     = item*
item        = import | declaration

import      = 'import' STRING_LIT ';'           // relative path
            | 'import' '<' module-path '>' ';'  // stdlib module

Declarations

declaration =
    function-decl | struct-decl | union-decl | interface-decl | enum-decl
  | type-alias  | extern-decl  | intrinsic-decl | var-decl

function-decl   = ('async' | 'must_use')? type IDENT type-params? '(' param-list? ')' ( block | ';' )
extern-decl     = 'extern'    type IDENT '(' param-list? ')' ';'
intrinsic-decl  = 'intrinsic' type IDENT '(' param-list? ')' ';'

type-params     = '<' type-param (',' type-param)* '>'
type-param      = IDENT ( ':' IDENT ('+' IDENT)* )?   // optional interface constraint(s)

param-list      = param (',' param)* (',' '...')?  |  '...'
param           = 'escaping'? type IDENT

struct-decl     = 'packed'? 'struct' IDENT type-params? '{' struct-member* '}'
struct-member   = type IDENT (':' INT_LIT)? ';'                 // field, optional bitfield width
                | type IDENT '(' param-list? ')' block          // method

union-decl      = 'union' IDENT '{' ( type IDENT ';' )* '}'

interface-decl  = 'interface' IDENT '{' ( type IDENT '(' param-list? ')' ';' )* '}'

enum-decl       = 'enum' IDENT type-params? '{' enum-variant (',' enum-variant)* ','? '}'   // enum type-params are names only (no constraints)
enum-variant    = IDENT ( '(' type (',' type)* ')' )?           // ADT payload
                | IDENT ( '=' '-'? INT_LIT )?                    // classic, optional integer value

type-alias      = 'type' IDENT '=' type ';'

var-decl        = qualifier* 'let' IDENT ':' type ( '=' expr )? ';'
                | qualifier* type IDENT ( '=' expr )? ';'
qualifier       = 'const' | 'volatile' | 'static'

const placement follows C: before a let it qualifies the binding; within a type it qualifies the pointee (const int*) or pointer level (int* const).

static is a storage qualifier valid only on a local variable: the variable has a single instance that persists across calls (like C). Its initializer must be a compile-time constant. Applying static to a global is rejected (a global already has static storage).


Types

type        = '?'? 'const'? ptr* base suffix*      // leading '?' = checked nullable pointer `?*T`
suffix      = array | '*' 'const'?                   // arrays and trailing pointers, any order
base        = scalar-type
            | IDENT ( '<' type (',' type)* '>' )?   // named type or template instance
            | 'fn' '(' (type (',' type)*)? ')' '->' type   // function-pointer type
ptr         = '*'                                    // leading-pointer (spec) spelling
array       = '[' (INT_LIT | IDENT)? ']'             // IDENT = a named const dim; empty = slice `T[]`

scalar-type = 'int' | 'int8' | 'int16' | 'int32' | 'int64'
            | 'uint' | 'uint8' | 'uint16' | 'uint32' | 'uint64'
            | 'float' | 'double' | 'bool' | 'char' | 'string' | 'void'

Pointers may be written either C-style (int*) or leading (*int); both are equivalent. An array suffix binds tighter than a leading pointer, so *T[N] is an array of N pointers (each element a *T), while a pointer to an array is written with the star after the brackets: T[N]*. Suffixes chain for multidimensional arrays: T[N][M] is N arrays of M (C order, leftmost bracket outermost). Empty brackets make a slice: T[] is a fat pointer (data + length), constructed by slicing an array (a[lo..hi]). va_list is a built-in named type used by variadics.


Statements

statement =
    block | if-stmt | while-stmt | for-stmt | switch-stmt | match-stmt
  | do-while-stmt  | return-stmt | break-stmt | continue-stmt | throw-stmt | try-stmt
  | defer-stmt | asm-stmt | thread-join-stmt | var-decl | expr-stmt

block         = '{' ( declaration | statement )* '}'
if-stmt       = 'if' '(' expr ')' statement ( 'else' statement )?
while-stmt    = 'while' '(' expr ')' statement
do-while-stmt = 'do' statement 'while' '(' expr ')' ';'
for-stmt      = 'for' '(' ( var-decl | expr )? ';' expr? ';' expr? ')' statement
              | 'for' '(' IDENT 'in' expr ( '..' expr )? ')' statement   // iterate / half-open range
return-stmt   = 'return' expr? ';'
break-stmt    = 'break' ';'
continue-stmt = 'continue' ';'
throw-stmt    = 'throw' expr ';'
expr-stmt     = expr ';'

switch-stmt   = 'switch' '(' expr ')' '{' (switch-case | default-case)* '}'   // any order, `default` may repeat
switch-case   = 'case' expr ':' statement*
default-case  = 'default' ':' statement*

match-stmt    = 'match' expr '{' match-arm+ '}'
match-arm     = IDENT ( '(' IDENT (',' IDENT)* ')' )? '->' statement   // variant + payload bindings
              | '_' '->' statement                                     // default

try-stmt      = 'try' block ( 'catch' '(' type IDENT ')' block )* ( 'finally' block )?
defer-stmt    = ( 'defer' | 'errdefer' ) statement   // block-exit cleanup, LIFO; errdefer runs only on the `?`-error path
asm-stmt      = 'asm' '(' STRING_LIT ( ':' ':' asm-operand (',' asm-operand)*
                              ( ':' STRING_LIT (',' STRING_LIT)* )? )? ')' ';'
asm-operand   = STRING_LIT '(' expr ')'
thread-join-stmt = 'thread_join' '(' expr ')' ';'

for (i in A..B) desugars to a counted for (int i = A; i < B; i = i + 1).


Expressions

Precedence, lowest to highest. Each level is left-associative unless noted.

expr            = assignment
assignment      = ternary ( assign-op assignment )?              // right-assoc
assign-op       = '=' | '+=' | '-=' | '*=' | '/=' | '%='
                | '&=' | '|=' | '^=' | '<<=' | '>>='              // compound forms desugar
ternary         = logical-or ( '?' expr ':' ternary )?           // right-assoc
logical-or      = logical-and ( '||' logical-and )*
logical-and     = bitwise-or  ( '&&' bitwise-or  )*
bitwise-or      = bitwise-xor ( '|'  bitwise-xor )*
bitwise-xor     = bitwise-and ( '^'  bitwise-and )*
bitwise-and     = equality    ( '&'  equality    )*
equality        = comparison  ( ('==' | '!=') comparison )*
comparison      = shift       ( ('<' | '>' | '<=' | '>=') shift )*
shift           = additive    ( ('<<' | '>>') additive )*
additive        = multiplicative ( ('+' | '-') multiplicative )*
multiplicative  = unary       ( ('*' | '/' | '%') unary )*
unary           = ('!' | '-' | '+' | '&' | '*' | '~' | '++' | '--' | 'await') unary   // right-assoc
                | cast
cast            = '(' type ')' unary
                | postfix
postfix         = primary postfix-op*
postfix-op      = '(' arg-list? ')'          // call
                | '[' expr ']'               // index
                | '[' expr '..' expr ']'     // slice (half-open) → a `T[]` fat pointer
                | '.' IDENT                  // member
                | '?'                        // error propagation (Result)
                | '++' | '--'                // post-increment / decrement
arg-list        = expr (',' expr)*

? is overloaded: cond ? a : b is the conditional (ternary) operator, while a postfix expr? (with no matching : ahead) is the Result error-propagation operator (it returns early on the error variant). The parser disambiguates by scanning for a same-level : after the ?. Assignment is the lowest-precedence, right-associative level; the ternary sits just above it, also right-associative.

primary =
    INT_LIT | FLOAT_LIT | STRING_LIT | CHAR_LIT | 'true' | 'false' | 'null'
  | IDENT
  | array-lit                                          // { e0, e1, … }  (initializes an array)
  | IDENT struct-init                                  // Name { … }
  | IDENT '<' type (',' type)* '>' ( '(' arg-list? ')' | struct-init )  // turbofish call / templated literal
  | '(' expr ')'
  | lambda
  | 'sizeof' '(' type ')'
  | 'alloc_with' '(' expr ',' type ',' expr ')'
  | 'free_closure' '(' expr ')'
  | 'thread_create' '(' expr ')'

array-lit   = '{' ( expr (',' expr)* ','? )? '}'       // fewer elements than the size zero-fill
struct-init = '{' ( field-init (',' field-init)* )? '}'
field-init  = IDENT ':' expr        // named
            | expr                  // positional

lambda      = type '(' lambda-params? ')' block        // e.g. int (int x) { return x; }
lambda-params = ('escaping'? type IDENT) (',' 'escaping'? type IDENT)*

A struct literal is suppressed where a { would instead open a block, e.g. the subject of match/switch/if/while/for conditions.


See also