Marpa:...rogress(3pm)
๐ฆ Marpa::R2::Progress ๐
okay bestie, welcome to Marpa::R2::Progress โ that's the Marpa:...rogress(3pm) page you just typed into your terminal, straight out of the User Contributed Perl Documentation collection. it's giving "here's how you spy on your own parse". sit down, grab a strawberry ๐, we're doing this.
๐ธ NAME ๐ธ
Marpa::R2::Progress โ progress reports on your parse โจ
๐ About this document ๐
So this doc is all about the progress reports for Marpa's SLIF interface. what do they do? they let an application know exactly where it is in the parse, at any point. no cap. ๐ซถ
For parse locations of the user's own choosing, progress reports list all the rules in play, and they indicate the location where the rule started, plus how far into the rule parsing has progressed. literally a little GPS for your grammar. ๐บ๏ธ
Honestly bestie, progress reports are extremely useful for debugging grammars โ the detailed example down below is literally a debugging instance. if you came here specifically to debug a grammar, go read the document on tracing problems before you read this one. do it. i'm not mad, i'm just saying. ๐
โจ Introduction to Earley items โจ
To read the show_progress output, it's important to have a basic idea of what Earley items are, and what the info inside them actually means. everything you need to know is right here in this section. no gatekeeping. ๐
๐ Dotted rules
Marpa is based on Jay Earley's algorithm for parsing. the whole idea behind Earley's algorithm is that you can parse by building a table of rules, plus where you are in those rules. and "where" means two things, bestie:
- location in the rule relative to the rule's symbols
- location relative to the parse's input stream
Let's look at an example rule in a context-free grammar. here's the rule for assignment from the Perl distribution's perly.y:
termbinop -> term ASSIGNOP term
ASSIGNOP is perly.y's internal name for the assignment operator. in plain Perl terms? that's the "=" character. ๐
While parsing this rule, we can be at any of four possible locations. one location is at the beginning, before all of the symbols. the other three are immediately after each of the rule's three symbols.
Within a rule, position relative to the symbols of the rule is traditionally indicated with a dot. in fact, the symbol-relative rule position is very often called the dot location. taken as a pair, a rule and a dot location are called a dotted rule. โจ
Here's our rule with a dot location indicated:
termbinop -> . term ASSIGNOP term
the dot location in this dotted rule is at the beginning. a dot location at the beginning of a dotted rule means we haven't recognized any symbols in the rule yet. all we're doing is predicting that the rule will occur. a dotted rule with the dot before all of its symbols is called a prediction or a predicted rule.
Here's another dotted rule:
termbinop -> term . ASSIGNOP term
in this one, we're saying we've seen a term, but we haven't yet recognized an ASSIGNOP. ๐
There's another special kind of dotted rule: a completion. a completion (also called a completed rule) is a dotted rule with the dot after all of the symbols. here's the completion for the rule we've been using as our example:
termbinop -> term ASSIGNOP term .
a completion indicates that a rule has been fully recognized. slay. ๐
๐ Earley items
Dotted rules contain all but one piece of the information Marpa needs to track. the missing piece is the second of the two "wheres": where in the input stream.
To associate input stream location and dotted rules, Marpa uses what are now called Earley items.
A convenient way to think of an Earley item is as a triple, or 3-tuple, consisting of:
- dotted rule
- origin
- current location
The origin is the location in the input stream where the dotted rule starts. the current location (also called the dot location) is the location in the input stream which corresponds to the dot position.
In Marpa terms, G1 location is location in terms of the G1 subgrammar's Earley sets. when the term "location" is used in this document, it means G1 location unless otherwise indicated. ๐
A user often finds it much more convenient to think in terms of line and column position in the input stream, instead of G1 location. every G1 location corresponds to a range of positions in the input stream. when the term "position" is used in this document, it means input stream position, unless otherwise indicated.
Two noteworthy consequences follow from the way origin and current G1 location are defined:
- if a dotted rule is a prediction, then origin and current location will always be the same.
- the input stream location where a rule ends is not tracked unless the dotted rule is a completion. if its dotted rule is not a completion, an Earley item does not tell us whether a rule will ever be completed โ much less at which location. ๐
๐ญ The problem ๐ญ
For this debugging example, I took a very simple prototype of a string expression calculator and deliberately introduced a problem. i commented out one of the correct rules:
# <numeric assignment> ::= variable '=' <numeric expression>
and replaced it with an altered one:
<numeric assignment> ::= variable '=' expression
For those of you who like to look ahead (and honestly? i encourage you to be one of those readers ๐ ), all of the code and outputs for this example are collected in the "Appendix".
This altered rule contains a mistake of the kind that's easy to make in actual practice. (in this case, an unlucky choice of naming conventions may have contributed. ๐) The altered version will cause problems. in what follows, we're gonna pretend we don't already know where the problem is, and that while desk-checking the grammar our eye does not spot the mistake โ so we need to use the Marpa diagnostics and tracing facilities to "discover" it. detective mode. ๐
๐ The example ๐
The example we'll use is a prototype string calculator. it's extremely simple, so the example is easy to follow. but it can be seen as a realistic example if you think of it as a very early stage in the incremental development of something useful. ๐ฑ
:default ::= action => ::array bless => ::lhs
:start ::= statements
statements ::= statement *
statement ::= assignment | <numeric assignment>
assignment ::= 'set' variable 'to' expression
# This is a deliberate error in the grammar
# The next line should be:
# <numeric assignment> ::= variable '=' <numeric expression>
# I have changed the <numeric expression> to <expression> which
# will cause problems.
<numeric assignment> ::= variable '=' expression
expression ::=
variable | string
|| 'string' '(' <numeric expression> ')'
|| expression '+' expression
<numeric expression> ::=
variable | number
|| <numeric expression> '*' <numeric expression>
|| <numeric expression> '+' <numeric expression>
variable ~ [\w]+
number ~ [\d]+
string ~ ['] <string contents> [']
<string contents> ~ [^'\x{0A}\x{0B}\x{0C}\x{0D}\x{0085}\x{2028}\x{2029}]+
:discard ~ whitespace
whitespace ~ [\s]+
At this stage of developing our string calculator, we have assignment, variables, constants, concatenation and conversion of numerics. for numerics, we have assignment, variables, constants, multiplication and addition.
We decide that, since string expressions and variables are the "default", we'll make the symbol names for numeric assignment and expressions explicit in the grammar: <numeric expression> and <numeric assignment>. but since strings are the default, we decide to call our string expressions simply <expression>, and to call our string assignments simply <assignment>. this seems like a good idea, but it's also likely to cause confusion. for the sake of our example, we'll pretend it did. ๐
๐ The error message ๐
If we try the following input,
my $test_input = 'a = 8675309 + 42 * 711';
we'll get this error message,
Error in SLIF parse: No lexemes accepted at line 1, column 18
Rejected lexeme #0: '*'; value="*"; length = 1
* String before error: a = 8675309 + 42\s
* The error was at line 1, column 18, and at character 0x002a '*', ...
* here: * 711
The error message indicates that Marpa rejected the "*" operator. not me getting rejected by my own grammar ๐ญ
๐ซถ The value of the parse ๐ซถ
In debugging this issue, we'll look at the value of the parse first. the parse value differs from the other debugging aids we'll discuss โ every other debugging tool we describe is always available, no matter how badly the parse failed. but if you have a problem parsing, you often won't get a parse value. ๐ฅฒ
Our luck holds, though. here's a dump of the parse value at the point of failure. it's a nice way to see what Marpa thinks the parse was so far.
\bless(
[
bless(
[
bless(
[
'a', '=',
bless(
[
bless( ['8675309'], 'My_Nodes::expression' ),
'+',
bless( ['42'], 'My_Nodes::expression' )
],
'My_Nodes::expression'
)
],
'My_Nodes::numeric_assignment'
)
],
'My_Nodes::statement'
)
],
'My_Nodes::statements'
);
If we were perceptive, we might spot the error right here. our parse is not quite right, and that shows up in the outer My_Nodes::expression โ it should be My_Nodes::numeric_expression. but we'll assume we don't notice this. ๐
In fact, in the following, we'll pretend we haven't seen the dump of the parse value at all. we can't always get a parse value, so we don't wanna rely on it. ๐
more to come bestie, don't go anywhere ๐
๐ป Output from trace_terminals() ๐ป
okay so you can literally rely on getting the output from trace_terminals, and it is a good next place to check. Typically, you will be interested in the last tokens to be accepted. Sometimes that information alone is enough to make it clear where the problem is. no cap. โจ
The full trace_terminals output for this example is in the Appendix. We see that the recognizer accepts the input as far as the multiplication sign ("*"), which it rejects. In Marpa, a lexeme is "acceptable" if it fits the grammar and the input so far. A lexeme is rejected if it is not acceptable. periodt.
The last two lines of the trace_terminals output are:
Discarded lexeme L1c17: whitespace
Rejected lexeme L1c18: '*'; value="*"
A note in passing: Marpa shows the input string position of the tokens it accepts, discard and rejects. <whitespace> is supposed to be discarded and that was what happened at line 1, column 17. But the '*' that was next in the input was rejected, and that was not supposed to happen. ๐ญ not the vibe we wanted.
๐ Output from show_progress() ๐
Marpa's most powerful tool for debugging grammars is its progress report, which shows the Earley items being worked on. In the Appendix, progress reports for the entire parse are shown. Our example in this document is a very small one, so that producing progress reports for the entire parse is a reasonable thing to do in this case. If a parse is at all large, you will usually need to be selective. bestie, do not dump the whole forest unless you must. ๐ณ
The progress report that is usually of most interest is the one for the Earley set that you were working on when the error occurred. This is called the current location. In our example the current location is G1 location 5. By default, show_progress prints out only the progress reports for the current location. ๐
Here are the progress reports for the current location, location 5, from our example.
F0 @0-5 L1c1-16 statements -> statement * .
P1 @5-5 L1c15-16 statement -> . assignment
P2 @5-5 L1c15-16 statement -> . <numeric assignment>
F2 @0-5 L1c1-16 statement -> <numeric assignment> .
P3 @5-5 L1c15-16 assignment -> . 'set' variable 'to' expression
P4 @5-5 L1c15-16 <numeric assignment> -> . variable '=' expression
F4 @0-5 L1c1-16 <numeric assignment> -> variable '=' expression .
F5 @2-5 L1c3-16 expression -> expression .
F7 @4-5 L1c13-16 expression -> expression .
F8 @4-5 L1c13-16 expression -> variable .
R11:1 @2-5 L1c3-16 expression -> expression . '+' expression
F11 @2-5 L1c3-16 expression -> expression '+' expression .
F19 @0-5 L1c1-16 :start -> statements .
๐ Progress report lines ๐
F19 @0-5 L1c1-16 :start -> statements .
The last field of each progress report line shows, in fully expanded form, the dotted rule we were working on. Prefixed to the dotted rule are three fields. In the example just above they are "F0 @0-5 L1c1-16". The "F0" says that this is a completed or final rule, and that it is rule number 0. The rule number is a convenient way to refer to a rule and is used when displaying the whole rule would take too much space.
The "@0-5" describes the G1 locations of the dotted rule in the parse. In its simplest form, the location field is two G1 location numbers, separated by a hyphen. The first G1 location number is the origin, the place where Marpa first started recognizing the rule. The last G1 location number is the dot location, the G1 location of the dot in a dotted rule. "@0-3" says that this rule began at G1 location 0, and that the dot is at G1 location 3.
Following the G1 location is the range of positions in the input string: "L1c1-16". This indicates that the origin of dotted rule is at line 1, column 1, and that its dot position is after line 1, column 16.
The current location is also just after line 1, column 16, and at G1 location 5, and this is no coincidence. Whenever we are displaying the progress report for a G1 location, all the progress report lines will have their dot location at that G1 location.
As an aside, notice that the left hand side symbol is :start. That is the start pseudo-symbol. The presence of a completed start rule in our progress report indicates that if our input had ended at location 5, it would be a valid sentence in the language of our grammar. (And it is because the input at G1 location 5 was a valid sentence of the grammar, that we were able to look at the value of the parse at location 5 for debugging purposes.) ๐ฆ
Let's look at another progress report line:
R11:2 @2-4 L1c3-13 expression -> expression '+' . expression
Here the "R11:2" indicates that this is rule number 11 (the "R" stands for rule number) and that its dot position is after the second symbol on the right hand side. Symbol positions are numbered using the ordinal of the symbol just before the position. Symbols are numbered starting with 1, and symbol position 2 is the position immediately after symbol 2. iykyk โจ
Predicted rules also appear in progress reports:
P2 @3-3 L1c5-11 statement -> . <numeric assignment>
Here the "P" in the summary field means "predicted". Notice that in the predicted rule, the origin is the same as the dot location. This will always be the case with predicted rules. iconic behavior, honestly.
๐ต๏ธโโ๏ธ OK! Now to find the bug ๐ต๏ธโโ๏ธ
If we look again are progress reports at the location 5, the location where things went wrong: We see that we have completed rules for <expression>, <numeric assignment>, <statement>, <statements>, as expected. We also see two Earley items that show that we are in the process of building another <expression>, and that it is expecting a '"+"' symbol.
What we want to know is, why is the recognizer not expecting an '"*"' symbol? Looking back at the grammar, we see that only one rule uses the '"*"' symbol. Here it is as part of a prioritized rule in the DSL:
<numeric expression> ::=
variable | number
|| <numeric expression> '*' <numeric expression>
|| <numeric expression> '+' <numeric expression>
Here it is from the show_rules() listing:
G1 R17 <numeric expression> ::= <numeric expression> '*' <numeric expression>
It's rule 17 in subgrammar G1, and for convenience we will call it R17. The next step is to look at the Earley items for this rule. But there is a problem. We don't find any. ๐
Next, we ask ourselves, what is the earliest place R17 should be appearing? The answer is that there should be a prediction of R17 at location 0. So we look at the predictions at location 0.
P0 @0-0 L0c0 statements -> . statement *
P1 @0-0 L0c0 statement -> . assignment
P2 @0-0 L0c0 statement -> . <numeric assignment>
P3 @0-0 L0c0 assignment -> . 'set' variable 'to' expression
P4 @0-0 L0c0 <numeric assignment> -> . variable '=' expression
P19 @0-0 L0c0 :start -> . statements
No R17 predicted at G1 location 0. Next we look through the the entire progress report, at all G1 locations, to see if R17 is predicted anywhere. No R17. Not anywhere. not a single crumb. ๐ญ
The LHS of R17 is <numeric expression>. We look in the progress report for dotted rules where <numeric expression> is expected -- that is, dotted rules where <numeric expression> is the post-dot symbol. There are none.
Next we look for places in the progress reports where <numeric expression> occurs at all, whether post-dot or not. In the progress reports, <numeric expression> occurs in only two dotted rule instances. Here they are:
P10 @2-2 L1c3 expression -> . 'string' '(' <numeric expression> ')'
P10 @4-4 L1c13 expression -> . 'string' '(' <numeric expression> ')'
In both cases these are predictions of a string operator, the operator we plan to use for converting numerics to strings. They are just predictions, predictions which go no further because there is no '"string"' operator in our input. That's fine, but why no other, more relevant, occurrences of <numeric expression>?
We look back at the grammar. Aside for the rule for the '"string"' operator, <numeric expression> occurs on a RHS in two places. One is in the prioritized rule which defines <numeric expression>.
<numeric expression> ::=
variable | number
|| <numeric expression> '*' <numeric expression>
|| <numeric expression> '+' <numeric expression>
This rule will never put <numeric expression> into the Earley items unless there is a <numeric expression> already there. But that is not its job. This rule is just fine and does not need fixing. slay, R17's defining rule, you did nothing wrong. ๐
That leaves one rule to look at.
<numeric assignment> ::= variable '=' expression
This rule is one that should lead to the prediction of a new <numeric expression> in our example. And now we see our problem. This rule is never leading to the prediction of a new <numeric expression>, because there is no <numeric expression> on its RHS, or for that matter anywhere else in it. On the RHS, where we wrote <expression>, we should have written <numeric expression>. Change that and the problem is fixed. ๐
๐ซถ COMPLICATIONS ๐ซถ
okay bestie, the main example is done. wrapped. but there's a whole little pile of debugging lore that never came up in that example, and some of it is genuinely surprising. so buckle up, we're going there. ๐
๐ณ๏ธ Empty rules
when a symbol gets nulled in your parse, show_progress shows you only the nulled symbol. that's it. it does not show the symbol's expansion into rules, and it does not show any of its nulled child symbols. nope. ๐
why? because it cuts down the clutter, and honestly you usually don't even notice the missing nulled rules and symbols are gone. not showing them just... seems like the intuitive way to treat them? yeah. it's giving minimalism. โจ
๐ Input string ranges
so G1 locations are the well-behaved sibling: they run in a monotonic sequence, starting with 0. they never run backwards, they are never visited twice, and they leave no gaps. icon behavior, no notes. ๐
input string positions though?? chaos gremlins. they can do all of those things. an application is allowed to jump around in the input. an input string position may be encountered more than once. it is quite possible to write your application so that it encounters, for example, line 42 before line 7. and your application does not have to visit line 42 on its way from line 41 to line 43. for that matter, an application does not ever have to visit any position in its input. not one. fr fr. ๐
so how does Marpa deal with all that when reporting input string ranges?
- Marpa always reports the minimum range that includes all the input string positions visited in the dotted rule. ๐ฏ
- the range is always reported in increasing numeric order, even when the position at the end of the range was visited before the input string position at the beginning of the range. so yes, the timeline is a lie and that's fine.
- and, if necessary to include all visited input string positions, the range may include input string positions which were not visited. yep. generous king.
most applications move forward continuously in the input string, and if yours is one of them, you don't have to worry about any of these issues. ๐ซถ but if you do unusual things when reading the input, it helps to be aware of how input string ranges are reported by Marpa when tracing and debugging.
๐งฌ Multiple instances of dotted rules
our main example for this document doesn't do it, but a dotted rule can appear in the same Earley set more than once. and honestly? it happens frequently. when it does, the lines in the progress report will look like these:
F11 x12 @0...38-41 L1c1-L2c40 <plain assignment> -> 'x' '=' expression .
F1 x20 @0...38-41 L1c1-L2c40 expression -> assignment .
F6 x12 @0...38-41 L1c1-L2c40 assignment -> <plain assignment> .
these are some of the progress report lines for an indirect right recursion โ one that recurses from a <plain assignment> symbol to an <expression> symbol, and then to an <assignment> symbol, before completing the recursion by returning to a <plain assignment>. recursion within recursion within recursion, truly giving main character energy. ๐
in each of those three lines, peep that a new field appears second. that second field is variously "x12" or "x20". these are counts, indicating the number of instances of that dotted rule at the dotted rule's G1 dot location. every dotted rule instance will have the same G1 location, but the instances may have many different origins โ hundreds or even more. ๐ญ and in each of the three report lines above, the G1 dot location is 41.
ngl, this next bit is such a flex: when parsing, Marpa handles the long series of Earley items generated by right recursions very efficiently. it uses a technique invented by Joop Leo to memoize and eliminate them. when a progress report is requested at a G1 location, the Leo-memoization is unfolded, and the full list of Earley items is reported. ๐ง
each instance may have its own span in the input string, and the input string range will include them all. when there are many instances of a dotted rule at a single location, the origins in the location field are shown as a range, with the earliest separated from the most recent by a "...". for example, above, where the first four fields were "F7 x12 @0...38-41 L1c1-L2c40", that tells us that the dotted rule is rule 7, which has 12 instances. all 12 instances have their dot location at G1 location 41, but their origins are in the range from G1 location 0 to G1 location 38. see? the dots are just doing their best. ๐
the last field in "F7 x12 @0...38-41 L1c1-L2c40" is an input string range. "L1c1-L2c40" says that input string positions visited by the 12 instances start at line 1, column 1, and end at line 2, column 40. the reported input string range will be the shortest range that includes all of the input string positions visited by any of the dotted rule instances. shortest. range. periodt. ๐
if there are only a few origins, Marpa may just explicitly list them all. in this next example, there are only 2 instances of this rule, both with a dot location of 41. their origins are at G1 locations 8 and 18. the range of input string positions is from line 1, column 17 to line 2, column 40.
F2 x2 @8,18-41 L1c17-L2c40 assignment -> <divide assignment> .
๐ Access to the "raw" progress report information
this section deals with the progress() recognizer method, which allows access to the raw progress report information. heads up though: this method is not needed for typical debugging and tracing situations. it is intended for applications which want to leverage Marpa's "situational awareness" in innovative ways. so if that's your thing โ hi, welcome, you're the moment. ๐
๐งฎ progress()
my $report0 = $recce->progress(0);
my $latest_report = $recce->progress();
given the G1 location (Earley set ID) as its argument, the progress() recognizer method returns a reference to an array of "report items".
the G1 location may be given as a negative number. an argument of -X will be interpreted as G1 location N-(X+1), where N is the latest Earley set. this means that an argument of -1 indicates the latest Earley set, an argument of -2 indicates the Earley set just before the latest one, etc. counting backwards but make it slay. ๐
each report item is a triple: an array of three elements. the three elements are, in order, rule ID, dot position, and origin. the data returned by the two displays above, as well as the data for the other G1 locations in our example, are shown below.
- rule ID โ the same number that Marpa uses to identify rules in tracing and debugging output. given a rule ID, an application can expand it into its LHS and RHS symbols using the SLIF grammar's
rule_expand()method. given a symbol ID, its name and other information can be found using other SLIF grammar methods. ๐ง - dot position โ
-1for completions, and0for predictions. where the report item is not for a completion or a prediction, dot position isN, whereNis the number of RHS symbols successfully recognized at theG1location of the progress report. - origin โ the
G1location (Earley set ID) at which the rule application reported by the report item began. for a prediction, origin will always be the same as theG1location of the parse report. always. so consistent of it. ๐ซก
โก Progress reports and efficiency
now listen, bestie โ when progress reports get used for production parsing instead of just for debugging and tracing, efficiency considerations become significant. ๐ณ
here's the tea: progress reports themselves are implemented in optimized C, and that logic is very fast. ๐ฅ however, the use of progress reports usually implies considerable post-processing in Perl. it is almost always possible to use Marpa's named events instead of progress reports, and solutions using named events are usually better targeted, simpler and faster. so like... maybe do that? ๐
if you do decide to use progress reports in an application, you should be aware of the efficiency considerations when there are right recursions in the grammar.
- for most purposes, Marpa optimizes right recursions, so that they run in linear time.
- however, to create a progress report, every potential right recursion must be fully unfolded, and at each
G1location the number of these grows linearly with the length of the recursion. - if you are creating progress reports for more than a limited number of
G1locations, this means processing that can be quadratic in the length of the recursion. ๐ญ - when a right recursion is lengthy, the impact on speed can be very serious. ๐
if lengthy right recursions are being expanded, this will be evident from the parse report itself, which will contain one report item for every completion in the right-recursive chain of completions.
note that the efficiency consideration just mentioned for following right recursions is never an issue for left recursions. left recursions produce at most two report items per G1 location and are extremely fast to process. it is also not an issue for Marpa's sequence rules, because sequence rules are implemented internally as left recursions. so left recursion stays winning. ๐โจ
๐ APPENDIX
below are the code, the trace outputs and the progress report for the example used in this document. the whole stash. ๐
๐ Code
my $slif_debug_source = <<'END_OF_SOURCE';
:default ::= action => ::array bless => ::lhs
:start ::= statements
statements ::= statement *
statement ::= assignment | <numeric assignment>
assignment ::= 'set' variable 'to' expression
# This is a deliberate error in the grammar
# The next line should be:
# <numeric assignment> ::= variable '=' <numeric expression>
# I have changed the <numeric expression> to <expression> which
# will cause problems.
<numeric assignment> ::= variable '=' expression
expression ::=
variable | string
|| 'string' '(' <numeric expression> ')'
|| expression '+' expression
<numeric expression> ::=
variable | number
|| <numeric expression> '*' <numeric expression>
|| <numeric expression> '+' <numeric expression>
variable ~ [\w]+
number ~ [\d]+
string ~ ['] <string contents> [']
<string contents> ~ [^'\x{0A}\x{0B}\x{0C}\x{0D}\x{0085}\x{2028}\x{2029}]+
:discard ~ whitespace
whitespace ~ [\s]+
END_OF_SOURCE
my $grammar = Marpa::R2::Scanless::G->new(
{
bless_package => 'My_Nodes',
source => \$slif_debug_source,
});
my $recce = Marpa::R2::Scanless::R->new(
{ grammar => $grammar,
trace_terminals => 1,
trace_values => 1,
} );
my $test_input = 'a = 8675309 + 42 * 711' ;
my $eval_error = $EVAL_ERROR if not eval { $recce->read( \$test_input ); 1 };
$progress_report = $recce->show_progress( 0, -1 );
๐จ Error message
Error in SLIF parse: No lexemes accepted at line 1, column 18
Rejected lexeme #0: '*'; value="*"; length = 1
* String before error: a = 8675309 + 42\s
* The error was at line 1, column 18, and at character 0x002a '*', ...
* here: * 711
๐ต๏ธ Parse value at error location
note that when there is a parse error, there will not always be a parse value. ๐ but sometimes the parse is "successful" enough, in a technical sense, to produce a value, and in those cases examining the value can be helpful in determining what the parser thinks it has seen so far. so like โ read the receipt, bestie, it tells a story. ๐งพ
...so when the parse finally lands and you call $recce->value(), the thing you get back is deeply nested and honestly a little extra. Here's the hand-written expected output from the example, bless-stacked like a whole outfit ๐
:
my $value_ref = $recce->value();
my $expected_output = \bless(
[
bless(
[
bless(
[
'a', '=',
bless(
[
bless( ['8675309'], 'My_Nodes::expression' ),
'+',
bless( ['42'], 'My_Nodes::expression' )
],
'My_Nodes::expression'
)
],
'My_Nodes::numeric_assignment'
)
],
'My_Nodes::statement'
)
],
'My_Nodes::statements'
);
okay so look at that โ it's bless all the way down, every layer rocking its class name like a name tag. The outermost is a My_Nodes::statements, which holds a My_Nodes::statement, which holds a My_Nodes::numeric_assignment, which holds the 'a' and '=' and then yet another My_Nodes::expression โ and that one is the cute little trio of bless( ['8675309'], 'My_Nodes::expression' ), the '+', and bless( ['42'], 'My_Nodes::expression' ). And notice the backslash on \bless โ that's the reference to the whole blessed thing, because $expected_output is holding a ref, not the value itself. fer sure fer sure. ๐ซถ
โจ Trace output โจ
with trace_terminals and trace_values switched on, the run is chatty. Not me getting a full play-by-play of every lexeme ๐:
Setting trace_terminals option
Setting trace_values option
Accepted lexeme L1c1 e1: variable; value="a"
Discarded lexeme L1c2: whitespace
Accepted lexeme L1c3 e2: '='; value="="
Discarded lexeme L1c4: whitespace
Rejected lexeme L1c5-11: number; value="8675309"
Accepted lexeme L1c5-11 e3: variable; value="8675309"
Discarded lexeme L1c12: whitespace
Rejected lexeme L1c13: '+'; value="+"
Accepted lexeme L1c13 e4: '+'; value="+"
Discarded lexeme L1c14: whitespace
Rejected lexeme L1c15-16: number; value="42"
Accepted lexeme L1c15-16 e5: variable; value="42"
Discarded lexeme L1c17: whitespace
Rejected lexeme L1c18: '*'; value="*"
first two lines are just the options announcing themselves, very "hi bestie, I'm on now" โจ. Then it's the whole drama: variable for "a" gets accepted (that's e1), the whitespace just gets discarded like it's nothing, '=' is accepted as e2, more whitespace discarded, and then the plot twist โ number "8675309" gets rejected, but variable with that same value "8675309" gets accepted as e3. The way that lexer looked at 8675309 twice and picked the variable reading... main character behaviour, honestly ๐ญ.
same energy for "42": number rejected, variable accepted as e5. And '+'? Rejected once, then accepted as e4 โ iconic. Then at the very end, '*' at L1c18 gets rejected and that's just... where the trace stops. '*' said nope and left the group chat ๐.
๐ show_progress() output ๐
then there's show_progress(), which is the big one. it just dumps the entire progress report โ every P, F and R line, every position span, every Earley item lit up like a christmas tree ๐:
P0 @0-0 L0c0 statements -> . statement *
P1 @0-0 L0c0 statement -> . assignment
P2 @0-0 L0c0 statement -> . <numeric assignment>
P3 @0-0 L0c0 assignment -> . 'set' variable 'to' expression
P4 @0-0 L0c0 <numeric assignment> -> . variable '=' expression
P19 @0-0 L0c0 :start -> . statements
R4:1 @0-1 L1c1 <numeric assignment> -> variable . '=' expression
R4:2 @0-2 L1c1-3 <numeric assignment> -> variable '=' . expression
P5 @2-2 L1c3 expression -> . expression
P6 @2-2 L1c3 expression -> . expression
P7 @2-2 L1c3 expression -> . expression
P8 @2-2 L1c3 expression -> . variable
P9 @2-2 L1c3 expression -> . string
P10 @2-2 L1c3 expression -> . 'string' '(' <numeric expression> ')'
P11 @2-2 L1c3 expression -> . expression '+' expression
F0 @0-3 L1c1-11 statements -> statement * .
P1 @3-3 L1c5-11 statement -> . assignment
P2 @3-3 L1c5-11 statement -> . <numeric assignment>
F2 @0-3 L1c1-11 statement -> <numeric assignment> .
P3 @3-3 L1c5-11 assignment -> . 'set' variable 'to' expression
P4 @3-3 L1c5-11 <numeric assignment> -> . variable '=' expression
F4 @0-3 L1c1-11 <numeric assignment> -> variable '=' expression .
F5 @2-3 L1c3-11 expression -> expression .
F6 @2-3 L1c3-11 expression -> expression .
F7 @2-3 L1c3-11 expression -> expression .
F8 @2-3 L1c3-11 expression -> variable .
R11:1 @2-3 L1c3-11 expression -> expression . '+' expression
F19 @0-3 L1c1-11 :start -> statements .
P7 @4-4 L1c13 expression -> . expression
P8 @4-4 L1c13 expression -> . variable
P9 @4-4 L1c13 expression -> . string
P10 @4-4 L1c13 expression -> . 'string' '(' <numeric expression> ')'
R11:2 @2-4 L1c3-13 expression -> expression '+' . expression
F0 @0-5 L1c1-16 statements -> statement * .
P1 @5-5 L1c15-16 statement -> . assignment
P2 @5-5 L1c15-16 statement -> . <numeric assignment>
F2 @0-5 L1c1-16 statement -> <numeric assignment> .
P3 @5-5 L1c15-16 assignment -> . 'set' variable 'to' expression
P4 @5-5 L1c15-16 <numeric assignment> -> . variable '=' expression
F4 @0-5 L1c1-16 <numeric assignment> -> variable '=' expression .
F5 @2-5 L1c3-16 expression -> expression .
F7 @4-5 L1c13-16 expression -> expression .
F8 @4-5 L1c13-16 expression -> variable .
R11:1 @2-5 L1c3-16 expression -> expression . '+' expression
F11 @2-5 L1c3-16 expression -> expression '+' expression .
F19 @0-5 L1c1-16 :start -> statements .
ngl this looks like a lot but it's so orderly bestie. every line has its little prefix (P, F, R), its position span like @0-0 or @2-4, its location span like L1c3-13, and then the rule with a dot somewhere in it โ sometimes a . up front, sometimes one in the middle, sometimes the * or the . sitting pretty at the end. it's giving "I am the receipt for exactly how this parse got built" ๐ง and honestly? obsessed.
๐ show_rules() output ๐
And this note โ super important, don't skip it:
this is the G1 portion of the show_rules() output at verbosity level 3. in ordinary, everyday work you'd just use verbosity level 1 (that's the default, bestie), but the more verbose output is what's shown here, because it's the thing that actually illustrates the example. so when you see all that extra detail, that's why โ it's the loud version, on purpose ๐
so yeah, verbosity 3 = the dramatic retelling, verbosity 1 = the chill default. the actual rules themselves are coming up next, so stay with me ๐ธ
okay so this right here is the raw rule dump in all its glory โจ โ like, the actual guts of how the grammar got built. it's giving "I printed every single rule and I am not sorry." and babes, we're keeping every single line, so buckle up ๐
first up? the G1 rules โ the "real" grammar, the main character of this whole operation ๐
G1 Rules:
G1 R0 statements ::= statement *
Symbol IDs: <16> ::= <17>
Internal symbols: <statements> ::= <statement>
G1 R1 statement ::= assignment
Symbol IDs: <17> ::= <18>
Internal symbols: <statement> ::= <assignment>
G1 R2 statement ::= <numeric assignment>
Symbol IDs: <17> ::= <19>
Internal symbols: <statement> ::= <numeric assignment>
G1 R3 assignment ::= 'set' variable 'to' expression
Symbol IDs: <18> ::= <1> <20> <2> <21>
Internal symbols: <assignment> ::= <[Lex-0]> <variable> <[Lex-1]> <expression>
G1 R4 <numeric assignment> ::= variable '=' <numeric expression>
Symbol IDs: <19> ::= <20> <3> <22>
Internal symbols: <numeric assignment> ::= <variable> <[Lex-2]> <numeric expression>
G1 R5 expression ::= expression
Internal rule top priority rule for <expression>
Symbol IDs: <21> ::= <10>
Internal symbols: <expression> ::= <expression[0]>
G1 R6 expression ::= expression
Internal rule for symbol <expression> priority transition from 0 to 1
Symbol IDs: <10> ::= <11>
Internal symbols: <expression[0]> ::= <expression[1]>
G1 R7 expression ::= expression
Internal rule for symbol <expression> priority transition from 1 to 2
Symbol IDs: <11> ::= <12>
Internal symbols: <expression[1]> ::= <expression[2]>
G1 R8 expression ::= variable
Symbol IDs: <12> ::= <20>
Internal symbols: <expression[2]> ::= <variable>
G1 R9 expression ::= string
Symbol IDs: <12> ::= <23>
Internal symbols: <expression[2]> ::= <string>
G1 R10 expression ::= 'string' '(' <numeric expression> ')'
Symbol IDs: <11> ::= <4> <5> <22> <6>
Internal symbols: <expression[1]> ::= <[Lex-3]> <[Lex-4]> <numeric expression> <[Lex-5]>
G1 R11 expression ::= expression '+' expression
Symbol IDs: <10> ::= <10> <7> <11>
Internal symbols: <expression[0]> ::= <expression[0]> <[Lex-6]> <expression[1]>
G1 R12 <numeric expression> ::= <numeric expression>
Internal rule top priority rule for <numeric expression>
Symbol IDs: <22> ::= <13>
Internal symbols: <numeric expression> ::= <numeric expression[0]>
G1 R13 <numeric expression> ::= <numeric expression>
Internal rule for symbol <numeric expression> priority transition from 0 to 1
Symbol IDs: <13> ::= <14>
Internal symbols: <numeric expression[0]> ::= <numeric expression[1]>
G1 R14 <numeric expression> ::= <numeric expression>
Internal rule for symbol <numeric expression> priority transition from 1 to 2
Symbol IDs: <14> ::= <15>
Internal symbols: <numeric expression[1]> ::= <numeric expression[2]>
G1 R15 <numeric expression> ::= variable
Symbol IDs: <15> ::= <20>
Internal symbols: <numeric expression[2]> ::= <variable>
G1 R16 <numeric expression> ::= number
Symbol IDs: <15> ::= <24>
Internal symbols: <numeric expression[2]> ::= <number>
G1 R17 <numeric expression> ::= <numeric expression> '*' <numeric expression>
Symbol IDs: <14> ::= <14> <8> <15>
Internal symbols: <numeric expression[1]> ::= <numeric expression[1]> <[Lex-7]> <numeric expression[2]>
G1 R18 <numeric expression> ::= <numeric expression> '+' <numeric expression>
Symbol IDs: <13> ::= <13> <9> <14>
Internal symbols: <numeric expression[0]> ::= <numeric expression[0]> <[Lex-8]> <numeric expression[1]>
G1 R19 :start ::= statements
Symbol IDs: <0> ::= <16>
Internal symbols: <[:start]> ::= <statements>
okay like โ R0 is just statements ::= statement *, which is the vibe: zero or more statements, we love an asterisk queen ๐
and then R1 and R2 give you the two flavors of statement, plain assignment and <numeric assignment>. slay, duality.
and then R5 through R7?? those are the internal rules โ literally labeled "Internal rule top priority rule for <expression>" and "Internal rule for symbol <expression> priority transition from 0 to 1" and "โฆfrom 1 to 2". that's the priority ladder, bestie, it's giving staircase energy ๐ช and the [0], [1], [2] suffixes on the internal symbols are how you can see it happening. same exact story for <numeric expression> in R12 through R14. not me getting emotional over priority transitions ๐ญ
also shhh notice R10 is 'string' '(' <numeric expression> ')' โ a whole function-call-looking thing โ while R11, R17 and R18 are the binary operators '+' and '*'. and R19 is :start ::= statements, the literal entry point, symbol ID <0> to <16>. main character behavior fr fr ๐
and now?? the Lex (L0) rules โจ this is the lexer doing its little dance, turning raw characters into tokens. every single one:
Lex (L0) Rules:
L0 R0 'set' ::= [s] [e] [t]
Internal rule for single-quoted string 'set'
Symbol IDs: <2> ::= <27> <21> <28>
Internal symbols: <[Lex-0]> ::= <[[s]]> <[[e]]> <[[t]]>
L0 R1 'to' ::= [t] [o]
Internal rule for single-quoted string 'to'
Symbol IDs: <3> ::= <28> <25>
Internal symbols: <[Lex-1]> ::= <[[t]]> <[[o]]>
L0 R2 '=' ::= [\=]
Internal rule for single-quoted string '='
Symbol IDs: <4> ::= <16>
Internal symbols: <[Lex-2]> ::= <[[\=]]>
L0 R3 'string' ::= [s] [t] [r] [i] [n] [g]
Internal rule for single-quoted string 'string'
Symbol IDs: <5> ::= <27> <28> <26> <23> <24> <22>
Internal symbols: <[Lex-3]> ::= <[[s]]> <[[t]]> <[[r]]> <[[i]]> <[[n]]> <[[g]]>
L0 R4 '(' ::= [\(]
Internal rule for single-quoted string '('
Symbol IDs: <6> ::= <12>
Internal symbols: <[Lex-4]> ::= <[[\(]]>
L0 R5 ')' ::= [\)]
Internal rule for single-quoted string ')'
Symbol IDs: <7> ::= <13>
Internal symbols: <[Lex-5]> ::= <[[\)]]>
L0 R6 '+' ::= [\+]
Internal rule for single-quoted string '+'
Symbol IDs: <8> ::= <15>
Internal symbols: <[Lex-6]> ::= <[[\+]]>
L0 R7 '*' ::= [\*]
Internal rule for single-quoted string '*'
Symbol IDs: <9> ::= <14>
Internal symbols: <[Lex-7]> ::= <[[\*]]>
L0 R8 '+' ::= [\+]
Internal rule for single-quoted string '+'
Symbol IDs: <10> ::= <15>
Internal symbols: <[Lex-8]> ::= <[[\+]]>
L0 R9 variable ::= [\w] +
Symbol IDs: <29> ::= <19>
Internal symbols: <variable> ::= <[[\w]]>
L0 R10 number ::= [\d] +
Symbol IDs: <30> ::= <17>
Internal symbols: <number> ::= <[[\d]]>
L0 R11 string ::= ['] <string contents> [']
Symbol IDs: <31> ::= <11> <32> <11>
Internal symbols: <string> ::= <[[']]> <string contents> <[[']]>
L0 R12 <string contents> ::= [^'\x{0A}\x{0B}\x{0C}\x{0D}\x{0085}\x{2028}\x{2029}] +
Symbol IDs: <32> ::= <20>
Internal symbols: <string contents> ::= <[[^'\x{0A}\x{0B}\x{0C}\x{0D}\x{0085}\x{2028}\x{2029}]]>
L0 R13 :discard ::= whitespace
Discard rule for <whitespace>
Symbol IDs: <0> ::= <33>
Internal symbols: <[:discard]> ::= <whitespace>
L0 R14 whitespace ::= [\s] +
Symbol IDs: <33> ::= <18>
Internal symbols: <whitespace> ::= <[[\s]]>
L0 R15 :start_lex ::= :discard
Internal lexical start rule for <[:discard]>
Symbol IDs: <1> ::= <0>
Internal symbols: <[:start_lex]> ::= <[:discard]>
L0 R16 :start_lex ::= 'set'
Internal lexical start rule for <[Lex-0]>
Symbol IDs: <1> ::= <2>
Internal symbols: <[:start_lex]> ::= <[Lex-0]>
L0 R17 :start_lex ::= 'to'
Internal lexical start rule for <[Lex-1]>
Symbol IDs: <1> ::= <3>
Internal symbols: <[:start_lex]> ::= <[Lex-1]>
L0 R18 :start_lex ::= '='
Internal lexical start rule for <[Lex-2]>
Symbol IDs: <1> ::= <4>
Internal symbols: <[:start_lex]> ::= <[Lex-2]>
L0 R19 :start_lex ::= 'string'
Internal lexical start rule for <[Lex-3]>
Symbol IDs: <1> ::= <5>
Internal symbols: <[:start_lex]> ::= <[Lex-3]>
L0 R20 :start_lex ::= '('
Internal lexical start rule for <[Lex-4]>
Symbol IDs: <1> ::= <6>
Internal symbols: <[:start_lex]> ::= <[Lex-4]>
L0 R21 :start_lex ::= ')'
Internal lexical start rule for <[Lex-5]>
Symbol IDs: <1> ::= <7>
Internal symbols: <[:start_lex]> ::= <[Lex-5]>
L0 R22 :start_lex ::= '+'
Internal lexical start rule for <[Lex-6]>
Symbol IDs: <1> ::= <8>
Internal symbols: <[:start_lex]> ::= <[Lex-6]>
L0 R23 :start_lex ::= '*'
Internal lexical start rule for <[Lex-7]>
Symbol IDs: <1> ::= <9>
Internal symbols: <[:start_lex]> ::= <[Lex-7]>
L0 R24 :start_lex ::= '+'
Internal lexical start rule for <[Lex-8]>
Symbol IDs: <1> ::= <10>
Internal symbols: <[:start_lex]> ::= <[Lex-8]>
L0 R25 :start_lex ::= number
Internal lexical start rule for <number>
Symbol IDs: <1> ::= <30>
Internal symbols: <[:start_lex]> ::= <number>
L0 R26 :start_lex ::= string
Internal lexical start rule for <string>
Symbol IDs: <1> ::= <31>
Internal symbols: <[:start_lex]> ::= <string>
L0 R27 :start_lex ::= variable
Internal lexical start rule for <variable>
Symbol IDs: <1> ::= <29>
Internal symbols: <[:start_lex]> ::= <variable>
okay so R0 through R8 are literally the character-by-character spelling of the keywords and symbols ๐ซถ like 'set' is [s] [e] [t], 'string' is [s] [t] [r] [i] [n] [g], and the punctuators get their own little escaped regex vibes โ [\(], [\)], [\+], [\*], plus a lone [\=] for '='. each one also has that "Internal rule for single-quoted string 'โฆ'" label. iconic.
โ ๏ธ and no cap โ look atL0 R6andL0 R8. they are both'+' ::= [\+], both labeled "Internal rule for single-quoted string '+'" โ but R6 is<[Lex-6]>and R8 is<[Lex-8]>, with symbol IDs<8> ::= <15>and<10> ::= <15>. two separate rules, same spelling. not a typo bestie, that's just how it's printed. ๐
R9 and R10 are the pretty ones tho: variable ::= [\w] + and number ::= [\d] + โ one-or-more word chars and one-or-more digits, we love a plus sign ๐
and R11/R12?? the string handling!! string ::= ['] <string contents> ['] wrapped in quotes, and <string contents> is a blob of "everything that is NOT a quote or any of these line-break characters": [^'\x{0A}\x{0B}\x{0C}\x{0D}\x{0085}\x{2028}\x{2029}] + โ that's the negated class, periodt. no newline sneaking into your strings ๐ธ
R13 is the discard rule, labeled "Discard rule for <whitespace>" โ :discard ::= whitespace โ and R14 is whitespace ::= [\s] +. so whitespace just gets yeeted ๐๏ธ and :discard has symbol IDs <0> ::= <33>.
then R15 through R27 are the lexical start rules, all hanging off :start_lex, each one labeled "Internal lexical start rule for โฆ" โ one for the discard, one for every single-quoted string token, and one each for <number>, <string> and <variable>. <[:start_lex]> ::= โฆ over and over. it's giving a tiny army all reporting to the same commander ๐ญ
ngl the sheer number of <1> ::= <something> lines?? obsessed. and we are so not done here โ the next bit keeps rolling, so don't go anywhere ๐
show_symbols() output ๐
okay bestie, here's the entire symbol table, fresh out the oven, every single line of it, nothing skipped, nothing hidden ๐
G1 Symbols:
G1 S0 :start -- Internal G1 start symbol
Internal name: <[:start]>
G1 S1 'set' -- Internal lexical symbol for "'set'"
/* terminal */
Internal name: <[Lex-0]>
SLIF name: 'set'
G1 S2 'to' -- Internal lexical symbol for "'to'"
/* terminal */
Internal name: <[Lex-1]>
SLIF name: 'to'
G1 S3 '=' -- Internal lexical symbol for "'='"
/* terminal */
Internal name: <[Lex-2]>
SLIF name: '='
G1 S4 'string' -- Internal lexical symbol for "'string'"
/* terminal */
Internal name: <[Lex-3]>
SLIF name: 'string'
G1 S5 '(' -- Internal lexical symbol for "'('"
/* terminal */
Internal name: <[Lex-4]>
SLIF name: '('
G1 S6 ')' -- Internal lexical symbol for "')'"
/* terminal */
Internal name: <[Lex-5]>
SLIF name: ')'
G1 S7 '+' -- Internal lexical symbol for "'+'"
/* terminal */
Internal name: <[Lex-6]>
SLIF name: '+'
G1 S8 '*' -- Internal lexical symbol for "'*'"
/* terminal */
Internal name: <[Lex-7]>
SLIF name: '*'
G1 S9 '+' -- Internal lexical symbol for "'+'"
/* terminal */
Internal name: <[Lex-8]>
SLIF name: '+'
G1 S10 expression -- <expression> at priority 0
Internal name: <expression[0]>
SLIF name: expression
G1 S11 expression -- <expression> at priority 1
Internal name: <expression[1]>
SLIF name: expression
G1 S12 expression -- <expression> at priority 2
Internal name: <expression[2]>
SLIF name: expression
G1 S13 <numeric expression> -- <numeric expression> at priority 0
Internal name: <numeric expression[0]>
SLIF name: numeric expression
G1 S14 <numeric expression> -- <numeric expression> at priority 1
Internal name: <numeric expression[1]>
SLIF name: numeric expression
G1 S15 <numeric expression> -- <numeric expression> at priority 2
Internal name: <numeric expression[2]>
SLIF name: numeric expression
G1 S16 statements
Internal name: <statements>
G1 S17 statement
Internal name: <statement>
G1 S18 assignment
Internal name: <assignment>
G1 S19 <numeric assignment>
Internal name: <numeric assignment>
G1 S20 variable
/* terminal */
Internal name: <variable>
G1 S21 expression
Internal name: <expression>
G1 S22 <numeric expression>
Internal name: <numeric expression>
G1 S23 string
/* terminal */
Internal name: <string>
G1 S24 number
/* terminal */
Internal name: <number>
likeโฆ S24 symbols?? the girl has range ๐
progress() outputs ๐ฆ
These section contains samples of the output of the progress() method โ the progress reports in their "raw" format. The output is shown in Data::Dumper format, with Data::Dumper::Indent set to 0 and Data::Dumper::Terse set to 1. โจ
The punctuation goes inside the quotes up there. that's the original's energy, we honour it. ๐
The Data::Dumper output from progress() at G1 location 0:
[[0,0,0],[1,0,0],[2,0,0],[3,0,0],[4,0,0],[19,0,0]]
The Data::Dumper output from progress() at G1 location 1:
[[4,1,0]]
The Data::Dumper output from progress() at location 2:
[[5,0,2],[6,0,2],[7,0,2],[8,0,2],[9,0,2],[10,0,2],[11,0,2],[4,2,0]]
The Data::Dumper output from progress() for the latest Earley set:
The default progress() output is for the latest Earley set. Here is the progress() output for the latest Earley set. ๐
[[0,-1,0],[2,-1,0],[4,-1,0],[5,-1,2],[7,-1,4],[8,-1,4],[11,-1,2],[19,-1,0],[1,0,5],[2,0,5],[3,0,5],[4,0,5],[11,1,2]]
Copyright and License ๐
Copyright 2022 Jeffrey Kegler
This file is part of Marpa::R2. Marpa::R2 is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Marpa::R2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with Marpa::R2. If not, see http://www.gnu.org/licenses/.
and that's a wrap bestie, the whole page, every last crumb ๐
this little saga rode out of perl v5.42.3 and landed on 2026-08-20, signed off as Marpa:...rogress(3pm) โ the <rainbow>Marpa::R2::Progress</rainbow> era, documented and divine ๐ซถ periodt ๐
โจ
๐ฆ where this page comes from
- ubuntu stonking: libmarpa-r2-perl 12.000000-1build1 ยท LGPL-3+~Marpa
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms