diff --git a/src/main/java/org/codelibs/sai/internal/parser/Parser.java b/src/main/java/org/codelibs/sai/internal/parser/Parser.java index ad896ad..607ffe7 100644 --- a/src/main/java/org/codelibs/sai/internal/parser/Parser.java +++ b/src/main/java/org/codelibs/sai/internal/parser/Parser.java @@ -1521,13 +1521,45 @@ private void forStatement() { * @return true if a for-of head starts at the current token */ private boolean isForOf() { + return lookahead(this::isForOfAhead); + } + + private boolean isForOfAhead() { int i = k; if (T(i) == TokenType.VAR || T(i) == LET || T(i) == CONST) { i++; } - return T(i) == IDENT && T(i + 1) == IDENT && "of".equals(getValue(getToken(i + 1))); + if (T(i) == LBRACKET || T(i) == LBRACE) { + // A destructuring pattern. Find the bracket that closes it; what is in + // between only has to balance here, it is parsed properly once this is + // known to be a for-of. + final TokenType open = T(i); + final TokenType close = open == LBRACKET ? RBRACKET : RBRACE; + int depth = 0; + + for (;; i++) { + final TokenType tokenType = T(i); + + if (tokenType == open) { + depth++; + } else if (tokenType == close) { + if (--depth == 0) { + i++; + break; + } + } else if (tokenType == EOF) { + return false; + } + } + } else if (T(i) == IDENT) { + i++; + } else { + return false; + } + + return T(i) == IDENT && "of".equals(getValue(getToken(i))); } /** @@ -1565,8 +1597,19 @@ private ForNode forOf(final ForNode forNodeArg, final int forLine) { next(); } - final IdentNode name = getIdent(); - verifyStrictIdent(name, "for-of iterator"); + final List pattern; + final IdentNode name; + + if (type == LBRACKET || type == LBRACE) { + // The leaves of a pattern with no declaration are assignment targets rather + // than names to declare, the same distinction destructuringAssignment makes. + pattern = destructuringPattern(declarationType == null); + name = null; + } else { + pattern = null; + name = getIdent(); + verifyStrictIdent(name, "for-of iterator"); + } // "of" is a plain identifier, not a keyword. final long ofToken = token; @@ -1597,11 +1640,41 @@ private ForNode forOf(final ForNode forNodeArg, final int forLine) { Block body = newBlock(); try { - if (declarationType == null) { + final int varFlags = declarationType == LET ? VarNode.IS_LET + : declarationType == CONST ? VarNode.IS_CONST : 0; + + if (pattern != null) { + // Bind the element to a temporary and take the pattern apart from there, + // so the element expression is evaluated once per iteration. + final String elementName = newTemporary(); + + if (declarationType == null) { + final List steps = new ArrayList<>(); + steps.add(new BinaryNode(Token.recast(ofToken, TokenType.ASSIGN), + identifierFor(ofToken, elementName), element)); + assignBindings(elementName, pattern, steps); + + Expression result = steps.get(0); + + for (int i = 1; i < steps.size(); i++) { + result = new BinaryNode(Token.recast(ofToken, TokenType.COMMARIGHT), result, steps.get(i)); + } + + appendStatement(new ExpressionStatement(forLine, ofToken, finish, result)); + } else { + appendStatement(assignTemporary(forLine, ofToken, elementName, element)); + + final List statements = new ArrayList<>(); + declareBindings(elementName, pattern, forLine, varFlags, new ArrayList(), statements); + + for (final Statement statement : statements) { + appendStatement(statement); + } + } + } else if (declarationType == null) { appendStatement(new ExpressionStatement(forLine, ofToken, finish, new BinaryNode(Token.recast(ofToken, TokenType.ASSIGN), name, element))); } else { - final int varFlags = declarationType == LET ? VarNode.IS_LET : declarationType == CONST ? VarNode.IS_CONST : 0; appendStatement(new VarNode(forLine, declarationToken, finish, name.setIsDeclaredHere(), element, varFlags)); } diff --git a/test/script/basic/es6/for-of.js b/test/script/basic/es6/for-of.js index 324000d..e0cf908 100644 --- a/test/script/basic/es6/for-of.js +++ b/test/script/basic/es6/for-of.js @@ -121,3 +121,74 @@ print(out.join(",")); out = []; for (var w of [1, 2]) out.push(w * 10); print(out.join(",")); + +// The loop variable may be a destructuring pattern. The element is bound to a +// temporary once per iteration and the pattern is taken apart from there. +for (var [a, b] of [[1, 2], [3, 4]]) { + print(a + ":" + b); +} + +for (let [a, b] of [[5, 6]]) { + print(a + ":" + b); +} + +for (const [a, b] of [[7, 8]]) { + print(a + ":" + b); +} + +for (var { x, y } of [{ x: 9, y: 10 }]) { + print(x + ":" + y); +} + +for (const { k: renamed } of [{ k: 11 }]) { + print(renamed); +} + +// Rest, nesting and defaults inside the pattern. +for (const [head, ...rest] of [[12, 13, 14]]) { + print(head + ":" + rest); +} + +for (const [{ inner }] of [[{ inner: 15 }]]) { + print(inner); +} + +for (let [withDefault = 16] of [[]]) { + print(withDefault); +} + +// The common shape: iterating pairs. +var pairs = [["k", 17], ["j", 18]]; +for (const [key, value] of pairs) { + print(key + "=" + value); +} + +// With no declaration the leaves are assignment targets, member expressions included. +var lhs1, lhs2; +for ([lhs1, lhs2] of [[19, 20]]) { + print(lhs1 + ":" + lhs2); +} + +var target = {}; +for ([target.field] of [[21]]) { + print(target.field); +} + +// A let binding still gets a fresh copy per iteration, so closures made in the +// body capture that iteration's value. +var captured = []; +for (let [each] of [[22], [23]]) { + captured.push(function () { return each; }); +} +print(captured[0]() + "," + captured[1]()); + +// break and continue are unaffected. +for (const [n] of [[24], [25], [26]]) { + if (n === 25) { + continue; + } + if (n === 26) { + break; + } + print(n); +} diff --git a/test/script/basic/es6/for-of.js.EXPECTED b/test/script/basic/es6/for-of.js.EXPECTED index f48206c..48f2cae 100644 --- a/test/script/basic/es6/for-of.js.EXPECTED +++ b/test/script/basic/es6/for-of.js.EXPECTED @@ -10,3 +10,18 @@ x,y [] 1,2,3 10,20 +1:2 +3:4 +5:6 +7:8 +9:10 +11 +12:13,14 +15 +16 +k=17 +j=18 +19:20 +21 +22,23 +24