§1 Lexical & Grammar
1.1 Source Files
- Source files must be UTF-8 encoded; extension
.ct. - A source file is a module (§2.1); declaration order within a file is irrelevant (§2.6).
- Whitespace: space, Tab, newline. Indentation carries no meaning (P8/tokenizer friendly).
- Newlines are uniformly handled as
\n(\r\nis normalized).
1.2 Comments
- Line comments
//run to end of line; block comments do not exist (keeps the tokenizer simple and grep friendly). - Doc comments
///must immediately precede the declaration they document; code blocks inside doc comments participate in doc-test compilation and execution (§10.4).
1.3 Identifiers & Keywords
- Identifiers:
[A-Za-z_][A-Za-z0-9_]*(Unicode identifiers reserved). Externally visible names must not begin with an underscore (a leading_is for internal/placeholder use only). - Naming conventions (enforced as lint, not syntax errors):
- Types/enums/variants/constructors:
PascalCase - Functions/bindings/fields:
snake_case - Constants/statics:
SCREAMING_CASE - Package names: all-lowercase words
- Keywords (cannot be used as identifiers; the single authoritative list):
fn let var const static comptime
if else match while for in return
struct class enum trait impl
own scope test use pub extern
prop true false void self
- Reserved operator words (cannot be used as identifiers):
or(infix default value, §4.4). - Reserved words (currently syntax errors, reserved for evolution):
do async await interface module(as of v0.7,break continueare promoted to keywords, §4.2). asis not a keyword — explicit numeric conversion is a prelude method on numeric types,x.as[U64]()(§3.6).arena,Box,List,String,Channel,Mutex,Arena,Option,Result, etc. are prelude types/bindings, not keywords.- Forbidden punctuation (syntax errors):
;::.!serves only as unary not;&appears only in types;?only as a suffix.
1.4 Literals
- Integer literals: decimal;
0xhexadecimal,0ooctal,0bbinary; may contain_separators (1_000_000). - Without a suffix the default is
I32; in contexts where the expected type is clear, it adapts to the expected integer type (§3.7). - Suffixes:
i8 i16 i32 i64 isize u8 u16 u32 u64 usize f32 f64(e.g.255u8). - Floating-point literals: decimal, may carry an exponent; the default without a suffix is
F64; suffixesf32/f64. - Booleans:
true/false. - No character literals (there is no
chartype; code point iteration returns integers via read-only stdlib APIs). - String literals: double-quoted; escapes
\n \t \r \\ \" \0 \{ \u{HEX}; the literal's type isStr(an immutable borrow, §3.3), stored in static storage (available in every profile). - Interpolation:
{introduces an interpolation expression, which may contain identifiers, field/property/method chains, and indexing ({clock.now()},{xs[0]}); a literal{must be written\{. Interpolated strings are syntactic sugar, equivalent to afmtcall over concatenated fragments (§4.6).
1.5 Operators & Punctuation
+ - * / % arithmetic
+% -% *= /= %= += wrapping add/sub; compound assignment
== != < > <= >= comparison
= assignment (statement only, §4.2)
&& logical and (short-circuit)
|| logical or (short-circuit, v0.7; §4.4)
- ! unary: negation / logical not (Bool only; §4.3 level 7, §4.4)
or infix default value (Option/Result, §5.2); not logical or
.. ..= range (half-open / closed)
.. slice type / elision (see grammar)
-> => return type / match arm
? Result/Option propagation suffix (§5.3)
. path/field/method/tuple index (.0 .1)
, : ; forbidden
[ ] ( ) { } generic args, grouping/tuple/params, blocks
& types only: shared read-only references (&T / &Trait)
#[@ ] @derive(...) annotations / derive (§8.3)
| delimits closure parameters
_ wildcard
1.6 Newline Termination Rules (Statement Delimiting)
Ctron has no semicolons. A newline is the terminator of statements/fields/variants/match arms, unless any of the following holds:
- The line's final token belongs to the continuation set (the line's semantics are incomplete):
, = -> => && || or .. ..= + - * / % +% -% == != < > <= >= ( [ { |
```(`?` is not in the continuation set: it is a suffix and always terminates a statement)
2. **The next line starts with `.` or a binary operator** (supporting the "leading-dot" layout of chained calls:
```c
let y = xs
.filter(|x| x > 0)
.map(|x| x * 2)
A leading . is therefore always a continuation of the previous line's expression, never the start of a new statement; conversely, a trailing . is not in the continuation set, so trailing-dot chaining is illegal (leading-dot style is mandated, and the formatter emits the unique form).
elsemust be on the same line as}:} else {. The cross-line form (a newline after}, thenelse) is rejected by the parser (E1001) — the lexer does not suppress that newline.- After the last expression in a block (the block value),
}may follow directly without a newline.
1.7 Full Grammar (EBNF)
Notation: { X } repetition, [ X ] optionality, | alternation, NEWLINE a newline. NL+ at the right end of a production means "newline-separated repetition".
(* ---------- top level ---------- *)
File = { TopDecl } ;
TopDecl = UseDecl | StructDecl | ClassDecl | EnumDecl | TraitDecl | ImplDecl
| FnDecl | ConstDecl | StaticDecl | TestDecl ;
UseDecl = "use" Path [ "{" Path { "," Path } [ "," ] "}" ] NEWLINE ;
Path = IDENT { "." IDENT } ;
(* ---------- type declarations ---------- *)
StructDecl = { DeclAttr } "struct" IDENT [ TypeParams ] "{" NEWLINE* { Field NEWLINE+ } "}" ;
Field = Visibility [ "let" | "var" ] IDENT ":" Type ; (* let may be omitted; let/omitted = immutable, var = mutable *)
ClassDecl = { DeclAttr } "class" IDENT [ TypeParams ] "{" NEWLINE* { ClassItem NEWLINE+ } "}" ;
ClassItem = Field | Method | PropImpl ;
EnumDecl = { DeclAttr } "enum" IDENT [ TypeParams ] "{"
NEWLINE* { Variant NEWLINE+ } "}" ;
Variant = IDENT [ "(" [ Type { "," Type } ] ")"
| "{" Field { ( "," | NEWLINE+ ) Field } [ "," ] "}" ] ; (* variant fields: separated by commas or newlines *)
TypeParams = "[" TypeParam { "," TypeParam } "]" ;
TypeParam = IDENT [ ":" Bound ] | "comptime" IDENT ":" Type ;
Bound = Path { "+" Path } ;
Visibility = "pub" | "pub" "(" "pkg" ")" ;
(* ---------- traits and impls ---------- *)
TraitDecl = { DeclAttr } "trait" IDENT [ TypeParams ] [ ":" Bound ]
"{" NEWLINE* { TraitItem NEWLINE+ } "}" ; (* Bound = supertrait *)
TraitItem = Method | PropSig | PropImpl | ConstDecl ;
Method = { DeclAttr } Visibility "fn" IDENT [ TypeParams ] "(" ParamList ")" [ "->" Type ] Block ;
PropSig = Visibility "prop" IDENT ":" Type ;
PropImpl = Visibility "prop" IDENT ":" Type Block ;
ImplDecl = "impl" [ TypeParams ] Path [ TypeArgs ] "for" Type
"{" NEWLINE* { (Method | PropImpl) NEWLINE+ } "}" ;
(* ---------- functions and tests ---------- *)
FnDecl = { DeclAttr } [ "pub" ] [ "comptime" ] [ "extern" STRING_LIT ] "fn" IDENT
[ TypeParams ] "(" ParamList ")" [ "->" Type ] [ Block ] ; (* extern declarations omit Block, §9.6 *)
ParamList = [ Param { "," Param } [ "," ] ] ;
Param = Receiver | [ "var" ] IDENT ":" Type ;
Receiver = "&" "self" | "var" "self" ;
ConstDecl = "const" IDENT ":" Type "=" Expr NEWLINE ;
StaticDecl = "static" "let" IDENT ":" Type "=" Expr NEWLINE ;
TestDecl = "test" STRING_LIT Block ;
(* ---------- attributes ---------- *)
DeclAttr = Attribute | DeriveAttr ; (* modifies the declaration that immediately follows *)
Attribute = "#[" IDENT [ "(" AttrArgs ")" ] "]" ;
AttrArgs = Expr | IDENT { "," (Expr | IDENT) } ;
DeriveAttr = "@derive" "(" Path { "," Path } ")" ;
(* ---------- statements ---------- *)
Block = "{" NEWLINE* { (Stmt | Expr) NEWLINE+ } [ Expr NEWLINE* ] "}" ;
Stmt = LetStmt | VarStmt | ReturnStmt | ForStmt | WhileStmt | AssignStmt | ExprStmt ;
LetStmt = "let" Pattern [ ":" Type ] "=" Expr ;
VarStmt = "var" Pattern [ ":" Type ] "=" Expr ;
ReturnStmt = "return" [ Expr ] ;
ForStmt = "for" Pattern "in" Expr Block ;
WhileStmt = "while" Expr Block ;
AssignStmt = PostfixExpr AssignOp Expr ;
AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
(* ---------- patterns ---------- *)
Pattern = IDENT | "_" | LiteralPattern | TuplePattern | AggPattern ;
LiteralPattern = INT_LIT | FLOAT_LIT | STRING_LIT | "true" | "false" ;
TuplePattern = "(" [ Pattern { "," Pattern } ] ")" ;
AggPattern = PathPattern [ "(" [ Pattern { "," Pattern } ] ")"
| "{" FieldPattern { "," FieldPattern } "}" ] ;
PathPattern = IDENT { "." IDENT } ; (* enum variants / named types *)
FieldPattern= IDENT | IDENT ":" Pattern ;
(* ---------- expressions (ascending precedence; see §4.3 for details) ---------- *)
Expr = LogicOrOr ;
LogicOrOr = LogicOr { "||" LogicOr } ; (* v0.7: level 0; a || in starting position begins a zero-parameter closure, see §4.7 role separation *)
LogicOr = LogicAnd { "or" LogicAnd } ;
LogicAnd = Compare { "&&" Compare } ;
Compare = Range [ ( "==" | "!=" | "<" | ">" | "<=" | ">=" ) Range ] ; (* not chainable *)
Range = Additive [ ( ".." | "..=" ) Additive ] ;
Additive = Multiplicative { ( "+" | "-" | "+%" | "-%" ) Multiplicative } ;
Multiplicative = Unary { ( "*" | "/" | "%" ) Unary } ;
Unary = ( "-" | "!" ) Unary | Postfix ;
Postfix = Primary { Call | Index | Member | TypeArgs | Try } ;
Call = "(" [ Expr { "," Expr } [ "," ] ] ")" ;
Index = "[" Expr "]" ;
Member = "." ( IDENT | INT_LIT ) ; (* INT_LIT: tuple .0 .1 *)
TypeArgs = "[" Type { "," Type } "]" ;
Try = "?" ;
Primary = INT_LIT | FLOAT_LIT | STRING_LIT | "true" | "false" | "void" | IDENT
| "(" [ Expr { "," Expr } ] ")" (* grouping / tuple *)
| ArrayLit | StructLit | IfExpr | MatchExpr
| Closure | ScopeExpr | OwnExpr | Block ;
ArrayLit = "[" [ Expr { "," Expr } [ "," ] ] "]" ;
StructLit = Path [ TypeArgs ] "{" FieldInit { "," FieldInit } [ "," ] "}" ;
FieldInit = IDENT [ ":" Expr ] ; (* omitted = shorthand for the same-named field *)
IfExpr = "if" Expr Block ( "else" ( IfExpr | Block ) ) ; (* else is required when used as a value *)
MatchExpr = "match" Expr "{" NEWLINE* { MatchArm NEWLINE+ } "}" ;
MatchArm = Pattern "=>" Expr ;
Closure = "|" [ ClosureParam { "," ClosureParam } ] "|" [ "->" Type ] Expr ;
ClosureParam= [ "var" ] IDENT [ ":" Type ] ;
ScopeExpr = "scope" ClosureBlock ;
ClosureBlock= "{" "|" IDENT "|" NEWLINE* { (Stmt | Expr) NEWLINE+ } [ Expr NEWLINE* ] "}" ;
OwnExpr = "own" "(" IDENT ")" Block ;
(* ---------- types ---------- *)
Type = "&" Type (* shared read-only reference / trait object *)
| Type "[" "]" (* mutable slice view T[] (§3.1; &T[] is the read-only view) *)
| Type "[" Expr "]" (* fixed-size array T[N]; N is a comptime expression *)
| Type "?" (* Option sugar *)
| "(" [ Type { "," Type } ] ")" (* tuple / unit "()")
| "fn" "(" [ Type { "," Type } ] ")" [ "->" Type ] (* function type: parameter/return positions only *)
| Path [ TypeArgs ] (* named/generic type *)
| "self" ; (* inside impl: refers to the implementing type *)
1.8 Grammar Ambiguity Rulings
IDENT {in expression position: always a construction literal (of a named type). Bare blocks can only appear in keyword-introduced positions (fn/if/else/while/for/own/scope/matcharms, closure bodies), so the two never conflict.IDENT [ ... ]in expression position: if[...]is immediately followed by(or{→ generic instantiation (e.g.Channel[I32](4),Box[Point](p)); otherwise parsed as indexing, and when the index content is not a legal single expression (e.g. it contains a top-level comma) it falls back to type arguments (coveringSimd[F32, 4].splat(v)). Consequently, "indexing an array of closures and then calling" requires parentheses:(xs[i])(arg)— the parser can decide without type information.- Generics use
[]rather than<>(eliminating thea < b > cambiguity; a normative ruling, from a test pinned ruling). - The function type
fn(...) -> Tappears only in type positions;fnin expression position is an illegal Primary, so there is no ambiguity. [in type position disambiguation (P1-B ruling): empty[]→ slice; content is a single integer literal → fixed-size arrayType [ Expr ]; anything else → generic type arguments. Residual ambiguity:T[SIZE](where SIZE is a comptime constant identifier) parses as generic — fixed-size arrays use literal dimensions, or are used via the comptime-monomorphizedArr[T, N]form.
1.9 Correspondence with the Test Suite
Executable samples for lexical/grammar features: tests/01_basics.ct (literals/operations/loops), tests/04_generics_comptime.ct (generic and comptime syntax).