概要
本章ではScannerで作成したトークン列を評価しやすいように構造化します。完全な実装は次のURLにあります。
実装
実のところ、単なるStack型の言語の実装をしたいのであれば、この章は不要です。今回は説明を簡単にするためにBlockをあらかじめ作成します。
raw text: { 1 }
token: BraceLeft LiteralInteger(1) BraceRight
token with block: Block(LiteralInteger(1))
tokenを保持できればいいので次のようなClassがあれば良いです。
readonly class Block extends Token
{
/** @var Token[] */
public array $tokens;
/**
* @param Token[] $tokens
*/
public function __construct(int $line, int $position, array $tokens) {
parent::__construct(line: $line, position: $position);
$this->tokens = $tokens;
}
}
Block ParserはScannerと比べればよっぽど簡単なプログラムになっています。再帰で可変参照を渡して、それを編集するコードです。
参照と再帰の肩ならし
再帰と可変参照になれるためにちょっと肩慣らしをしましょう。PHPの場合、暗黙的に書くので先にRustで明示的に書くことでイメージを膨らませます。Rustの場合、可変にするには mut
で宣言する必要があります。また、参照渡しをするには &
をつけるので、 &mut
で可変参照渡しになります、
struct MyStruct {
value: i32,
}
// 可変参照を引数に取る関数
fn modify_struct(s: &mut MyStruct) {
s.value += 1;
}
fn main() {
let mut s = MyStruct { value: 10 };
println!("Before: {}", s.value);
// 可変参照渡し
modify_struct(&mut s);
println!("After: {}", s.value);
}
PHPの場合 classはデフォルトで可変で、functionには参照渡しされます。つまり下記のコードは先程のコードと等価です。
class MyClass {
public $value;
public function __construct($value) {
$this->value = $value;
}
}
function modifyClass($obj) {
// オブジェクトのプロパティを変更
$obj->value += 1;
}
$s = new MyClass(10);
echo "Before: " . $s->value . "\n";
modifyClass($s);
echo "After: " . $s->value . "\n";
再帰も少し練習します。空配列でないときに先頭の文字を出力する関数を作ります。
<?php
function print_character(array $arr) {
if (empty($arr)) {
return;
} else {
$firstCharacter = array_shift($arr); // $arrの最初の要素が消える
echo $firstCharacter;
print_character($arr);
}
}
print_character(["h", "e", "l", "l", "o"]);
// hello
ブロックをパースする
ここまでくればパースは簡単です。 array_shift
でtokenからとりだしつづけるコードを書き、LeftBraceをみたらBlockを探索する関数を起動します。
class Parser
{
/** @var Token[] */
private array $tokens;
/**
* @param list<Token> $tokens
*/
public function __construct(array $tokens) {
$this->tokens = $tokens;
}
/**
* @return Token[]
* @throws ParserException
*/
public function parse(): array {
$parsed_tokens = [];
while ($token = array_shift($this->tokens)) {
if ($token instanceof LeftBrace) {
$parsed_tokens[] = $this->parseBlock($this->tokens, $token->line, $token->position);
continue;
}
$parsed_tokens[] = $token;
}
return $parsed_tokens;
}
}
Blockを探索する関数は再帰をします。
- ネストが深くなるときに再帰呼び出し
- ブロックの終了条件のときにreturn
するところだけが異なります。
/**
* @param Token[]& $tokens
* @param int $line
* @param int $position
* @return Block
* @throws ParserException
*/
private function parseBlock(array &$tokens, int $line, int $position): Block {
$tokens_in_block = [];
while ($token = array_shift($tokens)) {
if ($token instanceof LeftBrace) {
$tokens_in_block[] = $this->parseBlock($tokens, $token->line, $token->position);
continue;
}
if ($token instanceof RightBrace) {
return new Block(
line: $line,
position: $position,
tokens: $tokens_in_block
);
}
$tokens_in_block[] = $token;
}
throw new ParserException(
source_code_line: $line,
source_code_position: $position,
message: "{ (line: " . $line . ", position: " . $position . ") に対応する } が存在しません"
);
}
まとめ
この章では、Block構造を作成しました。もちろん、この機能をStackMachineに入れてもいいですが、次の章のコードがかなり楽になります。このように評価器の前にデータ構造を扱いやすくすることは大切です。
補足: PHPで不変参照は使えるの?
readonlyを付けると不変になります。
<?php
readonly class MyClass {
public $value;
public function __construct($value) {
$this->value = $value;
}
}
function modifyClass($obj) {
// オブジェクトのプロパティを変更
$obj->value += 1;
}
$s = new MyClass(10);
echo "Before: " . $s->value . "\n";
modifyClass($s);
echo "After: " . $s->value . "\n";
// <br />
// <b>Fatal error</b>: Readonly property MyClass::$value must have type in <b>php-wasm run script</b> on line <b>2</b><br />