Divergences from Lark
Where Hyperlark’s Python package behaves differently from Lark. To switch a project, start at Coming from Lark. For what each target supports, see the feature matrix.
Tokens and trees
Section titled “Tokens and trees”Tokens are FastTokens by default, not the str subclass Lark uses. A
FastToken passes isinstance(tok, Token), compares equal to its text, has
a Token’s attributes, and supports string methods, formatting,
concatenation, int(tok) and float(tok). But "".join(children),
re.match(pattern, tok), json.dumps(tok) and tok in s raise
TypeError, and isinstance(tok, str) is False. Pass tok.value where a
str is required, or build with fast_tokens=False for slower
str-subclass tokens, as in Lark.
A FastToken’s .type must be a str; assigning any other object raises
TypeError. Lark accepts any object.
tree.data is a plain str, not Lark’s Token('RULE', name):
tree.data == "start" works in both, tree.data.type only in Lark.
parse() returns a Tree subclass, so type(tree) is Tree is False.
Test with isinstance.
Not implemented
Section titled “Not implemented”hyperlark.reconstruct, hyperlark.tree_templates, hyperlark.tools and
TextSlice do not exist (the
feature matrix
tracks their status), and Lark’s internal modules (lark.parsers,
lark.parser_frontends, lark.grammar) have no counterpart.
A truthy value for any of these options raises ConfigurationError when the
parser is built. The default, a falsy value, is accepted:
| Option | Note |
|---|---|
strict= | |
cache_grammar= | cache= alone works |
use_bytes= | |
edit_terminals= | |
regex= | the third-party regex module. \p{...} escapes work without it |
These values always raise ConfigurationError when the parser is built:
| Value | Note |
|---|---|
ambiguity='forest' | 'resolve' and 'explicit' work |
parser='cyk' | 'lalr' and 'earley' work |
parser=None | Lark’s lexer-only mode. To lex without parsing, build a parser whose rules use the terminals and call .lex() |
propagate_positions= a callable | Lark’s node filter. True and False work |
ordered_sets= is accepted and ignored: sets are always ordered.
Grammars
Section titled “Grammars”Terminal regexes use Python’s re syntax but are compiled by Rust regex
engines. A construct the two read differently, such as a trailing $, is
refused when the parser is built, and the error names it.
Terminals lists the refused constructs and the
experimental translate_python_regex=True option that rewrites them
instead.
A missing %import raises GrammarError. Lark raises IOError.
A terminal or rule priority outside ±2147483647, or a rule repeat count
(~ n..m) outside the signed 64-bit range, is rejected when the parser is
built. Lark accepts both.
TBD: a relative %import, such as %import .mygrammar (NAME), in a
grammar passed as a string raises GrammarError. Lark looks beside the
running script, or in the working directory when there is none. Until then,
pass source_path= or import_paths=.
TBD: a terminal that repeats part of its pattern very many times, such as
"a" ~ 1..300000 or /[a-z]{1,100000}/, exceeds the regex engine’s size
limit and is refused when the parser is built. Lark builds it.
The parser object
Section titled “The parser object”Lark has no rules, grammar, lexer_conf, ignore_tokens,
serialize(), deserialize() or memo_serialize(). To save and reload a
parser, use Lark.save() and Lark.load(). options, terminals and
get_terminal() are present, and options holds the same normalized values
as Lark’s.
debug=True sets the hyperlark logger to DEBUG. Lark’s debug leaves
its logger’s level unchanged.
With transformer=, a method named after a _-prefixed rule raises
ConfigurationError when the parser is built. Lark calls it, and so does
Hyperlark when the transformer also overrides __default__.
With transformer= and propagate_positions=True, Hyperlark calls
__lark_meta__ only on the first and last values of a node. Lark also calls
it on the first and last values of each _-rule and of each step of a
repetition (item+, item*), so a __lark_meta__ that raises there fails
the parse in Lark only. The positions are the same.
Errors and recovery
Section titled “Errors and recovery”- An Earley error has
state=None, somatch_examplesraisesAssertionErroron it; build withparser="lalr"to classify errors. err.interactive_parserisNonewith a custom lexer, so anon_error=handler there can skip the bad input or stop, but not feed tokens. Under LALR,err.statethen has onlypositionand equality, and for a lexing error inside the custom lexer it isNone.- With a custom lexer, an
UnexpectedInputthat the lexer or a lexer callback raises does not reach anon_error=handler: it propagates as raised. - An
UnexpectedInputthat thetransformer=raises does not reach anon_error=handler: it propagates as raised. Lark hands it over with the parser in the middle of that reduction. - A
transformer=method that raises while anon_error=handler feedserr.interactive_parserends the parse: if the handler catches the exception and returnsTrue, resuming raisesLarkError. Lark resumes. - Under LALR, an
UnexpectedTokenthat Hyperlark raises inparse()orresume_parse()hasexpectedequal toaccepts, the tokens that will advance the parser into a new valid state. Lark’sexpectedis not narrowed toaccepts, so it can name a token the parser would then reject. err.token_historynames the last token the parser received, where Lark names the last one its lexer produced.- Under LALR, a non-callable
on_error=raisesTypeErroras soon asparse()is called, even on input that parses. Lark raises only when it calls the handler, at the first error.
Lexer callbacks
Section titled “Lexer callbacks”lexer_callbacks can be used to interface with the lexer as it generates
tokens (Lexer callbacks). It accepts a
dictionary of the form {TOKEN_TYPE: callback}, where the callback is of type
f(Token) -> Token. The token’s .type and .value are writable, and tok.update(value=...) or
type(tok).new_borrow_pos(...) builds a replacement. A callback on a
terminal that is not ignored must return a token, or LexError is raised.
Under Earley’s dynamic lexer lexer_callbacks= raises GrammarError; use
lexer="basic".
It differs from Lark in these ways:
- The returned token’s
typemust be a terminal of the compiled grammar, orLexErroris raised; Lark lets it reach the parser and fail there asUnexpectedToken. To retype a token to a terminal no rule uses,%declareit. - Position fields the returned token leaves unset are copied from the input
token, so a bare
Token('X', 'y')keeps the original positions. Lark leaves themNone. - Writing
.valuechanges the emitted token’s text, the string it compares equal to and the tree leaf, with or withoutfast_tokens. In Lark it changes only the attribute. - A non-str
.value, as int.update(value=int(t)), is stored asstr(value); Lark keeps the object. Convert in a transformer instead. - Under the contextual lexer, an
UnexpectedInputa callback raises reaches you, or anon_error=handler, as raised. Lark’s contextual lexer replaces it with anUnexpectedTokenfor the next token, or anEOFErrorat the end of the input. - Under
lex(text, dont_ignore=True), a callback on an%ignored terminal that returns anything but a token leaves the original token in the stream. Lark raisesLexErrorthere.
On an %ignored terminal the return value is discarded, so
comments.append works as the callback:
import hyperlark
comments = []parser = hyperlark.Lark(r""" start: WORD+ COMMENT: /#[^\n]*/ %import common.WORD %ignore " " %ignore /\n/ %ignore COMMENT""", parser="lalr", lexer_callbacks={"COMMENT": comments.append})
parser.parse("hello # a note\nworld")print(comments) # [Token('COMMENT', '# a note')]Postlex
Section titled “Postlex”A postlex (postlex=)
post-processes the token stream between the lexer and the parser. Its
process() generator runs for the whole of one parse or one
interactive parser. Lark starts a fresh generator on every parse,
resume_parse, exhaust_lexer and iter_parse call, which for an Indenter
also resets its paren and indent state. The difference shows when one parse
is driven more than once: under on_error, by resume_parse after breaking
out of iter_parse, or by a second exhaust_lexer after a rejection.
- State can carry over. A new generator starts only after the lexer has raised an error inside it, which under the default contextual lexer includes a token the parser cannot accept.
- An exception other than an
UnexpectedInputthatprocess()itself raises is permanent for that parse: it does not reach anon_error=handler, and every later call that reads from the lexer (exhaust_lexer,iter_parse,resume_parse,peek_next,feed_next) raises it again. InteractiveParser.copy(),as_immutable(), and the immutable parser’speek_nextandexhaust_lexerfork without a Python postlex: the fork receives the lexer’s tokens, and nothing warns. Avoid them with a Python postlex.
An Indenter that does not override process, _process, handle_NL or
always_accept runs in the built-in implementation. Its paren and indent
state also carries across errors, and copy() carries that state into the
fork. An exception the Indenter raises is permanent in the same way.
Interactive parsing
Section titled “Interactive parsing”parse_interactive is LALR-only, as in Lark, and also requires the built-in
lexer: a parser built with a custom lexer raises NotImplementedError. The
InteractiveParser adds last_token (the most recent token the parser
consumed, or None before the first one and after feed_eof), peek_next()
and feed_next(), described in
Interactive parsing. It differs from Lark’s in these
ways:
- A rejected
feed_tokenraisesUnexpectedTokenwith.acceptsfilled; Lark’s isNone. - A token fed with
feed_tokencarries no line or column into the tree or into errors, even one that came fromlex(); tokens the parser lexes itself keep theirs. The positions of thelast_tokenpassed tofeed_eofare ignored too. choices()values are('Shift', state_id)or('Reduce', rule_id)tuples. In Lark the values are itsShiftandReduceaction objects, and only aReducecarries aRule.- Breaking out of
iter_parseleaves the yielded token queued, so a later call that advances the parse resumes with it. In Lark the lexer has already consumed it, andresume_parsecontinues without it. - Inside an
iter_parseloop, passing the yielded token tofeed_tokenfeeds it once. Lark feeds it a second time when the loop continues. - The
.valuerules for lexer callbacks apply to the tokeniter_parseyields and to apeek_nexttoken committed withfeed_next(tok). parser_statehas onlyposition,state_stack,value_stackand equality. Eachvalue_stackread copies the parse so far. It is empty once the parse has finished, and raisesNotImplementedErrorunder an embeddedtransformer=.lexer_threadandlexer_stateare not exposed.- Under an embedded
transformer=, a parser that has returned its tree raisesLarkErroron any further feed or resume, including the resume after anon_error=handler that finished it. Lark raisesUnexpectedToken. resume_parsecontinues the postlex where it stopped, so an indenter keeps its indentation levels. Lark starts the postlex again from no indentation, which breaks recovery inside an indented block or brackets.- On
ImmutableInteractiveParser,feed_nextanditer_parseraiseNotImplementedError. - On
ImmutableInteractiveParser, theinteractive_parserof an errorresume_parseraises is a mutable copy. Lark’s shares the immutable parser’s state. - Calling an interactive parser from one of its own callbacks (a lexer
callback, a postlex or a
transformer=method), or from a second thread while a call is in progress, raisesRuntimeError.
Class identity and compat mode
Section titled “Class identity and compat mode”Hyperlark’s Token, Tree and exception classes are its own. Without the
optional compat mode, isinstance(tok, lark.Token) is False, an
except lark.exceptions.UnexpectedToken clause does not catch Hyperlark’s
exception, and a lark.Transformer applied to a Hyperlark tree returns it
unchanged.
Using Hyperlark alongside lark
has the install and import steps.
With from hyperlark.compat import Lark, parse() returns lark.Tree and
lark.Token objects and raises the lark.exceptions classes. Installing the
extra alone changes nothing. Compat mode is slower than plain
hyperlark.Lark: it converts after the parse and defaults to
fast_tokens=False.
Compat mode is incomplete in this beta, and nothing raises at a gap:
- Only
parse()converts.lex(),parse_interactive()andparse_eager()return Hyperlark objects. - An
on_error=handler receives the Hyperlark exception. - A converted error’s
interactive_parserandstateare Hyperlark’s, so what they return or raise is not converted. compat.Larkhas noopen()oropen_from_package(); construct it from a grammar string.
Parsing from inside a callback
Section titled “Parsing from inside a callback”Code that runs during a parse (a lexer callback, a custom lexer, a postlex, a
transformer method) may itself parse, with the same or another instance, or
drive an interactive parser. Nesting deeper than 8 levels on one thread
raises RecursionError.
Building a Lark from code the constructor runs (a custom lexer’s __init__,
or a postlex, transformer or %import loader it inspects) has no such bound.
Unbounded recursion there exhausts the C stack and can kill the process with
SIGSEGV, where Lark raises an error. sys.setrecursionlimit() does not
help, and a thread with a small stack (under about 4 MiB) crashes far sooner
than the 8 MiB main thread. Give such code a base case or a depth counter.
Deep input and deep trees behave as in Lark: the parser is iterative, and
repr, pretty and Transformer on a very deep tree raise RecursionError.