Reference
Grammar & Operator Precedence
A formal reference for Velo's surface syntax. The grammar is written in EBNF:
=defines a rule, terminated by;|alternatives,[ x ]optional,{ x }zero or more,( x )grouping"lit"a literal token,'c'a literal character- lexical rules are
UPPERCASE, grammar rules arelowerCamel
Expression structure (which operator binds first) is governed by the precedence table at the end, not by the EBNF alone.
Lexical structure
program = { statement [ terminator ] } ;
terminator = NEWLINE | ";" ; (* a newline ends a statement; ";" only
needed to separate statements on one line *)
comment = "#" { any-char-except-newline } NEWLINE ;
IDENT = ( LETTER | "_" ) { LETTER | DIGIT | "_" } ;
LETTER = "a".."z" | "A".."Z" ;
DIGIT = "0".."9" ;
NUMBER = INT_LIT | FLOAT_LIT | HEX_LIT | BIN_LIT ; (* no type suffixes *)
INT_LIT = DIGIT { DIGIT | "_" } ; (* int; auto-long if > 32-bit; widens to float / fits a byte by context *)
FLOAT_LIT = DIGIT { DIGIT | "_" } "." DIGIT { DIGIT } ;
HEX_LIT = "0x" HEXDIGIT { HEXDIGIT | "_" } ;
BIN_LIT = "0b" ( "0" | "1" ) { "0" | "1" | "_" } ;
CHAR_LIT = "'" ( CHARACTER | ESCAPE ) "'" ; (* value is its code point *)
STRING = '"' { STRCHAR | ESCAPE | interpolation } '"' ;
interpolation = "$" IDENT | "${" expression "}" ; (* write \$ for a literal $ *)
ESCAPE = "\" ( "n" | "t" | "\" | '"' | "$" | "'" ) ;Declarations & statements
A statement is any of these forms or a bare expression.
statement = import
| typedDecl | letDecl
| funcDecl | extDecl | operatorDecl
| classDecl | dataDecl | actorDecl | interfaceDecl | enumDecl
| if | when | while | for | return | "break" | "continue"
| try | throw
| expression ;
import = "import" STRING [ "as" IDENT ] ;
(* .vel optional; std/ = stdlib; else relative to file.
`as ns` namespaces members: ns.func(), new ns.Class() *)
typedDecl = type IDENT [ "=" expression ] ; (* explicit type; mutable *)
letDecl = "let" IDENT "=" expression ; (* inferred type; immutable *)
funcDecl = "func" [ IDENT ] [ typeParams ] "(" params ")" type block ;
extDecl = "ext" "(" param ")" IDENT "(" params ")" type block ;
operatorDecl = "operator" operatorName "(" params ")" type block ;
operatorName = OPERATOR | "[" "]" [ "=" ] ; (* e.g. +, ==, [], []= *)
classDecl = "class" IDENT [ typeParams ] "(" params ")" [ ":" typeList ] block ;
dataDecl = "data" classDecl ; (* immutable value type *)
actorDecl = "actor" classDecl ; (* concurrent, message-driven *)
interfaceDecl = "interface" IDENT block ; (* body holds method signatures *)
enumDecl = "enum" IDENT "{" { variant [ terminator ] } "}" ; (* closed sum type *)
variant = IDENT [ "(" params ")" ] ; (* each variant is a value-type record *)
if = "if" [ "(" ] expression [ ")" ]
( block [ "else" ( block | if ) ]
| "then" expression "else" expression ) ; (* the then/else form is an expression *)
when = "when" expression "{" (* pattern match / switch, an expression *)
{ pattern "->" expression [ terminator ] }
[ "else" "->" expression [ terminator ] ] "}" ;
pattern = IDENT [ "(" [ IDENT { "," IDENT } ] ")" ] (* enum variant, optional field bindings *)
| expression ; (* literal value (primitive switch) *)
while = "while" [ "(" ] expression [ ")" ] block ;
for = "for" IDENT "in" ( expression ".." expression (* range, end exclusive *)
| expression ) block ; (* array iteration *)
return = "return" [ expression ] ;
try = "try" block "catch" "(" "Error" IDENT ")" block ; (* the caught type is always Error *)
throw = "throw" expression ; (* an Error value, or a string literal shorthand *)
block = "{" { statement [ terminator ] } "}" ;
typeParams = "[" typeParam { "," typeParam } "]" ;
typeParam = IDENT [ ":" type ] ; (* interface-bounded generic *)
params = [ param { "," param } ] ;
param = type IDENT [ "=" expression ] ; (* trailing default value *)
typeList = type { "," type } ;Types
type = "byte" | "int" | "long" | "float" | "str" | "bool"
| "void" | "any" | "Self"
| "array" "[" type "]"
| "tuple" "[" type { "," type } "]"
| "dict" "[" type ":" type "]" (* sugar for Map[K, V] *)
| "ptr" "[" type "]"
| "actor" "[" type "]"
| "future" "[" type "]"
| funcType
| IDENT [ "[" type { "," type } "]" ] ; (* class / interface, maybe generic *)
funcType = "func" "[" ( "(" [ typeList ] ")" type (* full signature: func[(int,int) int] *)
| type ) "]" ; (* loose form: func[int] *)Expressions
Velo is expression-oriented: if, while, for and blocks are parsed as expressions (so let x = if c then a else b is valid). The statement/expression split above is only for readability.
expression = assignment ;
assignment = binary [ assignOp expression ] ; (* right-associative *)
assignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
binary = unary { binOp unary } ; (* grouped per precedence table *)
binOp = "||" | "|" | "&&" | "&" | "^"
| "==" | "!=" | "<" | ">" | "<=" | ">="
| "+" | "-" | "*" | "/" | "%" ;
unary = [ "-" | "!" | "&" | "*" ] postfix ; (* neg, not, address-of, deref *)
postfix = primary { call | index | property | apply } ;
call = "(" [ argList ] ")" ;
index = "[" expression "]" ;
property = "." ( IDENT | INT_LIT ) [ "(" [ argList ] ")" ] ;
(* .field / .N → bare access; .method(...) / .conv() → call *)
apply = "{" { statement [ terminator ] } "}" ; (* apply block (e.g. UI builders) *)
argList = expression { "," expression } ;
primary = NUMBER | STRING | CHAR_LIT
| "true" | "false" | "null" | "void"
| IDENT
| lambda
| newExpr
| asyncExpr | awaitExpr
| if | while | for (* control forms are expressions too *)
| "(" expression ")" ;
lambda = "func" [ typeParams ] "(" params ")" type block ;
newExpr = "new" ( "array" "[" type "]" ( "(" expression ")" | "{" [ argList ] "}" )
| "tuple" "(" argList ")"
| "dict" "[" type ":" type "]" "{" [ dictEntries ] "}"
| "ptr" "[" type "]" "(" expression ")"
| IDENT [ "[" typeList "]" ] "(" [ argList ] ")" ) ;
dictEntries = dictEntry { "," dictEntry } ;
dictEntry = expression ":" expression ;
asyncExpr = "async" postfix "." IDENT "(" [ argList ] ")" ;
awaitExpr = "await" expression ;Operator precedence
From loosest (binds last) to tightest (binds first). Higher rows are evaluated later. Example: a + b * c parses as a + (b * c) (multiplicative binds tighter than additive); a == b && c == d parses as (a == b) && (c == d).
| Level | Operators | Kind | Associativity |
|---|---|---|---|
| 1 | = += -= *= /= %= | assignment | right |
| 2 | || | | logical-or / bitwise-or | left |
| 3 | && & | logical-and / bitwise-and | left |
| 4 | ^ | bitwise-xor | left |
| 7 | == != < > <= >= | comparison | left |
| 10 | + - | additive | left |
| 20 | * / % | multiplicative | left |
| 25 | - ! & * (prefix) | unary: negate, not, address-of, deref | right |
| 30 | (…) […] . {…} | call, index, property, apply | left |
Notes:
- Prefix unary binds looser than the postfix chain, as in C/Java/Kotlin: the operand of
-!&*is the wholepostfixproduction, so!d.key(k)is!(d.key(k)),-p.get()is-(p.get())and&a[i]is&(a[i]). It still binds tighter than every binary operator, so-a * bis(-a) * b. &&/||are logical with short-circuit;&/|/^are bitwise on ints (and&/|also short-circuit on bools, kept as aliases). Prefer&&/||...(range) is not a general operator — it appears only infor i in a..b.- There are no shift operators; use
x.shl(n)/x.shr(n). <is lowered internally but behaves as a normal left-associative comparison.