xp-framework/rfc

Compile-time metaprogramming

thekid opened this issue · 2 comments

Scope of Change

The XP Compiler will be extended to support compile-time metaprogramming

Rationale

Build libraries like xp-forge/partial into compiler.

Functionality

<<value>>
class Person {
  private int $id;
  private string $name;
}

The compiler finds an implementation which was previously registered via Transformations::register() for class nodes. The function gets executed and transforms the above to the following PHP:

class Person implements \lang\Value {
  private $id;
  private $name;

  public function __construct(int $id, string $name) {
    $this->id= $id;
    $this->name= $name;
  }

  public function id() { return $this->id; }

  public function name() { return $this->name; }

  // toString(), hashCode() and compareTo() omitted for brevity
}

Security considerations

Speed impact

Dependencies

xp-framework/compiler#19

Related documents

https://dzone.com/articles/compile-time-metaprogramming
http://groovy-lang.org/metaprogramming.html#_available_ast_transformations
http://groovy-lang.org/metaprogramming.html#developing-ast-xforms
http://notatube.blogspot.de/2010/12/project-lombok-creating-custom.html
xp-framework/compiler#15
https://wiki.php.net/rfc/parser-extension-api
microsoft/TypeScript#5595

E.g. inside the partial library, using a convenience class instead of assembling the code using AST nodes.

Transformations::register('class', function($class) {
  if (array_key_exists('value', $class->value->annotations)) {
    $class->value->implements[]= '\lang\Value';

    foreach ($class->value->body as $type => $node) {
      if ('$' !== $type{0}) continue;
      $fields[]= $node->value->name;
    }

    $args= $init= [];
    foreach ($fields as $name) {
      $class->value->body[$name.'()']= new Code('public function '.$name.'() {
        return $this->'.$name.';
      }');
      $args[]= '$'.$name;
      $init[]= '$this->'.$name.'= $'.$name.';';
    }

    $class->value->body['__construct()']= new Code('public function __construct('.
      implode(', ', $args).') { '.implode('', $init).' }'
    );

    $class->value->body['toString()']= new Code('public function toString() {
      return nameof($this)."@".\util\Objects::stringOf(get_object_vars($this));
    }');
    $class->value->body['hashCode()']= new Code('public function hashCode() {
      return nameof($this);
    }');
    $class->value->body['compareTo()']= new Code('public function compareTo($value) {
      return 1;
    }');
  }
  return $class;
});
$ cat Demo.php
<?php

use util\cmd\Console;

<<value>>
class Demo {
  private string $id, $name;

  public static function main(array<string> $args) {
    Console::writeLine(new self(...$args));
  }
}

$ xp Demo 1549 Timm
Demo@[
  id => "1549"
  name => "Timm"
]

API is now far more concise:

use lang\ast\transform\Transformations;
use lang\ast\nodes\Method;
use lang\ast\nodes\Signature;
use lang\ast\Code;

Transformations::register('class', function($class) {
  if ($class->value->annotation('getters')) {
    foreach ($class->value->properties() as $property) {
      $class->value->inject(new Method(
        ['public'],
        $property->name,
        new Signature([], $property->type),
        [new Code('return $this->'.$property->name.';')]
      ));
    }
  }
  yield $class;
});

See https://github.com/xp-framework/ast