Skip to content
cccskills
FreePublished on GitHub

Parser Internals

Comprehensive guide to the Chemical recursive descent parser — how Chemical source code is parsed into AST nodes.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31 stars(GitHub 31 + cccskills 0)
Works withAWS

Run score Not tested

See how to install

Files included (1)

  • SKILL.md19.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Parser Internals

The Chemical parser is a hand-written recursive descent parser. It takes a stream of tokens from the lexer and produces an AST (Abstract Syntax Tree).

Architecture

Pipeline

Source code → Lexer → Token stream → Parser → AST

Key Files

FilePurpose
parser/Parser.hParser class declaration — all parsing method declarations
parser/Parser.cppMain parser implementation — orchestration and entry points
lexer/Lexer.hLexer class — tokenizer
lexer/Lexer.cppLexer implementation
lexer/Token.hToken structure and types
lexer/TokenType.hToken type enum
lexer/LexUnit.hLexed token unit (whole-file token buffer)
parser/structures/Per-construct parsers (functions, structs, control flow, etc.)
parser/values/Lambda and struct-value parsers (LambdaValue.cpp, StructValue.cpp)
parser/statements/Statement parsers (dispatch, imports, assignments, annotations, etc.)
parser/utils/Expression, literal and helper parsing (Expression.cpp, LexValue.cpp, Helpers.cpp)

Per-Construct Parser Files

FileConstruct
parser/structures/Function.cppFunction declarations + inline asm parsing (parseInlineAsmStatement)
parser/structures/Struct.cppStruct definitions
parser/structures/Variant.cppVariant definitions
parser/structures/Enum.cppEnum definitions
parser/structures/Interface.cppInterface definitions
parser/structures/ImplDef.cppImplementation blocks (impl)
parser/structures/Namespace.cppNamespace declarations
parser/structures/Block.cppBlock scopes ({ })
parser/structures/IfBlock.cppIf/else statements
parser/structures/WhileBlock.cppWhile loops
parser/structures/DoWhile.cppDo-while loops
parser/structures/ForBlock.cppFor loops
parser/structures/Switch.cppSwitch statements
parser/structures/TryCatch.cppTry/catch blocks
parser/structures/Union.cppUnion definitions
parser/values/LambdaValue.cppLambda function values
parser/values/StructValue.cppStruct literal values
parser/utils/LexValue.cppLexed values (integers, floats, strings, chars)
parser/utils/Expression.cppBinary/unary expression parsing (shunting-yard)
parser/utils/Helpers.cppHelper functions (access specifiers, generics lookahead)
parser/statements/LexStatement.cppStatement dispatch (top-level & nested)
parser/statements/LexAssignment.cppAssignment statements
parser/statements/Import.cppImport statements
parser/statements/Export.cppExport statements
parser/statements/VarInitialization.cppVariable initialization
parser/statements/Typealias.cppType alias statements
parser/statements/AccessChain.cppAccess chain parsing (a.b.c)
parser/statements/AnnotationMacro.cppAnnotation/macro parsing
parser/statements/OptionStmt.cppoption statements in .mod files

Parser State

The Parser class holds:

class BasicParser : public ASTDiagnoser {
    unsigned int file_id;                    // file id for the location manager
    Token* token;                            // pointer to the current token
    ASTNode* parent_node = nullptr;          // current parent node

    // Token consumption helpers:
    void consumeAny();                       // advance to the next token
    bool consumeToken(TokenType type);       // advance if the current token matches
    Token* consumeOfType(TokenType type);    // like consumeToken, but returns the token
    Token* consumeIdentifier();              // consume strictly an Identifier
    Token* consumeIdentifierOrKeyword();     // consume an Identifier or a keyword
    void consumeNewLines();                   // skip NewLine tokens
};

class Parser : public BasicParser {
    CompilerBinder* const binder;            // CBI binder (nullptr when disabled)
    AnnotationController& controller;        // annotation definitions and handlers
    ASTAllocator& global_allocator;          // global (job) arena allocator
    ASTAllocator& mod_allocator;             // module arena allocator
    TypeBuilder& typeBuilder;                // cached type builder
    bool is64Bit;                            // target bitness
    int pending_greater_count = 0;           // handles '>>' inside nested generics
    std::vector<SavedAnnotation> annotations;// annotations awaiting the next node

    // Entry points / dispatch:
    void parse(std::vector<ASTNode*>& nodes);                       // parse all top-level nodes
    ASTNode* parseTopLevelStatement(ASTAllocator&, bool comptime);
    ASTNode* parseNestedLevelStatementTokens(ASTAllocator&, bool is_value = false, bool parse_value_node = false);
    Value* parseExpression(ASTAllocator&, bool parseStruct = false, bool parseLambda = true);
    TypeLoc parseTypeLoc(ASTAllocator&);
};

Note: the parser is not a separate token-stream class — the header is tokenized into a LexUnit/std::vector<Token> up front and the Parser walks a Token* pointer through it (with isGenericEndAhead() for limited lookahead).

Recursive Descent Pattern

The parser walks a Token* through the pre-lexed token vector with a small amount of lookahead (isGenericEndAhead() disambiguates < as generic vs. less-than):

ident<...> — generic arguments attach to three different places

In parseAccessChainAfterId's LessThanSym arm, after parseGenericArgsListNoStart has read the arguments, the next token decides where they go:

Followed byNodeHolds the arguments
{StructValuethe GenericType reference
(FunctionCallFunctionCall::generic_list
anything elseGenericInstIdentifier (an identifier that also carries the arguments)GenericInstIdentifier::generic_list

The third case is the bare reference var f : (x : int) => int = ident<int>, and it is the only case that must survive into symbol resolution as a value rather than as a type — see the Generics skill. It replaces chain->values.back() with a node built from the identifier's own value, type, location and is_ns (there is no wrapper object). GenericInstIdentifier is a VariableIdentifier subclass, so Value::isIdentifier() reports it and values.back()->as_identifier() / as_identifier_unsafe() return it — code that consumes chain leaves keeps working, as long as it tests the predicate instead of kind() == ValueKind::Identifier.

// Simplified illustration — the real entry point is
// Parser::parseFunctionStructureTokens in parser/structures/Function.cpp
ASTNode* Parser::parseFunctionStructureTokens(ASTAllocator& allocator, ASTAllocator& body_allocator,
                                              AccessSpecifier specifier, bool allow_extensions, bool comptime) {
    // 1. Check for 'func' keyword
    if(!consumeToken(TokenType::FuncKw)) return nullptr;

    // 2. Parse function name
    auto* name = consumeIdentifierOrKeyword();

    // 3. Parse generic parameters (optional)
    std::vector<GenericTypeParameter*> generic_params;
    if(token->type == TokenType::LessThanSym) {
        parseGenericParametersList(allocator, generic_params);
    }

    // 4. Parse parameters
    if(!consumeToken(TokenType::LParen)) { /* error */ }
    std::vector<FunctionParam*> params;
    bool variadic = parseParameterList(allocator, params);
    consumeToken(TokenType::RParen);

    // 5. Parse return type (optional)
    if(consumeToken(TokenType::ColonSym)) {
        auto return_type = parseTypeLoc(allocator);
    }

    // 6. Parse function body (declarations may omit it)
    if(token->type == TokenType::LBrace) {
        auto body = parseBraceBlock("function", nullptr, body_allocator);
    }

    // 7. The function node is then created / annotated
    return nullptr; // (node construction omitted)
}

Parsing Rules by Construct

Expressions

Expressions are parsed with a shunting-yard algorithm (shunting_yard_on_operator / Parser::parseExpressionWith in parser/utils/Expression.cpp). Operator precedence is defined by to_precedence() in ast/utils/Operation.cpp; left/right associativity by is_assoc_left_to_right(). Higher precedence numbers bind tighter:

// Precedence levels (lowest to highest):
Assignment    (=, +=, -=, *=, /=, ...)
Conditional   (?:)
LogicalOr     (||)
LogicalAnd    (&&)
BitwiseOr     (|)
BitwiseXor    (^)
BitwiseAnd    (&)
Equality      (==, !=)
Comparison    (<, >, <=, >=)
Shift         (<<, >>)
Additive      (+, -)
Multiplicative (*, /, %)
Unary         (!, -, ~, &, *, &raw, ++, --)
Postfix       (), [], ., ->

Types

Types are parsed with a recursive type parser. Parser::parseTypeLocNoPostOps handles leading type constructors, and Parser::parseArrayAndPointerTypesAfterTypeId handles post-ops (*, &, []) that follow a named type:

// Simplified illustration — see Parser::parseTypeLocNoPostOps in parser/statements/LexType.cpp
TypeLoc Parser::parseTypeLocNoPostOps(ASTAllocator& allocator) {
    switch(token->type) {
        // Pointer types: *int, *mut int
        case TokenType::MultiplySym: {
            const auto ptrToken = token; token++;
            auto is_mutable = token->type == TokenType::MutKw;
            if(is_mutable) token++;
            return { new(allocator.allocate<PointerType>()) PointerType(parseType(allocator), is_mutable), loc_single(ptrToken) };
        }
        // Reference types: &int, &mut int
        case TokenType::AmpersandSym: {
            const auto refToken = token; token++;
            auto is_mutable = token->type == TokenType::MutKw;
            if(is_mutable) token++;
            return { new(allocator.allocate<ReferenceType>()) ReferenceType(parseType(allocator), is_mutable), loc_single(refToken) };
        }
        // Array types: [10]int
        case TokenType::LBracket: {
            const auto& t = *token; token++;
            auto size = parseExpression(allocator);
            consumeToken(TokenType::RBracket);
            auto child = parseTypeLoc(allocator);
            return { new(allocator.allocate<ArrayType>()) ArrayType(child, size), loc_single(&t) };
        }
        // Function types: (int) => bool
        case TokenType::LParen:
            return parseLambdaTypeLoc(allocator, false);
        // Named types: int, string, MyStruct, GenericType<int>
        default:
            return { parseLinkedOrGenericType(allocator), loc_single(token) };
    }
}

Statements

Statement parsing dispatches on the first token. Top-level dispatch lives in Parser::parseTopLevelStatement / parseTopLevelAccessSpecifiedDecl, and nested dispatch in Parser::parseNestedLevelStatementTokens (both in parser/statements/LexStatement.cpp) — some constructs are only valid at top level:

TokenStatement
var / constVariable declaration
ifIf statement
whileWhile loop
doDo-while loop
forFor loop
loopLoop block (loop expression)
switchSwitch statement
returnReturn statement
breakBreak statement
continueContinue statement
unsafeUnsafe block
comptimeComptime block
{Scope block
importImport statement
exportExport statement (top level)
typeType alias
throwThrow statement
tryTry/catch
delete / destructDestruct statement
deallocDealloc statement
newPlacement new
unreachableUnreachable statement
aliasAlias statement
provideProvide statement
@Annotation (buffered for the next node)
#Macro node/macro value
funcFunction (top level)
structStruct (top level)
variantVariant (top level)
unionUnion (top level)
enumEnum (top level)
interfaceInterface (top level)
implImplementation (top level)
namespaceNamespace (top level)
usingUsing declaration
asmInline assembly statement
OtherExpression / assignment statement

Async / Await Parsing (contextual keywords)

async and await are contextual keywords: they must remain usable as identifiers (e.g. the async namespace, an async field). The parser distinguishes by position:

  • Token::isKeywordOrId accepts async/await in name/path/type position; Parser::consumeIdentifierOrKeyword and read_type_involving_token consume them as identifiers. A value dispatch has an explicit async::path disambiguation so async::block_on(...) parses as a namespaced call.
  • An async followed by func is an async function declaration (parser/structures/Function.cpp); async followed by a lambda introduces an async closure (Parser::parseAsyncClosureValue, parser/values/LambdaValue.cpp, called from AccessChain.cpp, LexStatement.cpp, LexValue.cpp). Async closures parse but are rejected in symres.
  • await expr is an expression (AwaitExpression), so var v = await f() and return await f() parse; a bare await f() statement does not — see the chemical_source skill.

Async closures parse here but are not lowered (symres diagnoses them). See lang/docs/async-remaining-work.md (item AC) for the fix plan.

Error Recovery

The parser uses basic error recovery:

  1. Missing semicolons: If a statement doesn't start with a known keyword, the parser tries to parse an expression statement and expects a semicolon
  2. Unexpected tokens: When a token is unexpected, the parser skips to the next semicolon or closing brace
  3. Missing closing brackets: Reports an error but continues parsing (assuming the closing bracket)
  4. Comptime if: Both branches are parsed for syntax, but only the selected branch is type-checked

consumeNewLines() — Newline Tolerance

The parser calls consumeNewLines() to skip NewLine tokens between elements. This enables newline-tolerant parsing in function signatures and asm statements. For example, in parseInlineAsmStatement, consumeNewLines() is called between operands, constraint strings, and parentheses to allow multi-line asm templates.

Inline Assembly Parsing (parseInlineAsmStatement)

Located in parser/structures/Function.cpp. Triggered when the parser sees TokenType::AsmKw.

Syntax:

// Simple form — template only
asm("movq %1, %0")

// Extended form — with outputs, inputs, clobbers
asm("lock cmpxchgq %3, %2\n\tsete %1"
    : "=a"(*expected), "=q"(success), "+m"(*ptr)    // outputs
    : "r"(desired), "0"(*expected)                   // inputs
    : "cc", "memory")                                // clobbers

// Skip outputs with :: (Go-style)
asm("" ::: "memory")

// Clobber-only
asm("" ::: "memory", "cc")

Parsing steps:

  1. Consume asm keyword and (
  2. Parse string literal as asm template
  3. If ) follows → return (simple form)
  4. If : follows → parse output operands: "constraint"(expr) separated by ,
  5. If : follows → parse input operands (same format)
  6. If : follows → parse clobber strings: "memory", "cc", etc.
  7. Consume )

Output operand constraint strings:

  • "=a"(expr) — write to eax/rax
  • "=q"(expr) — write to any register
  • "+m"(expr) — read/write memory operand
  • "=r"(expr) — write to any register

Input operand constraint strings:

  • "r"(expr) — value in any register
  • "0"(expr) — same register as operand 0
  • "m"(expr) — value in memory

The InlineAsmStatement AST node stores:

  • asm_template — the template string
  • output_operands — vector of (constraint, expr) pairs
  • input_operands — vector of (constraint, expr) pairs
  • clobbers — vector of clobber strings

The @ Annotation Macro System

The @ prefix (TokenType::Annotation) triggers annotation parsing, which can either:

  1. Registered annotations: annotations are looked up by name in the AnnotationController (controller.get_definition(name)). Built-in annotations such as @extern, @test, @deprecated, @no_mangle, @inline, @noinline, etc. register intrinsic handlers; CBI plugins register their own definitions too.
  2. Unknown annotations: an unregistered @name is reported as an error (unknown annotation found '...').

Annotations are buffered on the Parser (std::vector<SavedAnnotation> annotations) and applied to the next node by Parser::annotate(node), which calls controller.handle_annotation(...).

bool Parser::parseAnnotation(ASTAllocator& allocator) {
    if(token->type != TokenType::Annotation) return false;
    const auto annot = token;
    token++;
    auto name_view = chem::string_view(annot->value.data() + 1, annot->value.size() - 1);
    auto definition = controller.get_definition(name_view);
    if(definition == nullptr) {
        error() << "unknown annotation found '" << annot->value << "'";
        return true;
    }
    annotations.emplace_back(*definition, std::vector<Value*>{});
    // ... parse optional (args) and store them on the SavedAnnotation ...
}

Lexer Integration

The lexer is in lexer/Lexer.cpp and produces tokens consumed by the parser:

class Lexer : public LexerState {
    SourceProvider provider;                    // reads source input
    std::string file_path;                      // file being lexed
    CompilerBinder* const binder;               // CBI binder (nullptr when disabled)
    BatchAllocator& file_allocator;             // allocator for token strings
    bool lex_whitespace = false;                // LSP: keep Whitespace tokens
    bool keep_comments = false;                 // LSP: keep comment tokens

    Token getNextToken();                       // get the next token
    void getTokens(std::vector<Token>& tokens); // lex the whole file
    void getUnit(LexUnit& outUnit);             // lex the whole file into a LexUnit
};

Token Types

Token TypeExample
Identifierfoo, Bar, _baz
Number42, 0xFF, 0b1010, 3.14, 1e10
String"hello", "\"escaped\""
Char'a', '\n'
FuncKwfunc
StructKwstruct
VarKwvar
ConstKwconst
AsmKwasm
LParen(
RParen)
LBrace{
RBrace}
LBracket[
RBracket]
SemiColonSym;
ColonSym:
LambdaSym=>
AmpersandSym&
MultiplySym*
DotSym.
CommaSym,
Annotation@extern
HashMacro#html
......

AST Allocator

All AST nodes are allocated through ASTAllocator, a BatchAllocator subclass:

class ASTAllocator final : public BatchAllocator {
    template<typename T> T* allocate();              // Arena allocation — no individual free
    char* allocate_size(size_t size, size_t align);
    void clear();                                     // Free everything, reuse the arena
};

This means:

  • No per-node delete ever called
  • AST is deallocated in bulk when the compilation phase ends
  • Fast allocation (pointer bump)
  • Memory is contiguous, improving cache performance

Common Parsing Issues

IssueCauseFix
Ambiguous grammarExpression vs. declaration confusionUse consumeToken() with careful token checking
Left recursionDirect/indirect left-recursive rulesRewrite as iteration (e.g., a + b + c)
Operator precedenceWrong binding strengthUse the shunting-yard algorithm (to_precedence())
Lookahead limitNeed >1 token to disambiguateBuffer tokens or use backtracking
Generic parsing< vs. less-than ambiguityUse context tracking to distinguish

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Comprehensive guide to the Chemical compiler's annotation system (`@name`, `@name(args)`) — how annotations are lexed, parsed, registered in the `AnnotationController`, stored and queried centrally, consumed by the compiler and CBI plugins, and how to add a new one. Load when working on annotations, `AnnotationController`, compiler plugins that define/read annotations, or node attribute flags such as `@extern`, `@test`, `@make`, `@deprecated`, `@no_mangle`, `@inline`, and `@volatile`.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

The C++ AST node framework of the Chemical compiler — the ASTAny/ASTNode/Value/BaseType hierarchy, node/value/type kind enums and range predicates, the visitor dispatch framework, the arena allocator, TypeBuilder canonicalization, member containers, and cross-reference resolution. Load when adding or modifying AST nodes, writing compiler passes/codegen visitors, debugging kind mismatches or arena/lifetime issues, or extending the compiler API bindings.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

The Chemical compiler benchmark & release analytics system — how the static GitHub Pages dashboard works, the JSON data model (manifest/daily/release records), the bash collection scripts (bench-*.sh), the GitHub Actions workflows (benchmark-daily/release), and the critical pitfalls (hidden upload dirs, worktree submodules, gh-pages-only data). Load when working on the dashboard, benchmark collection, or analytics workflows.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

Comprehensive deep-dive into the Chemical build pipeline — how chemical.mod is converted to build.lab, how build scripts are JIT-compiled via TinyCC, how jobs are created and executed, and how ASTProcessor orchestrates the parallel compilation passes.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

All documentation related to building and running Project or Tests

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

Comprehensive guide to the C translation backend (2c) — how Chemical AST is translated to C code, key patterns, gotchas, and optimization strategies.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

See all skills by chemicallang

Report a problem with this skill