src/parser/Parser.php

Summary

Maintainability
A
25 mins
Test Coverage
<?php
/**
 * Quack Compiler and toolkit
 * Copyright (C) 2015-2017 Quack and CONTRIBUTORS
 *
 * This file is part of Quack.
 *
 * Quack is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Quack 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Quack.  If not, see <http://www.gnu.org/licenses/>.
 */
namespace QuackCompiler\Parser;

use \Exception;
use \QuackCompiler\Lexer\Tag;
use \QuackCompiler\Lexer\Token;
use \QuackCompiler\Lexer\Tokenizer;
use \QuackCompiler\Parselets\Parselet;

abstract class Parser
{
    use Parselet;

    public $input;
    public $lookahead;
    public $scope_level = 0;

    public function __construct(Tokenizer $input)
    {
        $this->input = $input;
        $this->consume();
    }

    public function match($tag)
    {
        $hint = null;

        if ($this->lookahead->getTag() === $tag) {
            return $this->consume();
        }

        // When, after an error, the programmer provided an identifier,
        // we'll calculate the levenshtein distance between the expected lexeme
        // and the provided lexeme and give a hint about
        if (Tag::T_IDENT === $this->lookahead->getTag() && array_key_exists($tag, $this->input->keywords)) {
            $expected_lexeme = $this->input->keywords[$tag];
            $provided_lexeme = $this->lookahead->getContent();

            $distance = levenshtein($expected_lexeme, $provided_lexeme);

            if ($distance <= 2) {
                $hint = "Did you mean \"{$expected_lexeme}\" instead of \"{$provided_lexeme}\"?";
            }
        }

        $params = [
            'expected' => $tag,
            'found'    => $this->lookahead,
            'parser'   => $this,
            'hint'     => $hint
        ];

        if (0 === $this->lookahead->getTag()) {
            throw new EOFError($params);
        };

        throw new SyntaxError($params);
    }

    public function opt($tag)
    {
        if ($this->lookahead->getTag() === $tag) {
            $pointer = $this->consume();
            return $pointer === null ? true : $pointer;
        }
        return false;
    }

    public function is($tag)
    {
        return $this->lookahead->getTag() === $tag;
    }

    public function isEOF()
    {
        return 0 === $this->lookahead->getTag();
    }

    public function consume()
    {
        $content = $this->lookahead === null ?: $this->lookahead->getContent();
        $this->lookahead = $this->input->nextToken();
        return $content;
    }

    public function consumeIf($symbol)
    {
        if ($this->is($symbol)) {
            $this->consume();
            return true;
        }

        return false;
    }

    public function consumeAndFetch()
    {
        $clone = $this->lookahead;
        $this->lookahead = $this->input->nextToken();
        return $clone;
    }

    public function position()
    {
        return ["line" => $this->input->line, "column" => $this->input->column];
    }

    public function openScope()
    {
        $this->scope_level++;
    }

    public function closeScope()
    {
        $this->scope_level--;
    }

    public function indent()
    {
        return str_repeat('  ', $this->scope_level);
    }

    public function dedent()
    {
        return str_repeat('  ', max(0, $this->scope_level - 1));
    }
}