diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 853a3b02..82b7de6a 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -22,14 +22,9 @@ private static function language($name, $emitter) { } private static function parse($lang, $in, $version, $out, $file) { - $language= isset(self::$lang[$lang]) - ? self::$lang[$lang] - : self::$lang[$lang]= self::language($lang, self::$emit[$version]) - ; - + $language= self::$lang[$lang] ?? self::$lang[$lang]= self::language($lang, self::$emit[$version]); try { - self::$emit[$version]->emitAll(new Result($out), $language->parse(new Tokens($in, $file))->stream()); - return $out; + return self::$emit[$version]->write($language->parse(new Tokens($in, $file))->stream(), $out); } finally { $in->close(); } diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index c7cc5b1a..148dd1e9 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,5 +1,6 @@ line > $result->line) { - $result->out->write(str_repeat("\n", $node->line - $result->line)); - $result->line= $node->line; - } - // Check for transformations if (isset($this->transformations[$node->kind])) { foreach ($this->transformations[$node->kind] as $transformation) { $r= $transformation($result->codegen, $node); if ($r instanceof Node) { if ($r->kind === $node->kind) continue; - $this->{"emit{$r->kind}"}($result, $r); + $this->{'emit'.$r->kind}($result, $r); return; } else if ($r) { foreach ($r as $n) { - $this->{"emit{$n->kind}"}($result, $n); + $this->{'emit'.$n->kind}($result, $n); $result->out->write(';'); } return; @@ -152,6 +147,33 @@ public function emitOne($result, $node) { } // Fall through, use default } + $this->{'emit'.$node->kind}($result, $node); } + + /** + * Creates result + * + * @param io.streams.OutputStream $target + * @return lang.ast.Result + */ + protected abstract function result($target); + + /** + * Emitter entry point, takes nodes and emits them to the given target. + * + * @param iterable $nodes + * @param io.streams.OutputStream $target + * @return io.streams.OutputStream + * @throws lang.ast.Errors + */ + public function write($nodes, OutputStream $target) { + $result= $this->result($target); + try { + $this->emitAll($result, $nodes); + return $target; + } finally { + $result->close(); + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index 2b3cd209..8870790c 100755 --- a/src/main/php/lang/ast/emit/AttributesAsComments.class.php +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -33,13 +33,12 @@ protected function emitAnnotations($result, $annotations) { $line= $annotations->line; $result->out->write('#['); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["\n" => " "])); + $result->out= new Escaping($result->out, ["\n" => " "]); foreach ($annotations->named as $annotation) { $this->emitOne($result, $annotation); $result->out->write(','); } - $result->out->redirect($out); + $result->out= $result->out->original(); $result->out->write("]\n"); $result->line= $line + 1; diff --git a/src/main/php/lang/ast/emit/Escaping.class.php b/src/main/php/lang/ast/emit/Escaping.class.php index cf3685fa..e096035f 100755 --- a/src/main/php/lang/ast/emit/Escaping.class.php +++ b/src/main/php/lang/ast/emit/Escaping.class.php @@ -21,4 +21,8 @@ public function flush() { public function close() { $this->target->close(); } + + public function original() { + return $this->target; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php similarity index 54% rename from src/main/php/lang/ast/Result.class.php rename to src/main/php/lang/ast/emit/GeneratedCode.class.php index 958145b0..8fbe4d27 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -1,35 +1,40 @@ -out= $out; - $this->out->write($preamble); - $this->codegen= new CodeGen(); + public function __construct($out, $prolog= '', $epilog= '') { + $this->prolog= $prolog; + $this->epilog= $epilog; + parent::__construct($out); } /** - * Creates a temporary variable and returns its name + * Initialize result. Guaranteed to be called *once* from constructor. + * Without implementation here - overwrite in subclasses. * - * @return string + * @return void */ - public function temp() { - return '$'.$this->codegen->symbol(); + protected function initialize() { + '' === $this->prolog || $this->out->write($this->prolog); + } + + /** + * Write epilog + * + * @return void + */ + protected function finalize() { + '' === $this->epilog || $this->out->write($this->epilog); } /** @@ -46,6 +51,15 @@ public function at($line) { return $this; } + /** + * Creates a temporary variable and returns its name + * + * @return string + */ + public function temp() { + return '$'.$this->codegen->symbol(); + } + /** * Looks up a given type * diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index d7d7728f..048cd88c 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -15,7 +15,7 @@ Variable }; use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable}; -use lang\ast\{Emitter, Node, Type}; +use lang\ast\{Emitter, Node, Type, Result}; abstract class PHP extends Emitter { const PROPERTY = 0; @@ -23,6 +23,16 @@ abstract class PHP extends Emitter { protected $literals= []; + /** + * Creates result + * + * @param io.streams.OutputStream $target + * @return lang.ast.Result + */ + protected function result($target) { + return new GeneratedCode($target, 'out->write('(eval: \''); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); + $result->out= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); // If exactly one unnamed argument exists, emit its value directly if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) { @@ -470,7 +479,7 @@ protected function emitAnnotation($result, $annotation) { $result->out->write(']'); } - $result->out->redirect($out); + $result->out= $result->out->original(); $result->out->write('\')'); return; } @@ -1026,4 +1035,15 @@ protected function emitFrom($result, $from) { $result->out->write('yield from '); $this->emitOne($result, $from->iterable); } + + /** + * Emit single nodes + * + * @param lang.ast.Result $result + * @param lang.ast.Node $node + * @return void + */ + public function emitOne($result, $node) { + parent::emitOne($result->at($node->line), $node); + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Result.class.php b/src/main/php/lang/ast/emit/Result.class.php new file mode 100755 index 00000000..a9b7dd58 --- /dev/null +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -0,0 +1,53 @@ +out= $out; + $this->codegen= new CodeGen(); + $this->initialize(); + } + + /** + * Initialize result. Guaranteed to be called *once* from constructor. + * Without implementation here - overwrite in subclasses. + * + * @return void + */ + protected function initialize() { + // NOOP + } + + /** + * Finalize result. Guaranteed to be called *once* from within `close()`. + * Without implementation here - overwrite in subclasses. + * + * @return void + */ + protected function finalize() { + // NOOP + } + + /** @return void */ + public function close() { + if (null === $this->out) return; + + $this->finalize(); + $this->out->close(); + unset($this->out); + } +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index d311e920..4858ad20 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -107,9 +107,7 @@ public static function main(array $args) { $file= $path->toString('/'); $t->start(); try { - $parse= $lang->parse(new Tokens($source, $file)); - $target= $output->target((string)$path); - $emit->emitAll(new Result($target), $parse->stream()); + $emit->write($lang->parse(new Tokens($source, $file))->stream(), $output->target((string)$path)); $t->stop(); $quiet || Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); @@ -121,7 +119,6 @@ public static function main(array $args) { $total++; $time+= $t->elapsedTime(); $source->close(); - $target->close(); } } diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index a6b0ea27..347ff56e 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -65,66 +65,64 @@ public function remove_unsets_empty_kind() { #[Test, Expect(IllegalStateException::class)] public function emit_node_without_kind() { - $this->newEmitter()->emitOne(new Result(new MemoryOutputStream()), new class() extends Node { + $node= new class() extends Node { public $kind= null; - }); + }; + $this->newEmitter()->write([$node], new MemoryOutputStream()); } #[Test] public function transform_modifying_node() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { $var->name= '_'.$var->name; return $var; }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function transform_to_node() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { return new Code('$variables["'.$var->name.'"]'); }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function transform_to_array() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { return [new Code('$variables["'.$var->name.'"]')]; }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function transform_to_null() { $fixture= $this->newEmitter(); $fixture->transform('variable', function($codegen, $var) { return null; }); - $out= new MemoryOutputStream(); - $fixture->emitOne(new Result($out), new Variable('a')); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); - Assert::equals('bytes()); + Assert::equals('bytes()); } #[Test] public function emit_multiline_comment() { - $fixture= $this->newEmitter(); - $out= new MemoryOutputStream(); - $fixture->emitAll(new Result($out), [ - new Comment( - "/**\n". - " * Doc comment\n". - " *\n". - " * @see http://example.com/\n". - " */", - 3 - ), - new Variable('a', 8) - ]); + $out= $this->newEmitter()->write( + [ + new Comment( + "/**\n". + " * Doc comment\n". + " *\n". + " * @see http://example.com/\n". + " */", + 3 + ), + new Variable('a', 8) + ], + new MemoryOutputStream() + ); $code= $out->bytes(); Assert::equals('$a;', explode("\n", $code)[7], $code); diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index f15bf8f9..c0ff9ed0 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -1,8 +1,7 @@ temp(), $r->temp(), $r->temp()]); - } - - #[Test] - public function writes_php_open_tag_as_default_preamble() { - $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); - Assert::equals('bytes()); - } - - #[Test, Values(['', 'bytes()); + new Result(new MemoryOutputStream()); } #[Test] public function write() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result($out); $r->out->write('echo "Hello";'); - Assert::equals('bytes()); + Assert::equals('echo "Hello";', $out->bytes()); } #[Test] public function write_escaped() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result($out); $r->out->write("'"); - $r->out->redirect(new Escaping($out, ["'" => "\\'"])); + $r->out= new Escaping($out, ["'" => "\\'"]); $r->out->write("echo 'Hello'"); - $r->out->redirect($out); - $r->out->write("'"); - - Assert::equals("bytes()); - } - - #[Test] - public function lookup_self() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); - - Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); - } - - #[Test] - public function lookup_parent() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], null, null, 1); - - Assert::equals(new Reflection(Value::class), $r->lookup('parent')); - } - #[Test] - public function lookup_named() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); - - Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); - } - - #[Test] - public function lookup_value_interface() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - - Assert::equals(new Reflection(Value::class), $r->lookup('\\lang\\Value')); - } - - #[Test, Expect(ClassNotFoundException::class)] - public function lookup_non_existant() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->lookup('\\NotFound'); - } - - #[Test] - public function line_number_initially_1() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - Assert::equals(1, $r->line); - } - - #[Test, Values([[1, 'at($line)->out->write('test'); - - Assert::equals($expected, $out->bytes()); - Assert::equals($line, $r->line); - } - - #[Test] - public function at_cannot_go_backwards() { - $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); - $r->at(0)->out->write('test'); + $r->out= $out; + $r->out->write("'"); - Assert::equals('bytes()); - Assert::equals(1, $r->line); + Assert::equals("'echo \'Hello\''", $out->bytes()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php index 722aca93..620eed0a 100755 --- a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php @@ -1,7 +1,8 @@ type= $type; $this->emitter->emitOne($result, $node); diff --git a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php index d1073736..594f9532 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -1,7 +1,8 @@ language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); $out= new MemoryOutputStream(); - $this->emitter->emitAll(new Result(new StringWriter($out), ''), $tree->children()); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); return $out->bytes(); } @@ -75,8 +76,6 @@ protected function emit($code) { */ protected function type($code) { $name= 'T'.(self::$id++); - $out= new MemoryOutputStream(); - $tree= $this->language->parse(new Tokens(str_replace('', $name, $code), static::class))->tree(); if (isset($this->output['ast'])) { Console::writeLine(); @@ -84,7 +83,8 @@ protected function type($code) { Console::writeLine($tree); } - $this->emitter->emitAll(new Result(new StringWriter($out), ''), $tree->children()); + $out= new MemoryOutputStream(); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); if (isset($this->output['code'])) { Console::writeLine(); Console::writeLine('=== ', static::class, ' ==='); diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php new file mode 100755 index 00000000..3d1ed780 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -0,0 +1,108 @@ +temp(), $r->temp(), $r->temp()]); + } + + #[Test] + public function prolog_and_epilog_default_to_emtpy_strings() { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out); + Assert::equals('', $out->bytes()); + } + + #[Test, Values(['', 'close(); + Assert::equals($prolog, $out->bytes()); + } + + #[Test] + public function writes_epilog_on_closing() { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out, ''); + $r->close(); + + Assert::equals('', $out->bytes()); + } + + #[Test] + public function lookup_self() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); + + Assert::equals(new Declaration($r->type[0], $r), $r->lookup('self')); + } + + #[Test] + public function lookup_parent() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], '\\T', '\\lang\\Value', [], [], null, null, 1); + + Assert::equals(new Reflection(Value::class), $r->lookup('parent')); + } + + #[Test] + public function lookup_named() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->type[0]= new ClassDeclaration([], '\\T', null, [], [], null, null, 1); + + Assert::equals(new Declaration($r->type[0], $r), $r->lookup('\\T')); + } + + #[Test] + public function lookup_value_interface() { + $r= new GeneratedCode(new MemoryOutputStream()); + + Assert::equals(new Reflection(Value::class), $r->lookup('\\lang\\Value')); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function lookup_non_existant() { + $r= new GeneratedCode(new MemoryOutputStream()); + $r->lookup('\\NotFound'); + } + + #[Test] + public function line_number_initially_1() { + $r= new GeneratedCode(new MemoryOutputStream()); + Assert::equals(1, $r->line); + } + + #[Test, Values([[1, 'test'], [2, "\ntest"], [3, "\n\ntest"]])] + public function write_at_line($line, $expected) { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out); + $r->at($line)->out->write('test'); + + Assert::equals($expected, $out->bytes()); + Assert::equals($line, $r->line); + } + + #[Test] + public function at_cannot_go_backwards() { + $out= new MemoryOutputStream(); + $r= new GeneratedCode($out); + $r->at(0)->out->write('test'); + + Assert::equals('test', $out->bytes()); + Assert::equals(1, $r->line); + } +} \ No newline at end of file