Skip to content

SYX Compiler — Syntax Analysis ​

简体中文

1. Overview ​

The parser is the second stage of the SYX compiler pipeline. It converts the Token stream from syx-lexer into an abstract syntax tree (AST).

The current implementation lives in crates/syx-parser/, using recursive descent for declarations and statements, plus precedence climbing for expressions. AST nodes are defined in crates/syx-ast/.

  • Entry: Parser::parse(source: SourceFile) -> SyxResult<Module>
  • Internal flow: lexical scan → parse_module() → top-level declaration list
  • Dependencies: syx-lexer, syx-ast, syx-diagnostics, smol_str

2. Architecture ​

2.1 Parse pipeline ​

mermaid
flowchart TD
    A["SourceFile"] --> B["Lexer::scan_all()"]
    B --> C["Vec&lt;Token&gt;"]
    C --> D["Parser::new()"]
    D --> E["parse_module()"]
    E --> F["Module { decls }"]
    F --> G["Later compile stages"]

2.2 Parser struct ​

rust
// crates/syx-parser/src/lib.rs
pub struct Parser {
    source: SourceFile,
    tokens: Vec<Token>,
    current: usize,
}

The parser has no separate error recovery or formatter; errors return directly as SyxDiagnostic (code P001).

2.3 Layered parse strategy ​

                    parse_module()
                         │
              ┌──────────┴──────────┐
              │    parse_decl()     │  ← recursive descent (top-level / member decls)
              └──────────┬──────────┘
                         │
         ┌───────────────┼───────────────┐
         │               │               │
   parse_function   parse_class    parse_protocol ...
         │               │
         └───────┬───────┘
                 │
           parse_block_stmts()
                 │
            parse_stmt()          ← recursive descent (statement dispatch)
                 │
            parse_expr()
                 │
         parse_precedence(0)      ← precedence climbing (infix operators)
                 │
            parse_unary()
                 │
            parse_postfix()        ← postfix: call, member, optional chain, subscript
                 │
            parse_primary()        ← literals, identifiers, parens, arrays, dicts

2.4 Newline handling ​

SYX uses newlines as statement separators. The parser handles TokenKind::Newline via:

  • skip_newlines(): advance current past consecutive newlines
  • peek_index(): index of the next non-newline token
  • peek(): effective current token from peek_index()

Block bodies (function, class, etc.) call skip_newlines() between statements/members.

3. AST nodes (syx-ast) ​

The AST is defined in crates/syx-ast/src/lib.rs.

3.1 Module — compilation unit ​

rust
pub struct Module {
    pub name: SmolStr,      // Currently fixed to "main"
    pub decls: Vec<Decl>,   // Top-level declarations
}

3.2 Decl — top-level / member declarations ​

rust
pub enum Decl {
    Function {
        span: Span,
        access: AccessLevel,
        name: SmolStr,
        params: Vec<Param>,
        return_type: Option<TypeExpr>,
        effect: Option<FunctionEffect>,  // Pure | Io
        body: Vec<Stmt>,
        is_async: bool,
    },
    Import { span, module },
    Class { span, access, name, members: Vec<Decl> },
    Struct { span, access, name, fields: Vec<(SmolStr, TypeExpr)> },
    Protocol { span, name, methods: Vec<Decl> },
    VarGlobal { span, access, mutable, name, ty, init },
    Extension { span, type_name, methods: Vec<Decl> },
    DataClass { span, access, name, fields: Vec<(SmolStr, TypeExpr)> },
    SealedClass { span, access, name, variants: Vec<(SmolStr, Vec<(SmolStr, TypeExpr)>)> },
    Actor { span, access, name, members: Vec<Decl> },
}

3.3 Stmt — statements ​

rust
pub enum Stmt {
    Expr { span, expr },
    VarDecl { span, mutable, name, ty, init },
    Return { span, value },
    If { span, condition, then_branch, else_branch },
    IfLet { span, name, pattern, body, else_branch },
    GuardLet { span, name, pattern, body, else_branch },
    While { span, condition, body },
    ForIn { span, name, iterable, body },
    Block { span, stmts },
    Break { span },
    Continue { span },
    Try { span, body, catch_name, catch_body },
    Switch { span, subject, cases, default },
    Print { span, expr },   // print(...) sugar
}

3.4 Expr — expressions ​

rust
pub enum Expr {
    Literal { span, value: Literal },
    Ident { span, name },
    Binary { span, left, op: BinOp, right },
    Unary { span, op: UnaryOp, expr },
    Call { span, callee, args },
    Member { span, object, name },
    Index { span, object, index },
    Assign { span, name, value },
    Group { span, expr },
    Array { span, elements },
    Await { span, expr },
    OptionalChain { span, object, name },
    ForceUnwrap { span, expr },
    HalfOpenRange { span, start, end },       // switch range patterns
    VariantPattern { span, variant, binding }, // switch variant patterns
}

3.5 Auxiliary types ​

rust
pub enum AccessLevel { Public, Internal, Private }
pub enum FunctionEffect { Pure, Io }
pub enum BinOp { Add, Sub, Mul, Div, Mod, Eq, NotEq, Lt, LtEq, Gt, GtEq, And, Or, NilCoalesce }
pub enum UnaryOp { Neg, Not, Try }

pub struct Param {
    pub name: SmolStr,
    pub ty: Option<TypeExpr>,
    pub default: Option<Expr>,
}

pub enum TypeExpr {
    Named { name, generics: Vec<TypeExpr> },
    Optional(Box<TypeExpr>),
    Array(Box<TypeExpr>),
    Dictionary { key: Box<TypeExpr>, value: Box<TypeExpr> },
    Function { params: Vec<TypeExpr>, returns: Option<Box<TypeExpr>> },
    Void,
}

pub enum Literal {
    Integer(i64), Double(f64), String(String),
    Interpolated(Vec<StringPart>),
    Regex(String),
    Dict(Vec<(String, Expr)>),
    Bool(bool), Nil,
}

4. Detailed design ​

4.1 Module parsing ​

rust
pub fn parse(source: SourceFile) -> SyxResult<Module> {
    let tokens = Lexer::new(source.clone()).scan_all()?;
    let mut parser = Self::new(source, tokens);
    parser.parse_module()
}

fn parse_module(&mut self) -> SyxResult<Module> {
    let mut decls = Vec::new();
    while !self.is_at_end() {
        self.skip_newlines();
        if self.is_at_end() { break; }
        decls.push(self.parse_decl()?);
    }
    Ok(Module { name: SmolStr::new("main"), decls })
}

BNF mapping: §5.0 (metasymbols), §5.1 (<module>)

4.2 Declaration parsing parse_decl() ​

Dispatched by lookahead token:

LookaheadParse methodProduces Decl
importparse_import()Decl::Import
public/internal/privateparse_access() + parse_decl_with_access()Decl with access level
funcparse_function(..., false)Decl::Function
asyncconsume then require funcDecl::Function { is_async: true }
classparse_class()Decl::Class
datarequire class → parse_data_class()Decl::DataClass
sealedrequire class → parse_sealed_class()Decl::SealedClass
extensionparse_extension()Decl::Extension
actorparse_actor()Decl::Actor
structparse_struct()Decl::Struct
protocolparse_protocol()Decl::Protocol
var/letparse_global_var()Decl::VarGlobal

Function declarations ​

swift
async func fetch(url: String) -> Data requires Pure {
    return await load(url)
}

Parse order: name → ( params ) → optional -> return_type → optional requires Pure|IO → { body }

Data class ​

swift
data class Point(x: Int, y: Int)

Primary constructor parameter list: name: Type, ..., no method body.

Sealed class ​

swift
sealed class Result {
    success(value: String)
    failure(code: Int)
}

Variant names may be followed by (field: Type, ...) field lists.

Extension ​

swift
extension Int {
    func doubled() -> Int {
        return self * 2
    }
}

The target is a single identifier type name (e.g. Int, String), read by consume_identifier, not a full <type> expression; method bodies recurse through parse_function.

Protocol ​

swift
protocol Drawable {
    func draw() {
    }
}

Protocol methods are parsed by parse_function and must include { } bodies (may be empty); the current parser does not support signature-only declarations without bodies.

Actor ​

swift
actor Counter {
    var count: Int = 0
    func increment() { ... }
}

Actor bodies recursively parse member declarations (parse_decl).

Parser entry points vs output ​

Parse functionOutput typeNotes
parse_moduleModuleTop-level decl list
parse_declDeclLookahead dispatch
parse_importDecl::Import
parse_functionDecl::FunctionAlso extension/protocol members
parse_data_classDecl::DataClassParen field list, no { }
parse_sealed_classDecl::SealedClassVariants + optional associated fields
parse_extensionDecl::ExtensionTarget is <ident>
parse_protocolDecl::Protocol
parse_actorDecl::ActorMembers recurse parse_decl
parse_classDecl::ClassMembers recurse parse_decl
parse_structDecl::StructBrace field list
parse_global_varDecl::VarGlobalTop-level var/let
parse_paramsVec<Param>Optional types and defaults

BNF mapping: §5.1 (<decl>), §5.2 (<function>, <param>)

4.3 Statement parsing parse_stmt() ​

LookaheadParse methodProduces Stmt
var/letparse_var_decl()Stmt::VarDecl
returnparse_return()Stmt::Return
ifparse_if_or_if_let()Stmt::If or Stmt::IfLet
guardparse_guard_let()Stmt::GuardLet
switchparse_switch()Stmt::Switch
whileparse_while()Stmt::While
forparse_for_in()Stmt::ForIn
tryparse_try()Stmt::Try
break/continueconsume directlyStmt::Break/Continue
{parse_block_stmts()Stmt::Block
expression startparse_expr()Stmt::Print (print call) or Stmt::Expr

For-in loop ​

swift
for item in collection {
    print(item)
}

Produces Stmt::ForIn { name, iterable, body }.

Try/catch ​

swift
do {
    risky()
} catch (e) {
    print(e)
}

Produces Stmt::Try { body, catch_name, catch_body }. After catch, (identifier) is required.

If / if let ​

swift
if (x > 0) {
    ...
} else {
    ...
}

if let value = optional {
    ...
}
  • Plain if: condition must be wrapped in ()
  • if let: if let name = pattern { ... } with optional else { ... }

Guard let ​

swift
guard let x = opt else {
    return
}
{
    use(x)
}

Parses the else block first, then the main block.

Switch and patterns ​

swift
switch value {
    case 1:
        print("one")
    case 2..<5:
        print("few")
    case .success(let msg):
        print(msg)
    default:
        print("other")
}

parse_switch_pattern() supports:

Pattern formAST nodeExample
Integer literalExpr::Literal { Integer }case 1:
Half-open rangeExpr::HalfOpenRange { start, end }case 2..<5:
Variant bindingExpr::VariantPattern { variant, binding }case .foo(let x):
Any expressionrecursive parse_expr()case someExpr:
Parse functionOutput type
parse_stmtStmt
parse_var_declStmt::VarDecl
parse_returnStmt::Return
parse_if_or_if_letStmt::If / Stmt::IfLet
parse_guard_letStmt::GuardLet
parse_switchStmt::Switch
parse_switch_patternExpr (pattern node)
parse_whileStmt::While
parse_tryStmt::Try
parse_for_inStmt::ForIn
parse_block_stmtsVec<Stmt>

BNF mapping: §5.4 (<stmt>), §5.5 (<switch-pattern>)

4.4 Expression parsing ​

Precedence climbing parse_precedence() ​

rust
fn parse_precedence(&mut self, min_prec: u8) -> SyxResult<Expr> {
    let mut left = self.parse_unary()?;
    loop {
        let Some((op, prec)) = infix_op(&self.peek().kind) else { break };
        if prec < min_prec { break; }
        self.advance();
        let right = self.parse_precedence(prec + 1)?;
        left = Expr::Binary { span, left: Box::new(left), op, right: Box::new(right) };
    }
    Ok(left)
}

infix_op() precedence table:

PrecedenceOperatorBinOp
1||Or
2&&And
3== !=Eq NotEq
4< <= > >=Lt LtEq Gt GtEq
5+ -Add Sub
6* / %Mul Div Mod
7??NilCoalesce

Unary expressions parse_unary() ​

PrefixUnaryOp / node
awaitExpr::Await
tryExpr::Unary { op: Try }
-Expr::Unary { op: Neg }
!Expr::Unary { op: Not }

Postfix expressions parse_postfix() ​

Loop postfix operations until none match:

PostfixAST node
(...)Expr::Call
.nameExpr::Member
?.nameExpr::OptionalChain
!Expr::ForceUnwrap
?Expr::Unary { op: Try } (same as prefix try expr; syx format canonical form is postfix expr?)
[index]Expr::Index

= after an identifier is detected on the first postfix iteration as Expr::Assign (only simple identifier lvalues ident = expr; no obj.field = v compound lvalues).

Half-open range expressions ​

Integer followed by ..< and end integer, in both parse_primary() and parse_switch_pattern():

swift
let r = 1..<10        // Expr::HalfOpenRange (expression position)
switch n {
    case 1..<10:       // same, for switch case
        ...
}

Optional chaining ​

swift
user?.name

Lexer emits Question + Dot + Identifier; the parser recognizes ?. and builds Expr::OptionalChain { object, name }.

Primary expressions parse_primary() ​

TokenAST
Literal tokenExpr::Literal
identifierExpr::Ident
( expr )Expr::Group
[e1, e2, ...]Expr::Array
{ "k": v, ... }Expr::Literal { Dict }
integer ..< integerExpr::HalfOpenRange

Dictionary literals parse_dict_literal() ​

swift
let map = { "name": "SYX", "version": 1 }

Keys must be string literals "key"; values via parse_expr(). Produces Literal::Dict(entries).

Parse functionOutput type
parse_expr / parse_precedenceExpr
parse_unaryExpr
parse_postfixExpr
parse_primaryExpr
parse_dict_literalExpr::Literal { Dict }
parse_embedded_exprExpr (interpolation sub-expressions)
parse_argsVec<Expr>

BNF mapping: §5.6 (<expr>), §5.7 (<literal>), §5.8 (terminal mapping)

Interpolated string re-parse ​

For each InterpPart::Expr in TokenKind::InterpolatedString, parse_embedded_expr() spins a sub-parser:

rust
fn parse_embedded_expr(&self, raw: &str) -> SyxResult<Expr> {
    let source = SourceFile::new("<interp>", raw);
    let tokens = Lexer::new(source.clone()).scan_all()?;
    let mut parser = Parser::new(source, tokens);
    parser.parse_expr()
}

4.5 Type parsing parse_type() ​

swift
Int
String?
Array<Int>
Dict<String, Int>
[Int]
(Int) -> Int
some Equatable
any Protocol
rust
// Named { name, generics } — generics in <T1, T2>
// suffix ? → Optional(Box<TypeExpr>)
// [T] → Array; [K: V] → Dictionary
// (params) -> Ret → Function; some/any → OpaqueSome / OpaqueAny
// (T, U) / (x: T, y: U) → Tuple

parse_type() produces TypeExpr: Named (with generics), Optional, Array, Dictionary, Function, OpaqueSome / OpaqueAny, Tuple, etc. For example Array<Int> may also be Named { name: "Array", generics: […] }; literal [Int] constructs TypeExpr::Array directly.

Formatting (W832): The AST formatter prints Optional / OpaqueSome / OpaqueAny / Function / Reference, oversized BigInt, interpolated string Interpolated, regex literal Regex (/pattern/; Literal residual surface); the CST subset (opt-in SYX_FMT_CST) deepens to 1..N func name([Ident[: Ident[<Named(, Named)*>][?]| &Ident[?]| &mut Ident[?]| &'a Ident[?]| &'a mut Ident[?]…]) [-> Ident[<NestedNamed>][?]| &Ident[?]| &mut Ident[?]| &'a Ident[?]| &'a mut Ident[?]] {…} (optional Named / Optional Named / nested multi-arg Named generic / Amp-family [?] type annotations and optional -> Ident[<NestedNamed>][?]| &Ident[?]| &mut Ident[?]| &'a Ident[?]| &'a mut Ident[?] returns; empty params and bare forms remain legal) interleaved with module-top simple stmts, each body 0..N matching simple stmts; default remains AST; still rejects multi Named generic / Function return / Amp·Function as generic args / effect / pure stmt without func / lazy computed var etc. Decl rejection surface (M0-A02 F001).

BNF mapping: §5.3 (<type>)

4.6 print sugar ​

When an expression statement is a print(...) call, the parser promotes it to Stmt::Print:

rust
fn is_print_call(expr: &Expr) -> bool {
    matches!(expr, Expr::Call { callee, .. }
        if matches!(callee.as_ref(), Expr::Ident { name, .. } if name == "print"))
}

BNF mapping: §5.4 (<expr-stmt> comment)

5. Complete grammar (BNF) ​

The BNF below matches the recursive descent + precedence climbing implementation in crates/syx-parser/src/lib.rs and is the authoritative SYX grammar spec. Terminal names align with TokenKind in 1_LexicalAnalysis.md. Quick examples: 8_QuickReference.md.

5.0 Metasymbols and conventions ​

SymbolMeaning
{ ... }Zero or more repetitions
[ ... ]Optional (0 or 1)
( ... )Grouping; or literal paren tokens in BNF
|Alternative
<ident>Identifier token (TokenKind::Identifier)
<integer> / <double> / <string>Corresponding literal tokens

Newlines and whitespace: The parser skips TokenKind::Newline via skip_newlines() and peek_index(); newlines between decls and statements act as separators; block statements end with }, semicolons not required.

Expression statements: <expr-stmt> may be followed by optional ;. If <expr> is a call to identifier print, lower to Stmt::Print (see §4.6).

Statement vs expression try: Statement do { } catch (id) { } is parsed by parse_try(); expression prefix try expr by parse_unary() as UnaryOp::Try.

return and {: check_expr_start() includes LBrace, so return { "k": v } parses correctly (see §11.4).

5.1 Module and declarations ​

text
<module>       ::= { <decl> }

<access>       ::= "public" | "internal" | "private"

<decl>         ::= "import" <ident>
                 | <access>? <function>
                 | <access>? "class" <ident> "{" { <decl> } "}"
                 | <access>? "data" "class" <ident> "(" [ <field-list> ] ")"
                 | <access>? "sealed" "class" <ident> "{" { <variant> } "}"
                 | <access>? "struct" <ident> "{" { <field> } "}"
                 | <access>? ( "var" | "let" ) <ident> [ ":" <type> ] [ "=" <expr> ]
                 | "extension" <ident> "{" { <function> } "}"
                 | "protocol" <ident> "{" { <function> } "}"
                 | <access>? "actor" <ident> "{" { <decl> } "}"

Notes:

  • extension target is a single <ident> (e.g. Int), not a full <type> expression.
  • data class uses a parenthesized field list, no { } body.
  • sealed class variants have no case keyword; optional associated-value tuple follows the variant name.
  • class / actor members recurse as <decl>; protocol / extension members are only <function>.

5.2 Functions, parameters, and fields ​

text
<function>     ::= [ "async" ] "func" <ident> "(" [ <param-list> ] ")"
                   [ "->" <type> ] [ "requires" ( "Pure" | "IO" ) ] "{" { <stmt> } "}"

<param-list>   ::= <param> { "," <param> }
<param>        ::= <ident> [ ":" <type> ] [ "=" <expr> ]

<field-list>   ::= <field> { "," <field> }
<field>        ::= <ident> ":" <type>

<variant>      ::= <ident> [ "(" [ <field-list> ] ")" ]

5.3 Types ​

The parser currently emits only TypeExpr::Named and TypeExpr::Optional:

text
<type>         ::= <named-type> [ "?" ]
<named-type>   ::= <ident> [ "<" <type> { "," <type> } ">" ]

Array<Int>, Dict<String, Int>, etc. parse as <named-type> with generic arguments, not the AST-reserved Array / Dictionary variants.

5.4 Statements ​

text
<stmt>         ::= ( "var" | "let" ) <ident> [ ":" <type> ] [ "=" <expr> ]
                 | "return" [ <expr> ]
                 | "if" "let" <ident> "=" <expr> "{" { <stmt> } "}" [ "else" "{" { <stmt> } "}" ]
                 | "if" "(" <expr> ")" "{" { <stmt> } "}" [ "else" "{" { <stmt> } "}" ]
                 | "guard" "let" <ident> "=" <expr> "else" "{" { <stmt> } "}" "{" { <stmt> } "}"
                 | "switch" <expr> "{" { <case> } [ <default> ] "}"
                 | "while" "(" <expr> ")" "{" { <stmt> } "}"
                 | "for" <ident> "in" <expr> "{" { <stmt> } "}"
                 | "try" "{" { <stmt> } "}" "catch" "(" <ident> ")" "{" { <stmt> } "}"
                 | "break"
                 | "continue"
                 | "{" { <stmt> } "}"
                 | <expr-stmt>

<case>         ::= "case" <switch-pattern> ":" { <stmt> }
<default>      ::= "default" ":" { <stmt> }

<expr-stmt>    ::= <expr> [ ";" ]
                 (* if <expr> is print(...) call → AST: Stmt::Print *)

5.5 Switch patterns ​

text
<switch-pattern> ::= <integer>
                 | <integer> "..<" <integer>
                 | "." <ident> [ "(" "let" <ident> ")" ]
                 | <expr>
FormAST
42Expr::Literal { Integer }
1..<10Expr::HalfOpenRange
.ok(let x)Expr::VariantPattern
OtherPlain Expr

5.6 Expressions ​

Matches §4.4 precedence; infix operators are left-associative, higher numbers bind tighter (parse_precedence(min_prec)).

text
<expr>           ::= <binary-expr>                    (* min_prec = 0 *)

<binary-expr>    ::= <unary-expr> { <bin-op> <unary-expr> }
<bin-op>         ::= "||" | "&&" | "==" | "!="
                 | "<" | "<=" | ">" | ">="
                 | "+" | "-" | "*" | "/" | "%" | "??"

<unary-expr>     ::= ( "await" | "try" | "-" | "!" ) <unary-expr>
                 | <postfix-expr>

<postfix-expr>   ::= <primary> { <postfix-op> }
                   | <ident> "=" <expr>               (* first round only: Assign *)

<postfix-op>     ::= "(" [ <arg-list> ] ")"
                 | "." <ident>
                 | "?." <ident>
                 | "!"
                 | "[" <expr> "]"

<primary>        ::= <literal>
                 | <ident>
                 | "(" <expr> ")"
                 | "[" [ <expr-list> ] "]"
                 | "{" [ <dict-entry-list> ] "}"
                 | <integer> "..<" <integer>

<arg-list>       ::= <expr> { "," <expr> }
<expr-list>      ::= <expr> { "," <expr> }

<dict-entry-list> ::= <dict-key> ":" <expr> { "," <dict-key> ":" <expr> }
<dict-key>       ::= <string>

Precedence (infix_op, higher = tighter):

LevelOperatorBinOp
1||Or
2&&And
3== !=Eq, NotEq
4< <= > >=Lt, LtEq, Gt, GtEq
5+ -Add, Sub
6* / %Mul, Div, Mod
7??NilCoalesce

5.7 Literals ​

text
<literal>      ::= <integer>
                 | <double>
                 | <string>
                 | <interpolated-string>
                 | <regex>
                 | "true" | "false"
                 | "nil"
FormTokenKind / notes
IntegerInteger
FloatDouble
StringString
"Hello \(name)"InterpolatedString (embedded expr re-parse_expr)
/pattern/Regex
Bool / nilBool / Nil

5.8 Terminals and token mapping ​

Source textTokenKindNotes
..<DotDotLessHalf-open range
??NilCoalesceNil coalescing
?.Question + DotOptional chain, merged by parser
->ArrowReturn type
requiresRequiresEffect constraint
Pure / IOPure / IoEffect enum
/.../RegexRegex literal
importImportModule import

Full keyword and operator list: 1_LexicalAnalysis.md.

6. Error handling ​

Syntax errors use diagnostic code P001:

rust
fn error<T>(&self, message: &str) -> SyxResult<T> {
    Err(SyxDiagnostic::error(message)
        .with_code("P001")
        .with_span(&self.source, self.peek().span, "here"))
}

fn error_at<T>(&self, token: Token, message: &str) -> SyxResult<T> {
    Err(SyxDiagnostic::error(message)
        .with_code("P001")
        .with_span(&self.source, token.span, "here"))
}

Common P001 cases:

ScenarioTypical message
Not a declaration startExpected declaration
Missing funcExpected 'func' after async
Missing parens/bracesExpected '(' after function name
Missing type nameExpected type name
Not an expression startExpected expression
Invalid dict keyExpected string key in dictionary literal
Invalid effect constraintExpected 'Pure' or 'IO' after requires

The current implementation has no error recovery: on P001 it returns Err immediately without synchronizing.

6.1 Token helpers ​

rust
fn consume(&mut self, kind: TokenKind, message: &str) -> SyxResult<&Token>
fn consume_identifier(&mut self, message: &str) -> SyxResult<SmolStr>
fn match_kind(&mut self, kind: TokenKind) -> bool
fn check(&self, kind: TokenKind) -> bool
fn check_expr_start(&self) -> bool
fn advance(&mut self) -> &Token
fn peek(&self) -> &Token
fn skip_newlines(&mut self)

7. AST visitor pattern ​

syx-ast provides ExprVisitor<T> for walking expression trees:

rust
pub trait ExprVisitor<T> {
    fn visit_literal(&mut self, value: &Literal) -> T;
    fn visit_ident(&mut self, name: &SmolStr) -> T;
    fn visit_binary(&mut self, left: &Expr, op: BinOp, right: &Expr) -> T;
    // ...
    fn walk_expr(&mut self, expr: &Expr) -> T { ... }
}

Await, OptionalChain, ForceUnwrap, etc. delegate via visit_group; HalfOpenRange and VariantPattern fall back to visit_literal(Nil) in default walk_expr.

8. Full example ​

8.1 Source code ​

swift
import utils

data class User(name: String, age: Int)

sealed class Response {
    ok(data: String)
    err(code: Int)
}

protocol Greeter {
    func greet(name: String) -> String {
        return name
    }
}

extension String {
    func shout() -> String {
        return self + "!"
    }
}

async func main() -> Int requires IO {
    let users = { "alice": User("Alice", 30) }
    let count = 3
    for key in users {
        let name = users[key]?.name ?? "unknown"
        print(name)
    }
    do {
        risky()
    } catch (e) {
        print(e)
    }
    switch count {
        case 0:
            print("empty")
        case 1..<10:
            print("few")
        default:
            print("many")
    }
    return 0
}

8.2 Parse invocation ​

rust
use syx_diagnostics::SourceFile;
use syx_parser::Parser;

let source = SourceFile::new("app.sy", SOURCE);
let module = Parser::parse(source)?;
// module.decls includes Import, DataClass, SealedClass, Protocol,
// Extension, Function, etc.

8.3 Resulting AST (sketch) ​

Module { name: "main" }
├── Decl::Import { module: "utils" }
├── Decl::DataClass { name: "User", fields: [(name, String), (age, Int)] }
├── Decl::SealedClass { name: "Response", variants: [ok, err] }
├── Decl::Protocol { name: "Greeter", methods: [greet] }
├── Decl::Extension { type_name: "String", methods: [shout] }
└── Decl::Function { name: "main", is_async: true, effect: Io, body: [...] }
    ├── Stmt::VarDecl { name: "users", init: DictLiteral }
    ├── Stmt::VarDecl { name: "count", init: Literal(3) }
    ├── Stmt::ForIn { name: "key", iterable: Ident("users") }
    │   └── Stmt::VarDecl { init: OptionalChain + NilCoalesce }
    ├── Stmt::Try { catch_name: "e" }
    └── Stmt::Switch { subject: Ident("count"), cases: [0, 1..<10], default: ... }

9. Tests ​

Unit tests in crates/syx-parser/src/lib.rs #[cfg(test)]:

TestValidates
parses_function_and_printFunction decl and print call
parses_hello_worldStandard Hello World
parses_for_in_loopfor i in items loop

Run tests:

bash
cargo test -p syx-parser

10. Crate dependencies ​

syx-parser
├── syx-lexer        (Lexer, Token, TokenKind)
├── syx-ast          (Module, Decl, Stmt, Expr, TypeExpr, ...)
├── syx-diagnostics  (SourceFile, Span, SyxDiagnostic, SyxResult)
└── smol_str         (SmolStr)

Upstream: syx-lexer supplies the token stream.
Downstream: later compiler stages (type checking, codegen, etc.) consume the Module AST.

11. Current limitations ​

Grammar listed in §11.1 is implemented in the current toolchain (Parser → Semantic → Mono → CodeGen → VM). Integration tests: p2_integration.rs (18 cases), p4_*.sy, p5_integration.rs (5 cases), p5_mir_parity.rs (5 MIR vs p4 parity cases).

11.1 Implemented ​

CategoryFeatureTests / notes
Parser fixesDict ident keys, return { }, public protocol/import, +=, selfp2_limits_parser.sy
Statementsthrow expr, repeat { } while (cond)p2_limits_stmt.sy
Declarationsenum associated values, init/deinit, struct/class/actor, class: Protocolp2_enum.sy, fixtures/lang/enum_associated.sy, p3_class_struct.sy, p4_protocol_witness.sy
Calls / modulesNamed arguments, import a.b, import x as yp2_named_args.sy, p3_import_alias.sy
Generics / attributesCall-site monomorphization (syx-mono), where arg check S010, S009 protocol name lint, @Main, @available(iOS 13)p4_generics.sy, p5_generics_where_fail.sy, p3_attributes.sy
Closures / HOFMulti-capture, MakeClosure(N), Array.map/filter/reducep4_closures_capture.sy, p4_hof.sy
class / actorImplicit self, instance fields, lock by __actor_id, ActorCall, cross-actor fields S008fixtures/parity/actor_isolation.sy, p5_actor_s008.sy
protocol / for-inWitness tables, for-in reads __witness_Iterator_nextp4_protocol_witness.sy, p5_for_in_witness.sy
enumAssociated-value switch, pattern-aware exhaustiveness S007fixtures/lang/enum_associated.sy, p5_enum_exhaustive.sy
asyncMultiple await, AwaitPoll instructionp5_async_multi.sy
Range / assignment(1..<10).contains(n), compound assignment, ObjectSet/ArraySetp3_range.sy
MIRMixed compile (type shell + MIR bodies), extended MirOp, SYX_USE_MIR=1p5_mir_parity.rs

11.2 Still limited (MVP) ​

FeatureStatus
GenericsMonomorphization names by argument types; no full trait constraint inference (only S010 explicit conformance)
asyncAwaitPoll remains blocking poll MVP
native / wasmLLVM/WASM backends stub new MirOps, not full instruction set

11.3 Not yet implemented ​

FeatureNotes
Top-level init/deinitModule-level parse → P001 (class/actor only)
case/default outside switchStill P001
Full Swift concurrency / ownershipSee 7_FeatureImplementationPriority.md P3+

11.4 check_expr_start (for parse_return) ​

Includes: Integer, Double, String, InterpolatedString, Regex, Bool, Nil, Identifier, LParen, LBracket, LBrace, Minus, Bang, Await, Try, Throw.

Note: return { "k": v } now parses as returning a dict because LBrace is included; check_expr_start matches parse_expr behavior.

As the implementation evolves, §5 BNF and this section should stay in sync.

SYX · Apache-2.0