Guides AILANG parser development with conventions and patterns. Use when user wants to modify parser, understand parser architecture, or debug parser issues. Saves 30% of development time by preventing token position bugs.
Master AILANG parser development with critical conventions that prevent common bugs.
⚠️ READ THIS BEFORE WRITING PARSER CODE
This skill documents critical parser conventions that prevent token position bugs (the #1 time sink in parser development).
Time savings: ~30% by avoiding common pitfalls
Key conventions:
DEBUG_PARSER=1 for token tracingmake doc PKG=<package> for API discoveryInvoke this skill when:
CRITICAL: AILANG parser functions follow this convention:
Example:
// To parse "42" followed by a comma:
p.nextToken() // move to 42
expr := p.parseExpression(LOWEST) // parses "42", leaves cur=42 (NOT comma!)
p.nextToken() // NOW we're at comma ✓
Functions following this convention:
parseExpression() - Leaves parser AT the last token of the expressionparseType() - Leaves parser AT the last token of the typeparsePattern() - Leaves parser AT the last token of the patternWhen writing new parser functions:
p.nextToken() AFTER calling these functionsp.nextToken() BEFORE - the caller handles positioningSee: resources/token_positioning.md for detailed examples
scripts/trace_parser.sh <file.ail>Run parser with DEBUG_PARSER=1 to trace token positions.
Usage:
.claude/skills/parser-developer/scripts/trace_parser.sh test.ail
Output:
[ENTER parseExpression] cur=INT(42) peek=COMMA
[EXIT parseExpression] cur=INT(42) peek=COMMA
scripts/check_ast_types.shList all AST node types in the codebase.
Usage:
.claude/skills/parser-developer/scripts/check_ast_types.sh
scripts/find_api.sh <package> <symbol>Quick API discovery using make doc.
Usage:
.claude/skills/parser-developer/scripts/find_api.sh internal/parser New
Quick type lookup:
grep "^type.*struct" internal/ast/ast.go | head -20
Expression types:
ast.Literal with Kind field
int64, not int for IntLitlit.Value.(int64)lit.Value.(int) (will panic!)ast.List with Elements []Exprast.Variable with Name stringast.FuncCall with Func Expr, Args []Exprast.Lambda with Params []*ast.Param, Body Exprast.Block with Exprs []ExprType types:
ast.SimpleType with Name stringast.ListType with Element Typeast.FuncType with Params []Type, Return Type, Effects *ast.EffectRowast.TypeApp with Con string, Args []TypePattern types:
ast.VarPattern with Name stringast.ConstructorPattern with Name string, Args []Patternast.LiteralPattern with Value Literalast.WildcardPattern (matches anything)See: resources/ast_quick_reference.md for complete listing
Check if a keyword exists:
grep -i "forall" internal/lexer/token.go
# Output: FORALL token exists!
Common testing keywords (already in lexer):
FORALL, EXISTS - QuantifiersTEST, TESTS - Test blocksPROPERTY, PROPERTIES - Property-based testsASSERT - AssertionsIf you see an identifier instead of a keyword:
lexer.FORALL, not lexer.IDENT + literal checkp.curTokenIs(lexer.IDENT) && p.curToken.Literal == "forall"p.curTokenIs(lexer.FORALL)Enable token position tracing:
DEBUG_PARSER=1 ailang run test.ail
Output example:
[ENTER parseType] cur=IDENT(int) peek=,
[EXIT parseType] cur=IDENT(int) peek=,
[ENTER parseExpression] cur=IDENT(x) peek=+
[EXIT parseExpression] cur=IDENT(x) peek=+
How it works:
cur) and next (peek) tokensDEBUG_PARSER=1 is set (zero overhead otherwise)See: resources/debug_mode.md for troubleshooting guide
Pattern for parsing optional sections:
// properties can be in PEEK (no tests) or CUR (after tests)
if p.peekTokenIs(lexer.PROPERTIES) || p.curTokenIs(lexer.PROPERTIES) {
// If in peek, advance to it
if p.peekTokenIs(lexer.PROPERTIES) {
p.nextToken()
}
// Now always at PROPERTIES
properties := p.parsePropertiesBlock()
}
Why: Previous optional section may or may not advance the parser, so check both positions.
❌ WRONG - Errors are hidden:
if len(p.Errors()) != 0 {
t.Fatalf("parser had %d errors:", len(p.Errors()))
// ⚠️ This never executes! t.Fatalf stops immediately
for _, err := range p.Errors() {
t.Errorf(" %s", err)
}
}
✅ CORRECT - Errors are visible:
if len(p.Errors()) != 0 {
// Print errors BEFORE Fatalf
for _, err := range p.Errors() {
t.Errorf(" %s", err)
}
t.Fatalf("parser had %d errors", len(p.Errors()))
}
⚠️ CRITICAL: string(rune(i)) produces unprintable characters!
// ❌ WRONG - Produces "\x01" instead of "1"
testName := "test_" + string(rune(i+1)) // BUG!
// ✅ CORRECT - Use fmt.Sprintf or strconv
testName := fmt.Sprintf("test_%d", i+1)
testName := "test_" + strconv.Itoa(i+1)
Why: rune(1) is Unicode U+0001 (unprintable), not "1" (U+0031).
See: resources/common_patterns.md for more patterns
When you need to know an API:
make doc (fastest - 30 seconds)make doc PKG=internal/parser | grep "parseExpression"
make doc PKG=internal/testing | grep "NewCollector"
grep "^func New" internal/testing/collector.go
grep "^type.*struct" internal/ast/ast.go
grep "NewCollector" internal/testing/*_test.go
Time savings:
make doc: ~5-10 min per API lookupmake doc: ~30 sec per API lookupSee: resources/api_discovery.md for constructor tables
Quick reference:
| Package | Constructor | Signature | Notes |
|---------|-------------|-----------|-------|
| internal/parser | New(lexer) | Takes lexer instance | Parser |
| internal/elaborate | NewElaborator() | No arguments | Surface → Core |
| internal/types | NewTypeChecker(core, imports) | Takes Core prog + imports | Type inference |
| internal/link | NewLinker() | No arguments | Dictionary linking |
| internal/testing | NewCollector(path string) | Takes module path | M-TESTING |
| internal/eval | NewEvaluator(ctx) | Takes EffContext | Core evaluator |
See: resources/api_discovery.md for complete reference
Typical compilation pipeline:
// Step 1: Parse
l := lexer.New(input, "test.ail")
p := parser.New(l)
file := p.ParseFile()
// Step 2: Elaborate (Surface → Core)
elab := elaborate.NewElaborator() // ⚠️ No arguments!
coreProg, err := elab.Elaborate(file)
// Step 3: Type check
tc := types.NewTypeChecker(coreProg, nil) // nil = no imports
typedProg, err := tc.Check()
// Step 4: Link dictionaries
linker := link.NewLinker()
linkedProg, err := linker.Link(typedProg, tc.CoreTI)
Use make doc to discover struct fields:
make doc PKG=internal/ast | grep -A 20 "type FuncDecl"
Common mistakes:
// ✅ CORRECT
funcDecl.Tests // []*ast.TestCase (not .Tests.Cases!)
funcDecl.Properties // []*ast.Property
funcDecl.Params // []*ast.Param
// ❌ WRONG (fields that don't exist)
funcDecl.InlineTests // Use .Tests
funcDecl.Tests.Cases // .Tests is already the slice
Token positioning:
AST types:
Common patterns:
API discovery:
Debug mode:
design_docs/planned/v0_3_15/m-dx9-parser-developer-experience.mddocs/CONTRIBUTING.mdinternal/lexer/, internal/parser/The lexer skips \n as whitespace. Even though lexer.NEWLINE exists, it's never generated!
❌ WRONG:
if p.curTokenIs(lexer.NEWLINE) { // This is NEVER true!
...
}
✅ CORRECT:
// After RPAREN of Leaf(int), next token is PIPE (not NEWLINE)
if p.curTokenIs(lexer.PIPE) {
...
}
Multi-line syntax "just works" because the lexer handles it.
// ❌ WRONG - Will panic!
value := lit.Value.(int)
// ✅ CORRECT
value := lit.Value.(int64)
t.Fatalf stops execution immediately, so print errors first!
This skill loads information progressively:
scripts/ (tracing, API discovery)resources/DEBUG_PARSER=1 for token tracingmake doc PKG=<package> for API discovery (80% faster)int64, not intt.Fatalf in testsSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer