Rules, Atoms, and State Storage
These classes and combinators define modular grammar rules (RuleItem, Ref), atomic token sequences (Item), literal value checks (Literal), and checkpoint-safe token storage (Save, Load).
1. RuleItem (Production Rule Class)
Declarative base class (gram.core.combinators.base.RuleItem) for grammar rules. Its metaclass (RuleMeta) automatically assigns name (defaulting to the class name) and allocates a unique numeric code via AutoCode in code_manager.
Class Attributes
class MyRule(RuleItem):
entrypoint: Combinator | None = None
name: str = "" # Defaults to 'MyRule'
code: int = 0 # Auto-allocated >= 21 unless SetCode(n) is used
nodeable: bool = True # When True, Ref(MyRule) wraps results in ASTRef
description: str = ""
# Optional VSIX / LSP metadata maps (keyed by token index or 'this'):
colors: dict[int | str, str] = {}
autocomplete: dict[int | str, str] = {}
suggestions: dict[int | str, Any] = {}
hints: dict[int | str, str] = {}
playhints: dict[int | str, str] = {}
Root Entrypoint Requirement
When a RuleItem subclass is passed as the root entrypoint to ASTAnalyzer(parser, RootRule) or Parser.parse(RootRule), ASTState.is_valid_entrypoint() enforces that:
RootRule.entrypointis anAltcombinator (otherwise reportsInvalidEntryPointCombinator).- Every child inside that
Altis aRefcombinator (otherwise reportsInvalidEntryPointElement).
2. Ref (Rule Reference)
Delegates parsing to rule.entrypoint at evaluation time, enabling mutual recursion and forward references.
Constructor
Ref(
rule: type[RuleItem],
name: str | None = None,
strict: bool = False,
)
name: Label for the resultingASTRefnode.strict:- When
False(default) andname is None, ifgetattr(rule, "nodeable", False)isTrue(which is the default onRuleItem),self.nameis automatically set torule.name. - When
Trueandname is None,self.nameremainsNoneeven ifrule.nodeableisTrue.
- When
Return Shape & Diagnostics
- If
rule.entrypointisNone, reportsEmptyRuleReference(GrammarError) and returnsNone. - If
self.nameis non-empty: wraps the matched result intoASTRef(name=self.name, nodes=...)(nodesis always alist). - If
self.nameisNone: returns the raw result ofrule.entrypointdirectly without wrapping inASTRef.
Example
from gram.core.combinators import MatchKeyword, MatchToken, Ref, RuleItem, Seq
from gram.core.lexer import Token
class Expr(RuleItem):
nodeable = True
entrypoint = MatchToken(Token.NUMBER)
class ReturnStmt(RuleItem):
entrypoint = Seq(
MatchKeyword("return"),
Ref(Expr), # produces ASTRef(name='Expr', nodes=[LiteralNumber])
)
3. Item
Evaluates a fixed sequence of token-producing combinators as an atomic unit and flattens all matched TokenType objects into an ItemNode (list[TokenType] subclass).
Constructor
Item(*matchs: Combinator)
Execution & Return Shape
- If called with no combinators (
Item()), returns an emptyItemNode([]). - Evaluates each child inside
analyzer.safe_mode(). - If any child returns
None, restores the initial checkpoint, reports/flushesIncompleteItemSequence(GrammarError), and returnsNone. - Extracts
TokenTypeobjects from each child result (TokenType,ASTTokenNodevia.token,ItemNode,LiteralNode,list,tuple). If a child returns any other object (such as anASTRef), restores the checkpoint, reportsInvalidItemElementType(GrammarError), and returnsNone. - Return Value:
ItemNode(tokens)on success, orNoneon failure.
Example
from gram.core.combinators import Item, MatchToken
from gram.core.lexer import Token
double_colon = Item(
MatchToken(Token.COLON),
MatchToken(Token.COLON),
)
4. Literal
Validates the primitive .value of the current token against literal (or accepts any non-keyword token when literal is omitted).
Constructor
Literal(literal: Any = _ANY_LITERAL)
Token.KEYWORDtokens are always rejected (reportingKeywordNotAllowedInLiteral).- When
literalis provided, checkstok.value == literal(including numeric equality acrossintandfloat).
Return Shape & Diagnostics
- Match: Consumes the token and returns
LiteralNode([tok])(alist[TokenType]subclass). - Mismatch / EOF: Reports
LiteralUnexpectedEOF,KeywordNotAllowedInLiteral, orLiteralValueMismatch(ParserError) and returnsNone.
Example
from gram.core.combinators import Literal
match_zero = Literal(0)
match_any_non_keyword = Literal()
5. Save
Stores the TokenType currently at parser.pos into the checkpoint-safe
dictionary parser.storage[name] or into an optional caller-owned mapping.
Constructor
Save(
name: str,
matcher: Combinator | None = None,
*,
consume: bool = True,
storage: MutableMapping[str, TokenType] | None = None,
)
When storage is omitted, Save uses parser.storage. When a mapping is
provided, the token is stored there and that mapping is included in parser
checkpoint tracking.
Consumption Modes
matcher=None(zero-width capture): Storesparser.peek()in the selected storage without advancingparser.posand returns the storedTokenType.matcherprovided,consume=True(default): Evaluatesmatcher(advancingparser.pos), stores the original token in the selected storage, and returnsmatcher's AST result. Ifmatcherfails, restores the checkpoint, reportsSaveMatcherFailed, and returnsNone.matcherprovided,consume=False(validated lookahead capture): Evaluatesmatcher, then restores the checkpoint soparser.posdoes not advance, stores the token in the selected storage, and returns theTokenType.
Example
from gram.core.combinators import Load, MatchToken, Save
from gram.core.lexer import Token
save_open_tag = Save("open_tag", MatchToken(Token.IDENT), consume=True)
typedef_storage = {}
save_typedef = Save(
"current_type",
MatchToken(Token.IDENT),
storage=typedef_storage,
)
load_typedef = Load(
"current_type",
mode="get",
storage=typedef_storage,
)
6. Load
Reads, compares, or swaps a TokenType previously saved in
parser.storage[name] or in an explicitly supplied external mapping.
Constructor
Load(
name: str,
mode: Literal["get", "match", "unmatch", "replace", "change"] = "get",
matcher: Combinator | None = None,
*,
storage: Mapping[str, TokenType] | None = None,
)
Pass the same mapping to Save and Load to share typedefs or dynamic types
without placing them in the parser's default storage.
Modes & Token Consumption
Important: In modes
"match","unmatch","replace", and"change",Loadonly advancesparser.posif amatchercombinator is passed. Whenmatcher=None, those modes operate atparser.poswithout consuming the token.
| Mode | Behavior | Return Value (matcher=None) |
Return Value (matcher provided) |
|---|---|---|---|
"get" |
Returns the saved token from the selected storage without inspecting or moving the cursor. | Stored TokenType |
Stored TokenType (matcher ignored) |
"match" |
Requires current.token == saved.token and current.value == saved.value (else reports LoadMatchFailed). |
current (TokenType), cursor unchanged |
Result of matcher (consumes token) |
"unmatch" |
Requires current to differ in type or value from saved (else reports LoadUnmatchFailed). |
current (TokenType), cursor unchanged |
Result of matcher (consumes token) |
"replace" |
Replaces parser.tokens[parser.pos] in-place with saved (tracked for checkpoint rollback). |
saved (TokenType), cursor unchanged |
Result of matcher on the replaced token |
"change" |
Swaps parser.tokens[parser.pos] and the token saved in the selected storage. |
saved (TokenType), cursor unchanged |
Result of matcher on the swapped-in token |
Diagnostics
StorageKeyNotFound(GrammarError) ifnameis not in the selected storage.LoadUnexpectedEOF(ParserError) ifmode != "get"and the stream is empty.LoadMatcherFailed(GrammarError) ifmatcherfails (restoring the checkpoint, including any"replace"or"change"mutation).
Example
from gram.core.combinators import Load, MatchToken, Save, Seq
from gram.core.lexer import Token
# Match an opening identifier and require the next identifier to have the same token & value:
paired_ident = Seq(
Save("tag", MatchToken(Token.IDENT)),
Load("tag", mode="match", matcher=MatchToken(Token.IDENT)),
)