Jik Grammar Reference
This document is an implementation-derived grammar reference for Jik. It is based on the current lexer and parser in src/jik/lexer.c and src/jik/parser.c.
It is intended to describe the accepted surface syntax of the language. It is not yet a complete formal language specification, because some forms are resolved later during semantic analysis.
Scope
- This grammar describes syntax, not full typing or region semantics.
- Some constructs are context-sensitive. In particular:
Type.Membermay denote enum values or variant tags.value.Memberdenotes a variant payload unwrap whenvaluehas a variant type.- allocation suffixes such as
[r],[.x], and@are syntactically regular, but semantically restricted.
- Newlines are significant between statements and after block headers.
Lexical Structure
Comments
comment ::= "//" { any-char-except-newline }
Identifiers
identifier ::= letter { letter | digit | "_" } | "_"
qualified_identifier ::= identifier [ "::" identifier ]
Notes:
_is a reserved standalone token for the local region.Vec,Dict,Option,Some, andNoneare reserved keywords.- Identifiers are currently ASCII-only.
Literals
digit_sequence ::= digit { digit | "_" digit }
integer_literal ::= digit_sequence
float_literal ::= digit_sequence "." digit_sequence
| digit_sequence [ "." digit_sequence ] exponent
exponent ::= ("e" | "E") [ "+" | "-" ] digit_sequence
char_literal ::= "'" char-char "'"
string_literal ::= '"' { string-char } '"'
ml_string ::= '"""' { any-char } '"""'
Notes:
- The current lexer requires a float to begin with a digit, so
0.5is valid while.5is not. - Numeric separators (
_) are allowed only between digits and are ignored in the value. - Character escapes support
\n,\t,\r,\0,\xNN,\\,\', and\". - Ordinary string literals support
\n,\t,\r,\xNN,\\,\', and\". - Ordinary and multiline string literals may contain UTF-8 text.
- Source files that contain non-ASCII text should be saved as UTF-8.
- Ordinary string literals do not support
\0; Jik strings are currently null-terminated. - String length, indexing, and slicing are byte-based.
Embedded C
embed_block ::= "@embed" "{" identifier "}" newline
{ any-line }
identifier newline?
The closing delimiter must appear alone on its line, apart from trailing whitespace.
Embedded C is copied through to the generated C translation. Non-ASCII text in embedded C therefore remains dependent on the host C compiler's handling of C source encoding.
Build Directives
build_directive ::= platform_directive | profile_directive | path_directive | link_directive
platform_directive ::= "@platform" "(" ( "all" | "windows" | "linux" ) ")" newline
profile_directive ::= "@profile" "(" ( "all" | "windows-x64-mingw" | "linux-x64-gnu" ) ")" newline
path_directive ::= ( "@includedir" | "@libdir" | "@copy" )
"(" string_literal { "," string_literal } ")" newline
link_directive ::= "@link" "(" string_literal { "," string_literal } ")" newline
Build directives are top-level declarations. Each physical module starts with platform and profile all. @platform(windows) or @platform(linux) selects the platform for subsequent build directives in that module, while @platform(all) returns to platform-independent requirements. Platform state does not cross module boundaries and does not conditionally compile Jik declarations or embedded C.
@profile(windows-x64-mingw) and @profile(linux-x64-gnu) restrict subsequent native build directives to a compatible host C compiler target. @profile(all) removes that restriction. A profile-restricted directive is active only when both its platform and profile match. jik build and jik run detect the selected compiler target before invoking the compiler and report an incompatible native module early. jik check and jik tran only parse these directives; they do not probe a compiler. MSVC is not currently a supported native build profile.
Relative include directories, library directories, and copied files are resolved from the source file containing the directive. Directives from all reachable modules are applied by build and run.
Types
type_desc ::= named_type [ alloc_suffix ]
| "Vec" "[" type_desc "]" [ alloc_suffix ]
| "Dict" "[" type_desc "]" [ alloc_suffix ]
| "Option" "[" type_desc "]" [ alloc_suffix ]
named_type ::= qualified_identifier
Named types include built-in names such as:
intdoubleboolcharStringRegionSite
They also include user-defined struct, enum, and variant names.
Allocation Suffixes
Composite literals and some type descriptions may be followed by an allocation suffix:
alloc_suffix ::= "[" identifier "]"
| "[" "." identifier "]"
| "@"
Examples:
Point{}[r]
Point{}[.p]
Some{"x"}[r]
None[r]
[1, 2, 3]@
The parser also accepts the local region value _ as an ordinary expression, but bracketed allocation suffix syntax expects either id or .id.
Postfix @ selects the enclosing function's implicit region. It is available only where the same-region rule establishes one shared region from the function's non-foreign composite and Region parameters. It is invalid outside a function and in compiler-proven region-safe functions.
Allocation suffix syntax takes precedence immediately after composite literals. As a result, forms like [1, 2][0] are not parsed as direct subscripting of a literal. Composite literals must first be bound to a name before subscripting, member access, mutation, or iteration.
Program Structure
program ::= { newline | use_decl | global_decl | top_decl | variant_decl
| enum_decl | table_decl | embed_block | build_directive }
Imports
use_decl ::= "use" string_literal [ "as" identifier ]
Globals
global_decl ::= identifier ":=" expr
Top-level typed globals such as x: int are not parsed directly. The current parser accepts only top-level := declarations.
Top-Level Declarations
top_decl ::= decl_prefix ( func_decl | struct_decl )
decl_prefix ::= { "extern" | "throws" | "init" }
Current parser restrictions:
externandthrowsmay prefixfunc.extern init func ...declares the default initializer for an opaque extern struct return type.initis valid only withextern func.externmay prefixstruct.throws struct ...is rejected.
Functions
func_decl ::= "func" identifier "(" [ params ] ")" [ "->" type_desc ] ":"
newline block "end"
Extern functions use a different form:
extern_func_decl ::= "extern" [ "init" | "throws" ] "func"
identifier "as" identifier
"(" [ extern_params ] ")"
"->" type_desc
Parameters
params ::= param { "," param }
param ::= [ "foreign" ] identifier [ ":" type_desc ]
extern_params ::= extern_param { "," extern_param }
extern_param ::= [ "foreign" ] identifier ":" type_desc
Notes:
- Ordinary function parameters may omit type annotations.
- Extern function parameters must have type annotations.
Structs
struct_decl ::= "struct" identifier ":" newline
{ identifier ":" type_desc newline }
"end"
extern_struct_decl ::= "extern" "struct" identifier "as" identifier
Variants
variant_decl ::= "variant" identifier ":" newline
{ identifier [ ":" type_desc ] newline }
"end"
Enums
enum_decl ::= "enum" identifier ":" newline
{ identifier newline }
"end"
Tables
table_decl ::= "table" identifier "[" type_desc "]"
"->" type_desc ":" newline
{ table_entry }
"end"
table_entry ::= identifier ":" expr newline
The key type must resolve to an enum or variant. Every enum member or variant tag must occur exactly once. A table lookup uses ordinary subscript syntax, such as Names[key]. Table entries cannot be assigned through, contain function calls (including built-ins), or read global variables.
Blocks and Statements
block ::= { statement newline }
The current parser expects at least one newline after each statement in a block.
Statements
statement ::= return_stmt
| if_stmt
| match_stmt
| while_stmt
| for_stmt
| break_stmt
| continue_stmt
| try_stmt
| decl_stmt
| assign_stmt
| call_stmt
| must_call_stmt
return_stmt ::= "return" [ expr ]
break_stmt ::= "break"
continue_stmt ::= "continue"
decl_stmt ::= identifier ":=" expr
| identifier ":=" "try" call_expr
| identifier ":" type_desc
| identifier ":" type_desc "=" expr
assign_stmt ::= lvalue assign_op expr
assign_op ::= "=" | "+=" | "-=" | "*=" | "/=" | "%="
call_stmt ::= call_expr
must_call_stmt ::= "must" call_expr
Valid lvalues in the parser are:
lvalue ::= identifier
| primary "." identifier
| primary "[" expr "]"
If
if_stmt ::= "if" expr ":" newline block
{ "elif" expr ":" newline block }
[ "else" ":" newline block ]
"end"
While
while_stmt ::= "while" expr ":" newline block "end"
For
for_stmt ::= "for" identifier "=" expr "," expr ":" newline block "end"
| "for" identifier "in" expr ":" newline block "end"
| "for" identifier "," identifier "in" expr ":" newline block "end"
In the two-name form, strings bind index, ch, vectors bind index, value, and dictionaries bind key, value.
Try / Except
try_stmt ::= "try" call_expr
| "try" ( call_expr | identifier ":=" call_expr )
":" newline block
"except" ":" newline block
"end"
try f(...) and x := try f(...) propagate a failure from f to the enclosing throws func. Unlike a try ... except ... end declaration, x belongs to the enclosing scope because execution can continue past the declaration only when f succeeds.
The call-only propagation form has no trailing colon. A colon starts the local try ... except ... end form.
Match
match_stmt ::= "match" expr ":" newline
{ case_clause }
[ other_clause ]
"end"
case_clause ::= "case" variant_pattern ":" newline block
| "case" enum_pattern ":" newline block
other_clause ::= "other" ":" newline block
variant_pattern ::= identifier
| identifier "{" identifier "}"
enum_pattern ::= identifier
Current implementation note:
case TAG{x}:binds a variant payload, whilecase TAG:ignores a payload or matches a payloadless variant tag or enum member.- The matched value determines the owner and whether a case denotes a variant tag or enum member. Enum matches require every member exactly once and do not permit payload bindings.
other:is optional, may appear only once as the final match arm, and handles every tag not covered by an explicit case. A match withother:need not be exhaustive.
Expressions
The parser uses the following precedence, from lowest to highest.
Ternary
expr ::= ternary
ternary ::= logical_or [ "if" expr "else" ternary ]
This yields expressions such as:
2 if x == 3 else 12
Logical Operators
logical_or ::= logical_and { "or" logical_and }
logical_and ::= is_expr { "and" is_expr }
is
is_expr ::= comparison [ "is" is_target ]
is_target ::= "Some"
| "None"
| identifier
| qualified_identifier "." identifier
Examples:
x is Some
x is None
v is INT
Comparison and Arithmetic
comparison ::= term { ("==" | "!=" | "<" | ">" | "<=" | ">=") term }
term ::= factor { ("+" | "-") factor }
factor ::= unary { ("*" | "/" | "%") unary }
unary ::= ( "not" | "-" | "must" ) unary | primary
Primary Expressions
primary ::= atom { postfix }
postfix ::= "." identifier [ "(" [ args ] ")" ]
| "[" expr "]"
| "[" [ expr ] ":" [ expr ] "]"
| "?"
| "!"
Examples:
point.x
values[i]
values[1:3]
values[:3]
values[1:]
values[:]
opt?
load_config()!
v.INT
Atoms
atom ::= integer_literal
| float_literal
| char_literal
| "true"
| "false"
| string_literal [ alloc_suffix ]
| ml_string [ alloc_suffix ]
| "Some" "{" expr "}" [ alloc_suffix ]
| "None" [ alloc_suffix ]
| qualified_identifier
| call_expr
| struct_literal [ alloc_suffix ]
| variant_literal [ alloc_suffix ]
| "(" expr ")"
| vector_literal [ alloc_suffix ]
| dict_literal [ alloc_suffix ]
| "." identifier
| "_"
Calls
call_expr ::= qualified_identifier "(" [ args ] ")"
args ::= expr { "," expr }
receiver.name(args) is a uniform function call. push, pop, len, clear, and copy support this form for any receiver type accepted by the corresponding builtin. Otherwise, receiver must have a struct or variant type and name must be an ordinary function in that type's defining module whose first parameter has an explicit annotation for the receiver's type. It is shorthand for a normal call with receiver as the first argument.
Special implementation rule:
unwrap(x)is parsed specially and treated as equivalent tox?.
Struct Literals
struct_literal ::= qualified_identifier "{"
[ struct_field { "," struct_field } ]
"}"
struct_field ::= identifier "=" expr
Variant Literals
variant_literal ::= qualified_identifier "." identifier "{" [ expr ] "}"
Examples:
Value.INT{3}
Value.TEXT{"hi"}[r]
Value.INT{}
Value.EOF{}
Vector Literals
vector_literal ::= "[" expr "of" expr "]"
| "[" expr { "," expr } "]"
Examples:
[10 of 0]
[1, 2, 3]
Dictionary Literals
dict_literal ::= "{"
[ dict_entry { "," dict_entry } [ "," ] ]
"}"
dict_entry ::= expr ":" expr
Keys are checked semantically and must be of type String.
Semantic Resolution Notes
These forms are parsed first and refined later:
EnumName.VALUEis initially parsed as member access, then rewritten as an enum value during semantic analysis.VariantName.TAGis initially parsed as member access, then rewritten as a variant tag marker during semantic analysis. Constructing a variant value always requires braces.value.TAGremains member access and is resolved as checked payload access whenvaluehas a variant type.
Current Implementation Gaps and Constraints
- Top-level typed globals are not currently parsed.
matchsupports payload-binding and payloadless variant-tag patterns; richer patterns are not part of the current parser.