Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F92644604
PhutilParserGenerator.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Fri, Nov 22, 08:26
Size
24 KB
Mime Type
text/x-php
Expires
Sun, Nov 24, 08:26 (1 d, 23 h)
Engine
blob
Format
Raw Data
Handle
22466447
Attached To
rPHU libphutil
PhutilParserGenerator.php
View Options
<?php
/**
* Simple LR(1) parser generator. Generally, build a parser by setting terminals
* and rules, then calling @{method:processGrammar}. For example, here is a
* simple grammar which accepts one or more "a" followed by exactly one "b":
*
* $parser = id(new PhutilParserGenerator())
* ->setTerminals(array('a', 'b'))
* ->setStartRule('S')
* ->setRules(
* array(
* 'S' => 'A b',
* 'A' => array(
* 'A a',
* 'a',
* )))
* ->processGrammar();
*
* To actually parse token streams, use @{method:parseTokens}.
*
* $tokens = get_tokens(); // Usually from PhutilLexer
* $callback = 'some_callback';
* $tree = $parser->parseTokens($tokens, $callback);
*
* The callback is invoked when a grammar rule matches. It should have this
* signature:
*
* function parser_callback($rule, $production, array $tokens) {
* // ...
* }
*
* The `$rule` is the matching rule; the `$production` is the matching
* production, and `$tokens` is the matching tokens (for terminal rules) or the
* return value of previous parse callbacks (for nonterminal rules).
*
* You should either return a result of evaluation, or some sort of abstract
* representation of the parse tree (this is more likely to be useful for more
* complex grammars).
*
* NOTE: This class generates LR(1) parsers, which perform less-than-optimally
* on large grammars. Worse, it is written in PHP. It is suitable only for
* very simple grammars with few states.
*
* NOTE: These parsers silently resolve reduce/reduce conflicts by choosing the
* first reduction, and silently resolve shift/reduce conflicts by shifting.
* These are the same rules used by Yacc, but are implicit.
*
* @task rules Grammar Rules
* @task rvalidation Rule Validation
* @task first Computing First()
* @task tables Computing Action and Goto Tables
* @task inspect Inspecting Generator State
*/
final
class
PhutilParserGenerator
{
private
$terminals
;
private
$rules
;
private
$startRule
=
'start'
;
private
$sets
=
array
();
private
$successor
=
array
();
private
$setHashes
=
array
();
private
$actionTable
;
private
$gotoTable
;
private
$rulesValidated
=
false
;
private
$eofSymbol
;
private
$initSymbol
;
private
$epsilonSymbol
;
private
$endSymbol
;
private
$firstTable
;
public
function
processGrammar
()
{
$this
->
validateRules
();
$this
->
buildFirstTable
();
$init
=
$this
->
getInitSymbol
();
$eof
=
$this
->
getEOFSymbol
();
$end
=
$this
->
getEndSymbol
();
$this
->
rules
[
$init
]
=
array
(
array
(
$this
->
startRule
,
$end
),
);
list
(
$is_new
,
$state
)
=
$this
->
addState
(
array
(
array
(
$this
->
getInitSymbol
(),
0
,
0
,
$eof
),
));
$this
->
buildSuccessors
(
$state
);
$this
->
buildTables
();
return
$this
;
}
/* -( Grammar Rules )------------------------------------------------------ */
public
function
setTerminals
(
array
$terminals
)
{
$this
->
terminals
=
array_fill_keys
(
$terminals
,
true
);
return
$this
;
}
public
function
setRules
(
array
$rules
)
{
$this
->
rules
=
$rules
;
return
$this
;
}
public
function
setStartRule
(
$rule_name
)
{
$this
->
startRule
=
$rule_name
;
return
$this
;
}
public
function
getStartRule
()
{
return
$this
->
startRule
;
}
public
function
getEOFSymbol
()
{
if
(
$this
->
eofSymbol
===
null
)
{
throw
new
Exception
(
'Call processGrammar() before getEOFSymbol()!'
);
}
return
$this
->
eofSymbol
;
}
public
function
getInitSymbol
()
{
if
(
$this
->
initSymbol
===
null
)
{
throw
new
Exception
(
'Call processGrammar() before getInitSymbol()!'
);
}
return
$this
->
initSymbol
;
}
public
function
getEpsilonSymbol
()
{
if
(
$this
->
epsilonSymbol
===
null
)
{
throw
new
Exception
(
'Call processGrammar() before getEpsilonSymbol()!'
);
}
return
$this
->
epsilonSymbol
;
}
public
function
getEndSymbol
()
{
if
(
$this
->
endSymbol
===
null
)
{
throw
new
Exception
(
'Call processGrammar() before getEndSymbol()!'
);
}
return
$this
->
endSymbol
;
}
public
function
isTerminal
(
$symbol
)
{
return
isset
(
$this
->
terminals
[
$symbol
]);
}
public
function
isRule
(
$symbol
)
{
return
isset
(
$this
->
rules
[
$symbol
]);
}
/* -( Rule Validation )---------------------------------------------------- */
/**
* Perform a battery of tests on the provided rules to detect problems which
* would prevent us from generating a parser.
*
* @return void
* @task rvalidation
*/
private
function
validateRules
()
{
// Rules must be specified in the right format.
$this
->
parseRules
();
// Rules must contain only known symbols.
$this
->
validateRuleSymbols
();
// The start rule must exist and be valid.
$this
->
validateStartRule
();
// Now, we select printable names for special symbols (EOF, epsilon, etc)
// that don't conflict with any symbols in the grammar.
$this
->
chooseSpecialSymbols
();
// Make sure every terminal can be reached by some rule.
$this
->
validateAllTerminalsReachable
();
// Make sure every rule can be reached.
$this
->
validateAllRulesReachable
();
// Make sure every rule has some valid reduction.
$this
->
validateAllRulesReducible
();
$this
->
rulesValidated
=
true
;
}
/**
* @task rvalidation
*/
private
function
parseRules
()
{
foreach
(
$this
->
rules
as
$rule_name
=>
$rule_variants
)
{
if
(!
is_array
(
$rule_variants
))
{
$rule_variants
=
array
(
$rule_variants
);
$this
->
rules
[
$rule_name
]
=
$rule_variants
;
}
foreach
(
$rule_variants
as
$vkey
=>
$variant
)
{
if
(
$variant
===
null
)
{
$variant
=
array
(
null
);
}
else
if
(!
is_array
(
$variant
))
{
$variant
=
preg_split
(
'/
\s
+/'
,
$variant
);
}
else
{
foreach
(
$variant
as
$symbol
)
{
if
((
$symbol
===
null
)
&&
count
(
$variant
)
>
1
)
{
throw
new
PhutilInvalidRuleParserGeneratorException
(
"Rule '{$rule_name}' contains a production '{$vkey}' which "
.
"is nonempty but has a null in it. A rule with other symbols "
.
"may not contain null."
);
}
}
}
$this
->
rules
[
$rule_name
][
$vkey
]
=
array_values
(
$variant
);
}
}
}
/**
* @task rvalidation
*/
private
function
validateRuleSymbols
()
{
foreach
(
$this
->
rules
as
$rule
=>
$productions
)
{
foreach
(
$productions
as
$production_name
=>
$production
)
{
foreach
(
$production
as
$symbol
)
{
if
(
$symbol
===
null
)
{
continue
;
}
if
(
$this
->
isTerminal
(
$symbol
))
{
continue
;
}
if
(
$this
->
isRule
(
$symbol
))
{
continue
;
}
$production_string
=
implode
(
' '
,
$production
);
throw
new
PhutilUnknownSymbolParserGeneratorException
(
"Symbol '{$symbol}' in production '{$production_name}' "
.
"('{$production_string}') of rule '{$rule}' does not name a rule "
.
"or terminal. Did you misspell a symbol, fail to specify a "
.
"terminal, or forget a rule?"
);
}
}
}
}
/**
* @task rvalidation
*/
private
function
validateStartRule
()
{
$start_rule
=
$this
->
getStartRule
();
if
(!
$this
->
isRule
(
$start_rule
))
{
throw
new
PhutilUnknownSymbolParserGeneratorException
(
"Start rule '{$start_rule}' does not appear in the rules for the "
.
"grammar. Use setStartRule() to choose a different start rule, or "
.
"add a rule named '{$start_rule}'."
);
}
}
/**
* @task rvalidation
*/
private
function
chooseSpecialSymbols
()
{
$special
=
array
(
'eofSymbol'
=>
'(end-of-file)'
,
'epsilonSymbol'
=>
'(epsilon)'
,
'initSymbol'
=>
'(init)'
,
'endSymbol'
=>
'(end)'
,
);
foreach
(
$special
as
$key
=>
$value
)
{
while
(
$this
->
isRule
(
$value
)
||
$this
->
isTerminal
(
$value
))
{
$value
.=
"'"
;
}
$special
[
$key
]
=
$value
;
}
$this
->
eofSymbol
=
$special
[
'eofSymbol'
];
$this
->
epsilonSymbol
=
$special
[
'epsilonSymbol'
];
$this
->
initSymbol
=
$special
[
'initSymbol'
];
$this
->
endSymbol
=
$special
[
'endSymbol'
];
foreach
(
$this
->
rules
as
$rule
=>
$productions
)
{
foreach
(
$productions
as
$production_name
=>
$production
)
{
foreach
(
$production
as
$key
=>
$symbol
)
{
if
(
$symbol
===
null
)
{
$this
->
rules
[
$rule
][
$production_name
][
$key
]
=
$this
->
epsilonSymbol
;
}
}
$this
->
rules
[
$rule
][
$production_name
][]
=
$this
->
endSymbol
;
}
}
$this
->
terminals
[
$this
->
getEOFSymbol
()]
=
true
;
}
/**
* @task rvalidation
*/
private
function
validateAllTerminalsReachable
()
{
$seen
=
array
();
foreach
(
$this
->
rules
as
$rule
=>
$productions
)
{
foreach
(
$productions
as
$production
)
{
foreach
(
$production
as
$symbol
)
{
$seen
[
$symbol
]
=
true
;
}
}
}
$missing
=
array_diff_key
(
$this
->
terminals
,
$seen
);
unset
(
$missing
[
$this
->
getEOFSymbol
()]);
if
(
$missing
)
{
$missing_terminals
=
array_keys
(
$missing
);
$missing_terminals
=
implode
(
', '
,
$missing_terminals
);
throw
new
PhutilUnreachableTerminalParserGeneratorException
(
'Some terminals do not appear in any rule: '
.
$missing_terminals
);
}
}
/**
* @task rvalidation
*/
private
function
validateAllRulesReachable
()
{
$stack
=
array
();
$reachable
=
$this
->
computeReachableRules
(
$this
->
getStartRule
(),
$stack
);
$missing
=
array_diff_key
(
$this
->
rules
,
$reachable
);
unset
(
$missing
[
$this
->
getStartRule
()]);
if
(
$missing
)
{
$missing_rules
=
array_keys
(
$missing
);
$missing_rules
=
implode
(
', '
,
$missing_rules
);
throw
new
PhutilUnreachableRuleParserGeneratorException
(
'Some rules can never be reached from any production: '
.
$missing_rules
);
}
}
/**
* @task rvalidation
*/
private
function
computeReachableRules
(
$rule
,
array
&
$stack
)
{
if
(
isset
(
$stack
[
$rule
]))
{
return
$stack
[
$rule
];
}
$stack
[
$rule
]
=
array
();
foreach
(
$this
->
rules
[
$rule
]
as
$production
)
{
foreach
(
$production
as
$symbol
)
{
if
(
$this
->
isRule
(
$symbol
))
{
$stack
[
$rule
][
$symbol
]
=
true
;
$stack
[
$rule
]
+=
$this
->
computeReachableRules
(
$symbol
,
$stack
);
}
}
}
return
$stack
[
$rule
];
}
/**
* @task rvalidation
*/
private
function
validateAllRulesReducible
()
{
$reducible
=
array
();
foreach
(
$this
->
rules
as
$rule
=>
$productions
)
{
if
(!
$this
->
isRuleReducible
(
$rule
,
$reducible
))
{
throw
new
PhutilIrreducibleRuleParserGeneratorException
(
"Rule '{$rule}' can never be reduced: it recurses indefinitely "
.
"and reaches no production of terminals."
);
}
}
}
/**
* @task rvalidation
*/
private
function
isRuleReducible
(
$rule
,
array
&
$reducible
)
{
if
(
isset
(
$reducible
[
$rule
]))
{
return
$reducible
[
$rule
];
}
// Set this ahead of time so we don't end up in an infinite loop if
// rules recurse. We'll overwrite it if we find a reduction.
$reducible
[
$rule
]
=
false
;
$reducible
[
$rule
]
=
$this
->
computeRuleReducible
(
$rule
,
$reducible
);
return
$reducible
[
$rule
];
}
/**
* @task rvalidation
*/
private
function
computeRuleReducible
(
$rule
,
array
&
$reducible
)
{
$epsilon
=
$this
->
getEpsilonSymbol
();
$end
=
$this
->
getEndSymbol
();
$productions
=
$this
->
rules
[
$rule
];
// In the first pass, try to find a trivially reducible production, e.g. one
// with epsilon or only terminals. Also, remove recursive productions (those
// which directly involve the rule itself) because we know we won't be able
// to reduce them. If we're lucky, this will allow us to determine that the
// rule is reducible without recursion. For example, we can immediately
// reduce these productions:
//
// R -> a
// R -> b c d
// R -> (epsilon)
//
// We can never reduce these productions:
//
// R -> R
// R -> a R b
//
// We might be able to reduce these productions, but they aren't as cheap
// or easy to figure out, since we need to first determine if other rules
// can be reduced:
//
// R -> X Y
// R -> X a
//
// If we find a reduction, we return immediately.
foreach
(
$productions
as
$key
=>
$production
)
{
$has_only_terminals
=
true
;
foreach
(
$production
as
$symbol
)
{
if
(
$symbol
==
$end
)
{
break
;
}
else
if
(
$symbol
==
$epsilon
)
{
// The rule contains an epsilon production, which can always reduce
// it.
return
true
;
}
else
if
(
$symbol
==
$rule
)
{
// The rule contains itself; this production is never reducible. We
// must find another reducible production.
unset
(
$productions
[
$key
]);
continue
2
;
}
else
if
(
$this
->
isTerminal
(
$symbol
))
{
// This is a terminal; keep looking. We'll be able to reduce the
// production if it contains only terminals.
continue
;
}
else
{
// This is a rule, so we can't trivially reduce it. We'll keep it
// for the next round if we can't find any trivial reductions.
$has_only_terminals
=
false
;
break
;
}
}
if
(
$has_only_terminals
)
{
return
true
;
}
}
// If we have no productions left, this rule can't be reduced.
if
(
empty
(
$productions
))
{
return
false
;
}
// We have remaining productions which include other rules. Look for a
// nontrivial reduction. For example:
//
// R -> X Y
// X -> x
// Y -> y
//
// In this case, X and Y are both reducible, so "X Y" is reducible and thus
// R is reducible.
foreach
(
$productions
as
$production
)
{
$can_reduce
=
true
;
foreach
(
$production
as
$symbol
)
{
// NOTE: We don't need to check for epsilon here, because we would
// already have determined the rule was reducible if we had an epsilon
// production.
if
(
$symbol
==
$end
)
{
break
;
}
else
if
(
$this
->
isTerminal
(
$symbol
))
{
continue
;
}
else
if
(!
$this
->
isRuleReducible
(
$symbol
,
$reducible
))
{
$can_reduce
=
false
;
break
;
}
}
if
(
$can_reduce
)
{
// The production contained only terminals and reducible rules, so it
// is reducible. We're good and don't need to examine remaining
// productions.
return
true
;
}
}
// We didn't find any reducible productions.
return
false
;
}
/* -( Computing First() )-------------------------------------------------- */
private
function
buildFirstTable
()
{
$this
->
firstTable
=
array
();
foreach
(
$this
->
rules
as
$rule
=>
$productions
)
{
$this
->
buildRuleFirst
(
$rule
);
}
}
private
function
buildRuleFirst
(
$rule
)
{
if
(
isset
(
$this
->
firstTable
[
$rule
]))
{
return
$this
->
firstTable
[
$rule
];
}
$this
->
firstTable
[
$rule
]
=
array
();
$productions
=
$this
->
rules
[
$rule
];
foreach
(
$productions
as
$key
=>
$production
)
{
$this
->
firstTable
[
$rule
]
+=
$this
->
getFirstForProduction
(
$production
);
}
return
$this
->
firstTable
[
$rule
];
}
private
function
getFirstForProduction
(
array
$production
)
{
$set
=
array
();
$end
=
$this
->
getEndSymbol
();
$epsilon
=
$this
->
getEpsilonSymbol
();
$eof
=
$this
->
getEOFSymbol
();
$accept_epsilon
=
true
;
foreach
(
$production
as
$symbol
)
{
if
(
$symbol
===
$end
)
{
break
;
}
else
if
(
$symbol
===
$epsilon
)
{
break
;
}
else
if
(
$this
->
isTerminal
(
$symbol
))
{
$set
[
$symbol
]
=
true
;
$accept_epsilon
=
false
;
break
;
}
else
{
$symbol_set
=
$this
->
buildRuleFirst
(
$symbol
);
$has_epsilon
=
isset
(
$symbol_set
[
$epsilon
]);
unset
(
$symbol_set
[
$epsilon
]);
$set
+=
$symbol_set
;
if
(!
$has_epsilon
)
{
$accept_epsilon
=
false
;
break
;
}
}
}
if
(
$accept_epsilon
)
{
$set
[
$epsilon
]
=
true
;
}
return
$set
;
}
/* -( Computing States )--------------------------------------------------- */
private
function
addState
(
array
$set
)
{
$seen
=
array
();
foreach
(
$set
as
$item
)
{
$seen
[
$item
[
0
]][
$item
[
1
]][
$item
[
2
]][
$item
[
3
]]
=
true
;
}
$end
=
$this
->
getEndSymbol
();
$epsilon
=
$this
->
getEpsilonSymbol
();
for
(
$ii
=
0
;
$ii
<
count
(
$set
);
$ii
++)
{
$item
=
$set
[
$ii
];
$production
=
$this
->
rules
[
$item
[
0
]][
$item
[
1
]];
$next
=
$production
[
$item
[
2
]];
if
(
$this
->
isTerminal
(
$next
))
{
continue
;
}
else
if
(
$next
===
$epsilon
)
{
continue
;
}
else
if
(
$next
===
$end
)
{
continue
;
}
$v
=
array_slice
(
$production
,
$item
[
2
]
+
1
,
-
1
);
$v
[]
=
$item
[
3
];
$v
[]
=
$end
;
$firsts
=
$this
->
getFirstForProduction
(
$v
);
foreach
(
$firsts
as
$nfirst
=>
$ignored
)
{
if
(!
$this
->
isTerminal
(
$nfirst
))
{
unset
(
$firsts
[
$nfirst
]);
}
}
foreach
(
$this
->
rules
[
$next
]
as
$pkey
=>
$nproduction
)
{
foreach
(
$firsts
as
$nfirst
=>
$ignored
)
{
if
(
isset
(
$seen
[
$next
][
$pkey
][
0
][
$nfirst
]))
{
continue
;
}
$set
[]
=
array
(
$next
,
$pkey
,
0
,
$nfirst
);
$seen
[
$next
][
$pkey
][
0
][
$nfirst
]
=
true
;
}
}
}
$hash
=
$this
->
hashSet
(
$set
);
if
(
isset
(
$this
->
setHashes
[
$hash
]))
{
return
array
(
false
,
$this
->
setHashes
[
$hash
]);
}
$this
->
states
[]
=
$set
;
$state
=
last_key
(
$this
->
states
);
$this
->
setHashes
[
$hash
]
=
$state
;
return
array
(
true
,
$state
);
}
private
function
buildSuccessors
(
$start_state
)
{
$end
=
$this
->
getEndSymbol
();
$nexts
=
array
();
foreach
(
$this
->
states
[
$start_state
]
as
$item
)
{
$next
=
$this
->
rules
[
$item
[
0
]][
$item
[
1
]][
$item
[
2
]];
if
(
$next
===
$end
)
{
continue
;
}
$nexts
[
$next
][]
=
array
(
$item
[
0
],
$item
[
1
],
$item
[
2
]
+
1
,
$item
[
3
],
);
}
foreach
(
$nexts
as
$next
=>
$items
)
{
list
(
$is_new
,
$state
)
=
$this
->
addState
(
$items
);
$this
->
successor
[
$start_state
][
$next
]
=
$state
;
if
(
$is_new
)
{
$this
->
buildSuccessors
(
$state
);
}
}
}
private
function
hashSet
(
array
$set
)
{
foreach
(
$set
as
$k
=>
$item
)
{
$set
[
$k
]
=
implode
(
"
\0
"
,
$item
);
}
sort
(
$set
);
$set
=
implode
(
"
\1
"
,
$set
);
return
md5
(
$set
);
}
private
function
buildTables
()
{
$action
=
array
();
$goto
=
array
();
$end
=
$this
->
getEndSymbol
();
$eof
=
$this
->
getEOFSymbol
();
$init
=
$this
->
getInitSymbol
();
foreach
(
$this
->
states
as
$state
=>
$items
)
{
$shift
=
array
();
$reduce
=
array
();
$accept
=
false
;
foreach
(
$items
as
$item
)
{
$next
=
$this
->
rules
[
$item
[
0
]][
$item
[
1
]][
$item
[
2
]];
if
(
$next
==
$end
)
{
if
(
$item
[
0
]
!==
$init
)
{
$reduce
[
$item
[
3
]][]
=
$item
;
}
else
if
(
$item
[
0
]
===
$init
&&
$item
[
3
]
===
$eof
)
{
$accept
=
$item
;
}
}
else
if
(
$this
->
isTerminal
(
$next
))
{
$shift
[
$next
]
=
$item
;
}
else
{
$goto
[
$state
][
$next
]
=
$this
->
successor
[
$state
][
$next
];
}
}
foreach
(
$reduce
as
$next
=>
$reductions
)
{
if
(
count
(
$reductions
)
>
1
)
{
$ways
=
array
();
foreach
(
$reductions
as
$reduction
)
{
$ways
[]
=
"{$reduction[0]}/{$reduction[1]}"
;
}
$ways
=
implode
(
'; '
,
$ways
);
// TODO: As below, we should have more explicit handling of
// reduce/reduce conflicts. For now, just pick the first one.
if
(
false
)
{
throw
new
Exception
(
"Reduce/reduce conflict: from state '{$state}', when a "
.
"'{$next}' is encountered, it may be reduced in multiple "
.
"ways: {$ways}"
);
}
}
$reduce
[
$next
]
=
head
(
$reductions
);
}
$srconflicts
=
array_intersect_key
(
$shift
,
$reduce
);
foreach
(
$srconflicts
as
$next
=>
$ignored
)
{
// TODO: We should probably have better or more explicit handling of
// shift/reduce conflicts. For now, we just shift.
if
(
false
)
{
$what
=
$reduce
[
$next
][
0
];
throw
new
Exception
(
"Shift/reduce conflict: from state '{$state}', when a '{$next}' "
.
"is encountered, shifting conflicts with reducing '{$what}'."
);
}
else
{
// Resolve the shift/reduce by shifting.
$reduce
=
array
();
}
}
if
(
$accept
&&
isset
(
$shift
[
$eof
]))
{
throw
new
Exception
(
'Accept/shift conflict!'
);
}
if
(
$accept
&&
isset
(
$reduce
[
$eof
]))
{
throw
new
Exception
(
'Accept/reduce conflict!'
);
}
foreach
(
$reduce
as
$next
=>
$item
)
{
$action
[
$state
][
$next
]
=
array
(
'R'
,
array
(
$item
[
0
],
$item
[
1
],
count
(
$this
->
rules
[
$item
[
0
]][
$item
[
1
]])
-
1
),
);
}
foreach
(
$shift
as
$next
=>
$item
)
{
$action
[
$state
][
$next
]
=
array
(
'S'
,
$this
->
successor
[
$state
][
$next
],
);
}
if
(
$accept
)
{
$action
[
$state
][
$eof
]
=
array
(
'A'
);
}
}
$this
->
actionTable
=
$action
;
$this
->
gotoTable
=
$goto
;
}
public
function
generateParserFunction
(
$name
)
{
$out
=
array
();
$out
[]
=
'function '
.
$name
.
'(array $tokens, $callback) {'
;
$out
[]
=
' return PhutilParserGenerator::parseTokensWithTables('
;
$out
[]
=
' '
.
$this
->
formatAndIndent
(
$this
->
actionTable
,
4
).
','
;
$out
[]
=
' '
.
$this
->
formatAndIndent
(
$this
->
gotoTable
,
4
).
','
;
$out
[]
=
' '
.
$this
->
formatAndIndent
(
$this
->
getEOFSymbol
(),
4
).
','
;
$out
[]
=
' $tokens,'
;
$out
[]
=
' $callback);'
;
$out
[]
=
'}'
;
return
implode
(
"
\n
"
,
$out
);
}
private
function
formatAndIndent
(
$var
,
$depth
)
{
$var
=
phutil_var_export
(
$var
);
$var
=
str_replace
(
"
\n
"
,
"
\n
"
.
str_repeat
(
' '
,
$depth
),
$var
);
return
$var
;
}
public
function
parseTokens
(
array
$tokens
,
$callback
)
{
return
self
::
parseTokensWithTables
(
$this
->
actionTable
,
$this
->
gotoTable
,
$this
->
getEOFSymbol
(),
$tokens
,
$callback
);
}
public
static
function
parseTokensWithTables
(
$action_table
,
$goto_table
,
$eof_symbol
,
array
$tokens
,
$callback
)
{
$state_stack
=
array
(
0
);
$token_stack
=
array
();
$tokens
=
array_reverse
(
$tokens
);
while
(
true
)
{
$state
=
end
(
$state_stack
);
if
(
empty
(
$tokens
))
{
$next
=
$eof_symbol
;
}
else
{
$next_token
=
end
(
$tokens
);
$next
=
$next_token
[
0
];
}
if
(!
isset
(
$action_table
[
$state
][
$next
]))
{
$expected
=
implode
(
', '
,
array_keys
(
$action_table
[
$state
]));
throw
new
Exception
(
"Unexpected '{$next}' in state {$state}! Expected: "
.
$expected
);
}
$action
=
$action_table
[
$state
][
$next
];
switch
(
$action
[
0
])
{
case
'S'
:
$state_stack
[]
=
$action
[
1
];
$token_stack
[]
=
array_pop
(
$tokens
);
break
;
case
'R'
:
$r_rule
=
$action
[
1
][
0
];
$r_prod
=
$action
[
1
][
1
];
$r_size
=
$action
[
1
][
2
];
$token_v
=
array
();
while
(
$r_size
--)
{
$token_v
[]
=
array_pop
(
$token_stack
);
array_pop
(
$state_stack
);
}
$token_v
=
array_reverse
(
$token_v
);
$token_stack
[]
=
call_user_func_array
(
$callback
,
array
(
$r_rule
,
$r_prod
,
$token_v
));
$goto
=
$goto_table
[
end
(
$state_stack
)][
$r_rule
];
$state_stack
[]
=
$goto
;
break
;
case
'A'
:
break
2
;
}
}
return
head
(
$token_stack
);
}
/* -( Inspecting Generator State )----------------------------------------- */
/**
* @task inspect
*/
public
function
inspectRules
()
{
if
(!
$this
->
rulesValidated
)
{
throw
new
Exception
(
'Call processGrammar() before inspectRules()!'
);
}
return
$this
->
rules
;
}
/**
* @task inspect
*/
public
function
inspectFirstTable
()
{
if
(
$this
->
firstTable
===
null
)
{
throw
new
Exception
(
'Call processGrammar() before inspectFirstTable()!'
);
}
return
$this
->
firstTable
;
}
}
Event Timeline
Log In to Comment