`RuleItem` y `nodeable`
En Gram, las reglas de producción gramaticales formales se declaran como subclases de RuleItem (gram.core.combinators.RuleItem). Comprender cómo interactúa RuleItem con Ref y con el atributo nodeable es fundamental para moldear Árboles de Sintaxis Abstracta (AST) limpios y predecibles.
1. Declarar Reglas de Producción con RuleItem
Cada subclase de RuleItem es procesada en tiempo de creación de la clase por la metaclase RuleMeta (gram.core.combinators.base.RuleMeta):
from gram import combinators as cmb
from gram import lexer
class VariableDecl(cmb.RuleItem):
"""Coincide con: int x = 10;"""
name = "VAR_DECL" # Opcional: toma el nombre de la clase ("VariableDecl") si se omite o es "auto"
code = cmb.AutoCode() # Opcional: asignado automáticamente si se omite
nodeable = True # Por defecto: True
description = "Sentencia de declaración de variable"
entrypoint = cmb.Seq(
cmb.MatchGroup("types"),
cmb.MatchToken(lexer.Token.IDENT),
cmb.Opt(
cmb.Seq(
cmb.MatchToken(lexer.Token.ASSIGN),
cmb.ArithmeticExpr(),
)
),
cmb.MatchToken(lexer.Token.SEMICOLON),
)
Atributos Principales de RuleItem
| Atributo | Tipo | Por Defecto | Descripción |
|---|---|---|---|
entrypoint |
Combinator | None |
None |
Requerido. El árbol combinador raíz evaluado cuando esta regla es referenciada por Ref o Parser.parse(). |
name |
str |
cls.__name__ |
Etiqueta de la regla usada en ASTRef(name, nodes), RayTracing.paths y VSIXAnalyzer. Si se omite, es "" o "auto", RuleMeta la establece como cls.__name__. |
code |
int |
AutoCode() |
Identificador numérico único registrado en code_manager. |
nodeable |
bool |
True |
Controla si Ref(MyRule) envuelve la salida coincidente en un nodo contenedor ASTRef (True) o inserta de forma plana los nodos secundarios directos (False). |
description |
str |
"" |
Documentación legible de la regla exportada a tooltips hover de LSP y descripciones de fragmentos por VSIXAnalyzer. |
ignore |
bool |
False |
Flag de metadatos inspeccionado por VSIXAnalyzer. |
is_structural |
bool |
False |
Flag de metadatos que indica una regla estructural en VSIXAnalyzer. |
2. Códigos de Regla Automáticos (AutoCode y SetCode)
RuleMeta asigna automáticamente un entero code sin colisiones a cada subclase de RuleItem mediante el gestor thread-safe code_manager (gram.core.rules.codes.code_manager):
- Los códigos
0al20(CodeManager.PROTECTED_MAX) están reservados para rangos internos del framework. - Por defecto, los códigos de regla del usuario comienzan en
10000(CodeManager.DEFAULT_BASE_CODE) e incrementan de 1 en 1. - Puedes cambiar la base inicial o el paso antes de definir un bloque de reglas usando
SetCode(base, step=1)(accesible víafrom gram.core.rules import AutoCode, SetCodeofrom gram.core.combinators import AutoCode, SetCode):
from gram import combinators as cmb
cmb.SetCode(20_000, step=10)
class RuleA(cmb.RuleItem):
entrypoint = cmb.MatchToken("IDENT")
class RuleB(cmb.RuleItem):
entrypoint = cmb.MatchToken("NUMBER")
print(RuleA.code, RuleB.code) # 20000 20010
Si config.RULES_WARN_COLLISION es True, asignar manualmente el mismo entero code (> 20) a dos clases RuleItem distintas emite un UserWarning.
3. ¿Qué es nodeable? (nodeable = True vs. nodeable = False)
Cuando una regla ParentRule hace referencia a otra regla ChildRule mediante Ref(ChildRule), ¿cómo decide Gram si ChildRule se convierte en una rama con nombre propio (ASTRef) en el AST o simplemente aporta sus tokens/nodos coincidentes directamente a ParentRule?
Ese comportamiento lo controla ChildRule.nodeable.
Cómo Evalúan nodeable los Métodos Ref.__init__ y Ref.parse
En gram/core/combinators/reference.py:
class Ref(Combinator):
def __init__(
self,
rule: type[RuleItem],
name: str | None = None,
strict: bool = False,
) -> None:
super().__init__()
self.rule = rule
self.name = name
if self.name is None and not strict and getattr(self.rule, "nodeable", False):
self.name = self.rule_name
Y durante Ref.parse(analyzer):
- Si
self.nametiene valor,Ref.parseenvuelve el resultado coincidente enASTRef(self.name, nodes). - Si
self.nameesNone,Ref.parseretorna elresultcrudo directamente (el cualSeqaplana dentro de la listanodesde la regla contenedora).
Caso 1: nodeable = True (Por Defecto — Crea un Nodo ASTRef)
Usa nodeable = True para reglas que representen construcciones sintácticas significativas en tu AST (como declaraciones, sentencias, parámetros, punteros o cláusulas). Cada vez que Ref(MyRule) coincide, produce un ASTRef(name="MyRule", nodes=[...]).
Caso 2: nodeable = False (Regla Auxiliar Transparente — Inserción Plana de Hijos)
Usa nodeable = False para reglas intermedias de conveniencia o agrupación que solo existen para mantener modular la gramática, sin añadir capas de anidamiento innecesarias al AST.
Por ejemplo, en un frontend de C:
SIGNEDcoincide conMatchGroup('signed_')(signedounsigned). Establecernodeable = Falsesignifica queINTRINSIC_TYPErecibe elASTKeyword('unsigned')directamente en lugar de un anidadoASTRef('SIGNED', [ASTKeyword('unsigned')]).TYPEes un despachadorAlt(Ref(INTRINSIC_TYPE), Ref(ALIASED_TYPE))dondeINTRINSIC_TYPEyALIASED_TYPEya tienennodeable = True. EstablecerTYPE.nodeable = Falseevita envolverASTRef('INTRINSIC_TYPE', ...)dentro de un innecesarioASTRef('TYPE', ...).
Modificar nodeable en la Llamada (Parámetros de Ref)
Puedes invalidar la configuración nodeable de una regla objetivo en cualquier instancia concreta de Ref:
TargetRule.nodeable |
Llamada a Ref(...) |
Salida AST Resultante |
|---|---|---|
True |
Ref(TargetRule) |
ASTRef(TargetRule.name, [...]) |
True |
Ref(TargetRule, name="Custom") |
ASTRef("Custom", [...]) |
True |
Ref(TargetRule, strict=True) |
Nodo(s) hijo(s) directos — sin contenedor ASTRef (strict=True impide copiar TargetRule.name cuando name=None) |
False |
Ref(TargetRule) |
Nodo(s) hijo(s) directos — sin contenedor ASTRef |
False |
Ref(TargetRule, name="ForcedNode") |
ASTRef("ForcedNode", [...]) |
Ejemplo Ejecutable Completo
from gram import combinators as cmb
from gram import lexer, parser
lexer.words.clear_all()
lexer.words.add_group("types", allow_override=True)
lexer.words.add_group("signed_", allow_override=True)
lexer.words.add_keyword("int", group="types", allow_override=True)
lexer.words.add_keyword("unsigned", group="signed_", allow_override=True)
# Regla auxiliar: nodeable = False (inserta su token directamente en INTRINSIC_TYPE)
class SIGNED(cmb.RuleItem):
entrypoint = cmb.MatchGroup("signed_")
nodeable = False
# Regla de tipo concreto: nodeable = True (produce ASTRef("INTRINSIC_TYPE", ...))
class INTRINSIC_TYPE(cmb.RuleItem):
entrypoint = cmb.Some(
cmb.Alt(
cmb.MatchGroup("types"),
cmb.Ref(SIGNED),
)
)
nodeable = True
# Regla despachadora: nodeable = False (deja pasar INTRINSIC_TYPE sin contenedor extra)
class TYPE(cmb.RuleItem):
entrypoint = cmb.Alt(
cmb.Ref(INTRINSIC_TYPE),
cmb.MatchToken(lexer.Token.IDENT),
)
nodeable = False
# Regla de parámetro: nodeable = True (produce ASTRef("PAIR_PARAM", ...))
class PAIR_PARAM(cmb.RuleItem):
entrypoint = cmb.Seq(
cmb.Ref(TYPE),
cmb.MatchToken(lexer.Token.IDENT),
)
nodeable = True
# Regla de punto de entrada raíz: debe ser un Alt de Ref(...)
class PROGRAM(cmb.RuleItem):
entrypoint = cmb.Alt(cmb.Ref(PAIR_PARAM))
tokens = lexer.Lexer("unsigned int count").process()
ast_nodes = parser.Parser(tokens).parse(PROGRAM)
print("Root:", ast_nodes)
print("Hijos de PAIR_PARAM:", ast_nodes[0].nodes)
print("Hijos de INTRINSIC_TYPE:", ast_nodes[0].nodes[0].nodes)
# Salida:
# Root: [AstRef(nodes=2, name=PAIR_PARAM)]
# Hijos de PAIR_PARAM: [AstRef(nodes=2, name=INTRINSIC_TYPE), Identifier(value='count', line=1, col=13)]
# Hijos de INTRINSIC_TYPE: [ASTKeyword(value='unsigned', line=1, col=0), ASTKeyword(value='int', line=1, col=9)]
Observa cómo PAIR_PARAM contiene directamente ASTRef('INTRINSIC_TYPE', [ASTKeyword('unsigned'), ASTKeyword('int')]) e Identifier('count'), sin nodos contenedores redundantes para TYPE o SIGNED.
4. Interacciones de nodeable con Separator y VSIX
- Validación en
Separator(UnnamedRefInSeparator): Cuando se pasa unRefdentro deSeparator(sep=..., values=[...]),Separatorrequiere que cada elementoRefproduzca un nodoASTRefnombrado. Si pasasRef(HelperRule)dondeHelperRule.nodeableesFalsey no especificas unname="..."explícito enRef,Separatorreporta unGrammarError(UnnamedRefInSeparator/GRAMMAR_COMBINATOR_UNSUPPORTED). - Mapas Declarativos de VSIX (
colors,autocomplete,suggestions,hints,playhints):ASTColorResolverproyectaRuleItem.colorssobre los nodosASTRefhaciendo coincidirref_node.namecon las clasesRuleItemregistradas. Si una regla tienenodeable = False, nunca emite un nodoASTRefen el AST, por lo que su mapacolorsno puede proyectarse sobre el árbol (VSIXAnalyzeremite una advertencia si se combinanodeable = Falsecon mapas de editor).
5. Reglas de Punto de Entrada Raíz (Parser.parse y ASTAnalyzer)
Cuando pasas una clase RuleItem raíz a Parser(tokens).parse(ENTRYPOINT, mode="Many") (o ASTAnalyzer(parser, ENTRYPOINT, mode="Many")), ASTState.is_valid_entrypoint() comprueba dos condiciones estructurales sobre ENTRYPOINT:
ENTRYPOINT.entrypointdebe ser un combinadorAlt(de lo contrario reportaAST_PROGRAM_INVALID_COMBINATOR).- Cada elemento dentro de
ENTRYPOINT.entrypoint.combinatorsdebe ser un combinadorRef(de lo contrario reportaAST_DECLARATOR_INVALID_ELEMENT).
Modos de Repetición de Nivel Superior ("Many" vs. "Some")
mode="Many"(por defecto): Evalúa repetidamenteENTRYPOINT.entrypointa lo largo del flujo de tokens (omitiendo automáticamente los tokensToken.NEWLINEsueltos en el nivel superior) hasta alcanzarToken.EOF. Un archivo vacío retorna[].mode="Some": Requiere que al menos una sentencia de nivel superior coincida antes deToken.EOF, reportandoEntryPointMinimumNotMet(GRAMMAR_COMBINATOR_FAILED) si la entrada no produce nodos en el AST.