-
Notifications
You must be signed in to change notification settings - Fork 0
Rules
In CDTk, grammar rules are the core of parsing logic. They define how tokens combine into higher-level structures (like expressions or statements) and ultimately form an Abstract Syntax Tree (AST). A RuleSet groups all the grammar rules for your language and is part of the pipeline after tokenization.
A grammar rule specifies:
- Input Structure: Defines the sequence or pattern of tokens that the rule matches.
- Action: Details the construction of structured objects, such as AST nodes, from the input tokens.
For example:
var rules = new RuleSet
{
new Rule<AssignmentNode>("variable:@Identifier '=' value:@Number")
.Returns<AssignmentNode>("variable", "value")
};-
Pattern: Describes the structure of tokens (e.g.,
@Identifier '=' @Number). - Returns: Indicates the type of AST node to produce and the fields to populate.
Rules are grouped into a RuleSet, and you use the new Rule<T>() API to define each rule. Here’s the basic syntax:
var rules = new RuleSet
{
new Rule<NodeType>("Grammar Pattern")
.Returns<NodeType>("field1", "field2")
};-
NodeType: The type of AST node to generate for this rule. -
Grammar Pattern: The formal definition of the structure using tokens and sub-rules. -
Returns: Maps tokens and rule outputs to fields of the AST node.
CDTk uses a declarative grammar syntax for rules. Here's what each part means:
| Symbol | Description |
|---|---|
@TokenName |
Matches a token defined in the TokenSet. |
RuleName |
References another rule. |
'=' |
Matches a literal string or operator. |
| ` | ` |
* |
Matches zero or more repetitions. |
+ |
Matches one or more repetitions. |
? |
Matches zero or one occurrence. |
-
Basic Rule:
new Rule<Expression>("term:Term '+' term:Term") .Returns<Expression>("term1", "term2")
-
Repetition:
new Rule<Expression>("terms:Term+") .Returns<Expression>("terms")
-
Optional Components:
new Rule<Statement>("expr:Expression ';'?") .Returns<Statement>("expr")
-
Alternation (OR):
new Rule<Factor>("number:@Number | '(' expr:Expression ')'") .Returns<Factor>("value")
Rules can specify how the output AST is constructed by mapping fields to token values or sub-rule outputs.
new Rule<AssignmentNode>("variable:@Identifier '=' value:@Number")
.Returns<AssignmentNode>("variable", "value");-
variable: The@Identifiertoken is mapped to this field. -
value: The@Numbertoken is mapped to this field.
Rules can also construct tree-like AST structures using nested patterns:
new Rule<Expression>("left:Expression '+' right:Term")
.Returns<Expression>("left", "right");Grammar rules can be made scope-aware to ensure proper context resolution when parsing. This feature is particularly useful when dealing with variable declarations, function bodies, or nested structures.
var scopedRules = new RuleSet
{
new Rule<Program>("statements:Statement+")
.Returns<Program>("statements"),
new Rule<Function>("'function' name:@Identifier args:Arguments body:Block")
.Returns<Function>("name", "args", "body")
.Validate((ctx, node, cancellationToken) =>
{
ctx.CurrentScope.Declare(node.GetString("name"));
}),
new Rule<Block>("'{' statements:Statement* '}'")
.Returns<Block>("statements")
.Validate((ctx, node, _) =>
{
ctx.PushScope();
foreach (var statement in node.GetNodes("statements"))
ctx.Eval(statement.Node, CancellationToken.None);
ctx.PopScope();
})
};-
ValidateHooks: These rules inject validation logic into the parsing process, leveraging the context (e.g.,ctx.PushScope()andctx.Declare()). -
Scopes:
- Support nested declarations within blocks.
- Check variable usage against declared scopes.
Rules can reference other rules, enabling complex, hierarchical grammar definitions.
new Rule<Expression>("left:Term addop:('+' | '-') right:Term")
.Returns<Expression>("left", "addop", "right");
new Rule<Term>("left:Factor mulop:('*' | '/') right:Factor")
.Returns<Term>("left", "mulop", "right");
new Rule<Factor>("@Number | '(' expr:Expression ')'")
.Returns<Factor>("value");Rule evaluation follows the order in which rules are defined. Place more specific patterns before general ones.
// Correct Order: Keyword before Identifier
new Rule<Keyword>("keyword:'if'");
new Rule<Identifier>("id:@Identifier");- Keep it Simple: Define one pattern at a time to improve readability and maintainability.
- Prioritize Specificity: Handle edge cases and corner patterns explicitly, especially if they overlap with generic rules.
- Test Rules Thoroughly: Validate grammar for all supported inputs as well as edge cases.
- Use Returns Consistently: Ensure all patterns populate AST fields correctly.
-
Incorporate Scopes for Context-Sensitive Constructs: Use
Validatehooks to manage symbol declarations and usage efficiently.
If a rule doesn't work as expected:
- Validate the TokenSet: Ensure tokens are matched correctly in the tokenizer.
- Check Rule Order: Ensure specific rules are defined before broader patterns.
- Analyze Rule Patterns: Test each rule independently with various inputs.
Use CDTk's debugging tools to inspect the Abstract Syntax Tree (AST) and validate scope management effectively.
Now that you understand how to define and use grammar rules: