Grammar
This page condenses the normative grammar of Geas: the lexical structure, the keyword lists, the literal forms, and the EBNF for declarations, statements, and expressions. Where the grammar alone cannot pin a rule down, a semantic note next to the production carries the rest.
Notation is EBNF: ::= is defined as, | alternation, { x } zero or more, [ x ] optional, ( x ) grouping, "x" a literal terminal, and NL one or more newlines.
Lexical structure
Section titled “Lexical structure”A source file is UTF-8 and ends in .geas. A string literal that is not valid UTF-8 is a compile error, not a silent replacement.
Statements and declarations are terminated by a newline. There are no semicolons. A newline inside parentheses, brackets, or an argument list does not terminate anything, so a long signature can wrap freely. A { at the end of a line opens a block and the matching } closes it.
Nesting depth is capped. A parser that hits the ceiling reports a diagnostic and stops rather than hanging or overflowing its stack.
Comments
Section titled “Comments”// line comment, to end of line/* block comment, no nesting */Identifiers and keywords
Section titled “Identifiers and keywords”IDENT ::= (letter | "_") { letter | digit | "_" }Hard keywords, reserved everywhere:
contract vow pledge clause provisionalsubcontract requirements incorporate internal recordimport let mut return ifelse match while for inbreak continue is either ortrue false schema transactionalContextual keywords, ordinary identifiers except in the positions named:
fulfillandpartialare labels inside arequirementsblock.abi,symbol, andversionare attribute keys.sign,resume,status,park, andpartialare builtin contract methods.
One carve out: after . any keyword is accepted as a member name. That is what lets payment.break() parse while break stays a hard keyword for loop control.
Literals
Section titled “Literals”INT ::= decimal | "0x" hex | "0b" binary // underscores allowed as separatorsFLOAT ::= digits "." digits [ ("e"|"E") ["+"|"-"] digits ]STRING ::= '"' { char | escape } '"'CHAR ::= "'" ( char | escape ) "'"BOOL ::= "true" | "false"escape ::= "\n" | "\t" | "\r" | "\0" | "\\" | "\"" | "\'" | "\u{" hex "}"An INT literal types as Int unless the expected type at its position is UInt or Byte and the value fits. A FLOAT literal types as Float. CHAR holds one Unicode scalar value. There is no implicit numeric conversion anywhere, widening included.
A raw newline inside a string literal is an error; a string that needs one writes \n. A \u{...} escape naming a surrogate or a value above 0x10FFFF is an error.
Type ::= NamedType | TupleType | PledgeTypeNamedType ::= IDENT [ "<" Type { "," Type } ">" ]TupleType ::= "Tuple" "<" Type { "," Type } ">"PledgeType ::= "pledge" "(" [ Type { "," Type } ] ")" "->" TypeThe builtin named types are Int, UInt, Float, Bool, Byte, Char, String, Unit, List<T>, Map<K, V>, Option<T>, and Result<T, E>. Type arguments belong to the builtin composites only. Map<K, V> keys are restricted to Int, UInt, Bool, Byte, Char, and String.
Program structure
Section titled “Program structure”File ::= { Import NL } { TopDecl NL }Import ::= "import" IDENT { "." IDENT }TopDecl ::= RecordDecl | SumDecl | ProvClause | ContractDeclA file is a module. A directory is a package. See Source files and packages.
Attributes
Section titled “Attributes”Attrs ::= "[" IDENT ":" Literal { "," IDENT ":" Literal } "]"An attribute list trails a declaration header. Unknown keys are compile errors. The known keys: version: INT on a contract, default 1, baked into every mangled name; abi: STRING and symbol: STRING on a pledge, binding it to a foreign symbol, with "c" the only ABI in the first version.
Declarations
Section titled “Declarations”RecordDecl ::= "record" IDENT "{" NL { FieldDecl NL } "}"FieldDecl ::= IDENT ":" Type
SumDecl ::= IDENT "is" "either" Variant { "or" Variant }Variant ::= IDENT [ "(" FieldDecl { "," FieldDecl } ")" ]
ProvClause ::= [ "internal" ] "provisional" "clause" IDENT "{" NL { ClauseSig NL } "}"ClauseSig ::= "clause" IDENT "(" [ Params ] ")" "->" Type
ContractDecl ::= [ "internal" ] "contract" IDENT [ Attrs ] "{" NL { Member NL } "}"Member ::= Incorporate | VowDecl | SchemaDecl | PledgeDecl | ClauseDecl | SubcontractDecl | RequirementsBlock
Incorporate ::= "incorporate" IDENT
VowDecl ::= "vow" IDENT ":" Type [ "=" Expr ]
SchemaDecl ::= "schema" IDENT "{" NL { ColumnDecl NL } "}"ColumnDecl ::= IDENT ":" Type
PledgeDecl ::= "pledge" IDENT "(" [ Params ] ")" "->" Type [ Attrs ] [ Block ]
ClauseDecl ::= "clause" IDENT "(" [ Params ] ")" "->" Type Block
Params ::= Param { "," Param }Param ::= IDENT ":" Type
SubcontractDecl ::= "subcontract" [ IDENT ] [ "transactional" ] "{" NL { PledgeDecl NL } "}"Semantic notes:
- A pledge return type must be
Result<T, E>,Option<T>, orUnit. Nothing else. - A pledge with a
Blockis implemented in Geas. A pledge without one is abstract and must be bound before the contract signs. - A vow initializer must be a constant expression of the vow type. A vow without an initializer must be supplied at sign time.
- Clauses are not first class. A clause is callable by bare name only inside its own contract.
- Subcontracts do not nest. An anonymous subcontract cannot be named in a
requirementsblock. - A sum declaration is one statement; a newline ends it, and a leading
oron the next line is an error.
Requirements
Section titled “Requirements”RequirementsBlock ::= "requirements" "{" NL [ "fulfill" ":" ReqExpr NL ] [ "partial" ":" ReqExpr NL ] [ "break" ":" ReqExpr NL ] "}"
ReqExpr ::= ReqOrReqOr ::= ReqAnd { "||" ReqAnd }ReqAnd ::= ReqNot { "&&" ReqNot }ReqNot ::= [ "!" ] ReqAtomReqAtom ::= IDENT | "(" ReqExpr ")"A ReqAtom names a subcontract or a loose pledge, at most 16 atoms per contract. The semantics are on Requirements and state.
Statements
Section titled “Statements”Block ::= "{" NL { Stmt NL } "}"Stmt ::= LetStmt | AssignStmt | ReturnStmt | IfStmt | MatchExpr | WhileStmt | ForStmt | "break" | "continue" | Expr
LetStmt ::= "let" [ "mut" ] IDENT [ ":" Type ] "=" ExprAssignStmt::= Lvalue "=" ExprLvalue ::= IDENT { "." IDENT | "[" Expr "]" }ReturnStmt::= "return" [ Expr ]IfStmt ::= "if" Expr Block { "else" "if" Expr Block } [ "else" Block ]WhileStmt ::= "while" Expr BlockForStmt ::= "for" IDENT "in" Expr BlockBindings are immutable unless declared let mut. There are no compound assignment operators in the first version. break and continue are loop control only. A block used as a value takes the type of its final expression statement, and Unit when it ends with anything else.
Expressions
Section titled “Expressions”Precedence, tightest first:
| Level | Operators | Associativity |
|---|---|---|
| 1 | call (), index [], member ., propagate ? | left |
| 2 | unary !, unary - | right |
| 3 | * / % | left |
| 4 | + - | left |
| 5 | < <= > >= | left |
| 6 | == != | left |
| 7 | && | left |
| 8 | || | left |
Primary ::= Literal | IDENT | "(" Expr ")" | TupleLit | ListLit | MatchExpr | CtorExpr | "mut" "(" Expr ")"
TupleLit ::= "(" Expr "," Expr { "," Expr } ")"ListLit ::= "[" [ Expr { "," Expr } ] "]"
CtorExpr ::= "Some" "(" Expr ")" | "None" | "Ok" "(" Expr ")" | "Err" "(" Expr ")" | IDENT "{" [ IDENT ":" Expr { "," IDENT ":" Expr } ] "}" // record | IDENT [ "(" Expr { "," Expr } ")" ] // sum variant
CallExpr ::= Expr "(" [ Args ] ")"MemberExpr ::= Expr "." IDENTArgs ::= Arg { "," Arg }Arg ::= [ IDENT ":" ] Expr
MatchExpr ::= "match" Expr "{" NL { MatchArm NL } "}"MatchArm ::= Pattern "->" ( Expr | Block )Pattern ::= Literal | "_" | IDENT // binding | IDENT "(" Pattern { "," Pattern } ")" // variant with payload | "Some" "(" Pattern ")" | "None" | "Ok" "(" Pattern ")" | "Err" "(" Pattern ")"&& and || short circuit. expr? unwraps an Option or a Result and returns early from the enclosing pledge or clause on None or Err. match is an expression, checked for exhaustiveness over sums, Option, Result, and Bool; a _ or binding arm covers the rest.
Two rules keep the record literal parse deterministic: a bare record literal is not allowed in the head expression of if, while, for, or match, and the { of a record literal must sit on the same line as the name.
Deliberate omissions
Section titled “Deliberate omissions”Kept out of the first version so the surface stays small. Each has a reserved spelling or a stated path so adding it later breaks nothing.
- Generic user declarations. Type parameters stay on the builtin composites.
- Map literals, compound assignment, ranges, and a
forover numeric ranges. - Async fulfillment surface. The runtime API is shaped for it; the language keeps the synchronous call form for now.
- Provisional clauses requiring pledges. Today they require clauses only, which makes them internal code sharing, not a public interface.