Browse documentation

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.Member may denote enum values or variant tags.
    • value.Member denotes a variant payload unwrap when value has 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, and None are 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.5 is valid while .5 is 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:

  • int
  • double
  • bool
  • char
  • String
  • Region
  • Site

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:

  • extern and throws may prefix func.
  • extern init func ... declares the default initializer for an opaque extern struct return type.
  • init is valid only with extern func.
  • extern may prefix struct.
  • 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, while case 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 with other: 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 to x?.

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.VALUE is initially parsed as member access, then rewritten as an enum value during semantic analysis.
  • VariantName.TAG is initially parsed as member access, then rewritten as a variant tag marker during semantic analysis. Constructing a variant value always requires braces.
  • value.TAG remains member access and is resolved as checked payload access when value has a variant type.

Current Implementation Gaps and Constraints

  • Top-level typed globals are not currently parsed.
  • match supports payload-binding and payloadless variant-tag patterns; richer patterns are not part of the current parser.