mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Call methods of class instances inline at untyped call sites (#2605)
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