Round-trip change due to "&&" constant-folding leaving extra parens

RESOLVED FIXED

Status

()

defect
--
minor
RESOLVED FIXED
11 years ago
10 years ago

People

(Reporter: jruderman, Assigned: jorendorff)

Tracking

(Blocks 1 bug, {regression, testcase})

Trunk
x86
macOS
Points:
---
Dependency tree / graph
Bug Flags:
in-testsuite +
in-litmus -

Firefox Tracking Flags

(Not tracked)

Details

Attachments

(2 attachments, 2 obsolete attachments)

js> f = (function() { if ((1 && w.valueOf())) {} })  
function () {
    if ((w.valueOf())) {
    }
}

js> eval(uneval(f));
function () {
    if (w.valueOf()) {
    }
}

This happens with "valueOf" and "eval" but not with most other names.
Brendan, earlier this week you mentioned the possibility of just ripping out TOK_RP throughout.  That would fix this, right?
Yeah, getting rid of JSOP_GROUP would fix this bug -- a bigger project than I can take on right now. You willing to try?

/be
Posted patch v1 (obsolete) — Splinter Review
This removes JSOP_GROUP.  It retains the TOK_RP nodes but doesn't emit anything for them.  Passes regrtests.

The TOK_RP nodes are useless, but I'd like to kill them in a separate patch; I am paranoid about regressions, being unfamiliar with this code, and would appreciate the separate bisect target.
Assignee: general → jorendorff
Attachment #343712 - Flags: review?(brendan)
Comment on attachment 343712 [details] [diff] [review]
v1

Correction -- I wasn't testing the right thing, and there are several decompiler bugs in v1.
Attachment #343712 - Flags: review?(brendan)
For your amusement--one of those is the venerable
  new (x(y).z)
decompiling as
  new x(y).z

js_Decompile, case JSOP_NEW, has a nice comment explaining why that needs to be parenthesized and how the code supposedly handles it, but there's a bug--I think it was unwittingly depending on JSOP_GROUP all along. :-)
(In reply to comment #5)
> For your amusement--one of those is the venerable
>   new (x(y).z)
> decompiling as
>   new x(y).z
> 
> js_Decompile, case JSOP_NEW, has a nice comment explaining why that needs to be
> parenthesized and how the code supposedly handles it, but there's a bug--I
> think it was unwittingly depending on JSOP_GROUP all along. :-)

The dependency is witting. The other comment under JSOP_GROUP's case is:

              case JSOP_GROUP:
                cs = &js_CodeSpec[lastop];
                if ((cs->prec != 0 &&
                     cs->prec <= js_CodeSpec[NEXT_OP(pc)].prec) ||
                    pc[JSOP_GROUP_LENGTH] == JSOP_NULL ||
                    pc[JSOP_GROUP_LENGTH] == JSOP_NULLTHIS ||
                    pc[JSOP_GROUP_LENGTH] == JSOP_DUP ||
                    pc[JSOP_GROUP_LENGTH] == JSOP_IFEQ ||
                    pc[JSOP_GROUP_LENGTH] == JSOP_IFNE) {
                    /*
                     * Force parens if this JSOP_GROUP forced re-association
                     * against precedence, or if this is a call or constructor
                     * expression, or if it is destructured (JSOP_DUP), or if
                     * it is an if or loop condition test.
                     *
                     * This is necessary to handle the operator new grammar,
                     * by which new x(y).z means (new x(y))).z.  For example
                     * new (x(y).z) must decompile with the constructor
                     * parenthesized, but normal precedence has JSOP_GETPROP
                     * (for the final .z) higher than JSOP_NEW.  In general,
                     * if the call or constructor expression is parenthesized,
                     * we preserve parens.
                     */
                    op = JSOP_NAME;
                    rval = POP_STR();

If you removed this code, then you broke the other code under JSOP_NEW/CALL/EVAL.

/be
Posted patch v2 (obsolete) — Splinter Review
This flunks two new tests, in addition to all the ones that were already failing. In both cases, the new breakage is clearly a harmless change in parenthesization due to JSOP_GROUP being removed.

js1_5/decompilation/regress-456964-01.js
  expect: o += (i + " = " + object[i] + "\n");
  actual: o += i + " = " + object[i] + "\n";
js1_7/decompilation/regress-352026.js
  expect: function ( ) { z ( ( yield 3 ) ) ; } 
  actual: function ( ) { z ( yield 3 ) ; } 

I had to tweak the logic for decompiling binary expressions to get
cases like (function () a + (b + c) + d) right.

I changed the precedence of the JSOP_DEL* opcodes from 17 (like new) to 15 (like the prefix operators ! ~ - ++ --), reflecting the precedence of delete expressions rather than the minimum permissible precedence of their subexpressions.  This was necessary to force parentheses when decompiling |new (delete foo)| and in turn necessitated changes to how those opcodes are decompiled.
Attachment #343712 - Attachment is obsolete: true
Attachment #343946 - Flags: review?(brendan)
(In reply to comment #7)
> This flunks two new tests, in addition to all the ones that were already
> failing.

By this I mean only that tracemonkey is in a failing state, I hope just because we haven't merged recent updates to js/src/public-failures.txt.

The patch is good stuff!
Comment on attachment 343946 [details] [diff] [review]
v2

Thanks for taking this on. Few can face the decompiler and not blink -- impressive patchwork.

Need to rev jsxdrapi.h's JSXDR_BYTECODE_VERSION when removing opcodes.

>@@ -5132,25 +5131,21 @@ js_Interpret(JSContext *cx)
>             }
> 
>             id = ATOM_TO_JSID(atom);
>             if (js_FindPropertyHelper(cx, id, &obj, &obj2, &prop, &entry) < 0)
>                 goto error;
>             if (!prop) {
>                 /* Kludge to allow (typeof foo == "undefined") tests. */
>                 endpc = script->code + script->length;
>-                for (pc2 = regs.pc + JSOP_NAME_LENGTH; pc2 < endpc; pc2++) {
>-                    op2 = (JSOp)*pc2;
>-                    if (op2 == JSOP_TYPEOF) {
>-                        PUSH_OPND(JSVAL_VOID);
>-                        len = JSOP_NAME_LENGTH;
>-                        DO_NEXT_OP(len);
>-                    }
>-                    if (op2 != JSOP_GROUP)
>-                        break;
>+                op2 = (JSOp) *(regs.pc + JSOP_NAME_LENGTH);

Nit: prefer p[i] to the longer *(p + i) form, even when p is a pointer not an array.

>+/*
>+ * To parenthesize correctly in case JSOP_NEW, we must know whether the
>+ * left-hand side contains any non-parenthesized function calls.  So when

s/non-/un/

>+ * building a MemberExpression or CallExpression, we set ss->opcodes[n] to
>+ * JSOP_CALL if this is true: |foo().x.y| gets JSOP_CALL, not JSOP_GETPROP.

See below for evolved comment relocation opportunity.

>+                rval = POP_STR_PREC(cs->prec + (inXML ? 0 : cs->format & JOF_LEFTASSOC ? 1 : 0));
>+                lval = POP_STR_PREC(cs->prec + (inXML ? 0 : cs->format & JOF_LEFTASSOC ? 0 : 1));

Nit: parenthesize condition to left of ? as a rule -- but:

Why not use (cs->prec + (inXML || !(cs->format & JOF_LEFTASSOC)) for the second line (!! instead of ! for the first line)? Hardly seems clearer spelled out with ?: but it is certainly longer.

>+                    op = JSOP_IFEQ;  /* extra parens around assignment */
>                     js_printf(jp, "\t} while (%s);\n", POP_STR());

Make a POP_COND_STR() to common these op = JSOP_IFEQ hacks? Better, it would use your POP_STR_PREC and not mutate op at all.

>-                     * This is necessary to handle the operator new grammar,
>-                     * by which new x(y).z means (new x(y))).z.  For example
>-                     * new (x(y).z) must decompile with the constructor
>-                     * parenthesized, but normal precedence has JSOP_GETPROP
>-                     * (for the final .z) higher than JSOP_NEW.  In general,
>-                     * if the call or constructor expression is parenthesized,
>-                     * we preserve parens.

Want an evolution of this comment to live on, mainly because it shows both cases and discusses precedence relations. JSOP_CALL, JSOP_GETPROP, and the other ops that call PROPAGATE_CALLNESS, all having the same precedence, and this should be commented too.

>diff --git a/js/src/jsopcode.tbl b/js/src/jsopcode.tbl
>--- a/js/src/jsopcode.tbl
>+++ b/js/src/jsopcode.tbl

Please update the big precedence table comment above the first OPDEF.

>@@ -5691,37 +5691,18 @@ PrimaryExpr(JSContext *cx, JSTokenStream
>             return NULL;
>         pn2 = ParenExpr(cx, ts, tc, pn, &genexp);
>         if (!pn2)
>             return NULL;
>         if (genexp)
>             return pn2;
> 
>         MUST_MATCH_TOKEN(TOK_RP, JSMSG_PAREN_IN_PAREN);
>-        if (pn2->pn_type == TOK_RP ||
>-            (js_CodeSpec[pn2->pn_op].prec >= js_CodeSpec[JSOP_GETPROP].prec &&
>-             !afterDot)) {
>-            /*
>-             * Avoid redundant JSOP_GROUP opcodes, for efficiency and mainly
>-             * to help the decompiler look ahead from a JSOP_ENDINIT to see a
>-             * JSOP_GROUP followed by a POP or POPV.  That sequence means the
>-             * parentheses are mandatory, to disambiguate object initialisers
>-             * as expression statements from block statements.
>-             *
>-             * Also drop pn if pn2 is a member or a primary expression of any
>-             * kind.  This is required to avoid generating a JSOP_GROUP that
>-             * will null the |obj| interpreter register, causing |this| in any
>-             * call of that member expression to bind to the global object.
>-             */
>-            RecycleTree(pn, tc);
>-            pn = pn2;
>-        } else {
>-            pn->pn_type = TOK_RP;
>-            pn->pn_kid = pn2;
>-        }
>+        pn->pn_type = TOK_RP;
>+        pn->pn_kid = pn2;

Are you sure code elsewhere (jsemit.cpp) does not depend on this code? I'd defer removing it if you can until that followup bug to remove TOK_RP nodes from the AST.

/be
Posted patch v3Splinter Review
Adopt all brendan's suggestions.
Attachment #343946 - Attachment is obsolete: true
Attachment #344096 - Flags: review?(brendan)
Attachment #343946 - Flags: review?(brendan)
I see I inadvertently added a second blank line before a comment in jsopcode.cpp.  I'll delete that for checkin.
Attachment #344096 - Flags: review?(brendan) → review+
Comment on attachment 344096 [details] [diff] [review]
v3

> #define POP_STR()             PopStr(ss, op)
>-#define LOCAL_ASSERT(expr)    LOCAL_ASSERT_RV(expr, NULL)
>+#define POP_STR_PREC(prec)    PopStrPrec(ss, prec)
>+
>+/*
>+ * Pop a condition expression for if/for/while. JSOP_IFEQ's precedence forces
>+ * extra parens around assignment, which avoids a strict-mode warning.
>+ */
>+#define POP_COND_STR()        POP_STR_PREC(js_CodeSpec[JSOP_IFEQ].prec)

Defining POP_COND_STR as PopStr(ss, JSOP_IFEQ) would minimize code size. r=me with that.

/be
http://hg.mozilla.org/tracemonkey/rev/ffb53ca317bc
Status: NEW → RESOLVED
Closed: 11 years ago
Resolution: --- → FIXED
Depends on: 461108
Before the patch, both of these were stable:
(function() { if(a = b) { } })
(function() { if((a = b)) { } })

After the patch, both decompile to the latter.  I think this is an improvement, since it makes a strict warning go away.  I'll adjust jsfunfuzz's "checkForExtraParens" to stop complaining.

I don't think this new behavior helps in "for" conditions, though.  I filed bug 461108 for that.
Depends on: 461110
Depends on: 461111
Depends on: 461233
Blocks: 461269
this breaks js1_8_1/decompilation/regress-346642-01.js 14 and 15. jorendorff says that is expected. I'll update the tests in the next push.
(In reply to comment #7)

> js1_5/decompilation/regress-456964-01.js
>   expect: o += (i + " = " + object[i] + "\n");
>   actual: o += i + " = " + object[i] + "\n";

the expect expression here is not part of the checked in test. Did you make local revisions?
http://hg.mozilla.org/mozilla-central/rev/37b3fdbb0f07
/cvsroot/mozilla/js/tests/js1_5/decompilation/regress-460501.js,v  <--  regress-460501.js
initial revision: 1.1
Flags: in-testsuite+
Flags: in-litmus-
Depends on: 486139
You need to log in before you can comment on or make changes to this bug.