Documentation
¶
Overview ¶
Package parser implements syntactic analysis for Buildfiles.
The parser transforms a token stream from the lexer into an AST. It maintains scope context to validate directive placement and handles error recovery to collect multiple errors from a single parse.
Scope Stack ¶
The parser tracks nested scopes during parsing:
- ScopeGlobal: Top-level scope
- ScopeEnvironment: Inside .environment: block
- ScopeRecipe: Inside a target's recipe
- ScopeBlock: Inside block: within a recipe
Directives are validated against their allowed scopes. For example, .shell: is valid at GLOBAL and RECIPE scopes, while .using: is only valid in ENVIRONMENT scope.
Error Handling ¶
The parser uses two complementary error handling patterns:
Top-level parsing functions (ParseVariable, ParseTarget, ParseConditional, etc.) return *ParseError. This allows ParseBuildfile to catch errors and perform recovery via recoverToLevel0().
Value/content parsing functions (ParseValue, parseInterpolation, parseFunctionCall, etc.) use addError() internally and return partial results. This enables continued parsing despite malformed values.
This two-tier pattern supports error recovery: structural errors stop the current block and trigger recovery, while value-level errors are collected but don't halt parsing.
Error Recovery ¶
On parse error, the parser:
- Records the error in the ParseErrors collection
- Skips to the next line at indentation level 0 (global scope)
- Continues parsing from there
- Stops after maxErrors (10) to avoid infinite loops
This enables partial analysis of broken files and provides comprehensive error feedback rather than stopping at the first error.
Parsing Buildfiles ¶
Use ParseBuildfile() for complete buildfile parsing with error recovery:
lex := lexer.New("Buildfile", content)
p := parser.New(lex)
stmts, errs := p.ParseBuildfile()
For parsing individual constructs (useful in testing or tooling), use the specific Parse* methods: ParseVariable, ParseTarget, ParseEnvironment, ParseConditional, ParseInclude.
Package parser implements the parsing phase for Buildfile source code. It transforms a token stream from the lexer into an Abstract Syntax Tree (AST).
Index ¶
- func DirectiveNameForError(tok lexer.TokenType) string
- func IsDirectiveValidAtScope(directive lexer.TokenType, scope Scope) bool
- func LookupKeyword(literal string) lexer.TokenType
- type ParseError
- type ParseErrors
- type Parser
- func (p *Parser) CurrentIndentLevel() int
- func (p *Parser) CurrentScope() Scope
- func (p *Parser) CurrentToken() lexer.Token
- func (p *Parser) EnterScope(scope Scope)
- func (p *Parser) Errors() *ParseErrors
- func (p *Parser) ExitScope() Scope
- func (p *Parser) HasErrors() bool
- func (p *Parser) IsConditionalLine() bool
- func (p *Parser) ParseBuildfile() ([]ast.Statement, *ParseErrors)
- func (p *Parser) ParseConditional() (*ast.Conditional, *ParseError)
- func (p *Parser) ParseEnvironment() (*ast.Environment, *ParseError)
- func (p *Parser) ParseInclude() (*ast.Directive, []ast.Statement, *ParseError)
- func (p *Parser) ParseTarget() (*ast.Target, *ParseError)
- func (p *Parser) ParseValue() *ast.Value
- func (p *Parser) ParseVariable() (*ast.Variable, *ParseError)
- type Scope
- type ScopeStack
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DirectiveNameForError ¶
DirectiveNameForError returns the directive name for error messages.
func IsDirectiveValidAtScope ¶
IsDirectiveValidAtScope returns true if the directive is valid at the given scope.
func LookupKeyword ¶
LookupKeyword is a helper that checks if a literal is a keyword. This is used to check for function names.
Types ¶
type ParseError ¶
type ParseError struct {
Message string
Location lexer.SourceLocation
Hint string // Optional hint for fixing the error
}
ParseError represents an error encountered during parsing.
func NewScopeError ¶
func NewScopeError(directive lexer.TokenType, found Scope, loc lexer.SourceLocation) *ParseError
NewScopeError creates a parse error for a directive used at an invalid scope.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
Error implements the error interface.
type ParseErrors ¶
type ParseErrors struct {
Errors []*ParseError
}
ParseErrors collects multiple parse errors.
func (*ParseErrors) Add ¶
func (pe *ParseErrors) Add(err *ParseError)
Add appends an error to the collection.
func (*ParseErrors) Error ¶
func (pe *ParseErrors) Error() string
Error implements the error interface.
func (*ParseErrors) HasErrors ¶
func (pe *ParseErrors) HasErrors() bool
HasErrors returns true if there are any errors.
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser transforms a token stream into an AST.
func (*Parser) CurrentIndentLevel ¶
CurrentIndentLevel returns the expected indentation level for the current scope. Level 0 = global, Level 1 = environment/recipe, Level 2 = block.
func (*Parser) CurrentScope ¶
CurrentScope returns the current parsing scope.
func (*Parser) CurrentToken ¶
CurrentToken returns the current token.
func (*Parser) EnterScope ¶
EnterScope pushes a new scope onto the stack.
func (*Parser) Errors ¶
func (p *Parser) Errors() *ParseErrors
Errors returns the collected parse errors.
func (*Parser) IsConditionalLine ¶
IsConditionalLine checks if the current token starts a conditional. Returns true for if, ifdef, or ifndef keywords.
func (*Parser) ParseBuildfile ¶
func (p *Parser) ParseBuildfile() ([]ast.Statement, *ParseErrors)
ParseBuildfile parses a complete buildfile with error recovery. It collects multiple errors and attempts to continue parsing after each error. Returns a slice of successfully parsed statements and all collected errors.
func (*Parser) ParseConditional ¶
func (p *Parser) ParseConditional() (*ast.Conditional, *ParseError)
ParseConditional parses a conditional block (if/elif/else/end or ifdef/ifndef/end). Grammar:
conditional = if_clause { elif_clause } [ else_clause ] "end" NEWLINE ;
if_clause = "if" condition NEWLINE { statement } ;
elif_clause = "elif" condition NEWLINE { statement } ;
else_clause = "else" NEWLINE { statement } ;
condition = interpolation "==" value | interpolation "!=" value ;
ifdef_clause = "ifdef" identifier NEWLINE { statement } "end" NEWLINE ;
ifndef_clause = "ifndef" identifier NEWLINE { statement } "end" NEWLINE ;
func (*Parser) ParseEnvironment ¶
func (p *Parser) ParseEnvironment() (*ast.Environment, *ParseError)
ParseEnvironment parses an environment block. Grammar: environment_block = ".environment:" [ identifier ] NEWLINE
INDENT { env_directive NEWLINE } DEDENT ;
env_directive = ".using:" value | ".source:" value | ".args:" value | ".requires:" value ;
func (*Parser) ParseInclude ¶
ParseInclude parses a .include: directive and recursively parses the included file. Returns:
- The directive AST node
- The statements from the included file (already parsed)
- Any error encountered
Grammar: ".include:" value NEWLINE
func (*Parser) ParseTarget ¶
func (p *Parser) ParseTarget() (*ast.Target, *ParseError)
ParseTarget parses a target definition. Grammar: target_def = target_spec ":" dependency_list NEWLINE [ recipe ] ; Grammar: target_spec = phony_target | file_target ; Grammar: phony_target = "@" identifier ; Grammar: file_target = path_pattern ;
func (*Parser) ParseValue ¶
ParseValue is the exported method for parsing a value.
func (*Parser) ParseVariable ¶
func (p *Parser) ParseVariable() (*ast.Variable, *ParseError)
ParseVariable parses a variable definition. Grammar: variable_def = [ "lazy" ] identifier "=" value NEWLINE ;
type Scope ¶
type Scope int
Scope represents the current parsing context. Different directives are valid at different scopes.
const ( // ScopeGlobal is the top-level scope for global directives, variables, targets. ScopeGlobal Scope = iota // ScopeEnvironment is inside an .environment: block. ScopeEnvironment // ScopeRecipe is inside a target's recipe section. ScopeRecipe // ScopeBlock is inside a block: within a recipe. ScopeBlock )
func ValidScopesForDirective ¶
ValidScopesForDirective returns the list of scopes where a directive is valid.
type ScopeStack ¶
type ScopeStack struct {
// contains filtered or unexported fields
}
ScopeStack tracks nested scopes during parsing. The parser uses this to validate directive placement.
func NewScopeStack ¶
func NewScopeStack() *ScopeStack
NewScopeStack creates a new scope stack initialized at global scope.
func (*ScopeStack) Current ¶
func (ss *ScopeStack) Current() Scope
Current returns the current (topmost) scope.
func (*ScopeStack) Depth ¶
func (ss *ScopeStack) Depth() int
Depth returns the current nesting depth.
func (*ScopeStack) IsIn ¶
func (ss *ScopeStack) IsIn(scope Scope) bool
IsIn returns true if the given scope is anywhere in the stack. This is useful for checking if we're "inside" a particular scope.
func (*ScopeStack) Pop ¶
func (ss *ScopeStack) Pop() Scope
Pop removes and returns the topmost scope. If at global scope, returns ScopeGlobal without modifying the stack.
func (*ScopeStack) Push ¶
func (ss *ScopeStack) Push(scope Scope)
Push adds a new scope to the stack.
func (*ScopeStack) Reset ¶
func (ss *ScopeStack) Reset()
Reset clears the stack and returns to global scope.