Formal Grammar Spec
This document contains the formal grammar and lexical specification for Silk as used by the compiler implementation.
Lexical Structure (Overview)#
The lexer operates over:
- Whitespace and comments (trivia):
- spaces, tabs, newlines;
- line comments starting with
//and continuing to the end of the line; doc line comments start with///and follow the same lexical rules; - block comments starting with
/*and ending with the next*/(non‑nesting); doc block comments start with/**and also end at the next*/.
Both doc-comment forms are still trivia for the parser (they do not affect the syntax tree), but tooling may preserve and parse their text for documentation generation as specified in doc comments.
- Identifiers:
- sequences of Unicode letters, digits, and
_, with language-specific rules for leading characters. - Keywords:
- packages and imports:
package,module,import, file imports:from, - control flow and host dispatch:
if,else,loop,while,for,in,match,return,panic,break,continue,assert,await,yield,gpu, - attributes and conditional compilation:
attr, - declarations and mutability:
export,public,private,default,const,let,var,mut,fn,test,theory,type, - types and declarations:
struct,enum,error,interface,impl,as,raw,extends,pure,task,async, - memory and regions:
move,region,with,new, - other operators:
sizeof,alignof,offsetof,typename, - optionals and literals:
None/none,Some,true,false,null, - verification, compile-time embedding, and external declarations:
ext, Formal Silk directives#const,#require,#assure,#assert,#invariant,#variant,#monovariant,#theory, and the compile-time file embed expression#embed, - other keywords as listed in the spec.
Keywords are lexed as distinct tokens, but in name positions (for example
qualified-name segments like std::test, function/method names, and member
access like value.test) the parser accepts keywords anywhere an
identifier is expected.
The #require / #assure / #assert / #invariant / #variant /
#monovariant / #const / #theory / #embed forms are not comments;
they are first-class lexical tokens that participate in the normal grammar.
Formal Silk directives are handled by the verifier; #embed is an ordinary
expression that embeds a file at compile time. A directive token begins with
# followed by optional horizontal whitespace and the directive name (so
#require and # require are equivalent spellings). When these sequences
appear inside a // line comment or anywhere inside a /* ... */ block
comment, the surrounding comment is treated as trivia and the directive
spellings are ignored by the lexer.
- Literals:
- numeric, duration, boolean, character, string, regexp, aggregate (see
*literals-*.md). - String literals have two surface forms:
"..."(escape sequences),`...`(raw/no-escape).- Regular expression literals have a JavaScript-style surface form:
/pattern/flags(see literals regexp).- Disambiguation is context-sensitive (like JavaScript): the parser only
recognizes a regex literal in expression-start positions where a
Primaryexpression may begin; otherwise/is parsed as the division operator. - Because
//starts a line comment, an empty regex literal//is not a valid token sequence; use/(?:)/for an explicit empty pattern. - Operators and delimiters:
- as listed in operators (assignment, arithmetic, logical, bitwise, ranges,
?.,??,::,->,=>, etc.).
The lexer must implement longest-token rules for ambiguous prefixes (e.g. ... vs ..= vs ..).
Lexical Grammar for Whitespace and Comments#
For the purposes of the grammar, whitespace and comments are treated as trivia that may appear between any two tokens and are ignored by the parser:
Trivia ::= (Whitespace | LineComment | BlockComment)+Whitespace ::= ' ' | '\t' | '\r' | '\n'LineComment ::= '//' LineCommentChar* Newline?LineCommentChar ::= any character except '\n'Newline ::= '\n'BlockComment ::= '/*' BlockCommentChar* '*/'BlockCommentChar ::= any character except the end of input
Block comments are not nesting: a /* sequence inside an existing
BlockComment has no special meaning and is treated as part of the comment
text until the first following */. Doc-style comments such as /** ... */
are just syntactic sugar for BlockComment at the lexical level.
High-Level Grammar Skeleton#
At a high level, the language can be structured as:
-
Compilation unit:
-
Module ::= (PackageDecl | ModuleDecl)? ImportDecl* TopLevelDecl* -
Top-level declarations:
-
AttrAnnot ::= 'attr' '(' AttrItemListOpt ')' -
AttrItemListOpt ::= AttrItemList -
AttrItemList ::= AttrItem (',' AttrItem)* ','? -
AttrItem ::= NameToken ( AttrOp AttrValue )? -
AttrOp ::= '=' | '<' | '<=' | '>' | '>=' -
AttrValue ::= 'true' | 'false' | IntLiteral | StringLiteral | NameToken -
TopLevelDecl ::= AttrAnnot* (PackageDecl | ModuleDecl | ImportDecl | InlineModuleDecl | UsingDecl | ReExportDecl | DefaultExportDecl | ExportableDecl | TestDecl) -
ExportableDecl ::= FnDecl | LetDecl | TypeDecl | StructDecl | EnumDecl | InterfaceDecl | ImplDecl | ExtDecl | ... -
Packages and imports:
The surface syntax for packages, imports, and exports is specified in detail in packages imports exports. The grammar in this file mirrors the currently Supported forms:
-
PackageDecl ::= 'package' PackagePath ';' -
ModuleDecl ::= 'module' PackagePath ModuleAsOpt ';' -
InlineModuleDecl ::= ExportModifier 'module' NameToken '{' InlineModuleItem* '}' -
InlineModuleItem ::= InlineModuleDecl | UsingDecl | ExportableDecl -
NameToken ::= Identifier | Keyword -
PackagePath ::= NameToken ('::' NameToken)* -
ModuleAsOpt ::= ('as' QualifiedName TypeArgListOpt) -
ImportDecl ::= 'import' ImportSpec ';' -
ImportSpec ::= ImportPath | FileImportSpec | FileDefaultImportSpec | AmbientFileImportSpec -
ImportPath ::= ('::')? NameToken ('::' NameToken)* -
ImportFrom ::= StringLiteral | PackagePath -
AmbientFileImportSpec ::= StringLiteral -
FileImportSpec ::= '{' ImportBindingListOpt '}' 'from' ImportFrom -
FileDefaultImportSpec ::= NameToken 'from' ImportFrom -
ImportBindingListOpt ::= ImportBindingList -
ImportBindingList ::= ImportBinding (',' ImportBinding)* ','? -
ImportBinding ::= NameToken ('as' NameToken)? -
ReExportDecl ::= 'export' '{' ReExportBindingListOpt '}' ';' -
ReExportBindingListOpt ::= ReExportBindingList -
ReExportBindingList ::= ReExportBinding (',' ReExportBinding)* ','? -
ReExportBinding ::= NameToken ('as' NameToken)? -
DefaultExportDecl ::= 'export' 'default' NameToken ';' -
UsingDecl ::= 'using' Identifier '=' QualifiedName ';' | 'using' QualifiedName UsingAsOpt ';' -
UsingAsOpt ::= ('as' Identifier) -
ExportModifier ::= 'export'? -
FnExportModifier ::= ('export' AttrAnnot* ('default')?)?
For top-level declarations that can be exported, the ExportModifier
appears before the declaration keyword (currently fn, let, ext, type,
struct, enum, theory, error, and interface):
FnDecl ::= FnSpecs FnExportModifier FnModifierOpt 'fn' FnGenericParamListOpt FnNameOpt FnSignature FnBodyFnBody ::= Block | ';'FnModifierOpt ::= FnModifier*FnModifier ::= 'const' | 'pure' | 'task' | 'async'FnGenericParamListOpt ::= GenericParamListFnNameOpt ::= NameTokenFnSpecs ::= (FnPrecondition | FnPostcondition | FnContractTheory)*FnPrecondition ::= '#require' Expr ';'FnPostcondition ::= '#assure' Expr ';'FnContractTheory ::= '#theory' QualifiedName '(' ArgListOpt ')' ';'
Declaration attributes may also appear immediately after export /
public before the exported declaration keyword. This supports forms such
as export attr(abi=c) fn add_i64 (...) -> i64 { ... }; it is equivalent to
the prefix annotation form attr(abi=c) export fn add_i64 (...) -> i64 { ... }.
Formal Silk theories may be declared either at top level (exportable) or inline inside blocks (non-exportable):
-
TheoryDecl ::= TheorySpecs ExportModifier 'theory' Identifier '(' TheoryParamsOpt ')' '{' TheoryBodyItem* '}' -
TheorySpecs ::= (TheoryPrecondition | TheoryPostcondition)* -
TheoryPrecondition ::= '#require' Expr ';' -
TheoryPostcondition ::= '#assure' Expr ';' -
TheoryParamsOpt ::= TheoryParams -
TheoryParams ::= TheoryParam (',' TheoryParam)* ','? -
TheoryParam ::= Identifier ':' Type -
TheoryBodyItem ::= SpecConstStmt | SpecTheoryStmt | TheoryRequires | TheoryInvariant | TheoryEnsures | TheoryVariant -
TheoryRequires ::= '#require' Expr ';' -
TheoryInvariant ::= '#invariant' Expr ';' -
TheoryEnsures ::= '#assure' Expr ';' -
TheoryVariant ::= '#variant' Expr ';' -
FnSignature ::= '(' FnParamsOpt ')' ResultTypeOpt -
FnParamsOpt ::= FnParams -
FnParams ::= GenericParamSectionOpt ';' ParamListOpt | ParamList -
GenericParamSectionOpt ::= GenericParamSection -
GenericParamSection ::= GenericParam (',' GenericParam)* ','? -
ParamListOpt ::= ParamList -
ParamList ::= Param (',' Param)* -
Param ::= VarArgsOpt MutOpt Identifier TypeAnnotationOpt DefaultArgOpt -
VarArgsOpt ::= '...' -
DefaultArgOpt ::= ('=' Expr) -
ResultTypeOpt ::= ('->' TypeNoPipe TypedErrorTypesOpt)? -
TypedErrorTypesOpt ::= ('|' TypeNoPipe)+
Notes:
-
When a top-level
;appears inside the function parameter list, it splits compile-time parameters (type/const parameters) from run-time value parameters. -
The compile-time side uses
GenericParamsyntax (TandN: int) and does not permitmut. -
FnNameOptis currently permitted only for default-exported functions (export default fn (...) { ... }). For non-default functions, thefnidentifier is required. -
Default export statements (
export default Name;) always name an existing symbol; they do not permit anonymous exports. -
The run-time side uses ordinary
Paramsyntax (mut x: Torx: T, with the type annotation optional in the Supported forms). -
If there is no
;, the entire list is treated as run-time parameters. -
Default arguments are supported in function parameter lists:
-
any parameter may provide a default expression (
x: int = 1), -
defaulted parameters must be trailing (once a parameter has a default, all subsequent parameters must also have defaults) because call syntax is positional-only in the current language subset,
-
and in the initial subset, default expressions are restricted to a constant/literal expression subset (no name references), so they can be inlined at call sites during lowering.
-
a parameter declared as
T?with a default has two effective behaviors in the initial subset: -
if the default expression has type
T, the parameter has effective typeT(the argument may be omitted at call sites, but the callee sees a non‑optional value), -
if the default expression is
None/Null, the parameter remainsT?(the argument may be omitted, and callers may still pass optional values explicitly). -
Varargs parameters are supported:
-
a varargs parameter is declared by prefixing the final parameter with
...(for examplefn f(x: int, ...rest: int) { ... }), -
only one varargs parameter is permitted per function and it must be final,
-
in the Supported forms, varargs parameters:
-
must have an explicit type annotation,
-
may not be
mut, -
and may not have a default expression.
-
The
fromstring literal is a module specifier (either"..."or`...`): -
strings starting with
./or../are treated as file specifiers and resolve to a module by file path, -
strings starting with
std/are treated as std package specifiers and resolve through package lookup after/is normalized to::(a trailing.slkis stripped for compatibility), -
other strings are dependency-rooted POSIX module paths matched against
[dependencies]keys. Dot-separated keys match slash-separated path prefixes (my.dep.bmatches"my/dep/b"), the longest matching key selects the dependency root, and the remainder is resolved under that dependency's source-module directory, -
quoted strings must not contain
::; package namespace specifiers use the unquotedPackagePathbranch ofImportFrom. -
BindingDecl ::= LetIntroducer Identifier TypeAnnotationOpt InitializerOpt ';' -
LetIntroducer ::= 'const' | 'let' LetModifierOpt | 'var' LetModifierOpt -
LetModifierOpt ::= LetModifier* -
LetModifier ::= 'mut' | 'move' -
LetDecl ::= ExportModifier BindingDecl -
TypeAnnotationOpt ::= (':' Type)? -
InitializerOpt ::= ('=' Expr)?
Type aliases are supported:
TypeDecl ::= ExportModifier 'type' TypeDeclKindOpt Identifier '=' Type ';'TypeDeclKindOpt ::= TypeDeclKindTypeDeclKind ::= 'struct' | 'enum' | 'error' | 'interface' | 'fn' | 'pure' 'fn'
test declarations are supported as Zig-inspired top-level test blocks:
TestDecl ::= 'test' StringLiteral? Block
FFI declarations are also part of the language grammar:
ExtDecl ::= ExportModifier 'ext' NameToken ExtExternNameOpt '=' Type ';'ExtExternNameOpt ::= StringLiteral
When ExtExternNameOpt is present, it sets the linked external symbol name.
This allows Silk code to bind a local name that differs from the C/FFI symbol
name (for example to avoid name collisions in wrapper modules).
The current compiler implementation supports external declarations (ext) whose type
is either:
- a
FunctionType(external functions, callable from Silk), or - a supported scalar type (external variables, readable as values in Silk).
Silk currently requires an initializer for runtime bindings
(let/var) and for compile-time constant bindings (const). Uninitialized
declarations like let x: int; / const x: int; are parsed but rejected by
the checker (see diagnostics, E2015).
Additionally, const initializers must be compile-time evaluable; otherwise
the compiler reports an error (see diagnostics, E2041).
In practice, prefer:
let x: int = 0;for a zero value, orlet x: T? = None;for an “empty” optional.
Struct declarations are also accepted by the current parser:
StructDecl ::= StructSpecs ExportModifier 'struct' Identifier GenericParamListOpt StructExtendsOpt ';' | StructSpecs ExportModifier 'struct' Identifier GenericParamListOpt StructExtendsOpt '{' StructFieldListOpt '}'StructSpecs ::= StructRequirement*StructRequirement ::= '#require' Expr ';'StructExtendsOpt ::= ('extends' QualifiedName)GenericParamListOpt ::= GenericParamListGenericParamList ::= '(' GenericParamListInnerOpt ')'GenericParamListInnerOpt ::= GenericParamListInnerGenericParamListInner ::= GenericParam (',' GenericParam)* ','?GenericParam ::= Identifier (':' Type)? ('=' Type)?StructFieldListOpt ::= StructFieldListStructFieldList ::= StructField (',' StructField)* ','?StructField ::= Identifier ':' Type StructFieldDefaultOptStructFieldDefaultOpt ::= ('=' Expr)
Notes:
- Only
#requiredirectives may appear inStructSpecsin the current language subset (#assure/#theoryare rejected onstruct).
Enum declarations are part of the core language design. They are specified in enums.
EnumDecl ::= ExportModifier 'enum' Identifier GenericParamListOpt '{' EnumVariantListOpt '}'EnumVariantListOpt ::= EnumVariantListEnumVariantList ::= EnumVariant (',' EnumVariant)* ','?EnumVariant ::= Identifier EnumVariantPayloadOptEnumVariantPayloadOpt ::= ('(' TypeListOpt ')')
Interface and impl declarations are part of the language design and are parsed by the front-end as the syntax is implemented:
-
InterfaceDecl ::= ExportModifier 'interface' Identifier GenericParamListOpt InterfaceExtendsOpt '{' InterfaceItem* '}' -
InterfaceExtendsOpt ::= ('extends' QualifiedName) -
InterfaceMethodDecl ::= 'fn' NameToken FnSignature ';' -
InterfaceItem ::= InterfaceMethodDecl | UsingDecl -
ImplDecl ::= 'impl' QualifiedName GenericParamListOpt ImplAsOpt '{' ImplMemberDecl* '}' -
ImplAsOpt ::= ('as' QualifiedName TypeArgListOpt) -
ImplMemberDecl ::= FnDecl | UsingDecl(within animplblock,exportis reserved for static members with noselfreceiver; instance method visibility usespublic/private)
Note: const-parameter-style generics (N: int parameters and integer literal
type arguments like Foo(u8, 1024)) remain tracked work; the front-end parses
these surface forms but the compiler currently focuses on type parameters
and monomorphization for type arguments.
Exception: the nominal optional form Option(T) is recognized as sugar for
T? and is accepted in the Supported forms.
-
Types (Supported forms):
-
Type ::= UnionType -
UnionType ::= TypeNoPipe ('|' TypeNoPipe)* -
TypeNoPipe ::= BaseType TypeSuffix -
TypeSuffix ::= TypeSuffixElem TypeSuffix -
TypeSuffixElem ::= OptionalTypeSuffix | ArrayTypeSuffix -
OptionalTypeSuffix ::= '?' | '??' -
ArrayTypeSuffix ::= '[' ']' | '[' ArrayLen ']' -
ArrayLen ::= IntLiteral | Identifier -
BaseType ::= ReferenceType | AttrFunctionType | FunctionType | CFunctionType | '(' Type ')' | SimpleType -
ReferenceType ::= '&' BaseType -
FunctionType ::= 'fn' '(' TypeListOpt ')' ResultTypeOpt -
CFunctionType ::= 'c_fn' '(' TypeListOpt ')' ResultTypeOpt -
AttrFunctionType ::= AttrAnnot FunctionType(in the Supported forms, this is accepted as sugar for selecting ABI variants such asattr(abi=c) fn (...) -> ...) -
TypeListOpt ::= TypeList -
TypeList ::= Type (',' Type)* -
SimpleType ::= PrimitiveType | NamedType -
PrimitiveType ::= 'bool' | 'i8' | 'u8' | 'i16' | 'u16' | 'i32' | 'u32' | 'i64' | 'u64' | 'i128' | 'u128' | 'int' | 'f32' | 'f64' | 'f128' | 'char' | 'string' | 'void' | 'Instant' | 'Duration' -
NamedType ::= QualifiedName TypeArgListOpt -
TypeArgListOpt ::= ('(' TypeArgListInnerOpt ')') -
TypeArgListInnerOpt ::= TypeArgListInner -
TypeArgListInner ::= TypeArg (',' TypeArg)* ','? -
TypeArg ::= Type | IntLiteral
This means that type annotations such as string? or int?? are parsed
into nested optional types. For simple nominal optionals, the parser also
recognizes Option(T) and desugars it to the same internal representation
as T?. Borrowed reference types (&T) are now parsed in type annotations.
Array/slice types (T[], T[N]) are
parsed and type-checked in the Supported forms (with element-type
restrictions), and are part of the implemented expression grammar via array
literals ([a, b, c]) and indexing (xs[i]). Function types
(fn (T, ...) -> R) are parsed as part of the Type grammar, and function
values are supported in the current lowering subset (including capturing
closures as a restricted scalar-only subset; see types and
memory model).
-
Statements (Supported forms):
-
Stmt ::= AttrAnnot* (LetStmt | LetElseStmt | SpecConstStmt | SpecAssertStmt | SpecTheoryDeclStmt | SpecTheoryStmt | AsyncBlockStmt | TaskBlockStmt | GpuLaunchStmt | ExprStmt | IfStmt | LoopStmt | WhileStmt | ForStmt | MatchStmt | ReturnStmt | PanicStmt | AssertStmt | BreakStmt | ContinueStmt) -
LetStmt ::= LetIntroducer LetBinder TypeAnnotationOpt InitializerOpt ';' -
LetElseStmt ::= ('let' LetModifierOpt | 'var' LetModifierOpt) MatchExprPattern TypeAnnotationOpt '=' Expr 'else' Block ';' -
LetBinder ::= Identifier | '_' | LetTupleBinder | LetStructBinder | LetArrayBinder | LetEnumBinder -
LetTupleBinder ::= '(' LetTupleBinderItemsOpt ')' -
LetTupleBinderItemsOpt ::= LetTupleBinderItem (',' LetTupleBinderItem)* ','? -
LetTupleBinderItem ::= Identifier | '_' -
LetStructBinder ::= '{' LetStructBinderItemsOpt '}' -
LetStructBinderItemsOpt ::= LetStructBinderItem (',' LetStructBinderItem)* ','? -
LetStructBinderItem ::= Identifier ('as' (Identifier | '_'))? -
LetArrayBinder ::= '[' LetArrayBinderItemsOpt ']' -
LetArrayBinderItemsOpt ::= LetArrayBinderItem (',' LetArrayBinderItem)* ','? -
LetArrayBinderItem ::= Identifier | '_' -
LetEnumBinder ::= QualifiedName '(' LetEnumBinderItemsOpt ')' -
LetEnumBinderItemsOpt ::= LetEnumBinderItem (',' LetEnumBinderItem)* ','? -
LetEnumBinderItem ::= Identifier | '_' -
SpecConstStmt ::= '#const' Identifier '=' Expr ';' -
SpecAssertStmt ::= '#assert' Expr ';' -
SpecTheoryDeclStmt ::= '#theory' Identifier '(' TheoryParamsOpt ')' '{' TheoryBodyItem* '}' -
SpecTheoryStmt ::= '#theory' QualifiedName '(' ArgListOpt ')' ';' -
AsyncBlockStmt ::= 'async' Block -
TaskBlockStmt ::= 'task' Block -
GpuLaunchStmt ::= GpuLaunchExpr ';'? -
GpuLaunchExpr ::= 'gpu' '(' GpuLaunchOption ',' GpuLaunchOption ')' '{' DirectKernelCall ';' '}' -
GpuLaunchOption ::= 'grid' '=' Expr | 'workspace' '=' Expr -
DirectKernelCall ::= Identifier '(' ArgListOpt ')' -
MutOpt ::= 'mut'? -
ExprStmt ::= Expr ';' -
IfStmt ::= 'if' IfCondition Block ('else' (IfStmt | Block))? -
IfCondition ::= Expr | IfLetCondition -
IfLetCondition ::= 'let' LetModifierOpt MatchExprPattern '=' Expr LetChainOpt -
LetChainOpt ::= ('&&' LetChainClause)* -
LetChainClause ::= 'let' LetModifierOpt MatchExprPattern '=' Expr | Expr -
LoopStmt ::= LoopPrefixOpt 'loop' Block -
LoopPrefixOpt ::= 'async' | 'task' -
WhileStmt ::= WhileSpecs 'while' WhileCondition Block -
WhileCondition ::= Expr | WhileLetCondition -
WhileLetCondition ::= 'let' LetModifierOpt MatchExprPattern '=' Expr LetChainOpt -
WhileSpecs ::= (LoopInvariant | LoopVariant | LoopMonovariant)* -
LoopInvariant ::= '#invariant' Expr ';' -
LoopVariant ::= '#variant' Expr ';' -
LoopMonovariant ::= '#monovariant' Expr ';' -
ForStmt ::= ForInStmt | ForCStmt -
ForInStmt ::= 'for' ForHead 'in' ExprNoRange (RangeOp ExprNoRange)? Block -
ForCStmt ::= 'for' '(' ForInit ';' Expr ';' Expr ')' Block -
ForInit ::= LetIntroducer Identifier TypeAnnotationOpt '=' Expr -
ForHead ::= ForBinder | 'let' MutOpt MatchExprPattern -
ForBinder ::= Identifier | '_' -
RangeOp ::= '..' | '..=' -
BlockStmt ::= Block -
Block ::= '{' Stmt* '}' -
ReturnStmt ::= 'return' ExprOpt ';' -
ExprOpt ::= Expr? -
PanicStmt ::= 'panic' QualifiedName StructLiteralSuffixOpt ';' -
AssertStmt ::= 'assert' Expr ';' | 'assert' '(' Expr (',' Expr)? ')' ';' -
BreakStmt ::= 'break' ';' -
ContinueStmt ::= 'continue' ';'
GpuLaunchExpr requires grid and workspace exactly once; the two named
options may appear in either order. Its body is deliberately narrower than a
general Block: it contains one unqualified, non-generic call. Semantic
checking requires that name to resolve to a launchable root-package
attr(device=gpu) function and checks the geometry and call arguments. The
form is valid in ordinary, async, and task host functions and dispatches
through std::gpu::launch_and_synchronize, which invokes synchronization
exactly once after launch. It has type std::gpu::DispatchResult; statement
position may omit the trailing semicolon and discards that value. See
gpu launch blocks.
LetModifierOpt accepts at most one mut and at most one move, in either
order. mut makes the introduced binders assignable. move consumes the
initializer/scrutinee for ownership-tracked values. var bindings are always
mutable; an explicit mut after var is accepted for symmetry, so
var move name = value;, var mut move name = value;, and
var move mut name = value; are the mutable-binding forms of
initialization-time ownership transfer. In pattern forms the consuming
modifier is written as let move Some(value) = maybe, if let move ...,
else if let move ..., chained && let move ..., or while let move ....
-
WithStmt ::= 'with' Identifier Block | 'with' WithBytes Block | 'with' WithBytes 'from' Identifier WithFromSliceOpt Block -
WithBytes ::= IntLiteral | '(' IntLiteral ')' -
WithFromSliceOpt ::= '[' IntLiteral '..' IntLiteralOpt ']' -
IntLiteralOpt ::= IntLiteral -
MatchStmt ::= 'match' Expr '{' MatchStmtArmListOpt '}' -
MatchStmtArmListOpt ::= MatchStmtArmList -
MatchStmtArmList ::= MatchStmtArm (',' MatchStmtArm)* ','? -
MatchStmtArm ::= MatchStmtPattern '=>' Block -
OptionalPattern ::= 'None' | 'Some' '(' (Identifier | '_') ')' -
MatchStmtPattern ::= OptionalPattern | '_' | Identifier | (Identifier | '_') ':' QualifiedName -
StructLiteralSuffixOpt ::= StructLiteralSuffix
Region declarations and with blocks are specified in regions.
match is implemented in two separate forms:
matchas an expression (arms are expressions; seeMatchExprbelow),matchas a statement (arms are blocks), used for typed errors as specified in typed errors.
In the Supported forms, the match statement form is restricted to a
call-expression scrutinee and the patterns listed above.
- Expressions (Supported forms):
Expressions follow a conventional precedence hierarchy, as implemented in
src/parser.zig:
Expr ::= AssignExprNoRange ::= AssignNoRangeAssign ::= Range (AssignOp Assign)?AssignNoRange ::= Coalesce (AssignOp AssignNoRange)?Range ::= Coalesce (RangeOp CoalesceOpt)? | RangeOp CoalesceOptCoalesceOpt ::= CoalesceAssignOp ::= '=' | '+=' | '-=' | '*=' | '/='Coalesce ::= LogicalOr ('??' CoalesceRhs)?CoalesceRhs ::= Coalesce | CoalesceTerminalCoalesceTerminal ::= 'return' ExprOpt | 'break' | 'continue'LogicalOr ::= LogicalAnd ('||' LogicalAnd)*LogicalAnd ::= BitOr ('&&' BitOr)*BitOr ::= BitXor ('|' BitXor)*BitXor ::= BitAnd ('^' BitAnd)*BitAnd ::= Equality ('&' Equality)*Equality ::= TypeTest (('==' | '!=') TypeTest)*TypeTest ::= Relational ('is' Type)?Relational ::= Shift (('<' | '<=' | '>' | '>=') Shift)*Shift ::= AddSub (('<<' | '>>') AddSub)*AddSub ::= MulDiv (('+' | '-') MulDiv)*MulDiv ::= Unary (('*' | '/' | '%') Unary)*Unary ::= ('!' | '~' | '-' | 'mut' | 'move' | 'new' | 'await' | 'yield' | 'sizeof' | 'alignof' | 'offsetof' | 'typename' | '&' | '*' | '++' | '--') Unary | PostfixPostfix ::= Primary PostfixSuffix*PostfixSuffix ::= CallSuffix | FieldSuffix | OptionalFieldSuffix | StructLiteralSuffix | IndexSuffix | SliceSuffix | CastSuffix | TrySuffix | IncDecSuffixCallSuffix ::= '(' CallArgsOpt ')'FieldSuffix ::= '.' NameToken
CoalesceTerminal is deliberately narrow. return, break, and
continue remain statements in the general language grammar and are only
admitted here as the immediate right-hand side of ??.
-
OptionalFieldSuffix ::= '?.' NameToken -
StructLiteralSuffix ::= '{' StructInitListOpt '}' -
IndexSuffix ::= '[' ExprNoRange ']' -
SliceSuffix ::= '[' SliceBoundOpt '..' SliceBoundOpt ']' -
SliceBoundOpt ::= ExprNoRange -
CastSuffix ::= 'as' RawOpt Type CastSliceLenOpt -
CastSliceLenOpt ::= '(' Expr ')' -
RawOpt ::= 'raw' -
TrySuffix ::= '?' -
IncDecSuffix ::= '++' | '--' -
StructInitListOpt ::= StructInitList -
StructInitList ::= StructInit (',' StructInit)* ','? -
StructInit ::= NameToken (':' Expr)? -
CallArgsOpt ::= CallArgs -
CallArgs ::= GenericArgListOpt ';' ArgListOpt | ArgList -
GenericArgListOpt ::= GenericArgList -
GenericArgList ::= GenericArg (',' GenericArg)* ','? -
GenericArg ::= Type | IntLiteral -
ArgListOpt ::= ArgList -
ArgList ::= Expr (',' Expr)*
Note: the parser treats mut <expr>, new <expr>, await <expr>, await * <expr>,
yield <expr>, yield * <expr>, sizeof <expr>, alignof <expr>, offsetof(Type, field_path),
typename <expr>, and prefix ++<expr> / --<expr> as unary expressions.
Note: CastSliceLenOpt is permitted only when Type is a slice type (T[])
or string, and RawOpt. It is used by unsafe pointer view casts
like ptr as u8[](/silk/docs/len) (slice view) and ptr as string(len) (string view).
-
The type checker currently permits
mut <expr>only in call arguments (and method receivers) when the corresponding parameter is declaredmutand is: -
a borrowed reference type (
mut r: &T), or -
a slice type (
mut s: T[]). -
The
move <expr>unary form is used for explicit ownership transfer; in the Supported forms it is restricted tomove <name>where<name>is a local binding. -
Binding declarations may also use
let move/var moveto request the same ownership transfer at initialization. Themutmodifier may appear before or aftermove, includinglet mut move value = source;and the redundant-but-acceptedvar mut move value = source;. This is especially useful for destructuring:let move (a, b) = pair;,let move Some(value) = maybe;,let move Some(value) = maybe else { ... };,if let move Some(value) = maybe { ... },else if let move Some(value) = maybe { ... }, andwhile let move Some(value) = next() { ... }. -
The type checker currently permits
new <expr>only when it can determine a concrete reference result type of the form&Struct. In the current implementation this happens either: -
from an expected
&Structtype context (for examplelet x: &Frame = new Frame{ ... };or as a call argument whose parameter type is&Struct) -
from the operand itself when it names the struct type (for example
let x = new Frame{ ... };orlet x = new Frame(...);)newis supported only in function bodies (not in top-levelletinitializers). -
Primary ::= IntegerLiteral | DurationLiteral | FloatLiteral | StringLiteral | RegexpLiteral | CharLiteral | 'true' | 'false' | 'None' | 'null' | 'Some' '(' Expr ')' | ArrayLiteral | IfExpr | MatchExpr | FnExpr | AttrQueryExpr | AsmExpr | EmbedExpr | '(' Expr ')' | InferredStructLiteral | QualifiedName -
AttrQueryExpr ::= 'attr' '(' AttrItemListOpt ')' -
EmbedExpr ::= '#embed' '(' StringLiteral EmbedEncodingOpt ')' -
EmbedEncodingOpt ::= ',' EmbedEncoding -
EmbedEncoding ::= StringLiteral
#embed paths are resolved relative to the containing Silk source file.
Absolute paths are accepted directly. If the parser has no source-file path
because it is parsing an in-memory buffer, relative paths are resolved from
the current working directory. The compiler rejects empty, unreachable, or
unreadable paths. #embed("file") and #embed("file", "utf8") validate
UTF-8 input and produce a string; omitting the encoding is equivalent to
"utf8". #embed("file", "utf16") decodes UTF-16 into a UTF-8 string;
#embed("file", "u8"), #embed("file", "u16"), and
#embed("file", "u32") produce compiler-owned array values for
array-typed use sites such as
let bytes: u8[] = #embed("./data.bin", "u8");. Multi-byte integer
encodings read file bytes as little-endian element values; the compiler
carries the file bytes as embed metadata instead of expanding them into
source-level integer literal nodes. Without an expected array type, a raw
integer embed infers a dynamic slice of the requested element type. The
encoding literal value must be one of "utf8", "utf16", "u8", "u16",
or "u32".
RegexpLiteral ::= '/' RegexpBody '/' RegexpFlagsOptRegexpFlagsOpt ::= Identifier
Notes:
-
RegexpBodyis scanned by the parser (not the lexer): it is the byte span between the opening and closing/, where the closing delimiter is the first unescaped/that is not inside a character class ([...]). -
ArrayLiteral ::= '[' ExprListOpt ']' -
ExprListOpt ::= ExprList -
ExprList ::= Expr (',' Expr)* ','? -
QualifiedName ::= GlobalPrefixOpt NameToken ('::' NameToken)* -
GlobalPrefixOpt ::= '::' -
InferredStructLiteral ::= '{' StructInitListOpt '}' -
FnExpr ::= 'fn' '(' LambdaParamListOpt ')' ( '->' LambdaBody | Block ) -
LambdaParamListOpt ::= LambdaParamList -
LambdaParamList ::= LambdaParam (',' LambdaParam)* ','? -
LambdaParam ::= Identifier ':' Type -
LambdaBody ::= Type Block | Expr
Disambiguation rule (current parser):
fn (...) -> Type Blockis treated as the block-body form only when the return type is followed immediately by{(starting the block).- Otherwise,
fn (...) -> Expris treated as an expression-body function expression and its result type is inferred by the checker. fn (...) Blockis treated as the block-body form with an implicitvoidresult type (shorthand forfn (...) -> void Block).
Notes:
-
InferredStructLiteralhas the same token-level shape asStructLiteralSuffix(used forType{ ... }), but appears as aPrimaryexpression with no explicit type name. The type checker requires an expected struct type context to resolve the literal’s target type. -
To avoid ambiguity with statement blocks, the parser only recognizes
InferredStructLiteralwhen the{ ... }contents look like a struct initializer list (or are{}): either the first token after{is}or it is anIdentifierfollowed by:(explicit initializer) or followed by,/}(shorthand initializer). -
Non-adjacent explicit struct literals (
Type { ... }) are suppressed only at the immediate expression-before-block boundary used by block-bearing constructs. Delimited subexpressions restore ordinary expression parsing, so forms such as[Type { field: value }]andcall(Type { field: value })remainStructLiteralSuffixexpressions even when the surrounding expression is followed by a block. -
MatchExpr ::= 'match' Expr '{' MatchArmListOpt '}' -
MatchArmListOpt ::= MatchArmList -
MatchArmList ::= MatchArm (',' MatchArm)* ','? -
MatchArm ::= MatchExprPattern '=>' Expr -
MatchExprPattern ::= OptionalPattern | EnumVariantPattern | ResultPattern | TypedBinderPattern -
IfExpr ::= 'if' Expr IfExprBlock 'else' (IfExpr | IfExprBlock) -
IfExprBlock ::= '{' Expr '}' -
AsmExpr ::= 'asm' StringLiteral -
ResultPattern ::= ('Ok' | 'Err') '(' (Identifier | '_') ')' -
EnumVariantPattern ::= QualifiedName EnumVariantBinderListOpt -
EnumVariantBinderListOpt ::= ('(' EnumVariantBinderListInnerOpt ')') -
EnumVariantBinderListInnerOpt ::= EnumVariantBinderList -
EnumVariantBinderList ::= EnumVariantBinder (',' EnumVariantBinder)* ','? -
EnumVariantBinder ::= Identifier | '_' -
TypedBinderPattern ::= (Identifier | '_') ':' TypeNoPipe -
Declarations (Supported forms additions):
-
Decl ::= ... | ErrorDecl -
ErrorDecl ::= ExportModifier 'error' Identifier '{' StructFieldListOpt '}'
This matches the current AST and checker:
PrimaryconstructsLiteralorNameexpressions (or a parenthesizedExpr),- unary expressions are represented as
UnaryExprwith a token kind indicating the operator, - binary expressions are represented as
BinaryExprwith a token kind indicating the operator, - identifiers and qualified names are stored as
NameExprwith the full slice of source text (e.g.util::answer), - simple function calls such as
helper()orutil::helper(1, 2)are parsed as call expressions using thePostfix/CallSuffixproductions; the compiler supports calls to named functions, but the type checker and back-end currently restrict which value types can appear at call boundaries; see cli silk for the exact supported subset.
Further expression forms (ranges, etc.) are described in other
language concept documents and in operators. The current
parser now accepts ?. optional field access (opt?.field) and the initial
match expression form as part of the implemented optional subset, but other
expression forms will be added here as they are implemented.
Role of This File#
This document serves as the reference for:
- lexer implementation (token categories and reserved words),
- parser implementation (production rules and precedence),
- pretty-printer or formatter behavior.
As the parser and lexer are implemented, this file must be updated with:
- the exact grammar that the compiler accepts (including any temporary limitations),
- clarifications or corrections discovered during implementation (recorded here so this file remains canonical),
- notes about desugaring and how surface constructs map into the internal AST,
- clear indication of which productions are implemented today vs. planned future work, so that downstream users can see both the full language design and the currently supported subset.
Source repository · Edit this page · View Markdown