Skip to content

Instantly share code, notes, and snippets.

@tuchida
Last active April 7, 2021 01:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tuchida/5330e03521928a46d22c320a9fdf70ad to your computer and use it in GitHub Desktop.
Save tuchida/5330e03521928a46d22c320a9fdf70ad to your computer and use it in GitHub Desktop.
Rhinoで1=1がParserExceptionになる問題

調査中なので推測や誤解があるものとして読んでください。

事象

Rhinoで 1=1 を評価すると ParserException が発生する

$ java -jar buildGradle/libs/rhino-1.7.14-SNAPSHOT.jar -version 200
Rhino 1.7.14-SNAPSHOT 2021 04 04
js> 1 = 1
js: line 1: Invalid assignment left-hand side.
js: 
js: ^
Exception in thread "main" org.mozilla.javascript.Parser$ParserException
	at org.mozilla.javascript.Parser.reportError(Parser.java:330)
	at org.mozilla.javascript.Parser.reportError(Parser.java:312)
	at org.mozilla.javascript.Parser.reportError(Parser.java:307)
	at org.mozilla.javascript.IRFactory.createAssignment(IRFactory.java:2275)
	at org.mozilla.javascript.IRFactory.transformAssignment(IRFactory.java:442)
	at org.mozilla.javascript.IRFactory.transform(IRFactory.java:215)
	at org.mozilla.javascript.IRFactory.transformExprStmt(IRFactory.java:547)
	at org.mozilla.javascript.IRFactory.transform(IRFactory.java:212)
	at org.mozilla.javascript.IRFactory.transformScript(IRFactory.java:1077)
	at org.mozilla.javascript.IRFactory.transform(IRFactory.java:194)
	at org.mozilla.javascript.IRFactory.transformTree(IRFactory.java:119)
	at org.mozilla.javascript.Context.parse(Context.java:2606)
	at org.mozilla.javascript.Context.compileImpl(Context.java:2540)
	at org.mozilla.javascript.Context.compileString(Context.java:1544)
	at org.mozilla.javascript.Context.compileString(Context.java:1533)
	at org.mozilla.javascript.tools.shell.Main.processSource(Main.java:495)
	at org.mozilla.javascript.tools.shell.Main.processFiles(Main.java:181)
	at org.mozilla.javascript.tools.shell.Main$IProxy.run(Main.java:101)
	at org.mozilla.javascript.Context.call(Context.java:586)
	at org.mozilla.javascript.ContextFactory.call(ContextFactory.java:525)
	at org.mozilla.javascript.tools.shell.Main.exec(Main.java:163)
	at org.mozilla.javascript.tools.shell.Main.main(Main.java:138)

なにが問題なのか

通常Rhinoで発生したJavaScriptエラーはErrorReporterがハンドリングしてEvaluatorExceptionを投げる
EvaluatorExceptionは適切にハンドリングされるので、たとえばREPLが止まったりしない

$ java -jar buildGradle/libs/rhino-1.7.14-SNAPSHOT.jar -version 200
Rhino 1.7.14-SNAPSHOT 2021 04 04
js> #
js: "<stdin>", line 2: illegal character: #
js: #
js: ^
js: "<stdin>", line 2: Compilation produced 1 syntax errors.

js> 

Parser$ParserExceptionはprivateクラスなため、これがRhinoの外に貫通するのは困る
https://github.com/mozilla/rhino/blob/af35d90e2022e82ca59bd58cef5b2f2824cb3a7c/src/org/mozilla/javascript/Parser.java#L162

どうしてこうなるのか

IRFactoryが構文エラーを見つけてreportErrorするとこうなる
Parser内でreportErrorした場合は適切にハンドリングされてEvaluatorExceptionへと変換される
なお、バージョン1.7R5でも再現しているため、かなり昔からの問題に見える

IRFactoryとはなにものなのか

このへん
https://github.com/mozilla/rhino/blob/af35d90e2022e82ca59bd58cef5b2f2824cb3a7c/src/org/mozilla/javascript/IRFactory.java#L110

まずRhinoに渡したJavaScriptのコードはParserで構文解析されてASTに変換される
次にASTがIRFactoryに渡され、ASTをソースコードに戻しながら再解析される
ソースコードに戻す役割はDecompiler

IRFactoryParserを継承しているため、どちらでなにをやっているのかがとてもわかりにくくなっている(継承によるコードの再利用問題)

なぜ2回も構文解析するのか

たとえば({a}){a}はオブジェクトリテラル(ObjectLiteral)だけど、({a} = { b: 1 })だと{a}は分割代入(DestructuringAssignment)の左辺値(LeftHandSideExpression)になる
=が出てくるまで{a}はどちらかわからない
varがついていればわかるんだけど、ついてないばあいはグローバル変数になる仕様なので

2回解析することの問題点

エラーの数や箇所が変わる

${IRFactoryでSyntaxErrorになるコード}
${ParserでSyntaxErrorになるコード}

こんなコードがあったばあい、SyntaxErrorを全部見つけたいならエラーの数が2つになってほしいし、SyntaxErrorで早期脱出したいなら1行目のエラーを報告してほしい
2回解析だとエラーの数が1つになるし、早期脱出なら2行目のエラーが報告されてしまう

仕様はどうなっているのか

https://262.ecma-international.org/11.0/#sec-syntactic-grammar

"P is not covering an N"

多分これが2回解析する根拠になっている(違うかも)

ほかのJavaScript Parser実装はどうなっているのか

SpiderMonkeyはこれ
https://searchfox.org/mozilla-central/source/js/src/frontend/Parser.cpp#11133

Esprimaだとこれ
https://github.com/jquery/esprima/blob/70c015998b5beb3b1f8c2277cd9288ad6917f378/src/parser.ts#L522

CoverInitializedNameとかisolateCoverGrammarとか、coverなんとかってやつ
ちゃんと読んでないので、2回解析してる実装になっているかはあやしい

https://qiita.com/uhyo/items/c1574cdd11b1a28b85b7 によるとV8は2回解析になってるらしい
coverがこのあたりの挙動を把握するキーワードになるのはまちがいなさそう

IRFactoryみたいにソースコード全体を再解析する必要はあるのか

ないと思う

Rhinoはどうして全体を再解析するのか

Rhinoの分割代入はECMAScript6の仕様ではなく、JavaScript 1.8とか呼ばれるMozilla拡張の仕様で実装されている
だからECMA262の仕様と乖離した実装になったのではないかな

ほかにもたとえばletなんかも実装されてるように見えるけど、TDZがない

IRFactoryいらないのでは

そう思う

Decompilerもいらないのでは

そう思う

Decompilerがなくなるとどんなことがおきるのか

Rhinoや古いFirefoxのFunction#toStringにはASTからソースコードに戻している挙動が現れていて、以下の特徴がある

  • コード内のコメントが消える
  • コードのインデントなどが整形される

なのでテンプレートリテラルがなかった時代にFunction#toStringでコメントをヒアドキュメント代わりに使うtipsがあったんだけどFirefoxでは使えなかったりとか、Function#toStringをフォーマッタ代わりに使うtipsがあったりとかした

Function#toStringはもとのソースコードを切り取って返せばよくって、現代のJavaScriptエンジンだとだいたいそうなってたはず

ということをふまえてどこを修正すればいいのか

わからん
問題を分割して段階的な移行ができればいいんだけど
大規模な変更をしたばあい、ECMA262準拠の部分はtest262があるからかんたんには壊れないはずだけど、Mozilla拡張とかE4Xとかを壊しそう(捨てたい)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment