@@ -146,6 +146,16 @@ public function __construct(Lexer $lexer)
146146 public function parse (string $ code ): Program
147147 {
148148 $ this ->tokens = $ this ->lexer ->tokenize ($ code );
149+ $ numTokens = count ($ this ->tokens );
150+
151+ // add sentinel token
152+ if ($ numTokens === 0 ) {
153+ $ this ->tokens [] = new Token ('EOF ' , "\0" , 0 );
154+ } else {
155+ $ lastToken = $ this ->tokens [$ numTokens - 1 ];
156+ $ this ->tokens [] = new Token ('EOF ' , "\0" , $ lastToken ->line );
157+ }
158+
149159 $ result = $ this ->doParse ();
150160
151161 // Clear out some of the interior state, so we don't hold onto unnecessary
@@ -170,7 +180,7 @@ protected function doParse(): Program
170180 {
171181 // We start off with no lookahead-token
172182 $ symbol = self ::SYMBOL_NONE ;
173- $ token = null ;
183+ $ tokenValue = null ;
174184 $ this ->tokenPos = -1 ;
175185
176186 // Keep stack of start and end attributes
@@ -223,10 +233,11 @@ protected function doParse(): Program
223233 */
224234 if ($ action > 0 ) {
225235 /* shift */
236+ //$this->traceShift($symbol);
226237
227238 ++$ stackPos ;
228239 $ stateStack [$ stackPos ] = $ state = $ action ;
229- $ this ->semStack [$ stackPos ] = $ token ;
240+ $ this ->semStack [$ stackPos ] = $ tokenValue ;
230241 $ this ->tokenStartStack [$ stackPos ] = $ this ->tokenPos ;
231242 $ this ->tokenEndStack [$ stackPos ] = $ this ->tokenPos ;
232243 $ symbol = self ::SYMBOL_NONE ;
@@ -252,10 +263,12 @@ protected function doParse(): Program
252263 for (;;) {
253264 if ($ rule === 0 ) {
254265 /* accept */
266+ //$this->traceAccept();
255267 return $ this ->semValue ;
256268 }
257269 if ($ rule !== $ this ->unexpectedTokenRule ) {
258270 /* reduce */
271+ //$this->traceReduce($rule);
259272
260273 $ ruleLength = $ this ->ruleToLength [$ rule ];
261274 $ callback = $ this ->reduceCallbacks [$ rule ];
@@ -310,8 +323,10 @@ protected function doParse(): Program
310323 throw new \Exception ('Parse error: failed to recover from error ' );
311324 }
312325 $ state = $ stateStack [--$ stackPos ];
326+ //$this->tracePop($state);
313327 }
314328
329+ //$this->traceShift($this->errorSymbol);
315330 ++$ stackPos ;
316331 $ stateStack [$ stackPos ] = $ state = ($ action ?? 0 );
317332
@@ -327,6 +342,7 @@ protected function doParse(): Program
327342 throw new \Exception ('Parse error: reached EOF without recovering from error ' );
328343 }
329344
345+ //$this->traceDiscard($symbol);
330346 $ symbol = self ::SYMBOL_NONE ;
331347 break 2 ;
332348 }
@@ -399,7 +415,7 @@ protected function getExpectedTokens(int $state): array {
399415 * @return array<string, int>
400416 */
401417 protected function createTokenMap (): array {
402- $ tokenMap = [];
418+ $ tokenMap = [' EOF ' => 0 ]; // for sentinel token
403419
404420 foreach ($ this ->symbolToName as $ name ) {
405421 if ($ name === 'EOF ' || $ name === 'error ' ) {
@@ -422,6 +438,40 @@ protected function createTokenMap(): array {
422438 return $ fullTokenMap ;
423439 }
424440
441+ /*
442+ * Tracing functions used for debugging the parser.
443+ */
444+
445+ /*
446+ protected function traceNewState($state, $symbol): void {
447+ echo '% State ' . $state
448+ . ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n";
449+ }
450+
451+ protected function traceRead($symbol): void {
452+ echo '% Reading ' . $this->symbolToName[$symbol] . "\n";
453+ }
454+
455+ protected function traceShift($symbol): void {
456+ echo '% Shift ' . $this->symbolToName[$symbol] . "\n";
457+ }
458+
459+ protected function traceAccept(): void {
460+ echo "% Accepted.\n";
461+ }
462+
463+ protected function traceReduce($n): void {
464+ echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n";
465+ }
466+
467+ protected function tracePop($state): void {
468+ echo '% Recovering, uncovered state ' . $state . "\n";
469+ }
470+
471+ protected function traceDiscard($symbol): void {
472+ echo '% Discard ' . $this->symbolToName[$symbol] . "\n";
473+ }*/
474+
425475 /*
426476 * Helper functions invoked by semantic actions
427477 * Based on https://github.com/handlebars-lang/handlebars-parser/blob/master/lib/helpers.js
@@ -447,31 +497,41 @@ protected function locInfo(int $tokenStartPos, int $tokenEndPos): SourceLocation
447497 return new SourceLocation ($ source , new Position ($ startToken ->line , -1 ), new Position ($ endToken ->line , -1 ));
448498 }
449499
450- protected function id (Token $ token ): string
500+ protected function id (string $ token ): string
451501 {
452- if (preg_match ('/^ \\[.*]$/ ' , $ token-> text )) {
453- return substr ($ token-> text , 1 , -1 );
502+ if (preg_match ('/^ \\[.*]$/ ' , $ token )) {
503+ return substr ($ token , 1 , -1 );
454504 } else {
455- return $ token-> text ;
505+ return $ token ;
456506 }
457507 }
458508
459- protected function stripFlags (Token $ open , Token $ close ): StripFlags
509+ protected function stripFlags (string $ open , string $ close ): StripFlags
460510 {
461- return new StripFlags ($ open-> text [2 ] === '~ ' , $ close-> text [strlen ($ close-> text ) - 3 ] === '~ ' );
511+ return new StripFlags ($ open [2 ] === '~ ' , $ close [strlen ($ close ) - 3 ] === '~ ' );
462512 }
463513
464- protected function stripComment (Token $ comment ): string
514+ protected function stripComment (string $ comment ): string
465515 {
466- $ comment = preg_replace ('/^ \\{ \\{~?!-?-?/ ' , '' , $ comment-> text );
516+ $ comment = preg_replace ('/^ \\{ \\{~?!-?-?/ ' , '' , $ comment );
467517 return preg_replace ('/-?-?~?}}$/ ' , '' , $ comment );
468518 }
469519
470520 /**
471521 * @param Statement[] $statements
472522 */
473- protected function prepareProgram (array $ statements , SourceLocation $ loc ): Program
523+ protected function prepareProgram (array $ statements , ? SourceLocation $ loc = null ): Program
474524 {
525+ if (!$ loc ) {
526+ if ($ statements ) {
527+ $ firstLoc = $ statements [0 ]->loc ;
528+ $ lastLoc = $ statements [count ($ statements ) - 1 ]->loc ;
529+ $ loc = new SourceLocation ($ firstLoc ->source , $ firstLoc ->start , $ lastLoc ->end );
530+ } else {
531+ $ loc = new SourceLocation ('' , new Position (0 , -1 ), new Position (0 , -1 ));
532+ }
533+ }
534+
475535 return new Program ($ statements , [], $ loc );
476536 }
477537
@@ -482,13 +542,13 @@ protected function prepareMustache(
482542 SubExpression | PathExpression | Literal $ path ,
483543 array $ params ,
484544 ?Hash $ hash ,
485- Token $ open ,
545+ string $ open ,
486546 StripFlags $ strip ,
487547 SourceLocation $ loc ,
488548 ): MustacheStatement {
489- $ escapeFlag = $ open-> text [3 ] ?? $ open-> text [2 ];
549+ $ escapeFlag = $ open [3 ] ?? $ open [2 ];
490550 $ escaped = $ escapeFlag !== '{ ' && $ escapeFlag !== '& ' ;
491- $ decorator = preg_match ('/ \\*/ ' , $ open-> text );
551+ $ decorator = preg_match ('/ \\*/ ' , $ open );
492552
493553 return new MustacheStatement (
494554 type: $ decorator ? 'Decorator ' : 'MustacheStatement ' ,
@@ -501,9 +561,11 @@ protected function prepareMustache(
501561 );
502562 }
503563
504- private function validateClose (OpenHelper $ open , CloseBlock | Token $ close ): void
564+ private function validateClose (OpenHelper $ open , CloseBlock | string $ close ): void
505565 {
506- $ close = $ close instanceof CloseBlock ? $ close ->path ->original : $ close ->text ;
566+ if ($ close instanceof CloseBlock) {
567+ $ close = $ close ->path ->original ;
568+ }
507569
508570 if ($ open ->path ->original !== $ close ) {
509571 throw new \Exception ("{$ open ->path ->original } doesn't match {$ close }" );
@@ -531,9 +593,9 @@ protected function preparePath(bool $data, ArrayLiteral | HashLiteral | SubExpre
531593 $ isLiteral = $ parts [$ i ]->original !== $ part ;
532594 $ separator = $ parts [$ i ]->separator ;
533595
534- $ partPrefix = $ separator?->text === '.# ' ? '# ' : '' ;
596+ $ partPrefix = $ separator === '.# ' ? '# ' : '' ;
535597
536- $ original .= ($ separator ? $ separator -> text : '' ) . $ part ;
598+ $ original .= ($ separator ?? '' ) . $ part ;
537599
538600 if (!$ isLiteral && ($ part === '.. ' || $ part === '. ' || $ part === 'this ' )) {
539601 if (count ($ tail ) > 0 ) {
@@ -563,7 +625,7 @@ protected function preparePath(bool $data, ArrayLiteral | HashLiteral | SubExpre
563625 /**
564626 * @param ContentStatement[] $contents
565627 */
566- protected function prepareRawBlock (OpenHelper $ openRawBlock , array $ contents , Token $ close , SourceLocation $ loc ): BlockStatement
628+ protected function prepareRawBlock (OpenHelper $ openRawBlock , array $ contents , string $ close , SourceLocation $ loc ): BlockStatement
567629 {
568630 $ this ->validateClose ($ openRawBlock , $ close );
569631 $ program = new Program ($ contents , [], $ loc );
@@ -595,7 +657,7 @@ protected function prepareBlock(
595657 $ this ->validateClose ($ openBlock , $ close );
596658 }
597659
598- $ decorator = preg_match ('/ \\*/ ' , $ openBlock ->open -> text );
660+ $ decorator = preg_match ('/ \\*/ ' , $ openBlock ->open );
599661
600662 $ program ->blockParams = $ openBlock ->blockParams ;
601663
@@ -635,7 +697,7 @@ protected function prepareBlock(
635697 inverse: $ inverse ,
636698 openStrip: $ openBlock ->strip ,
637699 inverseStrip: $ inverseStrip ,
638- closeStrip: $ close ->strip ,
700+ closeStrip: $ close? ->strip,
639701 loc: $ loc ,
640702 );
641703 }
0 commit comments