2121import org .codehaus .groovy .GroovyBugError ;
2222import org .codehaus .groovy .ast .ClassHelper ;
2323import org .codehaus .groovy .ast .ClassNode ;
24+ import org .codehaus .groovy .ast .MultipleAssignmentMetadata ;
2425import org .codehaus .groovy .ast .Variable ;
2526import org .codehaus .groovy .ast .expr .ArrayExpression ;
2627import org .codehaus .groovy .ast .expr .BinaryExpression ;
4344import org .codehaus .groovy .ast .tools .WideningCategories ;
4445import org .codehaus .groovy .classgen .AsmClassGenerator ;
4546import org .codehaus .groovy .classgen .BytecodeExpression ;
47+ import org .codehaus .groovy .runtime .MultipleAssignmentSupport ;
4648import org .codehaus .groovy .runtime .ScriptBytecodeAdapter ;
4749import org .codehaus .groovy .syntax .Token ;
4850import org .objectweb .asm .Label ;
5355import static org .codehaus .groovy .ast .tools .GeneralUtils .binX ;
5456import static org .codehaus .groovy .ast .tools .GeneralUtils .boolX ;
5557import static org .codehaus .groovy .ast .tools .GeneralUtils .callX ;
58+ import static org .codehaus .groovy .ast .tools .GeneralUtils .classX ;
5659import static org .codehaus .groovy .ast .tools .GeneralUtils .constX ;
5760import static org .codehaus .groovy .ast .tools .GeneralUtils .elvisX ;
5861import static org .codehaus .groovy .ast .tools .GeneralUtils .notX ;
5962import static org .codehaus .groovy .ast .tools .GeneralUtils .nullX ;
63+ import static org .codehaus .groovy .ast .tools .GeneralUtils .propX ;
6064import static org .codehaus .groovy .ast .tools .GeneralUtils .ternaryX ;
6165import static org .codehaus .groovy .syntax .Types .ASSIGN ;
6266import static org .codehaus .groovy .syntax .Types .BITWISE_AND ;
@@ -505,6 +509,101 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
505509 leftExpression .visit (acg );
506510 operandStack .remove (operandStack .getStackLength () - mark );
507511 } else { // multiple declaration or assignment
512+ TupleExpression tuple = (TupleExpression ) leftExpression ;
513+ java .util .List <Expression > elements = tuple .getExpressions ();
514+ int tupleSize = elements .size ();
515+ int restIndex = -1 ;
516+ for (int idx = 0 ; idx < tupleSize ; idx ++) {
517+ if (Boolean .TRUE .equals (elements .get (idx ).getNodeMetaData (MultipleAssignmentMetadata .REST_BINDING ))) {
518+ restIndex = idx ;
519+ break ;
520+ }
521+ }
522+ boolean hasRest = (restIndex >= 0 );
523+ boolean tailRest = hasRest && restIndex == tupleSize - 1 ;
524+ boolean isMapStyle = !elements .isEmpty ()
525+ && elements .get (0 ).getNodeMetaData (MultipleAssignmentMetadata .MAP_KEY ) != null ;
526+
527+ // GEP-20 map-style destructuring: def (name: n, age: a) = person
528+ // Each binder is emitted as a property access on the RHS, dispatched via the MOP
529+ // (Map → key lookup, bean → getter, GroovyObject → getProperty).
530+ if (isMapStyle ) {
531+ for (Expression e : elements ) {
532+ String key = (String ) e .getNodeMetaData (MultipleAssignmentMetadata .MAP_KEY );
533+ // Property access is a read here; the surrounding pushLHS(true) above would
534+ // otherwise mark it as a store target.
535+ compileStack .popLHS ();
536+ propX (rhsValueLoader , key ).visit (acg );
537+ compileStack .pushLHS (true );
538+ assignOneMultiAssignSlot (e , defineVariable , operandStack , compileStack , acg );
539+ }
540+ compileStack .popLHS ();
541+ if (returnRightValue ) rhsValueLoader .visit (acg );
542+ compileStack .removeVar (rhsValueId );
543+ return ;
544+ }
545+
546+ // GEP-20 degenerate case: `def (*t) = rhs` — single rest binder; equivalent to `def t = rhs`.
547+ if (tailRest && tupleSize == 1 ) {
548+ rhsValueLoader .visit (acg );
549+ if (defineVariable ) {
550+ Variable v = (Variable ) elements .get (0 );
551+ operandStack .doGroovyCast (v );
552+ compileStack .defineVariable (v , true );
553+ operandStack .remove (1 );
554+ } else {
555+ elements .get (0 ).visit (acg );
556+ }
557+ compileStack .popLHS ();
558+ if (returnRightValue ) rhsValueLoader .visit (acg );
559+ compileStack .removeVar (rhsValueId );
560+ return ;
561+ }
562+
563+ // GEP-20 head/middle rest: def (*f, last) = list, def (l, *m, r) = list, etc.
564+ // Requires a sized, indexable RHS (Path B only — no iterator fallback).
565+ // Load-bearing ordering (GEP lines 177-186): the IntRange call for the rest slot
566+ // must be emitted BEFORE any negative-index call, so that an iterator/stream RHS
567+ // fails fast with MissingMethodException instead of hanging via materialisation.
568+ if (hasRest && !tailRest ) {
569+ // 1. Emit the IntRange call for the rest slot first, via the helper that
570+ // returns an empty slice for inverted ranges (short RHS) and fails fast
571+ // for non-indexable RHS (iterator/stream/set), per GEP lines 177-186.
572+ // Number of fixed slots after the rest = tupleSize - restIndex - 1; their negative
573+ // indices span [-k, -1]; the rest slice therefore ends at -(k+1) = -(tupleSize - restIndex).
574+ // e.g. def (*f,last): -2; def (l,*m,r): -2; def (a,b,*m,y,z): -3
575+ int toIdx = -(tupleSize - restIndex );
576+ MethodCallExpression sliceCall = callX (
577+ classX (MultipleAssignmentSupport .class ),
578+ "nonTailRestSlice" ,
579+ args (rhsValueLoader , constX (restIndex , true ), constX (toIdx , true )));
580+ sliceCall .setImplicitThis (false );
581+ sliceCall .visit (acg );
582+ assignOneMultiAssignSlot (elements .get (restIndex ), defineVariable , operandStack , compileStack , acg );
583+
584+ // 2. Positive-index fixed slots (before rest), left-to-right.
585+ for (int idx = 0 ; idx < restIndex ; idx ++) {
586+ MethodCallExpression call = callX (rhsValueLoader , "getAt" , constX (idx , true ));
587+ call .setImplicitThis (false );
588+ call .visit (acg );
589+ assignOneMultiAssignSlot (elements .get (idx ), defineVariable , operandStack , compileStack , acg );
590+ }
591+
592+ // 3. Negative-index fixed slots (after rest), left-to-right.
593+ for (int idx = restIndex + 1 ; idx < tupleSize ; idx ++) {
594+ int negIdx = -(tupleSize - idx );
595+ MethodCallExpression call = callX (rhsValueLoader , "getAt" , constX (negIdx , true ));
596+ call .setImplicitThis (false );
597+ call .visit (acg );
598+ assignOneMultiAssignSlot (elements .get (idx ), defineVariable , operandStack , compileStack , acg );
599+ }
600+
601+ compileStack .popLHS ();
602+ if (returnRightValue ) rhsValueLoader .visit (acg );
603+ compileStack .removeVar (rhsValueId );
604+ return ;
605+ }
606+
508607 MethodCallExpression iterator = callX (rhsValueLoader , "iterator" );
509608 iterator .setImplicitThis (false );
510609 iterator .visit (acg );
@@ -529,9 +628,17 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
529628 mv .visitJumpInsn (IF_ACMPEQ , useGetAt );
530629
531630 boolean first = true ;
532- for (Expression e : (TupleExpression ) leftExpression ) {
533- if (first ) {
534- first = false ;
631+ for (int idx = 0 ; idx < tupleSize ; idx ++) {
632+ Expression e = elements .get (idx );
633+ if (idx == restIndex ) { // tail rest: dispatch Path B (slice) vs Path C (iterator) at runtime
634+ MethodCallExpression restCall = callX (
635+ classX (MultipleAssignmentSupport .class ),
636+ "tailRest" ,
637+ args (rhsValueLoader , constX (idx , true ), seq ));
638+ restCall .setImplicitThis (false );
639+ restCall .visit (acg );
640+ } else if (first ) {
641+ first = false ; // value already on stack from next() above
535642 } else {
536643 ternaryX (hasNext , next , nullX ()).visit (acg );
537644 }
@@ -553,11 +660,19 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
553660
554661 mv .visitLabel (useGetAt_noPop );
555662
556- int i = 0 ;
557- for (Expression e : (TupleExpression ) leftExpression ) {
558- MethodCallExpression getAt = callX (rhsValueLoader , "getAt" , constX (i ++, true ));
559- getAt .setImplicitThis (false );
560- getAt .visit (acg );
663+ for (int idx = 0 ; idx < tupleSize ; idx ++) {
664+ Expression e = elements .get (idx );
665+ MethodCallExpression call ;
666+ if (idx == restIndex ) { // tail rest: dispatch via helper so empty RHS / non-indexable cases are handled uniformly
667+ call = callX (
668+ classX (MultipleAssignmentSupport .class ),
669+ "tailRest" ,
670+ args (rhsValueLoader , constX (idx , true ), seq ));
671+ } else {
672+ call = callX (rhsValueLoader , "getAt" , constX (idx , true ));
673+ }
674+ call .setImplicitThis (false );
675+ call .visit (acg );
561676
562677 if (defineVariable ) {
563678 Variable v = (Variable ) e ;
@@ -585,6 +700,20 @@ public void evaluateEqual(final BinaryExpression expression, final boolean defin
585700 compileStack .removeVar (rhsValueId );
586701 }
587702
703+ /** GEP-20: assign the single value currently on the operand stack to the given declarator slot. */
704+ private void assignOneMultiAssignSlot (final Expression e , final boolean defineVariable ,
705+ final OperandStack operandStack , final CompileStack compileStack ,
706+ final AsmClassGenerator acg ) {
707+ if (defineVariable ) {
708+ Variable v = (Variable ) e ;
709+ operandStack .doGroovyCast (v );
710+ compileStack .defineVariable (v , true );
711+ operandStack .remove (1 );
712+ } else {
713+ e .visit (acg );
714+ }
715+ }
716+
588717 protected void evaluateCompareExpression (final MethodCaller compareMethod , final BinaryExpression expression ) {
589718 Expression leftExp = expression .getLeftExpression ();
590719 Expression rightExp = expression .getRightExpression ();
0 commit comments