Embedded ANTLR Code Execution Model
1. Introduction
This document defines the architecture boundary for embedded ANTLR code in Utils.Parser.
It is a design and documentation reference only. It does not introduce execution behavior changes.
The goal is to maximize ANTLR compatibility while preserving runtime determinism, parser authority, and language-neutral core responsibilities.
2. ANTLR standard behavior
In standard ANTLR, embedded code is interpreted as target-language code and injected into generated lexer/parser code.
Typical constructs:
- semantic predicates:
{ condition }? - inline parser actions:
{ code } - rule actions:
@init { code },@after { code } - grammar actions:
@header,@members,@parser::members,@lexer::members - runtime-inline lexer predicates and lexer actions
Standard ANTLR semantics:
{ code }is emitted as target-language executable code.{ condition }?is emitted as target-language conditional logic.@membersadds members to the generated target-language class.
3. Current Utils.Parser behavior
Current behavior is intentionally conservative by default. The canonical compatibility status is maintained in ANTLRCompatibility.md; this document focuses on execution architecture.
The embedded-code doctrine is:
The parser parses the grammar.
Embedded code is target-language code.
Embedded code is preserved unchanged by default.
Any transformation of embedded code must go through IParserEmbeddedCodeTransformer.
Generated mode emits transformed code.
Dynamic mode sends transformed code to the existing compiler/preparer mechanism.
The parser/generator core must not grow target-language-specific rewriters.
Default flow:
raw embedded code
→ RawEmbeddedCode
→ ParserEmbeddedCodeTransformationService.TransformOrThrow(...)
→ NoOpParserEmbeddedCodeTransformer
→ diagnostics validation
→ TransformedEmbeddedCode
→ generated C# emission OR existing dynamic compiler/preparer
Optional transformer flow:
raw embedded code
→ RawEmbeddedCode
→ ParserEmbeddedCodeTransformationService.TransformOrThrow(...)
→ custom IParserEmbeddedCodeTransformer
→ diagnostics validation
→ TransformedEmbeddedCode
→ generated C# emission OR existing dynamic compiler/preparer
- Semantic predicates are recognized and routed through
ISemanticPredicateEvaluator. - Inline parser actions are recognized and routed through
IParserActionExecutor. @initand@afterare recognized and stored on the rule model; they are not executed by default, but the source-generator C# path now generates and executes lifecycle hook methods for them throughParseWithEmbeddedCode(...)or an explicit-contextCreateRuntimePolicy(executionContext, basePolicy)result, with missing declared local names allocated as untypednullframe entries before@initand explicit helper methods for lifecycle hook code to read/write only that store. Existing entries are preserved; no typed fields/properties or implicit local variables are generated.- Grammar actions and
@membersare preserved as metadata only when visible to ingestion. - Runtime-inline lexer predicates and lexer actions remain outside the executable embedded-code scope. Simple lexer predicates and inline actions are executable only in the explicit generated-C# opt-in path.
- No raw embedded ANTLR target-language code is executed automatically.
Two explicit opt-in paths exist for parser semantic predicates and inline parser actions:
- a runtime-inline prepared expression path assembled by callers through
Utils.Parser.Expressions; - a source-generator C# path emitted by
Utils.Parser.Generatorsand activated through generated helpers.
Both paths install runtime policy handlers explicitly. ParserEngine remains language-neutral and default parsing remains conservative.
4. Two-path target architecture
The target architecture uses two distinct implementation paths over one shared parser model:
- Source-generation path (compile-time
.g4ingestion, C# source generation). - Runtime-inline path (runtime
.g4ingestion with an explicitly providedIExpressionCompiler).
These paths must not be conflated. They share the same intent:
compile/generate during parser model generation or source generation
execute during parsing
They must not converge on this weaker model as the architectural target:
store source text
compile opportunistically during predicate/action evaluation
Shared expectations across both paths:
- same ANTLR construct classification;
- same model concepts;
- same deterministic diagnostic vocabulary;
- same runtime authority boundaries;
- prepared executable output before parsing begins when executable embedded code is enabled.
The prepared output is intentionally path-specific:
Utils.Parser.Generatorsproduces C# source hooks compiled by Roslyn in the consuming project for supported parser predicates/actions;- runtime-inline ingestion produces compiled expression artifacts through the configured
IExpressionCompilerwhen callers run the prepared registry/policy builder.
Adapters may differ by path, but model semantics and runtime dispatch indexes must stay aligned.
4.1 Minimal preparation boundary
The repository now contains a minimal preparation boundary in Utils.Parser for embedded-code preparation work. These contracts are public so optional packages such as Utils.Parser.Expressions can produce artifacts without duplicating the embedded-code model. They model:
- raw embedded source text and construct kind (
EmbeddedCodeSource,EmbeddedCodeKind); - explicit target path metadata (
EmbeddedCodePreparationContext,EmbeddedCodeTarget); - the contextual symbol model (
ruleName,inputPosition,alternativeIndex,elementIndex); - preparation outcomes (
EmbeddedCodePreparationResult<TArtifact>,EmbeddedCodePreparationStatus); - one narrow preparation interface for semantic predicates and inline parser actions (
IEmbeddedCodePreparer<TPredicateArtifact, TActionArtifact>).
This boundary is intentionally metadata/preparation-only. It is not wired into ParserEngine, does not change scheduling or memoization, and does not activate embedded code by default. The neutral preserving preparer returns explicit preserved/unsupported metadata and never compiles or executes embedded source. Making the boundary public is an API exposure for pre-release preparation/tooling integration only; it is not runtime activation.
4.2 Embedded-code transformer boundary
IParserEmbeddedCodeTransformer is the only supported transformation boundary for embedded parser code. Runtime preparation and generated emission now materialize this boundary through ParserEmbeddedCodeTransformationService, with typed RawEmbeddedCode and TransformedEmbeddedCode values so injection and expression compilation consume transformed code explicitly rather than ambiguous strings. TransformedEmbeddedCode has no public constructor; callers obtain it through the service that invokes the transformer and validates error diagnostics. The transformer receives a ParserEmbeddedCodeTransformationContext and returns a ParserEmbeddedCodeTransformationResult. The result carries transformed target-language code and optional ParserEmbeddedCodeDiagnostic entries with a ParserEmbeddedCodeDiagnosticSeverity. The context includes passive/descriptive metadata such as Code, Location, GrammarName, RuleName, Parameters, Locals, Returns, and Labels; this metadata is available to transformers, but parser/generator core code must not depend on a specific transformer implementation.
ParserEmbeddedCodeLocation identifies supported locations, including parser @header, parser @footer, rule @init, rule @after, inline parser actions, and semantic predicates where that location exists in the current path. NoOpParserEmbeddedCodeTransformer is the default transformer. It returns the embedded code unchanged and is the correct default when grammar actions are already valid target-language code.
A transformer is not a compiler. It only prepares target-language source text. Generated mode emits the transformed target-language text, while dynamic mode passes the transformed text to the existing compiler/preparer supplied by the caller. If a transformer returns an error diagnostic, generation or preparation must surface that error deterministically and must not silently pass invalid transformed code to the compiler/preparer. The shared transformation service reports these failures through structured metadata that includes the transformation path (GeneratedCodeEmission or RuntimeCompilation), embedded-code location, grammar name, rule name, diagnostic code/message, span when available, and the original transformer exception when the transformer throws.
ANTLR-style $... convenience forms such as $x.value, $xs.value, $rule.value, $param, and $local are not core parser syntax. With the no-op transformer they are emitted or prepared unchanged. If unchanged $... code is not valid target-language code, generated compilation or dynamic preparation may fail. An optional C# ANTLR-style convenience transformer may rewrite documented current-rule forms such as $param, $local, declared bare $returnName, assignment-label $c.value, and list-label $xs.value; the transformer is target-language-specific compatibility code, not parser core behavior.
5. Source generator C# path
Pipeline:
.g4consumed asAdditionalFilesbyUtils.Parser.Generators;- grammar parsed by internal G4 tokenizer/parser;
- C# emitted by
GrammarEmitter; - final compilation performed by Roslyn.
Current state:
- generated model construction is implemented;
- embedded predicates and actions continue to be preserved as metadata strings such as
ValidatingPredicate("...")andEmbeddedAction("...", ...); - a generated execution context class (
{ClassName}ExecutionContext) owns generated C# hooks, any injected parser@membersblocks, and generatedFork()/CopyFrom(...)copy helpers; - generated C# hooks are emitted as instance methods on that context for supported parser semantic predicates and inline parser actions;
- generated dispatchers implement
ISemanticPredicateEvaluatorandIParserActionExecutorand are bound to one execution-context instance; - generated
ParseWithEmbeddedCode(...)helpers provide the fresh-context opt-in path, and generatedCreateRuntimePolicy(executionContext, basePolicy)binds a policy to a caller-supplied execution context; - generated
ParseWithEmbeddedCode(string input)creates a fresh execution context for that parse, while the overload accepting{ClassName}ExecutionContextlets advanced callers supply and observe a context explicitly; - generated
Fork()returns a copied execution context throughParserExecutionContextCopier<TContext>.Copy(...), preservingICloneableprecedence when a user partial context implements it; - generated
CopyFrom(source)validatessourceand copies source state into the current context throughParserExecutionContextCopier<TContext>.CopyTo(source, this); - generated
Parse(...)remains conservative and does not install generated embedded-code hooks.
Context-copy preparation:
Utils.Parser.Runtime.ParserExecutionContextCopier<TContext>is available as a public runtime helper for future generated-context snapshot/fork/commit designs and is exposed by generated execution contexts throughFork()andCopyFrom(...);- the helper inspects each closed context type once, builds a compiled
Action<TContext, TContext>field-copy delegate, and caches that delegate through the closed generic type; Copy(source, factory)first usessource.Clone()whensourceimplementsICloneable; the clone result must be non-null and assignable to the context type;- the semantics of
ICloneable.Clone()belong to the user context type, and the caller-provided factory is used only when the source does not implementICloneable; Copy(source, factory)creates the target instance through the caller-supplied factory for field-copy contexts so generated code can copy internal or non-public-constructor contexts from the consuming assembly;CopyTo(source, target)copies into an existing context through the field-copy delegate and intentionally does not useICloneable, making it suitable for future commit/restore experiments;- field-copy behavior is a shallow structural copy, not a universal deep copy: value fields, strings, nullable values, enums, and unrecognized references are assigned directly;
- known containers are recreated with explicit copy expressions (
T[],List<T>,Dictionary<TKey,TValue>, andHashSet<T>), but their elements remain shallow-copied references or values; - unknown
IEnumerable<T>collection fields may also be recreated when they expose a compatible public copy constructor, or a public parameterless constructor plusAddRange(IEnumerable<T>), or a public parameterless constructor plusAdd(T); - unknown collections without one of those safe reconstruction strategies are copied by reference instead of failing or producing a partial copy;
- null known or reconstructable containers remain null, static fields are not copied, field-like event backing fields are skipped, and readonly instance fields cause an explicit configuration exception instead of being ignored silently;
- compiler-generated auto-property backing fields are treated as context state and are copied unless they are readonly;
- generated
Fork()andCopyFrom(...)are helpers for snapshot/fork/commit work; generated policies expose them throughIParserExecutionStateManager, andParserEnginenow calls the manager around parser backtracking attempt boundaries: ordinary parser alternatives, left-recursive extensions, quantifier attempts, and negation probes. This rollback is limited to managed parser execution state: it does not add complete ANTLR transactional semantics, action buffering, external side-effect rollback, lexer actions, or runtime-inline lexer predicates. Parser@initand@afterhooks participate only in the source-generator C# opt-in path.
Execution boundary:
- embedded parser code selected for execution in this path is target-language C# source;
- the generator builds a
ParserEmbeddedCodeTransformationContext, applies the configuredIParserEmbeddedCodeTransformer, and emits the transformed code; - parser
@header, parser@footer, rule@init, rule@after, inline parser actions, and semantic predicates are transformed where those locations are supported by the generated path; - the standard source-generator path uses
NoOpParserEmbeddedCodeTransformer, so embedded code appears in generated C# as written unless direct emitter APIs are supplied a custom transformer; - Roslyn compiles that generated C# as part of the consuming project;
- parsing invokes the already-generated hook rather than asking
ParserEngineto compile source text; - the generator performs wrapping/indentation and leaves target-language validation to Roslyn.
Boundary rules:
- invalid embedded C# is a C# compilation problem, not a parser-runtime evaluation problem;
- this path is C#-specific and belongs to
Utils.Parser.Generators; - this path does not imply runtime support for arbitrary target-language code;
- generated hook execution remains opt-in through generated policy helpers.
6. Runtime-inline expression compiler path
Pipeline target:
.g4parsed at runtime;- embedded ANTLR code classified and preserved as raw text metadata;
- an explicit expression compiler is selected by configuration, not by
ParserEngine; - preparation/generation compiles embedded code through
IExpressionCompiler; - the prepared expression/delegate/function is stored in the parsing model or in an adjacent executable artifact;
- parsing executes that prepared artifact through runtime policy interfaces.
Conceptual mapping:
- semantic predicates execute through
ISemanticPredicateEvaluator; - parser inline actions execute through
IParserActionExecutor.
Dynamic transformation flow:
raw embedded code
→ RawEmbeddedCode
→ ParserEmbeddedCodeTransformationService.TransformOrThrow(...)
→ IParserEmbeddedCodeTransformer.Transform(...)
→ diagnostics validation
→ TransformedEmbeddedCode
→ existing compiler/preparer passed to the parser
→ prepared predicate/action
The transformer is not a compiler and does not replace the existing expression compiler/preparer mechanism. It only prepares target-language source text before the configured compiler/preparer is invoked. If a transformer reports an error diagnostic, the compiler/preparer is not invoked and the error is surfaced deterministically. No parallel compiler abstraction should be introduced.
ISemanticPredicateEvaluator and IParserActionExecutor are runtime execution interfaces. They are not, by themselves, the complete generation/preparation boundary for embedded code. The explicit preparation boundary now exists separately and can produce path-specific artifacts before parsing. Optional runtime adapters can consume prepared expression artifacts through ParserRuntimeFeaturePolicy, and Utils.Parser.Expressions now provides an explicit convenience builder that assembles that prepared policy. These artifacts are still not prepared automatically and are not invoked by default by ParserEngine.
Strict rules:
- no raw target-language execution;
- no implicit language selection;
IExpressionCompilermust be explicitly injected or otherwise explicitly selected by the caller;ParserEnginemust not know whether the embedded source was C#, VB-like syntax, or another expression language;ParserEnginemust not become responsible for compiling embedded source text;Utils.Parsercore must not referenceUtils.Expressions.CSyntaxorUtils.Expressions.VBSyntaxdirectly.
Current intermediate status:
ExpressionEmbeddedCodePreparerinUtils.Parser.Expressionscan prepare runtime-inline semantic predicate and inline parser action artifacts through an explicitly suppliedIExpressionCompiler.- Prepared expression artifacts only expose contextual symbols allowed by
EmbeddedCodePreparationContext.SupportedSymbols. Exposed symbols (ruleName,inputPosition,alternativeIndex,elementIndex) are resolved from the runtime context parameter at execution time, avoiding capture of preparation-time values. - The expression-backed preparer returns
PreservedNotCompiledfor the source-generator C# target because that path belongs toUtils.Parser.Generators, not to runtime-inline expression preparation. - The preparer is not connected to
ParserEngine; therefore it does not change default runtime behavior. PreparedExpressionEmbeddedCodeRegistryinUtils.Parser.Expressionscan store prepared semantic predicates separately from prepared parser inline actions. Its key uses the embedded-code kind, owning rule name, raw source text, alternative index, and element index, which is the safest audit-friendly identity currently available from preparation metadata and runtime contexts without modifyingParserEngine.PreparedExpressionEmbeddedCodeRegistryBuilderinUtils.Parser.Expressionscan explicitly scan an already-builtParserDefinition, prepareValidatingPredicateand inline parserEmbeddedActionnodes, populate a registry, and return build entries for successes, non-success preparation results, duplicate keys, and skipped unsupported actions.- The registry builder uses the same local index strategy exposed by runtime contexts: alternatives are considered in scheduler priority order, sequence items use zero-based element indexes, quantifier and negation inner probes use the active runtime direct-inner element index, direct-left-recursive recursive alternatives are prepared from the runtime tail after leading self-reference removal, and unavailable indexes remain absent rather than invented.
PreparedExpressionSemanticPredicateEvaluatormaps registeredPreparedExpressionSemanticPredicateartifacts toISemanticPredicateEvaluatorwithout depending onIExpressionCompileror compiling source text during evaluation.PreparedExpressionParserActionExecutormaps registeredPreparedExpressionParserActionartifacts toIParserActionExecutorwithout depending onIExpressionCompileror compiling source text during execution.PreparedExpressionRuntimePolicyBuilderassembles the full opt-in path: caller-suppliedIExpressionCompiler,ExpressionEmbeddedCodePreparer,PreparedExpressionEmbeddedCodeRegistryBuilder, registry-backed evaluator/executor, and aParserRuntimeFeaturePolicy. The build result exposes the policy, registry, registry build result, andHasFailuresaudit summary.PreparedExpressionRuntimePolicyBuilderOptions.BasePolicylets callers preserve unrelated policy settings while replacing onlySemanticPredicateEvaluatorandParserActionExecutor.- Missing prepared artifacts return conservative
NotEvaluated/NotExecutedoutcomes, so the existing parser fallback diagnostics and continuation behavior remain owned byParserEngine. - Automatic model-wide preparation from
ParserEngineis not implemented; callers may invoke the explicit runtime policy builder or assemble the registry builder and adapters manually. Skips remain limited to constructs outside this runtime-inline path, such as grammar-level actions, rule lifecycle actions, and non-inline actions. ExpressionSemanticPredicateEvaluatormapsIExpressionCompilertoISemanticPredicateEvaluatorfor semantic predicates ({ condition }?).ExpressionParserActionExecutormapsIExpressionCompilertoIParserActionExecutorfor inline parser actions ({ code }).- The expression-compiler adapters are useful explicit runtime integration points, but they are an intermediate step rather than the final architectural boundary because they may compile opportunistically during predicate/action invocation.
- Default parser runtime behavior is unchanged (
NotEvaluatedwithUP1006when applicable for predicates,NotExecuted/UP1005default behavior for actions). - Expression-backed semantic predicate evaluation returns a structured outcome so compilation failures and delegate-shape adaptation failures can carry
UP1026metadata, whileParserEngineremains the only component that emits diagnostics. - Inline actions still do not control parse acceptance, parse-tree shape, or branch rejection.
ExecutedandNotExecutedoutcomes both continue parsing; no context mutation, noContextDelta, and no lexer action/predicate or grammar-members execution support are introduced.- The symbol model is intentionally minimal and read-only (
ruleName,inputPosition,alternativeIndex,elementIndex). - Predicate adapter cache: compilation-only and not parse-result memoization. Predicates that do not reference contextual symbols can be cached by predicate source; predicates referencing
ruleName,inputPosition,alternativeIndex, orelementIndexare currently recompiled per evaluation to avoid context capture. - Action adapter cache: compilation-only and not parse-result memoization. Non-contextual actions can be cached by action source; actions referencing
ruleName,inputPosition,alternativeIndex, orelementIndexare currently recompiled per execution to avoid context capture.
The last two cache bullets describe the opportunistic-compilation adapters, not the prepared-artifact path. The prepared runtime-inline model prepares executable artifacts before parsing and executes those artifacts during parsing without opportunistic source compilation on predicate/action invocation. The prepared expression registry builder and runtime policy builder provide that explicit opt-in assembly step; automatic model preparation from ParserEngine remains out of scope.
7. Interface boundary
The preparation boundary is explicit and separated from runtime execution interfaces. The current interface is IEmbeddedCodePreparer<TPredicateArtifact, TActionArtifact>, with path-specific artifact types supplied by the implementation package.
Separation of concerns:
- preparation/generation boundary: receives embedded source plus compilation context and produces a path-specific executable artifact before parsing;
- runtime evaluation boundary:
ISemanticPredicateEvaluator; - runtime execution boundary:
IParserActionExecutor.
The execution interfaces consume runtime context and return runtime outcomes. They should not become responsible for selecting the embedded language, compiling raw source text as part of parsing, or owning parser diagnostics.
8. Cache boundary
Allowed cache scope for future embedded-code preparation:
- key: raw embedded code + compilation context;
- value: path-specific executable artifact, such as generated C# source/hook metadata for the generator path or a compiled expression/delegate/function for the runtime-inline path.
Example conceptual key fields:
- source text;
- construct kind;
- expected result type;
- compiler identity/language;
- symbol model version.
Non-goal boundary:
- this preparation cache is not parse-result memoization;
- no
(input position + rule) -> parse resultsemantic memoization changes; - future caching should avoid recompiling source text during predicate/action invocation.
9. Predicate vs action mapping
Semantic predicate { condition }?
Conceptual outcomes:
- compiled boolean expression;
true->SemanticPredicateEvaluationOutcome.Satisfied;false->SemanticPredicateEvaluationOutcome.Rejected;- unsupported/failed ->
NotEvaluated+ diagnostic.
Parser inline action { code }
Conceptual outcomes:
- compiled action/effect expression;
- executable path ->
ParserActionExecutionOutcome.Executed; - unavailable path ->
ParserActionExecutionOutcome.NotExecuted+ diagnostic.
Rule actions @init / @after
Current status:
- recognized;
- stored on
Rule.InitAction/Rule.AfterAction; - not automatically executed.
Future possibilities (separate PRs):
- source generator C# hook path;
- runtime explicit compiler + explicit runtime policy.
Grammar actions
@header and @lexer::members remain metadata-only by default. In the source-generator C# path only, unscoped @members and @parser::members are injected into the generated execution context; the runtime-inline prepared expression path still treats them as non-executable metadata.
Future source generator mapping may provide C#-specific explicit hooks. Runtime ingestion must not execute raw grammar members.
Lexer actions and predicates
Lexer embedded semantics are a separate, higher-risk domain because they can affect tokenization, mode transitions, channel/type behavior, and stateful lexing. The only executable lexer embedded-code surface is the explicit source-generator C# opt-in path. Conservative generated Parse(...) and the runtime-inline prepared expression path do not execute lexer actions or lexer predicates.
In the generated-C# opt-in path, simple lexer predicates are evaluated during lexer matching and reject only the current token path. Simple lexer inline actions are collected while matching but execute only after the owning token rule has been accepted. Accepted lexer actions execute before the language-neutral lexer engine applies accepted lexer commands for that token/chunk. Commands from rejected paths are not applied. Current regression coverage locks this boundary for skip, channel(...), type(...), more, mode(...), pushMode(...), popMode, and mode-scoped predicate rejection as already supported by the lexer runtime.
This support includes only the optional generated-C# lexer action read rewrite documented below for $text, $type, $channel, $mode, $line, and $pos, plus the bounded simple $type = ..., $channel = ..., and $mode = ... write subset through LexerActionExecutionResult; it does not add lexer predicate attributes, other lexer attribute writes, runtime-inline lexer execution, a separate runtime lexer, generalized action buffering/replay, complete ANTLR target-language lexer command/mode semantics beyond the explicit runtime commands and bounded generated-C# action-result writes documented here, general lexer rollback, or rollback of external side effects performed by lexer actions.
The execution sequence is deliberately asymmetric with parser speculation. A predicate runs during recognition with no supported managed mutation surface; its Boolean outcome affects only the current lexer path. Actions on unselected paths do not run. After token selection, the accepted actions receive one new LexerActionExecutionResult local to that acceptance, run in source occurrence order, and stage TokenType, Channel, and Mode requests with the existing last-write-wins property semantics. The engine applies those requests and then executes commands, so commands remain authoritative. $mode = ... replaces the current mode like mode(...) and never implies pushMode(...) or popMode.
Lexer-owned operational state has narrower lifetimes than a parser rollback snapshot. Persistent LexerEngine fields cover the current mode, mode stack, more accumulation, and associated more start position between token recognitions. A tokenization session owns the TextReaderBuffer and current input position, the emitted-token collection, and the per-call extension invocation contexts. Recognition and acceptance then use attempt- or acceptance-local values such as best-match bookkeeping, collected commands and action occurrences, token/chunk construction data, and the fresh LexerActionExecutionResult. None of these categories is managed parser state. IParserExecutionStateManager must not be reused for them, and no generic parser/lexer manager selected by an isLexer flag is implied. A lexer-specific snapshot contract would require a concrete future need and its own PR.
@lexer::members fields and mutable objects live in the generated execution context. Neither they nor arbitrary I/O, shared-service changes, or mutations of external objects receive a general rollback guarantee. Delaying actions until acceptance reduces rejected-alternative leakage; it does not mean that skip, a later exception, a parser failure, or another subsequent operation reverses accepted action effects. The same external-effect limitation applies to user predicate code.
10. Project responsibilities
Utils.Parser
Responsible for:
- runtime grammar ingestion;
- embedded-code recognition/classification;
- raw source preservation in model metadata;
- routing via runtime policy abstractions;
- internal preparation boundary contracts for future executable or generable artifacts;
- deterministic diagnostics;
- preserving
ParserEngineauthority.
Not responsible for:
- interpreting C# or VB source;
- implicit language selection;
- direct target-language compilation/execution;
- hidden semantic state ownership.
Current preparatory helpers:
ParserRuleInvocationFrame,ParserRuleInvocationDescriptor,IParserRuleInvocationFrameManager,NullParserRuleInvocationFrameManager, andStackParserRuleInvocationFrameManagerprovide passive per-rule invocation-frame infrastructure with explicit call-stack semantics.ParserEngineenters a frame for actual rule execution, passes a descriptor built from currently available parser-rule metadata, and exits it with a success flag in atry/finallyblock;ParserRuleLifecycleContextcan expose that frame to lifecycle hooks. Frames now exposeParent(the caller's frame, ornullfor root-level rules) andDepth(zero-based call-stack depth).StackParserRuleInvocationFrameManageris the stack-aware implementation:Enter(...)creates a child of the current frame and makes it current;Exit(frame, succeeded)pops the matching frame and restores the parent as current; a mismatched exit throwsInvalidOperationException. Generated C# opt-in runtime policies (CreateRuntimePolicy) now installStackParserRuleInvocationFrameManagerautomatically; lifecycle hooks inParseWithEmbeddedCode(...)can observecontext.InvocationFrame.Parentandcontext.InvocationFrame.Depth. The frame stack is implicitly rollback-aware: the engine'stry/finallystructure ensures every rule entry's frame is exited regardless of success, so failed alternatives, quantifier iterations, negation probes, and memoization hits cannot leave stale frames. The conservativeParse(...)policy continues to useNullParserRuleInvocationFrameManager.Instance. This call-stack model is preparatory infrastructure for future rule return and argument support only: rule parameters, returns, throws/catch/finally metadata, and rule options remain metadata-only and are not bound, typed, propagated, applied, or executed automatically. Rule locals also remain untyped and unbound; only the generated C# opt-in lifecycle executor allocates captured local names as missing-onlynullframe entries before@init. Rule locals are preserved as rawRule.Localsmetadata when available, and rule exception metadata is preserved as rawRule.ExceptionMetadataforthrows,catch, andfinallywhen available. Descriptors can expose that preserved metadata throughRawLocals,Locals,Returns, andExceptions; they do not parse C# or ANTLR target-language declarations semantically, and they do not invent locals, return, or exception metadata that the runtime model does not expose. Return descriptor names are extracted lexically (e.g.valueforint value) using the same top-level comma split strategy as locals; raw declarations are preserved verbatim.ParserRuleCallResultis available as an immutable snapshot of return values from a successfully completed child rule invocation; it is captured byStackParserRuleInvocationFrameManager.PrepareCallResultForSnapshot(called byParserEnginebefore the post-rule execution-state snapshot) and stored on the parent frame'sLastCompletedChildCall; the managed execution-state snapshot includes the call result via callback so rollback-safe memoization and alternative backtracking work correctly; failed alternatives clear stale call results on the parent frame and memoization hits restore the correct child result. Generated C# lifecycle hook bodies may explicitly call the frame-local helpers (GetRuleLocal,TryGetRuleLocal,SetRuleLocal,GetRuleLocalDescriptors), the frame-return helpers (GetRuleReturn,TryGetRuleReturn,SetRuleReturn,GetRuleReturnDescriptors), the frame-parameter helpers (GetRuleParameter,TryGetRuleParameter,GetRuleParameterDescriptors), and the parameter-seeding helpers (SetNextRuleParameter,ClearNextRuleParameters) inParseWithEmbeddedCode(...); parameter names are extracted lexically (e.g.valueforint value); frames are not auto-populated with parameter values by default; rule call arguments are not evaluated as arbitrary expressions, while the explicitly installed positional literal policy can seed its limited supported values; bare$paramreads are available only through an optional generated C# embedded-code transformer for current-rule parameters;SetNextRuleParameterseeds an untyped value for the next invocation of the named child rule — seeds are consumed byStackParserRuleInvocationFrameManager.Enter, copied into the matching child frame, and are rollback-safe (included in managed execution-state snapshots so failed alternatives do not leak seeds);callee[expr]is not evaluated and generated parser signatures are unchanged; inline-action overloads ofSetNextRuleParameterandClearNextRuleParametersacceptingParserActionExecutionContextare also available inParseWithEmbeddedCode(...), routing through the instance_frameManagerfield on the generated execution context with identical rollback-safe semantics; for locals: existing entries are not overwritten, array-looking declarations remainnull, and no typed default values, typed local members, or implicit action variables are generated; for returns: return entries are not auto-allocated, returns are not propagated to caller frames, no typed return fields/properties are generated, and only declared current-rule bare$returnNameconveniences are available through the optional generated C# embedded-code transformer;$rule.value,$child.value, and labeled$c.valuereturn access remain unsupported.throwsdoes not change parser exception behavior, catch/finally blocks remain non-executable, no typed ANTLR-compatible rule invocation semantics are implemented, no$rule.valueor argument passing is added, and generatedParse(...)remains conservative.ParserExecutionContextCopier<TContext>provides a reusable runtime copy primitive for parser execution contexts. Fields marked with[ParserExecutionStateIgnored]are excluded from copying and hashing; this attribute is applied to infrastructure fields on generated contexts (such as_frameManager) that must not participate in execution-state snapshots. Generated execution contexts expose this primitive throughFork()andCopyFrom(...). Generated runtime policies also expose anIParserExecutionStateManagerthroughParserRuntimeFeaturePolicy.ExecutionStateManager; the generated manager captures withFork()and restores withCopyFrom(...). The default runtime policy usesNullParserExecutionStateManager.Instance. ParserEngine now captures and restores managed parser execution state around parser backtracking attempt boundaries, and memoized rule results carry post-rule execution-state snapshots that are restored on memoization hits without replaying actions. This does not provide complete ANTLR transactional semantics.
API compatibility note: ParserRuntimeFeaturePolicy.ExecutionStateManager and ParserRuntimeFeaturePolicy.RuleInvocationFrameManager are required. Prefer ParserRuntimeFeaturePolicy.Default with { ... } when customizing a policy so the no-op default manager is preserved automatically. Direct new ParserRuntimeFeaturePolicy { ... } initializers must now set ExecutionStateManager = NullParserExecutionStateManager.Instance and RuleInvocationFrameManager = NullParserRuleInvocationFrameManager.Instance to keep conservative no-op behavior. This requirement enables managed parser execution-state rollback for parser backtracking attempt boundaries when a stateful manager is supplied, but it does not enable action buffering, replay, lifecycle hooks, external side-effect rollback, lexer embedded-code execution, typed rule invocation semantics, generated parser method signature changes, return propagation, local allocation, rule option semantics, or exception metadata execution.
var policy = ParserRuntimeFeaturePolicy.Default with
{
SemanticPredicateEvaluator = customEvaluator
};
var directPolicy = new ParserRuntimeFeaturePolicy
{
SemanticPredicateEvaluator = new DefaultSemanticPredicateEvaluator(),
ParserActionExecutor = new DefaultParserActionExecutor(),
ExecutionStateManager = NullParserExecutionStateManager.Instance,
RuleInvocationFrameManager = NullParserRuleInvocationFrameManager.Instance
};
Utils.Parser.Diagnostics
Responsible for shared diagnostic descriptors across runtime and generator/tooling surfaces.
Future shared embedded-code diagnostics should live here when shared by runtime and generator paths.
The shared embedded-code diagnostic taxonomy is:
UP1024 EmbeddedCodeLanguageUnsupportedUP1025 EmbeddedCodeCompilerNotConfiguredUP1026 EmbeddedCodeCompilationFailed: currently used by expression-backed adapters for compile and delegate adaptation failures, plus compiled-action execution failures.UP1027 EmbeddedCodePreservedNotCompiledUP1028 EmbeddedCodeExecutionDisabled: reserved for explicit runtime policies that intentionally disable embedded-code execution. Current expression-backed adapters do not expose anEnabled = falsepolicy and therefore do not emit this diagnostic.
These diagnostics define capability boundaries. They do not imply that every diagnostic is emitted by current adapters.
Utils.Parser.Generators
Current responsibilities:
- Roslyn source generation (
netstandard2.0analyzer/generator project); - compile-time
.g4ingestion viaAdditionalFiles; - internal G4 parsing and C# emission;
- preservation of embedded code as raw model metadata strings;
- C#-only executable hooks for parser semantic predicates and inline parser actions in generated grammars;
- generated
ISemanticPredicateEvaluator,IParserActionExecutor, andParserRuntimeFeaturePolicywiring; - generated
ParseWithEmbeddedCode(...)helper for explicit opt-in execution while generatedParse(...)keeps the default conservative policy; - generated execution context classes that own instance hooks and injected unscoped
@members/@parser::membersC# members; - runtime-index-aware hook dispatch for tested parser hook positions: single-item alternatives, sequence positions, quantified content, negation predicate probes, same-source hooks in distinct alternatives, and direct-left-recursive tail views because generated helpers resolve the generated definition before parsing with the generated policy;
- Roslyn diagnostic reporting and C# compilation errors for invalid embedded C# in the source-generator path, including invalid injected members or member-name collisions;
- generator warning
UP1031 EmbeddedMembersInjectedByGeneratorfor unscoped@membersand@parser::membersinjected into the generated execution context; - generator warning
UP1029 EmbeddedCodeConstructNotExecutedByGeneratorfor visible constructs outside the bounded generated hooks or named-action injection points; supported simple lexer predicates/actions and@lexer::header/@lexer::members/@lexer::footerin combined or lexer grammars are generated-C# opt-in surfaces, while unknown actions/scopes and contextually invalid forms still diagnose. Parser@initand@afterhooks likewise do not produceUP1029.
Future responsibilities (source-generation path):
- additional source-generator C# hook shapes beyond parser predicate expressions, parser predicate blocks with
return, and inline parser action statement bodies; - clear distinction between preserved raw metadata and executable generated hooks;
- broader lexer embedded-code shapes beyond the current bounded generated-C# opt-in hooks only after dedicated design work;
- deterministic semantics for parser actions inside negation probes only after dedicated design work and tests.
Utils.Parser.VisualStudio
Responsible for tooling and integration surfaces (diagnostic/grammar information presentation), not execution of embedded grammar code.
Utils.Parser.VisualStudio.Worker
Responsible for isolated tooling worker scenarios and future diagnostics surfacing, not arbitrary embedded-code execution without explicit future policy.
Utils.Expressions.*
Role:
- optional expression compiler providers;
- shared contract via
IExpressionCompiler; - examples:
Utils.Expressions.CSyntax,Utils.Expressions.VBSyntax.
Usage rules:
- injected explicitly by consumers/adapters;
- used through contracts;
- no direct dependency from
Utils.Parsercore.
11. Diagnostics strategy
Source-generator diagnostics include UP1029 EmbeddedCodeConstructNotExecutedByGenerator, a warning for visible unsupported embedded-code constructs in .g4 files. The diagnostic is emitted only for constructs that are not promoted to generated C# hooks or injected parser members; it must not be emitted for supported parser semantic predicates, supported inline parser actions, unscoped @members, or @parser::members, even when their C# is invalid. Invalid C# remains owned by Roslyn. Source-generator diagnostics also include UP1031 EmbeddedMembersInjectedByGenerator, a compatibility warning that states unscoped @members or @parser::members was injected into the generated execution context as C# source.
Diagnostics should continue to be defined in Utils.Parser.Diagnostics so they can be used by:
- runtime ingestion;
- source generator Roslyn reporting;
- Visual Studio/tooling display.
Candidate future diagnostic improvements include:
- embedded code language unsupported;
- embedded code compiler not configured;
- embedded code compilation failed;
- embedded code preserved but not compiled;
- embedded code execution disabled by policy.
12. Non-goals
This model explicitly excludes:
- runtime C# compilation in
Utils.Parsercore; - implicit embedded-code language inference;
- automatic execution of raw ANTLR target code;
- parser scheduler changes;
ParserEngineauthority transfer;- complete rollback/replay semantics;
- use of
IParserExecutionStateManager, generated execution-contextFork()/CopyFrom(...), orParserExecutionContextCopier<TContext>as speculative-execution authority beyond managed parser execution-state rollback at parser backtracking attempt boundaries; - hidden semantic runtime state;
- parse-tree shape changes;
- direct
Utils.Parserdependency onUtils.Expressions.CSyntaxorUtils.Expressions.VBSyntax; - runtime behavior changes from the current preparation-boundary contracts.
13. Future PR plan
Completed — Documentation/design realignment
- clarify the two execution paths and the prepare-before-parse target;
- document current expression-backed adapters as an intermediate runtime integration step;
- no behavior change.
Completed — Explicit preparation boundary
- introduce an internal minimal boundary that represents embedded-code source, preparation context, target path, preparation status, and path-specific artifacts;
- keep the boundary disconnected from automatic
ParserEngineactivation; - preserve
ParserEngineas an execution coordinator rather than a language compiler.
Runtime-inline prepared expression path
- preparation contracts, the expression-backed preparer, prepared registry, registry builder, registry-backed adapters, and prepared runtime policy builder are available;
- callers opt in before parsing by building a policy with a supplied
IExpressionCompiler; - prepared adapters execute artifacts during parsing without compiling source text during predicate/action invocation;
- automatic activation from
ParserEngineremains out of scope.
Source generator C# path
- initial explicit C# embedded-code hook support is implemented for parser semantic predicates and inline parser actions;
- generated hooks are private C# methods compiled by Roslyn with the consuming project;
- generated dispatchers implement
ISemanticPredicateEvaluatorandIParserActionExecutor, are bound to one caller-supplied generated execution-context instance, and are installed throughCreateRuntimePolicy(executionContext, basePolicy); reusing that policy reuses the same context state; - generated
ParseWithEmbeddedCode(string)opts into those hooks with a fresh execution context for that parse,ParseWithEmbeddedCode(string, context)intentionally reuses the supplied context, and generatedParse(...)keeps default conservative runtime behavior; - supported predicate bodies include C# boolean expressions and block-bodied predicate statements with
return, usingcontext,ruleName,inputPosition,alternativeIndex,elementIndex, andpredicateCode; - supported action bodies include single-statement, multi-statement, and multi-line C# statement bodies using
context,ruleName,inputPosition,alternativeIndex,elementIndex, andactionCode, including local variables and calls to user members in another partial class declaration; - invalid embedded C# is intentionally reported by Roslyn as a compilation error; predicate blocks without a valid
boolreturn and actions with invalid C# are not converted into custom parser diagnostics; - unscoped
@membersand@parser::membersare injected into the generated execution context and produce generator warningUP1031; - visible unsupported embedded-code constructs produce generator warning
UP1029without changing behavior. Supported simple lexer actions/predicates and the documented lexer header/member/footer injection points are generated only for the bounded generated-C# opt-in path; unknown actions/scopes and contextually invalid forms remain non-executable. Parser semantic predicates, inline parser actions, and parser@init/@afterhooks are also generated as executable opt-in hooks; - future work may add broader C# shape support without changing default parsing.
Rule-call argument syntax callee[...]
callee[...]argument clauses are metadata-only: parsed and preserved asRuleRef.RawArguments(raw text, outer brackets excluded);- reported with
UP1037 RuleCallArgumentsPreservedAsMetadata; - at runtime, the raw argument text is also carried into
ParserRuleCallResult.RawArgumentson the parent frame's last completed child call result (viaStackParserRuleInvocationFrameManager.AnnotateLastChildCallRawArguments, called byParserEngine.TryParseRuleRefafter each successful child rule parse, whether fresh or memoized); - generated C# opt-in code can inspect it explicitly via
GetLastRuleCallResult(context)?.RawArgumentsorTryGetLastRuleCallRawArguments(context, ruleName, out rawArgs)in parent lifecycle and inline-action hooks; SetNextRuleParameterFromRawArguments(context, ruleName, parameterName, rawArguments, map)allows explicit user-controlled mapping of raw text into a future child seed via a caller-supplied delegate; requires an explicit mapper; nullrawArgumentsreturnsfalse; mapper exceptions propagate; seeds the next invocation of the named rule;SplitRawArgumentsTopLevel(rawArguments)andTrySplitLastRuleCallRawArguments(context, ruleName, out args)split raw argument text into top-level slices at commas while respecting nested(),[],{}, and quoted strings; syntactic only — no argument is evaluated, no parameter is bound, no seed is set automatically; backed byUtils.Parser.Runtime.ParserRawArgumentSplitter.SplitTopLevel;SetNextRuleParametersFromRawArguments(context, ruleName, rawArgs, params mappings)maps multiple positional slices to named child seeds in a single call usingParserRawArgumentParameterMappingentries (ParameterName, Index, Map); validates all mappings before applying any seed (out-of-range index returns false with no partial seeding); duplicate parameter names: last mapping wins; mapper exceptions propagate; null mapped values allowed; both lifecycle and inline-action overloads available;ParserRawNamedArgumentSplitter.SplitNamedTopLevelparses named key–value forms (value: 42,value = 42) from top-level slices; throwsFormatExceptionon missing separator or empty key; duplicate keys: last wins;TrySplitLastRuleCallNamedRawArgumentswraps this in a Try… helper;SetNextRuleParametersFromNamedRawArguments(context, ruleName, named, params mappings)maps named entries to seeds usingParserRawNamedArgumentParameterMapping(ParameterName, ArgumentName, Map); validates all mappings before seeding (missing ArgumentName returns false, no partial seeding); lifecycle and inline-action overloads available; syntactic only — no evaluation;- call-site metadata is rollback-safe (execution-state snapshots include
_lastChildCallResult.RawArguments) and memoization-safe (annotation always reflects the current call site, not the cached snapshot); - raw argument text is not evaluated, not parsed as C# expressions, and not bound to child rule parameters;
PendingChildSeeds,InvocationFrame.Parameters, and frame behavior are unchanged;- generated
Parse(...)and generated rule method signatures are unchanged; - use
SetNextRuleParameter(...)for explicit parameter seeding from lifecycle hook code; Bare$param` current-rule reads are supported only in generated embedded C#; parameter writes/chains and call-argument evaluation remain unsupported.
Rule-reference label metadata (x=child, xs+=child)
Rule-reference labels are preserved as passive metadata end-to-end:
x=childandxs+=childare parsed by both the ANTLR converter and the source-generator G4 parser;- label metadata is stored on
RuleRef.Label(RuleLabelrecord: label name, rule name, additive flag), withRuleRef.LabelNameandRuleRef.LabelKind(ParserRuleReferenceLabelKind:None,Assignment,List) as computed properties; GrammarEmitteremitsLabel: new RuleLabel(...)in generatedBuildDefinition()when a label is present;- at runtime,
ParserEngine.TryParseRuleRefcallsIParserRuleInvocationFrameManager.AnnotateLastChildCallLabel(labelName, labelKind)after each successful child rule completion so the call-site label is visible inParserRuleCallResult.LabelNameandParserRuleCallResult.LabelKindon the parent frame; - labels compose with
callee[...]raw arguments: both metadata fields are set independently and can coexist; - label metadata is rollback-safe (included in
ParserRuleCallResult.GetParserExecutionStateHash(), tracked alongside raw arguments in execution-state snapshots) and memoization-safe (annotation always reflects the current call site, not the cached snapshot); - generated C# opt-in code can inspect label metadata explicitly via
GetLastRuleCallResult(context)?.LabelNameandGetLastRuleCallResult(context)?.LabelKindin parent lifecycle and inline-action hooks; - labels on non-rule-reference elements (literals, character classes, groups) are recognized and ignored with diagnostic
UP1022 LabelOnNonRuleReferenceIgnored; - labels are metadata-only: no
$x,$x.value,$xs, implicit label variables, typed label fields/properties, automatic parse-node storage, automatic return access, automatic binding, automatic argument evaluation, automatic parameter seeding, or generated parser method signatures are added; - conservative
Parse(...)remains conservative; lifecycle hooks do not execute and label metadata is not exposed to code.
Future PR — Lexer actions/predicates
- separate design and implementation;
- account for tokenization and lexer state impact.
Runtime policy architecture rule:
- Runtime policy contexts are immutable input snapshots.
- Runtime policy outcomes carry the decision and optional diagnostic metadata.
- Future outcomes may carry deterministic context transitions, but handlers must not mutate parser state directly.
- ParserEngine remains responsible for applying effects and emitting diagnostics.
Parser action execution now uses structured ParserActionExecutionOutcome, aligned with semantic predicate outcomes. Executors return a status plus optional diagnostic metadata. Inline parser actions still do not influence parse acceptance: Executed and NotExecuted both continue parsing.
Shared runtime indexing metadata
Parser embedded-code discovery now has a shared metadata model in Utils.Parser.EmbeddedCode. EmbeddedCodeRuntimeDiscovery walks a ParserDefinition and emits EmbeddedCodeRuntimeEntry values with the raw source, EmbeddedCodeKind, owning rule name, runtime-compatible alternative and element indexes, a runtime key for executable entries, and an explicit EmbeddedCodeUnsupportedReason for skipped entries. The metadata mirrors the existing parser runtime indexing rules for priority-ordered alternatives, single-item alternatives, sequences, quantifier inner parsing, negation probes, and direct-left-recursive base/tail alternatives. It is metadata only: it does not compile source, generate C#, execute actions, or change ParserEngine behavior.
The expression-backed prepared registry consumes this shared discovery result before invoking its preparer. On that runtime-inline surface, grammar actions, lexer actions/predicates, lexer lifecycle/member actions, and non-inline parser actions remain non-executable and carry explicit skip reasons. Parser @init / @after and the bounded lexer hooks are supported only by the source-generator C# opt-in path. The source generator reports UP1029 only for visible constructs outside its supported generated hooks/injection points; the warning never grants execution. Invalid C# in a source-generator-supported hook remains a Roslyn compilation error rather than a custom parser diagnostic.
Explicit parser rule-call execution policy
ParserRuntimeFeaturePolicy.RuleCallExecutionPolicy is an explicit opt-in extension point around parser rule references. ParserEngine creates a passive ParserRuleCallExecutionContext, calls BeforeRuleCall(...), invokes the child ParseRule(...), annotates a successful ParserRuleCallResult with the current call site's raw arguments and label, and then calls AfterRuleCall(...). The after callback reports Succeeded and exposes the annotated CompletedCallResult when stack-aware invocation-frame tracking is active. Context metadata also includes the target rule name and descriptor, caller frame when available, raw argument text, label name/kind, positional top-level slices, and named top-level slices when syntactically valid.
The default NullParserRuleCallExecutionPolicy performs no work, so default parsing behavior is unchanged. This policy does not make callee[...] executable: there is no automatic expression evaluation, positional or named binding, parameter seed, generated parser signature, typed parameter/return variable, $param, $x, $x.value, or $rule.value. Generated C# opt-in code preserves a custom rule-call policy supplied through the basePolicy passed to CreateRuntimePolicy(...); the generated three-argument ParseWithEmbeddedCode(input, executionContext, basePolicy) overload provides the same explicit path while existing overloads remain unchanged. Generated Parse(...) remains conservative.
Policy method calls are ordinary external callbacks. Their external side effects are not buffered, replayed, or automatically rolled back. Only mutations performed through separately documented rollback-aware parser state participate in parser rollback. Call-site raw arguments and labels remain rollback- and memoization-safe because successful child results are annotated after every child call, including memoization hits, before AfterRuleCall(...) observes them. Policy implementations must not retain mutable invocation frames beyond the callback.
Concrete positional literal rule-call policy
PositionalLiteralRuleCallExecutionPolicy provides a narrow, caller-installed bridge from parser call-site metadata to pending child parameters. BeforeRuleCall(...) reads the target descriptor and syntactically split positional arguments, requires exact arity and usable unique parameter names, parses every supported literal into temporary storage, and only then calls ParserRuleCallExecutionContext.TrySetParameterSeeds(...) once with the complete binding set. The context API fixes the target to the current RuleName and delegates to the frame manager's all-or-none batch contract; it does not expose the frame manager or directly mutate a child frame. The stack manager applies the batch through one immutable pending-seed-store replacement. The method returns false when the configured invocation-frame manager cannot retain the complete batch (including the conservative no-op manager), so ignore mode leaves the call unbound and throw mode reports a binding failure instead of claiming success. Policy seeds overwrite same-parameter pending seeds while unrelated seeds remain intact. AfterRuleCall(...) is intentionally a no-op.
Supported values are limited to null, lowercase Booleans, signed decimal int/long, finite invariant decimal/exponent double, double-quoted strings, and single-quoted characters with \\, \", \', \n, \r, \t, and \0. No declared-type validation, named binding, expression execution, reflection-based identifier resolution, or Roslyn compilation occurs. The default policy remains no-op and generated Parse(...) remains conservative. Generated callers opt in by passing a basePolicy containing this policy to ParseWithEmbeddedCode(...).
Named literal rule-call policy
NamedLiteralRuleCallExecutionPolicy complements, but does not compose automatically with, the positional policy. It is explicitly supplied through basePolicy; defaults and generated Parse(...) remain metadata-only. It reads only ParserRuleCallExecutionContext.NamedRawArguments, whose existing splitter accepts top-level name: literal and name = literal, ignores separators inside nesting or quotes, and resolves duplicate raw names with last-wins semantics. Exact ordinal parameter-name coverage is required, while call-site order may differ from declaration order. Missing, extra, case-mismatched, blank, or duplicate declared names, mixed syntax, optional/default parameters, and partial binding are rejected conservatively.
Each raw value is parsed solely by ParserSimpleLiteralParser; declared C# types are passive metadata and do not drive validation or conversion. Only the documented simple literals are supported, not arbitrary C# expressions. All validation precedes one atomic pending-child seed batch, so rollback and state-aware memoization use the same managed guarantees as positional binding. The policy does not bind returns or labels and does not add $param, $x, $x.value, $rule.value, or lexer execution.
Typed simple-literal call policies
Typed rule-call binding is a separate explicit execution strategy, not a change to grammar parsing or the conservative default. TypedPositionalLiteralRuleCallExecutionPolicy and TypedNamedLiteralRuleCallExecutionPolicy consume only values accepted by ParserSimpleLiteralParser, inspect the descriptor's conservatively preserved RawType, and delegate conversion to ParserLiteralTypeConverter. Existing positional and named policies remain untyped. Parameter descriptors also preserve a conservatively split RawDefaultValue as passive metadata. Only typed policies consume it: positional calls may omit trailing parameters, while named calls may omit any parameter whose declaration supplies a usable default. Explicit values win and prevent unused invalid defaults from being evaluated.
Every explicit value and every required default is parsed and converted before exactly one TrySetParameterSeeds(...) call. The resulting complete effective state follows the existing managed rollback and memoization paths. No general C# default-expression execution, parameter reference, constant/enum resolution, return/local/label binding, $param support, or lexer execution is introduced; generated Parse(...) remains conservative.
The converter recognizes only the exact aliases bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal, char, string, object and their canonical System.* equivalents. One nullable suffix is supported; string? and object? do not enforce nullable-reference annotations. It performs checked integral conversion, exact-preserving integral-to-floating and double-to-float conversion, exact integral-to-decimal conversion, and the limited char/string conversions. It never parses strings into numbers or Booleans and never converts floating-point values to integral values.
The execution order is deliberately transactional: validate call syntax and exact coverage, validate every descriptor name and supported type, parse every literal, convert every value, build the complete dictionary, then invoke the managed batch writer once. Unsupported types or values produce no mutation in IgnoreCall mode or a deterministic ParserRuleCallBindingException in Throw mode. There is no arbitrary type resolution, assembly loading, Roslyn conversion, expression evaluation, generated typed variable/signature support, $param/$x/$x.value/$rule.value, return or label binding, or lexer support.
Explicit labeled child-call result access
A successful child invocation is finalized in this order: child execution, child @after, immutable return capture, current-site raw-argument annotation, current-site label annotation, parent LastCompletedChildCall update, parent labeled-store binding, then AfterRuleCall(...). Assignment labels overwrite only after success; list labels append only after success. The immutable parent-frame store is core managed call behavior, not another argument policy, while access remains explicit through the generated generic helpers.
Lifecycle and inline-action helper overloads support assignment result lookup, ordered list result lookup, assignment return lookup, and ordered list return projection. Return names use ordinal matching with no conversion. A present-null return is included and reported present; an absent return is not. List return projection skips absent keys and preserves the order of calls containing the key. There is no fallback to LastCompletedChildCall, rule-name lookup, generated label-specific member, automatic return propagation, or lexer equivalent. Conservative Parse(...) does not opt into embedded-code access.
Optional C# ANTLR-style convenience transformer
ANTLR-style convenience syntax is not core parser behavior. $x.value, $xs.value, $rule.value, $param, and $local are preserved unchanged by the default NoOpParserEmbeddedCodeTransformer. If that unchanged text is not valid C#, normal generated compilation fails.
A C#-specific optional compatibility transformer may rewrite narrow read-only forms to generated helper calls before generated parser action and lifecycle bodies are emitted. Such a transformer may map assignment-label, list-label, current-rule return, current-rule parameter, and current-rule local reads to helpers such as GetRequiredLabeledRuleCallReturn(...), GetLabeledRuleCallReturns(...), GetRequiredRuleReturn(...), GetRequiredRuleParameter<T>(...), and GetRequiredRuleLocal<T>(...). It remains target-language-specific convenience code, not parser/generator core semantics.
Direct helper APIs are the recommended no-transformer style. Writes remain explicit through helpers such as SetRuleLocal(...) and SetRuleReturn(...). Lexer attributes, general ANTLR attributes, implicit variables, typed generated fields/properties, and full ANTLR embedded action semantics remain unsupported. Conservative Parse(...) is unchanged.
Pluggable embedded-code transformation boundary
Embedded parser code is preserved as target-language code by default. The default NoOpParserEmbeddedCodeTransformer returns the grammar text unchanged, so parser @header, parser @footer, rule @init, rule @after, inline parser actions, and semantic predicates are emitted or prepared without implicit $... rewriting.
Target-language conveniences such as ANTLR-style $param, $local, $x.value, $xs.value, or $rule.value are not intrinsic parser behavior. They belong behind an explicit IParserEmbeddedCodeTransformer. Generated C# emission uses the transformer result before writing hook bodies, and dynamic expression preparation transforms code before invoking the existing compiler/preparer selected by the caller. The parser runtime does not gain a second compiler abstraction.
Transformers receive the raw code, embedded-code location, grammar/rule names, declaration metadata, and visible rule-reference labels. Existing generated helper APIs remain available for users who write plain C# directly. Conservative Parse(...) remains unchanged and does not execute embedded code.
Optional C# ANTLR-style local writes
Local $... writes are not core parser syntax. By default, NoOpParserEmbeddedCodeTransformer preserves embedded code such as $total = 1; unchanged, and direct generated C# helper APIs remain the preferred no-transformer style. The optional CSharpAntlrStyleParserEmbeddedCodeTransformer may rewrite current-rule local writes in generated C# opt-in paths only: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, and standalone $local++, ++$local, $local--, --$local. The rewrite uses the local declaration raw type as T, stores through existing parser-managed frame local state, and relies on existing rollback behavior. Compound assignment is emitted as getter/operator/setter and does not emulate C# compound-assignment special conversions. Parameters, returns, labels, list-label projections, lexer attributes, ref/out, writes in semantic predicates, and increment/decrement expression values remain unsupported. Richer transformations must stay behind IParserEmbeddedCodeTransformer rather than moving into parser or generator core.
Optional C# transformer current-rule return writes
The optional C# ANTLR-style transformer supports a narrow current-rule return write convenience syntax in rule @after code and inline parser actions. The default no-op transformer preserves bare $returnName = ... text unchanged. Runtime execution is still limited to parser-managed frame return state and labeled child-call result snapshots; this is not full ANTLR action compatibility.
Supported forms use the bare return attribute declared by the current rule in @after and inline parser actions, for example $value = 42;, compound assignments such as $value += 1;, and standalone increment/decrement statements. The transformer rewrites those forms to explicit typed helper calls such as SetRequiredRuleReturn<T>(context, "value", ...) and GetRequiredRuleReturn<T>(context, "value"). Direct helper APIs remain the preferred non-transformer style. Writes update parser-managed current-rule frame return state, are included in rollback snapshots, and are captured into ParserRuleCallResult only for successful child calls, but ANTLR-style $child.value and $label.value conveniences remain unsupported.
Parameters writes, child return access such as $child.value, lexer attributes, ref/out, semantic predicates, @init, and dotted current-rule return attributes such as $rule.value and $rule.value = ... remain unsupported. Assignment-label $c.value reads and list-label $xs.value reads are generated-C# opt-in sugar only in inline parser actions and @after; $xs.value is read-only, applies only to visible xs+=child parser-rule list labels, and rewrites to GetLabeledRuleCallReturns(context, "xs", "value"). Use bare $returnName = ... only for declared current-rule return attributes when opting into the C# transformer.
Parser and lexer grammar-level named-action support is source-generator C# only. In parser or combined grammars, unscoped @header / @members / @footer are treated as parser compatibility blocks, and scoped @parser::header / @parser::members / @parser::footer are equivalent parser compatibility blocks. They emit parser header code, generated execution-context members, or deterministic trailing parser source in grammar source order, and they still produce compatibility warnings (UP1035, UP1031, or UP1036) because invalid C# remains a Roslyn responsibility. Scoped lexer named actions (@lexer::header, @lexer::members, @lexer::footer) mirror the same limited injection model in combined or lexer grammars only with dedicated lexer markers; parser-only grammars keep them unsupported because no lexer is generated; lexer members are emitted into the existing generated execution context and do not create a separate ANTLR lexer runtime type. Parser named actions in lexer grammars are invalid for this generator, and unscoped @header, @members, and @footer are not parser compatibility blocks in lexer grammars. Unsupported named actions, unknown lexer/parser action names such as @lexer::custom or @parser::custom, and unknown scopes such as @tree::members produce deterministic UP1029 diagnostics and are not silently injected. The default/no-op transformer preserves named-action content unchanged; optional transformer behavior remains opt-in, and $... current-rule attribute rewriting is intentionally limited to parser actions/lifecycle code, not parser or lexer header/member/footer content. Parser and lexer members can be called from generated inline parser actions and supported @init/@after lifecycle hooks, but runtime-inline lexer predicates remain unsupported; simple lexer inline actions and simple lexer predicates are supported only in the explicit generated-C# opt-in path.
Lexer grammar-level named actions
The source-generator C# path supports @lexer::header, @lexer::members, and @lexer::footer as limited grammar-level named-action injection points mirroring @parser::* in combined or lexer grammars only; parser-only grammars keep scoped lexer actions unsupported because no lexer is generated. The generator emits these blocks verbatim with lexer-specific markers and deterministic ordering. @lexer::members is injected into the existing generated execution context because this repository does not yet generate a separate ANTLR lexer runtime type. This is a compatibility bridge only: lexer $... rewriting is limited to generated-C# opt-in inline action reads and complete ANTLR lexer target-language semantics remain unsupported; simple lexer inline actions and simple lexer predicates are supported only in the explicit generated-C# opt-in path.
Lexer inline actions and predicates: simple source-generator C# lexer inline actions and simple lexer predicates are now supported only through the explicit opt-in generated path.
Parse(...)remains conservative. Predicates run during lexer matching and reject only the current matching path; actions run after token acceptance and are not executed when an earlier predicate rejects that path. The tested boundary covers hooks reached through lexer rule references and fragments, hooks in simple quantifiers, duplicate source text at distinct hook positions, and already-supported lexer commands such asskip.AlternativeIndexandElementIndexidentify the source hook location, not a quantified runtime iteration. Lexer$...rewriting is limited to generated-C# opt-in inline action reads ($text,$type,$channel,$mode,$line,$pos) and simple$type = .../$channel = .../$mode = ...statement writes throughLexerActionExecutionResult; lexer predicate attributes, runtime-inline lexer execution, a separate runtime lexer, generalized action buffering/replay, general lexer rollback, and external side-effect rollback remain unsupported.
Generated-C# lexer attribute rewrite boundary
The optional C# ANTLR-style transformer owns the limited lexer $... rewrite for generated-C# opt-in lexer inline actions through EmbeddedLexerAttributeRewriter. The supported lexer action reads are $text, $type, $channel, $mode, $line, and $pos; they are rewritten before hook emission to generated execution-context helpers that read passive LexerActionExecutionContext metadata.
$textis rewritten toGetRequiredLexerText(context)and readsLexerActionExecutionContext.Text. It exposes the accepted token/chunk text available to the accepted lexer action context, not an ANTLR-guaranteed local slice. Tests lock the current behavior: simple and quantified token actions read the accepted token text; actions before or after fragments, inside fragments, or inside referenced lexer rules read the context-level accepted outer token text; alternatives read the selected alternative text;skip,type(...), andchannel(...)actions read text before the command suppresses, retags, or hides the token; andmoreactions read the current accepted chunk before final accumulation, so a later token action reads its own chunk text rather than the accumulated text.$typeis rewritten toGetRequiredLexerType(context)and readsLexerActionExecutionContext.TokenType. The value is the passive token type available after the token/chunk is accepted and before lexer commands run, sotype(...)does not change the value read by the current token action. Fragment and lexer-rule-reference actions read the available accepted outer token context metadata; current tests lock this as the outer accepted token type.$channelis rewritten toGetRequiredLexerChannel(context)and readsLexerActionExecutionContext.Channel. The value is available after acceptance and before commands run, sochannel(...)does not change the value read by the current token action.$modeis rewritten toGetRequiredLexerMode(context)and readsLexerActionExecutionContext.Mode. The value is the lexer mode that accepted the current token/chunk.pushMode(...),mode(...), andpopModeare applied after the current token/chunk action, so an action before a mode switch reads the previous mode while an action accepted in a secondary mode reads that secondary mode.$lineis rewritten toGetRequiredLexerLine(context)and readsLexerActionExecutionContext.Line.$posis rewritten toGetRequiredLexerPos(context)and readsLexerActionExecutionContext.Column. The line and position values are sourced fromSourceSpan.LineandSourceSpan.Columnat the start of the accepted token or chunk;$posis therefore a 1-based column in this runtime, not complete ANTLRcharPositionInLinecompatibility.
The supported write subset is deliberately bounded. Generated hooks do not mutate Token directly. $type = ..., $channel = ..., and $mode = ... are rewritten to helpers that set LexerActionExecutionResult.TokenType, LexerActionExecutionResult.Channel, or LexerActionExecutionResult.Mode; LexerEngine applies those requested token and mode mutations once before lexer commands run. Commands remain authoritative.
The runtime flow is: EmbeddedLexerAttributeRewriter rewrites simple $type = IDENTIFIER;, $type = "IDENTIFIER";, $channel = IDENTIFIER;, $channel = "IDENTIFIER";, $mode = IDENTIFIER;, and $mode = "IDENTIFIER"; statements to SetLexerType(result, "..."), SetLexerChannel(result, "..."), or SetLexerMode(result, "..."); the generated lexer hook receives both LexerActionExecutionContext context and LexerActionExecutionResult result; the hook records requested token type, channel, or mode changes in the result; LexerEngine applies result.TokenType, result.Channel, and result.Mode; and accepted lexer commands then run. The ordering is intentionally write-before-command: type(...), channel(...), and mode(...) override previous action writes, skip remains authoritative over emission, and more preserves the existing intermediate/final chunking behavior. Reads such as $type, $channel, and $mode stay tied to the passive LexerActionExecutionContext and are not updated by writes earlier in the same action body.
Regression coverage stabilizes last-write-wins, $type plus $channel/$mode in the same action, multiple accepted actions in one token, contradictory writes, fragments, lexer rule references, quantifiers, rejected alternatives, more intermediate and final chunks, command override, whitespace/newline variations, string and escaped-string values, deterministic diagnostics for complex unsupported forms, and direct Token.RuleName/Token.Channel observation and following lexer mode behavior.
Lexer attribute writes such as $text = ..., $line = ..., $pos = ..., compound/coalescing/increment writes including $mode += ... and $mode++, dotted/chained writes, expression writes, embedded assignment expressions, ref/out, and lexer $... attributes in predicates are unsupported and produce deterministic transformer diagnostics. The no-op transformer preserves lexer $... source unchanged, conservative Parse(...) does not execute lexer hooks, runtime-inline lexer execution remains unsupported, no separate runtime lexer exists, no general lexer action buffering/replay or rollback is added, and no C# target-language rewriting exists in LexerEngine or ParserRuntimeFeaturePolicy. $index, $int, $token, $start, $stop, $ctx, and $input are explicitly deferred and must not be inferred from this bounded metadata/result support.
Generated-C# explicit simple positional rule-call binding
Generated parsers can explicitly install a generated-C#-only rule-call policy for ParseWithEmbeddedCode(...) when generation enables simple positional rule-argument binding. When a parser rule call supplies raw positional arguments, the generated policy first requires the raw positional argument count to exactly match the declared target-rule parameter count, including zero-parameter target rules; an explicit empty argument list such as child[] is therefore valid only when the target declares zero parameters. This generated-C# automatic boundary is stricter than the reusable typed runtime policy: declared parameter defaults are not consumed to satisfy omitted generated-C# call-site arguments. After exact arity passes, the generated policy converts supported simple literals and submits one atomic managed seed batch to the existing invocation-frame parameter store. The conservative generated Parse(...) path remains unchanged and does not execute this binding path.
Supported automatic generated-C# argument forms are intentionally narrow: exact-arity simple positional literals that the typed literal binding policy can convert to the declared parameter type, including decimal integer literals for int parameters. Named arguments and arbitrary C# expressions remain unsupported and are rejected deterministically in the generated-C# explicit binding path before child lifecycle hooks can observe partially seeded state. Full ANTLR-compatible generated rule signatures such as child(int value) are still not emitted; generated hooks should continue to read parameters through frame helpers such as GetRequiredRuleParameter<T>(context, "name"), and the optional C# ANTLR-style transformer may rewrite $name to those helpers. Explicit runtime policies such as TypedPositionalLiteralRuleCallExecutionPolicy may still support simple typed defaults separately when callers install them directly.
The implementation uses existing parser-managed pending seeds, invocation frames, execution-state snapshots, rollback, and memoization boundaries. No target-language expression evaluator was added to ParserEngine.
Generated-C# returns/labels boundary and named-action strategy
The rule-return and labeled rule-call boundary follows the existing parser named-action architecture rather than a parallel implementation path. Classification of grammar-level named actions is centralized in EmbeddedMembersSupport: @members and @parser::members are parser compatibility blocks injected into the generated execution context, @header and @parser::header are injected near the top of generated C# source, and @footer and @parser::footer are injected as trailing generated C# source. Unsupported parser-scoped actions such as @parser::init and parser named actions inside lexer grammars remain deterministic diagnostics and are not generated-source injection points.
Parser embedded code must continue to pass through IParserEmbeddedCodeTransformer via TransformEmbeddedCode(...). The default path preserves target-language code, and generated-C# embedded-code paths remain opt-in. Metadata is not execution authority: rule-return declarations may be present in grammar metadata, and labeled rule-call storage may be present in parser-managed frame state, but metadata/storage alone does not imply automatic runtime support, ANTLR-compatible label access, public typed parser contexts, $label.ctx, $ctx, or public ANTLR-style rule methods. Conservative Parse(...) remains unchanged, and ParserEngine remains target-language-neutral.
Future simple generated-C# return assignment/access should reuse generated execution-context helpers and optional transformer rewriting. Future labeled rule-call return access should build on existing labeled result storage where available. Any $... syntax support must be implemented through the parser embedded-code transformer, not the runtime parser core. No full ANTLR parser context model is promised by the current generated-C# compatibility bridge.
Explicit labeled child-return helper access
Generated C# code can read assignment-labeled child rule returns through narrow $c.value sugar in inline parser actions and @after, or through explicit helper APIs. A child rule may assign its own declared return with bare $value in supported generated-C# parser action locations when the C# ANTLR-style transformer is enabled, but a parent rule must read c=child and xs+=child returns through helpers: GetRequiredLabeledRuleCallReturn, TryGetLabeledRuleCallReturn, TryGetLabeledRuleCallResult, GetLabeledRuleCallResults, and GetLabeledRuleCallReturns. These helpers project existing ParserRuleCallResult snapshots from the invocation-frame labeled result store; no new return store or target-language behavior is added to ParserEngine.
The helper semantics are intentionally explicit: assignment labels read the successful call bound to the label; list labels read successful calls in order; absent list labels return empty lists; present-null return entries are returned as null; missing labels or missing return names fail through deterministic parser attribute access exceptions in required helpers. Managed rollback prevents failed alternatives from leaking label returns, and state-aware memoization preserves child returns without confusing the current call-site label. $xs.value, $child.value, $rule.value, $c.ctx, $ctx, typed parser contexts, and public ANTLR-style parser rule methods remain unsupported; $c.value/$x.value are supported only for assignment-label child return reads in inline parser actions and @after. Conservative Parse(...) still does not execute embedded code.
Generated-C# parser return convenience boundary
Supported generated-C# opt-in convenience forms are deliberately narrow:
- bare
$valuereads/writes a declared return of the current rule; $c.value/$x.valueread a declared child return through an assignment label such asc=child;$xs.valuereads a list-label projection throughxs+=childand returns the generated helper list.
The following remain unsupported and must produce deterministic transformer diagnostics rather than new runtime syntax: $child.value, $rule.value, $ctx, $c.ctx, $xs.ctx, bare $c / $xs label objects, writes to $c.value or $xs.value, label-return reads in @init, label-return reads in semantic predicates, token attributes such as $t.text, lexer attributes, typed parser contexts, public ANTLR-style parser rule methods, and general ANTLR attribute compatibility.
These forms are optional IParserEmbeddedCodeTransformer rewrites for generated C# only. The default/no-op transformer leaves $... text unchanged, conservative Parse(...) remains unchanged, and ParserEngine remains target-language-neutral. Parser-managed return and label state follows the existing rollback semantics; no rollback of external side effects is implied.
Generated C# injection boundary
Generated C# injection is centralized in CSharpEmbeddedCodeInjector after transformation. The injector is intentionally a writer only: it normalizes line endings, applies generated-source indentation, emits known markers, and writes TransformedEmbeddedCode into named-action regions or hook bodies without calling transformers, compiling C#, or changing parser rollback semantics.