/* "TypeError: not a function" must name the method that was missing.
 *
 * A bare message is what made the Google search page unfixable from the log:
 * the minified xjs bundle died on some absent binding and the error said only
 * "not a function", with no way to tell which member. The interpreter now
 * records the atom behind every name-carrying fetch -- property, global,
 * local, argument, closure variable -- together with the stack slot and the
 * value it landed in, and the call opcodes use that record only when slot and
 * value both still match and the callee is genuinely not callable.
 *
 * The slot check is the point of half these cases: a name that is sometimes
 * wrong is worse than no name, so where the record cannot be trusted the
 * message must stay bare rather than guess. */

var fail = 0;

function check(what, got, want)
{
	var ok = got === want;
	if (!ok)
		fail++;
	print((ok ? "  ok   " : "  FAIL ") + what +
	      ": got " + got + (ok ? "" : ", want " + want));
}

/* Message of the TypeError thrown by fn(), or a marker if it did not throw
 * one. */
function msg(fn)
{
	try {
		fn();
	} catch (e) {
		return (e instanceof TypeError) ? e.message : "not-a-TypeError";
	}
	return "no-throw";
}

print("not-a-function naming:");

/* The plain case: the property is simply absent. This is what a missing DOM
 * binding looks like from script. */
var obj = {};
check("absent property named", msg(function () { obj.frobnicate(); }),
      "Object.frobnicate is not a function");

/* Present but not callable -- the shape of a binding published as a value
 * where the IDL says operation. Indistinguishable from the case above in the
 * old message, which is exactly why it was useless. */
var stub = { render: {} };
check("non-callable property named", msg(function () { stub.render(); }),
      "Object.render is not a function");

/* Reached through a prototype rather than an own property. */
function Base() {}
Base.prototype.inherited = 42;
check("inherited property named", msg(function () { new Base().inherited(); }),
      "Base.inherited is not a function");

/* Deep member expression: the atom must be the last component, not the
 * intermediate one that also went through get_field. */
var deep = { mid: { leaf: null } };
check("deep member names the leaf", msg(function () { deep.mid.leaf(); }),
      "Object.leaf is not a function");

/* A plain call names the identifier and no receiver: "undefined.bare" would
 * claim a receiver the call never had. */
var bare = null;
check("bare call names the identifier", msg(function () { bare(); }),
      "bare is not a function");

/* Computed access carries no atom either -- OP_get_array_el2, not
 * get_field2. Must not inherit a stale name from an earlier fetch. */
var computed = { a: 1 };
var key = "a";
check("computed access stays unnamed", msg(function () {
	deep.mid.leaf;              /* plants an atom in the record */
	computed[key]();
}), "not a function");

/* The anti-lie case. Evaluating the argument runs its own method call, which
 * overwrites the record before the outer call fails; the slot no longer
 * matches, so the outer message must stay bare. Reporting "ok" here would be
 * a confident wrong answer. */
var helper = { ok: function () { return 1; } };
var outer = { gone: undefined };
check("nested arg call does not misname", msg(function () {
	outer.gone(helper.ok());
}), "not a function");

/* Same shape, one level down: the INNER call is the one that fails, and it is
 * the innermost record, so it does get named. */
check("inner call of a nested pair named", msg(function () {
	helper.ok(outer.gone());
}), "Object.gone is not a function");

/* The receiver is what disambiguates a name shared by unrelated interfaces --
 * "supports is not a function" fits both CSS.supports and relList.supports.
 * A receiver that cannot be described must degrade, not throw. */
check("receiver with a throwing constructor still reports", msg(function () {
	var hostile = Object.defineProperty({}, 'constructor', {
		get: function () { throw new Error('no'); }
	});
	hostile.absent();
}), "object.absent is not a function");
check("null-prototype receiver reports", msg(function () {
	Object.create(null).absent();
}), "object.absent is not a function");

/* Rewriting the message must not cost the traceback -- the stack is the only
 * thing that locates the failure inside a minified bundle. */
var stack = "";
try {
	obj.frobnicate();
} catch (e) {
	stack = String(e.stack || "");
}
check("traceback survives the rewrite", stack.length > 0, true);
check("traceback mentions this file", stack.indexOf("notfn.js") >= 0, true);

/* Unrelated TypeErrors must pass through untouched. */
check("other TypeError untouched",
      msg(function () { null.x; }).indexOf("not a function") >= 0, false);

/* Plain calls, one per way the callee can be fetched. A minified bundle calls
 * its helpers by bare identifier far more often than by member expression, so
 * these are the cases that decide whether a log entry is actionable. */

check("local named", msg(function () {
	var localHelper;
	localHelper();
}), "localHelper is not a function");

/* An initialised local never reaches OP_get_loc: the peephole fuses
 * "store then load" into set_loc, which leaves the value on the stack and is
 * the fetch. Missing this made a1-style code inherit the caller's name. */
check("initialised local named", msg(function () {
	var initialised = 2;
	initialised();
}), "initialised is not a function");

check("argument named", msg(function () {
	(function (argHelper) { argHelper(); })(undefined);
}), "argHelper is not a function");

check("closure variable named", msg((function () {
	var closureHelper;
	return function () { closureHelper(); };
})()), "closureHelper is not a function");

/* A callee that IS callable and throws from inside keeps its own error, even
 * when that error says "not a function" -- otherwise every exception crossing
 * a call frame would be relabelled with the (innocent) callee's name. */
check("inner throw keeps its message", msg(function () {
	function thrower() { throw new TypeError("not a function"); }
	thrower();
}), "not a function");

print(fail === 0 ? "PASS" : "FAIL (" + fail + ")");
