如何设计脚本语言 用 PHP 花两小时自制脚本语言
用 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;
}
}
4. 所有代码整合 && 测试
所有代码均发布到了 github.com/xtlsoft/Pisp 。
来源:【九爱网址导航www.fuzhukm.com】 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!