// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flags: --harmony-do-expressions --harmony-sloppy-let --allow-natives-syntax
// Flags: --harmony-default-parameters --harmony-destructuring-bind

function returnValue(v) { return v; }
function MyError() {}
var global = this;

function TestBasic() {
  // Looping and lexical declarations
  assertEquals(512, returnValue(do {
    let n = 2;
    for (let i = 0; i < 4; i++) n <<= 2;
  }));

  // Strings do the right thing
  assertEquals("spooky halloween", returnValue(do {
    "happy halloween".replace('happy', 'spooky');
  }));

  // Do expressions with no completion produce an undefined value
  assertEquals(undefined, returnValue(do {}));
  assertEquals(undefined, returnValue(do { var x = 99; }));
  assertEquals(undefined, returnValue(do { function f() {}; }));
  assertEquals(undefined, returnValue(do { let z = 33; }));

  // Propagation of exception
  assertThrows(function() {
    (do {
      throw new MyError();
      "potatoes";
    });
  }, MyError);

  assertThrows(function() {
    return do {
      throw new MyError();
      "potatoes";
    };
  }, MyError);

  // Return value within do-block overrides `return |do-expression|`
  assertEquals("inner-return", (function() {
    return "outer-return" + do {
      return "inner-return";
      "";
    };
  })());

  var count = 0, n = 1;
  // Breaking out |do-expression|
  assertEquals(3, (function() {
    for (var i = 0; i < 10; ++i) (count += 2 * do { if (i === 3) break; ++n });
    return i;
  })());
  // (2 * 2) + (2 * 3) + (2 * 4)
  assertEquals(18, count);

  // Continue in |do-expression|
  count = 0, n = 1;
  assertEquals([1, 3, 5, 7, 9], (function() {
    var values = [];
    for (var i = 0; i < 10; ++i) {
      count += 2 * (do {
        if ((i & 1) === 0) continue;
        values.push(i);
        ++n;
      }) + 1;
    }
    // (2*2) + 1 + (2*3) + 1 + (2*4) + 1 + (2*5) + 1 + (2*6) + 1
    return values;
  })());
  assertEquals(count, 45);

  assertThrows("(do { break; });", SyntaxError);
  assertThrows("(do { continue; });", SyntaxError);

  // Real-world use case for desugaring
  var array = [1, 2, 3, 4, 5], iterable = [6, 7, 8,9];
  assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9], do {
    for (var element of iterable) array.push(element);
    array;
  });

  // Nested do-expressions
  assertEquals(125, do { (do { (do { 5 * 5 * 5 }) }) });

  // Directives are not honoured
  (do {
    "use strict";
    foo = 80;
    assertEquals(foo, 80);
  });

  // Non-empty operand stack testing
  var O = {
    method1() {
      let x = 256;
      return x + do {
        for (var i = 0; i < 4; ++i) x += i;
      } + 17;
    },
    method2() {
      let x = 256;
      this.reset();
      return x + do {
        for (var i = 0; i < this.length(); ++i) x += this.index() * 2;
      };
    },
    _index: 0,
    index() {
      return ++this._index;
    },
    _length: 4,
    length() { return this._length; },
    reset() { this._index = 0; }
  };
  assertEquals(535, O["method" + do { 1 } + ""]());
  assertEquals(532, O["method" + do { ({ valueOf() { return "2"; } }); }]());
  assertEquals(532, O[
      do { let s = ""; for (let c of "method") s += c; } + "2"]());
}
TestBasic();


function TestDeoptimization1() {
  function f(v) {
    return 88 + do {
      v.a * v.b + v.c;
    };
  }

  var o1 = {};
  o1.a = 10;
  o1.b = 5;
  o1.c = 50;

  var o2 = {};
  o2.c = 100;
  o2.a = 10;
  o2.b = 10;

  assertEquals(188, f(o1));
  assertEquals(188, f(o1));
  %OptimizeFunctionOnNextCall(f);
  assertEquals(188, f(o1));
  assertOptimized(f);
  assertEquals(288, f(o2));
  assertUnoptimized(f);
  assertEquals(288, f(o2));
}
TestDeoptimization1();


function TestInParameterInitializers() {
  var first_name = "George";
  var last_name = "Jetson";
  function fn1(name = do { first_name + " " + last_name }) {
    return name;
  }
  assertEquals("George Jetson", fn1());

  var _items = [1, 2, 3, NaN, 4, 5];
  function fn2(items = do {
    let items = [];
    for (var el of _items) {
      if (el !== el) {
        items;
        break;
      }
      items.push(el), items;
    }
  }) {
    return items;
  }
  assertEquals([1, 2, 3], fn2());

  function thrower() { throw new MyError(); }
  function fn3(exception = do {  try { thrower(); } catch (e) { e } }) {
    return exception;
  }
  assertDoesNotThrow(fn3);
  assertInstanceof(fn3(), MyError);

  function fn4(exception = do { throw new MyError() }) {}
  function catcher(fn) {
    try {
      fn();
      assertUnreachable("fn() initializer should throw");
    } catch (e) {
      assertInstanceof(e, MyError);
    }
  }
  catcher(fn4);
}
TestInParameterInitializers();


function TestWithEval() {
  (function sloppy1() {
    assertEquals(do { eval("var x = 5"), x }, 5);
    assertEquals(x, 5);
  })();

  assertThrows(function strict1() {
    "use strict";
    (do { eval("var x = 5"), x }, 5);
  }, ReferenceError);

  assertThrows(function strict2() {
    (do { eval("'use strict'; var x = 5"), x }, 5);
  }, ReferenceError);
}
TestWithEval();


function TestHoisting() {
  (do { var a = 1; });
  assertEquals(a, 1);
  assertEquals(global.a, undefined);

  (do {
    for (let it of [1, 2, 3, 4, 5]) {
      var b = it;
    }
  });
  assertEquals(b, 5);
  assertEquals(global.b, undefined);

  {
    let x = 1

    // TODO(caitp): ensure VariableStatements in |do-expressions| in parameter
    // initializers, are evaluated in the same VariableEnvironment as they would
    // be for eval().
    // function f1(a = do { var x = 2 }, b = x) { return b }
    // assertEquals(1, f1())

    // function f2(a = x, b = do { var x = 2 }) { return a }
    // assertEquals(1, f2())

    function f3({a = do { var x = 2 }, b = x}) { return b }
    assertEquals(2, f3({}))

    function f4({a = x, b = do { var x = 2 }}) { return b }
    assertEquals(undefined, f4({}))

    function f5(a = do { var y = 0 }) {}
    assertThrows(() => y, ReferenceError)
  }

  // TODO(caitp): Always block-scope function declarations in |do| expressions
  //(do {
  //  assertEquals(true, inner_func());
  //  function inner_func() { return true; }
  //});
  //assertThrows(function() { return innerFunc(); }, ReferenceError);
}
TestHoisting();


// v8:4661

function tryFinallySimple() { (do { try {} finally {} }); }
tryFinallySimple();
tryFinallySimple();
tryFinallySimple();
tryFinallySimple();

var finallyRanCount = 0;
function tryFinallyDoExpr() {
  return (do {
    try {
      throw "BOO";
    } catch (e) {
      "Caught: " + e + " (" + finallyRanCount + ")"
    } finally {
      ++finallyRanCount;
    }
  });
}
assertEquals("Caught: BOO (0)", tryFinallyDoExpr());
assertEquals(1, finallyRanCount);
assertEquals("Caught: BOO (1)", tryFinallyDoExpr());
assertEquals(2, finallyRanCount);
assertEquals("Caught: BOO (2)", tryFinallyDoExpr());
assertEquals(3, finallyRanCount);
assertEquals("Caught: BOO (3)", tryFinallyDoExpr());
assertEquals(4, finallyRanCount);


function TestOSR() {
  var numbers = do {
    let nums = [];
    for (let i = 0; i < 1000; ++i) {
      let value = (Math.random() * 100) | 0;
      nums.push(value === 0 ? 1 : value), nums;
    }
  };
  assertEquals(numbers.length, 1000);
}

for (var i = 0; i < 64; ++i) TestOSR();
