diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 00000000..9c3a5c3c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + tests: + if: "!contains(github.event.head_commit.message, 'skip ci')" + name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.php-versions >= '8.5' }} + strategy: + fail-fast: false + matrix: + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + os: [ubuntu-latest, windows-latest] + + steps: + - name: Configure git + if: runner.os == 'Windows' + run: git config --system core.autocrlf false; git config --system core.eol lf + + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP ${{ matrix.php-versions }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + ini-values: date.timezone=Europe/Berlin + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: > + curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.8.0.sh > xp-run && + composer install --prefer-dist && + echo "vendor/autoload.php" > composer.pth + + - name: Run test suite + run: sh xp-run xp.test.Runner -r Dots src/test/php diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index 1d41e73e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -# xp-framework/compiler - -language: php - -dist: trusty - -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4snapshot - - nightly - -matrix: - allow_failures: - - php: nightly - -before_script: - - curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-master.sh > xp-run - - composer install --prefer-dist - - echo "vendor/autoload.php" > composer.pth - -script: - - sh xp-run xp.unittest.TestRunner src/test/php diff --git a/ChangeLog.md b/ChangeLog.md index 8164add4..b5c18b7a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,587 @@ XP Compiler ChangeLog ## ?.?.? / ????-??-?? +* Merged PR #187: Add PHP 8.5 emitter - @thekid + +## 9.5.0 / 2025-05-28 + +* Merged PR #181: Add support for pipelines with `|>` and `?|>`, see + https://wiki.php.net/rfc/pipe-operator-v3 and issue #180 + (@thekid) + +## 9.4.0 / 2025-04-05 + +* Merged PR #184: Add support for final properties added in PHP 8.4, see + https://wiki.php.net/rfc/property-hooks; including parameter promotion, + see https://wiki.php.net/rfc/final_promotion + (@thekid) + +## 9.3.3 / 2025-03-02 + +* Fixed callable new syntax when using a variable or expression, e.g. + `new $class(...)`. See also https://github.com/php/php-src/issues/12336 + (@thekid) + +## 9.3.2 / 2024-11-02 + +* Fixed empty match expressions and match expressions with default + case only in PHP 7 + (@thekid) +* Added tests verifying closures are supported in constant expressions + https://wiki.php.net/rfc/closures_in_const_expr + (@thekid) + +## 9.3.1 / 2024-10-05 + +* Fixed `private(set)` not being implicitely marked as *final*, see + https://wiki.php.net/rfc/asymmetric-visibility-v2#inheritance + (@thekid) +* Fixed enclosing scopes when using references - @thekid + +## 9.3.0 / 2024-08-31 + +* Fixed checks for property hooks emulation with asymmetric visibility + (@thekid) +* Added PHP 8.4 emitter which natively emits property hooks and asymmetric + visibility syntax. This is integration-tested with PHP 8.4.0 Beta 4. + See https://github.com/php/php-src/blob/php-8.4.0beta4/NEWS + (@thekid) +* Changed emitter to use native readonly classes in PHP 8.2, fixing an + inconsistency with accessing undefined properties + (@thekid) + +## 9.2.0 / 2024-08-27 + +* Merged PR #183: Add emitting support for asymmetric visibility. See + https://wiki.php.net/rfc/asymmetric-visibility-v2, targeted for PHP 8.4 + (@thekid) + +## 9.1.1 / 2024-08-27 + +* Forward compatibility with newer `xp-framework/ast` releases - @thekid + +## 9.1.0 / 2024-06-15 + +* Merged PR #166: Implement property hooks via virtual properties, see + https://wiki.php.net/rfc/property-hooks. Includes support for native + implementation, which is yet to be merged (php/php-src#13455). Thus, + this still might be a moving target in some regards! + (@thekid) + +## 9.0.0 / 2024-03-23 + +* Merged PR #179: XP 12 compatibility, dropping PHP 7.0 - 7.3 support! + (@thekid) +* Removed deprecated *lookup()* from `lang.ast.emit.GeneratedCode` + (@thekid) + +## 8.17.2 / 2024-03-23 + +* Fixed *implicitely nullable type* warnings for parameters with non- + constant expressions (e.g. `$param= new Handle(0)`) in PHP 8.4, see + https://wiki.php.net/rfc/deprecate-implicitly-nullable-types + (@thekid) + +## 8.17.1 / 2024-01-06 + +* Fixed emitting captures and return types for closures - @thekid + +## 8.17.0 / 2023-10-03 + +* Merged PR #177: Remove Result::$stack. Use local variables for backing + up and restoring locals instead. Slight performance improvement. + (@thekid) +* Fixed issue #176: Lambda parameters bleeding into locals - @thekid + +## 8.16.0 / 2023-10-01 + +* Merged PR #175: Transform multiple nodes without creating statements + (@thekid) +* Refactored code base to use the new reflection library instead of + the *Package* class from `lang.reflect`. See xp-framework/rfc#338 + (@thekid) + +## 8.15.1 / 2023-07-16 + +* Fixed parent constructors not being invoked when using non-constant + property initialization. + (@thekid) + +## 8.15.0 / 2023-07-16 + +* Merged PR #173: Check for `#[Override]` annotation, implementing this + PHP 8.3 RFC: https://wiki.php.net/rfc/marking_overriden_methods + (@thekid) +* Merged PR #171: Refactor scope lookup from Result to CodeGen - @thekid + +## 8.14.0 / 2023-07-15 + +* Fixed error *Cannot access offset of type array on array* when using + reflection for non-constant parameter defaults + (@thekid) +* Implemented feature #162: Arbitrary static variable initializers, see + https://wiki.php.net/rfc/arbitrary_static_variable_initializers + (@thekid) + +## 8.13.0 / 2023-06-04 + +* Merged PR #169: Refactor how annotations with non-constant arguments + are emitted. This enables a transition to `xp-framework/reflection` + as proposed in xp-framework/rfc#338 + (@thekid) + +## 8.12.0 / 2023-05-21 + +* Merged PR #168: Return by reference from methods and functions, see + https://www.php.net/manual/en/language.references.return.php + (@thekid) +* Make it possible to refer to constructor parameters inside property + initialization expressions. + (@thekid) + +## 8.11.0 / 2023-04-17 + +* Merged PR #165: Omit constant types for all PHP versions < 8.3. This + is the second part of #157, and fully implements typed constants, + https://wiki.php.net/rfc/typed_class_constants + (@thekid) +* Merged PR #163: Emit meta data for class constants. This makes types + accessible via reflection and is part of #157. + (@thekid) + +## 8.10.0 / 2023-04-08 + +* Merged PR #161: Support dynamic class constant fetch, the first PHP + 8.3 feature: https://wiki.php.net/rfc/dynamic_class_constant_fetch + (@thekid) + +## 8.9.0 / 2023-02-19 + +* Merged PR #159: Implement command line option to set file extension + for compiled files. This makes integrating with PSR-4 autoloading an + easy task, see https://www.php-fig.org/psr/psr-4/ + (@thekid) + +## 8.8.5 / 2023-02-19 + +* Fixed issue #160: Fatal error: Label 'c1107822099' already defined + (@thekid) + +## 8.8.4 / 2023-02-19 + +* Fixed optimization for `::class` constant resolution - @thekid + +## 8.8.3 / 2023-02-12 + +* Merged PR #155: Do not error at compile time for unresolved classes + (@thekid) + +## 8.8.2 / 2023-02-12 + +* Fixed emitted meta information for generic declarations - @thekid +* Merged PR #153: Migrate to new testing library - @thekid + +## 8.8.1 / 2023-01-29 + +* Added flag to meta information to resolve ambiguity when exactly one + unnamed annotation argument is present and an array or NULL. See + xp-framework/reflection#27 + (@thekid) + +## 8.8.0 / 2022-12-04 + +* Merged PR #152: Make enclosing type(s) accessible via code generator + (@thekid) + +## 8.7.0 / 2022-11-12 + +* Fixed issue #31: Support `list()` reference assignment for PHP < 7.3 + (@thekid) +* Fixed `foreach` statement using destructuring assignments in PHP 7.0 + (@thekid) +* Merged PR #146: Add support for omitting expressions in destructuring + assignments, e.g. `list($a, , $b)= $expr` or `[, $a]= $expr`. + (@thekid) +* Merged PR #145: Add support for specifying keys in list(), which has + been in PHP since 7.1.0, see https://wiki.php.net/rfc/list_keys + (@thekid) +* Merged PR #144: Rewrite `list(&$a)= $expr` for PHP 7.0, 7.1 and 7.2 + (@thekid) + +## 8.6.0 / 2022-11-06 + +* Bumped dependency on `xp-framework/ast` to version 9.1.0 - @thekid +* Merged PR #143: Adapt to AST type refactoring (xp-framework/ast#39) + (@thekid) + +## 8.5.1 / 2022-09-03 + +* Fixed issue #142: Nullable type unions don't work as expected - @thekid + +## 8.5.0 / 2022-07-20 + +* Merged PR #140: Add support for null, true and false types - @thekid + +## 8.4.0 / 2022-05-14 + +* Merged PR #138: Implement readonly modifier for classes - a PHP 8.2 + feature, see https://wiki.php.net/rfc/readonly_classes + (@thekid) +* Merged PR #135: Move responsibility for creating result to emitter + (@thekid) + +## 8.3.0 / 2022-03-07 + +* Made JIT class loader ignore *autoload.php* files - @thekid +* Fixed #136: Line number inconsistent after multi-line doc comments + (@thekid) + +## 8.2.0 / 2022-01-30 + +* Support passing emitter-augmenting class names to the instanceFor() + method of `lang.ast.CompilingClassLoader`. + (@thekid) + +## 8.1.0 / 2022-01-29 + +* Merged PR #131: Inline nullable checks when casting - @thekid + +## 8.0.0 / 2022-01-16 + +This release is the first in a series of releases to make the XP compiler +more universally useful: Compiled code now doesn't include generated XP +meta information by default, and is thus less dependant on XP Core, see +https://github.com/xp-framework/compiler/projects/4. + +* Merged PR #129: Add augmentable emitter to create property type checks + for PHP < 7.4 + (@thekid) +* Fixed private and protected readonly properties being accessible from + any scope in PHP < 8.1 + (@thekid) +* Merged PR #127: Do not emit XP meta information by default. This is + the first step towards generating code that runs without a dependency + on XP core. + (@thekid) + +## 7.3.0 / 2022-01-07 + +* Merged PR #128: Add support for static closures - @thekid +* Upgraded dependency on `xp-framework/ast` to version 8.0.0 - @thekid + +## 7.2.1 / 2021-12-28 + +* Fixed PHP 8.1 not emitting native callable syntax - @thekid +* Fixed `isConstant()` for constant arrays - @thekid + +## 7.2.0 / 2021-12-20 + +* Optimized generated code for arrays including unpack expressions for + PHP 8.1+, which natively supports unpacking with keys. See + https://wiki.php.net/rfc/array_unpacking_string_keys + (@thekid) + +## 7.1.0 / 2021-12-08 + +* Added preliminary PHP 8.2 support by fixing various issues throughout + the code base and adding a PHP 8.2 emitter. + (@thekid) + +## 7.0.0 / 2021-10-21 + +This major release drops compatiblity with older XP versions. + +* Made compatible with XP 11 - @thekid +* Implemented xp-framework/rfc#341, dropping compatibility with XP 9 + (@thekid) + +## 6.11.0 / 2021-10-06 + +* Merged PR #125: Support `new T(...)` callable syntax - @thekid + +## 6.10.0 / 2021-09-12 + +* Implemented feature request #123: Use `php:X.Y` instead of *PHP.X.Y* + for target runtimes. The older syntax is still supported! + (@thekid) + +## 6.9.0 / 2021-09-12 + +* Merged PR #124: Add support for readonly properties. Implements feature + request #115, using native code for PHP 8.1 and simulated via virtual + properties for PHP 7.X and PHP 8.0 + (@thekid) + +## 6.8.3 / 2021-09-11 + +* Fixed types not being emitted for promoted properties - @thekid + +## 6.8.2 / 2021-09-09 + +* Fixed *Call to undefined method ...::emitoperator())* which do not + provide any context as to where an extraneous operator was encountered. + (@thekid) + +## 6.8.1 / 2021-08-16 + +* Fixed issue #122: Undefined constant `Sources::FromEmpty` (when using + PHP 8.1 native enumerations in PHP 8.0 and lower *and* the JIT compiler) + (@thekid) + +## 6.8.0 / 2021-08-04 + +* Merged PR #120: Support for PHP 8.1 intersection types, accessible via + reflection in all PHP versions, and runtime type-checked with 8.1.0+ + (@thekid) + +## 6.7.0 / 2021-07-12 + +* Changed emitter to omit extra newlines between members, making line + numbers more consistent with the original code. + (@thekid) +* Merged PR #114: Implements first-class callable syntax: `strlen(...)` + now returns a closure which if invoked with a string argument, returns + its length. Includes support for static and instance methods as well as + indirect references like `$closure(...)` and `self::{$expression}(...)`, + see https://wiki.php.net/rfc/first_class_callable_syntax + (@thekid) + +## 6.6.0 / 2021-07-10 + +* Emit null-coalesce operator as `$a ?? $a= expression` instead of as + `$a= $a ?? expression`, saving one assignment operation for non-null + case. Applies to PHP 7.0, 7.1 and 7.2. + (@thekid) +* Removed conditional checks for PHP 8.1 with native enum support, all + releases and builds available on CI systems now contain it. + (@thekid) +* Increased test coverage significantly (to more than 90%), especially + for classes used by the compiler command line. + (@thekid) + +## 6.5.0 / 2021-05-22 + +* Merged PR #111: Add support for directives using declare - @thekid + +## 6.4.0 / 2021-04-25 + +* Merged PR #110: Rewrite `never` return type to void in PHP < 8.1, adding + support for this PHP 8.1 feature. XP Framework reflection supports this + as of its 10.10.0 release. + (@thekid) + +## 6.3.2 / 2021-03-14 + +* Allowed `new class() extends self` inside class declarations - @thekid + +## 6.3.1 / 2021-03-14 + +* Fix being able to clone enum lookalikes - @thekid +* Fix `clone` operator - @thekid + +## 6.3.0 / 2021-03-13 + +* Merged PR #106: Compile PHP enums to PHP 7/8 lookalikes, PHP 8.1 native. + (@thekid) + +## 6.2.0 / 2021-03-06 + +* Merged PR #104: Support arbitrary expressions in property initializations + and parameter defaults + (@thekid) + +## 6.1.1 / 2021-01-04 + +* Fixed issue #102: Call to a member function children()... - @thekid +* Fixed issue #102: PHP 8.1 compatibility - @thekid +* Fixed issue #103: Nullable types - @thekid + +## 6.1.0 / 2021-01-04 + +* Included languages and emitters in `xp compile` output - @thekid + +## 6.0.0 / 2020-11-28 + +This major release removes legacy XP and Hack language annotations as +well as curly braces for string and array offsets. It also includes the +first PHP 8.1 features. + +* Added `-q` command line option which suppresses all diagnostic output + from the compiler except for errors + (@thekid) +* Removed support for using curly braces as offset (e.g. `$value{0}`) + (@thekid) +* Merged PR #92: Add support for explicit octal integer literal notation + See https://wiki.php.net/rfc/explicit_octal_notation (PHP 8.1) + (@thekid) +* Merged PR #93: Allow match without expression: `match { ... }`. See + https://wiki.php.net/rfc/match_expression_v2#allow_dropping_true + (@thekid) +* Removed support for legacy XP and Hack language annotations, see #86 + (@thekid) +* Merged PR #96: Enclose blocks where PHP only allows expressions. This + not only allows `fn() => { ... }` but also using blocks in `match`. + (@thekid) + +## 5.7.0 / 2020-11-26 + +* Verified full PHP 8 support now that PHP 8.0.0 has been released, + see https://www.php.net/archive/2020.php#2020-11-26-3 + (@thekid) +* Merged PR #95: Support compiling to XAR archives - @thekid + +## 5.6.0 / 2020-11-22 + +* Merged PR #94: Add support for `static` return type - @thekid +* Optimized null-safe instance operator for PHP 8.0 - @thekid +* Added PHP 8.1-dev to test matrix now that is has been branched + (@thekid) +* Added support for non-capturing catches, see this PHP 8 RFC: + https://wiki.php.net/rfc/non-capturing_catches + (@thekid) + +## 5.5.0 / 2020-11-15 + +* Merged PR #91 - Refactor rewriting type literals: + - Changed implementation to be easier to maintain + - Emit function types as `callable` in all PHP versions + - Emit union types as syntax in PHP 8+ + (@thekid) + +## 5.4.1 / 2020-10-09 + +* Fixed #90: Namespace declaration statement has to be the very first + statement, which occured with PHP 8.0.0RC1 + (@thekid) + +## 5.4.0 / 2020-09-12 + +* Implemented second step for #86: Add an E_DEPRECATED warning to the + hacklang annotation syntax `<<...>>`; details in xp-framework/ast#9 + (@thekid) +* Merged PR #89: Add annotation type mappings to `TARGET_ANNO` detail + (@thekid) +* Changed PHP 8 attributes to be emitted in XP meta information without + namespaces, and with their first characters lowercased. This way, code + using annotations will continue to work, see xp-framework/rfc#336. + (@thekid) + +## 5.3.0 / 2020-09-12 + +* Merged PR #88: Emit named arguments for PHP 8 - @thekid + +## 5.2.1 / 2020-09-09 + +* Adjusted to `xp-framework/ast` yielding comments as-is, transform + them to the form XP meta information expects. + (@thekid) + +## 5.2.0 / 2020-07-20 + +* Merged PR #87: Add support for match expression - @thekid +* Implemented first step of #86: Support PHP 8 attributes - @thekid +* Removed `lang.ast.syntax.php.NullSafe` in favor of builtin support + (@thekid) +* Merged PR #84: Extract parser - @thekid + +## 5.1.3 / 2020-04-04 + +* Allowed `::class` on objects (PHP 8.0 forward compatibility) - @thekid + +## 5.1.2 / 2020-04-04 + +* Fixed promotion for by-reference arguments - @thekid + +## 5.1.1 / 2020-03-29 + +* Fixed ternary and instanceof operators' precedence - @thekid + +## 5.1.0 / 2020-03-28 + +* Merged PR #82: Allow chaining scope resolution operator `::` - @thekid +* Merged PR #81: Allow `instanceof ()` as syntax - @thekid +* Merged PR #80: Allow `new ()` as syntax - @thekid + +## 5.0.0 / 2019-11-30 + +This major release drops PHP 5 support. The minimum required PHP version +is now 7.0.0. + +* Merged PR #70: Extract compact methods; to use these, require the + library https://github.com/xp-lang/php-compact-methods + (@thekid) +* Merged PR #79: Convert testsuite to baseless tests - @thekid +* Merged PR #78: Deprecate curly brace syntax for offsets; consistent + with PHP 7.4 + (@thekid) +* Added support for XP 10 and newer versions of library dependencies + (@thekid) +* Implemented xp-framework/rfc#334: Drop PHP 5.6. The minimum required + PHP version is now 7.0.0! + (@thekid) + +## 4.3.1 / 2019-11-30 + +* Added compatibility with XP 10, see xp-framework/rfc#333 - @thekid + +## 4.3.0 / 2019-11-24 + +* Fixed global constants in ternaries being ambiguous with goto labels + (@thekid) +* Fixed emitting `switch` statements and case labels' ambiguity w/ goto + (@thekid) +* Fixed an operator precedence problem causing incorrect nesting in the + parsed AST for unary prefix operators. + (@thekid) +* Merged PR #77: Add support for #-style comments including support for + XP style annotations + (@thekid) + +## 4.2.1 / 2019-10-05 + +* Fixed parser to allow "extending" final and abstract types - @thekid + +## 4.2.0 / 2019-10-04 + +* Fixed issue #74: No longer shadow compiler errors in certain cases + (@thekid) +* Merged PR #75: Add "ast" subcommand to display the abstract syntax tree + (@thekid) + +## 4.1.0 / 2019-10-01 + +* Merged PR #73: Add support for annotations in anonymous classes + (@thekid) + +## 4.0.0 / 2019-09-09 + +This major release adds an extension mechanisms. Classes inside the package +`lang.ast.syntax.php` (regardless of their class path) will be loaded auto- +matically on startup. + +* Merged PR #69: Remove support for Hack arrow functions - @thekid +* Fixed operator precedence for unary prefix operators - @thekid +* Merged PR #66: Syntax plugins. With this facility in place, the compiler + can be extended much like [Babel](https://babeljs.io/docs/en/plugins). + This is useful for adapting features which may or may not make it into + PHP one day. Current extensions like compact methods are kept for BC + reasons, but will be extracted into their own libraries in the future! + (@thekid) + +## 3.0.0 / 2019-08-10 + +This release aligns XP Compiler compatible with PHP 7.4 and changes it +to try to continue parsing after an error has occured, possibly yielding +multiple errors. + +* Made compatible with PHP 7.4 - refrain using `{}` for string offsets + (@thekid) +* Merged PR #45 - Multiple errors - @thekid +* Changed compiler to emit deprecation warnings for Hack language style + arrow functions and compact methods using `==>`, instead advocating the + use of PHP 7.4 with the `fn` keyword; see issue #65 + (@thekid) + ## 2.13.0 / 2019-06-15 * Added preliminary PHP 8 support - see #62 (@thekid) @@ -149,6 +730,10 @@ XP Compiler ChangeLog ## 2.0.0 / 2017-11-06 +This major release extracts the AST API to its own library, and cleans it +up while doing so. The idea is to be able to use this library in other +places in the future. + * Implemented `use function` and `use const` - @thekid * Fixed issue #21: Comments are not escaped - @thekid * Project [AST API](https://github.com/xp-framework/compiler/projects/1): @@ -201,6 +786,11 @@ XP Compiler ChangeLog ## 1.0.0 / 2017-10-25 +This first release brings consistency to annotations, casting and how +and where keywords can be used. XP Compiler is already being used in +production in an internal project at the time of writing, so you might +do so too:) + * Indexed type members by name; implementing feature suggested in #10 (@thekid) * **Heads up:** Implemented syntax for parameter annotations as stated diff --git a/README.md b/README.md index ba02bb9d..74f99e88 100755 --- a/README.md +++ b/README.md @@ -1,48 +1,105 @@ XP Compiler =========== -[![Build Status on TravisCI](https://secure.travis-ci.org/xp-forge/sequence.svg)](http://travis-ci.org/xp-framework/compiler) +[![Build status on GitHub](https://github.com/xp-framework/compiler/workflows/Tests/badge.svg)](https://github.com/xp-framework/compiler/actions) [![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core) [![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md) -[![Required PHP 5.6+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-5_6plus.png)](http://php.net/) -[![Supports PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.png)](http://php.net/) -[![Supports HHVM 3.20+](https://raw.githubusercontent.com/xp-framework/web/master/static/hhvm-3_20plus.png)](http://hhvm.com/) -[![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler) +[![Requires PHP 7.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_4plus.svg)](http://php.net/) +[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/) +[![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.svg)](https://packagist.org/packages/xp-framework/compiler) Compiles future PHP to today's PHP. Usage ----- -After adding the compiler to your project via `composer require xp-framework/compiler` classes will be passed through the compiler during autoloading. Code inside files with a *.class.php* ending is considered already compiled; files need to renamed `T.class.php` => `T.php` in order to be picked up. +After adding the compiler to your project via `composer require xp-framework/compiler`, it will hook into the class loading chain and compile `.php`-files on-demand. This keeps the efficient code-save-reload/rerun development process typical for PHP. Example ------- -The following code uses Hack, PHP 7.3, PHP 7.2, PHP 7.1 and PHP 7.0 features but runs on anything >= PHP 5.6. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. +The following code uses Hack language, PHP 8.4, PHP 8.3, PHP 8.2, 8.1 and 8.0 features but runs on anything >= PHP 7.4. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary. ```php > +#[Author('Timm Friebe')] +#[Permissions(0o777)] class HelloWorld { - public const GREETING = 'Hello'; + private const string GREETING= 'Hello'; public static function main(array $args): void { $greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from; - $author= Type::forName(self::class)->getAnnotation('author'); + $author= Reflection::type(self::class)->annotation(Author::class)->argument(0); - Console::writeLine($greet($args[0] ?? 'World', $author)); + Console::writeLine(new Date()->toString(), ': ', $greet($args[0] ?? 'World', from: $author)); } } ``` +To run this code, use `xp -m /path/to/xp/reflection HelloWorld` in a terminal window. + +Compilation +----------- +Compilation can also be performed explicitely by invoking the compiler: + +```bash +# Compile code and write result to a class file +$ xp compile HelloWorld.php HelloWorld.class.php + +# Compile standard input and write to standard output. +$ echo " [] + +@FileSystemCL<./vendor/xp-framework/compiler/src/main/php> +lang.ast.emit.PHP74 +lang.ast.emit.PHP80 +lang.ast.emit.PHP81 +lang.ast.emit.PHP82 +lang.ast.emit.PHP83 [*] +lang.ast.emit.PHP84 +lang.ast.emit.PHP85 +lang.ast.syntax.php.Using [*] + +@FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php> +lang.ast.syntax.php.IsOperator +``` Implementation status --------------------- @@ -54,5 +111,6 @@ To contribute, open issues and/or pull requests. See also -------- -* [XP RFC #0299: Make XP compiler the TypeScript of PHP](https://github.com/xp-framework/rfc/issues/299) +* [XP Compiler design](https://github.com/xp-framework/compiler/wiki/Compiler-design) +* [XP RFC #0299: Make XP compiler the Babel of PHP](https://github.com/xp-framework/rfc/issues/299) * [XP RFC #0327: Compile-time metaprogramming](https://github.com/xp-framework/rfc/issues/327) \ No newline at end of file diff --git a/bin/xp.xp-framework.compiler.ast b/bin/xp.xp-framework.compiler.ast new file mode 100755 index 00000000..5837670f --- /dev/null +++ b/bin/xp.xp-framework.compiler.ast @@ -0,0 +1,2 @@ +#!/usr/bin/env php +This must be run from within an XP runner \ No newline at end of file diff --git a/composer.json b/composer.json index 073135fa..4ce18abd 100755 --- a/composer.json +++ b/composer.json @@ -3,21 +3,18 @@ "type" : "library", "homepage" : "http://xp-framework.net/", "license" : "BSD-3-Clause", - "description" : "AST for the XP Framework", + "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^9.0 | ^8.0 | ^7.0 | ^6.10", - "xp-framework/tokenize": "^8.0", - "xp-framework/ast": "^1.3", - "php" : ">=5.6.0" + "xp-framework/core": "^12.0 | ^11.6 | ^10.16", + "xp-framework/reflection": "^3.2 | ^2.15", + "xp-framework/ast": "^11.6", + "php" : ">=7.4.0" }, "require-dev" : { - "xp-framework/unittest": "^9.3" + "xp-framework/test": "^2.0 | ^1.5" }, - "suggest" : { - "xp-framework/core": "^9.4" - }, - "bin": ["bin/xp.xp-framework.compiler.compile"], + "bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"], "autoload" : { "files" : ["src/main/php/autoload.php"] } diff --git a/src/main/php/lang/ast/CodeGen.class.php b/src/main/php/lang/ast/CodeGen.class.php new file mode 100755 index 00000000..4f1b936a --- /dev/null +++ b/src/main/php/lang/ast/CodeGen.class.php @@ -0,0 +1,67 @@ +id++); } + + public function enter($scope) { + array_unshift($this->scope, $scope); + return $scope; + } + + public function leave() { + return array_shift($this->scope); + } + + /** + * Search a given scope recursively for nodes with a given kind + * + * @param lang.ast.Node $node + * @param string $kind + * @return iterable + */ + public function search($node, $kind) { + if ($node->kind === $kind) yield $node; + + foreach ($node->children() as $child) { + foreach ($this->search($child, $kind) as $result) { + yield $result; + } + } + } + + /** + * Looks up a given type + * + * @param string $type + * @return lang.ast.emit.Type + */ + public function lookup($type) { + $enclosing= $this->scope[0] ?? null; + + if ('self' === $type || 'static' === $type) { + return new Declaration($enclosing->type, $this); + } else if ('parent' === $type) { + return isset($enclosing->type->parent) ? $this->lookup($enclosing->type->parent->literal()) : null; + } + + foreach ($this->scope as $scope) { + if ($scope->type->name && $type === $scope->type->name->literal()) { + return new Declaration($scope->type, $this); + } + } + + if (class_exists($type) || interface_exists($type) || trait_exists($type) || enum_exists($type)) { + return new Reflection($type); + } else { + return new Incomplete($type); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Compiled.class.php b/src/main/php/lang/ast/Compiled.class.php index 8902deed..361c9585 100755 --- a/src/main/php/lang/ast/Compiled.class.php +++ b/src/main/php/lang/ast/Compiled.class.php @@ -1,14 +1,34 @@ getResourceAsStream($file); + return self::parse($source[0], $stream->in(), $version, new self(), $file)->compiled; + } + + private static function language($name, $emitter) { + $language= Language::named(strtoupper($name)); + foreach ($language->extensions() as $extension) { + $extension->setup($language, $emitter); + } + return $language; + } + + private static function parse($lang, $in, $version, $out, $file) { + $language= self::$lang[$lang] ?? self::$lang[$lang]= self::language($lang, self::$emit[$version]); + try { + return self::$emit[$version]->write($language->parse(new Tokens($in, $file))->stream(), $out); + } finally { + $in->close(); + } + } /** * Opens path @@ -19,25 +39,11 @@ class Compiled implements OutputStream { * @param string $opened */ public function stream_open($path, $mode, $options, &$opened) { - list($version, $file)= explode('://', $path); - $stream= self::$source[$file]->getResourceAsStream($file); - $in= $stream->in(); - - try { - $parse= new Parse(new Tokens(new StreamTokenizer($in)), $file); - $emitter= self::$emit[$version]->newInstance($this); - foreach (Transformations::registered() as $kind => $function) { - $emitter->transform($kind, $function); - } - $emitter->emit($parse->execute()); - $opened= $stream->getURI(); - return true; - } catch (Error $e) { - $message= sprintf('Syntax error in %s, line %d: %s', $e->getFile(), $e->getLine(), $e->getMessage()); - throw new ClassFormatException($message); - } finally { - $in->close(); - } + [$version, $file]= explode('://', $path); + $stream= self::$source[$file][1]->getResourceAsStream($file); + self::parse(self::$source[$file][0], $stream->in(), $version, $this, $file); + $opened= $stream->getURI(); + return true; } /** @param string $bytes */ @@ -45,12 +51,12 @@ public function write($bytes) { $this->compiled.= $bytes; } - /** @return void */ + /** @codeCoverageIgnore */ public function flush() { // NOOP } - /** @return void */ + /** @codeCoverageIgnore */ public function close() { // NOOP } @@ -67,12 +73,6 @@ public function stream_read($count) { return $chunk; } - /** @return [:var] */ - public function url_stat($path) { - $opened= substr($path, strpos($path, '://') + 3); - return ['size' => self::$source[$opened]->getResourceAsStream($opened)->size()]; - } - /** @return [:var] */ public function stream_stat() { return ['size' => strlen($this->compiled)]; @@ -88,8 +88,15 @@ public function stream_close() { // NOOP } - /** @return void */ - public function stream_flush() { - // NOOP + /** + * Stream wrapper method stream_set_option + * + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + */ + public function stream_set_option($option, $arg1, $arg2) { + return true; } } \ No newline at end of file diff --git a/src/main/php/lang/ast/CompilingClassloader.class.php b/src/main/php/lang/ast/CompilingClassloader.class.php index 929a352e..02031d10 100755 --- a/src/main/php/lang/ast/CompilingClassloader.class.php +++ b/src/main/php/lang/ast/CompilingClassloader.class.php @@ -1,24 +1,36 @@ true, '__xp.php' => true]; private static $instance= []; private $version; + private $source= []; + + static function __static() { + + // See https://github.com/xp-framework/compiler/issues/122 + Reflection::__static(); + } /** Creates a new instances with a given PHP runtime */ private function __construct($emit) { - $this->version= $emit->getSimpleName(); - Compiled::$emit[$this->version]= $emit; + $this->version= strtr($emit->getSimpleName(), ['⋈' => '+', '·' => '.']); + Compiled::$emit[$this->version]= $emit->newInstance(); stream_wrapper_register($this->version, Compiled::class); } @@ -33,7 +45,7 @@ protected function locateSource($class) { $uri= strtr($class, '.', '/').self::EXTENSION; foreach (ClassLoader::getDefault()->getLoaders() as $loader) { if ($loader instanceof self) continue; - if ($loader->providesResource($uri)) return $this->source[$class]= $loader; + if ($loader->providesResource($uri)) return $this->source[$class]= [substr(self::EXTENSION, 1), $loader]; } return null; } @@ -48,7 +60,9 @@ protected function locateSource($class) { */ public function providesUri($uri) { if (isset($this->source[$uri])) return true; - if (0 !== substr_compare($uri, self::EXTENSION, -4)) return false; + + $e= strlen(self::EXTENSION); + if (0 !== substr_compare($uri, self::EXTENSION, -$e)) return false; if (0 === substr_compare($uri, \xp::CLASS_FILE_EXT, -strlen(\xp::CLASS_FILE_EXT))) return false; foreach (ClassLoader::getDefault()->getLoaders() as $loader) { @@ -56,7 +70,7 @@ public function providesUri($uri) { $l= strlen($loader->path); if (0 === substr_compare($loader->path, $uri, 0, $l)) { - $this->source[$uri]= strtr(substr($uri, $l, -4), [DIRECTORY_SEPARATOR => '.']); + $this->source[$uri]= strtr(substr($uri, $l, -$e), [DIRECTORY_SEPARATOR => '.']); return true; } } @@ -105,6 +119,7 @@ public function packageContents($package) { foreach (ClassLoader::getDefault()->getLoaders() as $loader) { if ($loader instanceof self) continue; foreach ($loader->packageContents($package) as $content) { + if (isset(self::$ignore[$content])) continue; if (self::EXTENSION === substr($content, $p= strpos($content, '.'))) { $r[]= substr($content, 0, $p).\xp::CLASS_FILE_EXT; } @@ -164,8 +179,8 @@ public function loadClass0($class) { try { include($this->version.'://'.$uri); } catch (ClassLoadingException $e) { - unset(\xp::$cl[$class]); - throw $e; + unset(\xp::$cl[$class]); // @codeCoverageIgnore + throw $e; // @codeCoverageIgnore } catch (\Throwable $e) { unset(\xp::$cl[$class]); throw new ClassFormatException('Compiler error: '.$e->getMessage(), $e); @@ -183,6 +198,21 @@ public function loadClass0($class) { return $name; } + /** + * Loads class bytes + * + * @param string $class + * @return string + * @throws lang.ClassLoadingException + */ + public function loadClassBytes($class) { + if (null === ($source= $this->locateSource($class))) { + throw new ClassNotFoundException($class); + } + + return Compiled::bytes($this->version, $source, strtr($class, '.', '/').self::EXTENSION); + } + /** * Gets a resource * @@ -215,13 +245,14 @@ public function instanceId() { } /** - * Fetch instance of classloader by path + * Fetch instance of classloader by version * - * @param string path the identifier - * @return lang.IClassLoader + * @param string $version + * @return lang.IClassLoader */ public static function instanceFor($version) { - $emit= Emitter::forRuntime($version); + sscanf($version, "%[^+]+%[^\r]", $emitter, $augmented); + $emit= Emitter::forRuntime($emitter, $augmented ? explode('+', $augmented) : [XpMeta::class]); $id= $emit->getName(); if (!isset(self::$instance[$id])) { @@ -257,4 +288,4 @@ public function hashCode() { public function compareTo($value) { return $value instanceof self ? version_compare($this->version, $value->version) : 1; } -} +} \ No newline at end of file diff --git a/src/main/php/lang/ast/Emitter.class.php b/src/main/php/lang/ast/Emitter.class.php index 16e8aef7..987a532f 100755 --- a/src/main/php/lang/ast/Emitter.class.php +++ b/src/main/php/lang/ast/Emitter.class.php @@ -1,896 +1,192 @@ providesClass($impl)) return $p->loadClass($impl); + $impl= "lang.ast.emit.{$engine}{$major}{$minor}"; + if ($cl->providesClass($impl)) { + if (empty($emitters)) return $cl->loadClass($impl); + + // Extend loaded class, including all given emitters + $extended= ['kind' => 'class', 'extends' => [$cl->loadClass($impl)], 'implements' => [], 'use' => []]; + foreach ($emitters as $class) { + if ($class instanceof XPClass) { + $impl.= '⋈'.strtr($class->getName(), ['.' => '·']); + $extended['use'][]= $class; + } else { + $impl.= '⋈'.strtr($class, ['.' => '·', '\\' => '·']); + $extended['use'][]= XPClass::forName($class); + } + } + return ClassLoader::defineType($impl, $extended, '{}'); + } } while ($minor-- > 0); throw new IllegalArgumentException('XP Compiler does not support '.$runtime.' yet'); } - /** @param io.streams.Writer */ - public function __construct($out) { - $this->out= $out; - $this->id= 0; - } - - public function transform($kind, $function) { - $this->transformations[$kind]= $function; - return $this; - } - /** - * Creates a temporary variable and returns its name + * Raises an exception * - * @param string + * @param string $error + * @param ?Throwable $cause + * @return never */ - protected function temp() { - return '$T'.($this->id++); + public function raise($error, $cause= null) { + throw new IllegalStateException($error, $cause); } /** - * Collects emitted code into a buffer and returns it + * Transforms nodes of a certain kind using the given function, which + * may return either single node, which will be then emitted, or an + * iterable producing nodes, which will then be emitted as statements. + * Returns a handle to remove the transformation again * - * @param function(): void $callable - * @return string + * @param string $kind + * @param function(lang.ast.Node): lang.ast.Node|iterable $function + * @return var */ - protected function buffer($callable) { - $o= $this->out; - $buffer= new MemoryOutputStream(); - $this->out= new StringWriter($buffer ); - - try { - $callable(); - return $buffer->getBytes(); - } finally { - $this->out= $o; + public function transform($kind, $function) { + if (isset($this->transformations[$kind])) { + $i= sizeof($this->transformations[$kind]); + $this->transformations[$kind][]= $function; + } else { + $i= 0; + $this->transformations[$kind]= [$function]; } + return ['kind' => $kind, 'id' => $i]; } /** - * Returns the simple name for use in a declaration + * Removes a transformation added with transform() * - * @param string $name E.g. `\lang\ast\Parse` - * @return string In the above example, `Parse`. + * @param var $transformation + * @return void */ - protected function declaration($name) { - return substr($name, strrpos($name, '\\') + 1); + public function remove($transformation) { + $kind= $transformation['kind']; + array_splice($this->transformations[$kind], $transformation['id'], 1); + if (empty($this->transformations[$kind])) unset($this->transformations[$kind]); } /** - * Returns type literal or NULL + * Returns all transformations * - * @param string $name - * @return string + * @return [:var[]] */ - protected function type($name) { - return ( - '?' === $name{0} || // nullable - 0 === strncmp($name, 'function', 8) || // function - strstr($name, '|') || // union - isset($this->unsupported[$name]) - ) ? null : $name; + public function transformations() { + return $this->transformations; } /** - * Search a given scope recursively for nodes with a given kind + * Catch-all, should `$node->kind` be empty in `"emit{$node->kind}"`. * - * @param lang.ast.Node|lang.ast.Node[] $arg - * @param string $kind - * @return iterable + * @return void */ - protected function search($arg, $kind) { - if ($arg instanceof Node) { // TODO: Do we need this? - if ($arg->kind === $kind) { - yield $arg; - } else { - foreach ($this->search($arg->value, $kind) as $result) { - yield $result; - } - } - } else if ($arg instanceof Value) { // TODO: Move recursion into Kind subclasses - foreach ((array)$arg as $node) { - foreach ($this->search($node, $kind) as $result) { - yield $result; - } - } - } else if (is_array($arg)) { - foreach ($arg as $node) { - foreach ($this->search($node, $kind) as $result) { - yield $result; - } - } - } - } - - protected function paramType($type) { - return $this->type($type->literal()); + protected function emit() { + throw new IllegalStateException('Called without node kind'); } - protected function returnType($type) { - return $this->type($type->literal()); - } - - // See https://wiki.php.net/rfc/typed_properties_v2#supported_types - protected function propertyType($type) { - if (null === $type || $type instanceof UnionType || $type instanceof FunctionType) { - return ''; - } else if ($type instanceof ArrayType || $type instanceof MapType) { - return 'array'; - } else if ($type instanceof Type && 'callable' !== $type->literal() && 'void' !== $type->literal()) { - return $type->literal(); - } else { - return ''; - } - } - - protected function emitStart($start) { - $this->out->write('out->write('namespace '.$package.";\n"); - } - - protected function emitImport($import) { - foreach ($import as $type => $alias) { - $this->out->write('use '.$type.($alias ? ' as '.$alias : '').';'); - } - } - - protected function emitImportConst($import) { - foreach ($import as $type => $alias) { - $this->out->write('use const '.$type.($alias ? ' as '.$alias : '').';'); - } - } - - protected function emitImportFunction($import) { - foreach ($import as $type => $alias) { - $this->out->write('use function '.$type.($alias ? ' as '.$alias : '').';'); - } - } - - protected function emitAnnotation($annotations) { - // NOOP - } - - protected function emitCode($code) { - $this->out->write($code); - } - - protected function emitLiteral($literal) { - $this->out->write($literal); - } - - protected function emitName($name) { - $this->out->write($name); - } - - protected function emitEcho($echo) { - $this->out->write('echo '); - $s= sizeof($echo) - 1; - foreach ($echo as $i => $expr) { - $this->emit($expr); - if ($i < $s) $this->out->write(','); - } - } - - protected function emitBlock($block) { - $this->out->write('{'); - $this->emit($block); - $this->out->write('}'); - } - - protected function emitStatic($static) { - foreach ($static as $variable => $initial) { - $this->out->write('static $'.$variable); - if ($initial) { - $this->out->write('='); - $this->emit($initial); - } - $this->out->write(';'); - } - } - - protected function emitVariable($variable) { - $this->out->write('$'.$variable); + /** + * Standalone operators + * + * @param lang.ast.Result $result + * @param lang.ast.Token $operator + * @return void + */ + protected function emitOperator($result, $operator) { + throw new IllegalStateException('Unexpected operator '.$operator->value.' at line '.$operator->line); } - protected function emitCast($cast) { - static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; - - $name= $cast->type->name(); - if ('?' === $name{0}) { - $this->out->write('cast('); - $this->emit($cast->expression); - $this->out->write(',\''.$name.'\', false)'); - } else if (isset($native[$name])) { - $this->out->write('('.$cast->type->literal().')'); - $this->emit($cast->expression); - } else { - $this->out->write('cast('); - $this->emit($cast->expression); - $this->out->write(',\''.$name.'\')'); + /** + * Emit nodes seperated as statements + * + * @param lang.ast.Result $result + * @param iterable $nodes + * @return void + */ + public function emitAll($result, $nodes) { + foreach ($nodes as $node) { + $this->emitOne($result, $node); + $result->out->write(';'); } } - protected function emitArray($array) { - if (empty($array)) { - $this->out->write('[]'); - return; - } - - $unpack= false; - foreach ($array as $pair) { - if ('unpack' === $pair[1]->kind) { - $unpack= true; - break; - } - } - - if ($unpack) { - $this->out->write('array_merge(['); - foreach ($array as $pair) { - if ($pair[0]) { - $this->emit($pair[0]); - $this->out->write('=>'); - } - if ('unpack' === $pair[1]->kind) { - if ('array' === $pair[1]->value->kind) { - $this->out->write('],'); - $this->emit($pair[1]->value); - $this->out->write(',['); - } else { - $t= $this->temp(); - $this->out->write('],('.$t.'='); - $this->emit($pair[1]->value); - $this->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); + /** + * Emit single nodes + * + * @param lang.ast.Result $result + * @param lang.ast.Node $node + * @return void + */ + public function emitOne($result, $node) { + + // 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); + return; + } else if ($r) { + foreach ($r as $s => $n) { + $this->{'emit'.$n->kind}($result, $n); + null === $s || $result->out->write(';'); } - } else { - $this->emit($pair[1]); - $this->out->write(','); - } - } - $this->out->write('])'); - } else { - $this->out->write('['); - foreach ($array as $pair) { - if ($pair[0]) { - $this->emit($pair[0]); - $this->out->write('=>'); - } - $this->emit($pair[1]); - $this->out->write(','); - } - $this->out->write(']'); - } - } - - protected function emitParameter($parameter) { - if ($parameter->type && $t= $this->paramType($parameter->type)) { - $this->out->write($t.' '); - } - if ($parameter->variadic) { - $this->out->write('... $'.$parameter->name); - } else { - $this->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); - } - if ($parameter->default) { - $this->out->write('='); - $this->emit($parameter->default); - } - $this->locals[$parameter->name]= true; - } - - protected function emitSignature($signature) { - $this->out->write('('); - $s= sizeof($signature->parameters) - 1; - foreach ($signature->parameters as $i => $parameter) { - $this->emitParameter($parameter); - if ($i < $s) $this->out->write(', '); - } - $this->out->write(')'); - - if ($signature->returns && $t= $this->returnType($signature->returns)) { - $this->out->write(':'.$t); - } - } - - protected function emitFunction($function) { - $this->stack[]= $this->locals; - $this->locals= []; - - $this->out->write('function '.$function->name); - $this->emitSignature($function->signature); - - $this->out->write('{'); - $this->emit($function->body); - $this->out->write('}'); - - $this->locals= array_pop($this->stack); - } - - protected function emitClosure($closure) { - $this->stack[]= $this->locals; - $this->locals= []; - - $this->out->write('function'); - $this->emitSignature($closure->signature); - - if ($closure->use) { - $this->out->write(' use('.implode(',', $closure->use).') '); - foreach ($closure->use as $variable) { - $this->locals[substr($variable, 1)]= true; - } - } - $this->out->write('{'); - $this->emit($closure->body); - $this->out->write('}'); - - $this->locals= array_pop($this->stack); - } - - protected function emitLambda($lambda) { - $this->out->write('fn'); - $this->emitSignature($lambda->signature); - $this->out->write('=>'); - - if (is_array($lambda->body)) { - $this->out->write('{'); - $this->emit($lambda->body); - $this->out->write('}'); - } else { - $this->emit($lambda->body); - } - } - - protected function emitClass($class) { - array_unshift($this->meta, []); - - $this->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $this->out->write(' extends '.$class->parent); - $class->implements && $this->out->write(' implements '.implode(', ', $class->implements)); - $this->out->write('{'); - foreach ($class->body as $member) { - $this->emit($member); - } - - $this->out->write('static function __init() {'); - $this->emitMeta($class->name, $class->annotations, $class->comment); - $this->out->write('}} '.$class->name.'::__init();'); - } - - protected function emitAnnotations($annotations) { - foreach ($annotations as $name => $annotation) { - $this->out->write("'".$name."' => "); - if ($annotation) { - $this->emit($annotation); - $this->out->write(','); - } else { - $this->out->write('null,'); - } - } - } - - protected function emitMeta($name, $annotations, $comment) { - $this->out->write('\xp::$meta[\''.strtr(ltrim($name, '\\'), '\\', '.').'\']= ['); - $this->out->write('"class" => [DETAIL_ANNOTATIONS => ['); - $this->emitAnnotations($annotations); - $this->out->write('], DETAIL_COMMENT => \''.str_replace("'", "\\'", $comment).'\'],'); - - foreach (array_shift($this->meta) as $type => $lookup) { - $this->out->write($type.' => ['); - foreach ($lookup as $key => $meta) { - $this->out->write("'".$key."' => [DETAIL_ANNOTATIONS => ["); - $this->emitAnnotations($meta[DETAIL_ANNOTATIONS]); - $this->out->write('], DETAIL_TARGET_ANNO => ['); - foreach ($meta[DETAIL_TARGET_ANNO] as $target => $annotations) { - $this->out->write("'$".$target."' => ["); - $this->emitAnnotations($annotations); - $this->out->write('],'); + return; } - $this->out->write('], DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); - $this->out->write(', DETAIL_COMMENT => \''.str_replace("'", "\\'", $meta[DETAIL_COMMENT]).'\''); - $this->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],'); - } - $this->out->write('],'); - } - $this->out->write('];'); - } - - protected function emitInterface($interface) { - array_unshift($this->meta, []); - - $this->out->write('interface '.$this->declaration($interface->name)); - $interface->parents && $this->out->write(' extends '.implode(', ', $interface->parents)); - $this->out->write('{'); - foreach ($interface->body as $member) { - $this->emit($member); - $this->out->write("\n"); - } - $this->out->write('}'); - - $this->emitMeta($interface->name, $interface->annotations, $interface->comment); - } - - protected function emitTrait($trait) { - array_unshift($this->meta, []); - - $this->out->write('trait '.$this->declaration($trait->name)); - $this->out->write('{'); - foreach ($trait->body as $member) { - $this->emit($member); - $this->out->write("\n"); - } - - $this->out->write('static function __init() {'); - $this->emitMeta($trait->name, $trait->annotations, $trait->comment); - $this->out->write('}} '.$trait->name.'::__init();'); - } - - protected function emitUse($use) { - $this->out->write('use '.implode(',', $use->types)); - if ($use->aliases) { - $this->out->write('{'); - foreach ($use->aliases as $reference => $alias) { - $this->out->write($reference.' as '.$alias.';'); - } - $this->out->write('}'); - } else { - $this->out->write(';'); - } - } - - protected function emitConst($const) { - $this->out->write(implode(' ', $const->modifiers).' const '.$const->name.'='); - $this->emit($const->expression); - $this->out->write(';'); - } - - protected function emitProperty($property) { - $this->meta[0][self::PROPERTY][$property->name]= [ - DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', - DETAIL_ANNOTATIONS => $property->annotations ? $property->annotations : [], - DETAIL_COMMENT => $property->comment, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; - - $this->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); - if (isset($property->expression)) { - $this->out->write('='); - $this->emit($property->expression); - } - $this->out->write(';'); - } - - protected function emitMethod($method) { - $this->stack[]= $this->locals; - $this->locals= ['this' => true]; - $meta= [ - DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', - DETAIL_ANNOTATIONS => isset($method->annotations) ? $method->annotations : [], - DETAIL_COMMENT => $method->comment, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; - - $declare= $promote= $params= ''; - foreach ($method->signature->parameters as $param) { - if (isset($param->promote)) { - $declare.= $param->promote.' $'.$param->name.';'; - $promote.= '$this->'.$param->name.'= $'.$param->name.';'; - $this->meta[0][self::PROPERTY][$param->name]= [ - DETAIL_RETURNS => $param->type ? $param->type->name() : 'var', - DETAIL_ANNOTATIONS => [], - DETAIL_COMMENT => null, - DETAIL_TARGET_ANNO => [], - DETAIL_ARGUMENTS => [] - ]; } - $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; - $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; - } - $this->out->write($declare); - $this->out->write(implode(' ', $method->modifiers).' function '.$method->name); - $this->emitSignature($method->signature); - - if (null === $method->body) { - $this->out->write(';'); - } else { - $this->out->write(' {'.$promote); - $this->emit($method->body); - $this->out->write('}'); + // Fall through, use default } - $this->meta[0][self::METHOD][$method->name]= $meta; - $this->locals= array_pop($this->stack); - } - - protected function emitBraced($braced) { - $this->out->write('('); - $this->emit($braced); - $this->out->write(')'); - } - - protected function emitBinary($binary) { - $this->emit($binary->left); - $this->out->write(' '.$binary->operator.' '); - $this->emit($binary->right); - } - - protected function emitUnary($unary) { - $this->out->write($unary->operator); - $this->emit($unary->expression); - } - - protected function emitTernary($ternary) { - $this->emit($ternary->condition); - $this->out->write('?'); - $this->emit($ternary->expression); - $this->out->write(':'); - $this->emit($ternary->otherwise); - } - - protected function emitOffset($offset) { - $this->emit($offset->expression); - if (null === $offset->offset) { - $this->out->write('[]'); - } else { - $this->out->write('['); - $this->emit($offset->offset); - $this->out->write(']'); - } - } - - protected function emitAssign($target) { - if ('variable' === $target->kind) { - $this->out->write('$'.$target->value); - $this->locals[$target->value]= true; - } else if ('array' === $target->kind) { - $this->out->write('list('); - foreach ($target->value as $pair) { - $this->emitAssign($pair[1]); - $this->out->write(','); - } - $this->out->write(')'); - } else { - $this->emit($target); - } + $this->{'emit'.$node->kind}($result, $node); } - protected function emitAssignment($assignment) { - $this->emitAssign($assignment->variable); - $this->out->write($assignment->operator); - $this->emit($assignment->expression); - } - - protected function emitReturn($return) { - $this->out->write('return '); - $return && $this->emit($return); - $this->out->write(';'); - } - - protected function emitIf($if) { - $this->out->write('if ('); - $this->emit($if->expression); - $this->out->write(') {'); - $this->emit($if->body); - $this->out->write('}'); - - if ($if->otherwise) { - $this->out->write('else {'); - $this->emit($if->otherwise); - $this->out->write('}'); - } - } - - protected function emitSwitch($switch) { - $this->out->write('switch ('); - $this->emit($switch->expression); - $this->out->write(') {'); - foreach ($switch->cases as $case) { - if ($case->expression) { - $this->out->write('case '); - $this->emit($case->expression); - $this->out->write(':'); - } else { - $this->out->write('default:'); - } - $this->emit($case->body); - } - $this->out->write('}'); - } - - protected function emitCatch($catch) { - if (empty($catch->types)) { - $this->out->write('catch(\\Throwable $'.$catch->variable.') {'); - } else { - $this->out->write('catch('.implode('|', $catch->types).' $'.$catch->variable.') {'); - } - $this->emit($catch->body); - $this->out->write('}'); - } - - protected function emitTry($try) { - $this->out->write('try {'); - $this->emit($try->body); - $this->out->write('}'); - if (isset($try->catches)) { - foreach ($try->catches as $catch) { - $this->emitCatch($catch); - } - } - if (isset($try->finally)) { - $this->out->write('finally {'); - $this->emit($try->finally); - $this->out->write('}'); - } - } - - protected function emitThrow($throw) { - $this->out->write('throw '); - $this->emit($throw); - $this->out->write(';'); - } - - protected function emitThrowExpression($throw) { - $capture= []; - foreach ($this->search($throw, 'variable') as $var) { - if (isset($this->locals[$var->value])) { - $capture[$var->value]= true; - } - } - unset($capture['this']); - - $this->out->write('(function()'); - $capture && $this->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $this->out->write('{ throw '); - $this->emit($throw); - $this->out->write('; })()'); - } - - protected function emitForeach($foreach) { - $this->out->write('foreach ('); - $this->emit($foreach->expression); - $this->out->write(' as '); - if ($foreach->key) { - $this->emit($foreach->key); - $this->out->write(' => '); - } - $this->emit($foreach->value); - $this->out->write(') {'); - $this->emit($foreach->body); - $this->out->write('}'); - } - - protected function emitFor($for) { - $this->out->write('for ('); - $this->emitArguments($for->initialization); - $this->out->write(';'); - $this->emitArguments($for->condition); - $this->out->write(';'); - $this->emitArguments($for->loop); - $this->out->write(') {'); - $this->emit($for->body); - $this->out->write('}'); - } - - protected function emitDo($do) { - $this->out->write('do'); - $this->out->write('{'); - $this->emit($do->body); - $this->out->write('} while ('); - $this->emit($do->expression); - $this->out->write(');'); - } - - protected function emitWhile($while) { - $this->out->write('while ('); - $this->emit($while->expression); - $this->out->write(') {'); - $this->emit($while->body); - $this->out->write('}'); - } - - protected function emitBreak($break) { - $this->out->write('break '); - $break && $this->emit($break); - $this->out->write(';'); - } - - protected function emitContinue($continue) { - $this->out->write('continue '); - $continue && $this->emit($continue); - $this->out->write(';'); - } - - protected function emitLabel($label) { - $this->out->write($label.':'); - } - - protected function emitGoto($goto) { - $this->out->write('goto '.$goto); - } - - protected function emitInstanceOf($instanceof) { - $this->emit($instanceof->expression); - $this->out->write(' instanceof '); - if ($instanceof->type instanceof Node) { - $this->emit($instanceof->type); - } else { - $this->out->write($instanceof->type); - } - } - - protected function emitArguments($arguments) { - $s= sizeof($arguments) - 1; - foreach ($arguments as $i => $argument) { - $this->emit($argument); - if ($i < $s) $this->out->write(', '); - } - } - - protected function emitNew($new) { - $this->out->write('new '.$new->type.'('); - $this->emitArguments($new->arguments); - $this->out->write(')'); - } - - protected function emitNewClass($new) { - $this->out->write('new class('); - $this->emitArguments($new->arguments); - $this->out->write(')'); - - $new->definition->parent && $this->out->write(' extends '.$new->definition->parent); - $new->definition->implements && $this->out->write(' implements '.implode(', ', $new->definition->implements)); - $this->out->write('{'); - foreach ($new->definition->body as $member) { - $this->emit($member); - $this->out->write("\n"); - } - $this->out->write('}'); - } - - protected function emitInvoke($invoke) { - $this->emit($invoke->expression); - $this->out->write('('); - $this->emitArguments($invoke->arguments); - $this->out->write(')'); - } - - protected function emitScope($scope) { - $this->out->write($scope->type.'::'); - $this->emit($scope->member); - } - - protected function emitInstance($instance) { - if ('new' === $instance->expression->kind) { - $this->out->write('('); - $this->emit($instance->expression); - $this->out->write(')->'); - } else { - $this->emit($instance->expression); - $this->out->write('->'); - } - - if ('name' === $instance->member->kind) { - $this->out->write($instance->member->value); - } else { - $this->out->write('{'); - $this->emit($instance->member); - $this->out->write('}'); - } - } - - protected function emitNullSafeInstance($instance) { - $t= $this->temp(); - $this->out->write('null === ('.$t.'= '); - $this->emit($instance->expression); - $this->out->write(') ? null : '.$t.'->'); - - if ('name' === $instance->member->kind) { - $this->out->write($instance->member->value); - } else { - $this->out->write('{'); - $this->emit($instance->member); - $this->out->write('}'); - } - } - - protected function emitUnpack($unpack) { - $this->out->write('...'); - $this->emit($unpack); - } - - protected function emitYield($yield) { - $this->out->write('yield '); - if ($yield->key) { - $this->emit($yield->key); - $this->out->write('=>'); - } - if ($yield->value) { - $this->emit($yield->value); - } - } - - protected function emitFrom($from) { - $this->out->write('yield from '); - $this->emit($from); - } - - protected function emitUsing($using) { - $variables= []; - foreach ($using->arguments as $expression) { - switch ($expression->kind) { - case 'variable': $variables[]= '$'.$expression->value; break; - case 'assignment': $variables[]= '$'.$expression->value->variable->value; break; - default: $temp= $this->temp(); $variables[]= $temp; $this->out->write($temp.'='); - } - $this->emit($expression); - $this->out->write(';'); - } - - $this->out->write('try {'); - $this->emit($using->body); - - $this->out->write('} finally {'); - foreach ($variables as $variable) { - $this->out->write('if ('.$variable.' instanceof \lang\Closeable) { '.$variable.'->close(); }'); - $this->out->write('else if ('.$variable.' instanceof \IDisposable) { '.$variable.'->__dispose(); }'); - $this->out->write('unset('.$variable.');'); - } - $this->out->write('}'); - } - - public function emit($arg) { - if ($arg instanceof Element) { - if ($arg->line > $this->line) { - $this->out->write(str_repeat("\n", $arg->line - $this->line)); - $this->line= $arg->line; - } + /** + * Creates result + * + * @param io.streams.OutputStream $target + * @return lang.ast.Result + */ + protected abstract function result($target); - if (isset($this->transformations[$arg->kind])) { - foreach ($this->transformations[$arg->kind]($arg) as $n) { - $this->{'emit'.$n->kind}($n->value); - } - } else { - $this->{'emit'.$arg->kind}($arg->value); - } - } else { - foreach ($arg as $node) { - $this->emit($node); - isset($node->symbol->std) || $this->out->write(';'); - } + /** + * Emitter entry point, takes nodes and emits them to the given target. + * + * @param iterable $nodes + * @param io.streams.OutputStream $target + * @param ?string $source + * @return io.streams.OutputStream + * @throws lang.ast.Errors + */ + public function write($nodes, OutputStream $target, $source= null) { + $result= $this->result($target)->from($source); + try { + $this->emitAll($result, $nodes); + return $target; + } catch (Error $e) { + throw new Errors([$e], $source); + } finally { + $result->close(); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/Error.class.php b/src/main/php/lang/ast/Error.class.php deleted file mode 100755 index 10f9a0a8..00000000 --- a/src/main/php/lang/ast/Error.class.php +++ /dev/null @@ -1,17 +0,0 @@ -file= $file; - $this->line= $line; - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Parse.class.php b/src/main/php/lang/ast/Parse.class.php deleted file mode 100755 index e7d31398..00000000 --- a/src/main/php/lang/ast/Parse.class.php +++ /dev/null @@ -1,1600 +0,0 @@ -tokens= $tokens->getIterator(); - $this->scope= $scope ?: new Scope(null); - $this->file= $file; - - // Setup parse rules - $this->infixt('??', 30); - $this->infixt('?:', 30); - - $this->infixr('&&', 30); - $this->infixr('||', 30); - - $this->infixr('==', 40); - $this->infixr('===', 40); - $this->infixr('!=', 40); - $this->infixr('!==', 40); - $this->infixr('<', 40); - $this->infixr('<=', 40); - $this->infixr('>', 40); - $this->infixr('>=', 40); - $this->infixr('<=>', 40); - - $this->infix('+', 50); - $this->infix('-', 50); - $this->infix('&', 50); - $this->infix('|', 50); - $this->infix('^', 50); - - $this->infix('*', 60); - $this->infix('/', 60); - $this->infix('%', 60); - $this->infix('.', 60); - $this->infix('**', 60); - - $this->infixr('<<', 70); - $this->infixr('>>', 70); - - $this->infix('instanceof', 60, function($node, $left) { - if ('name' === $this->token->kind) { - $node->value= new InstanceOfExpression($left, $this->scope->resolve($this->token->value)); - $this->token= $this->advance(); - } else { - $node->value= new InstanceOfExpression($left, $this->expression(0)); - } - - $node->kind= 'instanceof'; - return $node; - }); - - $this->infix('->', 80, function($node, $left) { - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $expr= $this->expression(0); - $this->token= $this->expect('}'); - } else { - $expr= $this->token; - $this->token= $this->advance(); - } - - $node->value= new InstanceExpression($left, $expr); - $node->kind= 'instance'; - return $node; - }); - - $this->infix('?->', 80, function($node, $left) { - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $expr= $this->expression(0); - $this->token= $this->expect('}'); - } else { - $expr= $this->token; - $this->token= $this->advance(); - } - - $node->value= new InstanceExpression($left, $expr); - $node->kind= 'nullsafeinstance'; - return $node; - }); - - $this->infix('::', 80, function($node, $left) { - $node->value= new ScopeExpression($this->scope->resolve($left->value), $this->token); - $node->kind= 'scope'; - $this->token= $this->advance(); - return $node; - }); - - $this->infix('==>', 80, function($node, $left) { - $signature= new Signature([new Parameter($left->value, null)], null); - - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } else { - $statements= $this->expressionWithThrows(0); - } - - $node->value= new LambdaExpression($signature, $statements); - $node->kind= 'lambda'; - return $node; - }); - - $this->infix('(', 80, function($node, $left) { - $arguments= $this->arguments(); - $this->token= $this->expect(')'); - $node->value= new InvokeExpression($left, $arguments); - $node->kind= 'invoke'; - return $node; - }); - - $this->infix('[', 80, function($node, $left) { - if (']' === $this->token->value) { - $expr= null; - } else { - $expr= $this->expression(0); - } - $this->token= $this->expect(']'); - - $node->value= new OffsetExpression($left, $expr); - $node->kind= 'offset'; - return $node; - }); - - $this->infix('{', 80, function($node, $left) { - $expr= $this->expression(0); - $this->token= $this->expect('}'); - - $node->value= new OffsetExpression($left, $expr); - $node->kind= 'offset'; - return $node; - }); - - $this->infix('?', 80, function($node, $left) { - $when= $this->expressionWithThrows(0); - $this->token= $this->expect(':'); - $else= $this->expressionWithThrows(0); - $node->value= new TernaryExpression($left, $when, $else); - $node->kind= 'ternary'; - return $node; - }); - - $this->suffix('++', 50); - $this->suffix('--', 50); - - $this->prefix('@'); - $this->prefix('&'); - $this->prefix('~'); - $this->prefix('!'); - $this->prefix('+'); - $this->prefix('-'); - $this->prefix('++'); - $this->prefix('--'); - $this->prefix('clone'); - - $this->assignment('='); - $this->assignment('&='); - $this->assignment('|='); - $this->assignment('^='); - $this->assignment('+='); - $this->assignment('-='); - $this->assignment('*='); - $this->assignment('/='); - $this->assignment('.='); - $this->assignment('**='); - $this->assignment('>>='); - $this->assignment('<<='); - $this->assignment('??='); - - // This is ambiguous: - // - // - An arrow function `($a) ==> $a + 1` - // - An expression surrounded by parentheses `($a ?? $b)->invoke()`; - // - A cast `(int)$a` or `(int)($a / 2)`. - // - // Resolve by looking ahead after the closing ")" - $this->prefix('(', function($node) { - static $types= [ - '<' => true, - '>' => true, - ',' => true, - '?' => true, - ':' => true - ]; - - $skipped= [$node, $this->token]; - $cast= true; - $level= 1; - while ($level > 0 && null !== $this->token->value) { - if ('(' === $this->token->value) { - $level++; - } else if (')' === $this->token->value) { - $level--; - } else if ('name' !== $this->token->kind && !isset($types[$this->token->value])) { - $cast= false; - } - $this->token= $this->advance(); - $skipped[]= $this->token; - } - $this->queue= array_merge($skipped, $this->queue); - - if (':' === $this->token->value || '==>' === $this->token->value) { - $node->kind= 'lambda'; - - $this->token= $this->advance(); - $signature= $this->signature(); - $this->token= $this->advance(); - - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } else { - $statements= $this->expressionWithThrows(0); - } - - $node->value= new LambdaExpression($signature, $statements); - } else if ($cast && ('operator' !== $this->token->kind || '(' === $this->token->value || '[' === $this->token->value)) { - $node->kind= 'cast'; - - $this->token= $this->advance(); - $this->token= $this->expect('('); - $type= $this->type0(false); - $this->token= $this->expect(')'); - $node->value= new CastExpression($type, $this->expression(0)); - } else { - $node->kind= 'braced'; - - $this->token= $this->advance(); - $this->token= $this->expect('('); - $node->value= $this->expression(0); - $this->token= $this->expect(')'); - } - return $node; - }); - - $this->prefix('[', function($node) { - $values= []; - while (']' !== $this->token->value) { - $expr= $this->expression(0); - - if ('=>' === $this->token->value) { - $this->token= $this->advance(); - $values[]= [$expr, $this->expression(0)]; - } else { - $values[]= [null, $expr]; - } - - if (']' === $this->token->value) break; - $this->token= $this->expect(',', 'array literal'); - } - - $this->token= $this->expect(']', 'array literal'); - $node->kind= 'array'; - $node->value= $values; - return $node; - }); - - $this->prefix('{', function($node) { - $node->kind= 'block'; - $node->value= $this->statements(); - $this->token= new Node(self::symbol(';')); - return $node; - }); - - $this->prefix('new', function($node) { - $type= $this->token; - $this->token= $this->advance(); - - $this->token= $this->expect('('); - $arguments= $this->arguments(); - $this->token= $this->expect(')'); - - if ('variable' === $type->kind) { - $node->value= new NewExpression('$'.$type->value, $arguments); - $node->kind= 'new'; - } else if ('class' === $type->value) { - $node->value= new NewClassExpression($this->clazz(null), $arguments); - $node->kind= 'newclass'; - } else { - $node->value= new NewExpression($this->scope->resolve($type->value), $arguments); - $node->kind= 'new'; - } - return $node; - }); - - $this->prefix('yield', function($node) { - if (';' === $this->token->value) { - $node->kind= 'yield'; - $node->value= new YieldExpression(null, null); - } else if ('from' === $this->token->value) { - $this->token= $this->advance(); - $node->kind= 'from'; - $node->value= $this->expression(0); - } else { - $node->kind= 'yield'; - $expr= $this->expression(0); - if ('=>' === $this->token->value) { - $this->token= $this->advance(); - $node->value= new YieldExpression($expr, $this->expression(0)); - } else { - $node->value= new YieldExpression(null, $expr); - } - } - return $node; - }); - - $this->prefix('...', function($node) { - $node->kind= 'unpack'; - $node->value= $this->expression(0); - return $node; - }); - - $this->prefix('fn', function($node) { - $signature= $this->signature(); - - $this->token= $this->expect('=>'); - - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } else { - $statements= $this->expressionWithThrows(0); - } - - $node->value= new LambdaExpression($signature, $statements); - $node->kind= 'lambda'; - return $node; - }); - - $this->prefix('function', function($node) { - - // Closure `$a= function() { ... };` vs. declaration `function a() { ... }`; - // the latter explicitely becomes a statement by pushing a semicolon. - if ('(' === $this->token->value) { - $node->kind= 'closure'; - $signature= $this->signature(); - - if ('use' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->advance(); - $use= []; - while (')' !== $this->token->value) { - if ('&' === $this->token->value) { - $this->token= $this->advance(); - $use[]= '&$'.$this->token->value; - } else { - $use[]= '$'.$this->token->value; - } - $this->token= $this->advance(); - if (')' === $this->token->value) break; - $this->token= $this->expect(',', 'use list'); - } - $this->token= $this->expect(')'); - } else { - $use= null; - } - - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - - $node->value= new ClosureExpression($signature, $use, $statements); - } else { - $node->kind= 'function'; - $name= $this->token->value; - $this->token= $this->advance(); - $signature= $this->signature(); - - if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' - $n= new Node($this->token->symbol); - $this->token= $this->advance(); - $n->value= $this->expressionWithThrows(0); - $n->line= $this->token->line; - $n->kind= 'return'; - $statements= [$n]; - $this->token= $this->expect(';'); - } else { // Regular function - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } - - $this->queue= [$this->token]; - $this->token= new Node(self::symbol(';')); - $node->value= new FunctionDeclaration($name, $signature, $statements); - } - - return $node; - }); - - $this->prefix('static', function($node) { - if ('variable' === $this->token->kind) { - $node->kind= 'static'; - $node->value= []; - while (';' !== $this->token->value) { - $variable= $this->token->value; - $this->token= $this->advance(); - - if ('=' === $this->token->value) { - $this->token= $this->expect('='); - $initial= $this->expression(0); - } else { - $initial= null; - } - - $node->value[$variable]= $initial; - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } - } - } - return $node; - }); - - $this->prefix('goto', function($node) { - $node->kind= 'goto'; - $node->value= $this->token->value; - $this->token= $this->advance(); - return $node; - }); - - $this->prefix('(name)', function($node) { - if (':' === $this->token->value) { - $node->kind= 'label'; - $this->token= new Node(self::symbol(';')); - } - return $node; - }); - - $this->stmt('kind= 'start'; - $node->value= $this->token->value; - - $this->token= $this->advance(); - return $node; - }); - - $this->prefix('echo', function($node) { - $node->kind= 'echo'; - $node->value= $this->arguments(';'); - return $node; - }); - - $this->stmt('namespace', function($node) { - $node->kind= 'package'; - $node->value= $this->token->value; - - $this->token= $this->advance(); - $this->token= $this->expect(';'); - - $this->scope->package($node->value); - return $node; - }); - - $this->stmt('use', function($node) { - if ('function' === $this->token->value) { - $node->kind= 'importfunction'; - $this->token= $this->advance(); - } else if ('const' === $this->token->value) { - $node->kind= 'importconst'; - $this->token= $this->advance(); - } else { - $node->kind= 'import'; - } - - $import= $this->token->value; - $this->token= $this->advance(); - - if ('{' === $this->token->value) { - $types= []; - $this->token= $this->advance(); - while ('}' !== $this->token->value) { - $class= $import.$this->token->value; - - $this->token= $this->advance(); - if ('as' === $this->token->value) { - $this->token= $this->advance(); - $types[$class]= $this->token->value; - $this->scope->import($this->token->value); - $this->token= $this->advance(); - } else { - $types[$class]= null; - $this->scope->import($class); - } - - if (',' === $this->token->value) { - $this->token= $this->advance(); - } else if ('}' === $this->token->value) { - break; - } else { - $this->expect(', or }'); - } - } - $this->token= $this->advance(); - } else if ('as' === $this->token->value) { - $this->token= $this->advance(); - $types= [$import => $this->token->value]; - $this->scope->import($import, $this->token->value); - $this->token= $this->advance(); - } else { - $types= [$import => null]; - $this->scope->import($import); - } - - $this->token= $this->expect(';'); - $node->value= $types; - return $node; - }); - - $this->stmt('if', function($node) { - $this->token= $this->expect('('); - $condition= $this->expression(0); - $this->token= $this->expect(')'); - - $when= $this->block(); - - if ('else' === $this->token->value) { - $this->token= $this->advance(); - $otherwise= $this->block(); - } else { - $otherwise= null; - } - - $node->value= new IfStatement($condition, $when, $otherwise); - $node->kind= 'if'; - return $node; - }); - - $this->stmt('switch', function($node) { - $this->token= $this->expect('('); - $condition= $this->expression(0); - $this->token= $this->expect(')'); - - $cases= []; - $this->token= $this->expect('{'); - while ('}' !== $this->token->value) { - if ('default' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->expect(':'); - $cases[]= new CaseLabel(null, []); - } else if ('case' === $this->token->value) { - $this->token= $this->advance(); - $expr= $this->expression(0); - $this->token= $this->expect(':'); - $cases[]= new CaseLabel($expr, []); - } else { - $cases[sizeof($cases) - 1]->body[]= $this->statement(); - } - } - $this->token= $this->expect('}'); - - $node->value= new SwitchStatement($condition, $cases); - $node->kind= 'switch'; - return $node; - }); - - $this->stmt('break', function($node) { - if (';' === $this->token->value) { - $node->value= null; - $this->token= $this->advance(); - } else { - $node->value= $this->expression(0); - $this->token= $this->expect(';'); - } - - $node->kind= 'break'; - return $node; - }); - - $this->stmt('continue', function($node) { - if (';' === $this->token->value) { - $node->value= null; - $this->token= $this->advance(); - } else { - $node->value= $this->expression(0); - $this->token= $this->expect(';'); - } - - $node->kind= 'continue'; - return $node; - }); - - $this->stmt('do', function($node) { - $block= $this->block(); - - $this->token= $this->expect('while'); - $this->token= $this->expect('('); - $expression= $this->expression(0); - $this->token= $this->expect(')'); - $this->token= $this->expect(';'); - - $node->value= new DoLoop($expression, $block); - $node->kind= 'do'; - return $node; - }); - - $this->stmt('while', function($node) { - $this->token= $this->expect('('); - $expression= $this->expression(0); - $this->token= $this->expect(')'); - $block= $this->block(); - - $node->value= new WhileLoop($expression, $block); - $node->kind= 'while'; - return $node; - }); - - $this->stmt('for', function($node) { - $this->token= $this->expect('('); - $init= $this->arguments(';'); - $this->token= $this->advance(';'); - $cond= $this->arguments(';'); - $this->token= $this->advance(';'); - $loop= $this->arguments(')'); - $this->token= $this->advance(')'); - - $block= $this->block(); - - $node->value= new ForLoop($init, $cond, $loop, $block); - $node->kind= 'for'; - return $node; - }); - - $this->stmt('foreach', function($node) { - $this->token= $this->expect('('); - $expression= $this->expression(0); - - $this->token= $this->advance('as'); - $expr= $this->expression(0); - - if ('=>' === $this->token->value) { - $this->token= $this->advance(); - $key= $expr; - $value= $this->expression(0); - } else { - $key= null; - $value= $expr; - } - - $this->token= $this->expect(')'); - - $block= $this->block(); - $node->value= new ForeachLoop($expression, $key, $value, $block); - $node->kind= 'foreach'; - return $node; - }); - - $this->stmt('throw', function($node) { - $node->value= $this->expression(0); - $node->kind= 'throw'; - $this->token= $this->expect(';'); - return $node; - }); - - $this->stmt('try', function($node) { - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - - $catches= []; - while ('catch' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->expect('('); - - $types= []; - while ('name' === $this->token->kind) { - $types[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if ('|' !== $this->token->value) break; - $this->token= $this->advance(); - } - - $variable= $this->token; - $this->token= $this->advance(); - $this->token= $this->expect(')'); - - $this->token= $this->expect('{'); - $catches[]= new CatchStatement($types, $variable->value, $this->statements()); - $this->token= $this->expect('}'); - } - - if ('finally' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->expect('{'); - $finally= $this->statements(); - $this->token= $this->expect('}'); - } else { - $finally= null; - } - - $node->value= new TryStatement($statements, $catches, $finally); - $node->kind= 'try'; - return $node; - }); - - $this->stmt('return', function($node) { - if (';' === $this->token->value) { - $expr= null; - } else { - $expr= $this->expression(0); - } - $this->token= $this->expect(';'); - - $node->value= $expr; - $node->kind= 'return'; - return $node; - }); - - $this->stmt('abstract', function($node) { - $this->token= $this->advance(); - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - - $node->value= $this->clazz($type, ['abstract']); - $node->kind= 'class'; - return $node; - }); - - $this->stmt('final', function($node) { - $this->token= $this->advance(); - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - - $node->value= $this->clazz($type, ['final']); - $node->kind= 'class'; - return $node; - }); - - $this->stmt('<<', function($node) { - do { - $name= $this->token->value; - $this->token= $this->advance(); - - if ('(' === $this->token->value) { - $this->token= $this->expect('('); - $this->scope->annotations[$name]= $this->expression(0); - $this->token= $this->expect(')'); - } else { - $this->scope->annotations[$name]= null; - } - - if (',' === $this->token->value) { - continue; - } else if ('>>' === $this->token->value) { - break; - } else { - $this->expect(', or >>', 'annotation'); - } - } while (true); - - $this->token= $this->expect('>>', 'annotation'); - $node->kind= 'annotation'; - return $node; - }); - - $this->stmt('class', function($node) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - - $node->value= $this->clazz($type); - $node->kind= 'class'; - return $node; - }); - - $this->stmt('interface', function($node) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - $comment= $this->comment; - $this->comment= null; - - $parents= []; - if ('extends' === $this->token->value) { - $this->token= $this->advance(); - do { - $parents[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } else if ('{' === $this->token->value) { - break; - } else { - $this->expect(', or {', 'interface parents'); - } - } while (true); - } - - $this->token= $this->expect('{'); - $body= $this->body(); - $this->token= $this->expect('}'); - - $node->value= new InterfaceDeclaration([], $type, $parents, $body, $this->scope->annotations, $comment); - $node->kind= 'interface'; - $this->scope->annotations= []; - return $node; - }); - - $this->stmt('trait', function($node) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - $comment= $this->comment; - $this->comment= null; - - $this->token= $this->expect('{'); - $body= $this->body(); - $this->token= $this->expect('}'); - - $node->value= new TraitDeclaration([], $type, $body, $this->scope->annotations, $comment); - $node->kind= 'trait'; - $this->scope->annotations= []; - return $node; - }); - - $this->stmt('using', function($node) { - $this->token= $this->expect('('); - $arguments= $this->arguments(); - $this->token= $this->expect(')'); - - $this->token= $this->expect('{'); - $statements= $this->statements(); - $this->token= $this->expect('}'); - - $node->value= new UsingStatement($arguments, $statements); - $node->kind= 'using'; - return $node; - }); - } - - private function type($optional= true) { - $t= []; - do { - $t[]= $this->type0($optional); - if ('|' === $this->token->value) { - $this->token= $this->advance(); - continue; - } - return 1 === sizeof($t) ? $t[0] : new UnionType($t); - } while (true); - } - - private function type0($optional) { - if ('?' === $this->token->value) { - $this->token= $this->advance(); - $type= '?'.$this->scope->resolve($this->token->value); - $this->token= $this->advance(); - } else if ('(' === $this->token->value) { - $this->token= $this->advance(); - $type= $this->type(false); - $this->token= $this->advance(); - return $type; - } else if ('name' === $this->token->kind && 'function' === $this->token->value) { - $this->token= $this->advance(); - $this->token= $this->expect('('); - $signature= []; - if (')' !== $this->token->value) do { - $signature[]= $this->type(false); - if (',' === $this->token->value) { - $this->token= $this->advance(); - } else if (')' === $this->token->value) { - break; - } else { - $this->expect(', or )', 'function type'); - } - } while (true); - $this->token= $this->expect(')'); - $this->token= $this->expect(':'); - return new FunctionType($signature, $this->type(false)); - } else if ('name' === $this->token->kind) { - $type= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - } else if ($optional) { - return null; - } else { - $this->expect('type name'); - } - - if ('<' === $this->token->value) { - $this->token= $this->advance(); - $components= []; - do { - $components[]= $this->type(false); - if (',' === $this->token->value) { - $this->token= $this->advance(); - } else if ('>' === $this->token->symbol->id) { - break; - } else if ('>>' === $this->token->value) { - $this->queue[]= $this->token= new Node(self::symbol('>')); - break; - } - } while (true); - $this->token= $this->expect('>'); - - if ('array' === $type) { - return 1 === sizeof($components) ? new ArrayType($components[0]) : new MapType($components[0], $components[1]); - } else { - return new GenericType($type, $components); - } - } else { - return new Type($type); - } - } - - private function parameters() { - static $promotion= ['private' => true, 'protected' => true, 'public' => true]; - - $parameters= []; - $annotations= []; - while (')' !== $this->token->value) { - if ('<<' === $this->token->value) { - do { - $this->token= $this->advance(); - - $name= $this->token->value; - $this->token= $this->advance(); - - if ('(' === $this->token->value) { - $this->token= $this->expect('('); - $annotations[$name]= $this->expression(0); - $this->token= $this->expect(')'); - } else { - $annotations[$name]= null; - } - - if (',' === $this->token->value) { - continue; - } else if ('>>' === $this->token->value) { - break; - } else { - $this->expect(', or >>', 'parameter annotation'); - } - } while (true); - $this->token= $this->expect('>>', 'parameter annotation'); - } - - if ('name' === $this->token->kind && isset($promotion[$this->token->value])) { - $promote= $this->token->value; - $this->token= $this->advance(); - } else { - $promote= null; - } - - $type= $this->type(); - - if ('...' === $this->token->value) { - $variadic= true; - $this->token= $this->advance(); - } else { - $variadic= false; - } - - if ('&' === $this->token->value) { - $byref= true; - $this->token= $this->advance(); - } else { - $byref= false; - } - - $name= $this->token->value; - $this->token= $this->advance(); - - $default= null; - if ('=' === $this->token->value) { - $this->token= $this->advance(); - $default= $this->expression(0); - } - $parameters[]= new Parameter($name, $type, $default, $byref, $variadic, $promote, $annotations); - - if (')' === $this->token->value) break; - $this->token= $this->expect(',', 'parameter list'); - $annotations= []; - } - return $parameters; - } - - private function signature() { - $this->token= $this->expect('('); - $parameters= $this->parameters(); - $this->token= $this->expect(')'); - - if (':' === $this->token->value) { - $this->token= $this->advance(); - $return= $this->type(); - } else { - $return= null; - } - - return new Signature($parameters, $return); - } - - private function block() { - if ('{' === $this->token->value) { - $this->token= $this->expect('{'); - $block= $this->statements(); - $this->token= $this->expect('}'); - return $block; - } else { - return [$this->statement()]; - } - } - - private function expressionWithThrows($bp) { - if ('throw' === $this->token->value) { - $expr= new Node($this->token->symbol); - $expr->kind= 'throwexpression'; - $this->token= $this->advance(); - $expr->value= $this->expression($bp); - return $expr; - } else { - return $this->expression($bp); - } - } - - private function clazz($name, $modifiers= []) { - $comment= $this->comment; - $this->comment= null; - - $parent= null; - if ('extends' === $this->token->value) { - $this->token= $this->advance(); - $parent= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - } - - $implements= []; - if ('implements' === $this->token->value) { - $this->token= $this->advance(); - do { - $implements[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } else if ('{' === $this->token->value) { - break; - } else { - $this->expect(', or {', 'interfaces list'); - } - } while (true); - } - - $this->token= $this->expect('{'); - $body= $this->body(); - $this->token= $this->expect('}'); - - $return= new ClassDeclaration($modifiers, $name, $parent, $implements, $body, $this->scope->annotations, $comment); - $this->scope->annotations= []; - return $return; - } - - private function arguments($end= ')') { - $arguments= []; - while ($end !== $this->token->value) { - $arguments[]= $this->expression(0, false); // Undefined arguments are OK - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } else if ($end === $this->token->value) { - break; - } else { - $this->expect($end.' or ,', 'argument list'); - } - } - return $arguments; - } - - /** - * Type body - * - * - `use [traits]` - * - `[modifiers] int $t = 5` - * - `[modifiers] const int T = 5` - * - `[modifiers] function t(): int { }` - */ - private function body() { - static $modifier= [ - 'private' => true, - 'protected' => true, - 'public' => true, - 'static' => true, - 'final' => true, - 'abstract' => true - ]; - - $body= []; - $modifiers= []; - $annotations= []; - $type= null; - while ('}' !== $this->token->value) { - if (isset($modifier[$this->token->value])) { - $modifiers[]= $this->token->value; - $this->token= $this->advance(); - } else if ('use' === $this->token->value) { - $member= new Node($this->token->symbol); - $member->kind= 'use'; - $member->line= $this->token->line; - - $this->token= $this->advance(); - $types= []; - do { - $types[]= $this->scope->resolve($this->token->value); - $this->token= $this->advance(); - if (',' === $this->token->value) { - $this->token= $this->advance(); - continue; - } else { - break; - } - } while ($this->token->value); - - $aliases= []; - if ('{' === $this->token->value) { - $this->token= $this->advance(); - while ('}' !== $this->token->value) { - $method= $this->token->value; - $this->token= $this->advance(); - if ('::' === $this->token->value) { - $this->token= $this->advance(); - $method= $this->scope->resolve($method).'::'.$this->token->value; - $this->token= $this->advance(); - } - $this->token= $this->expect('as'); - $alias= $this->token->value; - $this->token= $this->advance(); - $this->token= $this->expect(';'); - $aliases[$method]= $alias; - } - $this->token= $this->expect('}'); - } else { - $this->token= $this->expect(';'); - } - - $member->value= new UseExpression($types, $aliases); - $body[]= $member; - } else if ('fn' === $this->token->value) { - $member= new Node($this->token->symbol); - $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; - - $this->token= $this->advance(); - $name= $this->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); - } - - $this->token= $this->advance(); - $signature= $this->signature(); - - $this->token= $this->expect('=>'); - - $n= new Node($this->token->symbol); - $n->line= $this->token->line; - $n->value= $this->expressionWithThrows(0); - $n->kind= 'return'; - $statements= [$n]; - $this->token= $this->expect(';'); - - $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); - $body[$lookup]= $member; - $modifiers= []; - $annotations= []; - } else if ('function' === $this->token->value) { - $member= new Node($this->token->symbol); - $member->kind= 'method'; - $member->line= $this->token->line; - $comment= $this->comment; - $this->comment= null; - - $this->token= $this->advance(); - $name= $this->token->value; - $lookup= $name.'()'; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare method '.$lookup); - } - - $this->token= $this->advance(); - $signature= $this->signature(); - - if ('{' === $this->token->value) { // Regular body - $this->token= $this->advance(); - $statements= $this->statements(); - $this->token= $this->expect('}'); - } else if (';' === $this->token->value) { // Abstract or interface method - $statements= null; - $this->token= $this->expect(';'); - } else if ('==>' === $this->token->value) { // Compact syntax, terminated with ';' - $n= new Node($this->token->symbol); - $n->line= $this->token->line; - $this->token= $this->advance(); - $n->value= $this->expressionWithThrows(0); - $n->kind= 'return'; - $statements= [$n]; - $this->token= $this->expect(';'); - } else { - $this->token= $this->expect('{, ; or ==>', 'method declaration'); - } - - $member->value= new Method($modifiers, $name, $signature, $statements, $annotations, $comment); - $body[$lookup]= $member; - $modifiers= []; - $annotations= []; - } else if ('const' === $this->token->value) { - $n= new Node($this->token->symbol); - $n->kind= 'const'; - $this->token= $this->advance(); - - $type= null; - while (';' !== $this->token->value) { - $member= clone $n; - $member->line= $this->token->line; - $first= $this->token; - $this->token= $this->advance(); - - // Untyped `const T = 5` vs. typed `const int T = 5` - if ('=' === $this->token->value) { - $name= $first->value; - } else { - $this->queue[]= $first; - $this->queue[]= $this->token; - $this->token= $first; - - $type= $this->type(false); - $this->token= $this->advance(); - $name= $this->token->value; - $this->token= $this->advance(); - } - - if (isset($body[$name])) { - $this->raise('Cannot redeclare constant '.$name); - } - - $this->token= $this->expect('='); - $member->value= new Constant($modifiers, $name, $type, $this->expression(0)); - $body[$name]= $member; - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } - } - $this->token= $this->expect(';', 'constant declaration'); - $modifiers= []; - } else if ('variable' === $this->token->kind) { - $n= new Node($this->token->symbol); - $n->kind= 'property'; - $comment= $this->comment; - $this->comment= null; - - while (';' !== $this->token->value) { - $member= clone $n; - $member->line= $this->token->line; - - // Untyped `$a` vs. typed `int $a` - if ('variable' === $this->token->kind) { - $name= $this->token->value; - } else { - $type= $this->type(false); - $name= $this->token->value; - } - - $lookup= '$'.$name; - if (isset($body[$lookup])) { - $this->raise('Cannot redeclare property '.$lookup); - } - - $this->token= $this->advance(); - if ('=' === $this->token->value) { - $this->token= $this->expect('='); - $member->value= new Property($modifiers, $name, $type, $this->expression(0), $annotations, $comment); - } else { - $member->value= new Property($modifiers, $name, $type, null, $annotations, $comment); - } - - $body[$lookup]= $member; - if (',' === $this->token->value) { - $this->token= $this->expect(','); - } - } - $modifiers= []; - $annotations= []; - $type= null; - $this->token= $this->expect(';', 'field declaration'); - } else if ('<<' === $this->token->symbol->id) { - do { - $this->token= $this->advance(); - - $name= $this->token->value; - $this->token= $this->advance(); - - if ('(' === $this->token->value) { - $this->token= $this->expect('('); - $annotations[$name]= $this->expression(0); - $this->token= $this->expect(')'); - } else { - $annotations[$name]= null; - } - - if (',' === $this->token->value) { - continue; - } else if ('>>' === $this->token->value) { - break; - } else { - $this->expect(', or >>', 'annotations'); - } - } while (true); - $this->token= $this->expect('>>'); - } else if ($type= $this->type()) { - continue; - } else { - $this->expect('a type, modifier, property, annotation, method or }', 'type body'); - } - } - return $body; - } - - private function expression($rbp, $nud= true) { - $t= $this->token; - $this->token= $this->advance(); - if ($nud || $t->symbol->nud) { - $left= $t->nud(); - } else { - $left= $t; - } - - while ($rbp < $this->token->symbol->lbp) { - $t= $this->token; - $this->token= $this->advance(); - $left= $t->led($left); - } - - return $left; - } - - private function top() { - while (null !== $this->token->value) { - if (null === ($statement= $this->statement())) break; - yield $statement; - } - } - - private function statements() { - $statements= []; - while ('}' !== $this->token->value) { - if (null === ($statement= $this->statement())) break; - $statements[]= $statement; - } - return $statements; - } - - private function statement() { - if ($this->token->symbol->std) { - $t= $this->token; - $this->token= $this->advance(); - return $t->std(); - } - - $expr= $this->expression(0); - $this->token= $this->expect(';'); - return $expr; - } - - // {{ setup - private static function symbol($id, $lbp= 0) { - if (isset(self::$symbols[$id])) { - $symbol= self::$symbols[$id]; - if ($lbp > $symbol->lbp) { - $symbol->lbp= $lbp; - } - } else { - $symbol= new Symbol(); - $symbol->id= $id; - $symbol->lbp= $lbp; - self::$symbols[$id]= $symbol; - } - return $symbol; - } - - private static function constant($id, $value) { - $const= self::symbol($id); - $const->nud= function($node) use($value) { - $node->kind= 'literal'; - $node->value= $value; - return $node; - }; - return $const; - } - - private function stmt($id, $func) { - $stmt= self::symbol($id); - $stmt->std= $func; - return $stmt; - } - - private function assignment($id) { - $infix= self::symbol($id, 10); - $infix->led= function($node, $left) use($id) { - $node->kind= 'assignment'; - $node->value= new Assignment($left, $id, $this->expression(9)); - return $node; - }; - return $infix; - } - - private function infix($id, $bp, $led= null) { - $infix= self::symbol($id, $bp); - $infix->led= $led ?: function($node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expression($bp)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - private function infixr($id, $bp, $led= null) { - $infix= self::symbol($id, $bp); - $infix->led= $led ?: function($node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expression($bp - 1)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - private function infixt($id, $bp) { - $infix= self::symbol($id, $bp); - $infix->led= function($node, $left) use($id, $bp) { - $node->value= new BinaryExpression($left, $id, $this->expressionWithThrows($bp - 1)); - $node->kind= 'binary'; - return $node; - }; - return $infix; - } - - private function prefix($id, $nud= null) { - $prefix= self::symbol($id); - $prefix->nud= $nud ?: function($node) use($id) { - $node->value= new UnaryExpression($this->expression(70), $id); - $node->kind= 'unary'; - return $node; - }; - return $prefix; - } - - private function suffix($id, $bp, $led= null) { - $suffix= self::symbol($id, $bp); - $suffix->led= $led ?: function($node, $left) use($id) { - $node->value= new UnaryExpression($left, $id); - $node->kind= 'unary'; - return $node; - }; - return $suffix; - } - // }}} - - /** - * Raise an error - * - * @param string $error - * @param string $context - * @return void - */ - private function raise($message, $context= null) { - $context && $message.= ' in '.$context; - throw new Error($message, $this->file, $this->token->line); - } - - /** - * Expect a given token, raise an error if another is encountered - * - * @param string $id - * @param string $context - * @return var - */ - private function expect($id, $context= null) { - if ($id !== $this->token->symbol->id) { - $message= sprintf( - 'Expected "%s", have "%s"%s', - $id, - $this->token->value ?: $this->token->symbol->id, - $context ? ' in '.$context : '' - ); - throw new Error($message, $this->file, $this->token->line); - } - - return $this->advance(); - } - - private function advance() { - if ($this->queue) return array_shift($this->queue); - - $line= 1; - while ($this->tokens->valid()) { - $type= $this->tokens->key(); - list($value, $line)= $this->tokens->current(); - $this->tokens->next(); - if ('name' === $type) { - $node= new Node(isset(self::$symbols[$value]) ? self::$symbols[$value] : self::symbol('(name)')); - $node->kind= $type; - } else if ('operator' === $type) { - $node= new Node(self::symbol($value)); - $node->kind= $type; - } else if ('string' === $type || 'integer' === $type || 'decimal' === $type) { - $node= new Node(self::symbol('(literal)')); - $node->kind= 'literal'; - } else if ('variable' === $type) { - $node= new Node(self::symbol('(variable)')); - $node->kind= 'variable'; - } else if ('comment' === $type) { - $this->comment= $value; - continue; - } else { - throw new Error('Unexpected token '.$value, $this->file, $line); - } - - $node->value= $value; - $node->line= $line; - return $node; - } - - $node= new Node(self::symbol('(end)')); - $node->line= $line; - return $node; - } - - public function execute() { - $this->token= $this->advance(); - return $this->top(); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Scope.class.php b/src/main/php/lang/ast/Scope.class.php deleted file mode 100755 index 4b0c076a..00000000 --- a/src/main/php/lang/ast/Scope.class.php +++ /dev/null @@ -1,77 +0,0 @@ - true, - 'int' => true, - 'float' => true, - 'double' => true, - 'bool' => true, - 'array' => true, - 'void' => true, - 'callable' => true, - 'iterable' => true, - 'object' => true, - 'self' => true, - 'static' => true, - 'parent' => true, - 'mixed' => true - ]; - - public $parent; - public $package= null; - public $imports= []; - public $annotations= []; - - public function __construct(self $parent= null) { - $this->parent= $parent; - } - - /** - * Sets package - * - * @param string $name - * @return void - */ - public function package($name) { - $this->package= '\\'.$name; - } - - /** - * Adds an import - * - * @param string $name - * @param string $alias - * @return void - */ - public function import($name, $alias= null) { - $this->imports[$alias ?: substr($name, strrpos($name, '\\') + 1)]= '\\'.$name; - } - - /** - * Resolves a type to a fully qualified name - * - * @param string $name - * @return string - */ - public function resolve($name) { - if (isset(self::$reserved[$name])) { - return $name; - } else if ('\\' === $name{0}) { - return $name; - } else if (isset($this->imports[$name])) { - return $this->imports[$name]; - } else if ($this->package) { - return $this->package.'\\'.$name; - } else if ($this->parent) { - return $this->parent->resolve($name); - } else { - return '\\'.$name; - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php deleted file mode 100755 index 313d9096..00000000 --- a/src/main/php/lang/ast/Tokens.class.php +++ /dev/null @@ -1,121 +0,0 @@ -(){}[]#+-*/'\$\"\r\n\t"; - - private static $operators= [ - '<' => ['<=', '<<', '<>', '', '<<='], - '>' => ['>=', '>>', '>>='], - '=' => ['=>', '==', '==>', '==='], - '!' => ['!=', '!=='], - '&' => ['&&', '&='], - '|' => ['||', '|='], - '^' => ['^='], - '+' => ['+=', '++'], - '-' => ['-=', '--', '->'], - '*' => ['*=', '**', '**='], - '/' => ['/='], - '~' => ['~='], - '%' => ['%='], - '?' => ['?:', '??', '?->', '??='], - '.' => ['.=', '...'], - ':' => ['::'], - "\303" => ["\303\227"] - ]; - - private $source; - - /** - * Create new iterable tokens from a string or a stream tokenizer - * - * @param text.Tokenizer $source - */ - public function __construct(Tokenizer $source) { - $this->source= $source; - $this->source->delimiters= self::DELIMITERS; - $this->source->returnDelims= true; - } - - /** @return php.Iterator */ - public function getIterator() { - $line= 1; - while ($this->source->hasMoreTokens()) { - $token= $this->source->nextToken(); - if ('$' === $token) { - yield 'variable' => [$this->source->nextToken(), $line]; - } else if ('"' === $token || "'" === $token) { - $string= $token; - $end= '\\'.$token; - do { - $t= $this->source->nextToken($end); - if (null === $t) { - throw new FormatException('Unclosed string literal starting at line '.$line); - } else if ('\\' === $t) { - $string.= $t.$this->source->nextToken($end); - } else if ($token === $t) { - break; - } else { - $string.= $t; - } - } while (true); - - yield 'string' => [$string.$token, $line]; - $line+= substr_count($string, "\n"); - } else if (0 === strcspn($token, " \r\n\t")) { - $line+= substr_count($token, "\n"); - continue; - } else if (0 === strcspn($token, '0123456789')) { - if ('.' === ($next= $this->source->nextToken())) { - yield 'decimal' => [str_replace('_', '', $token.$next.$this->source->nextToken()), $line]; - } else { - $this->source->pushBack($next); - yield 'integer' => [str_replace('_', '', $token), $line]; - } - } else if (0 === strcspn($token, self::DELIMITERS)) { - if ('.' === $token) { - $next= $this->source->nextToken(); - if (0 === strcspn($next, '0123456789')) { - yield 'decimal' => [".$next", $line]; - continue; - } - $this->source->pushBack($next); - } else if ('/' === $token) { - $next= $this->source->nextToken(); - if ('/' === $next) { - $this->source->nextToken("\r\n"); - continue; - } else if ('*' === $next) { - $comment= ''; - do { - $t= $this->source->nextToken('/'); - $comment.= $t; - } while ('*' !== $t{strlen($t)- 1} && $this->source->hasMoreTokens()); - $comment.= $this->source->nextToken('/'); - yield 'comment' => [trim(preg_replace('/\n\s+\* ?/', "\n", substr($comment, 1, -2))), $line]; - $line+= substr_count($comment, "\n"); - continue; - } - $this->source->pushBack($next); - } - - if (isset(self::$operators[$token])) { - $combined= $token; - foreach (self::$operators[$token] as $operator) { - while (strlen($combined) < strlen($operator) && $this->source->hasMoreTokens()) { - $combined.= $this->source->nextToken(); - } - $combined === $operator && $token= $combined; - } - - $this->source->pushBack(substr($combined, strlen($token))); - } - yield 'operator' => [$token, $line]; - } else { - yield 'name' => [$token, $line]; - } - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php new file mode 100755 index 00000000..d3a6551e --- /dev/null +++ b/src/main/php/lang/ast/emit/ArbitrayNewExpressions.class.php @@ -0,0 +1,30 @@ +type instanceof IsExpression)) return parent::emitNew($result, $new); + + // Emit supported `new $var`, rewrite unsupported `new ($expr)` + if ($new->type->expression instanceof Variable && $new->type->expression->const) { + $result->out->write('new $'.$new->type->expression->pointer.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } else { + $t= $result->temp(); + $result->out->write('('.$t.'= '); + $this->emitOne($result, $new->type->expression); + $result->out->write(') ? new '.$t.'('); + $this->emitArguments($result, $new->arguments); + $result->out->write(') : null'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php b/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php new file mode 100755 index 00000000..4d90205a --- /dev/null +++ b/src/main/php/lang/ast/emit/ArrayUnpackUsingMerge.class.php @@ -0,0 +1,67 @@ +values)) { + $result->out->write('[]'); + return; + } + + $unpack= false; + foreach ($array->values as $pair) { + $element= $pair[1] ?? $this->raise('Cannot use empty array elements in arrays'); + if ('unpack' === $element->kind) { + $unpack= true; + break; + } + } + + if ($unpack) { + $result->out->write('array_merge(['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + + if ('unpack' === $pair[1]->kind) { + if ('array' === $pair[1]->expression->kind) { + $result->out->write('],'); + $this->emitOne($result, $pair[1]->expression); + $result->out->write(',['); + } else { + $t= $result->temp(); + $result->out->write('],('.$t.'='); + $this->emitOne($result, $pair[1]->expression); + $result->out->write(') instanceof \Traversable ? iterator_to_array('.$t.') : '.$t.',['); + } + } else { + $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + } + $result->out->write('])'); + } else { + $result->out->write('['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(']'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php new file mode 100755 index 00000000..54008a01 --- /dev/null +++ b/src/main/php/lang/ast/emit/AsymmetricVisibility.class.php @@ -0,0 +1,65 @@ +codegen->scope[0]; + $modifiers= Modifiers::bits($property->modifiers); + + // Declare checks for private(set) and protected(set), folding declarations + // like `[visibility] [visibility](set)` to just the visibility itself. + if ($modifiers & 0x1000000) { + $checks= []; + $modifiers&= ~0x1000000; + } else if ($modifiers & 0x0000800) { + $checks= [$this->protected($property->name, 'modify protected(set)')]; + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800; + } else if ($modifiers & 0x0001000) { + $checks= [$this->private($property->name, 'modify private(set)')]; + $modifiers & MODIFIER_PRIVATE ? $modifiers&= ~0x0001000 : $modifiers|= MODIFIER_FINAL; + } + + // Emit XP meta information for the reflection API + $scope->meta[self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [$modifiers] + ]; + + // The readonly flag is really two flags in one: write-once and restricted(set) + if (in_array('readonly', $property->modifiers)) { + $checks[]= $this->initonce($property->name); + } + + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( + new Literal('__virtual'), + new Literal("'{$property->name}'")) + ); + $scope->virtual[$property->name]= [ + new ReturnStatement($virtual), + new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]), + ]; + if (isset($property->expression)) { + $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; + } + } +} \ 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 new file mode 100755 index 00000000..09929b96 --- /dev/null +++ b/src/main/php/lang/ast/emit/AttributesAsComments.class.php @@ -0,0 +1,67 @@ +out->write('\\'.$annotation->name); + if (empty($annotation->arguments)) return; + + // Check whether arguments are constant, enclose in `eval` array + // otherwise. This is not strictly necessary but will ensure + // forward compatibility with PHP 8 + foreach ($annotation->arguments as $argument) { + if ($this->isConstant($result, $argument)) continue; + + $escaping= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); + $result->out->write('(eval: ['); + foreach ($annotation->arguments as $name => $argument) { + is_string($name) && $result->out->write("'{$name}'=>"); + + $result->out->write("'"); + $result->out= $escaping; + $this->emitOne($result, $argument); + $result->out= $escaping->original(); + $result->out->write("',"); + } + $result->out->write('])'); + return; + } + + // We can use named arguments here as PHP 8 attributes are parsed + // by the XP reflection API when using PHP 7. However, we may not + // emit trailing commas here! + $result->out->write('('); + $i= 0; + foreach ($annotation->arguments as $name => $argument) { + $i++ > 0 && $result->out->write(','); + is_string($name) && $result->out->write("{$name}:"); + $this->emitOne($result, $argument); + } + $result->out->write(')'); + } + + protected function emitAnnotations($result, $annotations) { + $line= $annotations->line; + $result->out->write('#['); + + $result->out= new Escaping($result->out, ["\n" => " "]); + foreach ($annotations->named as $annotation) { + $this->emitOne($result, $annotation); + $result->out->write(','); + } + $result->out= $result->out->original(); + + $result->out->write("]\n"); + $result->line= $line + 1; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/CallablesAsClosures.class.php b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php new file mode 100755 index 00000000..f2c2ce3f --- /dev/null +++ b/src/main/php/lang/ast/emit/CallablesAsClosures.class.php @@ -0,0 +1,55 @@ + "f" + $result->out->write('"'.trim($node, '"\'').'"'); + } else if ($node instanceof InstanceExpression) { + + // Rewrite $this->f => [$this, "f"] + $result->out->write('['); + $this->emitOne($result, $node->expression); + $result->out->write(','); + $this->emitQuoted($result, $node->member); + $result->out->write(']'); + } else if ($node instanceof ScopeExpression) { + + // Rewrite T::f => [T::class, "f"] + $result->out->write('['); + if ($node->type instanceof Node) { + $this->emitOne($result, $node->type); + } else { + $result->out->write($node->type.'::class'); + } + $result->out->write(','); + $this->emitQuoted($result, $node->member); + $result->out->write(']'); + } else if ($node instanceof Expression) { + + // Rewrite T::{} => [T::class, ] + $this->emitOne($result, $node->inline); + } else { + + // Emit other expressions as-is + $this->emitOne($result, $node); + } + } + + protected function emitCallable($result, $callable) { + $result->out->write('\Closure::fromCallable('); + $this->emitQuoted($result, $callable->expression); + $result->out->write(')'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/ChainScopeOperators.class.php b/src/main/php/lang/ast/emit/ChainScopeOperators.class.php new file mode 100755 index 00000000..13bd4411 --- /dev/null +++ b/src/main/php/lang/ast/emit/ChainScopeOperators.class.php @@ -0,0 +1,32 @@ +type instanceof Node)) return $this->rewriteDynamicClassConstants($result, $scope); + + if ($scope->member instanceof Literal && 'class' === $scope->member->expression) { + $result->out->write('\\get_class('); + $this->emitOne($result, $scope->type); + $result->out->write(')'); + } else { + $t= $result->temp(); + $result->out->write('(null==='.$t.'='); + $this->emitOne($result, $scope->type); + $result->out->write(")?null:{$t}::"); + $this->emitOne($result, $scope->member); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php new file mode 100755 index 00000000..190038f2 --- /dev/null +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -0,0 +1,109 @@ +type= $type; + $this->codegen= $codegen; + } + + /** @return string */ + public function name() { return ltrim($this->type->name, '\\'); } + + /** + * Checks `#[Override]` + * + * @param lang.ast.emit.Type $type + * @return void + * @throws lang.ast.Error + */ + public function checkOverrides($type) { + foreach ($this->type->body as $member) { + if ($member instanceof Method && $member->annotations && $member->annotations->named(Override::class)) { + $type->checkOverride($member->name, $member->line); + } + } + } + + /** + * Checks `#[Override]` for a given method + * + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + public function checkOverride($method, $line) { + if ($this->type instanceof TraitDeclaration) { + + // Do not check traits, this is done when including them into the type + return; + } else if ($this->type instanceof InterfaceDeclaration) { + + // Check parent interfaces + foreach ($this->type->parents as $interface) { + if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return; + } + } else { + + // Check parent for non-private methods + if ($this->type->parent && $this->codegen->lookup($this->type->parent->literal())->providesMethod( + $method, + MODIFIER_PUBLIC | MODIFIER_PROTECTED + )) return; + + // Finally, check all implemented interfaces + foreach ($this->type->implements as $interface) { + if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return; + } + } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + isset($this->type->name) ? substr($this->type->name->literal(), 1) : 'class@anonymous', + $method + ), + $this->codegen->source, + $line + ); + } + + /** + * Checks whether a given method exists + * + * @param string $named + * @param ?int $select + * @return bool + */ + public function providesMethod($named, $select= null) { + if ($method= $this->type->body["{$named}()"] ?? null) { + return null === $select || (new Modifiers($method->modifiers))->bits() & $select; + } + return false; + } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function rewriteEnumCase($member) { + if (!self::$ENUMS && 'enum' === $this->type->kind) { + return ($this->type->body[$member] ?? null) instanceof EnumCase; + } else if ('class' === $this->type->kind && $this->type->parent && '\\lang\\Enum' === $this->type->parent->literal()) { + return ($this->type->body['$'.$member] ?? null) instanceof Property; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/EmulatePipelines.class.php b/src/main/php/lang/ast/emit/EmulatePipelines.class.php new file mode 100755 index 00000000..828f3829 --- /dev/null +++ b/src/main/php/lang/ast/emit/EmulatePipelines.class.php @@ -0,0 +1,49 @@ +type->arguments= [new Variable(substr($arg, 1))]; + $this->emitOne($result, $target->type); + $target->type->arguments= null; + } else if ($target instanceof CallableExpression) { + $this->emitOne($result, $target->expression); + $result->out->write('('.$arg.')'); + } else { + $result->out->write('('); + $this->emitOne($result, $target); + $result->out->write(')('.$arg.')'); + } + } + + protected function emitPipe($result, $pipe) { + + // $expr |> strtoupper(...) => [$arg= $expr, strtoupper($arg)][1] + $t= $result->temp(); + $result->out->write('['.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(','); + $this->emitPipeTarget($result, $pipe->target, $t); + $result->out->write('][1]'); + } + + protected function emitNullsafePipe($result, $pipe) { + + // $expr ?|> strtoupper(...) => null === ($arg= $expr) ? null : strtoupper($arg) + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(')?null:'); + $this->emitPipeTarget($result, $pipe->target, $t); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Escaping.class.php b/src/main/php/lang/ast/emit/Escaping.class.php new file mode 100755 index 00000000..e096035f --- /dev/null +++ b/src/main/php/lang/ast/emit/Escaping.class.php @@ -0,0 +1,28 @@ +target= $target; + $this->replacements= $replacements; + } + + public function write($bytes) { + $this->target->write(strtr($bytes, $this->replacements)); + } + + public function flush() { + $this->target->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/FinalProperties.class.php b/src/main/php/lang/ast/emit/FinalProperties.class.php new file mode 100755 index 00000000..b5e9851b --- /dev/null +++ b/src/main/php/lang/ast/emit/FinalProperties.class.php @@ -0,0 +1,18 @@ +modifiers= array_diff($property->modifiers, ['final']); + parent::emitProperty($result, $property); + $result->codegen->scope[0]->meta[self::PROPERTY][$property->name][DETAIL_ARGUMENTS]= [MODIFIER_FINAL]; + } +} \ No newline at end of file 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..9e5b36bd --- /dev/null +++ b/src/main/php/lang/ast/emit/GeneratedCode.class.php @@ -0,0 +1,61 @@ +prolog= $prolog; + $this->epilog= $epilog; + parent::__construct($out); + } + + /** + * 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); + } + + /** + * 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 + * + * @return string + */ + public function temp() { + return '$'.$this->codegen->symbol(); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/HHVM320.class.php b/src/main/php/lang/ast/emit/HHVM320.class.php deleted file mode 100755 index f6ddcec3..00000000 --- a/src/main/php/lang/ast/emit/HHVM320.class.php +++ /dev/null @@ -1,28 +0,0 @@ - 72, - 'void' => 71, - 'iterable' => 71, - 'mixed' => null, - ]; - - protected function emitParameter($parameter) { - if ($parameter->variadic) { - $this->out->write('... $'.$parameter->name); - $this->locals[$parameter->name]= true; - } else { - parent::emitParameter($parameter); - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/InType.class.php b/src/main/php/lang/ast/emit/InType.class.php new file mode 100755 index 00000000..4df91107 --- /dev/null +++ b/src/main/php/lang/ast/emit/InType.class.php @@ -0,0 +1,13 @@ +type= $type; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Incomplete.class.php b/src/main/php/lang/ast/emit/Incomplete.class.php new file mode 100755 index 00000000..dfda0688 --- /dev/null +++ b/src/main/php/lang/ast/emit/Incomplete.class.php @@ -0,0 +1,55 @@ +name= $name; } + + /** @return string */ + public function name() { return $this->name; } + + /** + * Checks whether a given method exists + * + * @param string $named + * @param ?int $select + * @return bool + */ + public function providesMethod($named, $select= null) { + return false; + } + + /** + * Checks `#[Override]` + * + * @param lang.ast.emit.Type $type + * @return void + * @throws lang.ast.Error + */ + public function checkOverrides($type) { + // NOOP + } + + /** + * Checks `#[Override]` for a given method + * + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + public function checkOverride($method, $line) { + // NOOP + } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function rewriteEnumCase($member) { + return false; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/MatchAsTernaries.class.php b/src/main/php/lang/ast/emit/MatchAsTernaries.class.php new file mode 100755 index 00000000..9e82693d --- /dev/null +++ b/src/main/php/lang/ast/emit/MatchAsTernaries.class.php @@ -0,0 +1,44 @@ +temp(); + if (null === $match->expression) { + $result->out->write('('.$t.'=true)'); + } else { + $result->out->write('('.$t.'='); + $this->emitOne($result, $match->expression); + $result->out->write(')'); + } + + $b= 0; + foreach ($match->cases as $case) { + foreach ($case->expressions as $expression) { + $b && $result->out->write($t); + $result->out->write('===('); + $this->emitOne($result, $expression); + $result->out->write(')?'); + $this->emitAsExpression($result, $case->body); + $result->out->write(':('); + $b++; + } + } + + // Match without cases => create something that will never match + $b || $result->out->write('===NAN?:'); + + // Emit IIFE for raising an error until we have throw expressions + if (null === $match->default) { + $result->out->write('(function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })()'); + } else { + $this->emitAsExpression($result, $match->default); + } + $result->out->write(str_repeat(')', $b)); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Modifiers.class.php b/src/main/php/lang/ast/emit/Modifiers.class.php new file mode 100755 index 00000000..7e55b853 --- /dev/null +++ b/src/main/php/lang/ast/emit/Modifiers.class.php @@ -0,0 +1,30 @@ + MODIFIER_PUBLIC, + 'protected' => MODIFIER_PROTECTED, + 'private' => MODIFIER_PRIVATE, + 'static' => MODIFIER_STATIC, + 'final' => MODIFIER_FINAL, + 'abstract' => MODIFIER_ABSTRACT, + 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY + 'public(set)' => 0x1000000, + 'protected(set)' => 0x0000800, + 'private(set)' => 0x0001000, + ]; + + /** + * Converts modifiers to a bit set + * + * @param string[] $modifiers + * @return int + */ + public static function bits($modifiers) { + $bits= 0; + foreach ($modifiers as $name) { + $bits|= self::LOOKUP[$name]; + } + return $bits; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php b/src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php new file mode 100755 index 00000000..cd431929 --- /dev/null +++ b/src/main/php/lang/ast/emit/NonCapturingCatchVariables.class.php @@ -0,0 +1,20 @@ +variable ? '$'.$catch->variable : $result->temp(); + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable '.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).' '.$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php b/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php new file mode 100755 index 00000000..32fe0a5b --- /dev/null +++ b/src/main/php/lang/ast/emit/NullsafeAsTernaries.class.php @@ -0,0 +1,17 @@ +temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $instance->expression); + $result->out->write(')?null:'.$t.'->'); + $this->emitOne($result, $instance->member); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/OmitArgumentNames.class.php b/src/main/php/lang/ast/emit/OmitArgumentNames.class.php new file mode 100755 index 00000000..eb487440 --- /dev/null +++ b/src/main/php/lang/ast/emit/OmitArgumentNames.class.php @@ -0,0 +1,17 @@ +out->write(','); + $this->emitOne($result, $argument); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php b/src/main/php/lang/ast/emit/OmitConstModifiers.class.php deleted file mode 100755 index 882223a6..00000000 --- a/src/main/php/lang/ast/emit/OmitConstModifiers.class.php +++ /dev/null @@ -1,16 +0,0 @@ -out->write('const '.$const->name.'='); - $this->emit($const->expression); - $this->out->write(';'); - } -} diff --git a/src/main/php/lang/ast/emit/OmitConstantTypes.class.php b/src/main/php/lang/ast/emit/OmitConstantTypes.class.php new file mode 100755 index 00000000..7685d428 --- /dev/null +++ b/src/main/php/lang/ast/emit/OmitConstantTypes.class.php @@ -0,0 +1,18 @@ +literals[get_class($type)]($type); + } + + /** + * Returns whether a given node is a constant expression: + * + * - Any literal + * - Arrays where all members are literals + * - Scope expressions with literal members (self::class, T::const) + * - Binary expression where left- and right hand side are literals + * + * @see https://wiki.php.net/rfc/const_scalar_exprs + * @param lang.ast.Result $result + * @param lang.ast.Node $node + * @return bool + */ + protected function isConstant($result, $node) { + if ($node instanceof Literal) { + return true; + } else if ($node instanceof ArrayLiteral) { + foreach ($node->values as $element) { + if (!$this->isConstant($result, $element[1])) return false; + } + return true; + } else if ($node instanceof ScopeExpression) { + return ( + $node->member instanceof Literal && + is_string($node->type) && + !$result->codegen->lookup($node->type)->rewriteEnumCase($node->member->expression) + ); + } else if ($node instanceof BinaryExpression) { + return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); + } + return false; + } + + /** + * As of PHP 7.4: Property type declarations support all type declarations + * supported by PHP with the exception of void and callable. + * + * @see https://wiki.php.net/rfc/typed_properties_v2#supported_types + * @param ?lang.ast.Type $type + * @return ?string + */ + protected function propertyType($type) { + if (null === $type || $type instanceof IsFunction || 'callable' === $type->literal()) { + return null; + } else { + return $this->literal($type); + } + } + + /** + * As of PHP 8.3: Constant type declarations support all type declarations + * supported by PHP with the exception of void and callable. + * + * @see https://wiki.php.net/rfc/typed_class_constants#supported_types + * @param ?lang.ast.Type $type + * @return ?string + */ + protected function constantType($type) { + if (null === $type || $type instanceof IsFunction || 'callable' === $type->literal()) { + return null; + } else { + return $this->literal($type); + } + } + + /** + * Enclose a node inside a closure + * + * @param lang.ast.Result $result + * @param lang.ast.Node $node + * @param ?lang.ast.nodes.Signature $signature + * @param bool $static + * @param function(lang.ast.Result, lang.ast.Node): void $emit + */ + protected function enclose($result, $node, $signature, $static, $emit) { + $capture= []; + foreach ($result->codegen->search($node, 'variable') as $var) { + if (isset($result->locals[$var->pointer])) { + $capture[$var->pointer]??= ($result->locals[$var->pointer] ? '&$' : '$').$var->pointer; + } + } + unset($capture['this']); + + $locals= $result->locals; + $result->locals= []; + if ($signature) { + $static ? $result->out->write('static function') : $result->out->write('function'); + $this->emitSignature($result, $signature); + foreach ($signature->parameters as $param) { + unset($capture[$param->name]); + } + } else { + $result->out->write('function()'); + } + + if ($capture) { + $result->out->write('use('.implode(', ', $capture).')'); + foreach ($capture as $name => $variable) { + $result->locals[$name]= '&' === $variable[0]; + } + } + + $result->out->write('{'); + $emit($result, $node); + $result->out->write('}'); + $result->locals= $locals; + } + + /** + * Emits local initializations + * + * @param lang.ast.Result $result + * @param [:lang.ast.Node] $init + * @return void + */ + protected function emitInitializations($result, $init) { + foreach ($init as $assign => $expression) { + $result->out->write($assign.'='); + $this->emitOne($result, $expression); + $result->out->write(';'); + } + } + + /** + * Convert blocks to IIFEs to allow a list of statements where PHP syntactically + * doesn't, e.g. `fn`-style lambdas or match expressions. + * + * @param lang.ast.Result $result + * @param lang.ast.Node $expression + * @return void + */ + protected function emitAsExpression($result, $expression) { + if ($expression instanceof Block) { + $result->out->write('('); + $this->enclose($result, $expression, null, false, function($result, $expression) { + $this->emitAll($result, $expression->statements); + }); + $result->out->write(')()'); + } else { + $this->emitOne($result, $expression); + } + } + + protected function emitDirectives($result, $directives) { + $result->out->write('declare('); + foreach ($directives->declare as $directive => $value) { + $result->out->write($directive.'='); + $this->emitOne($result, $value); + } + $result->out->write(')'); + } + + protected function emitNamespace($result, $declaration) { + $result->out->write('namespace '.$declaration->name); + } + + protected function emitImport($result, $import) { + foreach ($import->names as $name => $alias) { + $result->out->write('use '.$import->type.' '.$name.($alias ? ' as '.$alias : '').';'); + } + } + + protected function emitCode($result, $code) { + $result->out->write($code->value); + } + + protected function emitLiteral($result, $literal) { + $result->out->write($literal->expression); + } + + protected function emitEcho($result, $echo) { + $result->out->write('echo '); + foreach ($echo->expressions as $i => $expr) { + if ($i++) $result->out->write(','); + $this->emitOne($result, $expr); + } + } + + protected function emitBlock($result, $block) { + $result->out->write('{'); + $this->emitAll($result, $block->statements); + $result->out->write('}'); + } + + protected function emitStatic($result, $static) { + foreach ($static->initializations as $variable => $initial) { + $result->out->write('static $'.$variable); + if ($initial) { + $result->out->write('='); + $this->emitOne($result, $initial); + } + $result->out->write(';'); + } + } + + protected function emitVariable($result, $variable) { + if ($variable->const) { + $result->out->write('$'.$variable->pointer); + } else { + $result->out->write('$'); + $this->emitOne($result, $variable->pointer); + } + } + + protected function emitExpression($result, $expression) { + $result->out->write('{'); + $this->emitOne($result, $expression->inline); + $result->out->write('}'); + } + + protected function emitCast($result, $cast) { + static $native= ['string' => true, 'int' => true, 'float' => true, 'bool' => true, 'array' => true, 'object' => true]; + + // Inline nullable checks using ternaries + if ($cast->type instanceof IsNullable) { + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $cast->expression); + $result->out->write(')?null:'); + + $name= $cast->type->element->name(); + $expression= new Variable(substr($t, 1)); + } else { + $name= $cast->type->name(); + $expression= $cast->expression; + } + + if (isset($native[$name])) { + $result->out->write('('.$name.')'); + $this->emitOne($result, $expression); + } else { + $result->out->write('cast('); + $this->emitOne($result, $expression); + $result->out->write(',\''.$name.'\')'); + } + } + + protected function emitArray($result, $array) { + $result->out->write('['); + foreach ($array->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + $this->emitOne($result, $pair[1] ?? $this->raise('Cannot use empty array elements in arrays')); + $result->out->write(','); + } + $result->out->write(']'); + } + + protected function emitParameter($result, $parameter) { + $result->locals[$parameter->name]= $parameter->reference; + $parameter->annotations && $this->emitOne($result, $parameter->annotations); + + // If we have a non-constant default and a type, emit a nullable type hint + // to prevent "implicitely nullable type" warnings being raised. See here: + // https://wiki.php.net/rfc/deprecate-implicitly-nullable-types + $type= $parameter->type; + if ($parameter->default) { + $const= $this->isConstant($result, $parameter->default); + if ($type && !$const && !$type instanceof IsNullable) { + $type= new IsNullable($parameter->type); + } + } + if ($type && $t= $this->literal($type)) $result->out->write($t.' '); + + if ($parameter->variadic) { + $result->out->write('... $'.$parameter->name); + } else { + $result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name); + } + + if ($parameter->default) { + if ($const) { + $result->out->write('='); + $this->emitOne($result, $parameter->default); + } else { + $result->out->write('=null'); + } + } + } + + protected function emitSignature($result, $signature, $use= null) { + $result->out->write('('); + foreach ($signature->parameters as $i => $parameter) { + if ($i++) $result->out->write(','); + $this->emitParameter($result, $parameter); + } + $result->out->write(')'); + + if ($use) { + $result->out->write(' use('.implode(',', $use).') '); + foreach ($use as $variable) { + $result->locals[ltrim($variable, '&$')]= '&' === $variable[0]; + } + } + + if ($signature->returns && $t= $this->literal($signature->returns)) { + $result->out->write(':'.$t); + } + } + + protected function emitFunction($result, $function) { + $locals= $result->locals; + $result->locals= []; + + $result->out->write('function '.($function->signature->byref ? '&' : '').$function->name); + $this->emitSignature($result, $function->signature); + + $result->out->write('{'); + $this->emitAll($result, $function->body); + $result->out->write('}'); + + $result->locals= $locals; + } + + protected function emitClosure($result, $closure) { + $locals= $result->locals; + $result->locals= []; + + $closure->static ? $result->out->write('static function') : $result->out->write('function'); + $this->emitSignature($result, $closure->signature, $closure->use); + + $result->out->write('{'); + $this->emitAll($result, $closure->body); + $result->out->write('}'); + + $result->locals= $locals; + } + + protected function emitLambda($result, $lambda) { + $locals= $result->locals; + + $lambda->static ? $result->out->write('static fn') : $result->out->write('fn'); + $this->emitSignature($result, $lambda->signature); + $result->out->write('=>'); + $this->emitOne($result, $lambda->body); + + $result->locals= $locals; + } + + protected function emitEnumCase($result, $case) { + $result->codegen->scope[0]->meta[self::CONSTANT][$case->name]= [ + DETAIL_RETURNS => 'self', + DETAIL_ANNOTATIONS => $case->annotations, + DETAIL_COMMENT => $case->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $case->annotations && $this->emitOne($result, $case->annotations); + $result->out->write('case '.$case->name); + if ($case->expression) { + $result->out->write('='); + $this->emitOne($result, $case->expression); + } + $result->out->write(';'); + } + + protected function emitEnum($result, $enum) { + $context= $result->codegen->enter(new InType($enum)); + + $enum->comment && $this->emitOne($result, $enum->comment); + $enum->annotations && $this->emitOne($result, $enum->annotations); + $result->at($enum->declared)->out->write('enum '.$enum->declaration()); + $enum->base && $result->out->write(':'.$enum->base); + + if ($enum->implements) { + $list= ''; + foreach ($enum->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write(' implements '.substr($list, 2)); + } + + $result->out->write('{'); + foreach ($enum->body as $member) { + $this->emitOne($result, $member); + } + $result->out->write('static function __init() {'); + $this->emitInitializations($result, $context->statics); + $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); + $result->out->write('}} '.$enum->declaration().'::__init();'); + + $result->codegen->leave(); + } + + protected function emitClass($result, $class) { + $context= $result->codegen->scope[0] ?? $result->codegen->enter(new InType($class)); + + $class->comment && $this->emitOne($result, $class->comment); + $class->annotations && $this->emitOne($result, $class->annotations); + $result->at($class->declared)->out->write(implode(' ', $class->modifiers).' class '.$class->declaration()); + $class->parent && $result->out->write(' extends '.$class->parent->literal()); + + if ($class->implements) { + $list= ''; + foreach ($class->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write(' implements '.substr($list, 2)); + } + + $result->out->write('{'); + foreach ($class->body as $member) { + $this->emitOne($result, $member); + } + + // Virtual properties support: __virtual member + __get() and __set() + if ($context->virtual) { + $result->out->write('private $__virtual= ['); + foreach ($context->virtual as $name => $access) { + $name && $result->out->write("'{$name}' => null,"); + } + $result->out->write('];'); + + $result->out->write('public function &__get($name) { switch ($name) {'); + foreach ($context->virtual as $name => $access) { + $result->out->write($name ? 'case "'.$name.'":' : 'default:'); + $this->emitOne($result, $access[0]); + $result->out->write(';break;'); + } + isset($context->virtual[null]) || $result->out->write( + 'default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING);' + ); + $result->out->write('}}'); + + $result->out->write('public function __set($name, $value) { switch ($name) {'); + foreach ($context->virtual as $name => $access) { + $result->out->write($name ? 'case "'.$name.'":' : 'default:'); + $this->emitOne($result, $access[1]); + $result->out->write(';break;'); + } + $result->out->write('}}'); + } + + // Create constructor for property initializations to non-static scalars + // which have not already been emitted inside constructor + if ($context->init) { + $t= $result->temp(); + $result->out->write("public function __construct(... {$t}) {"); + + // If existant, invoke parent constructor, passing all parameters as arguments + if (($parent= $result->codegen->lookup('parent')) && $parent->providesMethod('__construct')) { + $result->out->write("parent::__construct(... {$t});"); + } + + $this->emitInitializations($result, $context->init); + $result->out->write('}'); + } + + $result->out->write('static function __init() {'); + $this->emitInitializations($result, $context->statics); + $this->emitMeta($result, $class->name, $class->annotations, $class->comment); + $result->out->write('}} '.$class->declaration().'::__init();'); + + $result->codegen->leave(); + } + + protected function emitMeta($result, $name, $annotations, $comment) { + // NOOP + } + + protected function emitComment($result, $comment) { + $result->out->write($comment->declaration); + $result->line+= substr_count($comment->declaration, "\n"); + } + + protected function emitAnnotation($result, $annotation) { + $result->out->write('\\'.$annotation->name); + if (empty($annotation->arguments)) return; + + // Check whether arguments are constant + foreach ($annotation->arguments as $argument) { + if ($this->isConstant($result, $argument)) continue; + + // Found first non-constant argument, enclose in `eval` + $escaping= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']); + $result->out->write('(eval: ['); + foreach ($annotation->arguments as $name => $argument) { + is_string($name) && $result->out->write("'{$name}'=>"); + + $result->out->write("'"); + $result->out= $escaping; + $this->emitOne($result, $argument); + $result->out= $escaping->original(); + $result->out->write("',"); + } + $result->out->write('])'); + return; + } + + $result->out->write('('); + $this->emitArguments($result, $annotation->arguments); + $result->out->write(')'); + } + + protected function emitAnnotations($result, $annotations) { + $result->out->write('#['); + foreach ($annotations->named as $annotation) { + $this->emitOne($result, $annotation); + $result->out->write(','); + } + $result->out->write(']'); + } + + protected function emitInterface($result, $interface) { + $result->codegen->enter(new InType($interface)); + + $interface->comment && $this->emitOne($result, $interface->comment); + $interface->annotations && $this->emitOne($result, $interface->annotations); + $result->at($interface->declared)->out->write('interface '.$interface->declaration()); + $interface->parents && $result->out->write(' extends '.implode(', ', $interface->parents)); + + $result->out->write('{'); + foreach ($interface->body as $member) { + $this->emitOne($result, $member); + } + $result->out->write('}'); + + $this->emitMeta($result, $interface->name, $interface->annotations, $interface->comment); + $result->codegen->leave(); + } + + protected function emitTrait($result, $trait) { + $result->codegen->enter(new InType($trait)); + + $trait->comment && $this->emitOne($result, $trait->comment); + $trait->annotations && $this->emitOne($result, $trait->annotations); + $result->at($trait->declared)->out->write('trait '.$trait->declaration()); + $result->out->write('{'); + foreach ($trait->body as $member) { + $this->emitOne($result, $member); + } + $result->out->write('}'); + + $this->emitMeta($result, $trait->name, $trait->annotations, $trait->comment); + $result->codegen->leave(); + } + + protected function emitUse($result, $use) { + $result->out->write('use '.implode(',', $use->types)); + + // Verify Override + $self= $result->codegen->lookup('self'); + foreach ($use->types as $type) { + $result->codegen->lookup($type)->checkOverrides($self); + } + + if ($use->aliases) { + $result->out->write('{'); + foreach ($use->aliases as $reference => $alias) { + $result->out->write($reference.' '.key($alias).' '.current($alias).';'); + } + $result->out->write('}'); + } else { + $result->out->write(';'); + } + } + + protected function emitConst($result, $const) { + $result->codegen->scope[0]->meta[self::CONSTANT][$const->name]= [ + DETAIL_RETURNS => $const->type ? $const->type->name() : 'var', + DETAIL_ANNOTATIONS => $const->annotations, + DETAIL_COMMENT => $const->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $const->comment && $this->emitOne($result, $const->comment); + $const->annotations && $this->emitOne($result, $const->annotations); + $result->at($const->declared)->out->write(implode(' ', $const->modifiers).' const '.$this->constantType($const->type).' '.$const->name.'='); + $this->emitOne($result, $const->expression); + $result->out->write(';'); + } + + protected function emitProperty($result, $property) { + $result->codegen->scope[0]->meta[self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $property->comment && $this->emitOne($result, $property->comment); + $property->annotations && $this->emitOne($result, $property->annotations); + $result->at($property->declared)->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); + if (isset($property->expression)) { + if ($this->isConstant($result, $property->expression)) { + $result->out->write('='); + $this->emitOne($result, $property->expression); + } else if (in_array('static', $property->modifiers)) { + $result->codegen->scope[0]->statics['self::$'.$property->name]= $property->expression; + } else { + $result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression; + } + } + + if ($property->hooks) { + $result->out->write('{'); + foreach ($property->hooks as $type => $hook) { + $hook->byref && $result->out->write('&'); + $result->out->write($type); + if ($hook->parameter) { + $result->out->write('('); + $this->emitOne($result, $hook->parameter); + $result->out->write(')'); + } + + if (null === $hook->expression) { + $result->out->write(';'); + } else if ($hook->expression instanceof Block) { + $this->emitOne($result, $hook->expression); + } else { + $result->out->write('=>'); + $this->emitOne($result, $hook->expression); + $result->out->write(';'); + } + } + $result->out->write('}'); + } else { + $result->out->write(';'); + } + } + + protected function emitMethod($result, $method) { + $locals= $result->locals; + $result->locals= ['this' => false]; + $meta= [ + DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var', + DETAIL_ANNOTATIONS => $method->annotations, + DETAIL_COMMENT => $method->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [] + ]; + + $method->comment && $this->emitOne($result, $method->comment); + if ($method->annotations) { + $this->emitOne($result, $method->annotations); + $method->annotations->named(Override::class) && $result->codegen->lookup('self')->checkOverride( + $method->name, + $method->line + ); + } + + $result->at($method->declared)->out->write( + implode(' ', $method->modifiers). + ' function '. + ($method->signature->byref ? '&' : ''). + $method->name + ); + + $promoted= $init= []; + foreach ($method->signature->parameters as $param) { + if (isset($param->promote)) $promoted[]= $param; + + // Create a parameter annotation named `Default` for non-constant parameter defaults + if (isset($param->default) && !$this->isConstant($result, $param->default)) { + $param->annotate(new Annotation('Default', [$param->default])); + $init[]= $param; + } + + $meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations; + $meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var'; + } + $this->emitSignature($result, $method->signature); + + if (null === $method->body) { + $result->out->write(';'); + } else { + $result->out->write(' {'); + + // Emit non-constant parameter defaults + foreach ($init as $param) { + $result->out->write('$'.$param->name.' ?? $'.$param->name.'='); + $this->emitOne($result, $param->default); + $result->out->write(';'); + } + + // Emit promoted parameters + foreach ($promoted as $param) { + $result->out->write('$this->'.$param->name.($param->reference ? '=&$' : '=$').$param->name.';'); + } + + // Emit initializations if inside constructor + if ('__construct' === $method->name) { + $this->emitInitializations($result, $result->codegen->scope[0]->init); + $result->codegen->scope[0]->init= []; + } + + $this->emitAll($result, $method->body); + $result->out->write('}'); + } + + foreach ($promoted as $param) { + $this->emitProperty($result, new Property( + is_array($param->promote) ? $param->promote : explode(' ', $param->promote), + $param->name, + $param->type + )); + } + + $result->locals= $locals; + $result->codegen->scope[0]->meta[self::METHOD][$method->name]= $meta; + } + + protected function emitBraced($result, $braced) { + $result->out->write('('); + $this->emitOne($result, $braced->expression); + $result->out->write(')'); + } + + protected function emitBinary($result, $binary) { + $this->emitOne($result, $binary->left); + $result->out->write(' '.$binary->operator.' '); + $this->emitOne($result, $binary->right); + } + + protected function emitPrefix($result, $unary) { + $result->out->write($unary->operator.' '); + $this->emitOne($result, $unary->expression); + } + + protected function emitSuffix($result, $unary) { + $this->emitOne($result, $unary->expression); + $result->out->write($unary->operator); + } + + protected function emitTernary($result, $ternary) { + $this->emitOne($result, $ternary->condition); + $result->out->write('?'); + $this->emitOne($result, $ternary->expression); + $result->out->write(':'); + $this->emitOne($result, $ternary->otherwise); + } + + protected function emitOffset($result, $offset) { + $this->emitOne($result, $offset->expression); + if (null === $offset->offset) { + $result->out->write('[]'); + } else { + $result->out->write('['); + $this->emitOne($result, $offset->offset); + $result->out->write(']'); + } + } + + protected function emitAssign($result, $target) { + if ($target instanceof Variable && $target->const) { + $result->out->write('$'.$target->pointer); + $result->locals[$target->pointer]= false; + } else if ($target instanceof ArrayLiteral) { + $result->out->write('['); + foreach ($target->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + if ($pair[1]) { + $this->emitAssign($result, $pair[1]); + } + $result->out->write(','); + } + $result->out->write(']'); + } else { + $this->emitOne($result, $target); + } + } + + protected function emitAssignment($result, $assignment) { + $this->emitAssign($result, $assignment->variable); + $result->out->write($assignment->operator); + $this->emitOne($result, $assignment->expression); + } + + protected function emitReturn($result, $return) { + $result->out->write('return '); + $return->expression && $this->emitOne($result, $return->expression); + } + + protected function emitIf($result, $if) { + $result->out->write('if ('); + $this->emitOne($result, $if->expression); + $result->out->write(') {'); + $this->emitAll($result, $if->body); + $result->out->write('}'); + + if ($if->otherwise) { + $result->out->write('else {'); + $this->emitAll($result, $if->otherwise); + $result->out->write('}'); + } + } + + protected function emitSwitch($result, $switch) { + $result->out->write('switch ('); + $this->emitOne($result, $switch->expression); + $result->out->write(') {'); + foreach ($switch->cases as $case) { + if ($case->expression) { + $result->out->write('case '); + $this->emitOne($result, $case->expression); + $result->out->write(':'); + } else { + $result->out->write('default:'); + } + $this->emitAll($result, $case->body); + } + $result->out->write('}'); + } + + protected function emitMatch($result, $match) { + if (null === $match->expression) { + $result->out->write('match (true) {'); + } else { + $result->out->write('match ('); + $this->emitOne($result, $match->expression); + $result->out->write(') {'); + } + + foreach ($match->cases as $case) { + $b= 0; + foreach ($case->expressions as $expression) { + $b && $result->out->write(','); + $this->emitOne($result, $expression); + $b++; + } + $result->out->write('=>'); + $this->emitAsExpression($result, $case->body); + $result->out->write(','); + } + + if ($match->default) { + $result->out->write('default=>'); + $this->emitAsExpression($result, $match->default); + } + + $result->out->write('}'); + } + + protected function emitCatch($result, $catch) { + $capture= $catch->variable ? ' $'.$catch->variable : ''; + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable'.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } + + protected function emitTry($result, $try) { + $result->out->write('try {'); + $this->emitAll($result, $try->body); + $result->out->write('}'); + if (isset($try->catches)) { + foreach ($try->catches as $catch) { + $this->emitCatch($result, $catch); + } + } + if (isset($try->finally)) { + $result->out->write('finally {'); + $this->emitAll($result, $try->finally); + $result->out->write('}'); + } + } + + protected function emitThrow($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + $result->out->write(';'); + } + + protected function emitThrowExpression($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + } + + protected function emitForeach($result, $foreach) { + $result->out->write('foreach ('); + $this->emitOne($result, $foreach->expression); + $result->out->write(' as '); + if ($foreach->key) { + $this->emitOne($result, $foreach->key); + $result->out->write(' => '); + } + + // Support empty elements: `foreach ( as [$a, , $b])` + if ($foreach->value instanceof ArrayLiteral) { + $result->out->write('['); + foreach ($foreach->value->values as $pair) { + if ($pair[0]) { + $this->emitOne($result, $pair[0]); + $result->out->write('=>'); + } + $pair[1] && $this->emitOne($result, $pair[1]); + $result->out->write(','); + } + $result->out->write(']'); + } else { + $this->emitOne($result, $foreach->value); + } + $result->out->write(') {'); + $this->emitAll($result, $foreach->body); + $result->out->write('}'); + } + + protected function emitFor($result, $for) { + $result->out->write('for ('); + $this->emitArguments($result, $for->initialization); + $result->out->write(';'); + $this->emitArguments($result, $for->condition); + $result->out->write(';'); + $this->emitArguments($result, $for->loop); + $result->out->write(') {'); + $this->emitAll($result, $for->body); + $result->out->write('}'); + } + + protected function emitDo($result, $do) { + $result->out->write('do'); + $result->out->write('{'); + $this->emitAll($result, $do->body); + $result->out->write('} while ('); + $this->emitOne($result, $do->expression); + $result->out->write(');'); + } + + protected function emitWhile($result, $while) { + $result->out->write('while ('); + $this->emitOne($result, $while->expression); + $result->out->write(') {'); + $this->emitAll($result, $while->body); + $result->out->write('}'); + } + + protected function emitBreak($result, $break) { + $result->out->write('break '); + $break->expression && $this->emitOne($result, $break->expression); + $result->out->write(';'); + } + + protected function emitContinue($result, $continue) { + $result->out->write('continue '); + $continue->expression && $this->emitOne($result, $continue->expression); + $result->out->write(';'); + } + + protected function emitLabel($result, $label) { + $result->out->write($label->name.':'); + } + + protected function emitGoto($result, $goto) { + $result->out->write('goto '.$goto->label); + } + + protected function emitInstanceOf($result, $instanceof) { + $type= $instanceof->type; + + // Supported: instanceof T, instanceof $t, instanceof $t->MEMBER; instanceof T::MEMBER + // Unsupported: instanceof EXPR + if ($type instanceof Variable || $type instanceof InstanceExpression || $type instanceof ScopeExpression) { + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '); + $this->emitOne($result, $type); + } else if ($type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'= '); + $this->emitOne($result, $type); + $result->out->write(')?'); + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '.$t.':null'); + } else { + $this->emitOne($result, $instanceof->expression); + $result->out->write(' instanceof '.$type); + } + } + + protected function emitArguments($result, $arguments) { + $i= 0; + foreach ($arguments as $name => $argument) { + if ($i++) $result->out->write(','); + if (is_string($name)) $result->out->write($name.':'); + $this->emitOne($result, $argument); + } + } + + protected function emitNew($result, $new) { + if ($new->type instanceof IsExpression) { + $result->out->write('new ('); + $this->emitOne($result, $new->type->expression); + $result->out->write(')('); + } else { + $result->out->write('new '.$new->type->literal().'('); + } + + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } + + protected function emitNewClass($result, $new) { + $enclosing= $result->codegen->scope[0] ?? null; + + $result->out->write('(new class('); + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + + // Allow "extends self" to reference enclosing class (except if this + // class is an anonymous class!) + if ($enclosing && $new->definition->parent && 'self' === $new->definition->parent->name()) { + $result->out->write(' extends '.$enclosing->type->name->literal()); + } else if ($new->definition->parent) { + $result->out->write(' extends '.$new->definition->parent->literal()); + } + + if ($new->definition->implements) { + $list= ''; + foreach ($new->definition->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write(' implements '.substr($list, 2)); + } + + $result->codegen->enter(new InType($new->definition)); + $result->out->write('{'); + foreach ($new->definition->body as $member) { + $this->emitOne($result, $member); + } + $result->out->write('function __new() {'); + $this->emitMeta($result, null, [], null); + $result->out->write('return $this; }})->__new()'); + $result->codegen->leave(); + } + + protected function emitCallable($result, $callable) { + + // Disambiguate the following: + // + // - `T::{$func}`, a dynamic class constant + // - `T::{$func}(...)`, a dynamic first-class callable + $callable->expression->line= -1; + + $this->emitOne($result, $callable->expression); + $result->out->write('(...)'); + } + + protected function emitCallableNew($result, $callable) { + $t= $result->temp(); + $result->out->write("fn(...{$t}) => "); + + $callable->type->arguments= [new UnpackExpression(new Variable(substr($t, 1)), $callable->line)]; + $this->emitOne($result, $callable->type); + $callable->type->arguments= null; + } + + protected function emitInvoke($result, $invoke) { + $this->emitOne($result, $invoke->expression); + $result->out->write('('); + $this->emitArguments($result, $invoke->arguments); + $result->out->write(')'); + } + + protected function emitScope($result, $scope) { + + // new T():: vs. e.g. $x:: vs. T:: + if ($scope->type instanceof NewExpression) { + $result->out->write('('); + $this->emitOne($result, $scope->type); + $result->out->write(')::'); + } else if ($scope->type instanceof Node) { + $this->emitOne($result, $scope->type); + $result->out->write('::'); + } else { + $result->out->write("{$scope->type}::"); + } + + // Rewrite T::member to T::$member for XP enums + if ( + $scope->member instanceof Literal && + is_string($scope->type) && + 'class' !== $scope->member->expression && + $result->codegen->lookup($scope->type)->rewriteEnumCase($scope->member->expression) + ) { + $result->out->write('$'.$scope->member->expression); + } else { + $this->emitOne($result, $scope->member); + } + } + + protected function emitInstance($result, $instance) { + if ($instance->expression instanceof NewExpression) { + $result->out->write('('); + $this->emitOne($result, $instance->expression); + $result->out->write(')->'); + } else { + $this->emitOne($result, $instance->expression); + $result->out->write('->'); + } + + $this->emitOne($result, $instance->member); + } + + protected function emitNullsafeInstance($result, $instance) { + $this->emitOne($result, $instance->expression); + $result->out->write('?->'); + $this->emitOne($result, $instance->member); + } + + protected function emitPipe($result, $pipe) { + $this->emitOne($result, $pipe->expression); + $result->out->write('|>'); + $this->emitOne($result, $pipe->target); + } + + protected function emitNullsafePipe($result, $pipe) { + + // $expr ?|> strtoupper(...) => null === ($t= $expr) ? null : $t |> strtoupper(...) + $t= $result->temp(); + $result->out->write('null===('.$t.'='); + $this->emitOne($result, $pipe->expression); + $result->out->write(')?null:'.$t.'|>'); + $this->emitOne($result, $pipe->target); + } + + protected function emitUnpack($result, $unpack) { + $result->out->write('...'); + $this->emitOne($result, $unpack->expression); + } + + protected function emitYield($result, $yield) { + $result->out->write('yield '); + if ($yield->key) { + $this->emitOne($result, $yield->key); + $result->out->write('=>'); + } + if ($yield->value) { + $this->emitOne($result, $yield->value); + } + } + + 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/PHP56.class.php b/src/main/php/lang/ast/emit/PHP56.class.php deleted file mode 100755 index 6f6e99c7..00000000 --- a/src/main/php/lang/ast/emit/PHP56.class.php +++ /dev/null @@ -1,266 +0,0 @@ - 72, - 'void' => 71, - 'iterable' => 71, - 'string' => 70, - 'int' => 70, - 'bool' => 70, - 'float' => 70, - 'mixed' => null, - ]; - private $call= []; - private static $keywords= [ - 'callable' => true, - 'class' => true, - 'trait' => true, - 'extends' => true, - 'implements' => true, - 'static' => true, - 'abstract' => true, - 'final' => true, - 'public' => true, - 'protected' => true, - 'private' => true, - 'const' => true, - 'enddeclare' => true, - 'endfor' => true, - 'endforeach' => true, - 'endif' => true, - 'endwhile' => true, - 'and' => true, - 'global' => true, - 'goto' => true, - 'instanceof' => true, - 'insteadof' => true, - 'interface' => true, - 'namespace' => true, - 'new' => true, - 'or' => true, - 'xor' => true, - 'try' => true, - 'use' => true, - 'var' => true, - 'exit' => true, - 'list' => true, - 'clone' => true, - 'include' => true, - 'include_once' => true, - 'throw' => true, - 'array' => true, - 'print' => true, - 'echo' => true, - 'require' => true, - 'require_once' => true, - 'return' => true, - 'else' => true, - 'elseif' => true, - 'default' => true, - 'break' => true, - 'continue' => true, - 'switch' => true, - 'yield' => true, - 'function' => true, - 'if' => true, - 'endswitch' => true, - 'finally' => true, - 'for' => true, - 'foreach' => true, - 'declare' => true, - 'case' => true, - 'do' => true, - 'while' => true, - 'as' => true, - 'catch' => true, - 'die' => true, - 'self' => true, - 'parent' => true - ]; - - - protected function emitLiteral($literal) { - if ('"' === $literal{0}) { - $this->out->write(preg_replace_callback( - '/\\\\u\{([0-9a-f]+)\}/i', - function($matches) { return html_entity_decode('&#'.hexdec($matches[1]).';', ENT_HTML5, \xp::ENCODING); }, - $literal - )); - } else { - $this->out->write($literal); - } - } - - protected function emitCatch($catch) { - if (empty($catch->types)) { - $this->out->write('catch(\\Exception $'.$catch->variable.') {'); - } else { - $last= array_pop($catch->types); - $label= sprintf('c%u', crc32($last)); - foreach ($catch->types as $type) { - $this->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); - } - $this->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); - } - - $this->emit($catch->body); - $this->out->write('}'); - } - - protected function emitBinary($binary) { - if ('??' === $binary->operator) { - $this->out->write('isset('); - $this->emit($binary->left); - $this->out->write(') ?'); - $this->emit($binary->left); - $this->out->write(' : '); - $this->emit($binary->right); - } else if ('<=>' === $binary->operator) { - $l= $this->temp(); - $r= $this->temp(); - $this->out->write('('.$l.'= '); - $this->emit($binary->left); - $this->out->write(') < ('.$r.'='); - $this->emit($binary->right); - $this->out->write(') ? -1 : ('.$l.' == '.$r.' ? 0 : 1)'); - } else { - parent::emitBinary($binary); - } - } - - protected function emitAssignment($assignment) { - if ('??=' === $assignment->operator) { - $this->out->write('isset('); - $this->emitAssign($assignment->variable); - $this->out->write(') ||'); - $this->emit($assignment->variable); - $this->out->write('='); - $this->emit($assignment->expression); - } else { - $this->emitAssign($assignment->variable); - $this->out->write($assignment->operator); - $this->emit($assignment->expression); - } - } - - /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitInvoke($invoke) { - $expr= $invoke->expression; - if ('braced' === $expr->kind) { - $t= $this->temp(); - $this->out->write('(('.$t.'='); - $this->emit($expr->value); - $this->out->write(') ? '.$t); - $this->out->write('('); - $this->emitArguments($invoke->arguments); - $this->out->write(') : __error(E_RECOVERABLE_ERROR, "Function name must be a string", __FILE__, __LINE__))'); - } else if ( - 'scope' === $expr->kind && - 'name' === $expr->value->member->kind && - isset(self::$keywords[strtolower($expr->value->member->value)]) - ) { - $this->out->write($expr->value->type.'::{\''.$expr->value->member->value.'\'}'); - $this->out->write('('); - $this->emitArguments($invoke->arguments); - $this->out->write(')'); - } else { - parent::emitInvoke($invoke); - } - } - - /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitThrowExpression($throw) { - $capture= []; - foreach ($this->search($throw, 'variable') as $var) { - if (isset($this->locals[$var->value])) { - $capture[$var->value]= true; - } - } - unset($capture['this']); - - $t= $this->temp(); - $this->out->write('(('.$t.'=function()'); - $capture && $this->out->write(' use($'.implode(', $', array_keys($capture)).')'); - $this->out->write('{ throw '); - $this->emit($throw); - $this->out->write('; }) ? '.$t.'() : null)'); - } - - protected function emitNewClass($new) { - $this->out->write('\\lang\\ClassLoader::defineType("class©anonymous'.md5(uniqid()).'", ["kind" => "class"'); - $definition= $new->definition; - $this->out->write(', "extends" => '.($definition->parent ? '[\''.$definition->parent.'\']' : 'null')); - $this->out->write(', "implements" => '.($definition->implements ? '[\''.implode('\', \'', $definition->implements).'\']' : 'null')); - $this->out->write(', "use" => []'); - $this->out->write('], \'{'); - $this->out->write(str_replace('\'', '\\\'', $this->buffer(function() use($definition) { - foreach ($definition->body as $member) { - $this->emit($member); - $this->out->write("\n"); - } - }))); - $this->out->write('}\')->newInstance('); - $this->emitArguments($new->arguments); - $this->out->write(')'); - } - - protected function emitFrom($from) { - $this->out->write('foreach ('); - $this->emit($from); - $this->out->write(' as $key => $val) yield $key => $val;'); - } - - /** @see https://wiki.php.net/rfc/context_sensitive_lexer */ - protected function emitMethod($method) { - if (isset(self::$keywords[strtolower($method->name)])) { - $this->call[in_array('static', $method->modifiers)][]= $method->name; - $method->name= '__'.$method->name; - } else if ('__call' === $method->name || '__callStatic' === $method->name) { - $method->name.= '0'; - } - parent::emitMethod($method); - } - - protected function emitClass($class) { - $this->call= [false => [], true => []]; - array_unshift($this->meta, []); - $this->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name)); - $class->parent && $this->out->write(' extends '.$class->parent); - $class->implements && $this->out->write(' implements '.implode(', ', $class->implements)); - $this->out->write('{'); - foreach ($class->body as $member) { - $this->emit($member); - } - - if ($this->call[false]) { - $this->out->write('function __call($name, $args) {'); - foreach ($this->call[false] as $name) { - $this->out->write('if (\''.$name.'\' === $name) return $this->__'.$name.'(...$args); else '); - } - $this->out->write('return $this->__call0($name, $args); }'); - } - if ($this->call[true]) { - $this->out->write('static function __callStatic($name, $args) {'); - foreach ($this->call[true] as $name) { - $this->out->write('if (\''.$name.'\' === $name) return self::__'.$name.'(...$args); else '); - } - $this->out->write('return self::__callStatic0($name, ...$args); }'); - } - - $this->out->write('static function __init() {'); - $this->emitMeta($class->name, $class->annotations, $class->comment); - $this->out->write('}} '.$class->name.'::__init();'); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php deleted file mode 100755 index 7385da0b..00000000 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ /dev/null @@ -1,20 +0,0 @@ - 72, - 'void' => 71, - 'iterable' => 71, - 'mixed' => null, - ]; -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php deleted file mode 100755 index 7fab7acf..00000000 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ /dev/null @@ -1,18 +0,0 @@ - 72, - 'mixed' => null, - ]; -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php deleted file mode 100755 index 43ba9e99..00000000 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ /dev/null @@ -1,17 +0,0 @@ - null, - ]; -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index 66554507..d8feaec8 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -1,16 +1,59 @@ null, - ]; + public $targetVersion= 70400; + + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { $l= $t->literal(); return 'static' === $l ? 'self' : $l; }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { return null; }, + IsIntersection::class => function($t) { return null; }, + IsLiteral::class => function($t) { + static $rewrite= [ + 'mixed' => 1, + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; + + $l= $t->literal(); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; + }, + IsGeneric::class => function($t) { return null; } + ]; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index d3b3b2f5..02c67c29 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -1,16 +1,76 @@ null, - ]; + public $targetVersion= 80000; + + /** Sets up type => literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { return null; }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if ('null' === $component->literal) { + $u.= '|null'; + } else if (null !== ($l= $this->literal($component))) { + $u.= '|'.$l; + } else { + return null; // One of the components didn't resolve + } + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { + static $rewrite= [ + 'null' => 1, + 'never' => 'void', + 'true' => 'bool', + 'false' => 'bool', + ]; + + $l= $t->literal(); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; + }, + IsGeneric::class => function($t) { return null; } + ]; + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php new file mode 100755 index 00000000..f0062593 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -0,0 +1,79 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if ('null' === $component->literal) { + $u.= '|null'; + } else if (null !== ($l= $this->literal($component))) { + $u.= '|'.$l; + } else { + return null; // One of the components didn't resolve + } + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { + static $rewrite= [ + 'null' => 1, + 'true' => 'bool', + 'false' => 'bool', + ]; + + $l= $t->literal(); + return (1 === ($r= $rewrite[$l] ?? $l)) ? null : $r; + }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php new file mode 100755 index 00000000..c8da1649 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -0,0 +1,64 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php new file mode 100755 index 00000000..101ed9f1 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -0,0 +1,57 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP84.class.php b/src/main/php/lang/ast/emit/PHP84.class.php new file mode 100755 index 00000000..54c0face --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP84.class.php @@ -0,0 +1,57 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP85.class.php b/src/main/php/lang/ast/emit/PHP85.class.php new file mode 100755 index 00000000..4bb3e5e9 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP85.class.php @@ -0,0 +1,57 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { + if (null === ($l= $this->literal($t->element))) return null; + return $t->element instanceof IsUnion ? $l.'|null' : '?'.$l; + }, + IsIntersection::class => function($t) { + $i= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $i.= '&'.$l; + } + return substr($i, 1); + }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); }, + IsGeneric::class => function($t) { return null; } + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php new file mode 100755 index 00000000..66bce050 --- /dev/null +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -0,0 +1,161 @@ +expression) return $literal; + + // Rewrite $this->propertyName to virtual property + if ( + $node instanceof InstanceExpression && + $node->expression instanceof Variable && 'this' === $node->expression->pointer && + $node->member instanceof Literal && $name === $node->member->expression + ) return $virtual; + + // ::$field::hook() => ::___() + if ( + $node instanceof ScopeExpression && + $node->member instanceof InvokeExpression && + $node->member->expression instanceof Literal && + $node->type instanceof ScopeExpression && + $node->type->member instanceof Variable && + is_string($node->type->type) && + is_string($node->type->member->pointer) + ) { + return new ScopeExpression($node->type->type, new InvokeExpression( + new Literal('__'.$node->member->expression->expression.'_'.$node->type->member->pointer), + $node->member->arguments + )); + } + + foreach ($node->children() as &$child) { + $child= $this->rewriteHook($child, $name, $virtual, $literal); + } + return $node; + } + + protected function withScopeCheck($modifiers, $nodes) { + if ($modifiers & MODIFIER_PRIVATE) { + return new Block([$this->private('$name', 'access private'), ...$nodes]); + } else if ($modifiers & MODIFIER_PROTECTED) { + return new Block([$this->protected('$name', 'access protected'), ...$nodes]); + } else if (1 === sizeof($nodes)) { + return $nodes[0]; + } else { + return new Block($nodes); + } + } + + protected function emitProperty($result, $property) { + $scope= $result->codegen->scope[0]; + $modifiers= Modifiers::bits($property->modifiers); + + // Derive modifiers for private(set) and protected(set), folding declarations + // like `[visibility] [visibility](set)` to just the visibility itself. + if ($modifiers & 0x1000000) { + $check= null; + $modifiers&= ~0x1000000; + $write= MODIFIER_PUBLIC; + } else if ($modifiers & 0x0000800) { + $modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800; + $write= MODIFIER_PROTECTED; + } else if ($modifiers & 0x0001000) { + $modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000; + $write= MODIFIER_PRIVATE; + } else { + $write= $modifiers; + } + + $scope->meta[self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => ['interface' === $scope->type->kind ? $modifiers | MODIFIER_ABSTRACT : $modifiers] + ]; + + $literal= new Literal("'{$property->name}'"); + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); + + // Emit get and set hooks in-place. Ignore any unknown hooks + $get= $set= null; + foreach ($property->hooks as $type => $hook) { + $method= '__'.$type.'_'.$property->name; + $modifierList= $modifiers & MODIFIER_ABSTRACT ? ['abstract'] : $hook->modifiers; + if ('get' === $type) { + $this->emitOne($result, new Method( + $modifierList, + $method, + new Signature([], null, $hook->byref), + null === $hook->expression ? null : [$this->rewriteHook( + $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression), + $property->name, + $virtual, + $literal + )], + null // $hook->annotations + )); + $get= $this->withScopeCheck($modifiers, [ + new Assignment(new Variable('r'), $hook->byref ? '=&' : '=', new InvokeExpression( + new InstanceExpression(new Variable('this'), new Literal($method)), + [] + )), + new ReturnStatement(new Variable('r')) + ]); + } else if ('set' === $type) { + $this->emitOne($result, new Method( + $modifierList, + $method, + new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null), + null === $hook->expression ? null : [$this->rewriteHook( + $hook->expression instanceof Block ? $hook->expression : new Assignment($virtual, '=', $hook->expression), + $property->name, + $virtual, + $literal + )], + null // $hook->annotations + )); + $set= $this->withScopeCheck($write, [new InvokeExpression( + new InstanceExpression(new Variable('this'), new Literal($method)), + [new Variable('value')] + )]); + } + } + + // Declare virtual properties with __set and __get as well as initializations + // except inside interfaces, which cannot contain properties. + if ('interface' === $scope->type->kind) return; + + $scope->virtual[$property->name]= [ + $get ?? new ReturnStatement($virtual), + $set ?? new Assignment($virtual, '=', new Variable('value')) + ]; + if (isset($property->expression)) { + $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/ReadonlyClasses.class.php b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php new file mode 100755 index 00000000..a9aa445b --- /dev/null +++ b/src/main/php/lang/ast/emit/ReadonlyClasses.class.php @@ -0,0 +1,45 @@ +modifiers))) { + unset($class->modifiers[$p]); + + // Inherit + foreach ($class->body as $member) { + if ($member->is('property')) { + $member->modifiers[]= 'readonly'; + } else if ($member->is('method')) { + foreach ($member->signature->parameters as $param) { + if (null === $param->promote) { + // NOOP + } else if (is_array($param->promote)) { + $param->promote[]= 'readonly'; + } else if (is_string($param->promote)) { + $param->promote.= ' readonly'; + } + } + } + } + + // Prevent dynamic members + $context= $result->codegen->enter(new InType($class)); + $context->virtual[null]= [ + new Code('trigger_error("Undefined property: ".__CLASS__."::\$".$name, E_USER_WARNING); return $_;'), + new Code('throw new \\Error("Cannot create dynamic property ".__CLASS__."::".$name);') + ]; + } + + return parent::emitClass($result, $class); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php new file mode 100755 index 00000000..1ccf5047 --- /dev/null +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -0,0 +1,65 @@ +codegen->scope[0]; + $modifiers= Modifiers::bits($property->modifiers); + $scope->meta[self::PROPERTY][$property->name]= [ + DETAIL_RETURNS => $property->type ? $property->type->name() : 'var', + DETAIL_ANNOTATIONS => $property->annotations, + DETAIL_COMMENT => $property->comment, + DETAIL_TARGET_ANNO => [], + DETAIL_ARGUMENTS => [$modifiers] + ]; + + // Add visibility check for accessing private and protected properties + if ($modifiers & MODIFIER_PRIVATE) { + $check= $this->private($property->name, 'access private'); + } else if ($modifiers & MODIFIER_PROTECTED) { + $check= $this->protected($property->name, 'access protected'); + } else { + $check= null; + } + + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression( + new Literal('__virtual'), + new Literal("'{$property->name}'")) + ); + + // Create virtual property implementing the readonly semantics + $scope->virtual[$property->name]= [ + $check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual), + new Block([ + $check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'), + $this->initonce($property->name), + new Code(sprintf( + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'. + '$this->__virtual["%1$s"]= [$value];', + $property->name + )), + new Assignment($virtual, '=', new Variable('value')) + ]), + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php new file mode 100755 index 00000000..b7f26660 --- /dev/null +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -0,0 +1,108 @@ +reflect= new ReflectionClass($type); + } catch (ReflectionException $e) { + throw new ClassNotFoundException($type); + } + } + + /** @return string */ + public function name() { return $this->reflect->name; } + + /** + * Checks whether a given method exists + * + * @param string $named + * @param ?int $select + * @return bool + */ + public function providesMethod($named, $select= null) { + if ($this->reflect->hasMethod($named)) { + return null === $select || $this->reflect->getMethod($named)->getModifiers() & $select; + } + return false; + } + + /** + * Checks `#[Override]` + * + * @param lang.ast.emit.Type $type + * @return void + * @throws lang.ast.Error + */ + public function checkOverrides($type) { + $meta= Reflect::meta(); + foreach ($this->reflect->getMethods() as $method) { + if (isset($meta->methodAnnotations($method)[Override::class])) { + $type->checkOverride($method->getName(), $method->getStartLine()); + } + } + } + + /** + * Checks `#[Override]` for a given method + * + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + public function checkOverride($method, $line) { + + // Ignore traits, check parents and interfaces for all other types + if ($this->reflect->isTrait()) { + return; + } else if ($parent= $this->reflect->getParentClass()) { + if ($parent->hasMethod($method)) { + if (!$this->reflect->getMethod($named)->isPrivate()) return; + } + } else { + foreach ($this->type->getInterfaces() as $interface) { + if ($interface->hasMethod($method)) return; + } + } + + throw new Error( + sprintf( + '%s::%s() has #[\\Override] attribute, but no matching parent method exists', + $this->reflect->isAnonymous() ? 'class@anonymous' : $this->reflect->getName(), + $method + ), + $this->reflect->getFileName(), + $line + ); + } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function rewriteEnumCase($member) { + if ($this->reflect->isSubclassOf(Enum::class)) { + return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; + } else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(UnitEnum::class)) { + $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); + return $value instanceof UnitEnum; + } + return false; + } +} \ 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..83a38911 --- /dev/null +++ b/src/main/php/lang/ast/emit/Result.class.php @@ -0,0 +1,63 @@ +out= $out; + $this->codegen= new CodeGen(); + $this->initialize(); + } + + /** + * Set filename this result originates from, defaulting to `(unknown)`. + * + * @param ?string $file + * @return self + */ + public function from($file) { + $this->codegen->source= $file ?? '(unknown)'; + return $this; + } + + + /** + * 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/lang/ast/emit/RewriteBlockLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php index 9b42223c..596014cd 100755 --- a/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php +++ b/src/main/php/lang/ast/emit/RewriteBlockLambdaExpressions.class.php @@ -1,5 +1,7 @@ body)) { - $this->rewriteLambda($lambda); + protected function emitLambda($result, $lambda) { + if ($lambda->body instanceof Block) { + $this->enclose($result, $lambda->body, $lambda->signature, $lambda->static, function($result, $body) { + $this->emitAll($result, $body->statements); + }); } else { - parent::emitLambda($lambda); + parent::emitLambda($result, $lambda); } } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php b/src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php new file mode 100755 index 00000000..b71d3f0d --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteDynamicClassConstants.class.php @@ -0,0 +1,21 @@ +}`. + * + * @see https://wiki.php.net/rfc/dynamic_class_constant_fetch + */ +trait RewriteDynamicClassConstants { + + protected function emitScope($result, $scope) { + if ($scope->member instanceof Expression && -1 !== $scope->line) { + $result->out->write('constant('.$scope->type.'::class."::".'); + $this->emitOne($result, $scope->member->inline); + $result->out->write(')'); + } else { + parent::emitScope($result, $scope); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteEnums.class.php b/src/main/php/lang/ast/emit/RewriteEnums.class.php new file mode 100755 index 00000000..8cd863df --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteEnums.class.php @@ -0,0 +1,87 @@ +out->write('public static $'.$case->name.';'); + } + + protected function emitEnum($result, $enum) { + $context= $result->codegen->enter(new InType($enum)); + $result->out->write('final class '.$enum->declaration().' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum')); + + if ($enum->implements) { + $list= ''; + foreach ($enum->implements as $type) { + $list.= ', '.$type->literal(); + } + $result->out->write($list); + } + + $result->out->write('{'); + + $cases= []; + foreach ($enum->body as $member) { + if ($member->is('enumcase')) $cases[]= $member; + $this->emitOne($result, $member); + } + + // Constructors + if ($enum->base) { + $result->out->write('public $name, $value;'); + $result->out->write('private static $values= [];'); + $result->out->write('private function __construct($name, $value) { + $this->name= $name; + $this->value= $value; + self::$values[$value]= $this; + }'); + $result->out->write('public static function tryFrom($value) { + return self::$values[$value] ?? null; + }'); + $result->out->write('public static function from($value) { + if ($r= self::$values[$value] ?? null) return $r; + throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum ".self::class); + }'); + } else { + $result->out->write('public $name;'); + $result->out->write('private function __construct($name) { + $this->name= $name; + }'); + } + + // Prevent cloning enums + $result->out->write('public function __clone() { + throw new \Error("Trying to clone an uncloneable object of class ".self::class); + }'); + + // Enum cases + $result->out->write('public static function cases() { return ['); + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.', '); + } + $result->out->write(']; }'); + + // Initializations + $result->out->write('static function __init() {'); + if ($enum->base) { + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'", '); + $this->emitOne($result, $case->expression); + $result->out->write(');'); + } + } else { + foreach ($cases as $case) { + $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); + } + } + $this->emitInitializations($result, $context->statics); + $this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment); + $result->out->write('}} '.$enum->name.'::__init();'); + $result->codegen->leave(); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php b/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php new file mode 100755 index 00000000..f44f61e2 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteExplicitOctals.class.php @@ -0,0 +1,17 @@ + `016`. + * + * @see https://wiki.php.net/rfc/explicit_octal_notation + */ +trait RewriteExplicitOctals { + + protected function emitLiteral($result, $literal) { + if ('0' === $literal->expression[0] && ($c= $literal->expression[1] ?? null) && ('o' === $c || 'O' === $c)) { + $result->out->write('0'.substr($literal->expression, 2)); + } else { + $result->out->write($literal->expression); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php b/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php deleted file mode 100755 index f58c43a7..00000000 --- a/src/main/php/lang/ast/emit/RewriteLambdaExpressions.class.php +++ /dev/null @@ -1,47 +0,0 @@ -search($lambda->body, 'variable') as $var) { - if (isset($this->locals[$var->value])) { - $capture[$var->value]= true; - } - } - unset($capture['this']); - - $this->stack[]= $this->locals; - $this->locals= []; - - $this->out->write('function'); - $this->emitSignature($lambda->signature); - foreach ($lambda->signature->parameters as $param) { - unset($capture[$param->name]); - } - - if ($capture) { - $this->out->write(' use($'.implode(', $', array_keys($capture)).')'); - foreach ($capture as $name => $_) { - $this->locals[$name]= true; - } - } - - if (is_array($lambda->body)) { - $this->out->write('{'); - $this->emit($lambda->body); - $this->out->write('}'); - } else { - $this->out->write('{ return '); - $this->emit($lambda->body); - $this->out->write('; }'); - } - - $this->locals= array_pop($this->stack); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php b/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php deleted file mode 100755 index 082004ce..00000000 --- a/src/main/php/lang/ast/emit/RewriteMultiCatch.class.php +++ /dev/null @@ -1,25 +0,0 @@ -types)) { - $this->out->write('catch(\\Throwable $'.$catch->variable.') {'); - } else { - $last= array_pop($catch->types); - $label= sprintf('c%u', crc32($last)); - foreach ($catch->types as $type) { - $this->out->write('catch('.$type.' $'.$catch->variable.') { goto '.$label.'; }'); - } - $this->out->write('catch('.$last.' $'.$catch->variable.') { '.$label.':'); - } - - $this->emit($catch->body); - $this->out->write('}'); - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php b/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php deleted file mode 100755 index 4480c808..00000000 --- a/src/main/php/lang/ast/emit/RewriteNullCoalesceAssignment.class.php +++ /dev/null @@ -1,22 +0,0 @@ -operator) { - $this->emitAssign($assignment->variable); - $this->out->write('='); - $this->emit($assignment->variable); - $this->out->write('??'); - $this->emit($assignment->expression); - } else { - parent::emitAssignment($assignment); - } - } -} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php new file mode 100755 index 00000000..72102e9b --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -0,0 +1,34 @@ +hooks) { + return $this->emitPropertyHooks($result, $property); + } else if ( + $this->targetVersion < 80400 && + array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)']) + ) { + return $this->emitAsymmetricVisibility($result, $property); + } else if ( + $this->targetVersion < 80400 && + in_array('final', $property->modifiers) + ) { + return $this->emitFinalProperties($result, $property); + } else if ( + $this->targetVersion < 80100 && + in_array('readonly', $property->modifiers) + ) { + return $this->emitReadonlyProperties($result, $property); + } + parent::emitProperty($result, $property); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php b/src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php new file mode 100755 index 00000000..618fe57c --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteStaticVariableInitializations.class.php @@ -0,0 +1,26 @@ +initializations as $variable => $initial) { + $result->out->write('static $'.$variable); + if ($initial) { + if ($this->isConstant($result, $initial)) { + $result->out->write('='); + $this->emitOne($result, $initial); + } else { + $result->out->write('= null; null === $'.$variable.' && $'.$variable.'= '); + $this->emitOne($result, $initial); + } + } + $result->out->write(';'); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php b/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php new file mode 100755 index 00000000..777a9dc7 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteThrowableExpressions.class.php @@ -0,0 +1,19 @@ +out->write('('); + $this->enclose($result, $throw->expression, null, false, function($result, $expression) { + $result->out->write('throw '); + $this->emitOne($result, $expression); + $result->out->write(';'); + }); + $result->out->write(')()'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php new file mode 100755 index 00000000..642f9008 --- /dev/null +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -0,0 +1,49 @@ += 80100; + } + + /** @return string */ + public abstract function name(); + + /** + * Checks whether a given method exists + * + * @param string $named + * @param ?int $select + * @return bool + */ + public abstract function providesMethod($named, $select= null); + + /** + * Checks `#[Override]` + * + * @param self $type + * @return void + * @throws lang.ast.Error + */ + public abstract function checkOverrides($type); + + /** + * Checks `#[Override]` for a given method + * + * @param string $method + * @param int $line + * @return void + * @throws lang.ast.Error + */ + public abstract function checkOverride($method, $line); + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public abstract function rewriteEnumCase($member); +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/VisibilityChecks.class.php b/src/main/php/lang/ast/emit/VisibilityChecks.class.php new file mode 100755 index 00000000..45672731 --- /dev/null +++ b/src/main/php/lang/ast/emit/VisibilityChecks.class.php @@ -0,0 +1,26 @@ +__virtual["'.$name.'"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::\$'.$name.'");'); + } + + private function private($name, $access) { + return new Code( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot '.$access.' property ".__CLASS__."::\$'.$name.' from ".($scope ? "scope ".$scope : "global scope"));' + ); + } + + private function protected($name, $access) { + return new Code( + '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. + 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. + 'throw new \\Error("Cannot '.$access.' property ".__CLASS__."::\$'.$name.' from ".($scope ? "scope ".$scope : "global scope"));' + ); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/php/XpMeta.class.php b/src/main/php/lang/ast/emit/php/XpMeta.class.php new file mode 100755 index 00000000..728bfd16 --- /dev/null +++ b/src/main/php/lang/ast/emit/php/XpMeta.class.php @@ -0,0 +1,113 @@ + $arguments) { + $p= strrpos($name, '\\'); + $key= lcfirst(false === $p ? $name : substr($name, $p + 1)); + $result->out->write("'".$key."' => "); + $name === $key || $lookup[$key]= $name; + + if (empty($arguments)) { + $result->out->write('null,'); + } else if (1 === sizeof($arguments) && isset($arguments[0])) { + $this->emitOne($result, $arguments[0]); + $result->out->write(','); + $lookup[$name][$resolve]= 1; // Resolve ambiguity + } else { + $result->out->write('['); + foreach ($arguments as $name => $argument) { + is_string($name) && $result->out->write("'".$name."' => "); + $this->emitOne($result, $argument); + $result->out->write(','); + } + $result->out->write('],'); + } + } + return $lookup; + } + + /** Emits annotations in XP format - and mappings for their names */ + private function attributes($result, $annotations, $target) { + $result->out->write('DETAIL_ANNOTATIONS => ['); + $lookup= $this->annotations($result, $annotations); + $result->out->write('], DETAIL_TARGET_ANNO => ['); + foreach ($target as $name => $annotations) { + $result->out->write("'$".$name."' => ["); + foreach ($this->annotations($result, $annotations, $name) as $key => $value) { + $lookup[$key]= $value; + } + $result->out->write('],'); + } + foreach ($lookup as $key => $value) { + $result->out->write("'{$key}' => ".var_export($value, true).','); + } + $result->out->write(']'); + } + + /** Emit comment inside meta information */ + private function comment($comment) { + return null === $comment ? 'null' : var_export($comment->content(), true); + } + + /** Emit xp::$meta */ + protected function emitMeta($result, $type, $annotations, $comment) { + if (null === $type) { + $result->out->write('\xp::$meta[strtr(self::class, "\\\\", ".")]= ['); + } else if ($type instanceof IsGeneric) { + $result->out->write('\xp::$meta[\''.$type->base->name().'\']= ['); + } else { + $result->out->write('\xp::$meta[\''.$type->name().'\']= ['); + } + $result->out->write('"class" => ['); + $this->attributes($result, $annotations, []); + $result->out->write(', DETAIL_COMMENT => '.$this->comment($comment).'],'); + + foreach ($result->codegen->scope[0]->meta as $type => $lookup) { + $result->out->write($type.' => ['); + foreach ($lookup as $key => $meta) { + $result->out->write("'".$key."' => ["); + $this->attributes($result, $meta[DETAIL_ANNOTATIONS], $meta[DETAIL_TARGET_ANNO]); + $result->out->write(', DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\''); + $result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT])); + $result->out->write(', DETAIL_ARGUMENTS => ['.($meta[DETAIL_ARGUMENTS] + ? "'".implode("', '", $meta[DETAIL_ARGUMENTS])."']]," + : ']],' + )); + } + $result->out->write('],'); + } + $result->out->write('];'); + } + + protected function emitComment($result, $comment) { + // Omit from generated code + } + + protected function emitAnnotation($result, $annotation) { + // Omit from generated code + } + + protected function emitAnnotations($result, $annotations) { + // Omit from generated code + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/php/Using.class.php b/src/main/php/lang/ast/syntax/php/Using.class.php new file mode 100755 index 00000000..3d0ce77f --- /dev/null +++ b/src/main/php/lang/ast/syntax/php/Using.class.php @@ -0,0 +1,65 @@ +open(File::WRITE); + * $f->write(...); + * } + * + * // Rewritten to + * $f= new File(); + * try { + * $f->open(File::WRITE); + * $f->write(...); + * } finally { + * $f->close(); + * } + * ``` + * + * @see https://github.com/xp-framework/compiler/pull/33 + * @test xp://lang.ast.unittest.emit.UsingTest + */ +class Using implements Extension { + + public function setup($language, $emitter) { + $language->stmt('using', function($parse, $node) { + $parse->expecting('(', 'using arguments'); + $arguments= $this->expressions($parse, ')'); + $parse->expecting(')', 'using arguments'); + + $parse->expecting('{', 'using block'); + $statements= $this->statements($parse); + $parse->expecting('}', 'using block'); + + return new UsingStatement($arguments, $statements); + }); + + $emitter->transform('using', function($codegen, $node) { + $cleanup= []; + foreach ($node->arguments as $expression) { + switch ($expression->kind) { + case 'variable': $variable= $expression; yield $expression; break; + case 'assignment': $variable= $expression->variable; yield $expression; break; + default: $variable= new Variable($codegen->symbol()); yield new Assignment($variable, '=', $expression); break; + } + + $cleanup[]= new IfStatement(new InstanceOfExpression($variable, '\lang\Closeable'), + [new InvokeExpression(new InstanceExpression($variable, new Literal('close')), [])], + [new IfStatement(new InstanceOfExpression($variable, '\IDisposable'), + [new InvokeExpression(new InstanceExpression($variable, new Literal('__dispose')), [])] + )] + ); + $cleanup[]= new InvokeExpression(new Literal('unset'), [$variable]); + } + + yield new TryStatement($node->body, null, $cleanup); + }); + } +} \ No newline at end of file diff --git a/src/main/php/module.xp b/src/main/php/module.xp index 798880e2..9517b43e 100755 --- a/src/main/php/module.xp +++ b/src/main/php/module.xp @@ -2,18 +2,11 @@ use lang\ClassLoader; -/** - * XP Compiler - */ +/** XP Compiler */ module xp-framework/compiler { /** @return void */ public function initialize() { - $runtime= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; - ClassLoader::registerLoader(CompilingClassloader::instanceFor($runtime)); - - if (!interface_exists(\IDisposable::class, false)) { - eval('interface IDisposable { public function __dispose(); }'); - } + ClassLoader::registerLoader(CompilingClassloader::instanceFor('php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION)); } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/AstRunner.class.php b/src/main/php/xp/compiler/AstRunner.class.php new file mode 100755 index 00000000..5a5bccfa --- /dev/null +++ b/src/main/php/xp/compiler/AstRunner.class.php @@ -0,0 +1,97 @@ + $prop) { + if ('kind' === $name || 'line' === $name) continue; + $p[$name]= $prop; + } + + $s= -1 === $value->line + ? sprintf("\033[36m%s\033[0m(\033[34m:%s\033[0m", nameof($value), $value->kind) + : sprintf("\033[36m%s\033[0m(\033[32m#%03d\033[0m, \033[34m:%s\033[0m", nameof($value), $value->line, $value->kind) + ; + switch (sizeof($p)) { + case 0: return $s.')'; + case 1: return $s.', '.key($p).'= '.self::stringOf(current($p), $indent).')'; + default: + $s.= ")@{\n"; + $i= $indent.' '; + foreach ($p as $name => $prop) { + $s.= $i.$name.' => '.self::stringOf($prop, $i)."\n"; + } + $s.= $indent.'}'; + return $s; + } + } else if (is_array($value)) { + if (empty($value)) return '[]'; + $s= "[\n"; + $i= $indent.' '; + if (0 === key($value)) { + foreach ($value as $val) { + $s.= $i.self::stringOf($val, $i)."\n"; + } + } else { + foreach ($value as $key => $val) { + $s.= $i.$key.' => '.self::stringOf($val, $i)."\n"; + } + } + return $s.$indent.']'; + } else if (is_string($value)) { + return "\033[34m\"".$value."\"\033[0m"; + } else { + return Objects::stringOf($value); + } + } + + /** @return int */ + public static function main(array $args) { + if (empty($args)) { + Console::writeLine('Usage: xp ast [file]'); + } + + $lang= Language::named('PHP'); + $emit= Emitter::forRuntime('php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION)->newInstance(); + foreach ($lang->extensions() as $extension) { + $extension->setup($lang, $emit); + } + + $input= Input::newInstance($args[0]); + $errors= 0; + foreach ($input as $path => $in) { + $file= $path->toString('/'); + Console::writeLinef("\033[1m══ %s ══%s\033[0m", $file, str_repeat('═', 72 - 6 - strlen($file))); + + try { + foreach ($lang->parse(new Tokens($in, $file))->stream() as $node) { + Console::writeLine(self::stringOf($node)); + } + } catch (Errors $e) { + Console::$err->writeLinef("\033[41;1;37m! %s: %s\033[0m", $file, $e->diagnostics(' ')); + $errors++; + } + } + return $errors ? 1 : 0; + } +} \ 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 82b0ce00..aee9e14a 100755 --- a/src/main/php/xp/compiler/CompileRunner.class.php +++ b/src/main/php/xp/compiler/CompileRunner.class.php @@ -1,18 +1,15 @@ writeLine('Usage: xp compile []'); - return 2; - } + if (empty($args)) return Usage::main($args); - $target= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; + $target= 'php:'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION; $in= $out= '-'; + $quiet= false; + $augment= []; + $ext= \xp::CLASS_FILE_EXT; for ($i= 0; $i < sizeof($args); $i++) { if ('-t' === $args[$i]) { $target= $args[++$i]; + } else if ('-q' === $args[$i]) { + $quiet= true; } else if ('-o' === $args[$i]) { $out= $args[++$i]; $in= array_slice($args, $i + 1); break; + } else if ('-e' === $args[$i]) { + $ext= $args[++$i]; } else if ('-n' === $args[$i]) { $out= null; $in= array_slice($args, $i + 1); break; + } else if ('-a' === $args[$i]) { + $augment[]= self::emitter($args[++$i]); } else { $in= $args[$i]; - $out= isset($args[$i + 1]) ? $args[$i + 1] : '-'; + $out= $args[$i + 1] ?? '-'; break; } } - $emit= Emitter::forRuntime($target); + $lang= Language::named('PHP'); + $emit= Emitter::forRuntime($target, $augment)->newInstance(); + foreach ($lang->extensions() as $extension) { + $extension->setup($lang, $emit); + } + $input= Input::newInstance($in); - $output= Output::newInstance($out); + $output= Output::newInstance($out)->using($ext); $t= new Timer(); $total= $errors= 0; $time= 0.0; - foreach ($input as $path => $in) { + foreach ($input as $path => $source) { + $file= $path->toString('/'); $t->start(); try { - $parse= new Parse(new Tokens(new StreamTokenizer($in))); - $emitter= $emit->newInstance($output->target((string)$path)); - foreach (Transformations::registered() as $kind => $function) { - $emitter->transform($kind, $function); - } - $emitter->emit($parse->execute()); + $parse= $lang->parse(new Tokens($source, $file)); + $emit->write($parse->stream(), $output->target((string)$path), $parse->file); $t->stop(); - Console::$err->writeLinef('> %s (%.3f seconds)', $path->toString('/'), $t->elapsedTime()); - } catch (Error $e) { + $quiet || Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime()); + } catch (Errors $e) { $t->stop(); - Console::$err->writeLinef('! %s %s at line %d', $path->toString('/'), $e->getMessage(), $e->getLine()); + Console::$err->writeLinef('! %s: %s ', $file, $e->diagnostics(' ')); $errors++; } finally { $total++; $time+= $t->elapsedTime(); - $in->close(); + $source->close(); } } - Console::$err->writeLine(); - Console::$err->writeLinef( - "%s Compiled %d file(s) to %s using %s, %d error(s) occurred\033[0m", - $errors ? "\033[41;1;37m×" : "\033[42;1;37m♥", - $total, - $out, - $emit->getName(), - $errors - ); - Console::$err->writeLinef( - "Memory used: %.2f kB (%.2f kB peak)\nTime taken: %.3f seconds", - Runtime::getInstance()->memoryUsage() / 1024, - Runtime::getInstance()->peakMemoryUsage() / 1024, - $time - ); + if (!$quiet) { + Console::$err->writeLine(); + Console::$err->writeLinef( + "%s Compiled %d file(s) to %s using %s, %d error(s) occurred\033[0m", + $errors ? "\033[41;1;37m×" : "\033[42;1;37m♥", + $total, + $out, + typeof($emit)->getName(), + $errors + ); + Console::$err->writeLinef( + "Memory used: %.2f kB (%.2f kB peak)\nTime taken: %.3f seconds", + Runtime::getInstance()->memoryUsage() / 1024, + Runtime::getInstance()->peakMemoryUsage() / 1024, + $time + ); + } + + $output->close(); return $errors ? 1 : 0; } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/FromFile.class.php b/src/main/php/xp/compiler/FromFile.class.php index a82cd330..b33d5e0e 100755 --- a/src/main/php/xp/compiler/FromFile.class.php +++ b/src/main/php/xp/compiler/FromFile.class.php @@ -1,7 +1,7 @@ file->getFileName()) => $this->file->in(); } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/FromFilesIn.class.php b/src/main/php/xp/compiler/FromFilesIn.class.php index 7cd099c7..c16d4fd0 100755 --- a/src/main/php/xp/compiler/FromFilesIn.class.php +++ b/src/main/php/xp/compiler/FromFilesIn.class.php @@ -1,5 +1,6 @@ filesIn($this->folder) as $path => $stream) { yield $path => $stream; } diff --git a/src/main/php/xp/compiler/FromInputs.class.php b/src/main/php/xp/compiler/FromInputs.class.php index 3fb63459..8b01a703 100755 --- a/src/main/php/xp/compiler/FromInputs.class.php +++ b/src/main/php/xp/compiler/FromInputs.class.php @@ -1,8 +1,10 @@ in as $in) { foreach (parent::newInstance($in) as $path => $stream) { yield $path => $stream; diff --git a/src/main/php/xp/compiler/FromStream.class.php b/src/main/php/xp/compiler/FromStream.class.php index 9d490e8e..6772e6f0 100755 --- a/src/main/php/xp/compiler/FromStream.class.php +++ b/src/main/php/xp/compiler/FromStream.class.php @@ -1,5 +1,6 @@ name) => $this->stream; } -} +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/Input.class.php b/src/main/php/xp/compiler/Input.class.php index 0ceb4032..322887b6 100755 --- a/src/main/php/xp/compiler/Input.class.php +++ b/src/main/php/xp/compiler/Input.class.php @@ -14,7 +14,7 @@ abstract class Input implements \IteratorAggregate { */ public static function newInstance($arg) { if ('-' === $arg) { - return new FromStream(Console::$in->getStream(), '-'); + return new FromStream(Console::$in->stream(), '-'); } else if (is_array($arg)) { return new FromInputs($arg); } else if (is_file($arg)) { diff --git a/src/main/php/xp/compiler/Output.class.php b/src/main/php/xp/compiler/Output.class.php index f257ad34..75b8837d 100755 --- a/src/main/php/xp/compiler/Output.class.php +++ b/src/main/php/xp/compiler/Output.class.php @@ -3,6 +3,7 @@ use util\cmd\Console; abstract class Output { + protected $extension= \xp::CLASS_FILE_EXT; /** * Returns output from the command line argument @@ -14,14 +15,27 @@ public static function newInstance($arg) { if (null === $arg) { return new CompileOnly(); } else if ('-' === $arg) { - return new ToStream(Console::$out->getStream()); + return new ToStream(Console::$out->stream()); } else if (strstr($arg, '.php')) { return new ToFile($arg); + } else if (strstr($arg, '.xar')) { + return new ToArchive($arg); } else { return new ToFolder($arg); } } + /** + * Change file extension, which defaults to `xp::CLASS_FILE_EXT`. + * + * @param string $extension + * @return self + */ + public function using($extension) { + $this->extension= '.' === $extension[0] ? $extension : '.'.$extension; + return $this; + } + /** * Returns the target for a given input * @@ -30,4 +44,8 @@ public static function newInstance($arg) { */ public abstract function target($name); + /** @return void */ + public function close() { + // NOOP + } } \ No newline at end of file diff --git a/src/main/php/xp/compiler/ToArchive.class.php b/src/main/php/xp/compiler/ToArchive.class.php new file mode 100755 index 00000000..91a3a65a --- /dev/null +++ b/src/main/php/xp/compiler/ToArchive.class.php @@ -0,0 +1,46 @@ +archive= new Archive($file instanceof File ? $file : new File($file)); + $this->archive->open(Archive::CREATE); + } + + /** + * Returns the target for a given input + * + * @param string $name + * @return io.streams.OutputStream + */ + public function target($name) { + return new class($this->archive, $name, $this->extension) implements OutputStream { + private $archive, $name, $replace; + private $bytes= ''; + + public function __construct($archive, $name, $extension) { + $this->archive= $archive; + $this->name= $name; + $this->replace= [DIRECTORY_SEPARATOR => '/', '.php' => $extension]; + } + + public function write($bytes) { $this->bytes.= $bytes; } + + public function flush() { } + + public function close() { $this->archive->addBytes(strtr($this->name, $this->replace), $this->bytes); } + }; + } + + /** @return void */ + public function close() { + $this->archive->create(); + } +} \ No newline at end of file diff --git a/src/main/php/xp/compiler/ToFolder.class.php b/src/main/php/xp/compiler/ToFolder.class.php index 2a0e77ce..56773dd2 100755 --- a/src/main/php/xp/compiler/ToFolder.class.php +++ b/src/main/php/xp/compiler/ToFolder.class.php @@ -1,7 +1,6 @@ folder, 'out'.\xp::CLASS_FILE_EXT); + $f= new File($this->folder, 'out'.$this->extension); } else { - $f= new File($this->folder, str_replace('.php', \xp::CLASS_FILE_EXT, $name)); + $f= new File($this->folder, str_replace('.php', $this->extension, $name)); } $this->ensure($f->path); return $f->out(); diff --git a/src/main/php/xp/compiler/Usage.class.php b/src/main/php/xp/compiler/Usage.class.php new file mode 100755 index 00000000..fdcef972 --- /dev/null +++ b/src/main/php/xp/compiler/Usage.class.php @@ -0,0 +1,52 @@ +writeLine('Usage: xp compile []'); + + $impl= new class() { + public $byLoader= []; + + public function add($t, $active= false) { + $this->byLoader[$t->classLoader()->toString()][$t->name()]= $active; + } + }; + + $emitter= Emitter::forRuntime(self::RUNTIME.':'.PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION.'.'.PHP_RELEASE_VERSION); + foreach ((new Package('lang.ast.emit'))->types() as $type) { + if ($type->is(Emitter::class) && !$type->modifiers()->isAbstract()) { + $impl->add($type, $type->class()->equals($emitter)); + } + } + + $language= Language::named(strtoupper(self::RUNTIME)); + foreach ((new Package('lang.ast.syntax'))->types() as $type) { + if ($type->is(Language::class) && !$type->modifiers()->isAbstract()) { + $impl->add($type, $type->isInstance($language)); + } + } + + foreach ($language->extensions() as $extension) { + $impl->add(Reflection::type($extension), true); + } + + // Show implementations sorted by class loader + foreach ($impl->byLoader as $loader => $list) { + Console::$err->writeLine(); + Console::$err->writeLine("\033[33m@", $loader, "\033[0m"); + foreach ($list as $impl => $active) { + Console::$err->writeLine($impl, $active ? " [\033[36m*\033[0m]" : ''); + } + } + return 2; + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/CodeGenTest.class.php b/src/test/php/lang/ast/unittest/CodeGenTest.class.php new file mode 100755 index 00000000..4e6abe3f --- /dev/null +++ b/src/test/php/lang/ast/unittest/CodeGenTest.class.php @@ -0,0 +1,62 @@ +enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); + + Assert::equals(new Declaration($context->type, $fixture), $fixture->lookup('self')); + } + + #[Test] + public function lookup_parent() { + $fixture= new CodeGen(); + $fixture->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), new IsValue('\\lang\\Value'), [], [], null, null, 1))); + + Assert::equals(new Reflection(Value::class), $fixture->lookup('parent')); + } + + #[Test] + public function lookup_parent_without_parent() { + $fixture= new CodeGen(); + $fixture->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); + + Assert::null($fixture->lookup('parent')); + } + + #[Test] + public function lookup_named() { + $fixture= new CodeGen(); + $context= $fixture->enter(new InType(new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1))); + + Assert::equals(new Declaration($context->type, $fixture), $fixture->lookup('\\T')); + } + + #[Test] + public function lookup_value_interface() { + $fixture= new CodeGen(); + + Assert::equals(new Reflection(Value::class), $fixture->lookup('\\lang\\Value')); + } + + #[Test] + public function lookup_non_existant() { + $fixture= new CodeGen(); + Assert::instance(Incomplete::class, $fixture->lookup('\\NotFound')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/EmitterTest.class.php b/src/test/php/lang/ast/unittest/EmitterTest.class.php index 591d293e..51c2f378 100755 --- a/src/test/php/lang/ast/unittest/EmitterTest.class.php +++ b/src/test/php/lang/ast/unittest/EmitterTest.class.php @@ -1,20 +1,137 @@ out= new MemoryOutputStream(); + private function newEmitter() { + return Emitter::forRuntime('php:'.PHP_VERSION)->newInstance(); } - #[@test] + #[Test] public function can_create() { - $runtime= defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION; - Emitter::forRuntime($runtime)->newInstance(new StringWriter($this->out)); + $this->newEmitter(); + } + + #[Test] + public function dotted_argument_bc() { + Assert::equals(Emitter::forRuntime('php:'.PHP_VERSION), Emitter::forRuntime('PHP.'.PHP_VERSION)); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function cannot_create_for_unsupported_php_version() { + Emitter::forRuntime('php:4.3.0'); + } + + #[Test] + public function transformations_initially_empty() { + Assert::equals([], $this->newEmitter()->transformations()); + } + + #[Test] + public function transform() { + $function= fn($class) => $class; + + $fixture= $this->newEmitter(); + $fixture->transform('class', $function); + Assert::equals(['class' => [$function]], $fixture->transformations()); + } + + #[Test] + public function remove() { + $first= fn($codegen, $class) => $class; + $second= function($codegen, $class) { $class->annotations['author']= 'Test'; return $class; }; + + $fixture= $this->newEmitter(); + $transformation= $fixture->transform('class', $first); + $fixture->transform('class', $second); + $fixture->remove($transformation); + Assert::equals(['class' => [$second]], $fixture->transformations()); + } + + #[Test] + public function remove_unsets_empty_kind() { + $function= fn($codegen, $class) => $class; + + $fixture= $this->newEmitter(); + $transformation= $fixture->transform('class', $function); + $fixture->remove($transformation); + Assert::equals([], $fixture->transformations()); + } + + #[Test, Expect(IllegalStateException::class)] + public function emit_node_without_kind() { + $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->pointer= '_'.$var->pointer; + return $var; + }); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); + + Assert::equals('bytes()); + } + + #[Test] + public function transform_to_node() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', function($codegen, $var) { + return new Code('$variables["'.$var->pointer.'"]'); + }); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); + + Assert::equals('bytes()); + } + + #[Test] + public function transform_to_array() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', function($codegen, $var) { + return [new Code('$variables["'.$var->pointer.'"]')]; + }); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); + + Assert::equals('bytes()); + } + + #[Test] + public function transform_to_null() { + $fixture= $this->newEmitter(); + $fixture->transform('variable', fn($codegen, $var) => null); + $out= $fixture->write([new Variable('a')], new MemoryOutputStream()); + + Assert::equals('bytes()); + } + + #[Test] + public function emit_multiline_comment() { + $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); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/LineNumberTest.class.php b/src/test/php/lang/ast/unittest/LineNumberTest.class.php deleted file mode 100755 index 1c49eb1b..00000000 --- a/src/test/php/lang/ast/unittest/LineNumberTest.class.php +++ /dev/null @@ -1,95 +0,0 @@ - $value) { - $actual[]= [$value[0] => $value[1]]; - } - $this->assertEquals($expected, $actual); - } - - #[@test] - public function starts_with_line_number_one() { - $this->assertPositions( - [['HERE' => 1]], - new Tokens(new StringTokenizer("HERE")) - ); - } - - #[@test] - public function unix_lines() { - $this->assertPositions( - [['LINE1' => 1], ['LINE2' => 2]], - new Tokens(new StringTokenizer("LINE1\nLINE2")) - ); - } - - #[@test] - public function windows_lines() { - $this->assertPositions( - [['LINE1' => 1], ['LINE2' => 2]], - new Tokens(new StringTokenizer("LINE1\r\nLINE2")) - ); - } - - #[@test] - public function after_regular_comment() { - $this->assertPositions( - [['HERE' => 2]], - new Tokens(new StringTokenizer("// Comment\nHERE")) - ); - } - - #[@test] - public function apidoc_comment() { - $this->assertPositions( - [['COMMENT' => 1], ['HERE' => 2]], - new Tokens(new StringTokenizer("/** COMMENT */\nHERE")) - ); - } - - #[@test] - public function multi_line_apidoc_comment() { - $this->assertPositions( - [["LINE1\nLINE2" => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("/** LINE1\nLINE2 */\nHERE")) - ); - } - - #[@test] - public function multi_line_apidoc_comment_is_trimmed() { - $this->assertPositions( - [['COMMENT' => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("/** COMMENT\n */\nHERE")) - ); - } - - #[@test] - public function multi_line_apidoc_comment_leading_stars_removed() { - $this->assertPositions( - [["LINE1\nLINE2" => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("/** LINE1\n * LINE2 */\nHERE")) - ); - } - - #[@test] - public function multi_line_string() { - $this->assertPositions( - [["'STRING\n'" => 1], ['HERE' => 3]], - new Tokens(new StringTokenizer("'STRING\n'\nHERE")) - ); - } -} \ 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 new file mode 100755 index 00000000..857e0dc6 --- /dev/null +++ b/src/test/php/lang/ast/unittest/ResultTest.class.php @@ -0,0 +1,36 @@ +out->write('echo "Hello";'); + Assert::equals('echo "Hello";', $out->bytes()); + } + + #[Test] + public function write_escaped() { + $out= new MemoryOutputStream(); + $r= new Result($out); + + $r->out->write("'"); + $r->out= new Escaping($out, ["'" => "\\'"]); + $r->out->write("echo 'Hello'"); + + $r->out= $out; + $r->out->write("'"); + + Assert::equals("'echo \'Hello\''", $out->bytes()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/ScopeTest.class.php b/src/test/php/lang/ast/unittest/ScopeTest.class.php deleted file mode 100755 index e53540b4..00000000 --- a/src/test/php/lang/ast/unittest/ScopeTest.class.php +++ /dev/null @@ -1,75 +0,0 @@ -package('test'); - - $this->assertEquals('\\test', $s->package); - } - - #[@test] - public function resolve_in_global_scope() { - $s= new Scope(); - - $this->assertEquals('\\Parse', $s->resolve('Parse')); - } - - #[@test] - public function resolve_in_package() { - $s= new Scope(); - $s->package('test'); - - $this->assertEquals('\\test\\Parse', $s->resolve('Parse')); - } - - #[@test] - public function resolve_relative_in_package() { - $s= new Scope(); - $s->package('test'); - - $this->assertEquals('\\test\\ast\\Parse', $s->resolve('ast\\Parse')); - } - - #[@test] - public function resolve_imported_in_package() { - $s= new Scope(); - $s->package('test'); - $s->import('lang\\ast\\Parse'); - - $this->assertEquals('\\lang\\ast\\Parse', $s->resolve('Parse')); - } - - #[@test] - public function resolve_imported_in_global_scope() { - $s= new Scope(); - $s->import('lang\\ast\\Parse'); - - $this->assertEquals('\\lang\\ast\\Parse', $s->resolve('Parse')); - } - - #[@test] - public function package_inherited_from_parent() { - $s= new Scope(); - $s->package('test'); - - $this->assertEquals('\\test\\Parse', (new Scope($s))->resolve('Parse')); - } - - #[@test] - public function import_inherited_from_parent() { - $s= new Scope(); - $s->import('lang\\ast\\Parse'); - - $this->assertEquals('\\lang\\ast\\Parse', (new Scope($s))->resolve('Parse')); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TokensTest.class.php b/src/test/php/lang/ast/unittest/TokensTest.class.php deleted file mode 100755 index ec3baa3c..00000000 --- a/src/test/php/lang/ast/unittest/TokensTest.class.php +++ /dev/null @@ -1,103 +0,0 @@ - $value) { - $actual[]= [$type => $value[0]]; - } - $this->assertEquals($expected, $actual); - } - - #[@test] - public function can_create() { - new Tokens(new StringTokenizer('test')); - } - - #[@test, @values([ - # '""', - # "''", - # "'\\\\'", - # '"Test"', - # "'Test'", - # "'Test\''", - # "'\\\\\\''", - #])] - public function string_literals($input) { - $this->assertTokens([['string' => $input]], new Tokens(new StringTokenizer($input))); - } - - #[@test, @expect(class= FormatException::class, withMessage= '/Unclosed string literal/'), @values([ - # '"', - # "'", - # '"Test', - # "'Test" - #])] - public function unclosed_string_literals($input) { - $t= (new Tokens(new StringTokenizer($input)))->getIterator(); - $t->current(); - } - - #[@test, @values(['0', '1', '1_000_000_000'])] - public function integer_literal($input) { - $this->assertTokens([['integer' => str_replace('_', '', $input)]], new Tokens(new StringTokenizer($input))); - } - - #[@test, @values(['0.0', '6.1', '.5', '107_925_284.88'])] - public function float_literal($input) { - $this->assertTokens([['decimal' => str_replace('_', '', $input)]], new Tokens(new StringTokenizer($input))); - } - - #[@test, @values([ - # '$a', - # '$_', - # '$input' - #])] - public function variables($input) { - $this->assertTokens([['variable' => substr($input, 1)]], new Tokens(new StringTokenizer($input))); - } - - #[@test, @values([ - # '+', '-', '*', '/', '**', - # '==', '!=', - # '<=', '>=', '<=>', - # '===', '!==', - # '=>', - # '==>', - # '->', - #])] - public function operators($input) { - $this->assertTokens([['operator' => $input]], new Tokens(new StringTokenizer($input))); - } - - #[@test] - public function annotation() { - $this->assertTokens( - [['operator' => '<<'], ['name' => 'test'], ['operator' => '>>']], - new Tokens(new StringTokenizer('<>')) - ); - } - - #[@test] - public function regular_comment() { - $this->assertTokens([], new Tokens(new StringTokenizer('// Comment'))); - } - - #[@test] - public function apidoc_comment() { - $this->assertTokens([['comment' => 'Test']], new Tokens(new StringTokenizer('/** Test */'))); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php new file mode 100755 index 00000000..999e24ce --- /dev/null +++ b/src/test/php/lang/ast/unittest/TypeLiteralsTest.class.php @@ -0,0 +1,126 @@ +base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'void']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), null]; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), null]; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), null]; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; + } + + /** + * PHP 8.0 added `mixed` and union types + * + * @return iterable + */ + private function php80() { + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'void']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), 'string|bool']; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), null]; + } + + /** + * PHP 8.1 added `never` and intersections + * + * @return iterable + */ + private function php81() { + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'never']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsLiteral('null'), null]; + yield [new IsLiteral('false'), 'bool']; + yield [new IsLiteral('true'), 'bool']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), 'string|bool']; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), 'Test&Iterator']; + } + + /** + * PHP 8.2 added `null`, `false` and `true` + * + * @return iterable + */ + private function php82() { + yield from $this->base(); + yield [new IsLiteral('object'), 'object']; + yield [new IsLiteral('void'), 'void']; + yield [new IsLiteral('never'), 'never']; + yield [new IsLiteral('iterable'), 'iterable']; + yield [new IsLiteral('mixed'), 'mixed']; + yield [new IsLiteral('null'), 'null']; + yield [new IsLiteral('false'), 'false']; + yield [new IsLiteral('true'), 'true']; + yield [new IsNullable(new IsLiteral('string')), '?string']; + yield [new IsNullable(new IsLiteral('object')), '?object']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('int')]), 'string|int']; + yield [new IsUnion([new IsLiteral('string'), new IsLiteral('false')]), 'string|false']; + yield [new IsIntersection([new IsValue('Test'), new IsValue('Iterator')]), 'Test&Iterator']; + } + + #[Test, Values(from: 'php74')] + public function php74_literals($type, $literal) { + Assert::equals($literal, (new PHP74())->literal($type)); + } + + #[Test, Values(from: 'php80')] + public function php80_literals($type, $literal) { + Assert::equals($literal, (new PHP80())->literal($type)); + } + + #[Test, Values(from: 'php81')] + public function php81_literals($type, $literal) { + Assert::equals($literal, (new PHP81())->literal($type)); + } + + #[Test, Values(from: 'php82')] + public function php82_literals($type, $literal) { + Assert::equals($literal, (new PHP82())->literal($type)); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php b/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php new file mode 100755 index 00000000..f5590344 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/CompileOnlyTest.class.php @@ -0,0 +1,23 @@ +target('Test.php'), function($out) { + $out->write('flush(); + $out->close(); + }); + $fixture->close(); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php new file mode 100755 index 00000000..6a1bf5b1 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/FromFileTest.class.php @@ -0,0 +1,47 @@ +folder= new Folder(Environment::tempDir(), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->file= new File($this->folder, 'Test.php'); + $this->file->touch(); + } + + #[After] + public function cleanup() { + $this->file->isOpen() && $this->file->close(); + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new FromFile($this->file); + } + + #[Test] + public function can_create_from_string() { + new FromFile($this->file->getURI()); + } + + #[Test] + public function iteration() { + $results= []; + foreach (new FromFile($this->file) as $path => $stream) { + $results[(string)$path]= get_class($stream); + } + + Assert::equals([$this->file->getFileName() => FileInputStream::class], $results); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php new file mode 100755 index 00000000..acc3f683 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/FromFilesInTest.class.php @@ -0,0 +1,62 @@ +folder, 'A.php'); + $a->touch(); + yield [['A.php' => FileInputStream::class]]; + + $b= new File($this->folder, 'B.php'); + $b->touch(); + yield [['A.php' => FileInputStream::class, 'B.php' => FileInputStream::class]]; + + $child= new Folder($this->folder, 'c'); + $child->create(); + $c= new File($child, 'C.php'); + $c->touch(); + yield [['A.php' => FileInputStream::class, 'B.php' => FileInputStream::class, 'c/C.php' => FileInputStream::class]]; + } + + #[Before] + public function folder() { + $this->folder= new Folder(Environment::tempDir(), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new FromFilesIn($this->folder); + } + + #[Test] + public function can_create_from_string() { + new FromFilesIn($this->folder->getURI()); + } + + #[Test, Values(from: 'files')] + public function iteration($expected) { + $results= []; + foreach (new FromFilesIn($this->folder) as $path => $stream) { + $results[$path->toString('/')]= get_class($stream); + } + + Assert::equals($expected, $results); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php b/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php new file mode 100755 index 00000000..1832bbf0 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/FromInputsTest.class.php @@ -0,0 +1,30 @@ + ConsoleInputStream::class]]; + yield [[__FILE__, '-'], [basename(__FILE__) => FileInputStream::class, '-' => ConsoleInputStream::class]]; + } + + #[Test] + public function can_create() { + new FromInputs([]); + } + + #[Test, Values(from: 'inputs')] + public function iteration($inputs, $expected) { + $results= []; + foreach (new FromInputs($inputs) as $path => $stream) { + $results[$path->toString('/')]= get_class($stream); + } + + Assert::equals($expected, $results); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/InputTest.class.php b/src/test/php/lang/ast/unittest/cli/InputTest.class.php new file mode 100755 index 00000000..90d2b2c3 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/InputTest.class.php @@ -0,0 +1,57 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->file= new File($this->folder, 'Test.php'); + $this->file->touch(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function from_stdin() { + Assert::equals(new FromStream(Console::$in->stream(), '-'), Input::newInstance('-')); + } + + #[Test] + public function from_file() { + Assert::equals(new FromFile($this->file), Input::newInstance($this->file->getURI())); + } + + #[Test] + public function from_folder() { + Assert::equals(new FromFilesIn($this->folder), Input::newInstance($this->folder->getURI())); + } + + #[Test] + public function from_empty_array() { + Assert::equals(new FromInputs([]), Input::newInstance([])); + } + + #[Test] + public function from_array() { + $array= ['-', $this->file]; + Assert::equals(new FromInputs($array), Input::newInstance($array)); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function from_illegal_argument() { + Input::newInstance(''); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/OutputTest.class.php b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php new file mode 100755 index 00000000..c4369971 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/OutputTest.class.php @@ -0,0 +1,54 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->file= new File($this->folder, 'Test.php'); + $this->file->touch(); + + $this->archive= new File($this->folder, 'dist.xar'); + $this->archive->touch(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function compile_only() { + Assert::equals(new CompileOnly(), Output::newInstance(null)); + } + + #[Test] + public function to_stdin() { + Assert::equals(new ToStream(Console::$out->stream()), Output::newInstance('-')); + } + + #[Test] + public function to_file() { + Assert::equals(new ToFile($this->file), Output::newInstance($this->file->getURI())); + } + + #[Test] + public function to_archive() { + Assert::equals(new ToArchive($this->archive), Output::newInstance($this->archive->getURI())); + } + + #[Test] + public function to_folder() { + Assert::equals(new ToFolder($this->folder), Output::newInstance($this->folder->getURI())); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php new file mode 100755 index 00000000..f65f822c --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToArchiveTest.class.php @@ -0,0 +1,51 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->archive= new File($this->folder, 'dist.xar'); + $this->archive->touch(); + } + + #[After] + public function cleanup() { + $this->archive->isOpen() && $this->archive->close(); + try { + $this->folder->unlink(); + } catch (IOException $ignored) { + // XP xar support holds on to file handles + } + } + + #[Test] + public function can_create() { + new ToArchive($this->archive); + } + + #[Test] + public function write_to_target_then_load_via_class_loader() { + $class= 'archive); + with ($fixture->target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, (new ArchiveClassLoader($this->archive->getURI()))->loadClassBytes('Test')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php new file mode 100755 index 00000000..43c2bef1 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToFileTest.class.php @@ -0,0 +1,45 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + + $this->target= new File($this->folder, 'Test.class.php'); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new ToFile($this->target); + } + + #[Test] + public function write_to_target_then_load_via_class_loader() { + $class= 'target); + with ($fixture->target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, (new FileSystemClassLoader($this->folder->getURI()))->loadClassBytes('Test')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php new file mode 100755 index 00000000..8b054314 --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToFolderTest.class.php @@ -0,0 +1,51 @@ +folder= new Folder(realpath(Environment::tempDir()), '.xp-'.crc32(self::class)); + $this->folder->exists() && $this->folder->unlink(); + $this->folder->create(); + } + + #[After] + public function cleanup() { + $this->folder->unlink(); + } + + #[Test] + public function can_create() { + new ToFolder($this->folder); + } + + #[Test] + public function dash_special_case() { + with ((new ToFolder($this->folder))->using('.php')->target('-'), function($out) { + $out->write('folder, 'out.php'))->exists()); + } + + #[Test] + public function write_to_target_then_load_via_class_loader() { + $class= 'folder); + with ($fixture->target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, (new FileSystemClassLoader($this->folder->getURI()))->loadClassBytes('Test')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php b/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php new file mode 100755 index 00000000..8b1e52cc --- /dev/null +++ b/src/test/php/lang/ast/unittest/cli/ToStreamTest.class.php @@ -0,0 +1,29 @@ +target('Test.php'), function($out) use($class) { + $out->write($class); + $out->flush(); + $out->close(); + }); + $fixture->close(); + + Assert::equals($class, $out->bytes()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php new file mode 100755 index 00000000..d2599d02 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AnnotationSupport.class.php @@ -0,0 +1,183 @@ + []], + $this->annotations($this->declare('#[Test]')) + ); + } + + #[Test] + public function within_namespace() { + Assert::equals( + ['tests\\Test' => []], + $this->annotations($this->declare('namespace tests; #[Test]')) + ); + } + + #[Test] + public function resolved_against_import() { + Assert::equals( + ['unittest\\Test' => []], + $this->annotations($this->declare('use unittest\Test; #[Test]')) + ); + } + + #[Test] + public function primitive_value() { + Assert::equals( + ['Author' => ['Timm']], + $this->annotations($this->declare('#[Author("Timm")]')) + ); + } + + #[Test] + public function array_value() { + Assert::equals( + ['Authors' => [['Timm', 'Alex']]], + $this->annotations($this->declare('#[Authors(["Timm", "Alex"])]')) + ); + } + + #[Test] + public function map_value() { + Assert::equals( + ['Expect' => [['class' => IllegalArgumentException::class]]], + $this->annotations($this->declare('#[Expect(["class" => \lang\IllegalArgumentException::class])]')) + ); + } + + #[Test] + public function named_argument() { + Assert::equals( + ['Expect' => ['class' => IllegalArgumentException::class]], + $this->annotations($this->declare('#[Expect(class: \lang\IllegalArgumentException::class)]')) + ); + } + + #[Test] + public function closure_value() { + $verify= $this->annotations($this->declare('#[Verify(function($arg) { return $arg; })]'))['Verify']; + Assert::equals('test', $verify[0]('test')); + } + + #[Test] + public function first_class_callable_value() { + $verify= $this->annotations($this->declare('#[Verify(strtoupper(...))]'))['Verify']; + Assert::equals('TEST', $verify[0]('test')); + } + + #[Test] + public function arrow_function_value() { + $verify= $this->annotations($this->declare('#[Verify(fn($arg) => $arg)]'))['Verify']; + Assert::equals('test', $verify[0]('test')); + } + + #[Test] + public function array_of_arrow_function_value() { + $verify= $this->annotations($this->declare('#[Verify([fn($arg) => $arg])]'))['Verify']; + Assert::equals('test', $verify[0][0]('test')); + } + + #[Test] + public function named_arrow_function_value() { + $verify= $this->annotations($this->declare('#[Verify(func: fn($arg) => $arg)]'))['Verify']; + Assert::equals('test', $verify['func']('test')); + } + + #[Test] + public function single_quoted_string_inside_non_constant_expression() { + $verify= $this->annotations($this->declare('#[Verify(fn($arg) => \'php\\\\\'.$arg)]'))['Verify']; + Assert::equals('php\\test', $verify[0]('test')); + } + + #[Test] + public function has_access_to_class() { + Assert::equals( + ['Expect' => [true]], + $this->annotations($this->declare('#[Expect(self::SUCCESS)] class %T { const SUCCESS = true; }')) + ); + } + + #[Test] + public function method() { + $t= $this->declare('class %T { #[Test] public function fixture() { } }'); + Assert::equals( + ['Test' => []], + $this->annotations($t->method('fixture')) + ); + } + + #[Test] + public function field() { + $t= $this->declare('class %T { #[Test] public $fixture; }'); + Assert::equals( + ['Test' => []], + $this->annotations($t->property('fixture')) + ); + } + + #[Test] + public function param() { + $t= $this->declare('class %T { public function fixture(#[Test] $param) { } }'); + Assert::equals( + ['Test' => []], + $this->annotations($t->method('fixture')->parameter(0)) + ); + } + + #[Test] + public function params() { + $t= $this->declare('class %T { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }'); + Assert::equals( + ['Inject' => [['name' => 'a']]], + $this->annotations($t->method('fixture')->parameter(0)) + ); + Assert::equals( + ['Inject' => []], + $this->annotations($t->method('fixture')->parameter(1)) + ); + } + + #[Test] + public function multiple_class_annotations() { + Assert::equals( + ['Resource' => ['/'], 'Authenticated' => []], + $this->annotations($this->declare('#[Resource("/"), Authenticated]')) + ); + } + + #[Test] + public function multiple_member_annotations() { + $t= $this->declare('class %T { #[Test, Values([1, 2, 3])] public function fixture() { } }'); + Assert::equals( + ['Test' => [], 'Values' => [[1, 2, 3]]], + $this->annotations($t->method('fixture')) + ); + } + + #[Test] + public function multiline_annotations() { + $annotations= $this->annotations($this->declare(' + #[Authors([ + "Timm", + "Mr. Midori", + ])] + class %T { }' + )); + Assert::equals(['Authors' => [['Timm', 'Mr. Midori']]], $annotations); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php new file mode 100755 index 00000000..cda1b705 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsOf.class.php @@ -0,0 +1,18 @@ +annotations() as $name => $annotation) { + $r[$name]= $annotation->arguments(); + } + return $r; + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php index 936f781c..dfea94ce 100755 --- a/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnnotationsTest.class.php @@ -1,80 +1,16 @@ type('<> class { }'); - $this->assertEquals(['test' => null], $t->getAnnotations()); - } + /** @return string[] */ + protected function emitters() { return [XpMeta::class]; } - #[@test] - public function primitive_value() { - $t= $this->type('<> class { }'); - $this->assertEquals(['author' => 'Timm'], $t->getAnnotations()); - } - - #[@test] - public function array_value() { - $t= $this->type('<> class { }'); - $this->assertEquals(['authors' => ['Timm', 'Alex']], $t->getAnnotations()); - } - - #[@test] - public function map_value() { - $t= $this->type('< \lang\IllegalArgumentException::class])>> class { }'); - $this->assertEquals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations()); - } - - #[@test] - public function closure_value() { - $t= $this->type('<> class { }'); - $f= $t->getAnnotation('verify'); - $this->assertEquals('test', $f('test')); - } - - #[@test] - public function has_access_to_class() { - $t= $this->type('<> class { const SUCCESS = true; }'); - $this->assertEquals(['expect' => true], $t->getAnnotations()); - } - - #[@test] - public function method() { - $t= $this->type('class { <> public function fixture() { } }'); - $this->assertEquals(['test' => null], $t->getMethod('fixture')->getAnnotations()); - } - - #[@test] - public function field() { - $t= $this->type('class { <> public $fixture; }'); - $this->assertEquals(['test' => null], $t->getField('fixture')->getAnnotations()); - } - - #[@test] - public function param() { - $t= $this->type('class { public function fixture(<> $param) { } }'); - $this->assertEquals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations()); - } - - #[@test] - public function params() { - $t= $this->type('class { public function fixture(< "a"])>> $a, <> $b) { } }'); - $m=$t->getMethod('fixture'); - $this->assertEquals( - [['inject' => ['name' => 'a']], ['inject' => null]], - [$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()] - ); - } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php index 4bd6a3bd..88513341 100755 --- a/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/AnonymousClassTest.class.php @@ -1,7 +1,8 @@ run('class { + $r= $this->run('class %T { public function run() { return new class() { public function id() { return "test"; } }; } }'); - $this->assertEquals('test', $r->id()); + Assert::equals('test', $r->id()); } - #[@test] + #[Test] public function extending_base_class() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { - return new class() extends \\util\\AbstractDeferredInvokationHandler { - public function initialize() { - // TBI + return new class() extends \\util\\Binford { + public function more(int $factor): self { + $this->poweredBy*= $factor; + return $this; } }; } }'); - $this->assertInstanceOf(AbstractDeferredInvokationHandler::class, $r); + Assert::instance(Binford::class, $r); } - #[@test] + #[Test] public function implementing_interface() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new class() implements \\lang\\Runnable { public function run() { @@ -48,6 +50,45 @@ public function run() { }; } }'); - $this->assertInstanceOf(Runnable::class, $r); + Assert::instance(Runnable::class, $r); + } + + #[Test] + public function method_annotations() { + $r= $this->run('class %T { + public function run() { + return new class() { + + #[Inside] + public function fixture() { } + }; + } + }'); + + Assert::equals([], Reflection::type($r)->method('fixture')->annotation('Inside')->arguments()); + } + + #[Test] + public function extending_enclosing_class() { + $t= $this->declare('class %T { + public static function run() { + return new class() extends self { }; + } + }'); + Assert::instance($t->class(), $t->method('run')->invoke(null)); + } + + #[Test] + public function referencing_enclosing_class() { + $r= $this->run('class %T { + const ID= 6100; + + public static function run() { + return new class() extends self { + public static $id= %T::ID; + }; + } + }'); + Assert::equals(6100, Reflection::type($r)->property('id')->get(null)); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 140dd415..6ea0214f 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -1,20 +1,23 @@ run('class { + $r= $this->run('class %T { public function __construct(private $id= "test") { // Empty } @@ -23,12 +26,12 @@ public function run() { return $this->id; } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } - #[@test] + #[Test] public function can_be_used_in_constructor() { - $r= $this->run('class { + $r= $this->run('class %T { public function __construct(private $id= "test") { $this->id.= "ed"; } @@ -37,12 +40,28 @@ public function run() { return $this->id; } }'); - $this->assertEquals('tested', $r); + Assert::equals('tested', $r); } - #[@test] + #[Test] + public function parameter_accessible() { + $r= $this->run('class %T { + public function __construct(private $id= "test") { + if (null === $id) { + throw new \\lang\\IllegalArgumentException("ID not set"); + } + } + + public function run() { + return $this->id; + } + }'); + Assert::equals('test', $r); + } + + #[Test] public function in_method() { - $r= $this->run('class { + $r= $this->run('class %T { public function withId(private $id) { return $this; } @@ -51,18 +70,86 @@ public function run() { return $this->withId("test")->id; } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } - #[@test] + #[Test] public function type_information() { - $t= $this->type('class { - public function __construct(private int $id, private string $name) { - } + $t= $this->declare('class %T { + public function __construct(private int $id, private string $name) { } }'); - $this->assertEquals( + Assert::equals( [Primitive::$INT, Primitive::$STRING], - [$t->getField('id')->getType(), $t->getField('name')->getType()] + [$t->property('id')->constraint()->type(), $t->property('name')->constraint()->type()] ); } + + #[Test, Expect(class: Errors::class, message: '/Variadic parameters cannot be promoted/')] + public function variadic_parameters_cannot_be_promoted() { + $this->declare('class %T { + public function __construct(private string... $in) { } + }'); + } + + #[Test] + public function can_be_mixed_with_normal_arguments() { + $t= $this->declare('class %T { + public function __construct(public string $name, ?string $initial= null) { + if (null !== $initial) $this->name.= " ".$initial."."; + } + }'); + + $names= []; + foreach ($t->properties() as $property) { + $names[]= $property->name(); + } + + Assert::equals(['name'], $names); + Assert::equals('Timm J.', $t->newInstance('Timm', 'J')->name); + } + + #[Test] + public function promoted_by_reference_argument() { + $t= $this->declare('class %T { + public function __construct(public array &$list) { } + + public static function test() { + $list= [1, 2, 3]; + $self= new self($list); + $list[]= 4; + return $self->list; + } + }'); + + Assert::equals([1, 2, 3, 4], $t->method('test')->invoke(null, [])); + } + + #[Test] + public function allows_trailing_comma() { + $this->declare('class %T { + public function __construct( + public float $x = 0.0, + public float $y = 0.0, + public float $z = 0.0, // <-- Allow this comma. + ) { } + }'); + } + + #[Test] + public function initializations_have_access() { + $t= $this->declare('class %T { + public $first= $this->list[0] ?? null; + public function __construct(private array $list) { } + }'); + Assert::equals('Test', $t->newInstance(['Test'])->first); + } + + #[Test] + public function promoted_final() { + $t= $this->declare('class %T { + public function __construct(public final string $name) { } + }'); + + Assert::equals(MODIFIER_PUBLIC | MODIFIER_FINAL, $t->property('name')->modifiers()->bits()); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php index 492de10b..612cf26c 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentUnpackingTest.class.php @@ -1,6 +1,6 @@ run('class { + $r= $this->run('class %T { public function fixture(... $args) { return $args; } public function run() { @@ -20,44 +20,44 @@ public function run() { return $this->fixture(...$args); } }'); - $this->assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); } - #[@test] + #[Test] public function in_array_initialization_with_variable() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $args= [3, 4]; return [1, 2, ...$args]; } }'); - $this->assertEquals([1, 2, 3, 4], $r); + Assert::equals([1, 2, 3, 4], $r); } - #[@test] + #[Test] public function in_array_initialization_with_literal() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return [1, 2, ...[3, 4]]; } }'); - $this->assertEquals([1, 2, 3, 4], $r); + Assert::equals([1, 2, 3, 4], $r); } - #[@test] + #[Test] public function in_map_initialization() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $args= ["type" => "car"]; return ["color" => "red", ...$args, "year" => 2002]; } }'); - $this->assertEquals(['color' => 'red', 'type' => 'car', 'year' => 2002], $r); + Assert::equals(['color' => 'red', 'type' => 'car', 'year' => 2002], $r); } - #[@test] + #[Test] public function from_generator() { - $r= $this->run('class { + $r= $this->run('class %T { private function items() { yield 1; yield 2; @@ -67,12 +67,12 @@ public function run() { return [...$this->items()]; } }'); - $this->assertEquals([1, 2], $r); + Assert::equals([1, 2], $r); } - #[@test] + #[Test] public function from_iterator() { - $r= $this->run('class { + $r= $this->run('class %T { private function items() { return new \ArrayIterator([1, 2]); } @@ -81,6 +81,6 @@ public function run() { return [...$this->items()]; } }'); - $this->assertEquals([1, 2], $r); + Assert::equals([1, 2], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php index 30f286db..4cfd15db 100755 --- a/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArrayTypesTest.class.php @@ -1,5 +1,7 @@ type('class { + $t= $this->declare('class %T { private array $test; }'); - $this->assertEquals('int[]', $t->getField('test')->getType()->getName()); + Assert::equals('int[]', $t->property('test')->constraint()->type()->getName()); } - #[@test] + #[Test] public function int_map_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array $test; }'); - $this->assertEquals('[:int]', $t->getField('test')->getType()->getName()); + Assert::equals('[:int]', $t->property('test')->constraint()->type()->getName()); } - #[@test] + #[Test] public function nested_map_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array> $test; }'); - $this->assertEquals('[:int[]]', $t->getField('test')->getType()->getName()); + Assert::equals('[:int[]]', $t->property('test')->constraint()->type()->getName()); } - #[@test] + #[Test] public function var_map_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private array $test; }'); - $this->assertEquals('[:var]', $t->getField('test')->getType()->getName()); + Assert::equals('[:var]', $t->property('test')->constraint()->type()->getName()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php index 1ce202f4..dd95c2aa 100755 --- a/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArraysTest.class.php @@ -1,32 +1,35 @@ run('class { + $r= $this->run('class %T { public function run() { return [1, 2, 3]; } }'); - $this->assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); } - #[@test] + #[Test] public function map_literal() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return ["a" => 1, "b" => 2]; } }'); - $this->assertEquals(['a' => 1, 'b' => 2], $r); + Assert::equals(['a' => 1, 'b' => 2], $r); } - #[@test] + #[Test] public function append() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $r= [1, 2]; $r[]= 3; @@ -34,42 +37,192 @@ public function run() { } }'); - $this->assertEquals([1, 2, 3], $r); + Assert::equals([1, 2, 3], $r); + } + + #[Test, Values(['[1, , 3]', '[1, , ]', '[, 1]']), Expect(class: IllegalStateException::class, message: 'Cannot use empty array elements in arrays')] + public function arrays_cannot_have_empty_elements($input) { + $r= $this->run('class %T { + public function run() { + return '.$input.'; + } + }'); } - #[@test] + #[Test] public function destructuring() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [$a, $b]= [1, 2]; return [$a, $b]; } }'); - $this->assertEquals([1, 2], $r); + Assert::equals([1, 2], $r); + } + + #[Test] + public function nested_destructuring() { + $r= $this->run('class %T { + public function run() { + [[$a, $b], $c]= [[1, 2], 3]; + return [$a, $b, $c]; + } + }'); + + Assert::equals([1, 2, 3], $r); + } + + #[Test] + public function destructuring_with_empty_between() { + $r= $this->run('class %T { + public function run() { + [$a, , $b]= [1, 2, 3]; + return [$a, $b]; + } + }'); + + Assert::equals([1, 3], $r); + } + + #[Test] + public function destructuring_with_empty_start() { + $r= $this->run('class %T { + public function run() { + [, $a, $b]= [1, 2, 3]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 3], $r); + } + + #[Test] + public function destructuring_map() { + $r= $this->run('class %T { + public function run() { + ["two" => $a, "one" => $b]= ["one" => 1, "two" => 2]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test] + public function destructuring_map_with_expression() { + $r= $this->run('class %T { + private $one= "one"; + public function run() { + $two= "two"; + [$two => $a, $this->one => $b]= ["one" => 1, "two" => 2]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test] + public function nested_destructuring_with_map() { + $r= $this->run('class %T { + public function run() { + ["nested" => ["one" => $a], "three" => $b]= ["nested" => ["one" => 1], "three" => 3]; + return [$a, $b]; + } + }'); + + Assert::equals([1, 3], $r); + } + + #[Test, Values(['$list', '$this->instance', 'self::$static'])] + public function reference_destructuring($reference) { + $r= $this->run('class %T { + private $instance= [1, 2]; + private static $static= [1, 2]; + + public function run() { + $list= [1, 2]; + [&$a, &$b]= '.$reference.'; + $a++; + $b--; + return '.$reference.'; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test] + public function list_destructuring() { + $r= $this->run('class %T { + public function run() { + list($a, $b)= [1, 2]; + return [$a, $b]; + } + }'); + + Assert::equals([1, 2], $r); + } + + #[Test] + public function swap_using_destructuring() { + $r= $this->run('class %T { + public function run() { + $a= 1; + $b= 2; + [$b, $a]= [$a, $b]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test] + public function result_of_destructuring() { + $r= $this->run('class %T { + public function run() { + return [$a, $b]= [1, 2]; + } + }'); + + Assert::equals([1, 2], $r); + } + + #[Test, Values([null, true, false, 0, 0.5, '', 'Test'])] + public function destructuring_with_non_array($value) { + $r= $this->run('class %T { + public function run($arg) { + $r= [$a, $b]= $arg; + return [$a, $b, $r]; + } + }', $value); + + Assert::equals([null, null, $value], $r); } - #[@test] + #[Test] public function init_with_variable() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $KEY= "key"; return [$KEY => "value"]; } }'); - $this->assertEquals(['key' => 'value'], $r); + Assert::equals(['key' => 'value'], $r); } - #[@test] + #[Test] public function init_with_member_variable() { - $r= $this->run('class { + $r= $this->run('class %T { private static $KEY= "key"; public function run() { return [self::$KEY => "value"]; } }'); - $this->assertEquals(['key' => 'value'], $r); + Assert::equals(['key' => 'value'], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php new file mode 100755 index 00000000..b72b9ae5 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AsymmetricVisibilityTest.class.php @@ -0,0 +1,156 @@ +declare('class %T { + public private(set) string $fixture= "Test"; + }'); + Assert::equals('Test', $t->newInstance()->fixture); + } + + #[Test] + public function writing_from_self_scope() { + $t= $this->declare('class %T { + public private(set) string $fixture= "Test"; + + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); + Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); + } + + #[Test] + public function writing_from_inherited_scope() { + $parent= $this->declare('class %T { public protected(set) string $fixture= "Test"; }'); + $t= $this->declare('class %T extends '.$parent->literal().' { + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); + Assert::equals('Changed', $t->newInstance()->rename('Changed')->fixture); + } + + #[Test] + public function writing_explicitely_public_set() { + $t= $this->declare('class %T { + public public(set) string $fixture= "Test"; + }'); + + $instance= $t->newInstance(); + $instance->fixture= 'Changed'; + Assert::equals('Changed', $instance->fixture); + } + + #[Test, Expect(class: Error::class, message: '/Cannot modify private\(set\) property T.+::\$fixture/')] + public function writing_private() { + $t= $this->declare('class %T { + public private(set) string $fixture= "Test"; + }'); + $t->newInstance()->fixture= 'Changed'; + } + + #[Test, Expect(class: Error::class, message: '/Cannot modify protected\(set\) property T.+::\$fixture/')] + public function writing_protected() { + $t= $this->declare('class %T { + public protected(set) string $fixture= "Test"; + }'); + $t->newInstance()->fixture= 'Changed'; + } + + #[Test] + public function promoted_constructor_parameter() { + $t= $this->declare('class %T { + public function __construct(public private(set) string $fixture) { } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test, Expect(class: Error::class, message: '/Cannot modify readonly property .+fixture/')] + public function readonly() { + $t= $this->declare('class %T { + + // public-read, protected-write, write-once property + public protected(set) readonly string $fixture; + + public function __construct() { + $this->fixture= "Test"; + } + + public function rename() { + $this->fixture= "Changed"; // Will always error + } + }'); + $t->newInstance()->rename(); + } + + #[Test] + public function protected_set_reflection() { + $t= $this->declare('class %T { + public protected(set) string $fixture= "Test"; + }'); + + Assert::equals( + 'public protected(set) string $fixture', + $t->property('fixture')->toString() + ); + } + + #[Test, Values(['protected', 'public'])] + public function private_set_implicitely_final_in_reflection($modifier) { + $t= $this->declare('class %T { + '.$modifier.' private(set) string $fixture= "Test"; + }'); + + Assert::equals( + $modifier.' final private(set) string $fixture', + $t->property('fixture')->toString() + ); + } + + #[Test, Values(['private', 'protected', 'public'])] + public function same_modifier_for_get_and_set($modifier) { + $t= $this->declare('class %T { + '.$modifier.' '.$modifier.'(set) string $fixture= "Test"; + }'); + + Assert::equals( + $modifier.' string $fixture', + $t->property('fixture')->toString() + ); + } + + #[Test] + public function interaction_with_hooks() { + $t= $this->declare('class %T { + public private(set) string $fixture { + get => $this->fixture; + set => strtolower($value); + } + + public function rename($name) { + $this->fixture= $name; + return $this; + } + }'); + + Assert::throws(Error::class, fn() => $t->newInstance()->fixture= 'Changed'); + Assert::equals('changed', $t->newInstance()->rename('Changed')->fixture); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/AttributesTest.class.php b/src/test/php/lang/ast/unittest/emit/AttributesTest.class.php new file mode 100755 index 00000000..c395d437 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/AttributesTest.class.php @@ -0,0 +1,15 @@ +run('class %T { + public function run() { + { + } + return true; + } + }'); + + Assert::true($r); + } + + #[Test] + public function block_with_assignment() { + $r= $this->run('class %T { + public function run() { + { + $result= true; + } + return $result; + } + }'); + + Assert::true($r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php index b885551a..c9e4c128 100755 --- a/src/test/php/lang/ast/unittest/emit/BracesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/BracesTest.class.php @@ -1,10 +1,12 @@ run('class { + $r= $this->run('class %T { private $id= 0; public function run() { @@ -12,34 +14,34 @@ public function run() { } }'); - $this->assertEquals('test1', $r); + Assert::equals('test1', $r); } - #[@test] + #[Test] public function braces_around_new() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return (new \\util\\Date(250905600))->getTime(); } }'); - $this->assertEquals(250905600, $r); + Assert::equals(250905600, $r); } - #[@test] + #[Test] public function no_braces_necessary_around_new() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new \\util\\Date(250905600)->getTime(); } }'); - $this->assertEquals(250905600, $r); + Assert::equals(250905600, $r); } - #[@test] + #[Test] public function property_vs_method_ambiguity() { - $r= $this->run('class { + $r= $this->run('class %T { private $f; public function __construct() { @@ -51,12 +53,12 @@ public function run() { } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } - #[@test] + #[Test] public function nested_braces() { - $r= $this->run('class { + $r= $this->run('class %T { private function test() { return "test"; } public function run() { @@ -64,12 +66,12 @@ public function run() { } }'); - $this->assertEquals('test', $r); + Assert::equals('test', $r); } - #[@test] + #[Test] public function braced_expression_not_confused_with_cast() { - $r= $this->run('class { + $r= $this->run('class %T { const WIDTH = 640; public function run() { @@ -77,27 +79,35 @@ public function run() { } }'); - $this->assertEquals(320, $r); + Assert::equals(320, $r); } - #[@test, @values(map= [ - # '(__LINE__)."test"' => '3test', - # '(__LINE__) + 1' => 4, - # '(__LINE__) - 1' => 2, - #])] + #[Test, Values([['(__LINE__)."test"', '3test'], ['(__LINE__) + 1', 4], ['(__LINE__) - 1', 2]])] public function global_constant_in_braces_not_confused_with_cast($input, $expected) { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return '.$input.'; } }'); - $this->assertEquals($expected, $r); + Assert::equals($expected, $r); + } + + #[Test] + public function function_call_in_braces() { + $r= $this->run('class %T { + public function run() { + $e= STDOUT; + return false !== (fstat(STDOUT)); + } + }'); + + Assert::true($r); } - #[@test] + #[Test] public function invoke_on_braced_null_coalesce() { - $r= $this->run('class { + $r= $this->run('class %T { public function __invoke() { return "OK"; } public function fail() { return function() { return "FAIL"; }; } @@ -106,6 +116,6 @@ public function run() { } }'); - $this->assertEquals('OK', $r); + Assert::equals('OK', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php new file mode 100755 index 00000000..f12f5038 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CallableSyntaxTest.class.php @@ -0,0 +1,220 @@ +run($code)('Test')); + } + + #[Test] + public function native_function() { + $this->verify('class %T { + public function run() { return strlen(...); } + }'); + } + + #[Test] + public function instance_method() { + $this->verify('class %T { + public function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + } + + #[Test] + public function class_method() { + $this->verify('class %T { + public static function length($arg) { return strlen($arg); } + public function run() { return self::length(...); } + }'); + } + + #[Test] + public function private_method() { + $this->verify('class %T { + private function length($arg) { return strlen($arg); } + public function run() { return $this->length(...); } + }'); + } + + #[Test] + public function string_reference() { + $this->verify('class %T { + public function run() { + $func= "strlen"; + return $func(...); + } + }'); + } + + #[Test] + public function fn_reference() { + $this->verify('class %T { + public function run() { + $func= fn($arg) => strlen($arg); + return $func(...); + } + }'); + } + + #[Test] + public function instance_property_reference() { + $this->verify('class %T { + private $func= "strlen"; + public function run() { + return ($this->func)(...); + } + }'); + } + + #[Test, Values(['$this->$func(...)', '$this->{$func}(...)'])] + public function variable_instance_method($expr) { + $this->verify('class %T { + private function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return '.$expr.'; + } + }'); + } + + #[Test, Values(['self::$func(...)', 'self::{$func}(...)'])] + public function variable_class_method($expr) { + $this->verify('class %T { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + return '.$expr.'; + } + }'); + } + + #[Test] + public function variable_class_method_with_variable_class() { + $this->verify('class %T { + private static function length($arg) { return strlen($arg); } + public function run() { + $func= "length"; + $class= __CLASS__; + return $class::$func(...); + } + }'); + } + + #[Test] + public function string_function_reference() { + $this->verify('class %T { + public function run() { return "strlen"(...); } + }'); + } + + #[Test] + public function array_instance_method_reference() { + $this->verify('class %T { + public function length($arg) { return strlen($arg); } + public function run() { return [$this, "length"](...); } + }'); + } + + #[Test] + public function array_class_method_reference() { + $this->verify('class %T { + public static function length($arg) { return strlen($arg); } + public function run() { return [self::class, "length"](...); } + }'); + } + + #[Test, Expect(Error::class), Values(['nonexistant', '$this->nonexistant', 'self::nonexistant', '$nonexistant', '$null'])] + public function non_existant($expr) { + $this->run('class %T { + public function run() { + $null= null; + $nonexistant= "nonexistant"; + return '.$expr.'(...); + } + }'); + } + + #[Test] + public function instantiation() { + $f= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + return new Handle(...); + } + }'); + Assert::equals(new Handle(1), $f(1)); + } + + #[Test] + public function instantiation_in_map() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + return array_map(new Handle(...), [0, 1, 2]); + } + }'); + Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); + } + + #[Test] + public function variable_instantiation() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + $class= Handle::class; + return array_map(new $class(...), [0, 1, 2]); + } + }'); + Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); + } + + #[Test] + public function expression_instantiation() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + $version= ""; + return array_map(new (Handle::class.$version)(...), [0, 1, 2]); + } + }'); + Assert::equals([new Handle(0), new Handle(1), new Handle(2)], $r); + } + + #[Test] + public function anonymous_instantiation() { + $f= $this->run('class %T { + public function run() { + return new class(...) { + public $value; + public function __construct($value) { $this->value= $value; } + }; + } + }'); + Assert::equals($this, $f($this)->value); + } + + #[Test] + public function inside_annotation() { + $f= $this->run('use lang\Reflection; class %T { + + #[Attr(strrev(...))] + public function run() { + return Reflection::of($this)->method("run")->annotation(Attr::class)->argument(0); + } + }'); + Assert::equals('cba', $f('abc')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php index 6fe8dc6f..45d0b585 100755 --- a/src/test/php/lang/ast/unittest/emit/CastingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CastingTest.class.php @@ -1,17 +1,17 @@ assertEquals((string)$value, $this->run( - 'class { + Assert::equals((string)$value, $this->run( + 'class %T { public function run($value) { return (string)$value; } @@ -20,16 +20,10 @@ public function run($value) { )); } - #[@test, @values([ - # "0", "1", "-1", "6100", - # "", - # 0.5, -1.5, - # 0, 1, -1, - # true, false - #])] + #[Test, Values(['0', '1', '-1', '6100', '', 0.5, -1.5, 0, 1, -1, true, false])] public function int_cast($value) { - $this->assertEquals((int)$value, $this->run( - 'class { + Assert::equals((int)$value, $this->run( + 'class %T { public function run($value) { return (int)$value; } @@ -38,15 +32,10 @@ public function run($value) { )); } - #[@test, @values([ - # [[]], - # [[0, 1, 2]], - # [['key' => 'value']], - # null, false, true, 1, 1.5, "", "test" - #])] + #[Test, Values([[[]], [[0, 1, 2]], [['key' => 'value']], null, false, true, 1, 1.5, '', 'test'])] public function array_cast($value) { - $this->assertEquals((array)$value, $this->run( - 'class { + Assert::equals((array)$value, $this->run( + 'class %T { public function run($value) { return (array)$value; } @@ -55,10 +44,10 @@ public function run($value) { )); } - #[@test] + #[Test] public function value_cast() { - $this->assertEquals($this, $this->run( - 'class { + Assert::equals($this, $this->run( + 'class %T { public function run($value) { return (\lang\ast\unittest\emit\CastingTest)$value; } @@ -67,10 +56,10 @@ public function run($value) { )); } - #[@test] + #[Test] public function int_array_cast() { - $this->assertEquals([1, 2, 3], $this->run( - 'class { + Assert::equals([1, 2, 3], $this->run( + 'class %T { public function run($value) { return (array)$value; } @@ -79,19 +68,19 @@ public function run($value) { )); } - #[@test, @expect(ClassCastException::class)] + #[Test, Expect(ClassCastException::class)] public function cannot_cast_object_to_int_array() { - $this->run('class { + $this->run('class %T { public function run() { return (array)$this; } }'); } - #[@test, @values([null, 'test'])] + #[Test, Values([null, 'test'])] public function nullable_string_cast_of($value) { - $this->assertEquals($value, $this->run( - 'class { + Assert::equals($value, $this->run( + 'class %T { public function run($value) { return (?string)$value; } @@ -100,10 +89,23 @@ public function run($value) { )); } - #[@test] + #[Test, Values([null, 'test'])] + public function nullable_string_cast_of_expression_returning($value) { + Assert::equals($value, $this->run( + 'class %T { + public function run($value) { + $values= [$value]; + return (?string)array_pop($values); + } + }', + $value + )); + } + + #[Test] public function cast_braced() { - $this->assertEquals(['test'], $this->run( - 'class { + Assert::equals(['test'], $this->run( + 'class %T { public function run($value) { return (array)($value); } @@ -112,22 +114,22 @@ public function run($value) { )); } - #[@test] + #[Test] public function cast_to_function_type_and_invoke() { - $this->assertEquals($this->getName(), $this->run( - 'class { + Assert::equals($this->test(), $this->run( + 'class %T { public function run($value) { return ((function(): string)($value))(); } }', - [$this, 'getName'] + [$this, 'test'] )); } - #[@test] + #[Test] public function object_cast_on_literal() { - $this->assertEquals((object)['key' => 'value'], $this->run( - 'class { + Assert::equals((object)['key' => 'value'], $this->run( + 'class %T { public function run($value) { return (object)["key" => "value"]; } @@ -136,14 +138,10 @@ public function run($value) { )); } - #[@test, @values([ - # [1, 1], - # ['123', 123], - # [null, null] - #])] + #[Test, Values([[1, 1], ['123', 123], [null, null]])] public function nullable_int($value, $expected) { - $this->assertEquals($expected, $this->run( - 'class { + Assert::equals($expected, $this->run( + 'class %T { public function run($value) { return (?int)$value; } @@ -152,10 +150,10 @@ public function run($value) { )); } - #[@test, @values([new Handle(10), null])] + #[Test, Values(eval: '[new Handle(10), null]')] public function nullable_value($value) { - $this->assertEquals($value, $this->run( - 'class { + Assert::equals($value, $this->run( + 'class %T { public function run($value) { return (?\lang\ast\unittest\emit\Handle)$value; } diff --git a/src/test/php/lang/ast/unittest/emit/ChainScopeOperatorsTest.class.php b/src/test/php/lang/ast/unittest/emit/ChainScopeOperatorsTest.class.php new file mode 100755 index 00000000..7e874e67 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ChainScopeOperatorsTest.class.php @@ -0,0 +1,31 @@ +emit( + new ScopeExpression(new Variable('instance'), new Literal('class'))) + ); + } + + #[Test] + public function does_not_rewrite_type_literal() { + Assert::equals('self::class', $this->emit( + new ScopeExpression('self', new Literal('class')), + [new ClassDeclaration([], new IsValue('\\T'), null, [], [], null, null, 1)] + )); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php b/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php new file mode 100755 index 00000000..132bc9fa --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ClassLiteralTest.class.php @@ -0,0 +1,52 @@ +run('use lang\Primitive; class %T { + public function run() { return Primitive::class; } + }'); + Assert::equals(Primitive::class, $r); + } + + #[Test] + public function on_self() { + $t= $this->declare('class %T { + public function run() { return self::class; } + }'); + Assert::equals($t->literal(), $t->newInstance()->run()); + } + + #[Test] + public function on_object() { + $t= $this->declare('class %T { + public function run() { return $this::class; } + }'); + Assert::equals($t->literal(), $t->newInstance()->run()); + } + + #[Test] + public function on_instantiation() { + $t= $this->declare('class %T { + public function run() { return new self()::class; } + }'); + Assert::equals($t->literal(), $t->newInstance()->run()); + } + + #[Test] + public function on_braced_expression() { + $t= $this->declare('class %T { + public function run() { return (new self())::class; } + }'); + Assert::equals($t->literal(), $t->newInstance()->run()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CloningTest.class.php b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php new file mode 100755 index 00000000..0dddf242 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/CloningTest.class.php @@ -0,0 +1,57 @@ +fixture= new class() { + public $id= 1; + + public function with($id) { + $this->id= $id; + return $this; + } + + public function __clone() { + $this->id++; + } + }; + } + + #[Test] + public function clone_operator() { + $clone= $this->run('class %T { + public function run($in) { + return clone $in; + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + + #[Test] + public function clone_function() { + $clone= $this->run('class %T { + public function run($in) { + return clone($in); + } + }', $this->fixture); + + Assert::true($clone instanceof $this->fixture && $this->fixture !== $clone); + } + + #[Test] + public function clone_interceptor_called() { + $clone= $this->run('class %T { + public function run($in) { + return clone $in; + } + }', $this->fixture->with(1)); + + Assert::equals([1, 2], [$this->fixture->id, $clone->id]); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php index 13251764..c341f60a 100755 --- a/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/CommentsTest.class.php @@ -1,28 +1,36 @@ type('/** Test */ class { }'); - $this->assertEquals('Test', $t->getComment()); + $t= $this->declare('/** Test */ class %T { }'); + Assert::equals('Test', $t->comment()); } - #[@test] + #[Test] public function on_interface() { - $t= $this->type('/** Test */ interface { }'); - $this->assertEquals('Test', $t->getComment()); + $t= $this->declare('/** Test */ interface %T { }'); + Assert::equals('Test', $t->comment()); } - #[@test] + #[Test] public function on_trait() { - $t= $this->type('/** Test */ trait { }'); - $this->assertEquals('Test', $t->getComment()); + $t= $this->declare('/** Test */ trait %T { }'); + Assert::equals('Test', $t->comment()); + } + + #[Test] + public function on_enum() { + $t= $this->declare('/** Test */ enum %T { }'); + Assert::equals('Test', $t->comment()); } - #[@test] + #[Test] public function on_method() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Test */ public function fixture() { @@ -30,18 +38,18 @@ public function fixture() { } }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } - #[@test] + #[Test] public function comments_are_escaped() { - $t= $this->type("/** Timm's test */ class { }"); - $this->assertEquals("Timm's test", $t->getComment()); + $t= $this->declare("/** Timm's test */ class %T { }"); + Assert::equals("Timm's test", $t->comment()); } - #[@test] + #[Test] public function only_last_comment_is_considered() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Not the right comment */ @@ -51,12 +59,12 @@ public function fixture() { } }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } - #[@test] + #[Test] public function next_comment_is_not_considered() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Test */ public function fixture() { @@ -66,12 +74,12 @@ public function fixture() { /** Not the right comment */ }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } - #[@test] + #[Test] public function inline_apidoc_comment_is_not_considered() { - $t= $this->type('class { + $t= $this->declare('class %T { /** Test */ public function fixture() { @@ -79,6 +87,6 @@ public function fixture() { } }'); - $this->assertEquals('Test', $t->getMethod('fixture')->getComment()); + Assert::equals('Test', $t->method('fixture')->comment()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php deleted file mode 100755 index d84a4228..00000000 --- a/src/test/php/lang/ast/unittest/emit/CompactFunctionsTest.class.php +++ /dev/null @@ -1,49 +0,0 @@ -run('class { - public fn run() => "test"; - }'); - $this->assertEquals('test', $r); - } - - #[@test] - public function with_property() { - $r= $this->run('class { - private $id= "test"; - - public fn run() => $this->id; - }'); - $this->assertEquals('test', $r); - } - - #[@test] - public function combined_with_argument_promotion() { - $r= $this->run('class { - public fn withId(private $id) => $this; - public fn id() => $this->id; - - public function run() { - return $this->withId("test")->id(); - } - }'); - $this->assertEquals('test', $r); - } - - #[@test] - public function hacklang_variation_also_supported() { - $r= $this->run('class { - public function run() ==> "test"; - }'); - $this->assertEquals('test', $r); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php new file mode 100755 index 00000000..299374a0 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -0,0 +1,258 @@ +run('class %T { + public function run($arg) { + if (0 === $arg) { + return "no items"; + } else if (1 === $arg) { + return "one item"; + } else { + return $arg." items"; + } + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items'],])] + public function switch_case($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + switch ($arg) { + case 0: return "no items"; + case 1: return "one item"; + default: return $arg." items"; + } + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([[SEEK_SET, 10], [SEEK_CUR, 11]])] + public function switch_case_goto_label_ambiguity($whence, $expected) { + $r= $this->run('class %T { + public function run($arg) { + $position= 1; + switch ($arg) { + case SEEK_SET: $position= 10; break; + case SEEK_CUR: $position+= 10; break; + } + return $position; + } + }', $whence); + + Assert::equals($expected, $r); + } + + #[Test, Values([[SEEK_SET, 10], [SEEK_CUR, 11]])] + public function switch_case_constant_ambiguity($whence, $expected) { + $r= $this->run('class %T { + const SET = SEEK_SET; + const CURRENT = SEEK_CUR; + public function run($arg) { + $position= 1; + switch ($arg) { + case self::SET: $position= 10; break; + case self::CURRENT: $position+= 10; break; + } + return $position; + } + }', $whence); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, 'no items'], [1, 'one item'], [2, '2 items'], [3, '3 items']])] + public function match($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return match ($arg) { + 0 => "no items", + 1 => "one item", + default => $arg." items", + }; + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test] + public function match_with_default_only() { + $r= $this->run('class %T { + public function run() { + return match (true) { + default => "Test", + }; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Values([[200, 'OK'], [302, 'Redirect'], [404, 'Error #404']])] + public function match_with_multiple_cases($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return match ($arg) { + 200, 201, 202, 203, 204 => "OK", + 300, 301, 302, 303, 307 => "Redirect", + default => "Error #$arg", + }; + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([['PING', '+PONG'], ['MSG', '+OK Re: Test'], ['XFER', '-ERR Unknown XFER']])] + public function match_with_multiple_statements($input, $expected) { + $r= $this->run('class %T { + public function run($type) { + $value= "Test"; + return match ($type) { + "PING" => "+PONG", + "MSG" => { + $reply= "Re: ".$value; + return "+OK $reply"; + }, + default => { + return "-ERR Unknown ".$type; + } + }; + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, 'no items'], [1, 'one item'], [5, '5 items'], [10, '10+ items'],])] + public function match_with_binary($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return match (true) { + $arg >= 10 => "10+ items", + $arg === 1 => "one item", + $arg === 0 => "no items", + default => $arg." items", + }; + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test] + public function match_allows_dropping_true() { + $r= $this->run('class %T { + public function run($arg) { + return match { + $arg >= 10 => "10+ items", + $arg === 1 => "one item", + $arg === 0 => "no items", + default => $arg." items", + }; + } + }', 10); + + Assert::equals('10+ items', $r); + } + + #[Test, Expect(class: Throwable::class, message: '/Unhandled match (value of type .+|case .+)/')] + public function empty_match() { + $r= $this->run('class %T { + public function run() { + return match (true) { }; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(class: Throwable::class, message: '/Unhandled match (value of type .+|case .+)/')] + public function unhandled_match() { + $this->run('class %T { + public function run($arg) { + $position= 1; + return match ($arg) { + SEEK_SET => 10, + SEEK_CUR => $position + 10, + }; + } + }', SEEK_END); + } + + #[Test, Expect(class: Throwable::class, message: '/Unknown seek mode .+/')] + public function match_with_throw_expression() { + $this->run('class %T { + public function run($arg) { + $position= 1; + return match ($arg) { + SEEK_SET => 10, + SEEK_CUR => $position + 10, + default => throw new \\lang\\IllegalArgumentException("Unknown seek mode ".$arg) + }; + } + }', SEEK_END); + } + + #[Test] + public function match_without_arg_inside_fn() { + $r= $this->run('class %T { + public function run() { + return fn($arg) => match { + $arg >= 10 => "10+ items", + $arg === 1 => "one item", + $arg === 0 => "no items", + default => $arg." items", + }; + } + }'); + + Assert::equals('10+ items', $r(10)); + } + + #[Test] + public function match_with_arg_inside_fn() { + $r= $this->run('class %T { + public function run() { + return fn($arg) => match ($arg) { + 0 => "no items", + 1 => "one item", + default => $arg." items", + }; + } + }'); + + Assert::equals('one item', $r(1)); + } + + #[Test] + public function match_block_inside_function_using_ref() { + $r= $this->run('class %T { + public function run() { + $test= "Original"; + (function() use(&$test) { + match (true) { + true => { + $test= "Changed"; + return true; + } + }; + })(); + return $test; + } + }'); + + Assert::equals('Changed', $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php new file mode 100755 index 00000000..8cf02941 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/DeclarationTest.class.php @@ -0,0 +1,34 @@ +type= new ClassDeclaration([], '\\T', new IsValue('\\lang\\Enum'), [], [ + '$ONE' => new Property(['public', 'static'], 'ONE', null, null, [], null, 1) + ]); + } + + #[Test] + public function can_create() { + new Declaration($this->type, null); + } + + #[Test] + public function name() { + Assert::equals('T', (new Declaration($this->type, null))->name()); + } + + #[Test] + public function rewrites_unit_enums() { + $declaration= new Declaration($this->type, null); + Assert::true($declaration->rewriteEnumCase('ONE')); + Assert::false($declaration->rewriteEnumCase('EMPTY')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php new file mode 100755 index 00000000..0956fdcf --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/DeclareTest.class.php @@ -0,0 +1,31 @@ +run('class %T { + public static function number(int $n) { return $n; } + public function run() { return self::number("1"); } + }')); + } + + #[Test] + public function strict_types_off() { + Assert::equals(1, $this->run('declare(strict_types = 0); class %T { + public static function number(int $n) { return $n; } + public function run() { return self::number("1"); } + }')); + } + + #[Test, Expect(class: Error::class, message: '/must be of (the )?type int(eger)?, string given/')] + public function strict_types_on() { + $this->run('declare(strict_types = 1); class %T { + public static function number(int $n) { return $n; } + public function run() { return self::number("1"); } + }'); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php index 6bccad27..0febecf9 100755 --- a/src/test/php/lang/ast/unittest/emit/EchoTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EchoTest.class.php @@ -1,5 +1,7 @@ run('class { - private function hello() ==> "Hello"; + $this->run('class %T { + private function hello() { return "Hello"; } public function run() { '.$statement.' } }'); - $this->assertEquals($expected, ob_get_contents()); + Assert::equals($expected, ob_get_contents()); } finally { ob_end_clean(); } } - #[@test] + #[Test] public function echo_literal() { $this->assertEchoes('Hello', 'echo "Hello";'); } - #[@test] + #[Test] public function echo_variable() { $this->assertEchoes('Hello', '$a= "Hello"; echo $a;'); } - #[@test] + #[Test] public function echo_call() { $this->assertEchoes('Hello', 'echo $this->hello();'); } - #[@test] + #[Test] public function echo_with_multiple_arguments() { $this->assertEchoes('Hello World', 'echo "Hello", " ", "World";'); } diff --git a/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php new file mode 100755 index 00000000..b21aa76e --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EmitterTraitTest.class.php @@ -0,0 +1,29 @@ +codegen->enter(new InType($t)); + } + + $this->emitter->emitOne($result, $node); + return $result->out->bytes(); + } + + /** @return lang.ast.Emitter */ + protected abstract function fixture(); + + #[Before] + public function emitter() { + $this->emitter= $this->fixture(); + } +} \ No newline at end of file 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 65dd1434..bfbc0eaf 100755 --- a/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EmittingTest.class.php @@ -1,38 +1,118 @@ null])] +abstract class EmittingTest { private static $id= 0; + private $cl, $language, $emitter, $output; + private $transformations= []; + + /** + * Constructor + * + * @param ?string $output E.g. `ast,code` to dump both AST and emitted code + */ + public function __construct($output= null) { + $this->output= $output ? array_flip(explode(',', $output)) : []; + $this->cl= DynamicClassLoader::instanceFor(self::class); + $this->language= Language::named('PHP'); + $this->emitter= Emitter::forRuntime($this->runtime(), $this->emitters())->newInstance(); + foreach ($this->language->extensions() as $extension) { + $extension->setup($this->language, $this->emitter); + } + } + + /** + * Returns emitters to use. Defaults to XpMeta + * + * @return string[] + */ + protected function emitters() { return [XpMeta::class]; } + + /** + * Returns runtime to use. Uses `PHP_VERSION` constant. + * + * @return string + */ + protected function runtime() { return 'php:'.PHP_VERSION; } - static function __static() { - self::$cl= DynamicClassLoader::instanceFor(self::class); + /** + * Register a transformation. Will take care of removing it on test shutdown. + * + * @param string $kind + * @param function(lang.ast.Node): lang.ast.Node|iterable $function + * @return void + */ + protected function transform($type, $function) { + $this->transformations[]= $this->emitter->transform($type, $function); } /** - * Declare a type + * Parse and emit given code + * + * @param string $code + * @return string + */ + protected function emit($code) { + $name= 'E'.(self::$id++); + $tree= $this->language->parse(new Tokens(str_replace('%T', $name, $code), static::class))->tree(); + + $out= new MemoryOutputStream(); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); + return $out->bytes(); + } + + /** + * Declare a type with a unique type name (which may be referenced by `%T`) + * and return a type referencing it. * * @param string $code * @return lang.XPClass */ protected function type($code) { + return $this->declare($code)->class(); + } + + /** + * Declare a type with a unique type name (which may be referenced by `%T`) + * and return a reflection instance referencing it. + * + * @param string $code + * @return lang.reflection.Type + */ + protected function declare($code) { $name= 'T'.(self::$id++); + if (strstr($code, '%T')) { + $declaration= str_replace('%T', $name, $code); + } else { + $declaration= $code.' class '.$name.' { }'; + } + + $tree= $this->language->parse(new Tokens($declaration, static::class))->tree(); + if (isset($this->output['ast'])) { + Console::writeLine(); + Console::writeLine('=== ', static::class, ' ==='); + Console::writeLine($tree); + } - $parse= new Parse(new Tokens(new StringTokenizer(str_replace('', $name, $code))), $this->getName()); $out= new MemoryOutputStream(); - $emit= Emitter::forRuntime(defined('HHVM_VERSION') ? 'HHVM.'.HHVM_VERSION : 'PHP.'.PHP_VERSION)->newInstance(new StringWriter($out)); - $emit->emit($parse->execute()); - // var_dump($out->getBytes()); - self::$cl->setClassBytes($name, $out->getBytes()); - return self::$cl->loadClass($name); + $this->emitter->emitAll(new GeneratedCode($out, ''), $tree->children()); + if (isset($this->output['code'])) { + Console::writeLine(); + Console::writeLine('=== ', static::class, ' ==='); + Console::writeLine($out->bytes()); + } + + $class= ($package= $tree->scope()->package) ? strtr(substr($package, 1), '\\', '.').'.'.$name : $name; + $this->cl->setClassBytes($class, $out->bytes()); + return Reflection::type($this->cl->loadClass0($class)); } /** @@ -43,6 +123,13 @@ protected function type($code) { * @return var */ protected function run($code, ... $args) { - return $this->type($code)->newInstance()->run(...$args); + return $this->declare($code)->newInstance()->run(...$args); + } + + #[After] + public function tearDown() { + foreach ($this->transformations as $transformation) { + $this->emitter->remove($transformation); + } } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php new file mode 100755 index 00000000..36a9ace1 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -0,0 +1,263 @@ +declare('enum %T { }')->kind()); + } + + #[Test] + public function name_property() { + $t= $this->declare('enum %T { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return self::Hearts->name; + } + }'); + + Assert::equals('Hearts', $t->method('run')->invoke(null)); + } + + #[Test] + public function cases_method_for_unit_enums() { + $t= $this->declare('enum %T { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->method('cases')->invoke(null)) + ); + } + + #[Test] + public function cases_method_for_backed_enums() { + $t= $this->declare('enum %T: string { + case Hearts = "♥"; + case Diamonds = "♦"; + case Clubs = "♣"; + case Spades = "♠"; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->method('cases')->invoke(null)) + ); + } + + #[Test] + public function cases_method_does_not_yield_constants() { + $t= $this->declare('enum %T { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + const COLORS = ["red", "black"]; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->method('cases')->invoke(null)) + ); + } + + #[Test] + public function used_as_parameter_default() { + $t= $this->declare('enum %T { + case ASC; + case DESC; + + public static function run($order= self::ASC) { + return $order->name; + } + }'); + + Assert::equals('ASC', $t->method('run')->invoke(null)); + } + + #[Test] + public function overwritten_parameter_default_value() { + $t= $this->declare('enum %T { + case ASC; + case DESC; + + public static function run($order= self::ASC) { + return $order->name; + } + }'); + + Assert::equals('DESC', $t->method('run')->invoke(null, [Enum::valueOf($t, 'DESC')])); + } + + #[Test] + public function value_property_of_backed_enum() { + $t= $this->declare('enum %T: string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run() { + return self::DESC->value; + } + }'); + + Assert::equals('desc', $t->method('run')->invoke(null)); + } + + #[Test, Values([[0, 'NO'], [1, 'YES']])] + public function backed_enum_from_int($arg, $expected) { + $t= $this->declare('enum %T: int { + case NO = 0; + case YES = 1; + }'); + + Assert::equals($expected, $t->method('from')->invoke(null, [$arg])->name); + } + + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] + public function backed_enum_from_string($arg, $expected) { + $t= $this->declare('enum %T: string { + case ASC = "asc"; + case DESC = "desc"; + }'); + + Assert::equals($expected, $t->method('from')->invoke(null, [$arg])->name); + } + + #[Test, Expect(class: Error::class, message: '/"illegal" is not a valid backing value for enum .+/')] + public function backed_enum_from_nonexistant() { + $t= $this->declare('enum %T: string { + case ASC = "asc"; + case DESC = "desc"; + }'); + try { + $t->method('from')->invoke(null, ['illegal']); + } catch (InvocationFailed $e) { + throw $e->getCause(); + } + } + + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] + public function backed_enum_tryFrom($arg, $expected) { + $t= $this->declare('enum %T: string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run($arg) { + return self::tryFrom($arg)?->name; + } + }'); + + Assert::equals($expected, $t->method('run')->invoke(null, [$arg])); + } + + #[Test] + public function declare_method_on_enum() { + $t= $this->declare('enum %T { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public function color() { + return match ($this) { + self::Hearts, self::Diamonds => "red", + self::Clubs, self::Spaces => "black", + }; + } + + public static function run() { + return self::Hearts->color(); + } + }'); + + Assert::equals('red', $t->method('run')->invoke(null)); + } + + #[Test] + public function enum_implementing_interface() { + $t= $this->declare('use lang\Closeable; enum %T implements Closeable { + case File; + case Stream; + + public function close() { } + }'); + + Assert::true($t->is('lang.Closeable')); + } + + #[Test] + public function enum_annotations() { + $t= $this->declare('#[Test] enum %T { }'); + Assert::equals(['Test' => []], $this->annotations($t)); + } + + #[Test, Runtime(php: '>=8.1')] + public function enum_member_annotations() { + $t= $this->declare('enum %T { #[Test] case ONE; }'); + Assert::equals(['Test' => []], $this->annotations($t->constant('ONE'))); + } + + #[Test] + public function cannot_be_cloned() { + $t= $this->declare('use lang\IllegalStateException; enum %T { + case ONE; + + public static function run() { + try { + return clone self::ONE; + throw new IllegalStateException("No exception raised"); + } catch (\Error $expected) { + return $expected->getMessage(); + } + } + }'); + + Assert::equals( + 'Trying to clone an uncloneable object of class '.$t->literal(), + $t->method('run')->invoke(null) + ); + } + + #[Test] + public function enum_values() { + $t= $this->declare('enum %T { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, Enum::valuesOf($t)) + ); + } + + #[Test] + public function enum_value() { + $t= $this->declare('enum %T { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + }'); + + Assert::equals('Hearts', Enum::valueOf($t, 'Hearts')->name); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php b/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php new file mode 100755 index 00000000..946b025c --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EscapingTest.class.php @@ -0,0 +1,54 @@ + "\\'"]); + $fixture->write("'Hello', he said"); + + Assert::equals("\\'Hello\\', he said", $out->bytes()); + } + + #[Test] + public function calls_underlying_flush() { + $out= new class() extends MemoryOutputStream { + public $flushed= false; + + public function flush() { + $this->flushed= true; + parent::flush(); + } + }; + $fixture= new Escaping($out, []); + $fixture->flush(); + + Assert::true($out->flushed); + } + + #[Test] + public function calls_underlying_close() { + $out= new class() extends MemoryOutputStream { + public $closed= false; + + public function close() { + $this->closed= true; + parent::close(); + } + }; + $fixture= new Escaping($out, []); + $fixture->close(); + + Assert::true($out->closed); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php index 67c8987c..e051c16f 100755 --- a/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ExceptionsTest.class.php @@ -1,12 +1,13 @@ type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -16,12 +17,12 @@ public function run() { } }'); - $this->assertEquals(IllegalArgumentException::class, $t->newInstance()->run()); + Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } - #[@test] + #[Test] public function line_number_matches() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -31,12 +32,12 @@ public function run() { } }'); - $this->assertEquals(4, $t->newInstance()->run()); + Assert::equals(4, $t->newInstance()->run()); } - #[@test] + #[Test] public function catch_without_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run() { try { throw new \\lang\\IllegalArgumentException("test"); @@ -46,12 +47,27 @@ public function run() { } }'); - $this->assertEquals(IllegalArgumentException::class, $t->newInstance()->run()); + Assert::equals(IllegalArgumentException::class, $t->newInstance()->run()); } - #[@test] + #[Test] + public function non_capturing_catch() { + $t= $this->declare('class %T { + public function run() { + try { + throw new \\lang\\IllegalArgumentException("test"); + } catch (\\lang\\IllegalArgumentException) { + return "Expected"; + } + } + }'); + + Assert::equals('Expected', $t->newInstance()->run()); + } + + #[Test] public function finally_without_exception() { - $t= $this->type('class { + $t= $this->declare('class %T { public $closed= false; public function run() { try { @@ -64,12 +80,12 @@ public function run() { $instance= $t->newInstance(); $instance->run(); - $this->assertTrue($instance->closed); + Assert::true($instance->closed); } - #[@test] + #[Test] public function finally_with_exception() { - $t= $this->type('class { + $t= $this->declare('class %T { public $closed= false; public function run() { try { @@ -85,13 +101,23 @@ public function run() { $instance->run(); $this->fail('Expected exception not caught', null, IllegalArgumentException::class); } catch (IllegalArgumentException $expected) { - $this->assertTrue($instance->closed); + Assert::true($instance->closed); } } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] + public function throw_expression_with_this() { + $this->run('class %T { + private $message= "test"; + public function run($user= null) { + return $user ?? throw new \\lang\\IllegalArgumentException($this->message); + } + }'); + } + + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_null_coalesce() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($user) { return $user ?? throw new \\lang\\IllegalArgumentException("test"); } @@ -99,9 +125,9 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_ternary() { - $t= $this->type('class { + #[Test, Expect(IllegalArgumentException::class)] + public function throw_expression_with_short_ternary() { + $t= $this->declare('class %T { public function run($user) { return $user ?: throw new \\lang\\IllegalArgumentException("test"); } @@ -109,9 +135,9 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_short_ternary() { - $t= $this->type('class { + #[Test, Expect(IllegalArgumentException::class)] + public function throw_expression_with_normal_ternary() { + $t= $this->declare('class %T { public function run($user) { return $user ? new User($user) : throw new \\lang\\IllegalArgumentException("test"); } @@ -119,20 +145,39 @@ public function run($user) { $t->newInstance()->run(null); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] + public function throw_expression_with_binary_or() { + $t= $this->declare('class %T { + public function run($user) { + return $user || throw new \\lang\\IllegalArgumentException("test"); + } + }'); + $t->newInstance()->run(null); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function throw_expression_with_binary_and() { + $t= $this->declare('class %T { + public function run($error) { + return $error && throw new \\lang\\IllegalArgumentException("test"); + } + }'); + $t->newInstance()->run(true); + } + + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda() { - $t= $this->type('class { + $this->run('use lang\IllegalArgumentException; class %T { public function run() { - $f= fn() => throw new \\lang\\IllegalArgumentException("test"); + $f= fn() => throw new IllegalArgumentException("test"); $f(); } }'); - $t->newInstance()->run(); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_throwing_variable() { - $t= $this->type('class { + $t= $this->declare('class %T { public function run($e) { $f= fn() => throw $e; $f(); @@ -141,33 +186,43 @@ public function run($e) { $t->newInstance()->run(new IllegalArgumentException('Test')); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_capturing_variable() { - $t= $this->type('class { + $this->run('use lang\IllegalArgumentException; class %T { public function run() { - $f= fn($message) => throw new \\lang\\IllegalArgumentException($message); + $f= fn($message) => throw new IllegalArgumentException($message); $f("test"); } }'); - $t->newInstance()->run(); } - #[@test, @expect(IllegalArgumentException::class)] + #[Test, Expect(IllegalArgumentException::class)] public function throw_expression_with_lambda_capturing_parameter() { - $t= $this->type('class { + $t= $this->declare('use lang\IllegalArgumentException; class %T { public function run($message) { - $f= fn() => throw new \\lang\\IllegalArgumentException($message); + $f= fn() => throw new IllegalArgumentException($message); $f(); } }'); $t->newInstance()->run('Test'); } - #[@test, @expect(IllegalArgumentException::class)] - public function throw_expression_with_compact_method() { - $t= $this->type('class { - public function run() ==> throw new \\lang\\IllegalArgumentException("test"); + #[Test] + public function try_catch_nested_inside_catch() { + $t= $this->declare('class %T { + public function run() { + try { + throw new \\lang\\IllegalArgumentException("test"); + } catch (\\lang\\IllegalArgumentException $expected) { + try { + throw $expected; + } catch (\\lang\\IllegalArgumentException $expected) { + return $expected->getMessage(); + } + } + } }'); - $t->newInstance()->run(); + + Assert::equals('test', $t->newInstance()->run()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/FileInput.class.php b/src/test/php/lang/ast/unittest/emit/FileInput.class.php index 6a898d9f..8d3d13c9 100755 --- a/src/test/php/lang/ast/unittest/emit/FileInput.class.php +++ b/src/test/php/lang/ast/unittest/emit/FileInput.class.php @@ -1,7 +1,6 @@ type('class { + $t= $this->declare('class %T { private (function(): string) $test; }'); - $this->assertEquals( + Assert::equals( new FunctionType([], Primitive::$STRING), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } - #[@test] + #[Test] public function function_with_parameters() { - $t= $this->type('class { + $t= $this->declare('class %T { private (function(int, string): string) $test; }'); - $this->assertEquals( + Assert::equals( new FunctionType([Primitive::$INT, Primitive::$STRING], Primitive::$STRING), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } } \ 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 new file mode 100755 index 00000000..03897d2d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/GeneratedCodeTest.class.php @@ -0,0 +1,70 @@ +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 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 diff --git a/src/test/php/lang/ast/unittest/emit/GotoTest.class.php b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php new file mode 100755 index 00000000..f300323b --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/GotoTest.class.php @@ -0,0 +1,35 @@ +run('class %T { + public function run() { + goto skip; + return false; + skip: return true; + } + }'); + + Assert::true($r); + } + + #[Test] + public function skip_backward() { + $r= $this->run('class %T { + public function run() { + $return= false; + retry: if ($return) return true; + + $return= true; + goto retry; + return false; + } + }'); + + Assert::true($r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/Handle.class.php b/src/test/php/lang/ast/unittest/emit/Handle.class.php index 964f3c04..d4c066bf 100755 --- a/src/test/php/lang/ast/unittest/emit/Handle.class.php +++ b/src/test/php/lang/ast/unittest/emit/Handle.class.php @@ -1,14 +1,24 @@ id= $id; } + public function redirect($id) { + $this->id= $id; + return $this; + } + public function read($bytes= 8192) { self::$called[]= 'read@'.$this->id; @@ -18,6 +28,12 @@ public function read($bytes= 8192) { return 'test'; } + public function toString() { return nameof($this).'<'.$this->id.'>'; } + + public function hashCode() { return '#'.$this->id; } + + public function compareTo($value) { return $value instanceof self ? $value->id <=> $this->id : 1; } + public function __dispose() { self::$called[]= '__dispose@'.$this->id; } diff --git a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php index ec8b93e1..81c4f5e4 100755 --- a/src/test/php/lang/ast/unittest/emit/ImportTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ImportTest.class.php @@ -1,7 +1,9 @@ getClassLoader()->getResourceAsStream('lang/ast/unittest/emit/import.php')->getURI()); } - #[@test] + #[Test] public function import_type() { - $this->assertEquals(Date::class, $this->run(' + Assert::equals(Date::class, $this->run(' use util\Date; - class { + class %T { public function run() { return Date::class; } }' )); } - #[@test] + #[Test] public function import_type_as_alias() { - $this->assertEquals(Date::class, $this->run(' + Assert::equals(Date::class, $this->run(' use util\Date as D; - class { + class %T { public function run() { return D::class; } }' )); } - #[@test] + #[Test] + public function import_type_as_alias_in_group() { + Assert::equals(Date::class, $this->run(' + use util\{Objects, Date as D}; + + class %T { + public function run() { return D::class; } + }' + )); + } + + #[Test] public function import_const() { - $this->assertEquals('imported', $this->run(' + Assert::equals('imported', $this->run(' use const lang\ast\unittest\emit\FIXTURE; - class { + class %T { public function run() { return FIXTURE; } }' )); } - #[@test] + #[Test] public function import_function() { - $this->assertEquals('imported', $this->run(' + Assert::equals('imported', $this->run(' use function lang\ast\unittest\emit\fixture; - class { + class %T { public function run() { return fixture(); } }' )); } + + #[Test] + public function import_global_into_namespace() { + Assert::equals(Traversable::class, $this->run('namespace test; + use Traversable; + + class %T { + public function run() { return Traversable::class; } + }' + )); + } + + #[Test] + public function import_globals_into_namespace() { + Assert::equals([Traversable::class, Iterator::class], $this->run('namespace test; + use Traversable, Iterator; + + class %T { + public function run() { return [Traversable::class, Iterator::class]; } + }' + )); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php new file mode 100755 index 00000000..8b5b674b --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/InitializeWithExpressionsTest.class.php @@ -0,0 +1,239 @@ +run(strtr('use lang\ast\unittest\emit\{FileInput, Handle}; class %T { + const INITIAL= "initial"; + private $h= %D; + + public function run() { + return $this->h; + } + }', ['%D' => $declaration]))); + } + + #[Test, Values(from: 'expressions')] + public function reflective_access_to_property($declaration, $expected) { + $t= $this->declare(strtr('use lang\ast\unittest\emit\{FileInput, Handle}; class %T { + const INITIAL= "initial"; + public $h= %D; + }', ['%D' => $declaration])); + + Assert::equals($expected, $t->property('h')->get($t->newInstance())); + } + + #[Test, Values(['fn($arg) => $arg->redirect(1)', 'function($arg) { return $arg->redirect(1); }'])] + public function using_closures($declaration) { + $r= $this->run(strtr('class %T { + private $h= %D; + + public function run() { + return $this->h; + } + }', ['%D' => $declaration])); + Assert::equals(new Handle(1), $r(new Handle(0))); + } + + #[Test] + public function using_closures_referencing_this() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + private $id= 1; + private $h= fn() => new Handle($this->id); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r()); + } + + #[Test] + public function using_new_referencing_this() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + private $id= 1; + private $h= new Handle($this->id); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r); + } + + #[Test] + public function using_anonymous_classes() { + $r= $this->run('class %T { + private $h= new class() { public function pipe($h) { return $h->redirect(1); } }; + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r->pipe(new Handle(0))); + } + + #[Test] + public function property_initialization_accessible_inside_constructor() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + private $h= new Handle(0); + + public function __construct() { + $this->h->redirect(1); + } + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(1), $r); + } + + #[Test] + public function promoted_property() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function __construct(private $h= new Handle(0)) { } + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function parameter_default_when_omitted() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run($h= new Handle(0)) { + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function parameter_default_when_passed() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run($h= new Handle(0)) { + return $h; + } + }', new Handle(1)); + Assert::equals(new Handle(1), $r); + } + + #[Test] + public function parameter_default_reflective_access() { + $t= $this->declare('use lang\ast\unittest\emit\Handle; class %T { + public function run($h= new Handle(0)) { + // NOOP + } + }'); + Assert::equals(new Handle(0), $t->method('run')->parameter(0)->default()); + } + + #[Test] + public function property_reference_as_parameter_default() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + private static $h= new Handle(0); + + public function run($h= self::$h) { + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function typed_proprety() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + private Handle $h= new Handle(0); + + public function run() { + return $this->h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function typed_parameter() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run(Handle $h= new Handle(0)) { + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function static_variable() { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { + public function run() { + static $h= new Handle(0); + + return $h; + } + }'); + Assert::equals(new Handle(0), $r); + } + + #[Test] + public function with_argument_promotion() { + $t= $this->declare('use lang\ast\unittest\emit\Handle; class %T { + private $h= new Handle(0); + + public function __construct(private Handle $p) { } + + public function run() { + return $this->h->compareTo($this->p); + } + }'); + Assert::equals(1, $t->newInstance(new Handle(1))->run()); + } + + #[Test] + public function invokes_parent_constructor() { + $t= $this->declare('class %T { + protected $invoked= false; + + public function __construct($invoked) { + $this->invoked= $invoked; + } + }'); + + $r= $this->declare('use lang\ast\unittest\emit\Handle; class %T extends '.$t->literal().' { + private $h= new Handle(0); + + public function run() { + return [$this->invoked, $this->h]; + } + }'); + Assert::equals([true, new Handle(0)], $r->newInstance(true)->run()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php index f126eca6..d591cbaa 100755 --- a/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InstanceOfTest.class.php @@ -1,59 +1,100 @@ run('class { + $r= $this->run('class %T { public function run() { return $this instanceof self; } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function new_self_is_instanceof_this() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new self() instanceof $this; } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function instanceof_qualified_type() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return new \util\Date() instanceof \util\Date; } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function instanceof_imported_type() { - $r= $this->run('use util\Date; class { + $r= $this->run('use util\Date; class %T { public function run() { return new Date() instanceof Date; } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function instanceof_aliased_type() { - $r= $this->run('use util\Date as D; class { + $r= $this->run('use util\Date as D; class %T { public function run() { return new D() instanceof D; } }'); - $this->assertTrue($r); + Assert::true($r); + } + + #[Test] + public function instanceof_instance_expr() { + $r= $this->run('class %T { + private $type= self::class; + + public function run() { + return $this instanceof $this->type; + } + }'); + + Assert::true($r); + } + + #[Test] + public function instanceof_scope_expr() { + $r= $this->run('class %T { + private static $type= self::class; + + public function run() { + return $this instanceof self::$type; + } + }'); + + Assert::true($r); + } + + #[Test] + public function instanceof_expr() { + $r= $this->run('class %T { + private function type() { return self::class; } + + public function run() { + return $this instanceof ($this->type()); + } + }'); + + Assert::true($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php new file mode 100755 index 00000000..6c501047 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/InstantiationTest.class.php @@ -0,0 +1,78 @@ +run('class %T { + public function run() { + return new \\util\\Date(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[Test] + public function new_var() { + $r= $this->run('class %T { + public function run() { + $class= \\util\\Date::class; + return new $class(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[Test] + public function new_dynamic_var() { + $r= $this->run('class %T { + public function run() { + $class= \\util\\Date::class; + $var= "class"; + return new $$var(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[Test] + public function new_var_expr() { + $r= $this->run('class %T { + public function run() { + $class= \\util\\Date::class; + $var= "class"; + return new ${$var}(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[Test] + public function new_expr() { + $r= $this->run('class %T { + private function factory() { return \\util\\Date::class; } + + public function run() { + return new ($this->factory())(); + } + }'); + Assert::instance(Date::class, $r); + } + + #[Test] + public function passing_argument() { + $r= $this->run('class %T { + public $value; + + public function __construct($value= null) { $this->value= $value; } + + public function run() { + return new self("Test"); + } + }'); + Assert::equals('Test', $r->value); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php new file mode 100755 index 00000000..82969599 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/IntersectionTypesTest.class.php @@ -0,0 +1,85 @@ +declare('class %T { + private Traversable&Countable $test; + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->property('test')->constraint()->type() + ); + } + + #[Test] + public function parameter_type() { + $t= $this->declare('class %T { + public function test(Traversable&Countable $arg) { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->method('test')->parameter(0)->constraint()->type() + ); + } + + #[Test] + public function return_type() { + $t= $this->declare('class %T { + public function test(): Traversable&Countable { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->method('test')->returns()->type() + ); + } + + #[Test, Runtime(php: '>=8.1.0-dev')] + public function field_type_restriction_with_php81() { + $t= $this->declare('class %T { + private Traversable&Countable $test; + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->property('test')->constraint()->type() + ); + } + + #[Test, Runtime(php: '>=8.1.0')] + public function parameter_type_restriction_with_php81() { + $t= $this->declare('class %T { + public function test(Traversable&Countable $arg) { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->method('test')->parameter(0)->constraint()->type() + ); + } + + #[Test, Runtime(php: '>=8.1.0')] + public function return_type_restriction_with_php81() { + $t= $this->declare('class %T { + public function test(): Traversable&Countable { } + }'); + + Assert::equals( + new TypeIntersection([new XPClass('Traversable'), new XPClass('Countable')]), + $t->method('test')->returns()->type() + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php index 99b989d5..15ae41bf 100755 --- a/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/InvocationTest.class.php @@ -1,11 +1,14 @@ assertEquals('instance', $this->run( - 'class { + Assert::equals('instance', $this->run( + 'class %T { public function instanceMethod() { return "instance"; } @@ -16,10 +19,10 @@ public function run() { )); } - #[@test] + #[Test] public function instance_method_dynamic_variable() { - $this->assertEquals('instance', $this->run( - 'class { + Assert::equals('instance', $this->run( + 'class %T { public function instanceMethod() { return "instance"; } @@ -31,10 +34,10 @@ public function run() { )); } - #[@test] + #[Test] public function instance_method_dynamic_expression() { - $this->assertEquals('instance', $this->run( - 'class { + Assert::equals('instance', $this->run( + 'class %T { public function instanceMethod() { return "instance"; } @@ -46,10 +49,10 @@ public function run() { )); } - #[@test] + #[Test] public function static_method() { - $this->assertEquals('static', $this->run( - 'class { + Assert::equals('static', $this->run( + 'class %T { public function staticMethod() { return "static"; } @@ -60,10 +63,10 @@ public function run() { )); } - #[@test] + #[Test] public function static_method_dynamic() { - $this->assertEquals('static', $this->run( - 'class { + Assert::equals('static', $this->run( + 'class %T { public static function staticMethod() { return "static"; } @@ -75,10 +78,10 @@ public function run() { )); } - #[@test] + #[Test] public function closure() { - $this->assertEquals('closure', $this->run( - 'class { + Assert::equals('closure', $this->run( + 'class %T { public function run() { $f= function() { return "closure"; }; @@ -88,11 +91,11 @@ public function run() { )); } - #[@test] + #[Test] public function global_function() { - $this->assertEquals('function', $this->run( + Assert::equals('function', $this->run( 'function fixture() { return "function"; } - class { + class %T { public function run() { return fixture(); @@ -100,4 +103,71 @@ public function run() { }' )); } + + #[Test] + public function function_self_reference() { + Assert::equals(13, $this->run( + 'class %T { + + public function run() { + $fib= function($i) use(&$fib) { + if (0 === $i || 1 === $i) { + return $i; + } else { + return $fib($i - 1) + $fib($i - 2); + } + }; + return $fib(7); + } + }' + )); + } + + #[Test, Values(['"html(<) = <", flags: ENT_HTML5', '"html(<) = <", ENT_HTML5, double: true', 'string: "html(<) = <", flags: ENT_HTML5', 'string: "html(<) = <", flags: ENT_HTML5, double: true',])] + public function named_arguments_in_exact_order($arguments) { + Assert::equals('html(<) = &lt;', $this->run( + 'class %T { + + public function escape($string, $flags= ENT_HTML5, $double= true) { + return htmlspecialchars($string, $flags, null, $double); + } + + public function run() { + return $this->escape('.$arguments.'); + } + }' + )); + } + + #[Test, Runtime(php: '>=8.0')] + public function named_arguments_in_reverse_order() { + Assert::equals('html(<) = &lt;', $this->run( + 'class %T { + + public function escape($string, $flags= ENT_HTML5, $double= true) { + return htmlspecialchars($string, $flags, null, $double); + } + + public function run() { + return $this->escape(flags: ENT_HTML5, string: "html(<) = <"); + } + }' + )); + } + + #[Test, Runtime(php: '>=8.0')] + public function named_arguments_omitting_one() { + Assert::equals('html(<) = <', $this->run( + 'class %T { + + public function escape($string, $flags= ENT_HTML5, $double= true) { + return htmlspecialchars($string, $flags, null, $double); + } + + public function run() { + return $this->escape("html(<) = <", double: false); + } + }' + )); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index 82760eae..26ad8fd9 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -1,46 +1,43 @@ run('class { + $r= $this->run('class %T { public function run() { return fn($a) => $a + 1; } }'); - $this->assertEquals(2, $r(1)); + Assert::equals(2, $r(1)); } - #[@test] + #[Test] public function add() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($a, $b) => $a + $b; } }'); - $this->assertEquals(3, $r(1, 2)); + Assert::equals(3, $r(1, 2)); } - #[@test] + #[Test] public function captures_this() { - $r= $this->run('class { + $r= $this->run('class %T { private $addend= 2; public function run() { @@ -48,24 +45,46 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); + } + + #[Test, Condition(assert: 'property_exists(LambdaExpression::class, "static")')] + public function static_fn_does_not_capture_this() { + $r= $this->run('class %T { + public function run() { + return static fn() => isset($this); + } + }'); + + Assert::false($r()); + } + + #[Test, Condition(assert: 'property_exists(ClosureExpression::class, "static")')] + public function static_function_does_not_capture_this() { + $r= $this->run('class %T { + public function run() { + return static function() { return isset($this); }; + } + }'); + + Assert::false($r()); } - #[@test] + #[Test] public function captures_local() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; return fn($a) => $a + $addend; } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local_from_use_list() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; $f= function() use($addend) { @@ -75,12 +94,12 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local_from_lambda() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; $f= fn() => fn($a) => $a + $addend; @@ -88,91 +107,91 @@ public function run() { } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_local_assigned_via_list() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { [$addend]= [2]; return fn($a) => $a + $addend; } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_param() { - $r= $this->run('class { + $r= $this->run('class %T { public function run($addend) { return fn($a) => $a + $addend; } }', 2); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function captures_braced_local() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $addend= 2; return fn($a) => $a + ($addend); } }'); - $this->assertEquals(3, $r(1)); + Assert::equals(3, $r(1)); } - #[@test] + #[Test] public function typed_parameters() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn(\\lang\\Value $in) => $in; } }'); - $this->assertEquals('lang.Value', typeof($r)->signature()[0]->getName()); + Assert::equals('lang.Value', typeof($r)->signature()[0]->getName()); } - #[@test, @action(new RuntimeVersion('>=7.0'))] + #[Test] public function typed_return() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn($in): \\lang\\Value => $in; } }'); - $this->assertEquals('lang.Value', typeof($r)->returns()->getName()); + Assert::equals('lang.Value', typeof($r)->returns()->getName()); } - #[@test] + #[Test] public function without_params() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn() => 1; } }'); - $this->assertEquals(1, $r()); + Assert::equals(1, $r()); } - #[@test] + #[Test] public function immediately_invoked_function_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return (fn() => "IIFE")(); } }'); - $this->assertEquals('IIFE', $r); + Assert::equals('IIFE', $r); } - #[@test] + #[Test] public function with_block() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return fn() => { $a= 1; @@ -181,40 +200,61 @@ public function run() { } }'); - $this->assertEquals(2, $r()); + Assert::equals(2, $r()); } - #[@test] - public function hack_variant_also_supportted() { - $r= $this->run('class { + #[Test] + public function capturing_with_block() { + $r= $this->run('class %T { public function run() { - return ($a) ==> $a + 1; + $a= 1; + return fn() => { + return $a + 1; + }; } }'); - $this->assertEquals(2, $r(1)); + Assert::equals(2, $r()); } - #[@test] - public function hack_variant_without_braces() { - $r= $this->run('class { + #[Test, Expect(Errors::class)] + public function no_longer_supports_hacklang_variant() { + $this->run('class %T { public function run() { - return $a ==> $a + 1; + $func= ($arg) ==> { return 1; }; } }'); + } - $this->assertEquals(2, $r(1)); + #[Test] + public function issue_176() { + $r= $this->run('class %T { + public function run(iterable $records) { + $nonNull= fn($record) => null !== $record; + $process= fn($records, $filter) => { + foreach ($records as $record) { + if ($filter($record)) yield $record; + } + }; + + return $process($records, $nonNull); + } + }', [1, null, 2]); + + Assert::equals([1, 2], [...$r]); } - #[@test] - public function hack_variant_without_braces_as_argument() { - $r= $this->run('class { - private function apply($f, ... $args) { return $f(...$args); } + #[Test] + public function use_with_return_type() { + $r= $this->run('class %T { public function run() { - return $this->apply($a ==> $a + 1, 2); + $local= 1; + return function() use($local): int { + return $local; + }; } }'); - $this->assertEquals(3, $r); + Assert::equals(1, $r()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php b/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php new file mode 100755 index 00000000..c26410c0 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/LineNumbersTest.class.php @@ -0,0 +1,124 @@ +run('class %T { + public function run() { + return __LINE__; + } + }'); + Assert::equals(3, $r); + } + + #[Test] + public function array() { + $r= $this->run('class %T { + public function run() { + return [ + __LINE__, + __LINE__, + ]; + } + }'); + Assert::equals([4, 5], $r); + } + + #[Test] + public function addition() { + $r= $this->run('class %T { + private $lines= []; + + private function line($l) { + $this->lines[]= $l; + return 0; + } + + public function run() { + $r= + $this->line(__LINE__) + + $this->line(__LINE__) + ; + return $this->lines; + } + }'); + Assert::equals([11, 12], $r); + } + + #[Test] + public function chain() { + $r= $this->run('class %T { + private $lines= []; + + private function line($l) { + $this->lines[]= $l; + return $this; + } + + public function run() { + return $this + ->line(__LINE__) + ->line(__LINE__) + ->lines + ; + } + }'); + Assert::equals([11, 12], $r); + } + + #[Test, Values([[true, 4], [false, 5]])] + public function ternary($arg, $line) { + $r= $this->run('class %T { + public function run($arg) { + return $arg + ? __LINE__ + : __LINE__ + ; + } + }', $arg); + Assert::equals($line, $r); + } + + #[Test] + public function binary() { + $r= $this->run('class %T { + private $lines= []; + + private function line($l, $r) { + $this->lines[]= $l; + return $r; + } + + public function run() { + $r= ( + $this->line(__LINE__, true) && + $this->line(__LINE__, false) || + $this->line(__LINE__, true) + ); + return $this->lines; + } + }'); + Assert::equals([11, 12, 13], $r); + } + + #[Test] + public function arguments() { + $r= $this->run('class %T { + private function lines(... $lines) { + return $lines; + } + + public function run() { + return $this->lines( + __LINE__, + __LINE__, + ); + } + }'); + Assert::equals([8, 9], $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/Loading.class.php b/src/test/php/lang/ast/unittest/emit/Loading.class.php index e788f1d6..7ee70b08 100755 --- a/src/test/php/lang/ast/unittest/emit/Loading.class.php +++ b/src/test/php/lang/ast/unittest/emit/Loading.class.php @@ -2,6 +2,6 @@ trait Loading { - public function loaded() { } + public function loaded() { return 'Loaded'; } -} \ No newline at end of file +} diff --git a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php index 59505a88..4b0f40ad 100755 --- a/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LoopsTest.class.php @@ -1,10 +1,12 @@ run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([1, 2, 3] as $number) { @@ -14,12 +16,12 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3', $r); } - #[@test] + #[Test] public function foreach_with_key_and_value() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach (["a" => 1, "b" => 2, "c" => 3] as $key => $number) { @@ -29,12 +31,12 @@ public function run() { } }'); - $this->assertEquals('a=1,b=2,c=3', $r); + Assert::equals('a=1,b=2,c=3', $r); } - #[@test] + #[Test] public function foreach_with_single_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; foreach ([1, 2, 3] as $number) $result.= ",".$number; @@ -42,12 +44,68 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3', $r); + } + + #[Test] + public function foreach_with_destructuring() { + $r= $this->run('class %T { + public function run() { + $result= ""; + foreach ([[1, 2], [3, 4]] as [$a, $b]) $result.= ",".$a." & ".$b; + return substr($result, 1); + } + }'); + + Assert::equals('1 & 2,3 & 4', $r); + } + + #[Test] + public function foreach_with_destructuring_and_missing_expressions() { + $r= $this->run('class %T { + public function run() { + $result= ""; + foreach ([[1, 2, 3], [4, 5, 6]] as [$a, , $b]) $result.= ",".$a." & ".$b; + return substr($result, 1); + } + }'); + + Assert::equals('1 & 3,4 & 6', $r); + } + + #[Test] + public function foreach_with_destructuring_keys() { + $r= $this->run('class %T { + public function run() { + $result= ""; + foreach ([["a" => 1, "b" => 2], ["a" => 3, "b" => 4]] as ["a" => $a, "b" => $b]) $result.= ",".$a." & ".$b; + return substr($result, 1); + } + }'); + + Assert::equals('1 & 2,3 & 4', $r); } - #[@test] + #[Test] + public function foreach_with_destructuring_references() { + $r= $this->run('class %T { + private $list= [[1, 2], [3, 4]]; + + public function run() { + foreach ($this->list as [&$a, &$b]) { + $a*= 3; + $b*= 2; + } + return $this->list; + } + }'); + + Assert::equals([[3, 4], [9, 8]], $r); + } + + #[Test] public function for_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; for ($i= 1; $i < 4; $i++) { @@ -57,12 +115,12 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3', $r); } - #[@test] + #[Test] public function while_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; $i= 0; @@ -73,12 +131,12 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3,4', $r); } - #[@test] + #[Test] public function do_loop() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $result= ""; $i= 1; @@ -89,6 +147,46 @@ public function run() { } }'); - $this->assertEquals('1,2,3', $r); + Assert::equals('1,2,3,4', $r); + } + + #[Test] + public function break_while_loop() { + $r= $this->run('class %T { + public function run() { + $i= 0; + $r= []; + while ($i++ < 5) { + if (4 === $i) { + break; + } else { + $r[]= $i; + } + } + return $r; + } + }'); + + Assert::equals([1, 2, 3], $r); + } + + #[Test] + public function continue_while_loop() { + $r= $this->run('class %T { + public function run() { + $i= 0; + $r= []; + while (++$i < 5) { + if (1 === $i) { + continue; + } else { + $r[]= $i; + } + } + return $r; + } + }'); + + Assert::equals([2, 3, 4], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index f0827447..2465cb97 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -1,10 +1,13 @@ run('class { + $r= $this->run('class %T { private static $MEMBER= "Test"; public function run() { @@ -12,12 +15,25 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] + public function typed_class_property() { + $r= $this->run('class %T { + private static string $MEMBER= "Test"; + + public function run() { + return self::$MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] public function class_method() { - $r= $this->run('class { + $r= $this->run('class %T { private static function member() { return "Test"; } public function run() { @@ -25,12 +41,230 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); + } + + #[Test] + public function class_constant() { + $r= $this->run('class %T { + private const MEMBER = "Test"; + + public function run() { + return self::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function typed_class_constant() { + $t= $this->declare('class %T { + private const string MEMBER = "Test"; + }'); + $const= $t->constant('MEMBER'); + + Assert::equals('Test', $const->value()); + Assert::equals(Primitive::$STRING, $const->constraint()->type()); + } + + #[Test, Values(['$this->$member', '$this->{$member}', '$this->{strtoupper($member)}'])] + public function dynamic_instance_property($syntax) { + $r= $this->run('class %T { + private $MEMBER= "Test"; + + public function run() { + $member= "MEMBER"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Values(['self::$$member', 'self::${$member}', 'self::${strtoupper($member)}'])] + public function dynamic_class_property($syntax) { + $r= $this->run('class %T { + private static $MEMBER= "Test"; + + public function run() { + $member= "MEMBER"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Values(['$this->$method()', '$this->{$method}()', '$this->{strtolower($method)}()'])] + public function dynamic_instance_method($syntax) { + $r= $this->run('class %T { + private function test() { return "Test"; } + + public function run() { + $method= "test"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); } - #[@test] + #[Test, Values(['self::$method()', 'self::{$method}()', 'self::{strtolower($method)}()'])] + public function dynamic_class_method($syntax) { + $r= $this->run('class %T { + private static function test() { return "Test"; } + + public function run() { + $method= "test"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Values(['self::{$member}', 'self::{strtoupper($member)}'])] + public function dynamic_class_constant($syntax) { + $r= $this->run('class %T { + const MEMBER= "Test"; + + public function run() { + $member= "MEMBER"; + return '.$syntax.'; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function property_of_dynamic_class() { + $r= $this->run('class %T { + private static $MEMBER= "Test"; + + public function run() { + $class= self::class; + return $class::$MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function method_of_dynamic_class() { + $r= $this->run('class %T { + private static function member() { return "Test"; } + + public function run() { + $class= self::class; + return $class::member(); + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function constant_of_dynamic_class() { + $r= $this->run('class %T { + private const MEMBER = "Test"; + + public function run() { + $class= self::class; + return $class::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function object_class_constant() { + $r= $this->run('class %T { + private const MEMBER = "Test"; + + public function run() { + return $this::MEMBER; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function list_property() { + $r= $this->run('class %T { + private $list= [1, 2, 3]; + + public function run() { + return $this->list; + } + }'); + + Assert::equals([1, 2, 3], $r); + } + + #[Test] + public function list_method() { + $r= $this->run('class %T { + private function list() { return [1, 2, 3]; } + + public function run() { + return $this->list(); + } + }'); + + Assert::equals([1, 2, 3], $r); + } + + #[Test] + public function return_by_reference() { + $r= $this->run('class %T { + private $list= []; + + public function &list() { return $this->list; } + + public function run() { + $list= &$this->list(); + $list[]= "Test"; + return $this->list; + } + + }'); + Assert::equals(['Test'], $r); + } + + #[Test] + public function magic_class_constant() { + $t= $this->type('class %T { + public function run() { + return self::class; + } + }'); + Assert::equals($t->literal(), $t->newInstance()->run()); + } + + #[Test, Values(['variable', 'invocation', 'array'])] + public function class_on_objects($via) { + $t= $this->declare('class %T { + private function this() { return $this; } + + public function variable() { return $this::class; } + + public function invocation() { return $this->this()::class; } + + public function array() { return [$this][0]::class; } + }'); + + $fixture= $t->newInstance(); + Assert::equals(get_class($fixture), $t->method($via)->invoke($fixture)); + } + + #[Test] public function instance_property() { - $r= $this->run('class { + $r= $this->run('class %T { private $member= "Test"; public function run() { @@ -38,12 +272,25 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] + public function typed_instance_property() { + $r= $this->run('class %T { + private string $member= "Test"; + + public function run() { + return $this->member; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] public function instance_method() { - $r= $this->run('class { + $r= $this->run('class %T { private function member() { return "Test"; } public function run() { @@ -51,12 +298,12 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] public function static_initializer_run() { - $r= $this->run('class { + $r= $this->run('class %T { private static $MEMBER; static function __static() { @@ -68,12 +315,12 @@ public function run() { } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] public function enum_members() { - $r= $this->run('class extends \lang\Enum { + $r= $this->run('class %T extends \lang\Enum { public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; public function run() { @@ -81,30 +328,110 @@ public function run() { } }'); - $this->assertEquals('MON', $r); + Assert::equals('MON', $r); + } + + #[Test] + public function allow_constant_syntax_for_members() { + $r= $this->run('use lang\{Enum, CommandLine}; class %T extends Enum { + public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; + + public function run() { + return [self::MON->name(), %T::TUE->name(), CommandLine::WINDOWS->name()]; + } + }'); + + Assert::equals(['MON', 'TUE', 'WINDOWS'], $r); } - #[@test] + #[Test] public function method_with_static() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { static $var= "Test"; return $var; } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] public function method_with_static_without_initializer() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { static $var; return $var; } }'); - $this->assertNull($r); + Assert::null($r); + } + + #[Test] + public function chaining_sccope_operators() { + $r= $this->run('class %T { + private const TYPE = self::class; + + private const NAME = "Test"; + + private static $name = "Test"; + + private static function name() { return "Test"; } + + public function run() { + $name= "name"; + return [self::TYPE::NAME, self::TYPE::$name, self::TYPE::name(), self::TYPE::$name()]; + } + }'); + + Assert::equals(['Test', 'Test', 'Test', 'Test'], $r); + } + + #[Test] + public function self_return_type() { + $t= $this->declare(' + class %T { public function run(): self { return $this; } } + '); + Assert::equals($t->class(), $t->method('run')->returns()->type()); + } + + #[Test] + public function static_return_type() { + $t= $this->declare(' + class %TBase { public function run(): static { return $this; } } + class %T extends %TBase { } + '); + Assert::equals($t->parent()->class(), $t->method('run')->returns()->type()); + } + + #[Test] + public function array_of_self_return_type() { + $t= $this->declare(' + class %T { public function run(): array { return [$this]; } } + '); + Assert::equals(new ArrayType($t->class()), $t->method('run')->returns()->type()); + } + + #[Test, Values(['namespace', 'class', 'new', 'use', 'interface', 'trait', 'enum'])] + public function keyword_used_as_method_name($keyword) { + $r= $this->run('class %T { + private static function '.$keyword.'() { return "Test"; } + + public function run() { + return self::'.$keyword.'(); + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function final_property() { + $t= $this->declare('class %T { + public final string $fixture= "Test"; + }'); + + Assert::equals(MODIFIER_PUBLIC | MODIFIER_FINAL, $t->property('fixture')->modifiers()->bits()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php new file mode 100755 index 00000000..a7cc07e5 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/MethodOverridingTest.class.php @@ -0,0 +1,125 @@ +declare($code); + return null; + } catch (Error $e) { + return preg_replace('/T[0-9]+/', 'T', $e->getMessage()); + } + } + + #[Test] + public function without_annotations() { + Assert::null($this->verify('class %T { public function fixture() { } }')); + } + + #[Test] + public function correctly_overwriting_parent_method() { + Assert::null($this->verify('class %T extends \lang\Throwable { + #[Override] + public function compoundMessage() { } + }')); + } + + #[Test] + public function correctly_implementing_interface_method() { + Assert::null($this->verify('class %T implements \lang\Runnable { + #[Override] + public function run() { } + }')); + } + + #[Test] + public function correctly_overwriting_interface_method() { + Assert::null($this->verify('interface %T extends \lang\Runnable { + #[Override] + public function run(); + }')); + } + + #[Test] + public function without_parent() { + Assert::equals( + 'T::fixture() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T { + #[Override] + public function fixture() { } + }' + )); + } + + #[Test] + public function without_parent_in_anonymous_class() { + Assert::equals( + 'class@anonymous::fixture() has #[\Override] attribute, but no matching parent method exists', + $this->verify('new class() { + #[Override] + public function fixture() { } + };' + )); + } + + #[Test] + public function overriding_non_existant_method() { + Assert::equals( + 'T::nonExistant() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T extends \lang\Throwable { + #[Override] + public function nonExistant() { } + }' + )); + } + + #[Test] + public function override_inside_traits() { + Assert::null($this->verify('trait %T { + #[Override] + public function run() { } + }')); + } + + #[Test] + public function without_parent_with_method_from_trait() { + $trait= $this->declare('trait %T { + #[Override] + public function run() { } + }'); + + Assert::equals( + 'T::run() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T { use '.$trait->literal().'; }') + ); + } + + #[Test] + public function private_methods_not_considered() { + $parent= $this->declare('class %T { private function fixture() { } }'); + + Assert::equals( + 'T::fixture() has #[\Override] attribute, but no matching parent method exists', + $this->verify('class %T extends '.$parent->literal().' { + #[Override] + public function fixture() { } + }') + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php index cccf7cb1..bcad3634 100755 --- a/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MultipleCatchTest.class.php @@ -1,7 +1,7 @@ type('class { + $t= $this->declare('class %T { public function run($t) { try { throw new $t("test"); } catch (\\lang\\IllegalArgumentException | \\lang\\IllegalStateException $e) { - return "caught ".get_class($e); + return "Caught ".get_class($e); } } }'); - $this->assertEquals('caught '.$type, $t->newInstance()->run($type)); + Assert::equals('Caught '.$type, $t->newInstance()->run($type)); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php new file mode 100755 index 00000000..a4dc258f --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php @@ -0,0 +1,87 @@ +declare('class %T { }')->name(), '.')); + } + + #[Test] + public function with_namespace() { + Assert::equals('test', $this->declare('namespace test; class %T { }')->package()->name()); + } + + #[Test] + public function resolves_unqualified() { + $r= $this->run('namespace util; class %T { + public function run() { + return new Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_relative() { + $r= $this->run('class %T { + public function run() { + return new util\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_absolute() { + $r= $this->run('namespace test; class %T { + public function run() { + return new \util\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_import() { + $r= $this->run('namespace test; use util\Date; class %T { + public function run() { + return new Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_alias() { + $r= $this->run('namespace test; use util\Date as DateTime; class %T { + public function run() { + return new DateTime("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_namespace_keyword() { + $r= $this->run('namespace util; class %T { + public function run() { + return new namespace\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } + + #[Test] + public function resolves_sub_namespace() { + $r= $this->run('class %T { + public function run() { + return new namespace\util\Date("1977-12-14"); + } + }'); + Assert::equals(new Date('1977-12-14'), $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php index 51280dcd..0d15125f 100755 --- a/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullCoalesceTest.class.php @@ -1,40 +1,40 @@ =7.0'))] + #[Test] public function on_null() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return null ?? true; } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function on_unset_array_key() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return $array["key"] ?? true; } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function assignment_operator() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $array["key"] ??= true; return $array; } }'); - $this->assertEquals(['key' => true], $r); + Assert::equals(['key' => true], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php index 574422ec..f095031b 100755 --- a/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/NullSafeTest.class.php @@ -1,31 +1,32 @@ run('class { + $r= $this->run('class %T { public function run() { $object= null; return $object?->method(); } }'); - $this->assertNull($r); + Assert::null($r); } - #[@test] + #[Test] public function method_call_on_object() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= new class() { public function method() { return true; } @@ -34,24 +35,24 @@ public function method() { return true; } } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function member_access_on_null() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= null; return $object?->member; } }'); - $this->assertNull($r); + Assert::null($r); } - #[@test] + #[Test] public function member_access_on_object() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= new class() { public $member= true; @@ -60,44 +61,72 @@ public function run() { } }'); - $this->assertTrue($r); + Assert::true($r); } - #[@test] + #[Test] public function chained_method_call() { $r= $this->run(' - class Invocation { + class %TInvocation { public static $invoked= []; public function __construct(private $name, private $chained) { } public function chained() { self::$invoked[]= $this->name; return $this->chained; } } - class { + class %T { public function run() { - $invokation= new Invocation("outer", new Invocation("inner", null)); + $invokation= new %TInvocation("outer", new %TInvocation("inner", null)); $return= $invokation?->chained()?->chained()?->chained(); - return [$return, Invocation::$invoked]; + return [$return, %TInvocation::$invoked]; } } '); - $this->assertEquals([null, ['outer', 'inner']], $r); + Assert::equals([null, ['outer', 'inner']], $r); } - #[@test] + #[Test] public function dynamic_member_access_on_object() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { $object= new class() { public $member= true; }; $member= new class() { - public function name() ==> "member"; + public function name() { return "member"; } }; return $object?->{$member->name()}; } }'); - $this->assertTrue($r); + Assert::true($r); + } + + #[Test] + public function short_circuiting_chain() { + $r= $this->run('class %T { + public function run() { + $null= null; + return $null?->method($undefined->method()); + } + }'); + + Assert::null($r); + } + + #[Test] + public function short_circuiting_parameter() { + $r= $this->run('class %T { + private function pass($object) { + return $object; + } + + public function run() { + $null= null; + return $this->pass($null?->method()); + } + }'); + + Assert::null($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/NullableSupport.class.php b/src/test/php/lang/ast/unittest/emit/NullableSupport.class.php new file mode 100755 index 00000000..186ed02a --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/NullableSupport.class.php @@ -0,0 +1,13 @@ +run('class %T { + public function run() { + $a= 1; + $a'.$op.' 2; + return $a; + } + }'); + + Assert::equals($expected, $r); + } + + #[Test, Values([['|=', 0x0003], ['&=', 0x0002], ['^=', 0x0001], ['>>=', 0x0000], ['<<=', 0x000C]])] + public function assignment_and_bitwise($op, $expected) { + $r= $this->run('class %T { + public function run() { + $a= 0x0003; + $a'.$op.' 0x0002; + return $a; + } + }'); + + Assert::equals($expected, $r); + } + + #[Test] + public function concatenation() { + $r= $this->run('class %T { + public function run() { + $a= "A.."; + $a.= "B"; + return $a; + } + }'); + + Assert::equals('A..B', $r); + } + + #[Test, Values([['$a++', 2, 1], ['++$a', 2, 2], ['$a--', 0, 1], ['--$a', 0, 0]])] + public function inc_dec($op, $a, $b) { + $r= $this->run('class %T { + public function run() { + $a= 1; + $b= '.$op.'; + return [$a, $b]; + } + }'); + + Assert::equals([$a, $b], $r); + } + + #[Test] + public function references() { + $r= $this->run('class %T { + public function run() { + $a= 3; + $ptr= &$a; + $a++; + return $ptr; + } + }'); + + Assert::equals(4, $r); + } + + #[Test] + public function destructuring() { + $r= $this->run('class %T { + public function run() { + [$a, $b]= explode("..", "A..B"); + return [$a, $b]; + } + }'); + + Assert::equals(['A', 'B'], $r); + } + + #[Test] + public function swap_variables() { + $r= $this->run('class %T { + public function run() { + $a= 1; $b= 2; + [$a, $b]= [$b, $a]; + return [$a, $b]; + } + }'); + + Assert::equals([2, 1], $r); + } + + #[Test, Values([[null, true], [false, false], ['Test', 'Test']])] + public function null_coalesce_assigns_true_if_null($value, $expected) { + $r= $this->run('class %T { + public function run($arg) { + $arg??= true; + return $arg; + } + }', $value); + + Assert::equals($expected, $r); + } + + #[Test, Values([[[], true], [[null], true], [[false], false], [['Test'], 'Test']])] + public function null_coalesce_fills_array_if_non_existant_or_null($value, $expected) { + $r= $this->run('class %T { + public function run($arg) { + $arg[0]??= true; + return $arg; + } + }', $value); + + Assert::equals($expected, $r[0]); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php new file mode 100755 index 00000000..a89ff61d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP81Test.class.php @@ -0,0 +1,96 @@ +emit('f(key: "value");')); + } + + #[Test] + public function named_arguments() { + Assert::equals('f(color:"green",price:12.50);', $this->emit('f(color: "green", price: 12.50);')); + } + + #[Test] + public function unit_enum() { + Assert::equals( + 'enum OS{case WINDOWS;case UNIX;};', + preg_replace('/static function __init.+__init\(\);/', '}', $this->emit('enum OS { case WINDOWS; case UNIX; }')) + ); + } + + #[Test] + public function backed_enum() { + Assert::equals( + 'enum Suit:string{case Hearts="♥";};', + preg_replace('/static function __init.+__init\(\);/', '}', $this->emit('enum Suit : string { case Hearts= "♥"; }')) + ); + } + + #[Test] + public function new_type() { + Assert::equals('new \\T();', $this->emit('new T();')); + } + + #[Test] + public function new_expression() { + Assert::equals('new ($this->class)();', $this->emit('new ($this->class)();')); + } + + #[Test] + public function throw_expression() { + Assert::equals('fn()=>throw new \\T();', $this->emit('fn() => throw new T();')); + } + + #[Test] + public function catch_without_variable() { + Assert::equals('try {}catch(\\T) {};', $this->emit('try { } catch (\\T) { }')); + } + + #[Test] + public function catch_without_types() { + Assert::equals('try {}catch(\\Throwable $e) {};', $this->emit('try { } catch ($e) { }')); + } + + #[Test] + public function multi_catch_without_variable() { + Assert::equals('try {}catch(\\A|\\B) {};', $this->emit('try { } catch (\\A | \\B) { }')); + } + + #[Test] + public function null_safe() { + Assert::equals('$person?->name;', $this->emit('$person?->name;')); + } + + #[Test] + public function null_safe_expression() { + Assert::equals('$person?->{$name};', $this->emit('$person?->{$name};')); + } + + #[Test] + public function match() { + Assert::equals('match (true) {};', $this->emit('match (true) { };')); + } + + #[Test] + public function match_without_expression() { + Assert::equals('match (true) {};', $this->emit('match { };')); + } + + #[Test] + public function match_with_case() { + Assert::equals('match ($v) {1=>true,};', $this->emit('match ($v) { 1 => true };')); + } + + #[Test] + public function match_with_case_and_default() { + Assert::equals('match ($v) {1=>true,default=>false};', $this->emit('match ($v) { 1 => true, default => false };')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php new file mode 100755 index 00000000..089efb86 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP82Test.class.php @@ -0,0 +1,33 @@ +emit('strlen(...);')); + } + + #[Test] + public function callable_static_method_syntax() { + Assert::equals('\lang\Type::forName(...);', $this->emit('\lang\Type::forName(...);')); + } + + #[Test] + public function callable_instance_method_syntax() { + Assert::equals('$this->method(...);', $this->emit('$this->method(...);')); + } + + #[Test] + public function readonly_classes() { + Assert::matches( + '/readonly class [A-Z0-9]+{/', + $this->emit('readonly class %T { }') + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PHP83Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP83Test.class.php new file mode 100755 index 00000000..16197321 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP83Test.class.php @@ -0,0 +1,18 @@ +emit('readonly class %T { }') + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PHP84Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP84Test.class.php new file mode 100755 index 00000000..69c55acd --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP84Test.class.php @@ -0,0 +1,26 @@ +"Test";}/', + $this->emit('class %T { public $test { get => "Test"; } }') + ); + } + + #[Test] + public function asymmetric_visibility() { + Assert::matches( + '/public private\(set\) string \$test/', + $this->emit('class %T { public private(set) string $test; }') + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PHP85Test.class.php b/src/test/php/lang/ast/unittest/emit/PHP85Test.class.php new file mode 100755 index 00000000..5010b7e3 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PHP85Test.class.php @@ -0,0 +1,26 @@ +strtoupper(...);', + $this->emit('"test" |> strtoupper(...);') + ); + } + + #[Test] + public function nullsafepipe_operator() { + Assert::equals( + 'null===($_0="test")?null:$_0|>strtoupper(...);', + $this->emit('"test" ?|> strtoupper(...);') + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php index 11196a1b..14fd6efc 100755 --- a/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ParameterTest.class.php @@ -1,117 +1,152 @@ type('use lang\Value; class { public function fixture('.$declaration.') { } }') - ->getMethod('fixture') - ->getParameter(0) + return $this->declare('use lang\Value; use util\Binford; class %T { public function fixture('.$declaration.') { } }') + ->method('fixture') + ->parameter(0) ; } - #[@test] + /** @return iterable */ + private function special() { + yield ['array $param', Type::$ARRAY]; + yield ['callable $param', Type::$CALLABLE]; + yield ['iterable $param', Type::$ITERABLE]; + yield ['object $param', Type::$OBJECT]; + } + + #[Test] public function name() { - $this->assertEquals('param', $this->param('$param')->getName()); + Assert::equals('param', $this->param('$param')->name()); } - #[@test] + #[Test] public function without_type() { - $this->assertEquals(Type::$VAR, $this->param('$param')->getType()); + Assert::equals(Type::$VAR, $this->param('$param')->constraint()->type()); } - #[@test, @values([ - # ['array $param', Type::$ARRAY], - # ['callable $param', Type::$CALLABLE], - # ['iterable $param', Type::$ITERABLE], - # ['object $param', Type::$OBJECT], - #])] + #[Test, Values(from: 'special')] public function with_special_type($declaration, $type) { - $this->assertEquals($type, $this->param($declaration)->getType()); + Assert::equals($type, $this->param($declaration)->constraint()->type()); } - #[@test] + #[Test] public function value_typed() { - $this->assertEquals(new XPClass(Value::class), $this->param('Value $param')->getType()); + Assert::equals(new XPClass(Value::class), $this->param('Value $param')->constraint()->type()); } - #[@test] + #[Test] public function value_type_with_null() { - $this->assertEquals(new XPClass(Value::class), $this->param('Value $param= null')->getType()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('?Value $param= null')->constraint()->type()); } - #[@test] + #[Test] public function nullable_value_type() { - $this->assertEquals(new XPClass(Value::class), $this->param('?Value $param')->getType()); + Assert::equals($this->nullable(new XPClass(Value::class)), $this->param('?Value $param')->constraint()->type()); } - #[@test] + #[Test] public function string_typed() { - $this->assertEquals(Primitive::$STRING, $this->param('string $param')->getType()); + Assert::equals(Primitive::$STRING, $this->param('string $param')->constraint()->type()); } - #[@test] + #[Test] public function string_typed_with_null() { - $this->assertEquals(Primitive::$STRING, $this->param('string $param= null')->getType()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param= null')->constraint()->type()); } - #[@test] + #[Test] public function nullable_string_type() { - $this->assertEquals(Primitive::$STRING, $this->param('?string $param')->getType()); + Assert::equals($this->nullable(Primitive::$STRING), $this->param('?string $param')->constraint()->type()); } - #[@test] + #[Test] public function array_typed() { - $this->assertEquals(new ArrayType(Primitive::$INT), $this->param('array $param')->getType()); + Assert::equals( + new ArrayType(Primitive::$INT), + $this->param('array $param')->constraint()->type() + ); } - #[@test] + #[Test] public function map_typed() { - $this->assertEquals(new MapType(Primitive::$INT), $this->param('array $param')->getType()); + Assert::equals( + new MapType(Primitive::$INT), + $this->param('array $param')->constraint()->type() + ); } - #[@test] + #[Test] public function simple_annotation() { - $this->assertEquals(['inject' => null], $this->param('<> $param')->getAnnotations()); + Assert::equals(['Inject' => []], $this->annotations($this->param('#[Inject] $param'))); } - #[@test] + #[Test] public function annotation_with_value() { - $this->assertEquals(['inject' => 'dsn'], $this->param('<> $param')->getAnnotations()); + Assert::equals(['Inject' => ['dsn']], $this->annotations($this->param('#[Inject("dsn")] $param'))); } - #[@test] + #[Test] public function multiple_annotations() { - $this->assertEquals( - ['inject' => null, 'name' => 'dsn'], - $this->param('<> $param')->getAnnotations() + Assert::equals( + ['Inject' => [], 'Name' => ['dsn']], + $this->annotations($this->param('#[Inject, Name("dsn")] $param')) ); } - #[@test] + #[Test] public function required_parameter() { - $this->assertEquals(false, $this->param('$param')->isOptional()); + Assert::equals(false, $this->param('$param')->optional()); } - #[@test] + #[Test] public function optional_parameter() { - $this->assertEquals(true, $this->param('$param= true')->isOptional()); + Assert::equals(true, $this->param('$param= true')->optional()); } - #[@test] + #[Test] public function optional_parameters_default_value() { - $this->assertEquals(true, $this->param('$param= true')->getDefaultValue()); + Assert::equals(true, $this->param('$param= true')->default()); + } + + #[Test] + public function new_as_default() { + $power= $this->param('$power= new Binford(6100)')->default(); + Assert::equals(new Binford(6100), $power); + } + + #[Test] + public function closure_as_default() { + $function= $this->param('$op= fn($in) => $in * 2')->default(); + Assert::equals(2, $function(1)); + } + + #[Test] + public function first_class_callable_as_default() { + $function= $this->param('$op= strlen(...)')->default(); + Assert::equals(4, $function('Test')); + } + + #[Test] + public function trailing_comma_allowed() { + $p= $this->declare('class %T { public function fixture($param, ) { } }') + ->method('fixture') + ->parameters() + ; + Assert::equals(1, $p->size(), 'number of parameters'); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php new file mode 100755 index 00000000..6d017ef7 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PipelinesTest.class.php @@ -0,0 +1,349 @@ +run('class %T { + public function run() { + return "test" |> strtoupper(...); + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_variable() { + $r= $this->run('class %T { + public function run() { + $f= strtoupper(...); + return "test" |> $f; + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_string() { + $r= $this->run('class %T { + public function run() { + return "test" |> "strtoupper"; + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_array() { + $r= $this->run('class %T { + public function toUpper($x) { return strtoupper($x); } + + public function run() { + return "test" |> [$this, "toUpper"]; + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function pipe_to_callable_without_all_args() { + $r= $this->run('class %T { + public function run() { + return "A&B" |> htmlspecialchars(...); + } + }'); + + Assert::equals('A&B', $r); + } + + #[Test] + public function pipe_to_callable_new() { + $r= $this->run('class %T { + public function run() { + return "2024-03-27" |> new \util\Date(...); + } + }'); + + Assert::equals('2024-03-27', $r->toString('Y-m-d')); + } + + #[Test] + public function pipe_to_callable_anonymous_new() { + $r= $this->run('class %T { + public function run() { + return "2024-03-27" |> new class(...) { + public function __construct(public string $value) { } + }; + } + }'); + + Assert::equals('2024-03-27', $r->value); + } + + #[Test] + public function pipe_to_closure() { + $r= $this->run('class %T { + public function run() { + return "test" |> fn($x) => $x.": OK"; + } + }'); + + Assert::equals('test: OK', $r); + } + + #[Test, Expect(Error::class)] + public function pipe_to_throw() { + $this->run('use lang\Error; class %T { + public function run() { + return "test" |> throw new Error("Test"); + } + }'); + } + + #[Test, Expect(Error::class)] + public function missing_function() { + $this->run('class %T { + public function run() { + return "test" |> "__missing"; + } + }'); + } + + #[Test, Expect(Error::class)] + public function missing_argument() { + $this->run('class %T { + public function run() { + return 5 |> fn($a, $b) => $a * $b; + } + }'); + } + + #[Test] + public function pipe_chain() { + $r= $this->run('class %T { + public function run() { + return " test " |> trim(...) |> strtoupper(...); + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test, Values([[['test'], 'TEST'], [[''], ''], [[], null]])] + public function nullsafe_pipe($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return array_shift($arg) ?|> strtoupper(...); + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test, Values([[null, null], ['', ''], ['test', 'TEST'], [' test ', 'TEST']])] + public function nullsafe_chain($input, $expected) { + $r= $this->run('class %T { + public function run($arg) { + return $arg ?|> trim(...) ?|> strtoupper(...); + } + }', $input); + + Assert::equals($expected, $r); + } + + #[Test] + public function concat_precedence() { + $r= $this->run('class %T { + public function run() { + return "te" . "st" |> strtoupper(...); + } + }'); + + Assert::equals('TEST', $r); + } + + #[Test] + public function addition_precedence() { + $r= $this->run('class %T { + public function run() { + return 5 + 2 |> fn($i) => $i * 2; + } + }'); + + Assert::equals(14, $r); + } + + #[Test] + public function comparison_precedence() { + $r= $this->run('class %T { + public function run() { + return 5 |> fn($i) => $i * 2 === 10; + } + }'); + + Assert::true($r); + } + + #[Test, Values([[0, 'even'], [1, 'odd'], [2, 'even']])] + public function ternary_precedence($arg, $expected) { + $r= $this->run('class %T { + + private function odd($n) { return $n % 2; } + + public function run($arg) { + return $arg |> $this->odd(...) ? "odd" : "even"; + } + }', $arg); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, '(empty)'], [1, 'one element'], [2, '2 elements']])] + public function short_ternary_precedence($arg, $expected) { + $r= $this->run('class %T { + + private function number($n) { + return match ($n) { + 0 => null, + 1 => "one element", + default => "{$n} elements" + }; + } + + public function run($arg) { + return $arg |> $this->number(...) ?: "(empty)"; + } + }', $arg); + + Assert::equals($expected, $r); + } + + #[Test, Values([[0, 'root'], [1001, 'test'], [1002, '#unknown']])] + public function coalesce_precedence($arg, $expected) { + $r= $this->run('class %T { + private $users= [0 => "root", 1001 => "test"]; + + private function user($id) { return $this->users[$id] ?? null; } + + public function run($arg) { + return $arg |> $this->user(...) ?? "#unknown"; + } + }', $arg); + + Assert::equals($expected, $r); + } + + #[Test] + public function rfc_example() { + $r= $this->run('class %T { + public function run() { + return "Hello World" + |> "htmlentities" + |> str_split(...) + |> fn($x) => array_map(strtoupper(...), $x) + |> fn($x) => array_filter($x, fn($v) => $v != "O") + ; + } + }'); + Assert::equals(['H', 'E', 'L', 'L', ' ', 'W', 'R', 'L', 'D'], array_values($r)); + } + + #[Test, Expect(Error::class), Runtime(php: '>=8.5.0')] + public function rejects_by_reference_functions() { + $this->run('class %T { + private function modify(&$arg) { $arg++; } + + public function run() { + $val= 1; + return $val |> $this->modify(...); + } + }'); + } + + #[Test] + public function accepts_prefer_by_reference_functions() { + $r= $this->run('class %T { + public function run() { + return ["hello", "world"] |> array_multisort(...); + } + }'); + + Assert::true($r); + } + + #[Test] + public function execution_order() { + $r= $this->run('class %T { + public function run() { + $invoked= []; + + $first= function() use(&$invoked) { $invoked[]= "first"; return 1; }; + $second= function() use(&$invoked) { $invoked[]= "second"; return false; }; + $skipped= function() use(&$invoked) { $invoked[]= "skipped"; return $in; }; + $third= function($in) use(&$invoked) { $invoked[]= "third"; return $in; }; + $capture= function($result) use(&$invoked) { $invoked[]= $result; }; + + $first() |> ($second() ? $skipped : $third) |> $capture; + return $invoked; + } + }'); + + Assert::equals(['first', 'second', 'third', 1], $r); + } + + #[Test] + public function interrupted_by_exception() { + $r= $this->run('use lang\Error; class %T { + public function run() { + $invoked= []; + + $provide= function() use(&$invoked) { $invoked[]= "provide"; return 1; }; + $transform= function($in) use(&$invoked) { $invoked[]= "transform"; return $in * 2; }; + $throw= function() use(&$invoked) { $invoked[]= "throw"; throw new Error("Break"); }; + + try { + $provide() |> $transform |> $throw |> throw new Error("Unreachable"); + } catch (Error $e) { + $invoked[]= $e->compoundMessage(); + } + return $invoked; + } + }'); + + Assert::equals(['provide', 'transform', 'throw', 'Exception lang.Error (Break)'], $r); + } + + #[Test] + public function generators() { + $r= $this->run('class %T { + private function range($lo, $hi) { + for ($i= $lo; $i <= $hi; $i++) { + yield $i; + } + } + + private function map($fn) { + return function($it) use($fn) { + foreach ($it as $element) { + yield $fn($element); + } + }; + } + + public function run() { + return $this->range(1, 3) |> $this->map(fn($e) => $e + 1) |> iterator_to_array(...); + } + }'); + + Assert::equals([2, 3, 4], $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php new file mode 100755 index 00000000..91ec35ed --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PrecedenceTest.class.php @@ -0,0 +1,43 @@ +run( + 'class %T { + public function run() { + return '.$input.'; + } + }' + )); + } + + #[Test] + public function concatenation() { + $t= $this->declare( + 'class %T { + public function run() { + return "(".self::class.")"; + } + }' + ); + Assert::equals('('.$t->literal().')', $t->newInstance()->run()); + } + + #[Test] + public function plusplus() { + $t= $this->declare( + 'class %T { + private $number= 1; + + public function run() { + return ++$this->number; + } + }' + ); + Assert::equals(2, $t->newInstance()->run()); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php new file mode 100755 index 00000000..9c302b6d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -0,0 +1,360 @@ +run('class %T { + public $test { get => "Test"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function get_block() { + $r= $this->run('class %T { + public $test { get { return "Test"; } } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function abbreviated_get() { + $r= $this->run('class %T { + private $word= "Test"; + private $interpunction= "!"; + + public $test => $this->word.$this->interpunction; + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test!', $r); + } + + #[Test] + public function set_expression() { + $r= $this->run('class %T { + public $test { set => ucfirst($value); } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function set_block() { + $r= $this->run('class %T { + public $test { set { $this->test= ucfirst($value); } } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function set_raising_exception() { + $this->run('use lang\\IllegalArgumentException; class %T { + public $test { set { throw new IllegalArgumentException("Cannot set"); } } + + public function run() { + $this->test= "test"; + } + }'); + } + + #[Test] + public function get_and_set_using_property() { + $r= $this->run('class %T { + public $test { + get => $this->test; + set => ucfirst($value); + } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function implicit_set() { + $r= $this->run('class %T { + public $test { + get => ucfirst($this->test); + } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function typed_set() { + $r= $this->run('use util\\Bytes; class %T { + public string $test { + set(string|Bytes $arg) => ucfirst($arg); + } + + public function run() { + $this->test= new Bytes(["t", "e", "s", "t"]); + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(class: Error::class, message: '/Argument .+ type string, array given/')] + public function typed_mismatch() { + $this->run('class %T { + public string $test { + set(string $times) => $times." times"; + } + + public function run() { + $this->test= []; + } + }'); + } + + #[Test] + public function initial_value() { + $r= $this->run('class %T { + public $test= "test" { + get => ucfirst($this->test); + } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function by_reference_supports_array_modifications() { + $r= $this->run('class %T { + private $list= []; + public $test { + &get => $this->list; + } + + public function run() { + $this->test[]= "Test"; + return $this->test; + } + }'); + + Assert::equals(['Test'], $r); + } + + #[Test] + public function property_constant() { + $r= $this->run('class %T { + public $test { get => __PROPERTY__; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('test', $r); + } + + #[Test] + public function reflection() { + $t= $this->declare('class %T { + public string $test { + get => $this->test; + set => ucfirst($value); + } + }'); + + Assert::equals('public string $test', $t->property('test')->toString()); + } + + #[Test] + public function abstract_property() { + $t= $this->declare('abstract class %T { + public abstract string $test { get; set; } + }'); + + Assert::equals('public abstract string $test', $t->property('test')->toString()); + } + + #[Test] + public function reflective_get() { + $t= $this->declare('class %T { + public string $test { get => "Test"; } + }'); + + $instance= $t->newInstance(); + Assert::equals('Test', $t->property('test')->get($instance)); + } + + #[Test] + public function reflective_set() { + $t= $this->declare('class %T { + public string $test { set => ucfirst($value); } + }'); + + $instance= $t->newInstance(); + $t->property('test')->set($instance, 'test'); + Assert::equals('Test', $instance->test); + } + + #[Test] + public function interface_hook() { + $t= $this->declare('interface %T { + public string $test { get; } + }'); + + Assert::equals('public abstract string $test', $t->property('test')->toString()); + } + + #[Test] + public function line_number_in_thrown_expression() { + $r= $this->run('use lang\\IllegalArgumentException; class %T { + public $test { + set($name) { + if (strlen($name) > 10) throw new IllegalArgumentException("Too long"); + $this->test= $name; + } + } + + public function run() { + try { + $this->test= "this is too long"; + return null; + } catch (IllegalArgumentException $expected) { + return $expected->getLine(); + } + } + }'); + + Assert::equals(4, $r); + } + + #[Test] + public function accessing_private_property() { + $r= $this->run('class %T { + private string $test { get => "Test"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function accessing_protected_property() { + $r= $this->run('class %T { + protected string $test { get => "Test"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+test/')] + public function accessing_private_property_from_outside() { + $r= $this->run('class %T { + private string $test { get => "Test"; } + + public function run() { + return $this; + } + }'); + + $r->test; + } + + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+test/')] + public function accessing_protected_property_from_outside() { + $r= $this->run('class %T { + protected string $test { get => "Test"; } + + public function run() { + return $this; + } + }'); + + $r->test; + } + + #[Test] + public function accessing_private_property_reflectively() { + $t= $this->declare('class %T { + private string $test { get => "Test"; } + }'); + + Assert::equals('Test', $t->property('test')->get($t->newInstance(), $t)); + } + + #[Test] + public function accessing_protected_property_reflectively() { + $t= $this->declare('class %T { + protected string $test { get => "Test"; } + }'); + + Assert::equals('Test', $t->property('test')->get($t->newInstance(), $t)); + } + + #[Test] + public function get_parent_hook() { + $base= $this->declare('class %T { + public string $test { get => "Test"; } + }'); + $r= $this->run('class %T extends '.$base->literal().' { + public string $test { get => parent::$test::get()."!"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test!', $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php index eb40b901..09d481d7 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyTypesTest.class.php @@ -1,37 +1,40 @@ type('class { + $t= $this->declare('class %T { private int $test; }'); - $this->assertEquals('int', $t->getField('test')->getTypeName()); + Assert::equals(Primitive::$INT, $t->property('test')->constraint()->type()); } - #[@test] + #[Test] public function self_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private static self $instance; }'); - $this->assertEquals('self', $t->getField('instance')->getTypeName()); + Assert::equals($t->class(), $t->property('instance')->constraint()->type()); } - #[@test] + #[Test] public function interface_type() { - $t= $this->type('class { + $t= $this->declare('class %T { private \\lang\\Value $value; }'); - $this->assertEquals('lang.Value', $t->getField('value')->getTypeName()); + Assert::equals(XPClass::forName('lang.Value'), $t->property('value')->constraint()->type()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php new file mode 100755 index 00000000..9b9830fb --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ReadonlyTest.class.php @@ -0,0 +1,196 @@ +declare('readonly class %T { + public int $fixture; + }'); + + Assert::equals( + 'public readonly protected(set) int $fixture', + $t->property('fixture')->toString() + ); + } + + #[Test] + public function property_declaration() { + $t= $this->declare('class %T { + public readonly int $fixture; + }'); + + Assert::equals( + 'public readonly protected(set) int $fixture', + $t->property('fixture')->toString() + ); + } + + #[Test] + public function class_with_constructor_argument_promotion() { + $t= $this->declare('readonly class %T { + public function __construct(public string $fixture) { } + }'); + + Assert::equals( + 'public readonly protected(set) string $fixture', + $t->property('fixture')->toString() + ); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function property_defined_with_constructor_argument_promotion() { + $t= $this->declare('class %T { + public function __construct(public readonly string $fixture) { } + }'); + + Assert::equals( + 'public readonly protected(set) string $fixture', + $t->property('fixture')->toString() + ); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test, Values(from: 'modifiers')] + public function reading_from_class($modifiers) { + $t= $this->declare('class %T { + public function __construct('.$modifiers.' readonly string $fixture) { } + + public function run() { return $this->fixture; } + }'); + Assert::equals('Test', $t->newInstance('Test')->run()); + } + + #[Test] + public function reading_public_from_outside() { + $t= $this->declare('class %T { + public function __construct(public readonly string $fixture) { } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function reading_protected_from_subclass() { + $t= $this->declare('class %T { + public function __construct(protected readonly string $fixture) { } + }'); + $i= newinstance($t->name(), ['Test'], [ + 'run' => function() { return $this->fixture; } + ]); + Assert::equals('Test', $i->run()); + } + + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+fixture/')] + public function cannot_read_protected() { + $t= $this->declare('class %T { + public function __construct(protected readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture; + } + + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+fixture/')] + public function cannot_write_protected() { + $t= $this->declare('class %T { + public function __construct(protected readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture= 'Modified'; + } + + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+fixture/')] + public function cannot_read_private() { + $t= $this->declare('class %T { + public function __construct(private readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture; + } + + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+fixture/')] + public function cannot_write_private() { + $t= $this->declare('class %T { + public function __construct(private readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture= 'Modified'; + } + + #[Test] + public function assigning_inside_constructor() { + $t= $this->declare('class %T { + public readonly string $fixture; + public function __construct($fixture) { $this->fixture= $fixture; } + }'); + Assert::equals('Test', $t->newInstance('Test')->fixture); + } + + #[Test] + public function can_be_assigned_via_reflection() { + $t= $this->declare('class %T { + public readonly string $fixture; + }'); + $i= $t->newInstance(); + $t->property('fixture')->set($i, 'Test'); + + Assert::equals('Test', $i->fixture); + } + + #[Test, Expect(class: Error::class, message: '/Cannot (initialize readonly|modify protected\(set\) readonly) property .+fixture/')] + public function cannot_initialize_from_outside() { + $t= $this->declare('class %T { + public readonly string $fixture; + }'); + $t->newInstance()->fixture= 'Test'; + } + + #[Test, Expect(class: Error::class, message: '/Cannot modify readonly property .+fixture/')] + public function cannot_be_set_after_initialization() { + $t= $this->declare('class %T { + public function __construct(public readonly string $fixture) { } + }'); + $t->newInstance('Test')->fixture= 'Modified'; + } + + #[Test, Ignore('Until proper error handling facilities exist')] + public function cannot_have_an_initial_value() { + $this->declare('class %T { + public readonly string $fixture= "Test"; + }'); + } + + #[Test] + public function reading_dynamic_members_from_readonly_classes_causes_warning() { + $t= $this->declare('readonly class %T { }'); + Assert::null($t->newInstance()->fixture); + \xp::gc(); + } + + #[Test, Expect(class: Error::class, message: '/Cannot create dynamic property .+fixture/')] + public function cannot_write_dynamic_members_from_readonly_classes() { + $t= $this->declare('readonly class %T { }'); + $t->newInstance()->fixture= true; + } + + #[Test, Ignore('Until proper error handling facilities exist')] + public function readonly_classes_cannot_have_static_members() { + $this->declare('readonly class %T { + public static $test; + }'); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php new file mode 100755 index 00000000..e03fa8ff --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/ReflectionTest.class.php @@ -0,0 +1,74 @@ +name()); + } + + #[Test] + public function rewrites_xp_enums() { + $spec= ['kind' => 'class', 'extends' => [Enum::class], 'implements' => [], 'use' => []]; + $t= ClassLoader::defineType('ReflectionTestXPEnum', $spec, '{ + public static $ONE; + + public static $EMPTY= null; + + static function __static() { + self::$ONE= new self(); + } + }'); + + $reflect= new Reflection($t->literal()); + Assert::true($reflect->rewriteEnumCase('ONE')); + Assert::false($reflect->rewriteEnumCase('EMPTY')); + } + + #[Test, Runtime(php: '<8.1')] + public function rewrites_simulated_unit_enums() { + $spec= ['kind' => 'class', 'extends' => null, 'implements' => [\UnitEnum::class], 'use' => []]; + $t= ClassLoader::defineType('ReflectionTestSimulatedEnum', $spec, '{ + public static $ONE; + + public static $EMPTY= null; + + static function __static() { + self::$ONE= new self(); + } + }'); + + $reflect= new Reflection($t->literal()); + Assert::true($reflect->rewriteEnumCase('ONE')); + Assert::false($reflect->rewriteEnumCase('EMPTY')); + } + + #[Test, Runtime(php: '>=8.1')] + public function does_not_rewrite_native_enums() { + $spec= ['kind' => 'enum', 'extends' => null, 'implements' => [], 'use' => []]; + $t= ClassLoader::defineType('ReflectionTestNativeEnum', $spec, '{ + case ONE; + + const EMPTY= null; + }'); + + $reflect= new Reflection($t->literal()); + Assert::false($reflect->rewriteEnumCase('ONE')); + Assert::false($reflect->rewriteEnumCase('EMPTY')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php index b1132cd7..1b2fb1c5 100755 --- a/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ReturnTest.class.php @@ -1,36 +1,38 @@ run('class { + $r= $this->run('class %T { public function run() { return "Test"; } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] public function return_member() { - $r= $this->run('class { + $r= $this->run('class %T { private $member= "Test"; public function run() { return $this->member; } }'); - $this->assertEquals('Test', $r); + Assert::equals('Test', $r); } - #[@test] + #[Test] public function return_without_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return; } }'); - $this->assertEquals(null, $r); + Assert::equals(null, $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php index f5e8d30c..8ad1c0fe 100755 --- a/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ScalarsTest.class.php @@ -1,45 +1,41 @@ assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); + } + + #[Test, Values([['0b0', 0], ['0b10', 2], ['0B10', 2]])] + public function binary_numbers($literal, $result) { + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); + } + + #[Test, Values([['0x0', 0], ['0xff', 255], ['0xFF', 255], ['0XFF', 255]])] + public function hexadecimal_numbers($literal, $result) { + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); + } + + #[Test, Values([['0755', 493], ['0o16', 14], ['0O16', 14]])] + public function octal_numbers($literal, $result) { + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } - #[@test, @values([ - # ['135_99', 13599], - # ['107_925_284.88', 107925284.88], - # ['0xCAFE_F00D', 3405705229], - # ['0b0101_1111', 95], - # ['0137_041', 48673], - #])] + #[Test, Values([['135_99', 13599], ['107_925_284.88', 107925284.88], ['0xCAFE_F00D', 3405705229], ['0b0101_1111', 95], ['0137_041', 48673],])] public function numeric_literal_separator($literal, $result) { - $this->assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } - #[@test, @values([ - # ['""', ''], - # ['"Test"', 'Test'], - #])] + #[Test, Values([['""', ''], ['"Test"', 'Test'],])] public function strings($literal, $result) { - $this->assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } - #[@test, @values([ - # ['true', true], - # ['false', false], - # ['null', null], - #])] + #[Test, Values([['true', true], ['false', false], ['null', null],])] public function constants($literal, $result) { - $this->assertEquals($result, $this->run('class { public function run() { return '.$literal.'; } }')); + Assert::equals($result, $this->run('class %T { public function run() { return '.$literal.'; } }')); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/Spinner.class.php b/src/test/php/lang/ast/unittest/emit/Spinner.class.php new file mode 100755 index 00000000..2cdeb64f --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/Spinner.class.php @@ -0,0 +1,7 @@ +method('run')->invoke($t->newInstance(), $args); + } + + #[Test] + public function constant_static() { + $t= $this->declare('class %T { + public function run() { + static $i= 0; + + return $i++; + } + }'); + + Assert::equals(0, $this->apply($t)); + Assert::equals(1, $this->apply($t)); + Assert::equals(2, $this->apply($t)); + } + + #[Test] + public function initialization_to_new() { + $t= $this->declare('use util\\{Date, Dates}; class %T { + public function run() { + static $t= new Date(0); + + return $t= Dates::add($t, 86400); + } + }'); + + Assert::equals(new Date(86400), $this->apply($t)); + Assert::equals(new Date(86400 * 2), $this->apply($t)); + } + + #[Test] + public function initialization_to_parameter() { + $t= $this->declare('class %T { + public function run($initial) { + static $t= $initial; + + return $t; + } + }'); + + $instance= $t->newInstance(); + Assert::equals('initial', $this->apply($t, 'initial')); + Assert::equals('initial', $this->apply($t, 'changed')); + } + + #[Test] + public function initialization_when_throwing() { + $t= $this->declare('use lang\\IllegalArgumentException; class %T { + public function run($initial) { + static $t= $initial ?? throw new IllegalArgumentException("May not be null"); + + return $t; + } + }'); + + // This does not initialize the static + Assert::throws(InvocationFailed::class, function() use($t) { + $this->apply($t, null); + }); + + // This does + Assert::equals('initial', $this->apply($t, 'initial')); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php b/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php new file mode 100755 index 00000000..d6b3836d --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/SyntaxErrorsTest.class.php @@ -0,0 +1,23 @@ +emit('$greeting= hello() world()'); + } + + #[Test, Expect(class: Errors::class, message: '/Unexpected :/')] + public function unexpected_colon() { + $this->emit('$greeting= hello();:'); + } + + #[Test, Expect(class: IllegalStateException::class, message: '/Unexpected operator =/')] + public function operator() { + $this->emit('=;'); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php index a735848a..b0d7b133 100755 --- a/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TernaryTest.class.php @@ -1,11 +1,14 @@ "OK", false => "Fail"])] + #[Test, Values([[true, 'OK'], [false, 'Fail']])] public function ternary($value, $result) { - $this->assertEquals($result, $this->run( - 'class { + Assert::equals($result, $this->run( + 'class %T { public function run($value) { return $value ? "OK" : "Fail"; } @@ -14,10 +17,22 @@ public function run($value) { )); } - #[@test, @values(map= ["OK" => "OK", null => "Fail"])] + #[Test, Values([[true, MODIFIER_PUBLIC], [false, MODIFIER_PRIVATE]])] + public function ternary_constants_goto_label_ambiguity($value, $result) { + Assert::equals($result, $this->run( + 'class %T { + public function run($value) { + return $value ? MODIFIER_PUBLIC : MODIFIER_PRIVATE; + } + }', + $value + )); + } + + #[Test, Values([['OK', 'OK'], [null, 'Fail']])] public function short_ternary($value, $result) { - $this->assertEquals($result, $this->run( - 'class { + Assert::equals($result, $this->run( + 'class %T { public function run($value) { return $value ?: "Fail"; } @@ -26,10 +41,10 @@ public function run($value) { )); } - #[@test, @values([[['OK']], [[]]])] + #[Test, Values([[['OK']], [[]]])] public function null_coalesce($value) { - $this->assertEquals('OK', $this->run( - 'class { + Assert::equals('OK', $this->run( + 'class %T { public function run($value) { return $value[0] ?? "OK"; } @@ -37,4 +52,65 @@ public function run($value) { $value )); } + + #[Test, Values(eval: '[["."], [new Path(".")]]')] + public function with_instanceof($value) { + Assert::equals(new Path('.'), $this->run( + 'use io\\Path; class %T { + public function run($value) { + return $value instanceof Path ? $value : new Path($value); + } + }', + $value + )); + } + + #[Test, Values(['"te" . "st"', '1 + 2', '1 || 0', '2 ?? 1', '1 | 2', '4 === strlen("Test")'])] + public function precedence($lhs) { + Assert::equals('OK', $this->run( + 'class %T { + public function run() { + return '.$lhs.'? "OK" : "Error"; + } + }' + )); + } + + #[Test] + public function assignment_precedence() { + Assert::equals(['OK', 'OK'], $this->run( + 'class %T { + public function run() { + return [$a= 1 ? "OK" : "Error", $a]; + } + }' + )); + } + + #[Test] + public function yield_precedence() { + Assert::equals(['OK', null], iterator_to_array($this->run( + 'class %T { + public function run() { + yield (yield 1 ? "OK" : "Error"); + } + }' + ))); + } + + /** @see https://www.php.net/manual/en/language.operators.comparison.php#language.operators.comparison.ternary */ + #[Test] + public function chaining_short_ternaries() { + Assert::equals([1, 2, 3], $this->run( + 'class %T { + public function run() { + return [ + 0 ?: 1 ?: 2 ?: 3, + 0 ?: 0 ?: 2 ?: 3, + 0 ?: 0 ?: 0 ?: 3, + ]; + } + }' + )); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php index 0bd202be..03c70fcd 100755 --- a/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TrailingCommasTest.class.php @@ -1,52 +1,54 @@ run('class { public function run() { return ["test", ]; } }'); - $this->assertEquals(['test'], $r); + $r= $this->run('class %T { public function run() { return ["test", ]; } }'); + Assert::equals(['test'], $r); } - #[@test] + #[Test] public function in_map() { - $r= $this->run('class { public function run() { return ["test" => true, ]; } }'); - $this->assertEquals(['test' => true], $r); + $r= $this->run('class %T { public function run() { return ["test" => true, ]; } }'); + Assert::equals(['test' => true], $r); } - #[@test] + #[Test] public function in_function_call() { - $r= $this->run('class { public function run() { return sprintf("Hello %s", "test", ); } }'); - $this->assertEquals('Hello test', $r); + $r= $this->run('class %T { public function run() { return sprintf("Hello %s", "test", ); } }'); + Assert::equals('Hello test', $r); } - #[@test] + #[Test] public function in_parameter_list() { - $r= $this->run('class { public function run($a, ) { return $a; } }', 'Test'); - $this->assertEquals('Test', $r); + $r= $this->run('class %T { public function run($a, ) { return $a; } }', 'Test'); + Assert::equals('Test', $r); } - #[@test] + #[Test] public function in_isset() { - $r= $this->run('class { public function run() { return isset($a, ); } }'); - $this->assertEquals(false, $r); + $r= $this->run('class %T { public function run() { return isset($a, ); } }'); + Assert::equals(false, $r); } - #[@test] + #[Test] public function in_list() { - $r= $this->run('class { public function run() { list($a, )= [1, 2]; return $a; } }'); - $this->assertEquals(1, $r); + $r= $this->run('class %T { public function run() { list($a, )= [1, 2]; return $a; } }'); + Assert::equals(1, $r); } - #[@test] + #[Test] public function in_short_list() { - $r= $this->run('class { public function run() { [$a, ]= [1, 2]; return $a; } }'); - $this->assertEquals(1, $r); + $r= $this->run('class %T { public function run() { [$a, ]= [1, 2]; return $a; } }'); + Assert::equals(1, $r); } - #[@test] + #[Test] public function in_namespace_group() { - $r= $this->run('use lang\\{Type, }; class { public function run() { return Type::$ARRAY->getName(); } }'); - $this->assertEquals('array', $r); + $r= $this->run('use lang\\{Type, }; class %T { public function run() { return Type::$ARRAY->getName(); } }'); + Assert::equals('array', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php index 084de57c..a4c156e8 100755 --- a/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TraitsTest.class.php @@ -1,6 +1,8 @@ type('class { use \lang\ast\unittest\emit\Loading; }'); - $this->assertEquals([new XPClass(Loading::class)], $t->getTraits()); + $t= $this->declare('class %T { use \lang\ast\unittest\emit\Loading; }'); + Assert::equals([Reflection::type(Loading::class)], $t->traits()); } - #[@test] + #[Test] public function trait_method_is_part_of_type() { - $t= $this->type('class { use \lang\ast\unittest\emit\Loading; }'); - $this->assertTrue($t->hasMethod('loaded')); + $t= $this->declare('class %T { use \lang\ast\unittest\emit\Loading; }'); + Assert::notEquals(null, $t->method('loaded')); } - #[@test] + #[Test] public function trait_is_resolved() { - $t= $this->type('use lang\ast\unittest\emit\Loading; class { use Loading; }'); - $this->assertEquals([new XPClass(Loading::class)], $t->getTraits()); + $t= $this->declare('use lang\ast\unittest\emit\Loading; class %T { use Loading; }'); + Assert::equals([Reflection::type(Loading::class)], $t->traits()); } - #[@test] + #[Test] public function trait_method_aliased() { - $t= $this->type('use lang\ast\unittest\emit\Loading; class { + $t= $this->declare('use lang\ast\unittest\emit\Loading; class %T { use Loading { loaded as hasLoaded; } }'); - $this->assertTrue($t->hasMethod('hasLoaded')); + Assert::notEquals(null, $t->method('hasLoaded')); } - #[@test] + #[Test] public function trait_method_aliased_qualified() { - $t= $this->type('use lang\ast\unittest\emit\Loading; class { + $t= $this->declare('use lang\ast\unittest\emit\Loading; class %T { use Loading { Loading::loaded as hasLoaded; } }'); - $this->assertTrue($t->hasMethod('hasLoaded')); + Assert::notEquals(null, $t->method('hasLoaded')); + } + + #[Test] + public function trait_method_insteadof() { + $t= $this->declare('use lang\ast\unittest\emit\{Loading, Spinner}; class %T { + use Loading, Spinner { + Spinner::loaded as noLongerSpinning; + Loading::loaded insteadof Spinner; + } + }'); + $instance= $t->newInstance(); + Assert::equals('Loaded', $t->method('loaded')->invoke($instance)); + Assert::equals('Not spinning', $t->method('noLongerSpinning')->invoke($instance)); + } + + #[Test, Runtime(php: '>=8.2.0-dev')] + public function can_have_constants() { + $t= $this->declare('trait %T { const FIXTURE = 1; }'); + Assert::equals(1, $t->constant('FIXTURE')->value()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php new file mode 100755 index 00000000..6aadac81 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/TransformationsTest.class.php @@ -0,0 +1,104 @@ +transform('class', function($codegen, $class) { + if ($class->annotation('Repr')) { + $class->declare(new Method( + ['public'], + 'toString', + new Signature([], new IsLiteral('string')), + [new Code('return "T@".\util\Objects::stringOf(get_object_vars($this))')] + )); + } + return $class; + }); + $this->transform('class', function($codegen, $class) { + if ($class->annotation('Getters')) { + foreach ($class->properties() as $property) { + $class->declare(new Method( + ['public'], + $property->name, + new Signature([], $property->type), + [new Code('return $this->'.$property->name)] + )); + } + } + return $class; + }); + } + + #[Test] + public function leaves_class_without_annotations() { + $t= $this->declare('class %T { + private int $id; + + public function __construct(int $id) { + $this->id= $id; + } + }'); + Assert::equals(null, $t->method('id')); + } + + #[Test] + public function generates_string_representation() { + $t= $this->declare('#[Repr] class %T { + private int $id; + private string $name; + + public function __construct(int $id, string $name) { + $this->id= $id; + $this->name= $name; + } + }'); + Assert::notEquals(null, $t->method('toString')); + Assert::equals( + "T@[\n id => 1\n name => \"Test\"\n]", + $t->method('toString')->invoke($t->newInstance(1, 'Test')) + ); + } + + #[Test, Values([['id', 1], ['name', 'Test']])] + public function generates_accessor($name, $expected) { + $t= $this->declare('#[Getters] class %T { + private int $id; + private string $name; + + public function __construct(int $id, string $name) { + $this->id= $id; + $this->name= $name; + } + }'); + Assert::notEquals(null, $t->method($name)); + Assert::equals($expected, $t->method($name)->invoke($t->newInstance(1, 'Test'))); + } + + #[Test] + public function generates_both() { + $t= $this->declare('#[Repr, Getters] class %T { + private int $id; + private string $name; + + public function __construct(int $id, string $name) { + $this->id= $id; + $this->name= $name; + } + }'); + + $instance= $t->newInstance(1, 'Test'); + Assert::equals(1, $t->method('id')->invoke($instance)); + Assert::equals('Test', $t->method('name')->invoke($instance)); + Assert::equals( + "T@[\n id => 1\n name => \"Test\"\n]", + $t->method('toString')->invoke($instance) + ); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php new file mode 100755 index 00000000..5257ed34 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/TypeCheckingTest.class.php @@ -0,0 +1,77 @@ +run('class %T { + public function run(int $param) { + return $param; + } + }', $param); + Assert::equals((int)$param, $r); + } + + #[Test, Values(['', 'Test', 1, 1.5, true, false])] + public function string_type_coerces($param) { + $r= $this->run('class %T { + public function run(string $param) { + return $param; + } + }', $param); + Assert::equals((string)$param, $r); + } + + #[Test, Values([null, [[]]]), Expect(Error::class)] + public function string_type_rejects($param) { + $this->run('class %T { + public function run(string $param) { + return $param; + } + }', $param); + } + + #[Test, Values(eval: '[[new Date(), new Bytes("test"), null]]')] + public function nullable_value_type_accepts($param) { + $r= $this->run('use lang\\Value; class %T { + public function run(?Value $param) { + return $param; + } + }', $param); + Assert::equals($param, $r); + } + + #[Test, Values(eval: '[["test", new Bytes("test")]]')] + public function union_type_accepts($param) { + $r= $this->run('class %T { + public function run(string|Bytes $param) { + return $param; + } + }', $param); + Assert::equals($param, $r); + } + + #[Test, Values(eval: '[[new Bytes("test")]]')] + public function intersection_type_accepts($param) { + $r= $this->run('use lang\\Value; class %T { + public function run(Value&Traversable $param) { + return $param; + } + }', $param); + Assert::equals($param, $r); + } + + #[Test, Values([[[]], [['a', 'b', 'c']]])] + public function string_array_type_accepts($param) { + $r= $this->run('class %T { + public function run(array $param) { + return $param; + } + }', $param); + Assert::equals((array)$param, $r); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php index a8a8cea6..18fd7495 100755 --- a/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/TypeDeclarationTest.class.php @@ -1,94 +1,109 @@ type($kind.' { }'); - $this->assertEquals( - ['const' => [], 'fields' => [], 'methods' => []], - ['const' => $t->getConstants(), 'fields' => $t->getFields(), 'methods' => $t->getMethods()] + $t= $this->declare($kind.' %T { }'); + Assert::equals( + ['constants' => [], 'properties' => [], 'methods' => []], + [ + 'constants' => [...$t->constants()], + 'properties' => [...$t->properties()], + 'methods' => [...$t->methods()], + ] ); } - #[@test] + #[Test] public function abstract_class_type() { - $this->assertTrue(Modifiers::isAbstract($this->type('abstract class { }')->getModifiers())); + Assert::true($this->declare('abstract class %T { }')->modifiers()->isAbstract()); } - #[@test] + #[Test] public function final_class_type() { - $this->assertTrue(Modifiers::isFinal($this->type('final class { }')->getModifiers())); + Assert::true($this->declare('final class %T { }')->modifiers()->isFinal()); } - #[@test] + #[Test] public function class_without_parent() { - $this->assertNull($this->type('class { }')->getParentclass()); + Assert::null($this->declare('class %T { }')->parent()); } - #[@test] + #[Test] public function class_with_parent() { - $this->assertEquals( - new XPClass(EmittingTest::class), - $this->type('class extends \\lang\\ast\\unittest\\emit\\EmittingTest { }')->getParentclass() + Assert::equals( + EmittingTest::class, + $this->declare('class %T extends \\lang\\ast\\unittest\\emit\\EmittingTest { }')->parent()->literal() ); } - #[@test] + #[Test] public function trait_type() { - $this->assertTrue($this->type('trait { }')->isTrait()); + Assert::equals(Kind::$TRAIT, $this->declare('trait %T { }')->kind()); } - #[@test] + #[Test] + public function trait_type_with_method() { + Assert::equals( + Kind::$TRAIT, + $this->declare('trait %T { public function name() { return "Test"; }}')->kind() + ); + } + + #[Test] public function interface_type() { - $this->assertTrue($this->type('interface { }')->isInterface()); + Assert::equals(Kind::$INTERFACE, $this->declare('interface %T { }')->kind()); } - #[@test, @values(['public', 'private', 'protected'])] + #[Test] + public function interface_type_with_method() { + Assert::equals( + Kind::$INTERFACE, + $this->declare('interface %T { public function name(); }')->kind() + ); + } + + #[Test, Values(['public', 'private', 'protected'])] public function constant($modifiers) { - $c= $this->type('class { '.$modifiers.' const test = 1; }')->getConstant('test'); - $this->assertEquals(1, $c); + $c= $this->declare('class %T { '.$modifiers.' const test = 1; }')->constant('test'); + Assert::equals( + ['name' => 'test', 'type' => Type::$VAR, 'modifiers' => $modifiers], + ['name' => $c->name(), 'type' => $c->constraint()->type(), 'modifiers' => $c->modifiers()->names()] + ); } - #[@test, @values([ - # 'public', 'private', 'protected', - # 'public static', 'private static', 'protected static' - #])] - public function field($modifiers) { - $f= $this->type('class { '.$modifiers.' $test; }')->getField('test'); - $n= implode(' ', Modifiers::namesOf($f->getModifiers())); - $this->assertEquals( - ['name' => 'test', 'type' => 'var', 'modifiers' => $modifiers], - ['name' => $f->getName(), 'type' => $f->getTypeName(), 'modifiers' => $n] + #[Test, Values(['public', 'private', 'protected', 'public static', 'private static', 'protected static'])] + public function property($modifiers) { + $p= $this->declare('class %T { '.$modifiers.' $test; }')->property('test'); + Assert::equals( + ['name' => 'test', 'type' => Type::$VAR, 'modifiers' => $modifiers], + ['name' => $p->name(), 'type' => $p->constraint()->type(), 'modifiers' => $p->modifiers()->names()] ); } - #[@test, @values([ - # 'public', 'protected', 'private', - # 'public final', 'protected final', - # 'public static', 'protected static', 'private static' - #])] + #[Test, Values(['public', 'protected', 'private', 'public final', 'protected final', 'public static', 'protected static', 'private static'])] public function method($modifiers) { - $m= $this->type('class { '.$modifiers.' function test() { } }')->getMethod('test'); - $n= implode(' ', Modifiers::namesOf($m->getModifiers())); - $this->assertEquals( - ['name' => 'test', 'type' => 'var', 'modifiers' => $modifiers], - ['name' => $m->getName(), 'type' => $m->getReturnTypeName(), 'modifiers' => $n] + $m= $this->declare('class %T { '.$modifiers.' function test() { } }')->method('test'); + Assert::equals( + ['name' => 'test', 'type' => Type::$VAR, 'modifiers' => $modifiers], + ['name' => $m->name(), 'type' => $m->returns()->type(), 'modifiers' => $m->modifiers()->names()] ); } - #[@test] + #[Test] public function abstract_method() { - $m= $this->type('abstract class { abstract function test(); }')->getMethod('test'); - $this->assertTrue(Modifiers::isAbstract($m->getModifiers())); + $m= $this->declare('abstract class %T { abstract function test(); }')->method('test'); + Assert::true($m->modifiers()->isAbstract()); } - #[@test] + #[Test] public function method_with_keyword() { - $t= $this->type('class { + $t= $this->declare('class %T { private $items; public static function new($items) { @@ -105,6 +120,6 @@ public static function run($values) { return self::new($values)->forEach(function($a) { return $a * 2; }); } }'); - $this->assertEquals([2, 4, 6], $t->getMethod('run')->invoke(null, [[1, 2, 3]])); + Assert::equals([2, 4, 6], $t->method('run')->invoke(null, [[1, 2, 3]])); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php index 184168a2..c03b42e4 100755 --- a/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnicodeEscapesTest.class.php @@ -1,26 +1,28 @@ run('class { + $r= $this->run('class %T { public function run() { return "ma\u{00F1}ana"; } }'); - $this->assertEquals('mañana', $r); + Assert::equals('mañana', $r); } - #[@test] + #[Test] public function emoji() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { return "Smile! \u{1F602}"; } }'); - $this->assertEquals('Smile! 😂', $r); + Assert::equals('Smile! 😂', $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php index 7bd90cc7..f0509784 100755 --- a/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UnionTypesTest.class.php @@ -1,48 +1,121 @@ type('class { + $t= $this->declare('class %T { private int|string $test; }'); - $this->assertEquals( + Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), - $t->getField('test')->getType() + $t->property('test')->constraint()->type() ); } - #[@test] + #[Test] public function parameter_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(int|string $arg) { } }'); - $this->assertEquals( + Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), - $t->getMethod('test')->getParameter(0)->getType() + $t->method('test')->parameter(0)->constraint()->type() ); } - #[@test] + #[Test] public function return_type() { - $t= $this->type('class { + $t= $this->declare('class %T { public function test(): int|string { } }'); - $this->assertEquals( + Assert::equals( new TypeUnion([Primitive::$INT, Primitive::$STRING]), - $t->getMethod('test')->getReturnType() + $t->method('test')->returns()->type() + ); + } + + #[Test] + public function nullable_union_type() { + $t= $this->declare('class %T { + public function test(): int|string|null { } + }'); + + Assert::equals( + new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), + $t->method('test')->returns()->type() + ); + } + + #[Test] + public function nullable_union_type_alternative_syntax() { + $t= $this->declare('class %T { + public function test(): ?(int|string) { } + }'); + + Assert::equals( + new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), + $t->method('test')->returns()->type() + ); + } + + #[Test, Runtime(php: '>=8.0.0-dev')] + public function nullable_union_type_restriction() { + $t= $this->declare('class %T { + public function test(): int|string|null { } + }'); + + Assert::equals( + new Nullable(new TypeUnion([Primitive::$INT, Primitive::$STRING])), + $t->method('test')->returns()->type() + ); + } + + #[Test, Runtime(php: '>=8.0.0-dev')] + public function parameter_type_restriction_with_php8() { + $t= $this->declare('class %T { + public function test(int|string|array $arg) { } + }'); + + Assert::equals( + new TypeUnion([Primitive::$INT, Primitive::$STRING, Type::$ARRAY]), + $t->method('test')->parameter(0)->constraint()->type() + ); + } + + #[Test, Runtime(php: '>=8.0.0-dev')] + public function parameter_function_type_restriction_with_php8() { + $t= $this->declare('class %T { + public function test(): string|(function(): string) { } + }'); + + Assert::equals( + new TypeUnion([Primitive::$STRING, Type::$CALLABLE]), + $t->method('test')->returns()->type() + ); + } + + #[Test, Runtime(php: '>=8.0.0-dev')] + public function return_type_restriction_with_php8() { + $t= $this->declare('class %T { + public function test(): int|string|array { } + }'); + + Assert::equals( + new TypeUnion([Primitive::$INT, Primitive::$STRING, Type::$ARRAY]), + $t->method('test')->returns()->type() ); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php index aac25bf5..5652b3f5 100755 --- a/src/test/php/lang/ast/unittest/emit/UsingTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/UsingTest.class.php @@ -1,5 +1,7 @@ run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { Handle::$called= []; @@ -20,12 +22,12 @@ public function run() { return Handle::$called; } }'); - $this->assertEquals(['read@1', '__dispose@1'], $r); + Assert::equals(['read@1', '__dispose@1'], $r); } - #[@test] + #[Test] public function dispose_called_for_all() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { Handle::$called= []; @@ -36,12 +38,12 @@ public function run() { return Handle::$called; } }'); - $this->assertEquals(['read@1', '__dispose@1', '__dispose@2'], $r); + Assert::equals(['read@1', '__dispose@1', '__dispose@2'], $r); } - #[@test] + #[Test] public function dispose_called_even_when_exceptions_occur() { - $r= $this->run('use lang\{IllegalArgumentException, IllegalStateException}; use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\{IllegalArgumentException, IllegalStateException}; use lang\ast\unittest\emit\Handle; class %T { public function run() { Handle::$called= []; @@ -56,12 +58,12 @@ public function run() { throw new IllegalStateException("No exception caught"); } }'); - $this->assertEquals(['read@1', '__dispose@1'], $r); + Assert::equals(['read@1', '__dispose@1'], $r); } - #[@test] + #[Test] public function supports_closeables() { - $r= $this->run('use lang\ast\unittest\emit\FileInput; class { + $r= $this->run('use lang\ast\unittest\emit\FileInput; class %T { public function run() { FileInput::$open= false; @@ -72,12 +74,12 @@ public function run() { return FileInput::$open; } }'); - $this->assertFalse($r); + Assert::false($r); } - #[@test] + #[Test] public function can_return_from_inside_using() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { private function read() { using ($x= new Handle(1)) { return $x->read(); @@ -90,12 +92,12 @@ public function run() { return ["called" => Handle::$called, "returned" => $returned]; } }'); - $this->assertEquals(['called' => ['read@1', '__dispose@1'], 'returned' => 'test'], $r); + Assert::equals(['called' => ['read@1', '__dispose@1'], 'returned' => 'test'], $r); } - #[@test] + #[Test] public function variable_undefined_after_using() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { using ($x= new Handle(1)) { // NOOP @@ -103,12 +105,12 @@ public function run() { return isset($x); } }'); - $this->assertFalse($r); + Assert::false($r); } - #[@test] + #[Test] public function variable_undefined_after_using_even_if_previously_defined() { - $r= $this->run('use lang\ast\unittest\emit\Handle; class { + $r= $this->run('use lang\ast\unittest\emit\Handle; class %T { public function run() { $x= new Handle(1); using ($x) { @@ -117,6 +119,6 @@ public function run() { return isset($x); } }'); - $this->assertFalse($r); + Assert::false($r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php index 650019dd..f64b6c96 100755 --- a/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/VarargsTest.class.php @@ -1,30 +1,36 @@ run('class { - private function format(string $format, ... $args) ==> vsprintf($format, $args); + $r= $this->run('class %T { + private function format(string $format, ... $args) { + return vsprintf($format, $args); + } public function run() { return $this->format("Hello %s", "Test"); } }'); - $this->assertEquals('Hello Test', $r); + Assert::equals('Hello Test', $r); } - #[@test] + #[Test] public function list_of() { - $r= $this->run('class { - private function listOf(string... $args) ==> $args; + $r= $this->run('class %T { + private function listOf(string... $args) { + return $args; + } public function run() { return $this->listOf("Hello", "Test"); } }'); - $this->assertEquals(['Hello', 'Test'], $r); + Assert::equals(['Hello', 'Test'], $r); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php index 59f9dec9..40e2917b 100755 --- a/src/test/php/lang/ast/unittest/emit/YieldTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/YieldTest.class.php @@ -1,55 +1,57 @@ run('class { + $r= $this->run('class %T { public function run() { yield; yield; } }'); - $this->assertEquals([null, null], iterator_to_array($r)); + Assert::equals([null, null], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_values() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield 1; yield 2; yield 3; } }'); - $this->assertEquals([1, 2, 3], iterator_to_array($r)); + Assert::equals([1, 2, 3], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_keys_and_values() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield "color" => "orange"; yield "price" => 12.99; } }'); - $this->assertEquals(['color' => 'orange', 'price' => 12.99], iterator_to_array($r)); + Assert::equals(['color' => 'orange', 'price' => 12.99], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_from_array() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield from [1, 2, 3]; } }'); - $this->assertEquals([1, 2, 3], iterator_to_array($r)); + Assert::equals([1, 2, 3], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_from_generator() { - $r= $this->run('class { + $r= $this->run('class %T { private function values() { yield 1; yield 2; @@ -60,18 +62,37 @@ public function run() { yield from $this->values(); } }'); - $this->assertEquals([1, 2, 3], iterator_to_array($r)); + Assert::equals([1, 2, 3], iterator_to_array($r)); } - #[@test] + #[Test] public function yield_from_and_yield() { - $r= $this->run('class { + $r= $this->run('class %T { public function run() { yield 1; yield from [2, 3]; yield 4; } }'); - $this->assertEquals([1, 2, 3, 4], iterator_to_array($r, false)); + Assert::equals([1, 2, 3, 4], iterator_to_array($r, false)); + } + + #[Test] + public function yield_send() { + $r= $this->run('class %T { + public function run() { + while ($line= yield) { + echo $line, "\n"; + } + } + }'); + + ob_start(); + + $r->send('Hello'); + $r->send('World'); + $r->send(null); + + Assert::equals("Hello\nWorld\n", ob_get_clean()); } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index 7bf5c48f..604d6fcf 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -1,76 +1,224 @@ exists() || $folder->create(); - FileUtil::setContents(new File($folder, $type.'.php'), sprintf($source, $namespace)); - $cl= ClassLoader::registerPath($folder->path); + $names= []; + foreach ($structure as $type => $code) { + Files::write(new File($folder, $type.'.php'), sprintf($code, $namespace)); + $names[$type]= $namespace.'.'.$type; + } + + return [$folder, $names]; + } - $loader= CompilingClassLoader::instanceFor(self::$runtime); + /** + * Sets us compiling class loader with a given type and source code, then + * executes callback. + * + * @param [:string] $source + * @param function(lang.IClassLoader, string): var $callback + * @return var + */ + private function compile($source, $callback) { + [$folder, $names]= $this->tempFolder($source); + + $cl= ClassLoader::registerPath($folder->path); try { - return $loader->loadClass($namespace.'.'.$type); + return $callback(CompilingClassLoader::instanceFor(self::$runtime), $names, $cl); } finally { ClassLoader::removeLoader($cl); $folder->unlink(); } } - #[@test] + #[Test] public function can_create() { CompilingClassLoader::instanceFor(self::$runtime); } - #[@test] + #[Test, Values(['7.4.0', '7.4.12', '8.0.0', '8.1.0', '8.2.0', '8.3.0', '8.4.0'])] + public function supports_php($version) { + CompilingClassLoader::instanceFor('php:'.$version); + } + + #[Test] + public function string_representation() { + Assert::equals('CompilingCL', CompilingClassLoader::instanceFor('php:8.0.0')->toString()); + } + + #[Test] + public function hashcode() { + Assert::equals('CPHP80+lang.ast.emit.php.XpMeta', CompilingClassLoader::instanceFor('php:8.0.0')->hashCode()); + } + + #[Test] public function load_class() { - $this->assertEquals('Tests', $this->load('Tests', 'getSimpleName()); + Assert::equals('Tests', $this->compile( + ['Tests' => ' $loader->loadClass($types['Tests'])->getSimpleName() + )); + } + + #[Test] + public function compare() { + $cl= CompilingClassLoader::instanceFor(self::$runtime); + + Assert::equals(0, $cl->compareTo($cl), 'equals itself'); + Assert::equals(1, $cl->compareTo(null), 'does not equal null'); + } + + #[Test] + public function instanced_for_augmented() { + Assert::equals( + 'PHP80+lang.ast.emit.php.XpMeta', + CompilingClassLoader::instanceFor('php:8.0.0+lang.ast.emit.php.XpMeta')->instanceId() + ); + } + + #[Test] + public function package_contents() { + $contents= $this->compile( + ['Tests' => ' $loader->packageContents(strstr($types['Tests'], '.', true)) + ); + Assert::equals(['Tests'.\xp::CLASS_FILE_EXT], $contents); + } + + #[Test] + public function load_dependencies() { + $source= [ + 'Child' => ' ' ' 'compile($source, fn($loader, $types) => $loader->loadClass($types['Child']))); + $n= fn($class) => $class->declaredName(); + Assert::equals( + ['Child', 'Base', ['Impl'], ['Feature']], + [$n($t), $n($t->parent()), array_map($n, $t->interfaces()), array_map($n, $t->traits())] + ); + } + + #[Test] + public function load_class_bytes() { + $code= $this->compile( + ['Tests' => ' $loader->loadClassBytes($types['Tests']) + ); + Assert::matches('/<\?php .+ class Tests/', $code); + } + + #[Test] + public function load_uri() { + $class= $this->compile( + ['Tests' => ' $loader->loadUri($temp->path.strtr($types['Tests'], '.', DIRECTORY_SEPARATOR).CompilingClassLoader::EXTENSION) + ); + Assert::equals('Tests', $class->getSimpleName()); } - #[@test, @expect( - # class= ClassFormatException::class, - # withMessage= '/Syntax error in .+Errors.php, line 2: Expected ";", have "Syntax"/' - #)] + #[Test, Expect(class: ClassFormatException::class, message: '/Compiler error: Expected "type name", have .+/')] public function load_class_with_syntax_errors() { - $this->load('Errors', ""); + $this->compile(['Errors' => " $loader->loadClass($types['Errors'])); + } + + #[Test, Expect(class: ClassFormatException::class, message: '/Compiler error: Class .+ not found/')] + public function load_class_with_non_existant_parent() { + $code= "compile(['Orphan' => $code], fn($loader, $types) => $loader->loadClass($types['Orphan'])); } - #[@test] + #[Test] public function triggered_errors_filename() { - $t= $this->load('Triggers', ' 'compile($source, fn($loader, $types) => $loader->loadClass($types['Triggers'])); $t->newInstance()->trigger(); - $this->assertNotEquals(false, strpos( + Assert::notEquals(false, strpos( preg_replace('#^.+://#', '', key(\xp::$errors)), strtr($t->getName(), '.', DIRECTORY_SEPARATOR).'.php' )); \xp::gc(); } -} + + #[Test] + public function does_not_provide_non_existant_uri() { + Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesUri('NotFound.php')); + } + + #[Test] + public function does_not_provide_non_existant_resource() { + Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesResource('notfound.md')); + } + + #[Test] + public function does_not_provide_non_existant_package() { + Assert::false(CompilingClassLoader::instanceFor(self::$runtime)->providesPackage('notfound')); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function loading_non_existant_uri() { + CompilingClassLoader::instanceFor(self::$runtime)->loadUri('NotFound.php'); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function loading_non_existant_class() { + CompilingClassLoader::instanceFor(self::$runtime)->loadClass('NotFound'); + } + + #[Test, Expect(ClassNotFoundException::class)] + public function loading_non_existant_class_bytes() { + CompilingClassLoader::instanceFor(self::$runtime)->loadClassBytes('NotFound'); + } + + #[Test, Expect(ElementNotFoundException::class)] + public function loading_non_existant_resource() { + CompilingClassLoader::instanceFor(self::$runtime)->getResource('notfound.md'); + } + + #[Test, Expect(ElementNotFoundException::class)] + public function loading_non_existant_resource_as_stream() { + CompilingClassLoader::instanceFor(self::$runtime)->getResourceAsStream('notfound.md'); + } + + #[Test] + public function ignores_autoload_and_xp_entry() { + [$folder, $names]= $this->tempFolder([ + '__xp' => ' ' 'path); + try { + Assert::equals( + ['Fixture.class.php'], + CompilingClassLoader::instanceFor(self::$runtime)->packageContents($folder->dirname) + ); + } finally { + ClassLoader::removeLoader($cl); + $folder->unlink(); + } + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php b/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php deleted file mode 100755 index c8b7fde5..00000000 --- a/src/test/php/lang/ast/unittest/parse/BlocksTest.class.php +++ /dev/null @@ -1,18 +0,0 @@ -block= [['(' => [['(name)' => 'block'], []]]]; - } - - #[@test] - public function static_variable() { - $this->assertNodes( - [['{' => $this->block]], - $this->parse('{ block(); }') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php b/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php deleted file mode 100755 index 38b9c202..00000000 --- a/src/test/php/lang/ast/unittest/parse/CompactFunctionsTest.class.php +++ /dev/null @@ -1,23 +0,0 @@ -assertNodes( - [['function' => ['a', [[], null], [['==>' => ['null' => 'null']]]]]], - $this->parse('function a() ==> null;') - ); - } - - #[@test] - public function short_method() { - $block= [['==>' => ['true' => 'true']]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], null], [], $block, null]] - ], [], null]]], - $this->parse('class A { public function a() ==> true; }') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php b/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php deleted file mode 100755 index 9310f75a..00000000 --- a/src/test/php/lang/ast/unittest/parse/ConditionalTest.class.php +++ /dev/null @@ -1,84 +0,0 @@ -blocks= [ - [['(' => [['(name)' => 'action1'], []]]], - [['(' => [['(name)' => 'action2'], []]]] - ]; - } - - #[@test] - public function plain_if() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], null]]], - $this->parse('if ($condition) { action1(); }') - ); - } - - #[@test] - public function if_with_else() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], $this->blocks[1]]]], - $this->parse('if ($condition) { action1(); } else { action2(); }') - ); - } - - #[@test] - public function shortcut_if() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], null]]], - $this->parse('if ($condition) action1();') - ); - } - - #[@test] - public function shortcut_if_else() { - $this->assertNodes( - [['if' => [['(variable)' => 'condition'], $this->blocks[0], $this->blocks[1]]]], - $this->parse('if ($condition) action1(); else action2();') - ); - } - - #[@test] - public function empty_switch() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], []]]], - $this->parse('switch ($condition) { }') - ); - } - - #[@test] - public function switch_with_one_case() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], [ - [['(literal)' => '1'], $this->blocks[0]], - ]]]], - $this->parse('switch ($condition) { case 1: action1(); }') - ); - } - - #[@test] - public function switch_with_two_cases() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], [ - [['(literal)' => '1'], $this->blocks[0]], - [['(literal)' => '2'], $this->blocks[1]], - ]]]], - $this->parse('switch ($condition) { case 1: action1(); case 2: action2(); }') - ); - } - - #[@test] - public function switch_with_default() { - $this->assertNodes( - [['switch' => [['(variable)' => 'condition'], [ - [null, $this->blocks[0]] - ]]]], - $this->parse('switch ($condition) { default: action1(); }') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php b/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php deleted file mode 100755 index b6680079..00000000 --- a/src/test/php/lang/ast/unittest/parse/ErrorsTest.class.php +++ /dev/null @@ -1,78 +0,0 @@ -fail('No exception raised', null, Error::class); - } catch (Error $expected) { - $this->assertEquals($message, $expected->getMessage()); - } - } - - #[@test] - public function missing_semicolon() { - $this->assertError( - 'Expected ";", have "b"', - $this->parse('$a= 1 $b= 1;') - ); - } - - #[@test] - public function unclosed_brace_in_arguments() { - $this->assertError( - 'Expected ") or ,", have "(end)" in argument list', - $this->parse('call(') - ); - } - - #[@test] - public function unclosed_brace_in_parameters() { - $this->assertError( - 'Expected ",", have "(end)" in parameter list', - $this->parse('function($a') - ); - } - - #[@test] - public function unclosed_type() { - $this->assertError( - 'Expected "a type, modifier, property, annotation, method or }", have "-" in type body', - $this->parse('class T { -') - ); - } - - #[@test] - public function missing_comma_in_implements() { - $this->assertError( - 'Expected ", or {", have "B" in interfaces list', - $this->parse('class A implements I B') - ); - } - - #[@test] - public function missing_comma_in_interface_parents() { - $this->assertError( - 'Expected ", or {", have "B" in interface parents', - $this->parse('interface I extends A B') - ); - } - - #[@test] - public function unclosed_annotation() { - $this->assertError( - 'Expected ", or >>", have "(end)" in annotation', - $this->parse('<assertNodes( - [['function' => ['a', [[], null], []]]], - $this->parse('function a() { }') - ); - } - - #[@test] - public function two_functions() { - $this->assertNodes( - [['function' => ['a', [[], null], []]], ['function' => ['b', [[], null], []]]], - $this->parse('function a() { } function b() { }') - ); - } - - #[@test, @values(['param', 'protected'])] - public function with_parameter($name) { - $this->assertNodes( - [['function' => ['a', [[[$name, false, null, false, null, null, []]], null], []]]], - $this->parse('function a($'.$name.') { }') - ); - } - - #[@test] - public function with_reference_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', true, null, false, null, null, []]], null], []]]], - $this->parse('function a(&$param) { }') - ); - } - - #[@test] - public function dangling_comma_in_parameter_lists() { - $this->assertNodes( - [['function' => ['a', [[['param', false, null, false, null, null, []]], null], []]]], - $this->parse('function a($param, ) { }') - ); - } - - #[@test] - public function with_typed_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, new Type('string'), false, null, null, []]], null], []]]], - $this->parse('function a(string $param) { }') - ); - } - - #[@test] - public function with_nullable_typed_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, new Type('?string'), false, null, null, []]], null], []]]], - $this->parse('function a(?string $param) { }') - ); - } - - #[@test] - public function with_variadic_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, null, true, null, null, []]], null], []]]], - $this->parse('function a(... $param) { }') - ); - } - - #[@test] - public function with_optional_parameter() { - $this->assertNodes( - [['function' => ['a', [[['param', false, null, false, null, ['null' => 'null'], []]], null], []]]], - $this->parse('function a($param= null) { }') - ); - } - - #[@test] - public function with_parameter_named_function() { - $this->assertNodes( - [['function' => ['a', [[['function', false, null, false, null, null, []]], null], []]]], - $this->parse('function a($function) { }') - ); - } - - #[@test] - public function with_typed_parameter_named_function() { - $this->assertNodes( - [['function' => ['a', [[['function', false, new FunctionType([], new Type('void')), false, null, null, []]], null], []]]], - $this->parse('function a((function(): void) $function) { }') - ); - } - - #[@test] - public function with_return_type() { - $this->assertNodes( - [['function' => ['a', [[], new Type('void')], []]]], - $this->parse('function a(): void { }') - ); - } - - #[@test] - public function with_nullable_return() { - $this->assertNodes( - [['function' => ['a', [[], new Type('?string')], []]]], - $this->parse('function a(): ?string { }') - ); - } - - #[@test] - public function default_closure() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], null], null, [['return' => $block]]]]], - $this->parse('function() { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_use_by_value() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], null], ['$a', '$b'], [['return' => $block]]]]], - $this->parse('function() use($a, $b) { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_use_by_reference() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], null], ['$a', '&$b'], [['return' => $block]]]]], - $this->parse('function() use($a, &$b) { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_return_type() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], new Type('int')], null, [['return' => $block]]]]], - $this->parse('function(): int { return $a + 1; };') - ); - } - - #[@test] - public function default_closure_with_nullable_return_type() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => [[[], new Type('?int')], null, [['return' => $block]]]]], - $this->parse('function(): ?int { return $a + 1; };') - ); - } - - #[@test] - public function generator() { - $statement= ['yield' => [null, null]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield; }') - ); - } - - #[@test] - public function generator_with_value() { - $statement= ['yield' => [null, ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield 1; }') - ); - } - - #[@test] - public function generator_with_key_and_value() { - $statement= ['yield' => [['(literal)' => '"number"'], ['(literal)' => '1']]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield "number" => 1; }') - ); - } - - #[@test] - public function generator_delegation() { - $statement= ['yield' => ['[' => []]]; - $this->assertNodes( - [['function' => ['a', [[], null], [$statement]]]], - $this->parse('function a() { yield from []; }') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php b/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php deleted file mode 100755 index a0c2fb44..00000000 --- a/src/test/php/lang/ast/unittest/parse/InvokeTest.class.php +++ /dev/null @@ -1,49 +0,0 @@ -assertNodes( - [['(' => [['(name)' => 'test'], []]]], - $this->parse('test();') - ); - } - - #[@test] - public function invoke_method() { - $this->assertNodes( - [['(' => [['->' => [['(variable)' => 'this'], ['(name)' => 'test']]], []]]], - $this->parse('$this->test();') - ); - } - - #[@test] - public function invoke_function_with_argument() { - $this->assertNodes( - [['(' => [['(name)' => 'test'], [['(literal)' => '1']]]]], - $this->parse('test(1);') - ); - } - - #[@test] - public function invoke_function_with_arguments() { - $this->assertNodes( - [['(' => [['(name)' => 'test'], [['(literal)' => '1'], ['(literal)' => '2']]]]], - $this->parse('test(1, 2);') - ); - } - - #[@test] - public function invoke_function_with_dangling_comma() { - $this->assertNodes( - [['(' => [['(name)' => 'test'], [['(literal)' => '1'], ['(literal)' => '2']]]]], - $this->parse('test(1, 2, );') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php b/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php deleted file mode 100755 index 234ab6ae..00000000 --- a/src/test/php/lang/ast/unittest/parse/LambdasTest.class.php +++ /dev/null @@ -1,33 +0,0 @@ - [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['(' => [[[['a', false, null, false, null, null, []]], null], $block]]], - $this->parse('($a) ==> $a + 1;') - ); - } - - #[@test] - public function short_closure_as_arg() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['(' => [['(name)' => 'exec'], [ - ['(' => [[[['a', false, null, false, null, null, []]], null], $block]] - ]]]], - $this->parse('exec(($a) ==> $a + 1);') - ); - } - - #[@test] - public function short_closure_with_braces() { - $block= ['+' => [['(variable)' => 'a'], '+', ['(literal)' => '1']]]; - $this->assertNodes( - [['(' => [[[['a', false, null, false, null, null, []]], null], [['return' => $block]]]]], - $this->parse('($a) ==> { return $a + 1; };') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php b/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php deleted file mode 100755 index 95acdb9b..00000000 --- a/src/test/php/lang/ast/unittest/parse/LiteralsTest.class.php +++ /dev/null @@ -1,86 +0,0 @@ -assertNodes([['(literal)' => $input]], $this->parse($input.';')); - } - - #[@test, @values(['0x00', '0x01', '0xFF', '0xff'])] - public function hexadecimal($input) { - $this->assertNodes([['(literal)' => $input]], $this->parse($input.';')); - } - - #[@test, @values(['00', '01', '010', '0777'])] - public function octal($input) { - $this->assertNodes([['(literal)' => $input]], $this->parse($input.';')); - } - - #[@test, @values(['1.0', '1.5'])] - public function decimal($input) { - $this->assertNodes([['(literal)' => $input]], $this->parse($input.';')); - } - - #[@test] - public function bool_true() { - $this->assertNodes([['true' => 'true']], $this->parse('true;')); - } - - #[@test] - public function bool_false() { - $this->assertNodes([['false' => 'false']], $this->parse('false;')); - } - - #[@test] - public function null() { - $this->assertNodes([['null' => 'null']], $this->parse('null;')); - } - - #[@test] - public function empty_string() { - $this->assertNodes([['(literal)' => '""']], $this->parse('"";')); - } - - #[@test] - public function non_empty_string() { - $this->assertNodes([['(literal)' => '"Test"']], $this->parse('"Test";')); - } - - #[@test] - public function empty_array() { - $this->assertNodes([['[' => []]], $this->parse('[];')); - } - - #[@test] - public function int_array() { - $this->assertNodes( - [['[' => [[null, ['(literal)' => '1']], [null, ['(literal)' => '2']]]]], - $this->parse('[1, 2];') - ); - } - - #[@test] - public function key_value_map() { - $this->assertNodes( - [['[' => [[['(literal)' => '"key"'], ['(literal)' => '"value"']]]]], - $this->parse('["key" => "value"];') - ); - } - - #[@test] - public function dangling_comma_in_array() { - $this->assertNodes( - [['[' => [[null, ['(literal)' => '1']]]]], - $this->parse('[1, ];') - ); - } - - #[@test] - public function dangling_comma_in_key_value_map() { - $this->assertNodes( - [['[' => [[['(literal)' => '"key"'], ['(literal)' => '"value"']]]]], - $this->parse('["key" => "value", ];') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php b/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php deleted file mode 100755 index bffd3db2..00000000 --- a/src/test/php/lang/ast/unittest/parse/LoopsTest.class.php +++ /dev/null @@ -1,134 +0,0 @@ -block= ['(' => [['(name)' => 'loop'], []]]; - } - - #[@test] - public function foreach_value() { - $this->assertNodes( - [['foreach' => [ - ['(variable)' => 'iterable'], - null, - ['(variable)' => 'value'], - [$this->block] - ]]], - $this->parse('foreach ($iterable as $value) { loop(); }') - ); - } - - #[@test] - public function foreach_key_value() { - $this->assertNodes( - [['foreach' => [ - ['(variable)' => 'iterable'], - ['(variable)' => 'key'], - ['(variable)' => 'value'], - [$this->block] - ]]], - $this->parse('foreach ($iterable as $key => $value) { loop(); }') - ); - } - - #[@test] - public function foreach_value_without_curly_braces() { - $this->assertNodes( - [['foreach' => [ - ['(variable)' => 'iterable'], - null, - ['(variable)' => 'value'], - [$this->block] - ]]], - $this->parse('foreach ($iterable as $value) loop();') - ); - } - - #[@test] - public function for_loop() { - $this->assertNodes( - [['for' => [ - [['=' => [['(variable)' => 'i'], '=', ['(literal)' => '0']]]], - [['<' => [['(variable)' => 'i'], '<', ['(literal)' => '10']]]], - [['++' => [['(variable)' => 'i'], '++']]], - [$this->block] - ]]], - $this->parse('for ($i= 0; $i < 10; $i++) { loop(); }') - ); - } - - #[@test] - public function while_loop() { - $this->assertNodes( - [['while' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('while ($continue) { loop(); }') - ); - } - - #[@test] - public function while_loop_without_curly_braces() { - $this->assertNodes( - [['while' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('while ($continue) loop();') - ); - } - - #[@test] - public function do_loop() { - $this->assertNodes( - [['do' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('do { loop(); } while ($continue);') - ); - } - - #[@test] - public function do_loop_without_curly_braces() { - $this->assertNodes( - [['do' => [['(variable)' => 'continue'], [$this->block]]]], - $this->parse('do loop(); while ($continue);') - ); - } - - #[@test] - public function break_statement() { - $this->assertNodes( - [['break' => null]], - $this->parse('break;') - ); - } - - #[@test] - public function break_statement_with_level() { - $this->assertNodes( - [['break' => ['(literal)' => '2']]], - $this->parse('break 2;') - ); - } - - #[@test] - public function continue_statement() { - $this->assertNodes( - [['continue' => null]], - $this->parse('continue;') - ); - } - - #[@test] - public function continue_statement_with_level() { - $this->assertNodes( - [['continue' => ['(literal)' => '2']]], - $this->parse('continue 2;') - ); - } - - #[@test] - public function goto_statement() { - $this->assertNodes( - [['(name)' => 'start'], $this->block, ['goto' => 'start']], - $this->parse('start: loop(); goto start;') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php b/src/test/php/lang/ast/unittest/parse/MembersTest.class.php deleted file mode 100755 index 3f2b55a6..00000000 --- a/src/test/php/lang/ast/unittest/parse/MembersTest.class.php +++ /dev/null @@ -1,224 +0,0 @@ -assertNodes( - [['class' => ['\\A', [], null, [], ['$a' => ['(variable)' => ['a', ['private'], null, null, [], null]]], [], null]]], - $this->parse('class A { private $a; }') - ); - } - - #[@test] - public function private_instance_properties() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - '$a' => ['(variable)' => ['a', ['private'], null, null, [], null]], - '$b' => ['(variable)' => ['b', ['private'], null, null, [], null]], - ], [], null]]], - $this->parse('class A { private $a, $b; }') - ); - } - - #[@test] - public function private_instance_method() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['private'], [[], null], [], [], null]] - ], [], null]]], - $this->parse('class A { private function a() { } }') - ); - } - - #[@test] - public function private_static_method() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['private', 'static'], [[], null], [], [], null]] - ], [], null]]], - $this->parse('class A { private static function a() { } }') - ); - } - - #[@test] - public function class_constant() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], ['T' => ['const' => ['T', [], ['(literal)' => '1'], null]]], [], null]]], - $this->parse('class A { const T = 1; }') - ); - } - - #[@test] - public function class_constants() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'T' => ['const' => ['T', [], ['(literal)' => '1'], null]], - 'S' => ['const' => ['S', [], ['(literal)' => '2'], null]] - ], [], null]]], - $this->parse('class A { const T = 1, S = 2; }') - ); - } - - #[@test] - public function private_class_constant() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], ['T' => ['const' => ['T', ['private'], ['(literal)' => '1'], null]]], [], null]]], - $this->parse('class A { private const T = 1; }') - ); - } - - #[@test] - public function method_with_return_type() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], new Type('void')], [], [], null]] - ], [], null]]], - $this->parse('class A { public function a(): void { } }') - ); - } - - #[@test] - public function method_with_annotation() { - $annotations= ['test' => null]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], null], $annotations, [], null]] - ], [], null]]], - $this->parse('class A { <> public function a() { } }') - ); - } - - #[@test] - public function method_with_annotations() { - $annotations= ['test' => null, 'ignore' => ['(literal)' => '"Not implemented"']]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], [ - 'a()' => ['function' => ['a', ['public'], [[], null], $annotations, [], null]] - ], [], null]]], - $this->parse('class A { <> public function a() { } }') - ); - } - - #[@test] - public function instance_property_access() { - $this->assertNodes( - [['->' => [['(variable)' => 'a'], ['(name)' => 'member']]]], - $this->parse('$a->member;') - ); - } - - #[@test] - public function dynamic_instance_property_access_via_variable() { - $this->assertNodes( - [['->' => [['(variable)' => 'a'], ['(variable)' => 'member']]]], - $this->parse('$a->{$member};') - ); - } - - #[@test] - public function dynamic_instance_property_access_via_expression() { - $this->assertNodes( - [['->' => [['(variable)' => 'a'], ['(' => [ - ['->' => [['(variable)' => 'field'], ['(name)' => 'get']]], - [['(variable)' => 'instance']] - ]]]]], - $this->parse('$a->{$field->get($instance)};') - ); - } - - #[@test] - public function static_property_access() { - $this->assertNodes( - [['::' => ['\\A', ['(variable)' => 'member']]]], - $this->parse('A::$member;') - ); - } - - #[@test, @values(['self', 'parent', 'static'])] - public function scope_resolution($scope) { - $this->assertNodes( - [['::' => [$scope, ['class' => 'class']]]], - $this->parse($scope.'::class;') - ); - } - - #[@test] - public function class_resolution() { - $this->assertNodes( - [['::' => ['\\A', ['class' => 'class']]]], - $this->parse('A::class;') - ); - } - - #[@test] - public function instance_method_invocation() { - $this->assertNodes( - [['(' => [['->' => [['(variable)' => 'a'], ['(name)' => 'member']]], [['(literal)' => '1']]]]], - $this->parse('$a->member(1);') - ); - } - - #[@test] - public function static_method_invocation() { - $this->assertNodes( - [['(' => [['::' => ['\\A', ['(name)' => 'member']]], [['(literal)' => '1']]]]], - $this->parse('A::member(1);') - ); - } - - #[@test] - public function typed_property() { - $decl= ['$a' => ['(variable)' => ['a', ['private'], null, new Type('string'), [], null]]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { private string $a; }') - ); - } - - #[@test] - public function typed_property_with_value() { - $decl= ['$a' => ['(variable)' => ['a', ['private'], ['(literal)' => '"test"'], new Type('string'), [], null]]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { private string $a = "test"; }') - ); - } - - #[@test] - public function typed_properties() { - $decl= [ - '$a' => ['(variable)' => ['a', ['private'], null, new Type('string'), [], null]], - '$b' => ['(variable)' => ['b', ['private'], null, new Type('string'), [], null]], - '$c' => ['(variable)' => ['c', ['private'], null, new Type('int'), [], null]], - ]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { private string $a, $b, int $c; }') - ); - } - - #[@test] - public function typed_constant() { - $decl= ['T' => ['const' => ['T', [], ['(literal)' => '1'], new Type('int')]]]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { const int T = 1; }') - ); - } - - #[@test] - public function typed_constants() { - $decl= [ - 'T' => ['const' => ['T', [], ['(literal)' => '1'], new Type('int')]], - 'S' => ['const' => ['S', [], ['(literal)' => '2'], new Type('int')]], - 'I' => ['const' => ['I', [], ['(literal)' => '"i"'], new Type('string')]], - ]; - $this->assertNodes( - [['class' => ['\\A', [], null, [], $decl, [], null]]], - $this->parse('class A { const int T = 1, S = 2, string I = "i"; }') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php deleted file mode 100755 index c6448322..00000000 --- a/src/test/php/lang/ast/unittest/parse/NamespacesTest.class.php +++ /dev/null @@ -1,37 +0,0 @@ -assertNodes([['namespace' => 'test']], $this->parse('namespace test;')); - } - - #[@test] - public function compound_namespace() { - $this->assertNodes([['namespace' => 'lang\\ast']], $this->parse('namespace lang\ast;')); - } - - #[@test] - public function use_statement() { - $this->assertNodes([['use' => ['lang\ast\Parse' => null]]], $this->parse('use lang\ast\Parse;')); - } - - #[@test] - public function use_with_alias() { - $this->assertNodes([['use' => ['lang\ast\Parse' => 'P']]], $this->parse('use lang\ast\Parse as P;')); - } - - #[@test] - public function grouped_use_statement() { - $this->assertNodes( - [['use' => ['lang\ast\Parse' => null, 'lang\ast\Emitter' => null]]], - $this->parse('use lang\ast\{Parse, Emitter};') - ); - } - - #[@test] - public function grouped_use_with_alias() { - $this->assertNodes([['use' => ['lang\ast\Parse' => 'P']]], $this->parse('use lang\ast\{Parse as P};')); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php b/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php deleted file mode 100755 index 8e94def3..00000000 --- a/src/test/php/lang/ast/unittest/parse/OperatorTest.class.php +++ /dev/null @@ -1,178 +0,0 @@ ->', '<<' - #])] - public function binary($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator, ['(variable)' => 'b']]]], - $this->parse('$a '.$operator.' $b;') - ); - } - - #[@test] - public function ternary() { - $this->assertNodes( - [['?' => [['(variable)' => 'a'], ['(literal)' => '1'], ['(literal)' => '2']]]], - $this->parse('$a ? 1 : 2;') - ); - } - - #[@test, @values([ - # '==', '!=', - # '===', '!==', - # '>', '>=', '<=', '<', '<=>' - #])] - public function comparison($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator, ['(variable)' => 'b']]]], - $this->parse('$a '.$operator.' $b;') - ); - } - - #[@test, @values(['++', '--'])] - public function suffix($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator]]], - $this->parse('$a'.$operator.';') - ); - } - - #[@test, @values(['!', '~', '-', '+', '++', '--'])] - public function prefix($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator]]], - $this->parse(''.$operator.'$a;') - ); - } - - #[@test, @values([ - # '=', - # '+=', '-=', '*=', '/=', '.=', '**=', - # '&=', '|=', '^=', - # '>>=', '<<=' - #])] - public function assignment($operator) { - $this->assertNodes( - [[$operator => [['(variable)' => 'a'], $operator, ['(variable)' => 'b']]]], - $this->parse('$a '.$operator.' $b;') - ); - } - - #[@test] - public function assignment_to_offset() { - $this->assertNodes( - [['=' => [['[' => [['(variable)' => 'a'], ['(literal)' => '0']]], '=', ['(literal)' => '1']]]], - $this->parse('$a[0]= 1;') - ); - } - - #[@test] - public function destructuring_assignment() { - $this->assertNodes( - [['=' => [['[' => [[null, ['(variable)' => 'a']], [null, ['(variable)' => 'b']]]], '=', ['(' => [['(name)' => 'result'], []]]]]], - $this->parse('[$a, $b]= result();') - ); - } - - #[@test] - public function comparison_to_assignment() { - $this->assertNodes( - [['===' => [['(literal)' => '1'], '===', ['(' => ['=' => [['(variable)' => 'a'], '=', ['(literal)' => '1']]]]]]], - $this->parse('1 === ($a= 1);') - ); - } - - #[@test] - public function append_array() { - $this->assertNodes( - [['=' => [['[' => [['(variable)' => 'a'], null]], '=', ['(literal)' => '1']]]], - $this->parse('$a[]= 1;') - ); - } - - #[@test] - public function clone_expression() { - $this->assertNodes( - [['clone' => [['(variable)' => 'a'], 'clone']]], - $this->parse('clone $a;') - ); - } - - #[@test] - public function error_suppression() { - $this->assertNodes( - [['@' => [['(variable)' => 'a'], '@']]], - $this->parse('@$a;') - ); - } - - #[@test] - public function reference() { - $this->assertNodes( - [['&' => [['(variable)' => 'a'], '&']]], - $this->parse('&$a;') - ); - } - - #[@test] - public function new_type() { - $this->assertNodes( - [['new' => ['\\T', []]]], - $this->parse('new T();') - ); - } - - #[@test] - public function new_type_with_args() { - $this->assertNodes( - [['new' => ['\\T', [['(variable)' => 'a'], ['(variable)' => 'b']]]]], - $this->parse('new T($a, $b);') - ); - } - - #[@test] - public function new_anonymous_extends() { - $this->assertNodes( - [['new' => [[null, [], '\\T', [], [], [], null], []]]], - $this->parse('new class() extends T { };') - ); - } - - #[@test] - public function new_anonymous_implements() { - $this->assertNodes( - [['new' => [[null, [], null, ['\\A', '\\B'], [], [], null], []]]], - $this->parse('new class() implements A, B { };') - ); - } - - #[@test] - public function precedence_of_object_operator() { - $this->assertNodes( - [['.' => [ - ['->' => [['(variable)' => 'this'], ['(name)' => 'a']]], - '.', - ['(literal)' => '"test"'] - ]]], - $this->parse('$this->a."test";') - ); - } - - #[@test] - public function precedence_of_scope_resolution_operator() { - $this->assertNodes( - [['.' => [ - ['::' => ['self', ['class' => 'class']]], - '.', - ['(literal)' => '"test"'] - ]]], - $this->parse('self::class."test";') - ); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php b/src/test/php/lang/ast/unittest/parse/ParseTest.class.php deleted file mode 100755 index e6b3790c..00000000 --- a/src/test/php/lang/ast/unittest/parse/ParseTest.class.php +++ /dev/null @@ -1,62 +0,0 @@ -symbol->id => $this->value($arg->value)]; - } else if ($arg instanceof Value) { - $r= []; - foreach ((array)$arg as $key => $value) { - $r[]= $this->value($value); - } - return $r; - } else if (is_array($arg)) { - $r= []; - foreach ($arg as $key => $value) { - $r[$key]= $this->value($value); - } - return $r; - } else { - return $arg; - } - } - - /** - * Parse code, returning nodes on at a time - * - * @param string $code - * @return iterable - */ - protected function parse($code) { - return (new Parse(new Tokens(new StringTokenizer($code)), $this->getName()))->execute(); - } - - /** - * Assertion helper - * - * @param [:var][] $expected - * @param iterable $nodes - * @throws unittest.AssertionFailedError - * @return void - */ - protected function assertNodes($expected, $nodes) { - $actual= []; - foreach ($nodes as $node) { - $actual[]= $this->value($node); - } - $this->assertEquals($expected, $actual); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php b/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php deleted file mode 100755 index 46ab6bd7..00000000 --- a/src/test/php/lang/ast/unittest/parse/StartTokensTest.class.php +++ /dev/null @@ -1,20 +0,0 @@ -assertNodes( - [[' 'php'], ['namespace' => 'test']], - $this->parse('assertNodes( - [[' 'hh'], ['namespace' => 'test']], - $this->parse('assertNodes( - [['class' => ['\\A', [], null, [], [], [], null]]], - $this->parse('class A { }') - ); - } - - #[@test] - public function class_with_parent() { - $this->assertNodes( - [['class' => ['\\A', [], '\\B', [], [], [], null]]], - $this->parse('class A extends B { }') - ); - } - - #[@test] - public function class_with_interface() { - $this->assertNodes( - [['class' => ['\\A', [], null, ['\\C'], [], [], null]]], - $this->parse('class A implements C { }') - ); - } - - #[@test] - public function class_with_interfaces() { - $this->assertNodes( - [['class' => ['\\A', [], null, ['\\C', '\\D'], [], [], null]]], - $this->parse('class A implements C, D { }') - ); - } - - #[@test] - public function abstract_class() { - $this->assertNodes( - [['abstract' => ['\\A', ['abstract'], null, [], [], [], null]]], - $this->parse('abstract class A { }') - ); - } - - #[@test] - public function final_class() { - $this->assertNodes( - [['final' => ['\\A', ['final'], null, [], [], [], null]]], - $this->parse('final class A { }') - ); - } - - #[@test] - public function empty_interface() { - $this->assertNodes( - [['interface' => ['\\A', [], [], [], [], null]]], - $this->parse('interface A { }') - ); - } - - #[@test] - public function interface_with_parent() { - $this->assertNodes( - [['interface' => ['\\A', [], ['\\B'], [], [], null]]], - $this->parse('interface A extends B { }') - ); - } - - #[@test] - public function interface_with_parents() { - $this->assertNodes( - [['interface' => ['\\A', [], ['\\B', '\\C'], [], [], null]]], - $this->parse('interface A extends B, C { }') - ); - } - - #[@test] - public function empty_trait() { - $this->assertNodes( - [['trait' => ['\\A', [], [], [], null]]], - $this->parse('trait A { }') - ); - } - - #[@test] - public function class_with_trait() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [['use' => [['\\B'], []]]], [], null]]], - $this->parse('class A { use B; }') - ); - } - - #[@test] - public function class_with_multiple_traits() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [['use' => [['\\B'], []]], ['use' => [['\\C'], []]]], [], null]]], - $this->parse('class A { use B; use C; }') - ); - } - - #[@test] - public function class_with_comma_separated_traits() { - $this->assertNodes( - [['class' => ['\\A', [], null, [], [['use' => [['\\B', '\\C'], []]]], [], null]]], - $this->parse('class A { use B, C; }') - ); - } - - #[@test] - public function class_in_namespace() { - $this->assertNodes( - [['namespace' => 'test'], ['class' => ['\\test\\A', [], null, [], [], [], null]]], - $this->parse('namespace test; class A { }') - ); - } - - #[@test, @expect(class= Error::class, withMessage= 'Cannot redeclare method b()')] - public function cannot_redeclare_method() { - iterator_to_array($this->parse('class A { public function b() { } public function b() { }}')); - } - - #[@test, @expect(class= Error::class, withMessage= 'Cannot redeclare property $b')] - public function cannot_redeclare_property() { - iterator_to_array($this->parse('class A { public $b; private $b; }')); - } - - #[@test, @expect(class= Error::class, withMessage= 'Cannot redeclare constant B')] - public function cannot_redeclare_constant() { - iterator_to_array($this->parse('class A { const B = 1; const B = 3; }')); - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php b/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php deleted file mode 100755 index 82f402b7..00000000 --- a/src/test/php/lang/ast/unittest/parse/VariablesTest.class.php +++ /dev/null @@ -1,44 +0,0 @@ -assertNodes( - [['(variable)' => $name]], - $this->parse('$'.$name.';') - ); - } - - #[@test] - public function static_variable() { - $this->assertNodes( - [['static' => ['v' => null]]], - $this->parse('static $v;') - ); - } - - #[@test] - public function static_variable_with_initialization() { - $this->assertNodes( - [['static' => ['id' => ['(literal)' => '0']]]], - $this->parse('static $id= 0;') - ); - } - - #[@test] - public function array_offset() { - $this->assertNodes( - [['[' => [['(variable)' => 'a'], ['(literal)' => '0']]]], - $this->parse('$a[0];') - ); - } - - #[@test] - public function string_offset() { - $this->assertNodes( - [['{' => [['(variable)' => 'a'], ['(literal)' => '0']]]], - $this->parse('$a{0};') - ); - } -} \ No newline at end of file