mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Call methods of class instances inline at untyped call sites
Untyped code called a method of a compiled class instance through the site's dispatch helper: the callback that resolves own function-valued properties, then the helper, which normalizes the capsule, tests each class key and unwraps the instance before calling the method. A call of a method dispatch helper from a dynamic-tier function now tests the receiver's capsule key against the helper's plain class branches (a direct call of one method with the helper's arguments) and calls that branch's method with the capsule's instance. A branch qualifies when every subclass the normalization can rebox its key to calls the same method, and when the callback has no branch for the key or those subclasses, so it answers undefined for the capsule. Every other receiver calls the callback before the arguments and then the helper, as before. Module initializers call this way only inside loops.
This commit is contained in:
@@ -68,7 +68,7 @@ import {
|
||||
inputSuffixWrites,
|
||||
} from "./borrowed-inputs.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js";
|
||||
import { capsuleCallSite, emitCapsuleCall, emitJsIcCall, jsIcSiteOf } from "./js-ic.js";
|
||||
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
@@ -110,6 +110,10 @@ export function emitCallExpr(
|
||||
// cache site (two-tier design S4).
|
||||
const site = jsIcSiteOf(host, e);
|
||||
if (site) return emitJsIcCall(host, e, callee, site);
|
||||
// So is a method dispatch helper whose class branches a capsule
|
||||
// receiver can call directly.
|
||||
const capsuleCalls = capsuleCallSite(host, e, callee);
|
||||
if (capsuleCalls) return emitCapsuleCall(host, e, callee, capsuleCalls);
|
||||
}
|
||||
if (callee.fastRead === "index" && !plainCall && !awaitEntry && !resultSlot) {
|
||||
const indexed = emitNumericIndexRead(host, e, callee);
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
* keyed dense array element at an integral number key: inline;
|
||||
* otherwise scr_js_keyed_probe (string keys of ordinary objects)
|
||||
* method dynInvoke's method read: scr_js_load_probe
|
||||
* call a method dispatch helper's call: a capsule whose key is one of
|
||||
* the helper's plain class branches calls that branch's method
|
||||
* directly (capsuleCallSite); otherwise the helper
|
||||
* literal an object literal's constant-key member: scr_js_store_probe,
|
||||
* else scr_dyn_key_set
|
||||
*
|
||||
@@ -35,6 +38,7 @@ import { mangleBorrowedFunction } from "../mangle.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { classFieldBase, classFieldIndex } from "./classes.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { dispatchesCall } from "./integer-clones.js";
|
||||
import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { JS_BITS } from "./jsval-encoding.js";
|
||||
import { jsImmediates } from "./jsval.js";
|
||||
@@ -535,6 +539,320 @@ function inlineCapsuleLoad(
|
||||
});
|
||||
}
|
||||
|
||||
/** A class instance's answer that a method dispatch helper gives by
|
||||
* calling one compiled method: the branch `if (dyn.typedRefIs(p, KEY)) {
|
||||
* M(R, p1, ..., pn); return [js.fromDyn](p) }` of a method that returns
|
||||
* its receiver, or `{ return [js.fromDyn](M(R, p1, ..., pn)) }`, with R
|
||||
* `dynCheck(p)` or an upcast of it. */
|
||||
interface CapsuleCall {
|
||||
key: string;
|
||||
className: string;
|
||||
method: string;
|
||||
/** The branch returns the receiver after the call. */
|
||||
returnsReceiver: boolean;
|
||||
}
|
||||
|
||||
/** A call of a method dispatch helper `%dyn.class.call.N(receiver,
|
||||
* %dyn.class.callback.N(receiver), args..., calleeName)` whose class
|
||||
* branches a site can call inline. */
|
||||
export interface CapsuleCallSite {
|
||||
callback: IrFunction;
|
||||
calls: CapsuleCall[];
|
||||
}
|
||||
|
||||
/** The capsule call site a call from dynamic-tier code is, or null.
|
||||
*
|
||||
* The helper normalizes its receiver like a read helper, then tests each
|
||||
* class branch; the callback the site calls before evaluating the
|
||||
* arguments resolves own function-valued properties of bag classes. A
|
||||
* capsule whose key is a branch's key calls the branch's method directly
|
||||
* when the branch qualifies under capsuleReads' rule (every subclass the
|
||||
* normalization can rebox the key to has a branch calling the same
|
||||
* method) and the callback has no branch for the key or any of those
|
||||
* subclasses, so it answers undefined for the capsule without effects.
|
||||
* Module initializers call this way only inside loops (jsIcMethodSites). */
|
||||
export function capsuleCallSite(
|
||||
host: Host,
|
||||
e: ExprOf<"call">,
|
||||
helper: IrFunction,
|
||||
): CapsuleCallSite | null {
|
||||
if (!jsIcMethods(host) || !helper.name.startsWith("%dyn.class.call.")) return null;
|
||||
if (helper.async !== undefined || helper.generator !== undefined) return null;
|
||||
if (e.type.kind !== "jsval" && e.type.kind !== "dyn") return null;
|
||||
const { params } = helper;
|
||||
const arity = params.length - 3;
|
||||
if (
|
||||
arity < 0 ||
|
||||
e.args.length !== params.length ||
|
||||
params[1]!.localId !== "p.callback" ||
|
||||
params[params.length - 1]!.localId !== "p.calleeName" ||
|
||||
params.slice(2, -1).some((p, i) => p.localId !== `p.${i + 1}`)
|
||||
)
|
||||
return null;
|
||||
const [receiver, resolve] = e.args;
|
||||
if (
|
||||
receiver?.kind !== "varRef" ||
|
||||
resolve?.kind !== "call" ||
|
||||
resolve.args.length !== 1 ||
|
||||
!isLocal(resolve.args[0]!, receiver.localId)
|
||||
)
|
||||
return null;
|
||||
const callback = host.fnByName.get(resolve.callee);
|
||||
if (callback === undefined || callback.params.length !== 1) return null;
|
||||
const resolved = callbackKeys(callback);
|
||||
if (resolved === null) return null;
|
||||
const branches = new Map<string, CapsuleCall | null>();
|
||||
const self = params[0]!.localId;
|
||||
for (const stmt of helper.body) {
|
||||
if (normalizes(stmt, self)) continue;
|
||||
if (stmt.kind !== "if" || stmt.else_ !== null) break;
|
||||
if (notCapsule(stmt.cond, self) || classObjectTest(stmt.cond, self)) continue;
|
||||
const key = brandKey(stmt.cond, self);
|
||||
if (key === null) return null;
|
||||
if (!branches.has(key)) branches.set(key, capsuleCall(host, stmt, self, key, arity));
|
||||
}
|
||||
const reboxes = normalizationTargets(host);
|
||||
if (reboxes === null) return null;
|
||||
const calls: CapsuleCall[] = [];
|
||||
for (const call of branches.values()) {
|
||||
if (call === null) continue;
|
||||
const targets = reboxes.get(call.key) ?? [];
|
||||
if (resolved.has(call.key) || targets.some((t) => resolved.has(`object:${t}`))) continue;
|
||||
const same = targets.every((target) => {
|
||||
const other = branches.get(`object:${target}`);
|
||||
return (
|
||||
other != null &&
|
||||
other.method === call.method &&
|
||||
other.returnsReceiver === call.returnsReceiver
|
||||
);
|
||||
});
|
||||
if (same) calls.push(call);
|
||||
}
|
||||
return calls.length > 0 ? { callback, calls } : null;
|
||||
}
|
||||
|
||||
/** The capsule keys a dispatch callback has branches for, or null when
|
||||
* its body has another form: the normalization, branches on capsule or
|
||||
* class-object tests, and a final return of undefined. */
|
||||
function callbackKeys(callback: IrFunction): Set<string> | null {
|
||||
const value = callback.params[0]!.localId;
|
||||
const keys = new Set<string>();
|
||||
const body = callback.body;
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const stmt = body[i]!;
|
||||
if (normalizes(stmt, value)) continue;
|
||||
if (stmt.kind === "if" && stmt.else_ === null) {
|
||||
if (notCapsule(stmt.cond, value) || classObjectTest(stmt.cond, value)) continue;
|
||||
const key = brandKey(stmt.cond, value);
|
||||
if (key === null) return null;
|
||||
keys.add(key);
|
||||
continue;
|
||||
}
|
||||
if (i === body.length - 1 && stmt.kind === "return" && returnsUndefined(stmt.value))
|
||||
return keys;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `dyn.classIs(p, "classval:C")`: true only for a class object, never
|
||||
* for a capsule. */
|
||||
function classObjectTest(cond: IrExpr, receiver: string): boolean {
|
||||
return (
|
||||
cond.kind === "libCall" &&
|
||||
cond.fn === "dyn.classIs" &&
|
||||
cond.args.length === 2 &&
|
||||
isLocal(cond.args[0]!, receiver)
|
||||
);
|
||||
}
|
||||
|
||||
function returnsUndefined(value: IrExpr | null): boolean {
|
||||
if (value?.kind === "libCall" && value.fn === "js.fromDyn" && value.args.length === 1)
|
||||
value = value.args[0]!;
|
||||
return (
|
||||
value?.kind === "dynFrom" && value.value.kind === "unitLit" && value.value.unit === "undefined"
|
||||
);
|
||||
}
|
||||
|
||||
/** The method a class branch calls, or null for a branch of another
|
||||
* form. */
|
||||
function capsuleCall(
|
||||
host: Host,
|
||||
branch: IrStmt & { kind: "if" },
|
||||
receiver: string,
|
||||
key: string,
|
||||
arity: number,
|
||||
): CapsuleCall | null {
|
||||
const [first, second] = branch.then;
|
||||
let call: IrExpr | null | undefined;
|
||||
let returnsReceiver: boolean;
|
||||
if (branch.then.length === 2 && first?.kind === "exprStmt" && second?.kind === "return") {
|
||||
call = first.expr;
|
||||
let value = second.value;
|
||||
if (value?.kind === "libCall" && value.fn === "js.fromDyn" && value.args.length === 1)
|
||||
value = value.args[0]!;
|
||||
if (value === null || !isLocal(value, receiver) || call.type.kind !== "void") return null;
|
||||
returnsReceiver = true;
|
||||
} else if (branch.then.length === 1 && first?.kind === "return") {
|
||||
call = first.value;
|
||||
if (call?.kind === "libCall" && call.fn === "js.fromDyn" && call.args.length === 1)
|
||||
call = call.args[0]!;
|
||||
returnsReceiver = false;
|
||||
} else return null;
|
||||
if (call?.kind !== "call" || call.args.length !== arity + 1) return null;
|
||||
let self = call.args[0]!;
|
||||
if (self.kind === "upcast") self = self.value;
|
||||
if (self.kind !== "dynCheck" || !isLocal(self.value, receiver) || self.type.kind !== "object")
|
||||
return null;
|
||||
if (key !== `object:${self.type.className}`) return null;
|
||||
if (!call.args.slice(1).every((a, i) => isLocal(a, `p.${i + 1}`))) return null;
|
||||
if (!plainMethod(host, call.callee, returnsReceiver)) return null;
|
||||
return { key, className: self.type.className, method: call.callee, returnsReceiver };
|
||||
}
|
||||
|
||||
/** A method the site can call with the ordinary or borrowed ABI and a
|
||||
* dyn, jsval or void result. */
|
||||
function plainMethod(host: Host, name: string, returnsReceiver: boolean): boolean {
|
||||
const fn = host.fnByName.get(name);
|
||||
if (fn === undefined || fn.async !== undefined || fn.generator !== undefined) return false;
|
||||
if (fn.captures !== undefined) return false;
|
||||
if (host.int32Abis.has(name)) return false;
|
||||
if (dispatchesCall(host.integerClones, host.currentFunctionName, name)) return false;
|
||||
const result = fn.returnType.kind;
|
||||
return returnsReceiver ? result === "void" : result === "dyn" || result === "jsval";
|
||||
}
|
||||
|
||||
/** A call of a method dispatch helper from dynamic-tier code whose class
|
||||
* branches qualify (capsuleCallSite). A capsule with one of their keys
|
||||
* skips the callback (it answers undefined for it) and calls the
|
||||
* branch's method with the capsule's instance; anything else calls the
|
||||
* callback before the arguments and then the helper, as the plain call
|
||||
* does. The arguments are evaluated once, after the callback. */
|
||||
export function emitCapsuleCall(
|
||||
host: Host,
|
||||
e: ExprOf<"call">,
|
||||
helper: IrFunction,
|
||||
site: CapsuleCallSite,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const receiver = emitBorrowedInput(host, e.args[0]!);
|
||||
const capsule = B.newLabel("iccall.cap");
|
||||
const resolve = B.newLabel("iccall.cb");
|
||||
const evaluate = B.newLabel("iccall.args");
|
||||
const matched = site.calls.map(() => B.newLabel("iccall.k"));
|
||||
kindIs(host, receiver.name, DYN_KIND.TYPED_REF, capsule, resolve);
|
||||
B.startBlock(capsule);
|
||||
// Capsules boxed by this program share its key constants.
|
||||
const key = load(host, "ptr", receiver.name, 40);
|
||||
site.calls.forEach((call, i) => {
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq ptr ${key}, ${host.cstr(call.key)}`);
|
||||
const next = i + 1 < site.calls.length ? B.newLabel("iccall.t") : resolve;
|
||||
B.condBr(same, matched[i]!, next);
|
||||
if (next !== resolve) B.startBlock(next);
|
||||
});
|
||||
for (const label of matched) {
|
||||
B.startBlock(label);
|
||||
B.br(evaluate);
|
||||
}
|
||||
B.startBlock(resolve);
|
||||
const resolved = helperCall(host, e.args[1] as ExprOf<"call">, site.callback, [receiver]);
|
||||
const resolveEnd = B.currentLabel();
|
||||
B.br(evaluate);
|
||||
B.startBlock(evaluate);
|
||||
// The branch a capsule takes, or -1 for the helper.
|
||||
const branch = B.tmp();
|
||||
const branches = matched.map((label, i) => `[ ${i}, %${label} ]`);
|
||||
B.line(`${branch} = phi i32 ${[...branches, `[ -1, %${resolveEnd} ]`].join(", ")}`);
|
||||
// The callback's result for an inline capsule is never read: undefined
|
||||
// for a jsval result, null for a dyn one.
|
||||
const unread =
|
||||
site.callback.returnType.kind === "jsval"
|
||||
? `inttoptr (i64 ${JS_BITS.undefined} to ptr)`
|
||||
: "null";
|
||||
const cb = B.tmp();
|
||||
const cbIn = matched.map((label) => `[ ${unread}, %${label} ]`);
|
||||
B.line(`${cb} = phi ptr ${[...cbIn, `[ ${resolved}, %${resolveEnd} ]`].join(", ")}`);
|
||||
host.own({ name: cb, type: site.callback.returnType });
|
||||
const rest = e.args.slice(2).map((a) => emitBorrowedInput(host, a));
|
||||
const slow = B.newLabel("iccall.slow");
|
||||
const join = B.newLabel("iccall.j");
|
||||
const calls = site.calls.map(() => B.newLabel("iccall.m"));
|
||||
const cases = calls.map((label, i) => `i32 ${i}, label %${label}`).join(" ");
|
||||
B.terminate(`switch i32 ${branch}, label %${slow} [ ${cases} ]`);
|
||||
const incoming: string[] = [];
|
||||
site.calls.forEach((call, i) => {
|
||||
B.startBlock(calls[i]!);
|
||||
const instance = load(host, "ptr", receiver.name, 16);
|
||||
const self: LlValue = { name: instance, type: { kind: "object", className: call.className } };
|
||||
const result = methodCall(host, call.method, [self, ...rest.slice(0, -1)]);
|
||||
let out: string;
|
||||
if (call.returnsReceiver) {
|
||||
const held = host.retainFrame(receiver.name, DYN);
|
||||
out = e.type.kind === "jsval" ? cellWord(host, held) : held;
|
||||
} else {
|
||||
const returned = host.fnByName.get(call.method)!.returnType;
|
||||
out = returned.kind === e.type.kind ? result : asResult(host, result, e, true);
|
||||
}
|
||||
incoming.push(`[ ${out}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
});
|
||||
B.startBlock(slow);
|
||||
const result = helperCall(host, e, helper, [
|
||||
receiver,
|
||||
{ name: cb, type: site.callback.returnType },
|
||||
...rest,
|
||||
]);
|
||||
incoming.push(`[ ${result}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi ptr ${incoming.join(", ")}`);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
/** A held cell as a jsval word. */
|
||||
function cellWord(host: Host, cell: string): string {
|
||||
const B = host.B;
|
||||
const bits = B.tmp();
|
||||
const word = B.tmp();
|
||||
const tagged = B.tmp();
|
||||
B.line(`${bits} = ptrtoint ptr ${cell} to i64`);
|
||||
B.line(`${word} = or i64 ${bits}, ${JS_BITS.cellTag}`);
|
||||
B.line(`${tagged} = inttoptr i64 ${word} to ptr`);
|
||||
return tagged;
|
||||
}
|
||||
|
||||
/** A direct call of a compiled method with the site's evaluated
|
||||
* (borrowed) arguments, as helperCall passes them. Returns the owned
|
||||
* result, or "" for a void method. */
|
||||
function methodCall(host: Host, name: string, args: LlValue[]): string {
|
||||
const B = host.B;
|
||||
const method = host.fnByName.get(name)!;
|
||||
const borrowed = host.callLifetimes.borrowed.get(name);
|
||||
const argList = args
|
||||
.map((a, i) => {
|
||||
const type = method.params[i]!.type;
|
||||
const value = borrowed?.has(i) ? a.name : host.retainFrame(a.name, type);
|
||||
return `${host.llType(type)} ${value}`;
|
||||
})
|
||||
.join(", ");
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(name) : host.callTarget(name)}`;
|
||||
if (method.returnType.kind === "void") {
|
||||
const invoked = host.emitDirectCall(`call void ${target}(${argList})`, name);
|
||||
if (!invoked && host.mayThrow.has(name)) host.emitPendingCheck();
|
||||
return "";
|
||||
}
|
||||
let t = B.tmp();
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(method.returnType)} ${target}(${argList})`,
|
||||
name,
|
||||
);
|
||||
if (!invoked && host.mayThrow.has(name)) host.emitPendingCheck();
|
||||
if (borrowed && host.borrowedReturns.has(name)) t = host.retainFrame(t, method.returnType);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Emits a call to a classified helper as a cache site. */
|
||||
export function emitJsIcCall(
|
||||
host: Host,
|
||||
|
||||
@@ -187,6 +187,70 @@ console.log(read(p), read(t), read(dyn(new Gauge())), read({ x: 1 }), tag(t));
|
||||
}
|
||||
});
|
||||
|
||||
test("method calls on class instances call the method inline", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-capsule-call-"));
|
||||
try {
|
||||
const entry = join(dir, "main.js");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
class Cursor {
|
||||
constructor(x) { this.x = x; }
|
||||
step(d) { this.x += d; return this; }
|
||||
sum(k) { return this.x * k; }
|
||||
}
|
||||
class Marker extends Cursor {
|
||||
constructor(x, name) { super(x); this.name = name; }
|
||||
}
|
||||
class Fast extends Cursor {
|
||||
step(d) { this.x += d * 2; return this; }
|
||||
}
|
||||
class Shadowed {
|
||||
scale(k) { return k; }
|
||||
}
|
||||
function move(c) { return c.step(1); }
|
||||
function total(c) { return c.sum(3); }
|
||||
function scaled(c) { return c.scale(2); }
|
||||
var list = [0, 1, 2].map((i) => (i === 0 ? new Cursor(1) : i === 1 ? new Marker(2, "m") : new Fast(3)));
|
||||
var own = new Shadowed();
|
||||
own.scale = function (k) { return -k; };
|
||||
for (const c of list) console.log(move(c).x, total(c));
|
||||
console.log(total({ sum: (k) => k + 1 }), scaled(own), scaled(new Shadowed()));
|
||||
top: for (let i = 0; i < 2; i++) console.log(list[i].sum(i));
|
||||
console.log(list[2].sum(5));
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
const inline = (fn: string): number => fn.match(/^iccall\.m\d+:/gm)?.length ?? 0;
|
||||
// Fast overrides step, so the helper calls Cursor's step on Cursor and
|
||||
// Marker capsules through a virtual call, and only Fast capsules call
|
||||
// step inline. Every class answers sum with Cursor's, so all three
|
||||
// call it inline; every other receiver keeps the callback and the
|
||||
// helper.
|
||||
const move = body(llvm, "move");
|
||||
expect(inline(move)).toBe(1);
|
||||
expect(move).toContain("@sc_bf__x25_dyn_class_receiver_x3a_Fast_step(");
|
||||
expect(move).toMatch(/@sc_b?f__x25_dyn_class_callback_\d+\([^)]*\) noinline cold/);
|
||||
expect(move).toMatch(/@sc_b?f__x25_dyn_class_call_\d+\([^)]*\) noinline cold/);
|
||||
const total = body(llvm, "total");
|
||||
expect(inline(total)).toBe(3);
|
||||
expect(total).toMatch(/switch i32 %t\d+, label %iccall\.slow\d+ \[/);
|
||||
// Shadowed's instances may have an own scale, so its capsules keep the
|
||||
// callback, which reads it.
|
||||
expect(inline(body(llvm, "scaled"))).toBe(0);
|
||||
// A module initializer calls inline only inside its loops.
|
||||
expect(inline(body(llvm, "_x25_init_0"))).toBe(3);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("typed programs have no cache sites and keep the ordinary runtime", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-"));
|
||||
try {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/dynamic-capsule-method-calls.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Untyped code calls methods of compiled class instances: methods that
|
||||
// return their receiver, numbers, strings or nothing, inherited and
|
||||
// overriding methods, an instance whose own property shadows a method,
|
||||
// receivers of mixed kinds at one site, argument evaluation order, missing
|
||||
// and extra arguments, and throws from the called method.
|
||||
class Cursor {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
step(dx, dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
return this;
|
||||
}
|
||||
sum(k) {
|
||||
return (this.x + this.y) * k;
|
||||
}
|
||||
label(prefix) {
|
||||
return prefix + "(" + this.x + "," + this.y + ")";
|
||||
}
|
||||
reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
checked(v) {
|
||||
if (v > 40) throw new RangeError("too large: " + v);
|
||||
return v * 2;
|
||||
}
|
||||
}
|
||||
class Marker extends Cursor {
|
||||
constructor(x, y, name) {
|
||||
super(x, y);
|
||||
this.name = name;
|
||||
}
|
||||
label(prefix) {
|
||||
return "[" + this.name + "] " + super.label(prefix);
|
||||
}
|
||||
}
|
||||
class Fast extends Cursor {
|
||||
step(dx, dy) {
|
||||
this.x += dx * 2;
|
||||
this.y += dy * 2;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
const order = [];
|
||||
function note(tag, value) {
|
||||
order.push(tag);
|
||||
return value;
|
||||
}
|
||||
|
||||
function drive(list, rounds) {
|
||||
let total = 0;
|
||||
let text = "";
|
||||
for (let r = 0; r < rounds; r++) {
|
||||
const c = list[r % list.length];
|
||||
const moved = c.step((r % 3) - 1, (r % 5) - 2);
|
||||
total += moved === c ? 1 : 1000;
|
||||
total += c.sum(r % 4);
|
||||
if (r % 50 === 0) text += c.label("r" + r) + ";";
|
||||
if (Math.abs(c.x) > 60) c.reset();
|
||||
}
|
||||
return total + " " + text;
|
||||
}
|
||||
|
||||
const cursors = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (i % 3 === 0) cursors.push(new Cursor(i, -i));
|
||||
else if (i % 3 === 1) cursors.push(new Marker(i, i * 2, "m" + i));
|
||||
else cursors.push(new Fast(-i, i));
|
||||
}
|
||||
console.log(drive(cursors, 400));
|
||||
|
||||
// Instances built by one expression for both classes.
|
||||
const mixed = [];
|
||||
for (let i = 0; i < 4; i++) mixed.push(i % 2 === 0 ? new Cursor(i, i) : new Marker(i, -i, "t" + i));
|
||||
console.log(drive(mixed, 200));
|
||||
console.log(mixed.map((c) => c.label("m")).join(" "));
|
||||
|
||||
// The same site sees object literals and a shadowing own property.
|
||||
const literal = {
|
||||
x: 1,
|
||||
y: 2,
|
||||
step(dx, dy) {
|
||||
this.x -= dx;
|
||||
this.y -= dy;
|
||||
return this;
|
||||
},
|
||||
sum(k) {
|
||||
return k;
|
||||
},
|
||||
label(prefix) {
|
||||
return prefix + "!";
|
||||
},
|
||||
reset() {},
|
||||
};
|
||||
const shadowed = new Cursor(5, 5);
|
||||
shadowed.sum = function (k) {
|
||||
return -k;
|
||||
};
|
||||
console.log(drive([cursors[0], literal, shadowed, cursors[1]], 120));
|
||||
|
||||
// Chained calls and results used as values.
|
||||
let chain = new Marker(1, 1, "chain");
|
||||
for (let i = 0; i < 20; i++) chain = chain.step(1, 2).step(-0.5, 0.25);
|
||||
console.log(chain.x, chain.y, chain.name, chain.label("end"));
|
||||
|
||||
// Arguments are evaluated after the receiver, left to right.
|
||||
const target = new Cursor(0, 0);
|
||||
for (let i = 0; i < 3; i++) note("recv" + i, target).step(note("a" + i, 1), note("b" + i, 2));
|
||||
console.log(order.join(","), target.x, target.y);
|
||||
|
||||
// Missing and extra arguments.
|
||||
const loose = new Fast(1, 1);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
loose.step(1);
|
||||
loose.step(1, 2, 3);
|
||||
}
|
||||
console.log(loose.x, Number.isNaN(loose.y), cursors[2].sum(), cursors[3].reset());
|
||||
|
||||
// A throw from the called method unwinds to the caller's handler.
|
||||
let caught = 0;
|
||||
let doubled = 0;
|
||||
for (let i = 35; i < 45; i++) {
|
||||
try {
|
||||
doubled += cursors[i % 2].checked(i);
|
||||
} catch (e) {
|
||||
if (caught++ === 0) console.log(e instanceof RangeError, e.message);
|
||||
}
|
||||
}
|
||||
console.log(caught, doubled);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3900, "san": 7200 }
|
||||
Reference in New Issue
Block a user