From 720ed32b4d93310f80744793f6c643e7fe577214 Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:44:29 -0600 Subject: [PATCH] Store the constant pool in typed arrays instead of an object per entry --- src/classfile.ts | 45 +++++++++++++++++++------- src/indy.ts | 79 ++++++++++++++++++++++------------------------ src/jit.ts | 19 ++++++----- src/jvm.ts | 32 +++++++++---------- src/natives/npe.ts | 6 ++-- 5 files changed, 98 insertions(+), 83 deletions(-) diff --git a/src/classfile.ts b/src/classfile.ts index aa0bd6b..2bd2656 100644 --- a/src/classfile.ts +++ b/src/classfile.ts @@ -16,6 +16,15 @@ export interface CpEntry { b: number; } +// Utf8 and numeric values live in values. Other entries keep a and b as two +// u16 in one word of ab. +export interface ConstantPool { + length: number; + values: (string | number | bigint | undefined)[]; + tags: Uint8Array; + ab: Uint32Array; +} + export interface ExceptionEntry { start: number; end: number; @@ -52,7 +61,7 @@ export interface ClassFile { renamedFrom?: string; minor: number; major: number; - cp: (CpEntry | string | undefined)[]; + cp: ConstantPool; access: number; name: string; superName: string | null; @@ -181,22 +190,27 @@ function readCode(raw: Uint8Array, cp: ClassFile["cp"]): Code { return { maxStack, maxLocals, code, handlers, lines, localVars }; } -// UTF-8 entries are stored as strings to avoid an object per constant. export function cpEntry(cp: ClassFile["cp"], i: number): CpEntry | undefined { - const e = cp[i]; - return typeof e === "string" ? { tag: Tag.Utf8, value: e, a: 0, b: 0 } : e; + const tag = cp.tags[i]; + if (!tag) return undefined; + const value = cp.values[i]; + return { tag, value, a: cp.ab[i] >>> 16, b: cp.ab[i] & 0xffff }; } +export function cpTag(cp: ClassFile["cp"], i: number): number { return cp.tags[i]; } +export function cpA(cp: ClassFile["cp"], i: number): number { return cp.ab[i] >>> 16; } +export function cpB(cp: ClassFile["cp"], i: number): number { return cp.ab[i] & 0xffff; } +export function cpValue(cp: ClassFile["cp"], i: number): number | bigint | undefined { return cp.values[i] as number | bigint | undefined; } + export function utf8(cp: ClassFile["cp"], i: number): string { - const e = cp[i]; - if (typeof e !== "string") throw new ClassFormatError(`bad utf8 index ${i}`); - return e; + const s = cp.values[i]; + if (typeof s !== "string") throw new ClassFormatError(`bad utf8 index ${i}`); + return s; } export function className(cp: ClassFile["cp"], i: number): string { - const e = cpEntry(cp, i); - if (!e || e.tag !== Tag.Class) throw new ClassFormatError(`bad class index ${i}`); - return utf8(cp, e.a); + if (cp.tags[i] !== Tag.Class) throw new ClassFormatError(`bad class index ${i}`); + return utf8(cp, cp.ab[i] >>> 16); } export function parseClass(bytes: Uint8Array, preview = false): ClassFile { @@ -221,7 +235,12 @@ function parseBody(r: Reader, preview: boolean): ClassFile { if (major < 45 || major > MAX_MAJOR) throw new UnsupportedVersion(major, minor); if (major >= 56 && minor !== 0 && !(minor === 0xffff && major === MAX_MAJOR && preview)) throw new UnsupportedVersion(major, minor); const count = r.u2(); - const cp: ClassFile["cp"] = new Array(count); + const cp: ClassFile["cp"] = { + length: count, + values: new Array(count), + tags: new Uint8Array(count), + ab: new Uint32Array(count), + }; for (let i = 1; i < count; i++) { const tag = r.u1(); let value: CpEntry["value"], a = 0, b = 0; @@ -239,7 +258,9 @@ function parseBody(r: Reader, preview: boolean): ClassFile { a = r.u2(); b = r.u2(); break; default: throw new ClassFormatError(`bad constant pool tag ${tag} at ${i}`); } - cp[i] = tag === Tag.Utf8 ? value as string : { tag, value, a, b }; + cp.tags[i] = tag; + if (value !== undefined) cp.values[i] = value; + cp.ab[i] = a << 16 | b; if (tag === Tag.Long || tag === Tag.Double) i++; } const access = r.u2(); diff --git a/src/indy.ts b/src/indy.ts index 2a0d411..f1666fe 100644 --- a/src/indy.ts +++ b/src/indy.ts @@ -5,7 +5,7 @@ // the same thing the JDK bootstraps do with the ClassFile API, minus the // cost of running that machinery in the interpreter. -import { cpEntry, Tag, className, utf8 } from "./classfile.ts"; +import { cpA, cpB, cpTag, cpValue, Tag, className, utf8 } from "./classfile.ts"; import { ClassWriter } from "./classwriter.ts"; import type { Code } from "./classwriter.ts"; import { @@ -42,22 +42,22 @@ export type BsmArg = export function cpArg(cls: JClass, idx: number): BsmArg { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - switch (e.tag) { - case Tag.Integer: case Tag.Float: case Tag.Long: case Tag.Double: return { kind: "value", tag: e.tag, value: e.value }; - case Tag.String: return { kind: "value", tag: e.tag, value: utf8(cp, e.a) }; + const tag = cpTag(cp, idx), a = cpA(cp, idx), b = cpB(cp, idx); + switch (tag) { + case Tag.Integer: case Tag.Float: case Tag.Long: case Tag.Double: return { kind: "value", tag, value: cpValue(cp, idx) }; + case Tag.String: return { kind: "value", tag, value: utf8(cp, a) }; case Tag.Class: return { kind: "class", name: className(cp, idx) }; - case Tag.MethodType: return { kind: "mt", desc: utf8(cp, e.a) }; + case Tag.MethodType: return { kind: "mt", desc: utf8(cp, a) }; case Tag.MethodHandle: { - const ref = cpEntry(cp, e.b)!, nat = cpEntry(cp, ref.b)!; + const ref = b, nat = cpB(cp, ref); return { - kind: "mh", refKind: e.a, owner: className(cp, ref.a), name: utf8(cp, nat.a), desc: utf8(cp, nat.b), - iface: ref.tag === Tag.InterfaceMethodref, + kind: "mh", refKind: a, owner: className(cp, cpA(cp, ref)), name: utf8(cp, cpA(cp, nat)), desc: utf8(cp, cpB(cp, nat)), + iface: cpTag(cp, ref) === Tag.InterfaceMethodref, }; } case Tag.Dynamic: return { kind: "condy", index: idx }; } - throw new PendingThrow("java/lang/InternalError", `bad bootstrap argument tag ${e.tag}`); + throw new PendingThrow("java/lang/InternalError", `bad bootstrap argument tag ${tag}`); } function wordSize(descs: string[]) { @@ -434,15 +434,14 @@ function* methodHandleConstant(vm: JVM, t: JThread, cls: JClass, mh: MethodHandl function* dynamicConstant(vm: JVM, t: JThread, cls: JClass, idx: number): NativeGen { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - const bsm = cls.cf!.bootstrap[e.a]; - const nat = cpEntry(cp, e.b)!; + const bsm = cls.cf!.bootstrap[cpA(cp, idx)]; + const nat = cpB(cp, idx); const bsmHandle = yield* constantObject(vm, t, cls, bsm.ref); - const type: JClass = yield* typeOf(t, cls, utf8(cp, nat.b)); + const type: JClass = yield* typeOf(t, cls, utf8(cp, cpB(cp, nat))); const args = yield* staticArgs(vm, t, cls, bsm.args); return yield* upcall(vm, t, "linkDynamicConstant(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", - vm.mirror(cls), bsmHandle, vm.intern(utf8(cp, nat.a)), vm.mirror(type), args); + vm.mirror(cls), bsmHandle, vm.intern(utf8(cp, cpA(cp, nat))), vm.mirror(type), args); } function* staticArgs(vm: JVM, t: JThread, cls: JClass, idxs: number[]): NativeGen { @@ -455,31 +454,31 @@ function* staticArgs(vm: JVM, t: JThread, cls: JClass, idxs: number[]): NativeGe // The Java object for a loadable constant, boxing primitives. export function* constantObject(vm: JVM, t: JThread, cls: JClass, idx: number): NativeGen { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - switch (e.tag) { - case Tag.Integer: return yield* box(t, "I", e.value); - case Tag.Float: return yield* box(t, "F", e.value); - case Tag.Long: return yield* box(t, "J", e.value); - case Tag.Double: return yield* box(t, "D", e.value); - case Tag.String: return vm.intern(utf8(cp, e.a)); + const tag = cpTag(cp, idx), a = cpA(cp, idx), b = cpB(cp, idx), value = cpValue(cp, idx); + switch (tag) { + case Tag.Integer: return yield* box(t, "I", value); + case Tag.Float: return yield* box(t, "F", value); + case Tag.Long: return yield* box(t, "J", value); + case Tag.Double: return yield* box(t, "D", value); + case Tag.String: return vm.intern(utf8(cp, a)); case Tag.Class: return vm.mirror(yield* vm.loadClassGen(t, cls.loader, className(cp, idx))); } const cached = cls.ldc[idx]; if (cached !== undefined) { - if (e.tag !== Tag.Dynamic) return cached; - const desc = utf8(cp, cpEntry(cp, e.b)!.b); + if (tag !== Tag.Dynamic) return cached; + const desc = utf8(cp, cpB(cp, b)); return isPrim(desc) ? yield* box(t, desc, cached) : cached; } let v: any; - if (e.tag === Tag.MethodType) v = yield* methodType(vm, t, cls.loader, utf8(cp, e.a)); - else if (e.tag === Tag.MethodHandle) v = yield* methodHandleConstant(vm, t, cls, cpArg(cls, idx) as MethodHandleRef); - else if (e.tag === Tag.Dynamic) { + if (tag === Tag.MethodType) v = yield* methodType(vm, t, cls.loader, utf8(cp, a)); + else if (tag === Tag.MethodHandle) v = yield* methodHandleConstant(vm, t, cls, cpArg(cls, idx) as MethodHandleRef); + else if (tag === Tag.Dynamic) { v = yield* dynamicConstant(vm, t, cls, idx); - const desc = utf8(cp, cpEntry(cp, e.b)!.b); + const desc = utf8(cp, cpB(cp, b)); cls.ldc[idx] = isPrim(desc) && v !== null ? vm.getField(v, v.cls.name, "value") : v; return v; } - else throw new PendingThrow("java/lang/InternalError", `bad constant tag ${e.tag}`); + else throw new PendingThrow("java/lang/InternalError", `bad constant tag ${tag}`); cls.ldc[idx] = v; return v; } @@ -487,10 +486,10 @@ export function* constantObject(vm: JVM, t: JThread, cls: JClass, idx: number): // ldc of a MethodType, MethodHandle or dynamic constant. export function* resolveConstant(vm: JVM, t: JThread, cls: JClass, idx: number): NativeGen { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; + const tag = cpTag(cp, idx), b = cpB(cp, idx); const v = yield* constantObject(vm, t, cls, idx); - if (e.tag === Tag.Dynamic) { - const desc = utf8(cp, cpEntry(cp, e.b)!.b); + if (tag === Tag.Dynamic) { + const desc = utf8(cp, cpB(cp, b)); if (isPrim(desc)) return v === null ? null : vm.getField(v, v.cls.name, "value"); } return v; @@ -498,16 +497,15 @@ export function* resolveConstant(vm: JVM, t: JThread, cls: JClass, idx: number): function* linkGeneral(vm: JVM, t: JThread, cls: JClass, idx: number): NativeGen { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - const bsm = cls.cf!.bootstrap[e.a]; - const nat = cpEntry(cp, e.b)!; + const bsm = cls.cf!.bootstrap[cpA(cp, idx)]; + const nat = cpB(cp, idx); const bsmHandle = yield* constantObject(vm, t, cls, bsm.ref); - const type = yield* methodType(vm, t, cls.loader, utf8(cp, nat.b)); + const type = yield* methodType(vm, t, cls.loader, utf8(cp, cpB(cp, nat))); const args = yield* staticArgs(vm, t, cls, bsm.args); const appendix = vm.newObjectArray("java/lang/Object", [null]); const mn: JObject = yield* upcall(vm, t, "linkCallSite(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/invoke/MemberName;", - vm.mirror(cls), bsmHandle, vm.intern(utf8(cp, nat.a)), type, args, appendix); + vm.mirror(cls), bsmHandle, vm.intern(utf8(cp, cpA(cp, nat))), type, args, appendix); return { m: memberTarget(t, mn), appendix: appendix.a[0] as JObject | null }; } @@ -515,10 +513,9 @@ function* linkGeneral(vm: JVM, t: JThread, cls: JClass, idx: number): NativeGen export function* linkCallSite(vm: JVM, t: JThread, cls: JClass, idx: number): Generator { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - const bsm = cls.cf!.bootstrap[e.a]; - const nat = cpEntry(cp, e.b)!; - const name = utf8(cp, nat.a), desc = utf8(cp, nat.b); + const bsm = cls.cf!.bootstrap[cpA(cp, idx)]; + const nat = cpB(cp, idx); + const name = utf8(cp, cpA(cp, nat)), desc = utf8(cp, cpB(cp, nat)); const ref = cpArg(cls, bsm.ref) as MethodHandleRef; const args = bsm.args.map((a) => cpArg(cls, a)); let m: JMethod | null = null; diff --git a/src/jit.ts b/src/jit.ts index 255f21a..1bc55c1 100644 --- a/src/jit.ts +++ b/src/jit.ts @@ -14,7 +14,7 @@ // caller spill too, and later delivers its result through the frame like an // interpreted call. -import { cpEntry, Tag, utf8 } from "./classfile.ts"; +import { cpB, cpTag, cpValue, Tag, utf8 } from "./classfile.ts"; import { ACC_FINAL, INITIALIZED, JArray, JClass, JField, JMethod, JObject, isWide, longToFloat, newArrayData, parseMethodDesc, } from "./runtime.ts"; @@ -708,9 +708,9 @@ function insn(g: Gen, b: Body, pc: number) { case 0x11: emit(`${S(d)}=${s16(c, pc + 1)};`); break; case 0x12: case 0x13: case 0x14: { const i = op === 0x12 ? c[pc + 1] : idx2; - const e = cpEntry(cp, i)!; - if (e.tag === Tag.Integer || e.tag === Tag.Float || e.tag === Tag.Double) emit(`${S(d)}=${numLit(e.value as number)};`); - else if (e.tag === Tag.Long) emit(`${S(d)}=${e.value}n;`); + const tag = cpTag(cp, i), value = cpValue(cp, i); + if (tag === Tag.Integer || tag === Tag.Float || tag === Tag.Double) emit(`${S(d)}=${numLit(value as number)};`); + else if (tag === Tag.Long) emit(`${S(d)}=${value}n;`); else { const v = cls.ldc[i]; if (v === undefined) { emit(slow(pc, d)); break; } @@ -966,7 +966,7 @@ function fuseConcat(g: Gen, b: Body, pc: number): { pieces: string[]; end: numbe else if (op === 0x12 || op === 0x13) { const i = op === 0x12 ? c[p + 1] : u16(c, p + 1); const v = cls.ldc[i]; - if (cpEntry(cls.cf!.cp, i)!.tag !== Tag.String || v === undefined) return null; + if (cpTag(cls.cf!.cp, i) !== Tag.String || v === undefined) return null; pieces.push(() => g.K(v)); p += op === 0x12 ? 2 : 3; } else return null; @@ -1249,13 +1249,12 @@ function stackDelta(cls: JClass, c: Uint8Array, pc: number): number { const op = c[pc]; const cp = cls.cf!.cp; const fieldSize = (idx: number) => { - const nat = cpEntry(cp, cpEntry(cp, idx)!.b)!; - return isWide(utf8(cp, nat.b)) ? 2 : 1; + const nat = cpB(cp, idx); + return isWide(utf8(cp, cpB(cp, nat))) ? 2 : 1; }; const methodSlots = (idx: number, withReceiver: boolean) => { - const e = cpEntry(cp, idx)!; - const nat = cpEntry(cp, e.b)!; - const { params, ret } = parseMethodDesc(utf8(cp, nat.b)); + const nat = cpB(cp, idx); + const { params, ret } = parseMethodDesc(utf8(cp, cpB(cp, nat))); let n = withReceiver ? 1 : 0; for (const p of params) n += isWide(p) ? 2 : 1; return (ret === "V" ? 0 : isWide(ret) ? 2 : 1) - n; diff --git a/src/jvm.ts b/src/jvm.ts index 4e8359c..430ae38 100644 --- a/src/jvm.ts +++ b/src/jvm.ts @@ -2,7 +2,7 @@ // pool resolution, strings, monitors and the thread scheduler. import type { Assertions } from "./assertions.ts"; -import { cpEntry, BadMagic, ClassFormatError, MAX_MAJOR, Tag, UnsupportedVersion, className, parseClass, utf8 } from "./classfile.ts"; +import { cpA, cpB, cpTag, cpValue, BadMagic, ClassFormatError, MAX_MAJOR, Tag, UnsupportedVersion, className, parseClass, utf8 } from "./classfile.ts"; import type { ClassFile } from "./classfile.ts"; import { deliver, raise, runThread } from "./interp.ts"; import { NativeMemory } from "./memory.ts"; @@ -432,12 +432,11 @@ export class JVM { fieldRef(t: JThread, cls: JClass, idx: number): JField | undefined { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - const owner = cls.rcp[e.a] ?? this.classRef(t, cls, e.a); + const ownerIndex = cpA(cp, idx), natIndex = cpB(cp, idx); + const owner = cls.rcp[ownerIndex] ?? this.classRef(t, cls, ownerIndex); if (!owner) return undefined; - const nat = cpEntry(cp, e.b)!; - const name = utf8(cp, nat.a); - const f = owner.findField(name, utf8(cp, nat.b)); + const name = utf8(cp, cpA(cp, natIndex)); + const f = owner.findField(name, utf8(cp, cpB(cp, natIndex))); if (!f) throw new PendingThrow("java/lang/NoSuchFieldError", name); cls.rcp[idx] = f; return f; @@ -445,11 +444,10 @@ export class JVM { methodRef(t: JThread, cls: JClass, idx: number): JMethod | undefined { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; - const owner = cls.rcp[e.a] ?? this.classRef(t, cls, e.a); + const ownerIndex = cpA(cp, idx), natIndex = cpB(cp, idx); + const owner = cls.rcp[ownerIndex] ?? this.classRef(t, cls, ownerIndex); if (!owner) return undefined; - const nat = cpEntry(cp, e.b)!; - const name = utf8(cp, nat.a), desc = utf8(cp, nat.b); + const name = utf8(cp, cpA(cp, natIndex)), desc = utf8(cp, cpB(cp, natIndex)); const m = this.resolveMethod(owner, name, desc); cls.rcp[idx] = m; return m; @@ -485,11 +483,11 @@ export class JVM { // ldc / ldc_w / ldc2_w constant(t: JThread, cls: JClass, idx: number): any { const cp = cls.cf!.cp; - const e = cpEntry(cp, idx)!; + const tag = cpTag(cp, idx), a = cpA(cp, idx); let v: any; - switch (e.tag) { - case Tag.Integer: case Tag.Float: case Tag.Long: case Tag.Double: v = e.value; break; - case Tag.String: v = this.intern(utf8(cp, e.a)); break; + switch (tag) { + case Tag.Integer: case Tag.Float: case Tag.Long: case Tag.Double: v = cpValue(cp, idx); break; + case Tag.String: v = this.intern(utf8(cp, a)); break; case Tag.Class: { const c = this.classRef(t, cls, idx); if (!c) return undefined; @@ -500,7 +498,7 @@ export class JVM { t.pushNative(this.resolveConstantGen(t, cls, idx), null, "V", true); return undefined; default: - throw new PendingThrow("java/lang/InternalError", `bad ldc constant tag ${e.tag}`); + throw new PendingThrow("java/lang/InternalError", `bad ldc constant tag ${tag}`); } cls.ldc[idx] = v; return v; @@ -624,8 +622,8 @@ export class JVM { if (!f.isStatic) continue; const raw = f.attrs.get("ConstantValue"); if (!raw) continue; - const e = cpEntry(cp, (raw[0] << 8) | raw[1])!; - c.statics[f.slot] = e.tag === Tag.String ? this.intern(utf8(cp, e.a)) : e.value; + const i = (raw[0] << 8) | raw[1], tag = cpTag(cp, i); + c.statics[f.slot] = tag === Tag.String ? this.intern(utf8(cp, cpA(cp, i))) : cpValue(cp, i); } } diff --git a/src/natives/npe.ts b/src/natives/npe.ts index 4e7872b..e54255f 100644 --- a/src/natives/npe.ts +++ b/src/natives/npe.ts @@ -3,7 +3,7 @@ // operand stack slot, then the message is built by walking back from the // bytecode that hit null. -import { cpEntry, Tag, className, utf8 } from "../classfile.ts"; +import { cpA, cpB, cpEntry, Tag, className, utf8 } from "../classfile.ts"; import type { ClassFile } from "../classfile.ts"; import type { JMethod } from "../runtime.ts"; import type { Natives } from "./util.ts"; @@ -248,10 +248,10 @@ class Builder { } private refDesc(i: number): string { - return utf8(this.cp, cpEntry(this.cp, cpEntry(this.cp, i)!.b)!.b); + return utf8(this.cp, cpB(this.cp, cpB(this.cp, i))); } private refName(i: number): string { - return utf8(this.cp, cpEntry(this.cp, cpEntry(this.cp, i)!.b)!.a); + return utf8(this.cp, cpA(this.cp, cpB(this.cp, i))); } private step(bci: number): number {