php 两小时后,用 PHP 花两小时自制脚本语言

原标题:用 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 方法来清除注释、格式化空格。

废话不多说,直接上代码

// 判断是否是空字符

function isBlankCharacter(string $ch): bool {

static $blanks = [' ', " ", "

", "

", '']; // 空字符列表

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 PispExceptionsParseException("Comment brackets not matched.");

}

// 从栈中 pop

$commentStack->pop();

// 阻止执行

continue;

}

// 栈中不空,在注释内

if (!$commentStack->isEmpty()) {

continue;

}

// 写入结果

$rslt .= $v;

}

return $rslt;

}

1.3. 对于代码的类型判断

判断是 Calling 或 Literal。

代码:

// 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

比较傻瓜,直接上代码:

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

详情说明看注释。

// 解析 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。

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;

}

}

本文系转载,如有侵权请告知删除,已经配置来源地址

https://zhuanlan.zhihu.com/p/42274829返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值