Diagnostics & Error System
Gram includes a hierarchical diagnostic telemetry engine (gram.utilities.Diagnostics, gram.utilities.info) and a formal 5-dimension error catalog (gram.errors, gram.utilities.error).
Both ParseState (Parser) and ASTState (ASTAnalyzer) subclass Diagnostics, and you can also subclass Diagnostics directly in your own compiler stages (such as semantic analyzers or code generators).
1. Using Diagnostics for Hierarchical Telemetry
Diagnostics (gram.utilities.Diagnostics) manages a tree of telemetry Node objects backed by StackInfo (gram.utilities.info.StackInfo).
Subclassing or Instantiating Diagnostics
from gram.utilities import Diagnostics
class CompilerState(Diagnostics):
def __init__(self) -> None:
super().__init__(
file="compiler-log", # Log file name under ./log/
name="COMPILER", # Root telemetry node title
description="Semantic pass", # Root node description
expose_nodes=True, # Render child nodes when stringifying
generate_log_file=False, # Set True to register ./log/compiler-log.log immediately
generate_on_error=False, # Set True to flush log file at process exit (atexit)
)
Emitting Notes and Building Telemetry Subtrees
- Logging methods on
Diagnostics:diag.debug(msg)$\rightarrow$ emits a'normal'note on the active node.diag.warn(msg)$\rightarrow$ emits a'warn'note.diag.advice(msg)$\rightarrow$ emits an'advice'note.diag.success(msg)$\rightarrow$ emits a'success'note.diag.error(msg)$\rightarrow$ emits an'error'note.
- Scoping child nodes (
nodeanddown):diag.node(name, description="", set_target=True)creates a child telemetryNodeunder the current target and pushes it ontoself._targetswhenset_target=True.diag.down()pops the active node fromself._targets, returning focus to its parent node.
from gram.utilities import Diagnostics
diag = Diagnostics(
file="semantic",
name="SEMANTIC",
description="Type checking pipeline",
expose_nodes=True,
)
diag.debug("Starting module resolution")
diag_node = diag.node("CheckFunction", "Validating fn 'main'", set_target=True)
try:
diag.advice("Return type inferred as 'int'")
diag.success("Function 'main' validated")
finally:
diag.down()
# Render the hierarchical telemetry tree as text (or print via diag.stack.console()):
print(diag.stack.stringify(ansi=False))
Persisting Telemetry Logs to Disk
- Calling
diag.stack.write()writes the formatted tree to./log/<file>.logviagram.utilities.info.Log. - Calling
gram.utilities.info.StackInfo.show_all()flushes all registeredStackInfoinstances. - For built-in Gram phases (
LexerState,Parser,ASTAnalyzer), telemetry verbosity and automatic log file creation are controlled bygram.config:config.LEXER_ADD_INFO,config.PARSER_ADD_INFO,config.AST_ADD_INFO(defaultFalse).config.INFO_GENERATE_LOGFILE(defaultFalse) andconfig.INFO_GENERATE_LOGFILE_ON_ERROR(defaultFalse).
2. Structured OSGDC Errors (gram.errors)
All formal exceptions in Gram inherit from gram.utilities.error.Error and carry a 5-character OSGDC code (CodeError) defined in gram.errors.
The 5 OSGDC Dimensions
Every CodeError formats as <Type>-<O><S><G><D><C> · <Name> (for example, S-12411 · Gram.Parser.UnexpectedToken), where S indicates a standard in-bounds code:
| Position | Dimension | Values in gram.errors |
|---|---|---|
O |
Origin | 0: Unknown (CODE_UNKNOWN_ORIGIN), 1: Native (CODE_NATIVE_ERROR), 2: Plugin (CODE_PLUGIN_ERROR) |
S |
Scope | 0: Unknown, 1: Lexer, 2: Parser, 3: Grammar/AST, 4: Semantic, 5: Compilation/DSL |
G |
Gravity | 0: Minor, 1: Implementation Error, 2: Obsolete, 3: Internal, 4: Fatal |
D |
Documentation | 0: Documented, 1: Documented & Traceable, 2: Not Documented |
C |
Condition | 0: Usable, 1: Requires Repair, 2: Should Repair |
Domain Error Classes
gram.errors provides 8 domain-specific subclasses of Error:
errors.LexerError(e.g.,LEXER_UNEXPECTED_CHARACTER,LEXER_UNCLOSED_STRING,LEXER_INDENTATION_MISMATCH)errors.ParserError(e.g.,PARSER_UNEXPECTED_TOKEN,PARSER_EARLY_EOF,PARSER_SYNTAX_ERROR)errors.GrammarError(e.g.,GRAMMAR_COMBINATOR_FAILED,GRAMMAR_RULE_NOT_FOUND,GRAMMAR_COMBINATOR_UNSUPPORTED)errors.ASTError(e.g.,AST_PROGRAM_RULE_NOT_FOUND,AST_PROGRAM_INVALID_COMBINATOR,AST_DECLARATOR_INVALID_ELEMENT)errors.SemanticError(e.g.,SEMANTIC_TYPE_MISMATCH,SEMANTIC_UNDEFINED_SYMBOL,SEMANTIC_DUPLICATE_SYMBOL,SEMANTIC_SCOPE_VIOLATION)errors.CompilationErrorerrors.PluginErrorerrors.SystemError
Creating and Raising a Structured Error
from gram import config, errors
err = errors.SemanticError(
"TypeMismatch",
errors.SEMANTIC_TYPE_MISMATCH,
"Cannot assign value of type 'str' to variable 'count' of type 'int' at line 4, column 10.",
"Convert the expression to 'int' or change the variable type declaration.",
)
# Format without raising:
print(err.stringify(ansi=False))
Output:
|---------------- ERROR -----------------|
Name: TypeMismatch, Type: SemanticError.
CAUTION: Cannot assign value of type 'str' to variable 'count' of type 'int' at line 4, column 10.
Convert the expression to 'int' or change the variable type declaration.
INF:
FILE: unknown
ORIGIN: ...
CODE: S-14411 · Gram.Semantic.TypeMismatch
|------------- END OF ERROR -------------|
Exit Policy (config.ERROR_EXIT_ON_ERROR) & StackError
When err.raise_error() (or parser.report_error(err) / analyzer.report_error(err) outside safe_mode()) is called:
- Unless
config.ERROR_HIDE_CONSOLEisTrue, the error box is printed to stdout. - If
config.ERROR_EXIT_ON_ERRORisTrue(the default in2.0.0b2), Gram callssys.exit(1). - If
config.ERROR_EXIT_ON_ERRORisFalse, Gram raiseserras a Python exception.
To track errors in the global StackError registry (gram.utilities.error.StackError) and dump them to ./log/error.log:
StackError.add(err)appends an error.StackError.all(),StackError.last(),StackError.count(),StackError.clear()inspect or reset the stack.StackError.write()writes all accumulated errors in plain text tolog/error.log.
3. Non-Invasive Error Interception (Diagnostics.hook_errors)
When building an IDE Language Server (LSPState), a multi-error linter, or a test suite, you often want to run the lexer and parser without sys.exit(1) terminating the process or raising an unhandled exception, while still collecting every Error object.
Diagnostics provides instance-level hooks (diag.add_hook(fn)) and a global context manager (Diagnostics.hook_errors(fn)). Each hook receives (sender, err) and returns an ErrorAction directive (gram.utilities.ErrorAction):
| Directive | Constant | Effect |
|---|---|---|
"store" |
ErrorAction.STORE |
Default behavior: appends err to sender._errors and respects config.ERROR_EXIT_ON_ERROR. |
"suppress" |
ErrorAction.SUPPRESS_RAISE |
Appends err to sender._errors, sets err._suppress_raise = True, and prevents printing, raising, or calling sys.exit(1). |
"omit" |
ErrorAction.OMIT |
Completely ignores err: does not store it in sender._errors and does not raise or exit. |
"raise" |
ErrorAction.RAISE |
Appends err to sender._errors and immediately invokes err.raise_error(). |
Example: Collecting Syntax Errors Without Exiting
from gram import combinators as cmb
from gram import lexer, parser
from gram.utilities import Diagnostics, ErrorAction, error
class Stmt(cmb.RuleItem):
entrypoint = cmb.Seq(
cmb.MatchToken(lexer.Token.IDENT),
cmb.MatchToken(lexer.Token.ASSIGN),
cmb.MatchToken(lexer.Token.NUMBER),
)
class Program(cmb.RuleItem):
entrypoint = cmb.Alt(cmb.Ref(Stmt))
captured: list[error.Error] = []
def intercept_errors(sender: object, err: error.Error) -> str:
captured.append(err)
return ErrorAction.SUPPRESS_RAISE
# Parse malformed input ("x = ;" instead of "x = 1") without exiting or printing to console
with Diagnostics.hook_errors(intercept_errors):
tokens = lexer.Lexer("x = ;").process()
ast_nodes = parser.Parser(tokens).parse(Program)
print("Captured errors:", len(captured))
print("Deepest error name:", captured[0].name)
print("Error code:", captured[0].code.stringify())
Output:
Captured errors: 2
Deepest error name: TokenMismatch
Error code: S-12411 · Gram.Parser.UnexpectedToken