yay/yay

高级PHP预处理器

0.7.0 2018-06-26 11:52 UTC

README

Build Status Coverage Status Latest Stable Version Join the chat at https://gitter.im/marcioAlmada/yay License

YAY! 是一个高级解析组合器PHP预处理器,允许任何人用PHP 💥 扩充PHP。

这意味着语言功能可以作为composer包分发(只要基于宏的实现可以用纯PHP代码表达,并且实现足够快)。

路线图.

设置

composer require yay/yay:dev-master

使用方法

命令行

yay some/file/with/macros.php >> target/file.php

运行时模式

“运行时”模式是W.I.P,将使用流封装以及与composer的集成来预处理每个包含的文件。它可能有一些opcache/cache支持,因此文件将只预处理/展开一次,并在需要时进行。

查看功能进度在问题#11

它是如何工作的

一个非常简单的例子

每个宏由一个匹配器和扩展器组成,当执行时,允许你扩充PHP。考虑最简单的例子

$(macro :unsafe) { $ } >> { $this } // this shorthand

该宏基本上将字面量$标记扩展为$this。以下代码将扩展为

// source                                |   // expansion
class Foo {                              |   class Foo {
    protected $a = 1, $b = 2, $c = 3;    |       protected $a = 1, $b = 2, $c = 3;
                                         |        
    function getProduct(): int {         |       function getProduct(): int {
        return $->a * $->b * $->c;       |           return $this->a * $this->b *$this->c;
    }                                    |       }
}                                        |   }

请注意,在$this扩展中需要:unsafe标记来避免宏卫生。

这个宏实际上非常简单,一个更成熟的版本会是

$(macro :unsafe){
    $ // litterally matches '$'
    // but not followed by:
    $(not(token(T_VARIABLE))) // avoids var var false positives such as '$$foo'
    $(not(token('{'))) // avoids false positives such as '${foo}'
} >> {
    $this
}

简单示例

除了字面字符序列之外,还可以使用$(TOKEN_TYPE as label)形式的token匹配器来匹配特定的标记类型。

以下宏匹配像__swap($x, $y)__swap($foo, $bar)这样的标记序列

$(macro) {
    __swap ( $(T_VARIABLE as A) , $(T_VARIABLE as B) )
} >> {
    (list($(A), $(B)) = [$(B), $(A)])
}

扩展应该是显而易见的

// source              |    // expansion
__swap($foo, $bar);    |    (list($foo, $bar) = [$bar, $foo]); 

另一个简单示例

要实现unless,我们需要匹配字面量unless关键字后跟括号内的标记层(...)和代码块{...}。幸运的是,宏DSL有一个非常直接的层匹配构造

$(macro) {
    unless ($(layer() as expression)) { $(layer() as body) }
} >> {
    if (! ($(expression))) {
        $(body)
    }
}

宏的实际作用

// source                   |   // expansion
unless ($x === 1) {         |   if (! ($x === 1)) {
    echo "\$x is not 1";    |       echo "\$x is not 1";
}                           |   }

PS:请勿实现“unless”。这里只是为了教学目的。

高级示例

一个更复杂的例子可能是将枚举从未来迁移到PHP,语法如下

enum Fruits {
    Apple,
    Orange
}

var_dump(\Fruits::Orange <=> \Fruits::Apple);

因此,从语法上讲,枚举是用字面量enum单词声明的,后跟一个T_STRING和一个用逗号分隔的标识符列表,例如{A, B, C}

YAY内部使用解析组合器进行一切操作,并且这些更高级的解析器在宏声明中完全暴露。我们的枚举宏将需要像ls()label()这样的高级匹配器组合来匹配所需的语法,如下所示

$(macro) {
    enum $(T_STRING as name) {
        $(
            // ls() matches a delimited list
            // in this case a list of label() delimited by ',' such as `foo, bar, baz`
            ls
            (
                label() as field
                ,
                token(',')
            )
            as fields
        )
    }
} >> {
    "it works";
}

该宏已经能够匹配枚举语法

// source                      // expansion
enum Order {ASC, DESC};    |   "it works";

我不会解释枚举是如何实现的,如果您愿意,可以阅读RFC,然后查看以下扩展是如何工作的

// things here would normally be under a namespace, but since we want a concise example...

interface Enum
{
}

function enum_field_or_class_constant(string $class, string $field)
{
    return (\in_array(\Enum::class, \class_implements($class)) ? $class::$field() : \constant("{$class}::{$field}"));
}

$(macro :unsafe) {
    // the enum declaration
    enum $(T_STRING as name) {
        $(
            ls
            (
                label() as field
                ,
                token(',')
            )
            as fields
        )
    }
} >> {
    class $(name) implements Enum {
        private static $registry;

        private function __construct() {}

        static function __callStatic(string $type, array $args) : self {
            if(! self::$registry) {
                self::$registry = new \stdclass;
                $(fields ... {
                    self::$registry->$(field) = new class extends $(name) {};
                })
            }

            if (isset(self::$registry->$type)) return self::$registry->$type;

            throw new \Exception(sprintf('Undefined enum type %s->%s', __CLASS__, $type));
        }
    }
}

$(macro) {
    $(
        // sequence that matches the enum field access syntax:
        chain(
            ns() as class, // matches a namespace
            token(T_DOUBLE_COLON), // matches T_DOUBLE_COLON used for static access
            not(class), // avoids matching `Foo::class`, class resolution syntax
            label() as field, // matches the enum field name
            not(token('(')) // avoids matching static method calls such as `Foo::bar()`
        )
    )
} >> {
    \enum_field_or_class_constant($(class)::class, $$(stringify($(field))))
}

更多示例在phpt测试文件夹中https://github.com/marcioAlmada/yay/tree/master/tests/phpt

常见问题解答

为什么叫“YAY!”?

- PHP具有功能“x”:yay还是nay?😉

文档在哪里?

正在制作食谱

你为什么在制作这个?

因为它很有趣。它可能变得有用。 因为我们能™

结论

目前这是一个关于如何使用解析器组合器在类似PHP这样的语言上构建高级预处理器领域特定语言(DSL)的实验。为什么?

PHP离同构性非常远,因此需要复杂的确定性解析和大的AST实现,以及节点访问者API来修改源代码——最终,你甚至无法轻松处理未知语法 ¯\_(⊙_ʖ⊙)_/¯

因此产生了这个项目。它也是挑战的一部分

  1. 创建一个简约的架构,将预处理程序本身的内部组件子集暴露给用户DSL。
  2. 创建具有良好错误报告和语法无效化功能的解析器组合器,因为1

版权

版权(c)2015-* Márcio Almada。在MIT风格的许可下分发。有关详细信息,请参阅LICENSE。