用 PHP 花两小时自制 脚本语言
0. 初步
用 PHP 写语言? 啥?
相信大家会有这样的疑问,但是今天我就要和大家一起花两个小时使用 PHP 打造一个脚本语言。
当然这个脚本语言将会十分简单,将不会有很多特性。我们准备参考 Lisp 的语法,最终这个脚本语言将不会比一个模板引擎的实现复杂。
在实现时,我有一个原则:不使用正则。(用了正则就会变得更加简单)
1. 解释器
1.1. 语法的简单介绍
对,上来就是解释器,我们忽略掉了词法分析器。
虽然这么做不太符合惯例,但是也可以实现。并且会减轻工作量。
首先,我们来简单地看下语法:
#| 基本的调用 |# (do-some-function) #| 字面量用 [] 包裹 |# (some-function ["Hello"] [123]) #| 字面量的 List 支持 |# (print [:"Hello", 123, 345]) #| 对 lazy-call 的支持 |# (@print ["lazy"]) #| 对无参函数无括号地调用 |# (print some-function-without-arguments)
对,在 AST 中,我们只有三种 Node 类型:Root, Calling, Literal。
想必大家也看出来这是纯函数式的了吧。
1.2 对于代码的 clean
为了方便解析,我们将实现一个统一的 clean 方法来清除注释、格式化空格。
废话不多说,直接上代码
<?php // 判断是否是空字符 function isBlankCharacter(string $ch): bool { static $blanks = [' ', "\t", "\n", "\r", '']; // 空字符列表 return in_array($ch, $blanks); // 使用 in_array 检查 } function clean(string $code): string { $codeArr = str_split( trim ($code)); // 使用 str_split 转换为数组,并且进行 trim 。 $ quote = false ; // 定义一个 quote flag,标记前一个字符是不是空字符 $flag = false; // 定义一个 flag,判断是否在 [] 内(字面量内空字符无需清理) $rslt = ''; // 存储 clean 后结果 foreach ($codeArr as $k=>$v) { // 遍历 code 字符串 if ($v === '[' && (@$codeArr[$k - 1] != "\\")) $flag = true; // 当为 [ 时,进入字面量,设置 flag 为 true if ($v === ']' && (@$codeArr[$k - 1] != "\\")) $flag = false; // 当为 ] 时,结束字面量,设置 flag 为 false // 当在字面量内时,无需判断空字符 if ($flag) { $rslt .= $v; continue; } if ($quote) { // 当前一个字符是空字符,这个字符不是时,设置 quote 为 false。 if (!$this->isBlankCharacter($v)) { $quote = false; $rslt .= $v; } } else { if ($this->isBlankCharacter($v)) { // 第一个空字符,格式化为空格,并且设置 quote $quote = true; $rslt .= " "; } else { // 否则直接向后增加字符 $rslt .= $v; } } } return $rslt; } // 去除注释 function parseComment(string $codeStr): string { $code = str_split($codeStr); // 创建栈,支持嵌套注释用 $commentStack = new \SplStack(); $rslt = ""; foreach ($code as $k=>$v) { // 是注释,压栈 if ($v === '#' && @$code[$k + 1] === '|') { $commentStack->push(true); continue; // 是结束注释 } else if ($v === '#' && $code[$k - 1] === '|') { // 当栈空时,抛出异常(多了|#) if ($commentStack-> isEmpty ()) { throw new \Pisp\Exceptions\ParseException("Comment brackets not matched."); } // 从栈中 pop $commentStack->pop(); // 阻止执行 continue; } // 栈中不空,在注释内 if (!$commentStack->isEmpty()) { continue; } // 写入结果 $rslt .= $v; } return $rslt; }
1.3. 对于代码的类型判断
判断是 Calling 或 Literal。
代码:
<?php
// node 是 AST 的 Node,马上会放出定义
function doParse(string $code, Node $parentNode) {
$code = $this->cleanCode($code); // 清理代码
if ($code === "") { // 代码为空,不做处理
return;
}
// 通过括号特征判断是 Calling
if (substr($code, 0, 1) == '(' && substr($code, -1, 1) == ')') {
// 去做 Calling 的 parse,下面会介绍
doParseCalling(str_split(substr($code, 1, -1)), $parentNode);
// 通过括号特征判断是 Literal
} else if (substr($code, 0, 1) == '[' && substr($code, -1, 1) == ']') {
// 去做 Literal 的 parse,会介绍
doParseLiteral(str_split(trim(substr($code, 1, -1))), $parentNode);
// 或者是直接对无参函数的调用
} else if (str_replace([' ', ')', '(', '[', ']', ';'], ['', '', '', '', '', ''], $code) == $code) {
$this->doParseCalling(str_split($code), $parentNode);
} else {
// 否则抛出异常
throw new ParseException("Parse error: unmatched brackets.");
}
}
1.4. 定义 AST
比较傻瓜,直接上代码:
<?php class Node { /** * The node's type. * * @var string */ public $type = "expr"; /** * The name of the node. * * @var string */ public $name = "collection"; /** * Children of it * * @var Node[] */ public $children = []; /** * The parent of it * * @var Node */ public $parent = null; /** * The node's data * * @var mixed */ public $data = null; /** * Add a child * * @param Node $child * @return self */ public function addChild(Node $child): Node { $this->children[] = $child; return $this; } /** * Set the data * * @param mixed $data * @return self */ public function setData($data): Node { $this->data = $data; return $this; } }
然后创建 CallingNode, LiteralNode 和 Root 继承 Node 即可。
1.5. 解析 CallingNode
详情说明看注释。
<?php // 解析 CallingNode 的函数 function doParseCalling(array $code, Node $parentNode) { // 创建 CallingNode 节点 $node = new CallingNode; // 设置父节点 $node->parent = $parentNode; // 创建栈,用来存储是否在 ( 内 $stack = new \SplStack(); // 创建栈,用来判断是否在字面量内 $stack2 = new \SplStack(); // 创建数组,用来存储分割参数后的代码 $splited = [""]; // 存储当前的 $splited 的下标 $curr = 0; // 遍历代码 foreach ($code as $k=>$v) { // 当进入一个新的 Calling 的时候 if ($v === '(' && $stack2->isEmpty()) { $stack->push(true); // 当退出一个 Calling 的时候 } else if (($v === ')') && !$stack->isEmpty() && $stack2->isEmpty()) { // 从栈中 pop $stack->pop(); } // 当进入一个新的字面量时 if ($v === '[') { $stack2->push(true); // 当退出一个字面量时 } else if (($v === ']') && !$stack2->isEmpty()) { $stack2->pop(); } // 当在根 CallingNode 时 if ($stack->count() <= 1) { // 当遇到空格时,分割参数 if ($v === ' ' && $stack->isEmpty() && $stack2->isEmpty()) { $curr ++; $splited[$curr] = ""; } } // 最后一个参数的 fix $splited[$curr] .= $v; } // 存储真实参数列表 $real = []; // 循环去除空参数 foreach ($splited as $v) { if (!$this->isBlankCharacter($v)) { $real[] = trim($v); } } // 重新赋值 $splited = $real; // 获取函数名 $node->name = $splited[0]; // 添加到父节点 $parentNode->addChild($node); // 遍历各个参数,并且将他们一个个 parse for ($i = 1; $i < count($splited); ++ $i) { $v = $splited[$i]; doParse($v, $node); } }
1.6. 解析 Literal
首先,实现 doParseLiteral。
<?php function doParseLiteral(array $code, Node $parentNode) { // 创建 Node 实例 $node = new LiteralNode; // 把锅推给 parseLiteral 函数,解释期就获得真正的值 $data = parseLiteral($code); // 设置为节点的附属数据 $node->setData($data); // 设置父节点 $node->parent = $parentNode; // 添加到父节点 $parentNode->addChild($node); }
然后过来看看主角 parseLiteral。
function parseLiteral(array $code) { // 还原 code 为字符串 $codeStr = join($code, ""); // 判断是否为字符串 if ($code[0] == '"' || $code[0] == "'") { // 偷懒的做法,直接 substr 获得字符串 $data = substr($codeStr, 1, -1); // 如果可以转换为数字 } else if (is_numeric($codeStr)) { // 通过某种特殊的方法强制转换为各种 numeric 值,$data = $codeStr + 0 也是一个办法 $data = $codeStr * 1; // 如果是个 list } else if ($code[0] == ':') { // data 就是一个数组 $data = []; // 创建一个 flag,用来判断字符串的双引号 $flag1 = false; // 创建一个 flag,用来判断字符串的单引号 $flag2 = false; // 当前元素的字面量值 $curr = ""; // 遍历代码 foreach ($code as $k=>$v) { // 当 $k 为 0 也就是指向 ":" 时,跳过 if ($k === 0) continue; // 如果是双引号,且不在单引号的字符串内,那么直接取反 $flag1 if ($v === '"' && !$flag2) $flag1 = !$flag1; // 如果是单引号,且不在双引号的字符串内,那么直接取反 $flag2 if ($v === "'" && !$flag1) $flag2 = !$flag2; // 当不在单引号和双引号内且当前是隔开元素的逗号符号 if ($v === ',' && !$flag1 && !$flag2) { // 解析当前元素的值 $data[] = parseLiteral(str_split(trim($curr))); // 转到下一个元素 $curr = ""; // 不需要添加 , continue; } // 添加到当前元素的值 $curr .= $v; } // 最后一个值的 hack $data[] = parseLiteral(str_split(trim($curr))); // 不是任何已知type } else { $data = null; } return $data; }
1.7 创建门面
对解析所有代码的封装函数。
function parse(string $code): Root { // 清理注释 $code = $this->parseComment($code); // 创建根节点 $root = new Root; // 进行解析 $this->doParse($code, $root); // 返回根节点 return $root; }
2. 创建一个没用的 ASTWalker
对,遍历 AST 的小工具,顺便可以测试我们的代码。
function walk(Node $ast, Callable $callback) { $callback($ast); foreach ($ast->children as $child) { walk($child, $callback); } }
不解释。
3. 运行时 VM
说这个算得上一个 VM 的话,可能有点夸张。但是他能使我们的程序跑起来。
3.1. 定义和删除 Functions 的方法
基本类和定义删除 Functions 的方法,由于太简单,不做赘述。
class VM { /** * Functions * * @var array */ protected $functions = []; /** * Define a function * * @param string $name * @param mixed $value * @return self */ public function define(string $name, $value): VM { $this->functions[$name] = $value; return $this; } /** * Delete a function * * @param string $name * @return self */ public function delete(string $name): VM { unset($this->functions[$name]); return $this; } }
3.2 定义执行 Node 所用的方法
public function runNode(Node $node) { // 是字面量,直接返回数据 if ($node instanceof LiteralNode) { return $node->data; // 是执行函数的节点 } else if ($node instanceof CallingNode) { // 取出函数名 $name = $node->name; // 判断是否是 lazy 的 if (substr($name, 0, 1) == "@") { // 是 lazy 的,直接以 AST 作为参数 $args = $node->children; // 将 name 去除 @ 符号 $name = substr($name, 1); } else { // 创建参数列表 $args = []; // 遍历参数的 AST foreach ($node->children as $child) { // 执行 AST $args[] = $this->runNode($child); } } // 执行这个函数 return $this->doFunction($name, $args); // 如果是根节点 } else if ($node instanceof Root) { // 执行其中的第 0 个 child return $this->runNode($node->children[0]); // 否则抛出异常 } else { throw new UnknownNodeException("Unknown node type: {$node->type}"); } }
3.3. 定义执行函数的方法
public function doFunction(string $name, array $args) { // 如果没有该函数,抛出异常 if (!isset($this->functions[$name])) { throw new NoFunctionException("Unknown function: {$name}"); return; } // 获取函数 $func = $this->functions[$name]; // 如果是一个合法的回调 if (is_callable($func)) { // 就去执行这个回调 return $func($args, $this); // 如果是一个合法的 AST 节点 } else if ($func instanceof Node) { // 就去执行这个节点 return $this->runNode($func); // 否则是一个变量 } else { // 返回它的值 return $func; } }
本文系转载,如有侵权请告知删除,已经配置来源地址