如何设计脚本语言 用 PHP 花两小时自制脚本语言

11/28 04:13:42 来源网站:辅助卡盟网

如何设计脚本语言 用 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】 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

    暂无相关资讯
如何设计脚本语言 用 PHP 花两小时自制脚本语言