From 12dad6b328adc643289487abb894f1057c72f0bc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Feb 2022 23:17:57 +0100 Subject: [PATCH 01/10] Move responsibility for creating result to emitter --- src/main/php/lang/ast/Compiled.class.php | 9 ++---- src/main/php/lang/ast/Emitter.class.php | 27 ++++++++++++++++++ src/main/php/lang/ast/Result.class.php | 24 ++++++++++++---- src/main/php/lang/ast/emit/PHP.class.php | 12 +++++++- .../php/xp/compiler/CompileRunner.class.php | 11 +++----- .../lang/ast/unittest/EmitterTest.class.php | 20 ++++++------- .../lang/ast/unittest/ResultTest.class.php | 28 +++++++++++++------ 7 files changed, 90 insertions(+), 41 deletions(-) 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..fc89857b 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,5 +1,6 @@ {'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(iterable $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/Result.class.php b/src/main/php/lang/ast/Result.class.php index 958145b0..23360b06 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,8 +1,9 @@ out= $out; - $this->out->write($preamble); + $this->out->write($prolog); + $this->epilog= $epilog; $this->codegen= new CodeGen(); } @@ -65,4 +69,14 @@ public function lookup($type) { return new Reflection($type); } + + /** @return void */ + public function close() { + if (null === $this->out) return; + + // Write epilog, then close and ensure this doesn't happen twice + '' === $this->epilog || $this->out->write($this->epilog); + $this->out->close(); + unset($this->out); + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index c0971156..8d5160d7 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 Result($target, 'newInstance(); + $emit= Emitter::forRuntime($emitter, $augment)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } @@ -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 76844908..3ed5b28e 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -74,39 +74,35 @@ public function emit_node_without_kind() { 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()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index f15bf8f9..f31d72f5 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -21,23 +21,33 @@ public function creates_unique_temporary_variables() { } #[Test] - public function writes_php_open_tag_as_default_preamble() { + public function prolog_and_epilog_default_to_emtpy_strings() { $out= new MemoryOutputStream(); $r= new Result(new StringWriter($out)); - Assert::equals('bytes()); + Assert::equals('', $out->bytes()); } #[Test, Values(['', 'bytes()); + $r= new Result(new StringWriter($out), $prolog); + $r->close(); + Assert::equals($prolog, $out->bytes()); + } + + #[Test] + public function writes_epilog_on_closing() { + $out= new MemoryOutputStream(); + $r= new Result(new StringWriter($out), ''); + $r->close(); + + Assert::equals('', $out->bytes()); } #[Test] public function write() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result(new StringWriter($out), 'out->write('echo "Hello";'); Assert::equals('bytes()); } @@ -45,7 +55,7 @@ public function write() { #[Test] public function write_escaped() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result(new StringWriter($out), 'out->write("'"); $r->out->redirect(new Escaping($out, ["'" => "\\'"])); @@ -102,7 +112,7 @@ public function line_number_initially_1() { #[Test, Values([[1, 'at($line)->out->write('test'); Assert::equals($expected, $out->bytes()); @@ -112,7 +122,7 @@ public function write_at_line($line, $expected) { #[Test] public function at_cannot_go_backwards() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out)); + $r= new Result(new StringWriter($out), 'at(0)->out->write('test'); Assert::equals('bytes()); From 29effe32a701a46e283aa0c646a21c44dbc5d878 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 6 Feb 2022 23:21:05 +0100 Subject: [PATCH 02/10] Restore PHP 7.0 compatibility PHP 7.0 does not have the `iterable` type --- src/main/php/lang/ast/Emitter.class.php | 2 +- src/main/php/xp/compiler/CompileRunner.class.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index fc89857b..2943bc76 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -172,7 +172,7 @@ protected abstract function result($target); * @return io.streams.OutputStream * @throws lang.ast.Errors */ - public function write(iterable $nodes, OutputStream $target) { + public function write($nodes, OutputStream $target) { $result= $this->result($target); try { $this->emitAll($result, $nodes); diff --git a/src/main/php/xp/compiler/CompileRunner.class.php b/src/main/php/xp/compiler/CompileRunner.class.php index 03cc7341..4858ad20 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -65,13 +65,13 @@ private static function emitter(string $name): XPClass { public static function main(array $args) { if (empty($args)) return Usage::main($args); - $emitter= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; + $target= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; $in= $out= '-'; $quiet= false; $augment= []; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { - $emitter= $args[++$i]; + $target= $args[++$i]; } else if ('-q' === $args[$i]) { $quiet= true; } else if ('-o' === $args[$i]) { @@ -92,7 +92,7 @@ public static function main(array $args) { } $lang= Language::named('PHP'); - $emit= Emitter::forRuntime($emitter, $augment)->newInstance(); + $emit= Emitter::forRuntime($target, $augment)->newInstance(); foreach ($lang->extensions() as $extension) { $extension->setup($lang, $emit); } From fd48145e6309918456ac728666ecbf556d868e06 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 15 Feb 2022 22:10:52 +0100 Subject: [PATCH 03/10] Make lang.ast.Result a slimmer base class, extract code generation --- src/main/php/lang/ast/Result.class.php | 42 +-------- .../php/lang/ast/emit/GeneratedCode.class.php | 60 ++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 8 +- .../lang/ast/unittest/EmitterTest.class.php | 5 +- .../lang/ast/unittest/ResultTest.class.php | 94 +++---------------- .../unittest/emit/EmitterTraitTest.class.php | 5 +- .../ast/unittest/emit/EmittingTest.class.php | 10 +- .../unittest/emit/GeneratedCodeTest.class.php | 82 ++++++++++++++++ 8 files changed, 174 insertions(+), 132 deletions(-) create mode 100755 src/main/php/lang/ast/emit/GeneratedCode.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 23360b06..0b3e5175 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,7 +1,7 @@ out= $out; - $this->out->write($prolog); - $this->epilog= $epilog; $this->codegen= new CodeGen(); } - /** - * Creates a temporary variable and returns its name - * - * @return string - */ - public function temp() { - return '$'.$this->codegen->symbol(); - } - /** * Forwards output line to given line number * @@ -50,32 +36,10 @@ public function at($line) { return $this; } - /** - * Looks up a given type - * - * @param string $type - * @return lang.ast.emit.Type - */ - public function lookup($type) { - if ('self' === $type || 'static' === $type) { - return new Declaration($this->type[0], $this); - } else if ('parent' === $type) { - return $this->lookup($this->type[0]->parent); - } - - foreach ($this->type as $enclosing) { - if ($type === $enclosing->name) return new Declaration($enclosing, $this); - } - - return new Reflection($type); - } - /** @return void */ public function close() { if (null === $this->out) return; - // Write epilog, then close and ensure this doesn't happen twice - '' === $this->epilog || $this->out->write($this->epilog); $this->out->close(); unset($this->out); } diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php new file mode 100755 index 00000000..f986b502 --- /dev/null +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -0,0 +1,60 @@ +write($prolog); + $this->epilog= $epilog; + } + + /** + * Creates a temporary variable and returns its name + * + * @return string + */ + public function temp() { + return '$'.$this->codegen->symbol(); + } + + /** + * Looks up a given type + * + * @param string $type + * @return lang.ast.emit.Type + */ + public function lookup($type) { + if ('self' === $type || 'static' === $type) { + return new Declaration($this->type[0], $this); + } else if ('parent' === $type) { + return $this->lookup($this->type[0]->parent); + } + + foreach ($this->type as $enclosing) { + if ($type === $enclosing->name) return new Declaration($enclosing, $this); + } + + return new Reflection($type); + } + + /** @return void */ + public function close() { + if (null === $this->out) return; + + // Write epilog, then close and ensure this doesn't happen twice + '' === $this->epilog || $this->out->write($this->epilog); + $this->out->close(); + unset($this->out); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 8d5160d7..0e401e86 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -30,7 +30,7 @@ abstract class PHP extends Emitter { * @return lang.ast.Result */ protected function result($target) { - return new Result($target, 'out->write('(eval: \''); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["'" => "\\'", '\\' => '\\\\'])); + $out= $result->out; + $result->out= new Escaping($out, ["'" => "\\'", '\\' => '\\\\']); // If exactly one unnamed argument exists, emit its value directly if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) { @@ -479,7 +479,7 @@ protected function emitAnnotation($result, $annotation) { $result->out->write(']'); } - $result->out->redirect($out); + $result->out= $out; $result->out->write('\')'); return; } diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 3ed5b28e..e4c4476f 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -65,9 +65,10 @@ 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] diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index f31d72f5..3ded288c 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -1,6 +1,6 @@ temp(), $r->temp(), $r->temp()]); - } - - #[Test] - public function prolog_and_epilog_default_to_emtpy_strings() { - $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($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 Result(new StringWriter($out), ''); - $r->close(); - - Assert::equals('', $out->bytes()); + new Result(new MemoryOutputStream()); } #[Test] public function write() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out), '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), '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')); - } + $r->out= $out; + $r->out->write("'"); - #[Test, Expect(ClassNotFoundException::class)] - public function lookup_non_existant() { - $r= new Result(new StringWriter(new MemoryOutputStream())); - $r->lookup('\\NotFound'); + Assert::equals("'echo \'Hello\''", $out->bytes()); } #[Test] public function line_number_initially_1() { - $r= new Result(new StringWriter(new MemoryOutputStream())); + $r= new Result(new MemoryOutputStream()); Assert::equals(1, $r->line); } - #[Test, Values([[1, 'at($line)->out->write('test'); Assert::equals($expected, $out->bytes()); @@ -122,10 +56,10 @@ public function write_at_line($line, $expected) { #[Test] public function at_cannot_go_backwards() { $out= new MemoryOutputStream(); - $r= new Result(new StringWriter($out), 'at(0)->out->write('test'); - Assert::equals('bytes()); + Assert::equals('test', $out->bytes()); Assert::equals(1, $r->line); } } \ 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..2c91574b --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -0,0 +1,82 @@ +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'); + } +} \ No newline at end of file From 6ba6343c167d58611ea6b2a15aec5fe998f6d5c6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 16 Feb 2022 18:09:40 +0100 Subject: [PATCH 04/10] Fix PHP 7 compatibility --- src/main/php/lang/ast/emit/AttributesAsComments.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index 2b3cd209..5282490b 100755 --- a/src/main/php/lang/ast/emit/AttributesAsComments.class.php +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -33,13 +33,13 @@ protected function emitAnnotations($result, $annotations) { $line= $annotations->line; $result->out->write('#['); - $out= $result->out->stream(); - $result->out->redirect(new Escaping($out, ["\n" => " "])); + $out= $result->out; + $result->out= new Escaping($out, ["\n" => " "]); foreach ($annotations->named as $annotation) { $this->emitOne($result, $annotation); $result->out->write(','); } - $result->out->redirect($out); + $result->out= $out; $result->out->write("]\n"); $result->line= $line + 1; From 00687835ee10dc54218af4943003c2dcb1ac0304 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 16 Feb 2022 18:12:53 +0100 Subject: [PATCH 05/10] Remove need for temporary variables --- src/main/php/lang/ast/emit/AttributesAsComments.class.php | 5 ++--- src/main/php/lang/ast/emit/Escaping.class.php | 4 ++++ src/main/php/lang/ast/emit/PHP.class.php | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/AttributesAsComments.class.php b/src/main/php/lang/ast/emit/AttributesAsComments.class.php index 5282490b..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; - $result->out= 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= $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/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0e401e86..cf0c0628 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -463,8 +463,7 @@ protected function emitAnnotation($result, $annotation) { // Found first non-constant argument, enclose in `eval` $result->out->write('(eval: \''); - $out= $result->out; - $result->out= 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)) { @@ -479,7 +478,7 @@ protected function emitAnnotation($result, $annotation) { $result->out->write(']'); } - $result->out= $out; + $result->out= $result->out->original(); $result->out->write('\')'); return; } From e3470da2fba4b80f2df34bec9edcdeea0be8db48 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 17 Feb 2022 19:51:00 +0100 Subject: [PATCH 06/10] Move at() to GeneratedCode class --- src/main/php/lang/ast/Result.class.php | 17 ++++------- .../php/lang/ast/emit/GeneratedCode.class.php | 29 ++++++++++++++----- .../lang/ast/unittest/ResultTest.class.php | 26 ----------------- .../unittest/emit/GeneratedCodeTest.class.php | 26 +++++++++++++++++ 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 0b3e5175..e3a8978c 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -6,11 +6,9 @@ class Result implements Closeable { public $out; public $codegen; - public $line= 1; public $meta= []; public $locals= []; public $stack= []; - public $type= []; /** * Starts a result stream, including an optional prolog and epilog @@ -23,23 +21,20 @@ public function __construct(OutputStream $out) { } /** - * Forwards output line to given line number + * Finalize result. Guaranteed to be called *once* from within `close()`. + * Without implementation here - overwrite in subclasses. * - * @param int $line - * @return self + * @return void */ - public function at($line) { - if ($line > $this->line) { - $this->out->write(str_repeat("\n", $line - $this->line)); - $this->line= $line; - } - return $this; + protected function finalize() { + // NOOP } /** @return void */ public function close() { if (null === $this->out) return; + $this->finalize(); $this->out->close(); unset($this->out); } diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index f986b502..6d4fad51 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -4,6 +4,8 @@ class GeneratedCode extends Result { private $epilog; + public $line= 1; + public $type= []; /** * Starts a result stream, including an optional prolog and epilog @@ -19,6 +21,20 @@ public function __construct($out, $prolog= '', $epilog= '') { $this->epilog= $epilog; } + /** + * Forwards output line to given line number + * + * @param int $line + * @return self + */ + public function at($line) { + if ($line > $this->line) { + $this->out->write(str_repeat("\n", $line - $this->line)); + $this->line= $line; + } + return $this; + } + /** * Creates a temporary variable and returns its name * @@ -48,13 +64,12 @@ public function lookup($type) { return new Reflection($type); } - /** @return void */ - public function close() { - if (null === $this->out) return; - - // Write epilog, then close and ensure this doesn't happen twice + /** + * Write epilog + * + * @return void + */ + protected function finalize() { '' === $this->epilog || $this->out->write($this->epilog); - $this->out->close(); - unset($this->out); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ResultTest.class.php b/src/test/php/lang/ast/unittest/ResultTest.class.php index 3ded288c..3847680c 100755 --- a/src/test/php/lang/ast/unittest/ResultTest.class.php +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -36,30 +36,4 @@ public function write_escaped() { Assert::equals("'echo \'Hello\''", $out->bytes()); } - - #[Test] - public function line_number_initially_1() { - $r= new Result(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 Result($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 Result($out); - $r->at(0)->out->write('test'); - - Assert::equals('test', $out->bytes()); - Assert::equals(1, $r->line); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php index 2c91574b..3d1ed780 100755 --- a/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -79,4 +79,30 @@ 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 From 91850eb9364a8fa078fcf33aa82aa7019d57efcf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 17 Feb 2022 19:52:07 +0100 Subject: [PATCH 07/10] Move Result class to lang.ast.emit --- src/main/php/lang/ast/emit/GeneratedCode.class.php | 2 -- src/main/php/lang/ast/{ => emit}/Result.class.php | 3 ++- src/test/php/lang/ast/unittest/ResultTest.class.php | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) rename src/main/php/lang/ast/{ => emit}/Result.class.php (93%) diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 6d4fad51..2950ac45 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -1,7 +1,5 @@ Date: Thu, 17 Feb 2022 20:04:24 +0100 Subject: [PATCH 08/10] Move output line positioning to GeneratedCode class --- src/main/php/lang/ast/Emitter.class.php | 11 +++-------- src/main/php/lang/ast/emit/PHP.class.php | 11 +++++++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 2943bc76..148dd1e9 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -129,23 +129,17 @@ public function emitAll($result, $nodes) { */ public function emitOne($result, $node) { - // Inlined Result::at() - if ($node->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; @@ -153,6 +147,7 @@ public function emitOne($result, $node) { } // Fall through, use default } + $this->{'emit'.$node->kind}($result, $node); } diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index cf0c0628..e67ab4da 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -1034,4 +1034,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 From ba39d7f7a31a8a29fde8100ff461476a9e0e1134 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 17 Feb 2022 20:12:28 +0100 Subject: [PATCH 09/10] Add Result::initialize() --- .../php/lang/ast/emit/GeneratedCode.class.php | 33 ++++++++++++------- src/main/php/lang/ast/emit/Result.class.php | 13 +++++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/main/php/lang/ast/emit/GeneratedCode.class.php b/src/main/php/lang/ast/emit/GeneratedCode.class.php index 2950ac45..8fbe4d27 100755 --- a/src/main/php/lang/ast/emit/GeneratedCode.class.php +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -1,7 +1,7 @@ prolog= $prolog; + $this->epilog= $epilog; parent::__construct($out); + } - $out->write($prolog); - $this->epilog= $epilog; + /** + * Initialize result. Guaranteed to be called *once* from constructor. + * Without implementation here - overwrite in subclasses. + * + * @return void + */ + protected function initialize() { + '' === $this->prolog || $this->out->write($this->prolog); + } + + /** + * Write epilog + * + * @return void + */ + protected function finalize() { + '' === $this->epilog || $this->out->write($this->epilog); } /** @@ -61,13 +79,4 @@ public function lookup($type) { return new Reflection($type); } - - /** - * Write epilog - * - * @return void - */ - protected function finalize() { - '' === $this->epilog || $this->out->write($this->epilog); - } } \ 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 index 8562d688..a9b7dd58 100755 --- a/src/main/php/lang/ast/emit/Result.class.php +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -12,13 +12,24 @@ class Result implements Closeable { public $stack= []; /** - * Starts a result stream, including an optional prolog and epilog + * Starts a result stream. * * @param io.streams.OutputStream $out */ public function __construct(OutputStream $out) { $this->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 } /** From 192ac0948a2ed732797039206adabae8bacfd3cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 7 May 2022 12:11:13 +0200 Subject: [PATCH 10/10] MFH --- .../lang/ast/unittest/EmitterTest.class.php | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 87b1a7fc..347ff56e 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -109,19 +109,20 @@ public function transform_to_null() { #[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);