FREAKV3 docs freak 0.15.1 (Maverick)

Grammar#

The complete V3 grammar, transcribed from src/compiler/v3/lexer.fk and parser.fk.

This is the whole language. If a construct is not derivable from these rules, it does not parse.

Lexical structure#

program-text  = { token | whitespace | comment }

comment       = "--" { any-char-except-newline }
whitespace    = " " | "\t" | "\r" | "\n"

token         = number | word-literal | identifier | keyword
              | bool-literal | punctuation

number        = digit { digit | "." }
                -- more than one "." is a lex error
                -- a "." followed by another "." ends the number, so 1..4
                -- is  number ".." number
                -- no sign, no exponent, no suffix

word-literal  = '"' { char | escape } '"'
escape        = "\\n" | "\\r" | "\\t" | "\\\"" | "\\\\" | "\\{" | "\\}"
              | "\\x" hex hex
                -- any other escape is a lex error
                -- "\x00" is rejected

identifier    = ( letter | "_" ) { letter | digit | "_" }
                -- letters are ASCII A-Z a-z only

Identifiers are classified against the keyword table case-insensitively: an identifier whose lowercase form is a keyword becomes that keyword token. The parser then matches a single-word keyword by its lowercase spelling, so Pilot is reserved — it cannot be a name — but does not start a declaration.

bool-literal  = "true" | "false" | "yes" | "no" | "hai" | "iie"

punctuation   = "==" | "!=" | "<=" | ">=" | "+=" | "-=" | "*=" | "/="
              | "%=" | "->" | "=>" | "**" | "|>" | "||" | "::"
              | "=" | "+" | "-" | "*" | "/" | "%" | "<" | ">" | "!"
              | ":" | "|" | "@" | "?" | "{" | "}" | "(" | ")"
              | "[" | "]" | "," | "." | "..=" | ".." | ";"

**, =>, ||, ? and ! are produced by the lexer but have no grammar rule — reaching one is a parse error.

Multi-word keywords#

Lexed greedily: on seeing the first word, the lexer looks ahead for the second and merges them.

"give back"  "or else"    "trust me"   "for each"   "for science"
"training arc"   "bringing back"   "only on"   "PLUS ULTRA"   "FINAL FORM"
"grounded pilot"

give back and grounded pilot are merged only when the second word is on the same line as the first.

Keyword table#

pilot fixed task say shape impl doctrine launch use as in lend mut move
copy break continue if else when repeat times until done for each check
result got nobody some ok err sessions max foreshadow payoff route sadly
deus_ex_machina isekai eventually and or not nakama tsundere extern while

Many of these are reserved without having a grammar rule — see the reserved list.

Program#

program    = { statement | ";" }

A ; separates statements and is otherwise ignored. It is never required.

There is no distinction between top-level and nested statements: the same statement rule is used everywhere. A task, shape or impl is syntactically just a statement, so it may legally appear nested inside another block.

Statements#

statement  = [ annotation ] statement-body

annotation = "@" identifier            -- exactly one, no arguments

statement-body =
      say-stmt | shape-decl | impl-decl | eventually-stmt
    | fixed-decl | pilot-decl | give-back | break-stmt | continue-stmt
    | if-stmt | repeat-stmt | while-stmt | for-each | for-stmt
    | training-arc | when-stmt | check-result
    | extern-decl | task-decl | block | assign-or-expr

say-stmt      = "say" expression

fixed-decl    = ( "fixed" "pilot" | "grounded pilot" )
                identifier [ ":" type ] "=" expression
pilot-decl    = "pilot" [ "mut" ] identifier [ ":" type ] "=" expression

give-back     = "give back" [ expression ]
break-stmt    = "break"
continue-stmt = "continue"

if-stmt       = "if" expression block [ "else" ( if-stmt | block ) ]

repeat-stmt   = "repeat" "until" expression block
              | "repeat" "while" expression block
              | "repeat" expression "times" [ "with" identifier ] block

while-stmt    = "while" expression block

for-each      = "for each" identifier "in" expression
                [ ( ".." | "..=" ) expression [ "step" expression ] ] block

for-stmt      = "for" "(" pilot-decl ";" expression ";" assign-or-expr ")" block

training-arc  = "training arc" "until" expression
                "max" expression "sessions" block

when-stmt     = "when" expression "{" { when-arm } "}"
when-arm      = ( "_" | expression ) "->" statement

check-result  = "check" "result" expression "{" { check-arm } "}"
check-arm     = ( "ok" | "err" ) "(" identifier ")" "->" block
                -- exactly one ok arm and one err arm

eventually-stmt = "eventually" block

block         = "{" { statement | ";" } "}"

assign-or-expr = expression [ assign-op expression ]
assign-op      = "=" | "+=" | "-=" | "*=" | "/=" | "%="

with and step are ordinary identifiers that the parser recognises only in those positions; they are not reserved.

Every type position — pilot-decl, fixed-decl, shape fields, parameters, return types, extern signatures — goes through one rule:

type        = identifier
            | "List" "<" identifier ">"
Note

List<T> is the language's only generic form, and it is a hard-coded special case in parser_take_type rather than a general mechanism: the element must be a plain identifier, so List<List<int>> is rejected. That single rule is why maybe<T>, result<T,E>, Map<K,V>, fixed arrays, tuples and raw pointers have no syntax.

Declarations#

shape-decl  = "shape" identifier "{" { field } "}"
field       = identifier ":" type [ "," ]

impl-decl   = "impl" identifier [ "for" identifier ] "{" { impl-method } "}"
impl-method = "task" identifier "(" params ")" [ "->" type ] block

task-decl   = "task" identifier "(" params ")" [ "->" type ] block

extern-decl = "extern" "task" identifier "(" ext-params ")" "->" type

params      = [ param { "," param } ]
param       = "self" | identifier ":" type
ext-params  = [ identifier ":" type { "," identifier ":" type } ]

In impl D for S, S owns the methods and D is recorded as provenance only — it is never validated. A method's mangled global name is S_method.

self is written bare; the compiler substitutes the owning shape's type.

Expressions#

Loosest to tightest:

expression  = and-expr { "or" and-expr }
and-expr    = cmp-expr { "and" cmp-expr }
cmp-expr    = add-expr { ( "==" | "!=" | "<" | ">" | "<=" | ">=" ) add-expr }
add-expr    = mul-expr { ( "+" | "-" | "NAKAMA" ) mul-expr }
mul-expr    = unary { ( "*" | "/" | "%" ) unary }

unary       = ( "not" | "-" | "PLUS ULTRA" | "FINAL FORM" | "TSUNDERE" ) unary
            | postfix

postfix     = primary { postfix-op }
postfix-op  = "." identifier [ "(" args ")" ]
            | "[" expression "]"
            | "|>" identifier [ "(" args ")" ]

primary     = number
            | word-literal
            | bool-literal
            | identifier [ "::" identifier ] [ ctor-args | "(" args ")" ]
            | "(" expression ")"
            | "[" [ expression { "," expression } ] "]"

ctor-args   = "{" [ field-init { "," field-init } ] "}"
field-init  = identifier ":" expression

args        = [ expression { "," expression } ]

ctor-args is only taken when the identifier is an already-registered shape name, which is why a shape must be declared before it is constructed in token order.

x |> f(a) desugars to f(x, a); the pipe target must be a bare identifier.

String interpolation#

Interpolation is resolved by the parser after lexing, over the literal's text.

interp-path = identifier { "." identifier }

A {...} whose body matches interp-path, where every segment is a valid identifier and not a keyword, becomes an interpolated expression. A body that is an expression instead, such as {1 + 2} or {f()}, is an error, and so is a { with no closing }. \{ and \} are literal braces.

The resolved path must have type word, int, num or bool.

Error recovery#

The parser reports a diagnostic, skips one token, and continues. After 100 syntax errors it stops:

error: too many errors, giving up (>100 syntax errors in <file>)

There are no ErrorNode or IncompleteNode tolerant-AST nodes — those are V4. Codegen is skipped whenever any frontend diagnostic fired.

Reading the real thing#

The authoritative sources, in the order they are concatenated:

FileRole
src/compiler/v3/globals.fkIdentity, compiler state, token and AST tables
src/compiler/v3/helpers.fkArrays, diagnostics, AST allocation, emit helpers
src/compiler/v3/lexer.fkTokenisation
src/compiler/v3/parser.fkRecursive descent into parallel AST arrays
src/compiler/v3/checker.fkType checks and the opt-in Phase-1 borrow checker
src/compiler/v3/emit_c.fkStreaming C lowering
src/compiler/v3/emit_llvm.fkStreaming LLVM IR lowering
src/compiler/v3/main.fkStandalone freakc entry point

The whole compiler is one flattened translation unit with globally unique symbols; the file split is a maintenance convention, not a module system.

Badged executable examples were compiled and run with the released freak 0.15.1 (Maverick). 84/84 examples match their reviewed expected output. Illustrative snippets are labelled separately. Regenerate with python tools/refresh.py.

V4 boxes were checked against the development snapshot 7c9a1f3c27 (2026-10-04); 46/46 V4 examples match their reviewed expectation. Regenerate with python tools/verify_v4.py --checkout <Freak-lang>.