Configuration & Diagnostics Reference
Gram provides a rich, unified telemetry and configuration framework governing the lexical analysis subsystem.
1. Exhaustive Configuration Options
All lexer flags reside in gram.config. When running inside a Session, the lexer reads overrides from session.config first before falling back to gram.config.
| Setting Flag | Type | Default | Operational Effect on Lexer |
|---|---|---|---|
LEXER_COMMENT_TOKEN |
str | Token | tuple |
'#' |
Defines prefix marker(s) for single-line comments outside string literals (e.g., '//', ';', '--'). |
LEXER_BLOCK_COMMENT_TOKEN |
str | None |
None |
Opening delimiter for multi-line block comments (e.g., '/*'). Closes strictly with '*/'. |
LEXER_SUPPORT_BLOCK_COMMENTS |
bool |
True |
When False, block comments configured in LEXER_BLOCK_COMMENT_TOKEN are parsed as normal symbols. |
LEXER_SUPPORT_DOCSTRINGS |
bool |
True |
Enables Python-style triple-quoted multiline string literals ("""...""" and '''...'''). |
LEXER_SUPPORT_PREFIXED_STRINGS |
bool |
True |
Recognizes C string prefixes (L"...", u"...", U"...", u8"..."), saving prefix to token.description. |
LEXER_SUPPORT_LEADING_DOT_NUMBERS |
bool |
True |
When True, .5 is tokenized as Token.DECIMAL(0.5). When False, emits Token.DOT and Token.NUMBER(5). |
LEXER_TAB_WIDTH |
int |
4 |
Visual column span for tab characters when LEXER_INDENT_STYLE is "tabs" or "mixed". Minimum 1. |
LEXER_INDENT_STYLE |
str |
'spaces' |
Indentation calculation policy: "spaces" (space-only), "tabs" (tab-only), or "mixed" (column expansion). |
SUPPORT_INDENT |
bool |
True |
Master toggle for indentation tracking. When False, omits Token.INDENT and Token.DEDENT entirely. |
SUPPORT_TRIPLE_DOT |
bool |
True |
When True, ... emits Token.THREE_DOTS. When False, emits three consecutive Token.DOT tokens. |
LEXER_IGNORE_EMPTY_LINES |
bool |
True |
When True, blank and comment-only lines are skipped without emitting redundant Token.NEWLINE tokens. |
LEXER_IGNORE_NEWLINES |
bool |
True |
When True, newline characters at the end of lines are discarded rather than emitting Token.NEWLINE. |
LEXER_SAVE_COMMENTS |
bool |
True |
When True, scanned comments are stored in lexer.comments. When False, comments are discarded immediately. |
LEXER_IDENT_UNDERSCORES |
int |
-1 |
Controls leading underscore behavior in identifiers: -1 (unlimited, default), -2 (unlimited only if followed by characters; standalone _ emits UNDERSCORE), 0 (no leading underscores permitted; all leading/standalone _ emit UNDERSCORE), or k >= 1 (at most k leading underscores in identifier; excess leading underscores emit UNDERSCORE). Internal and trailing underscores (e.g. A______B, D____________) remain agnostic to this setting and are always preserved. |
TOKEN_BOOL_TRUE |
str |
'true' |
Lexeme spelling converted to Token.BOOL with value=True (case-insensitive). |
TOKEN_BOOL_FALSE |
str |
'false' |
Lexeme spelling converted to Token.BOOL with value=False (case-insensitive). |
TOKEN_NULL |
str |
'null' |
Lexeme spelling converted to Token.NULL with value=None (case-insensitive). |
LEXER_ADD_INFO |
bool |
True |
Enables diagnostic execution nodes and notes in the Diagnostics stack. |
LEXER_ADD_ERROR |
bool |
True |
Records lexical failure entries in the active diagnostic stack. |
ERROR_EXIT_ON_ERROR |
bool |
True |
When True, unhandled errors call sys.exit(1). Set to False to collect diagnostics without crashing. |
ERROR_HIDE_CONSOLE |
bool |
False |
When True, suppresses ANSI console output during error reporting. |
2. Diagnostic Telemetry & Execution Tracing
LexerState inherits directly from Diagnostics (gram.utilities.Diagnostics), providing deep, structured telemetry for language frontends.
Hierarchical Diagnostic Stack
When LEXER_ADD_INFO=True, the lexer automatically creates nested telemetry nodes:
LEXER: Initialized at lexer creation, tracking total lines and logfile configuration.LexingProcess: Active throughoutprocess(), tracking step-by-step token production.VISITOR-STRING/VISITOR-NUMBER/VISITOR-WORD/VISITOR-SYMBOL: Nested underLexingProcessfor each dispatched character.
from gram import config
from gram.core.lexer import Lexer, words
words.add_keyword("let", allow_override=True)
# Enable detailed logging to a diagnostic file:
config.INFO_GENERATE_LOGFILE = True
lexer = Lexer("let x = 42;", logfile="my_compiler.log")
tokens = lexer.process()
# Access the active diagnostic stack:
stack = lexer.stack
print(f"Recorded diagnostic nodes: {len(stack.main.nodes)}")
Disabling Telemetry for High Performance
In production compilers, test suites, or performance benchmarks, set LEXER_ADD_INFO = False:
- Skips creation of
StackInfonodes. - Disables note string formatting and sender calls.
- Executes the scanning loop at raw Python speed with zero telemetry overhead.
3. Error Handling & Exit Policies
When the lexer encounters invalid syntax or malformed tokens, it invokes LexerState.report_error(err) or LexerState.fail(name, code, *caution).
def report_error(self, err: errors.Error) -> errors.Error:
res = self.record_failure(err)
if (
res is not None
and not getattr(err, "_suppress_raise", False)
and self.get_config("ERROR_EXIT_ON_ERROR", True)
):
err.raise_error()
return err
Collecting Errors Without Process Exit
Since version 2.0.0b1, config.ERROR_EXIT_ON_ERROR defaults to True. To collect errors programmatically (for example, in Language Server diagnostics, IDEs, or unit tests):
from gram import config, errors
from gram.core.lexer import Lexer, words
words.add_keyword("dummy", allow_override=True)
# 1. Disable exit on error:
config.ERROR_EXIT_ON_ERROR = False
config.ERROR_HIDE_CONSOLE = True
# 2. Tokenize malformed source:
lexer = Lexer('int x = "unclosed string')
tokens = lexer.process()
# 3. Inspect collected diagnostic errors:
for failure in lexer.failures:
print(f"Error [{failure.code}]: {failure.message}")
for hint in failure.caution:
print(f" Hint: {hint}")
Registering Error Hooks (ErrorAction)
You can intercept and modify error handling dynamically using error hooks:
from gram.utilities import Diagnostics, ErrorAction
def my_error_hook(err):
# Suppress terminal exit while recording telemetry:
return ErrorAction.SUPPRESS_RAISE
Diagnostics.register_error_hook(my_error_hook)
4. OSGDC Lexer Error Codes Matrix
Gram adheres to the OSGDC error code standard across all failure conditions in gram.errors:
| Error Code Identifier | Code Enum | Trigger Cause | Recommended Resolution Hint |
|---|---|---|---|
LEXER_UNEXPECTED_CHARACTER |
errors.LEXER_UNEXPECTED_CHARACTER |
Character encountered that cannot be matched to any number, string, word, comment, or symbol in MAP_SYMBOLS. |
Register the character/operator using insert_symbol(sym, Token) or configure custom comment markers. |
LEXER_UNCLOSED_STRING |
errors.LEXER_UNCLOSED_STRING |
End of line or end of file reached before closing quotation mark (", ') or docstring delimiter (""", '''). |
Ensure matching closing quotation marks or multiline delimiters before end of line. |
LEXER_INVALID_NUMBER |
errors.LEXER_INVALID_NUMBER |
Malformed numeric literal (e.g., misplaced underscores 1__2, 123_, invalid radix digit, or invalid exponent). |
Verify base prefix (0x, 0b, 0o), exponent syntax, and digit separator placement. |
LEXER_INDENTATION_MISMATCH |
errors.LEXER_INDENTATION_MISMATCH |
Decreased line indentation does not match any previous level on the indentation stack. | Check spacing consistency and ensure dedents align with an enclosing block. |
LEXER_EMPTY_KEYWORDS |
errors.LEXER_EMPTY_KEYWORDS |
Lexer.process() invoked with 0 registered keywords in words.MAP_KEYWORDS or the active session. |
Register at least one language keyword using words.add_keyword() before scanning. |
LEXER_KEYWORD_INVALID_NAME |
errors.LEXER_KEYWORD_INVALID_NAME |
words.add_keyword("") invoked with an empty or whitespace-only name. |
Provide a non-empty alphanumeric keyword identifier. |
LEXER_KEYWORD_ALREADY_EXISTS |
errors.LEXER_KEYWORD_ALREADY_EXISTS |
Attempting to register an already existing keyword without setting allow_override=True. |
Pass allow_override=True to replace an existing registration or choose another name. |
LEXER_KEYWORD_NOT_FOUND |
errors.LEXER_KEYWORD_NOT_FOUND |
Strict keyword lookup (words.get_keyword(name, strict=True)) failed. |
Ensure keyword is registered prior to querying. |
LEXER_WORDGROUP_NOT_FOUND |
errors.LEXER_WORDGROUP_NOT_FOUND |
Strict group query (words.get_group(name, strict=True)) failed. |
Register the group with words.add_group(name). |
LEXER_WORDGROUP_ALREADY_EXISTS |
errors.LEXER_WORDGROUP_ALREADY_EXISTS |
Attempting to create an existing WordGroup without allow_override=True. |
Pass allow_override=True or verify group naming. |
LEXER_WORDGROUP_KEYWORD_NOT_REGISTERED |
errors.LEXER_WORDGROUP_KEYWORD_NOT_REGISTERED |
Attempting to add an unregistered string keyword to a WordGroup. |
Register the keyword via words.add_keyword(kw) before adding it to a group. |
LEXER_FILE_NOT_FOUND |
errors.LEXER_FILE_NOT_FOUND |
Lexer(Path("nonexistent.gram")) received a file path that does not exist on disk. |
Verify file path and read permissions. |
5. Multi-Language Sessions & Lexicon Scoping
When building multi-language frontends, embedded DSLs, or parallel test suites, use gram.sessions.Session to prevent cross-contamination of keywords, symbols, and lexer configurations.
from gram.sessions import Session
from gram.core.lexer import Token
# 1. Create independent sessions for two distinct DSLs:
session_sql = Session("SQL_Dialect", "Many")
session_c = Session("C_Dialect", "Many")
# 2. Configure SQL session:
session_sql.init()
session_sql.config["LEXER_COMMENT_TOKEN"] = "--"
session_sql.add_keyword("select")
session_sql.add_keyword("from")
session_sql.migrate()
# 3. Configure C session:
session_c.init()
session_c.config["LEXER_COMMENT_TOKEN"] = "//"
session_c.add_keyword("int")
session_c.add_keyword("return")
session_c.migrate()
# 4. Tokenize with session isolation:
tokens_sql = session_sql.tokenize("select * from users; -- SQL query")
tokens_c = session_c.tokenize("int main() { return 0; } // C code")
# SQL session only recognizes SQL keywords:
print([t.value for t in tokens_sql if t.token == Token.KEYWORD])
# ['select', 'from']
# C session only recognizes C keywords:
print([t.value for t in tokens_c if t.token == Token.KEYWORD])
# ['int', 'return']
Inside Session.init(), Gram calls words.bind_session(session), routing all loose calls to words.add_keyword() and words.add_group() directly to the session's internal catalogs until session.migrate() is called.