Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve AST tests & tools #4873

Merged
merged 1 commit into from
Apr 27, 2021
Merged

improve AST tests & tools #4873

merged 1 commit into from
Apr 27, 2021

Conversation

alexlamsl
Copy link
Collaborator

No description provided.

@@ -8,7 +8,11 @@ jobs:
strategy:
fail-fast: false
matrix:
options: [ '-mb braces', '--ie8 -c', '-mc', '--toplevel -mc passes=3,pure_getters,unsafe' ]
options:
- '-p acorn -mb braces -o spidermonkey'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kzc so what I did is to augment --in-situ to handle -o spidermonkey by doing a round-trip, i.e. AST_Node.from_mozilla_ast(uglified.to_mozilla_ast()), which should cover this beyond .ts files which you've done in #4870 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot about -p acorn.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You weren't alone there 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So does --in-situ unconditionally round-trip uglify -> mozilla -> uglify AST now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with --in-situ alone − you need to specify --output spidermonkey

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Okay.

FROM_MOZ_STACK.pop();
return ret;
return node;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (moz) {
if (!HOP(MOZ_TO_ME, moz.type)) {
var s = my_start_token(moz);
js_error("Unsupported type: " + moz.type, s.file, s.line, s.col, s.pos);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that too - the s.file, s.line, s.col, s.pos part is useless for mozilla nodes. Only moz.start and moz.end positions are available. The other use of js_error in this source file may also be wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably should just stick to throw new Error() like most other places in this file...

@@ -15,7 +15,7 @@ minify_in_situ() {
for i in `find $DIRS -type f -name '*.ts' | grep -v '\.d\.ts'`
do
echo "$i"
node_modules/.bin/esbuild --loader=ts --target=node14 < "$i" \
node_modules/.bin/esbuild --loader=ts --target=es2019 < "$i" \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--target=es2020 is needed to produce ChainExpressions, otherwise they are down-leveled to ES2019.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?. aren't supported by uglify-js yet.

Copy link
Collaborator Author

@alexlamsl alexlamsl Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we'll probably need es2021 when we eventually clear that since otherwise private class prorperties won't get through (no idea if esbuild does that, but that's how acorn works...)

Copy link
Contributor

@kzc kzc Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esbuild handles everything in the ES standard, Typescript, and likely to be approved stage 3 proposals already implemented by Chrome, including private class properties. esbuild would have a different set of generated helper functions than what Babel or tsc produce for down-leveling to earlier versions of ES.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ esbuild --version
0.11.14
$ echo 'class C { #p; static #s; }' | esbuild --target=es2020
var _p, _s;
class C {
  constructor() {
    _p.set(this, void 0);
  }
}
_p = new WeakMap();
_s = new WeakMap();
_s.set(C, void 0);
$ echo 'class C { #p; static #s; }' | esbuild --target=esnext
class C {
  #p;
  static #s;
}

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

Using #4873:

$ echo 'const id = warning.id || warning.loc?.file;' | node_modules/.bin/acorn --module --ecma2020 | uglify-js -p spidermonkey -p spidermonkey
ERROR: Unsupported type: ChainExpression

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

Consider round tripping all compress tests through mozilla API as terser does to get better coverage:

https://github.com/terser/terser/blob/7230d7eb65/test/run-tests.js#L117-L126

@alexlamsl
Copy link
Collaborator Author

TBH I'm not that bothered by this functionality − I have absolutely zero use for it, not to mention I never use any features from ECMAScript in the first place.

So if build testing turns out to give me too much of a headache I'll just back those extra tests out, let along adding more.

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

No big deal. I don't use ESTree either. This is just a crossword puzzle like distraction for me. Tools like the Parcel bundler use ESTree. They might find this useful. Terser does not have full ES6+ ESTree support.

@alexlamsl
Copy link
Collaborator Author

To be fair, this exercise did uncover two parser bugs in uglify-js, so there's that 😎

@alexlamsl
Copy link
Collaborator Author

Oh great − those pesky /*#__PURE__*/ would never survive the ESTree conversion, would they? 🙄

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

Oh great − those pesky /#PURE/ would never survive the ESTree conversion, would they? 🙄

Yeah, that had crossed my mind as well. Once it passes through acorn that information is lost.

Acorn does have a comments API but it is not associated with AST nodes, rather it is solely based on position. Nevermind the parentheses issue.

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

For uglify-only purposes the CallExpression and NewExpression ESTree nodes could be augmented with a pure property.

@alexlamsl
Copy link
Collaborator Author

alexlamsl commented Apr 26, 2021

Guess I'll be backing those build test changes back out then − will fix up whatever it found and verifies them locally.

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

Can you put the test changes behind a flag?

Which files are we talking about specifically?

@alexlamsl
Copy link
Collaborator Author

Can you put the test changes behind a flag?

Oh I'm only changing .github/workflows/build.yml back − you can still run the same tests with those flags since I'm keeping the --in-situ upgrade.

Which files are we talking about specifically?

I don't have a comprehensive list yet since I'm still bisecting through the failed jobs, but this one issue is a showstopper for use on GitHub Actions anyway.

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

Sorry, I wasn't clear... I meant which uglify-js source code files or test script files need to be backed out?

@alexlamsl
Copy link
Collaborator Author

I meant which uglify-js source code files or test script files need to be backed out?

Only .github/workflows/build.yml − the functionality stays, we just won't be using it within GitHub Actions.

@alexlamsl
Copy link
Collaborator Author

OT: part of the motivation for working on lib/mozilla-ast.js came from me being impressed with the latest version of acorn still works on Node.js v0.8

@kzc
Copy link
Contributor

kzc commented Apr 26, 2021

This is how rollup uses acorn to assign comments to ESTree nodes. With a little effort pure annotated calls/news could be found...

https://github.com/rollup/rollup/blob/dba6f13132a1d7dac507d5056399d8af0eed6375/src/Graph.ts#L115-L141

https://github.com/rollup/rollup/blob/dba6f13132a1d7dac507d5056399d8af0eed6375/src/utils/pureComments.ts#L65-L69

https://github.com/acornjs/acorn/tree/master/acorn#interface

onComment: If a function is passed for this option, whenever a comment is encountered the function will be called with the following parameters:

block: true if the comment is a block comment, false if it is a line comment.
text: The content of the comment.
start: Character offset of the start of the comment.
end: Character offset of the end of the comment.

@alexlamsl
Copy link
Collaborator Author

@kzc would you mind running ./test/release/rollup-ts.sh with Node.js v14 on macOS and see if you are getting timed-out failures from mocha?

@alexlamsl
Copy link
Collaborator Author

Another reason we won't be testing -p acorn on GitHub Actions:
https://github.com/mishoo/UglifyJS/runs/2441217328?check_suite_focus=true#step:3:86

> uglify-js src -p acorn -mb braces -o spidermonkey
src/esprima-benchmark.test.js
src/bootstrap.js
ERROR: The keyword 'package' is reserved (6:6)
    at Parser.pp$4.raise (/home/runner/work/UglifyJS/UglifyJS/node_modules/acorn/dist/acorn.js:3206:15)
    at Parser.pp$3.checkUnreserved (/home/runner/work/UglifyJS/UglifyJS/node_modules/acorn/dist/acorn.js:3113:12)
    at Parser.pp$3.parseIdent (/home/runner/work/UglifyJS/UglifyJS/node_modules/acorn/dist/acorn.js:3142:12)

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

TIL:

$ echo 'var package = require("./package.json");' | node
$
$ echo '"use strict"; var package = require("./package.json");' | node
[stdin]:1
"use strict"; var package = require("./package.json");
                  ^^^^^^^

SyntaxError: Unexpected strict mode reserved word

acorn --module enables strict mode implicitly.

$ echo 'var package = require("./package.json");' | acorn --ecma2020 --silent && echo PASS || echo FAIL
PASS
$ echo '"use strict"; var package = require("./package.json");' | acorn --ecma2020 --silent && echo PASS || echo FAIL
The keyword 'package' is reserved (1:18)
FAIL
$ echo 'var package = require("./package.json");' | acorn --module --ecma2020 --silent && echo PASS || echo FAIL
The keyword 'package' is reserved (1:4)
FAIL

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

would you mind running ./test/release/rollup-ts.sh with Node.js v14 on macOS and see if you are getting timed-out failures from mocha?

These commands ran successfully to completion on macos with #4873:

$ bash test/release/rollup-ts.sh
$ bash test/release/rollup-ts.sh --toplevel -mc passes=3,unsafe,pure_getters
$ node -v
v14.16.0

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

acorn --module enables strict mode implicitly

It's an ECMAScript thing... NodeJS .mjs suffix files are also implicitly strict:

$ echo 'var package = require("./package.json");' > test.js
$ node test.js
$ echo 'var package = require("./package.json");' > test.mjs
$ node test.mjs
test.mjs:1
var package = require("./package.json");
    ^^^^^^^

SyntaxError: Unexpected strict mode reserved word

@alexlamsl
Copy link
Collaborator Author

I've tried both Node.js v14.16.0 & v14.16.1 − firstly, bash test/release/rollup-ts.sh fails hard:

+ esbuild-wasm@0.8.56
added 579 packages from 407 contributors in 10.676s

73 packages are looking for funding
  run `npm fund` for details

> uglify-js cli
test/release/rollup-ts.sh: line 14: uglify-js: command not found
cli/cli.ts
test/release/rollup-ts.sh: line 19: uglify-js: command not found
fatal error: too many writes on closed pipe

goroutine 1 [running]:
runtime.throw(0x9711b, 0x1e)
	/usr/local/go/src/runtime/panic.go:1117 +0x7 fp=0x4d0d88 sp=0x4d0d60 pc=0x11d60007
os.sigpipe()
	/usr/local/go/src/runtime/os_js.go:132 +0x2 fp=0x4d0da0 sp=0x4d0d88 pc=0x13690002
os.epipecheck(...)
	/usr/local/go/src/os/file_unix.go:195
os.(*File).Write(0x40c020, 0x52e280, 0x276, 0x276, 0x1, 0x0, 0x0)
	/usr/local/go/src/os/file.go:182 +0x45 fp=0x4d0e28 sp=0x4d0da0 pc=0x16230045
github.com/evanw/esbuild/pkg/cli.runImpl(0x40e250, 0x2, 0x2, 0x48800)
	/Users/evan/dev/esbuild/pkg/cli/cli_impl.go:643 +0x94 fp=0x4d1ca8 sp=0x4d0e28 pc=0x1d7e0094
github.com/evanw/esbuild/pkg/cli.Run(...)
	/Users/evan/dev/esbuild/pkg/cli/cli.go:30
main.main.func1(0x0, 0x0, 0x40e250, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0, 0x4d1eb8)
	/Users/evan/dev/esbuild/cmd/esbuild/main.go:242 +0x17 fp=0x4d1e28 sp=0x4d1ca8 pc=0x1d970017
main.main()
	/Users/evan/dev/esbuild/cmd/esbuild/main.go:244 +0x27 fp=0x4d1f88 sp=0x4d1e28 pc=0x1d850027
runtime.main()
	/usr/local/go/src/runtime/proc.go:225 +0x31 fp=0x4d1fe0 sp=0x4d1f88 pc=0x11f20031
runtime.goexit()
	/usr/local/go/src/runtime/asm_wasm.s:427 +0x1 fp=0x4d1fe8 sp=0x4d1fe0 pc=0x13bf0001

Anyway, with ./test/release/rollup-ts.sh I get these mocha time-outs:

  4138 passing (4m)
  6 failing

  1) rollup
       rollup.watch
         watches a file and triggers reruns if necessary:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (UglifyJS/tmp/rollup/test/test.js)
      at listOnTimeout (internal/timers.js:554:17)
      at processTimers (internal/timers.js:497:7)

  2) rollup
       rollup.watch
         passes file events to the watchChange plugin hook once for each change:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (UglifyJS/tmp/rollup/test/test.js)
      at listOnTimeout (internal/timers.js:554:17)
      at processTimers (internal/timers.js:497:7)

  3) rollup
       rollup.watch
         passes change parameter to the watchChange plugin hook:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (UglifyJS/tmp/rollup/test/test.js)
      at listOnTimeout (internal/timers.js:554:17)
      at processTimers (internal/timers.js:497:7)

  4) rollup
       rollup.watch
         watches a file in code-splitting mode with an input object:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (UglifyJS/tmp/rollup/test/test.js)
      at listOnTimeout (internal/timers.js:554:17)
      at processTimers (internal/timers.js:497:7)

  5) rollup
       rollup.watch
         ignores files that are specified in options.watch.exclude, if given:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (UglifyJS/tmp/rollup/test/test.js)
      at listOnTimeout (internal/timers.js:554:17)
      at processTimers (internal/timers.js:497:7)

  6) rollup
       rollup.watch
         skips filesystem writes when configured:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (UglifyJS/tmp/rollup/test/test.js)
      at listOnTimeout (internal/timers.js:554:17)
      at processTimers (internal/timers.js:497:7)

@alexlamsl alexlamsl merged commit 97bd56b into mishoo:master Apr 27, 2021
@alexlamsl alexlamsl deleted the output branch April 27, 2021 00:53
@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

I've tried both Node.js v14.16.0 & v14.16.1 − firstly, bash test/release/rollup-ts.sh fails hard:

Did it ever work on your mac? What version of macos are you running?

test/release/rollup-ts.sh: line 14: uglify-js: command not found

Clearly the uglify-js alias is not working in your version of bash - try using a bash $variable in the script instead.

This is my local version:

bash --version
GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin13)
Copyright (C) 2007 Free Software Foundation, Inc.

goroutine 1 [running]:
runtime.throw(0x9711b, 0x1e)
/usr/local/go/src/runtime/panic.go:1117 +0x7 fp=0x4d0d88 sp=0x4d0d60 pc=0x11d60007

I'm running mac OSX 10.9.5 with a macports legacy dylib installed in order to run esbuild and the go runtime. But this error is something I've never encountered.

Wait...

os.sigpipe()
/usr/local/go/src/runtime/os_js.go:132 +0x2 fp=0x4d0da0 sp=0x4d0d88 pc=0x13690002
os.epipecheck(...)
/usr/local/go/src/os/file_unix.go:195
os.(*File).Write(0x40c020, 0x52e280, 0x276, 0x276, 0x1, 0x0, 0x0)

This might be related to trying to replace a file while it is being read...

        node_modules/.bin/esbuild --loader=ts --target=es2019 < "$i" \
            | uglify-js $UGLIFY_OPTIONS -o "$i"

Try something like: -o "$i.tmp" && mv -f "$i.tmp" "$i"

Anyway, with ./test/release/rollup-ts.sh I get these mocha time-outs:

I'm less troubled by these watch timeouts. These tests are flaky at the best of times. Also, if npm wasn't able to build or install the native version of fsevents, that might explain these errors.

$ cd tmp/rollup/
$ npm ls | grep fsevents
│ ├── fsevents@2.3.2 deduped
├── fsevents@2.3.2
│ └── fsevents@2.3.2 deduped

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

Regarding the mv suggestion... this would be more error-proof:

        mv -f "$i" "$i.tmp"
        node_modules/.bin/esbuild --loader=ts --target=es2019 < "$i.tmp" \
            | uglify-js $UGLIFY_OPTIONS -o "$i"

In the event of failure, the resultant source file will not exist and will not build.

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

I forgot that we're using esbuild-wasm instead of native esbuild, but the pipe/mv thing above still applies.

What does this command produce on your mac?

$ esbuild-wasm --version && echo 'console.log(1 ? 2 : 3)' | esbuild-wasm --minify
0.11.15
console.log(2);

@alexlamsl
Copy link
Collaborator Author

alexlamsl commented Apr 27, 2021

Did it ever work on your mac? What version of macos are you running?

So I've never used bash script.sh on here before, so it may have never worked. Having said that, I'm surprised something that works in sh doesn't work under bash

$ sh --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15)
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15)
Copyright (C) 2007 Free Software Foundation, Inc.
$ uname -a
Darwin MacBook-Pro.local 15.6.0 Darwin Kernel Version 15.6.0: Thu Jun 21 20:07:40 PDT 2018; root:xnu-3248.73.11~1/RELEASE_X86_64 x86_64

@alexlamsl
Copy link
Collaborator Author

I'm less troubled by these watch timeouts.

I am rather concerned, as this isn't intermittent failures − they are readily reproducible. Yet they must have worked before, as I verified and committed the script from this exact machine in the first place❗🤨

@alexlamsl
Copy link
Collaborator Author

alexlamsl commented Apr 27, 2021

What does this command produce on your mac?

$ ./node_modules/.bin/esbuild --version && echo 'console.log(1 ? 2 : 3)' | ./node_modules/.bin/esbuild --minify
0.8.56
console.log(2);

@alexlamsl
Copy link
Collaborator Author

FWIW ./test/release/rollup-es.js (which requires Node.js v8 instead of v14) passes, and it contains similar set of tests:

    rollup.watch
      fs.watch
        ✓ watches a file (331ms)
        ✓ recovers from an error (536ms)
        ✓ recovers from an error even when erroring file was "renamed" (#38) (529ms)
        ✓ refuses to watch the output file (#15) (537ms)
        ✓ ignores files that are not specified in options.watch.include, if given (437ms)
        ✓ ignores files that are specified in options.watch.exclude, if given (429ms)
The following options have been renamed — please update your config: globals -> output.globals
        ✓ respects options.globals (110ms)
      chokidar
        ✓ watches a file (341ms)
        ✓ recovers from an error (657ms)
        ✓ recovers from an error even when erroring file was "renamed" (#38) (557ms)
        ✓ refuses to watch the output file (#15) (716ms)
        ✓ ignores files that are not specified in options.watch.include, if given (431ms)
        ✓ ignores files that are specified in options.watch.exclude, if given (441ms)
The following options have been renamed — please update your config: globals -> output.globals
        ✓ respects options.globals (107ms)
Total test time: 19389.992ms


  1541 passing (19s)
  3 pending

@alexlamsl
Copy link
Collaborator Author

./test/release/sucrase.sh which also relies on esbuild-wasm (& fsevents@2.1.3) passes with Node.js v14.16.1 on this macOS.

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

Darwin Kernel Version 15.6.0

Darwin Kernel Version 13.4.0 here... so you're a couple of major releases ahead of me.

GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15)

Just a few patch versions ahead of mine.

Anything different here?

$ ulimit -a | grep -v unlimited
core file size          (blocks, -c) 0
open files                      (-n) 8192
pipe size            (512 bytes, -p) 1
stack size              (kbytes, -s) 8192
max user processes              (-u) 709

FWIW ./test/release/rollup-es.js (which requires Node.js v8 instead of v14) passes, and it contains similar set of tests:

Interesting. Even with no minify options specified for rollup-ts.sh?

I am rather concerned, as this isn't intermittent failures − they are readily reproducible. Yet they must have worked before, as I verified and committed the script from this exact machine in the first place❗🤨

It's quite puzzling indeed. Your system is more recent than mine, but not substantially different.

Did replacing the uglify-js alias in the script with an env var help?

Maybe try upgrading esbuild-wasm?

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

So I've never used bash script.sh on here before, so it may have never worked. Having said that, I'm surprised something that works in sh doesn't work under bash

I missed this part - so rollup-ts.sh script works (better) in sh?

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

What's your default shell?

$ echo $SHELL
/bin/bash

https://www.howtogeek.com/444596/how-to-change-the-default-shell-to-bash-in-macos-catalina/

@alexlamsl
Copy link
Collaborator Author

Anything different here?

Looks like fewer open files:

$ ulimit -a | grep -v unlimited
core file size          (blocks, -c) 0
open files                      (-n) 256
pipe size            (512 bytes, -p) 1
stack size              (kbytes, -s) 8192
max user processes              (-u) 709

What's your default shell?

$ echo $SHELL
/bin/bash

Interesting. Even with no minify options specified for rollup-ts.sh?

No parameters at all for both rollup-es.sh & rollup-ts.sh since it won't be any difference to those time-out failures of the latter.

I missed this part - so rollup-ts.sh script works (better) in sh?

I think given #4873 (comment), sh & bash refers to the same executable. But clearly

$ ./test/release/rollup-ts.sh

behaves differently than

$ bash test/release/rollup-ts.sh

@alexlamsl
Copy link
Collaborator Author

Maybe try upgrading esbuild-wasm?

Using esbuild-wasm@0.11.15 (which seems to be the latest) fails the same way with bash test/release/rollup-ts.sh

@alexlamsl
Copy link
Collaborator Author

Yeah alias really doesn't like to work if you invoke a script with bash:

$ alias uglify-js=$PWD/bin/uglifyjs
$ uglify-js -v
uglify-js 3.13.4
$ cat test.sh
#!/bin/sh

alias uglify-js=$PWD/bin/uglifyjs
uglify-js -v
$ chmod a+x test.sh
$ ./test.sh
uglify-js 3.13.4
$ bash test.sh
test.sh: line 4: uglify-js: command not found

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

So where does your machine stand? Can it successfully run rollup-ts.sh using /bin/sh?

@alexlamsl
Copy link
Collaborator Author

Nothing's changed − still getting those time-outs as shown in 2nd part of #4873 (comment)

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

I've tried both Node.js v14.16.0 & v14.16.1 − firstly, bash test/release/rollup-ts.sh fails hard:

Granted, the alias problem is bash specific. So it works with sh or using a regular env var, I assume.

But does running /bin/sh test/release/rollup-ts.sh or just test/release/rollup-ts.sh directly also fail due to esbuild-wasm crashing with fatal error: too many writes on closed pipe?

Did the mv suggestion not work? I also use ulimit -n 8192 if that matters.

If you can isolate the esbuild-wasm problem then a bug can be filed with esbuild.

@alexlamsl
Copy link
Collaborator Author

Without the alias failure, esbuild-wasm seems to work just fine, so I guess it wasn't handling broken pipes correctly or something.

Did the mv suggestion not work? I also use ulimit -n 8192 if that matters.

None of those made any difference when alias fails to function.

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

Without the alias failure, esbuild-wasm seems to work just fine, so I guess it wasn't handling broken pipes correctly or something.

Great, use /bin/sh or don't use alias in the script - no problem.

None of those made any difference when alias fails to function.

That stands to reason.

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

Here's a sub-optimal but working proof of concept for parsing pure annotations with acorn. It's a peculiar algorithm where non-call non-new nodes consume all (invalid) pure comments before them, leaving the valid pure annotations solely to be consumed by the outermost AST_Calls. It seems to work, but there could be edge cases I missed.

--- a/bin/uglifyjs
+++ b/bin/uglifyjs
@@ -314,16 +314,36 @@ function run() {
     try {
         if (options.parse) {
             if (options.parse.acorn) {
+                var pureAnnotations = [];
                 files = convert_ast(function(toplevel, name) {
                     return require("acorn").parse(files[name], {
                         allowHashBang: true,
                         ecmaVersion: "latest",
                         locations: true,
+                        onComment: function(block, text, spos, epos) {
+                            if (/[@#]__PURE__/.test(text)) {
+                                pureAnnotations.push({ text: text, epos: epos, });
+                            }
+                        },
                         program: toplevel,
                         sourceFile: name,
                         sourceType: "module",
                     });
                 });
+                files.walk(new UglifyJS.TreeWalker(function(node) {
+                    if (node instanceof UglifyJS.AST_SimpleStatement) return;
+                    if (node instanceof UglifyJS.AST_Sequence) return;
+                    var before = pureAnnotations.filter(function(c) {
+                        return c.epos <= node.start.pos;
+                    });
+                    if (before.length && node instanceof UglifyJS.AST_Call) {
+                        node.pure = before[before.length - 1].text;
+                    }
+                    before.forEach(function(c) {
+                        var pos = pureAnnotations.indexOf(c);
+                        if (pos >= 0) pureAnnotations.splice(pos, 1);
+                    });
+                }));
             } else if (options.parse.spidermonkey) {
                 files = convert_ast(function(toplevel, name) {
                     var obj = JSON.parse(files[name]);
diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js
index 028441b..d044ab4 100644
--- a/lib/mozilla-ast.js
+++ b/lib/mozilla-ast.js
@@ -570,8 +570,8 @@
     map("AssignmentExpression", AST_Assign, "operator=operator, left>left, right>right");
     map("AssignmentPattern", AST_DefaultValue, "left>name, right>value");
     map("ConditionalExpression", AST_Conditional, "test>condition, consequent>consequent, alternate>alternative");
-    map("NewExpression", AST_New, "callee>expression, arguments@args");
-    map("CallExpression", AST_Call, "callee>expression, arguments@args");
+    map("NewExpression", AST_New, "callee>expression, arguments@args, pure=pure");
+    map("CallExpression", AST_Call, "callee>expression, arguments@args, pure=pure");
     map("SequenceExpression", AST_Sequence, "expressions@expressions");
     map("SpreadElement", AST_Spread, "argument>expression");
     map("ObjectExpression", AST_Object, "properties@properties");

A test case:

$ cat p.js

    /* @__PURE__ */ "incorrect annotation";
    keep();
    /*@__PURE__*/drop(), also.keep();
    ( /*@__PURE__*/ keep.me() ).foo();
    /* the quick #__PURE__ brown fox */
    new Pure;
    retained();
    ( /*@__PURE__*/ drop().me().bar() );
$ cat p.js | uglify-js -p acorn -bc
keep(), also.keep(), keep.me().foo(), retained();
$ cat p.js | uglify-js -p acorn -b annotations
"incorrect annotation";

keep();

/*@__PURE__*/drop(), also.keep();

keep.me().foo();

/* the quick #__PURE__ brown fox */new Pure();

retained();

/*@__PURE__*/drop().me().bar();

The patch also extends the mozilla translation layer by adding a pure property to both the ESTree CallExpression and NewExpression.

Demonstration using the acorn parser, output to estree, input from estree, output with annotations:

$ cat p.js | uglify-js -p acorn -o spidermonkey | uglify-js -p spidermonkey -b annotations
"incorrect annotation";

keep();

/*@__PURE__*/drop(), also.keep();

keep.me().foo();

/* the quick #__PURE__ brown fox */new Pure();

retained();

/*@__PURE__*/drop().me().bar();

same as above, but with compress:

$ cat p.js | uglify-js -p acorn -o spidermonkey | uglify-js -p spidermonkey -bc
keep(), also.keep(), keep.me().foo(), retained();

Demonstration using the uglify parser, output to estree, input from estree, output with annotations:

$ uglify-js p.js -o spidermonkey | uglify-js -p spidermonkey -b annotations
"incorrect annotation";

keep();

/*@__PURE__*/drop(), also.keep();

keep.me().foo();

/*#__PURE__*/new Pure();

retained();

/*@__PURE__*/drop().me().bar();

same as above, but with compress:

$ uglify-js p.js -o spidermonkey | uglify-js -p spidermonkey -bc
keep(), also.keep(), keep.me().foo(), retained();

This ESTree pure extension is presently implemented as the actual pure comment text:

$ uglify-js p.js -o spidermonkey | grep pure
            "pure": "@__PURE__",
            "pure": "@__PURE__",
        "pure": "#__PURE__",
        "pure": "@__PURE__",
$ uglify-js p.js -p acorn -o spidermonkey | grep pure
            "pure": "@__PURE__",
            "pure": "@__PURE__",
        "pure": " the quick #__PURE__ brown fox ",
        "pure": "@__PURE__",

This ESTree pure property probably should be extended to also handle boolean values and insert /*@__PURE__*/ annotations for true values if third parties are to generate this format.

NOTE: The patch doesn't work with multiple inputs and I did not bother to investigate it further:

$ cp p.js q.js

expected:

$ uglify-js p.js q.js -bc
keep(), also.keep(), keep.me().foo(), retained(), keep(), also.keep(), keep.me().foo(), 
retained();

actual:

$ uglify-js p.js q.js -p acorn -bc
keep(), also.keep(), keep.me().foo(), retained(), keep(), drop(), also.keep(), keep.me().foo(), 
new Pure(), retained(), drop().me().bar();
$ uglify-js p.js q.js -p acorn -b annotations
"incorrect annotation";

keep();

/*@__PURE__*/drop(), also.keep();

keep.me().foo();

/* the quick #__PURE__ brown fox */new Pure();

retained();

/*@__PURE__*/drop().me().bar();

"incorrect annotation";

keep();

drop(), also.keep();

keep.me().foo();

new Pure();

retained();

drop().me().bar();

@kzc
Copy link
Contributor

kzc commented Apr 27, 2021

This is a much more efficient implementation of the acorn pure annotation parser. Only the pureProcessed index is advanced per AST node visit. No temporary arrays are created or modified per node visit.

diff --git a/bin/uglifyjs b/bin/uglifyjs
index 69a98a4..33d5d72 100755
--- a/bin/uglifyjs
+++ b/bin/uglifyjs
@@ -314,16 +314,35 @@ function run() {
     try {
         if (options.parse) {
             if (options.parse.acorn) {
+                var pureAnnotations = [], pureProcessed = 0;
                 files = convert_ast(function(toplevel, name) {
                     return require("acorn").parse(files[name], {
                         allowHashBang: true,
                         ecmaVersion: "latest",
                         locations: true,
+                        onComment: function(block, text, spos, epos) {
+                            if (/[@#]__PURE__/.test(text)) {
+                                pureAnnotations.push({ text: text, epos: epos, });
+                            }
+                        },
                         program: toplevel,
                         sourceFile: name,
                         sourceType: "module",
                     });
                 });
+                files.walk(new UglifyJS.TreeWalker(function(node) {
+                    if (node instanceof UglifyJS.AST_SimpleStatement) return;
+                    if (node instanceof UglifyJS.AST_Sequence) return;
+                    while (pureProcessed < pureAnnotations.length) {
+                        var comment = pureAnnotations[pureProcessed];
+                        if (comment.epos > node.start.pos) break;
+                        var pure = comment;
+                        ++pureProcessed;
+                    }
+                    if (pure && node instanceof UglifyJS.AST_Call) {
+                        node.pure = pure.text;
+                    }
+                }));
             } else if (options.parse.spidermonkey) {
                 files = convert_ast(function(toplevel, name) {
                     var obj = JSON.parse(files[name]);
diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js
index 028441b..d044ab4 100644
--- a/lib/mozilla-ast.js
+++ b/lib/mozilla-ast.js
@@ -570,8 +570,8 @@
     map("AssignmentExpression", AST_Assign, "operator=operator, left>left, right>right");
     map("AssignmentPattern", AST_DefaultValue, "left>name, right>value");
     map("ConditionalExpression", AST_Conditional, "test>condition, consequent>consequent, alternate>alternative");
-    map("NewExpression", AST_New, "callee>expression, arguments@args");
-    map("CallExpression", AST_Call, "callee>expression, arguments@args");
+    map("NewExpression", AST_New, "callee>expression, arguments@args, pure=pure");
+    map("CallExpression", AST_Call, "callee>expression, arguments@args, pure=pure");
     map("SequenceExpression", AST_Sequence, "expressions@expressions");
     map("SpreadElement", AST_Spread, "argument>expression");
     map("ObjectExpression", AST_Object, "properties@properties");

However, both this patch and the previous one have a bug...

Expected result using the uglify parser:

$ echo 'foo()/*@__PURE__*/;bar();/*@__PURE__*/' | uglify-js -c
foo(),bar();

Actual incorrect result using the acorn parse option:

$ echo 'foo()/*@__PURE__*/;bar();/*@__PURE__*/' | uglify-js -p acorn -c
foo();
$ echo 'foo()/*@__PURE__*/;bar();/*@__PURE__*/' | uglify-js -p acorn --annotations
foo();/*@__PURE__*/bar();

And sure enough, Rollup has the same bug due to its similar algorithm:

$ rollup --version
rollup v2.44.0

$ echo 'foo()/*@__PURE__*/;bar();/*@__PURE__*/' | rollup --silent
foo();

One possible solution would be to scan the input source code string backwards from the node start to the end of the previous pure comment to verify that there's only whitespace - or possibly open parentheses ( characters.

There's probably a better solution. Have to give it some thought.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants