diff --git a/src/fs.ts b/src/fs.ts index 72eed71..ee50a89 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -223,7 +223,10 @@ export class VFS { } private changed(path: string, event: "create" | "modify" | "delete") { - path = normalize(path); + this.changedNormalized(normalize(path), event); + } + + private changedNormalized(path: string, event: "create" | "modify" | "delete") { for (const watcher of this.watchers) watcher(path, event); this.warmChanged?.add(path); } @@ -320,13 +323,14 @@ export class VFS { if (!r || ((write || truncate) && !r[0].writable) || !r[0].openFile) return undefined; const h = r[0].openFile(r[1], write, truncate, readable); // writes through the handle are changes watchers see, as writes through VFS.write are + const changed = normalize(path); return { size: () => h.size(), ino: () => h.ino(), read: (dst, off, len, pos) => h.read(dst, off, len, pos), close: () => h.close(), - write: (src, pos) => { h.write(src, pos); this.changed(path, "modify"); }, - truncate: (size) => { h.truncate(size); this.changed(path, "modify"); }, + write: (src, pos) => { h.write(src, pos); this.changedNormalized(changed, "modify"); }, + truncate: (size) => { h.truncate(size); this.changedNormalized(changed, "modify"); }, }; } diff --git a/src/inflate.ts b/src/inflate.ts index ec3298b..920f16f 100644 --- a/src/inflate.ts +++ b/src/inflate.ts @@ -25,7 +25,7 @@ function adler(d: Uint8Array): number { // Lookup table indexed by the next maxBits bits: entry = symbol << 4 | length. interface Table { - t: Int32Array; + t: Uint16Array; bits: number; } @@ -33,7 +33,7 @@ function buildTable(lengths: ArrayLike, n: number): Table { let max = 0; for (let i = 0; i < n; i++) if (lengths[i] > max) max = lengths[i]; const bits = Math.max(max, 1); - const t = new Int32Array(1 << bits); + const t = new Uint16Array(1 << bits); const count = new Array(16).fill(0); for (let i = 0; i < n; i++) count[lengths[i]]++; count[0] = 0; @@ -90,6 +90,9 @@ export class Inflate { private out!: Uint8Array; private op = 0; private oe = 0; + private start = 0; + // output before this has been added to the checksum + private summed = 0; constructor(nowrap: boolean) { this.wrap = !nowrap; @@ -150,6 +153,8 @@ export class Inflate { this.ip = 0; this.out = output; this.op = off; + this.start = off; + this.summed = off; this.oe = off + len; let ip = 0, buf = 0, cnt = 0; try { @@ -168,9 +173,40 @@ export class Inflate { this.ip -= this.bitcnt >> 3; this.bitbuf = this.bitcnt = 0; } + this.finishCall(); return [this.ip, this.op - off]; } + private sum() { + const out = this.out, op = this.op; + if (!this.wrap) return; + let s1 = this.s1, s2 = this.s2; + for (let i = this.summed; i < op;) { + const end = Math.min(op, i + 3800); + for (; i < end; i++) { s1 += out[i]; s2 += s1; } + s1 %= 65521; + s2 %= 65521; + } + this.s1 = s1; + this.s2 = s2; + this.summed = op; + } + + // The window and checksum take the call's output in one pass, not per byte. + private finishCall() { + const out = this.out, op = this.op, win = this.win; + this.sum(); + let from = Math.max(this.start, op - 32768); + let abs = this.wpos + (from - this.start); + this.wpos += op - this.start; + while (from < op) { + const at = abs & 32767, n = Math.min(op - from, 32768 - at); + win.set(out.subarray(from, from + n), at); + from += n; + abs += n; + } + } + private need(n: number) { while (this.bitcnt < n) { if (this.ip >= this.in.length) throw UNDERFLOW; @@ -205,17 +241,6 @@ export class Inflate { return e >> 4; } - private adlerByte(b: number) { - this.s1 = (this.s1 + b) % 65521; - this.s2 = (this.s2 + this.s1) % 65521; - } - - private emit(b: number) { - this.out[this.op++] = b; - this.win[this.wpos++ & 32767] = b; - if (this.wrap) this.adlerByte(b); - } - private align() { const drop = this.bitcnt & 7; this.bitbuf >>>= drop; @@ -262,9 +287,16 @@ export class Inflate { case Stage.Stored: { while (this.stored > 0) { if (this.op >= this.oe) return false; - if (this.bitcnt >= 8) this.emit(this.bits(8)); - else if (this.ip < this.in.length) this.emit(this.in[this.ip++]); - else return false; + if (this.bitcnt >= 8) this.out[this.op++] = this.bits(8); + else { + const n = Math.min(this.stored, this.in.length - this.ip, this.oe - this.op); + if (n === 0) return false; + this.out.set(this.in.subarray(this.ip, this.ip + n), this.op); + this.ip += n; + this.op += n; + this.stored -= n; + continue; + } this.stored--; } this.stage = Stage.Block; @@ -272,6 +304,7 @@ export class Inflate { } case Stage.Trailer: { this.align(); + this.sum(); if (this.u32be() !== this.adler) throw new DataFormatError("incorrect data check"); this.stage = Stage.Done; return false; @@ -313,45 +346,87 @@ export class Inflate { this.dist = buildTable(lens.subarray(hlit), hdist); } + // The hot loop, on locals. A symbol that the input cannot finish is backed + // out, so the next call decodes it again from its first bit. The window is + // behind the call's own output, which a match reads directly. private huffman(): boolean { - const win = this.win; - while (this.copyLen > 0) { - if (this.op >= this.oe) return false; - this.emit(win[(this.wpos - this.copyDist) & 32767]); - this.copyLen--; - } + const input = this.in, out = this.out, win = this.win, oe = this.oe, start = this.start; + const lit = this.lit!, dist = this.dist!; + const lt = lit.t, lbits = lit.bits, lmask = (1 << lbits) - 1; + const dt = dist.t, dbits = dist.bits, dmask = (1 << dbits) - 1; + const end = input.length; + // absolute position of out[i] is base + i + const base = this.wpos - start; + let ip = this.ip, buf = this.bitbuf, cnt = this.bitcnt, op = this.op; + let copyLen = this.copyLen; + const copyDist = this.copyDist; + let dd = copyDist; for (;;) { - if (this.op >= this.oe) return false; - const ip = this.ip, buf = this.bitbuf, cnt = this.bitcnt; - try { - const sym = this.decode(this.lit!); - if (sym < 256) { - this.emit(sym); - continue; - } - if (sym === 256) { - this.stage = Stage.Block; - return true; - } - const li = sym - 257; - if (li >= 29) throw new DataFormatError("invalid literal/length code"); - const len = LEN_BASE[li] + this.bits(LEN_EXTRA[li]); - const ds = this.decode(this.dist!); - if (ds >= 30) throw new DataFormatError("invalid distance code"); - const dist = DIST_BASE[ds] + this.bits(DIST_EXTRA[ds]); - if (dist > this.wpos) throw new DataFormatError("invalid distance too far back"); - this.copyLen = len; - this.copyDist = dist; - } catch (e) { - if (e !== UNDERFLOW) throw e; - this.ip = ip; this.bitbuf = buf; this.bitcnt = cnt; - return false; + if (copyLen > 0) { + let n = Math.min(copyLen, oe - op); + copyLen -= n; + let s = op - dd; + for (; n > 0 && s < start; n--) out[op++] = win[(base + s++) & 32767]; + if (n > 32 && dd >= n) { + out.copyWithin(op, s, s + n); + op += n; + } else for (; n > 0; n--) out[op++] = out[s++]; + if (copyLen > 0) break; + } + const sip = ip, sbuf = buf, scnt = cnt; + while (cnt <= 23 && ip < end) { buf |= input[ip++] << cnt; cnt += 8; } + let e = lt[buf & lmask]; + let len = e & 15; + if (len === 0) { + if (cnt < lbits) { ip = sip; buf = sbuf; cnt = scnt; break; } + this.ip = ip; this.bitbuf = buf; this.bitcnt = cnt; this.op = op; + throw new DataFormatError("invalid code"); + } + if (len > cnt) { ip = sip; buf = sbuf; cnt = scnt; break; } + buf >>= len; + cnt -= len; + const sym = e >> 4; + if (sym < 256) { + // a full output stops before the next literal but still sees the end of block, as zlib does + if (op >= oe) { ip = sip; buf = sbuf; cnt = scnt; break; } + out[op++] = sym; + continue; + } + if (sym === 256) { + this.stage = Stage.Block; + this.ip = ip; this.bitbuf = buf; this.bitcnt = cnt; this.op = op; this.copyLen = 0; + return true; } - while (this.copyLen > 0) { - if (this.op >= this.oe) return false; - this.emit(win[(this.wpos - this.copyDist) & 32767]); - this.copyLen--; + const li = sym - 257; + if (li >= 29) throw new DataFormatError("invalid literal/length code"); + const eb = LEN_EXTRA[li]; + if (cnt < eb) { ip = sip; buf = sbuf; cnt = scnt; break; } + const mlen = LEN_BASE[li] + (buf & ((1 << eb) - 1)); + buf >>= eb; + cnt -= eb; + while (cnt <= 23 && ip < end) { buf |= input[ip++] << cnt; cnt += 8; } + e = dt[buf & dmask]; + len = e & 15; + if (len === 0) { + if (cnt < dbits) { ip = sip; buf = sbuf; cnt = scnt; break; } + throw new DataFormatError("invalid code"); } + if (len > cnt) { ip = sip; buf = sbuf; cnt = scnt; break; } + buf >>= len; + cnt -= len; + const ds = e >> 4; + if (ds >= 30) throw new DataFormatError("invalid distance code"); + const deb = DIST_EXTRA[ds]; + while (cnt <= 23 && ip < end) { buf |= input[ip++] << cnt; cnt += 8; } + if (cnt < deb) { ip = sip; buf = sbuf; cnt = scnt; break; } + dd = DIST_BASE[ds] + (buf & ((1 << deb) - 1)); + buf >>= deb; + cnt -= deb; + if (dd > base + op) throw new DataFormatError("invalid distance too far back"); + copyLen = mlen; + this.copyDist = dd; } + this.ip = ip; this.bitbuf = buf; this.bitcnt = cnt; this.op = op; this.copyLen = copyLen; + return false; } } diff --git a/src/jit.ts b/src/jit.ts index fda5d80..d3e0d55 100644 --- a/src/jit.ts +++ b/src/jit.ts @@ -999,18 +999,27 @@ function fuseConcat(g: Gen, b: Body, pc: number): { pieces: string[]; end: numbe return { pieces: pieces.map((f) => f()), end: last + 3 }; } -// An invoke inside a region: the target must be fixed and inlinable itself. +// An invoke inside a region: the target must be fixed and inlinable itself, or a +// virtual call to the receiver class seen last here, which deoptimizes on any other. function regionInvoke(g: Gen, b: Body, op: number, pc: number, d: number, target: JMethod | undefined): string { - if (!target || target.polymorphic || op === 0xb9 || (op === 0xb8 && target.cls.state !== INITIALIZED)) throw new NoInline(); - const callee = op === 0xb7 ? g.t.vm.specialTarget(b.m.cls, target) : target; - if (!callee.code || callee.isAbstract || !isFixed(op, callee)) throw new NoInline(); + if (!target || target.polymorphic || (op === 0xb8 && target.cls.state !== INITIALIZED)) throw new NoInline(); + let callee = op === 0xb7 ? g.t.vm.specialTarget(b.m.cls, target) : target; + let guard: JClass | null = null; + if (op === 0xb9 || !isFixed(op, callee)) { + const site = b.m.cache?.[pc]; + if (!site || !site.m.code || site.m.isAbstract || site.m.polymorphic) throw new NoInline(); + callee = site.m; + guard = site.c; + } + if (!callee.code || callee.isAbstract) throw new NoInline(); const n = target.argSlots; const base = d - n; const args = Array.from({ length: n }, (_, i) => b.S(base + i)); const result = target.ret === "V" ? null : b.S(base); + const check = guard === null ? "" : `if(${args[0]}!==null&&${args[0]}.cls!==${g.K(guard)}){dpc=${pc};dsp=${d};throw rt.DEOPT;}`; const trivial = trivialBody(callee); - if (trivial) return genInline(trivial, result ?? "v", args, pc, callee.isStatic); - return (callee.isStatic ? "" : `if(${args[0]}===null)rt.npe(${pc});`) + genRegion(g, b, callee, args, result, pc, base); + if (trivial) return check + genInline(trivial, result ?? "v", args, pc, callee.isStatic); + return check + (callee.isStatic ? "" : `if(${args[0]}===null)rt.npe(${pc});`) + genRegion(g, b, callee, args, result, pc, base); } // The callee of an invoke does not depend on the receiver's class. @@ -1150,7 +1159,7 @@ function genInline(body: Trivial, res: string, args: string[], pc: number, isSta function genInvoke( g: Gen, op: number, pc: number, next: number, d: number, target: JMethod, cls: JClass, - slowDepth = d, inline = op !== 0xb9 && slowDepth === d, + slowDepth = d, inline = slowDepth === d, ): string { const { t, K, exit, call } = g; const n = target.argSlots; @@ -1158,11 +1167,24 @@ function genInvoke( const args = Array.from({ length: n }, (_, i) => `s${base + i}`); // inline the callee, guarded by the receiver's class unless the target is fixed if (inline) { - const callee = op === 0xb7 ? t.vm.specialTarget(cls, target) : target; + let callee = op === 0xb7 ? t.vm.specialTarget(cls, target) : target; + // the class guarding the inlined body, or null when the callee is fixed + let guard: JClass | null = isFixed(op, callee) && op !== 0xb9 ? null : callee.cls; + // a virtual or interface call inlines the method of the receiver class seen last at this site + let seen = false; + if (guard !== null && !target.isPrivate) { + const site = g.top.m.cache?.[pc]; + if (site && site.m.code && !site.m.isAbstract && !site.m.polymorphic) { + callee = site.m; + guard = site.c; + seen = true; + } + } const res = `s${base}`; let code: string | null = null; const trivial = trivialBody(callee); - if (trivial) code = genInline(trivial, res, args, pc, callee.isStatic); + if (op === 0xb9 && !seen) code = null; + else if (trivial) code = genInline(trivial, res, args, pc, callee.isStatic); else if (callee.code && !callee.isAbstract) { try { const nullCheck = callee.isStatic ? "" : `if(${args[0]}===null)rt.npe(${pc});`; @@ -1172,8 +1194,8 @@ function genInvoke( } } if (code !== null) { - if (isFixed(op, callee)) return code; - return `if(${args[0]}!==null&&${args[0]}.cls===${K(callee.cls)}){${code}}else{${genInvoke(g, op, pc, next, d, target, cls, d, false)}}`; + if (guard === null) return code; + return `if(${args[0]}!==null&&${args[0]}.cls===${K(guard)}){${code}}else{${genInvoke(g, op, pc, next, d, target, cls, d, false)}}`; } } const parts: string[] = [`f.pc=${pc};`]; diff --git a/src/natives/arrays.ts b/src/natives/arrays.ts new file mode 100644 index 0000000..54d530d --- /dev/null +++ b/src/natives/arrays.ts @@ -0,0 +1,66 @@ +// Array loops HotSpot replaces with intrinsics: mismatch, hash code and ASCII +// scanning. The Java versions read longs through Unsafe, which are BigInts here. + +import type { JArray } from "../runtime.ts"; +import { bytesAt } from "./misc.ts"; +import type { Natives } from "./util.ts"; + +const AS = "jdk/internal/util/ArraysSupport."; +const T_BOOLEAN = 4, T_CHAR = 5; + +// Byte index of the first difference in [0, n), or -1. +function firstDiff(a: Uint8Array, ap: number, b: Uint8Array, bp: number, n: number): number { + let i = 0; + while (i + 4 <= n && a[ap + i] === b[bp + i] && a[ap + i + 1] === b[bp + i + 1] && + a[ap + i + 2] === b[bp + i + 2] && a[ap + i + 3] === b[bp + i + 3]) i += 4; + for (; i < n; i++) if (a[ap + i] !== b[bp + i]) return i; + return -1; +} + +// Same results as ArraysSupport.vectorizedMismatch: the index of the first +// differing element within the whole 8-byte words and the 4-byte tail word, +// else the complement of the elements left for the caller's loop. +function vectorizedMismatch(a: Uint8Array, ap: number, b: Uint8Array, bp: number, length: number, scale: number): number { + const words = (length >> (3 - scale)) << 3; + const d = firstDiff(a, ap, b, bp, words); + if (d >= 0) return d >> scale; + let tail = length - (words >> scale); + if (scale < 2) { + const wordTail = 1 << (2 - scale); + if (tail >= wordTail) { + const t = firstDiff(a, ap + words, b, bp + words, 4); + if (t >= 0) return (words + t) >> scale; + tail -= wordTail; + } + } + return ~tail; +} + +// ArraysSupport.vectorizedHashCode; a byte[] with T_CHAR holds UTF-16 little-endian. +function hashCode(h: number, array: JArray, from: number, len: number, type: number): number { + const v = array.a as Int8Array | Uint16Array | Int16Array | Int32Array; + const end = from + len; + if (type === T_CHAR && v instanceof Int8Array) { + for (let i = from; i < end; i++) h = (Math.imul(31, h) + ((v[i << 1] & 0xff) | ((v[(i << 1) + 1] & 0xff) << 8))) | 0; + } else if (type === T_BOOLEAN) { + for (let i = from; i < end; i++) h = (Math.imul(31, h) + (v[i] & 0xff)) | 0; + } else { + for (let i = from; i < end; i++) h = (Math.imul(31, h) + v[i]) | 0; + } + return h; +} + +export const intrinsics: Natives = { + [`${AS}vectorizedMismatch(Ljava/lang/Object;JLjava/lang/Object;JII)I`]: (t, [a, aOff, b, bOff, length, scale]) => { + const [x, xp] = bytesAt(t, a, aOff); + const [y, yp] = bytesAt(t, b, bOff); + return vectorizedMismatch(x, xp, y, yp, length, scale); + }, + [`${AS}vectorizedHashCode(Ljava/lang/Object;IIII)I`]: (_t, [array, from, len, init, type]) => + hashCode(init, array, from, len, type), + "java/lang/StringCoding.countPositives([BII)I": (_t, [ba, off, len]) => { + const v = (ba as JArray).a as Int8Array; + for (let i = off; i < off + len; i++) if (v[i] < 0) return i - off; + return len; + }, +}; diff --git a/src/natives/index.ts b/src/natives/index.ts index b6b3f68..80092ef 100644 --- a/src/natives/index.ts +++ b/src/natives/index.ts @@ -1,4 +1,5 @@ import { natives as continuation } from "./continuation.ts"; +import { intrinsics as arrayIntrinsics } from "./arrays.ts"; import { intrinsics as cryptoIntrinsics } from "./crypto.ts"; import { natives as invoke } from "./invoke.ts"; import { natives as io } from "./io.ts"; @@ -25,4 +26,4 @@ import type { Natives } from "./util.ts"; export const natives: Natives = { ...lang, ...misc, ...io, ...loader, ...reflect, ...invoke, ...zip, ...nio, ...net, ...sockets, ...npe, ...stackwalk, ...ref, ...management, ...continuation, ...java2dShape, ...java2dAaPgram, ...java2dPath, ...java2d, ...font }; // Replace Java (non-native) methods of the JDK. -export const intrinsics: Natives = { ...miscIntrinsics, ...refIntrinsics, ...stringIntrinsics, ...cryptoIntrinsics }; +export const intrinsics: Natives = { ...miscIntrinsics, ...refIntrinsics, ...stringIntrinsics, ...cryptoIntrinsics, ...arrayIntrinsics }; diff --git a/src/natives/misc.ts b/src/natives/misc.ts index c03ac20..19a2c28 100644 --- a/src/natives/misc.ts +++ b/src/natives/misc.ts @@ -101,7 +101,7 @@ function cas(t: JThread, o: JObject | null, off: bigint, k: string, expect: any, } // A byte view of base+offset for bulk memory operations. -function bytesAt(t: JThread, base: JObject | null, offset: bigint): [Uint8Array, number] { +export function bytesAt(t: JThread, base: JObject | null, offset: bigint): [Uint8Array, number] { if (base === null) return [t.vm.mem.u8, Number(offset)]; const ta = (base as JArray).a as Int8Array; return [new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength), Number(offset) - ARRAY_BASE_OFFSET]; diff --git a/src/natives/nio.ts b/src/natives/nio.ts index e5f7ea3..792f076 100644 --- a/src/natives/nio.ts +++ b/src/natives/nio.ts @@ -444,9 +444,9 @@ function readAt(t: JThread, fdObj: JObject, addr: bigint, len: number, pos: numb function writeAt(t: JThread, fdObj: JObject, addr: bigint, len: number, pos: number | null): number { const fd = fdNum(t, fdObj); - const data = t.vm.mem.u8.slice(Number(addr), Number(addr) + len); + const data = t.vm.mem.u8.subarray(Number(addr), Number(addr) + len); if (fd === 1 || fd === 2) { - (fd === 1 ? t.vm.opts.stdout : t.vm.opts.stderr)(data); + (fd === 1 ? t.vm.opts.stdout : t.vm.opts.stderr)(data.slice()); return len; } const f = chanFile(t, fdObj); diff --git a/src/natives/zip.ts b/src/natives/zip.ts index b23d344..fc5ca29 100644 --- a/src/natives/zip.ts +++ b/src/natives/zip.ts @@ -6,19 +6,30 @@ import { JThread, PendingThrow } from "../thread.ts"; import { u8 } from "./util.ts"; import type { Natives } from "./util.ts"; +// slicing-by-8: table k holds the CRC of a byte followed by k zero bytes const CRC_TABLE = (() => { - const t = new Int32Array(256); + const t = new Int32Array(8 * 256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c; } + for (let n = 0; n < 256; n++) { + let c = t[n]; + for (let k = 1; k < 8; k++) t[k * 256 + n] = c = t[c & 0xff] ^ (c >>> 8); + } return t; })(); export function crc32(crc: number, b: Uint8Array): number { - let c = ~crc; - for (let i = 0; i < b.length; i++) c = CRC_TABLE[(c ^ b[i]) & 0xff] ^ (c >>> 8); + const T = CRC_TABLE, n = b.length; + let c = ~crc, i = 0; + for (; i + 8 <= n; i += 8) { + const x = c ^ (b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)); + c = T[1792 + (x & 0xff)] ^ T[1536 + ((x >>> 8) & 0xff)] ^ T[1280 + ((x >>> 16) & 0xff)] ^ T[1024 + (x >>> 24)] ^ + T[768 + b[i + 4]] ^ T[512 + b[i + 5]] ^ T[256 + b[i + 6]] ^ T[b[i + 7]]; + } + for (; i < n; i++) c = T[(c ^ b[i]) & 0xff] ^ (c >>> 8); return ~c; } @@ -229,7 +240,7 @@ export const natives: Natives = { [`${D}reset(J)V`]: (t, [addr]) => deflater(t, addr).reset(), [`${D}end(J)V`]: (t, [addr]) => void streams(t).delete(addr), - "java/util/zip/CRC32.update(II)I": (_t, [crc, b]) => crc32(crc, Uint8Array.of(b)), + "java/util/zip/CRC32.update(II)I": (_t, [crc, b]) => ~(CRC_TABLE[(~crc ^ b) & 0xff] ^ (~crc >>> 8)), "java/util/zip/CRC32.updateBytes0(I[BII)I": (_t, [crc, b, off, len]) => crc32(crc, u8(b as JArray, off, len)), "java/util/zip/CRC32.updateByteBuffer0(IJII)I": (t, [crc, addr, off, len]) => crc32(crc, mem(t, addr + BigInt(off), len)), "java/util/zip/Adler32.update(II)I": (_t, [a, b]) => adler32(a, Uint8Array.of(b)), diff --git a/test/java/ArrayIntrinsics.java b/test/java/ArrayIntrinsics.java new file mode 100644 index 0000000..15c4dec --- /dev/null +++ b/test/java/ArrayIntrinsics.java @@ -0,0 +1,47 @@ +import java.util.Arrays; +import java.util.Random; + +// Arrays.mismatch, Arrays.equals, hashCode and UTF-8 decoding over the array intrinsics. +public class ArrayIntrinsics { + public static void main(String[] args) { + Random r = new Random(7); + int sum = 0; + for (int len = 0; len < 70; len++) { + for (int diff = -1; diff < len; diff++) { + byte[] a = new byte[len], b; + r.nextBytes(a); + b = a.clone(); + if (diff >= 0) b[diff] ^= 0x40; + sum += Arrays.mismatch(a, b) * 31 + Arrays.hashCode(a) + Boolean.hashCode(Arrays.equals(a, b)); + short[] s = new short[len], u; + for (int i = 0; i < len; i++) s[i] = (short) r.nextInt(); + u = s.clone(); + if (diff >= 0) u[diff]++; + sum += Arrays.mismatch(s, u) * 17 + Arrays.hashCode(s); + char[] c = new char[len], d; + for (int i = 0; i < len; i++) c[i] = (char) r.nextInt(); + d = c.clone(); + if (diff >= 0) d[diff]++; + sum += Arrays.mismatch(c, d) * 13 + Arrays.hashCode(c); + int[] x = new int[len], y; + for (int i = 0; i < len; i++) x[i] = r.nextInt(); + y = x.clone(); + if (diff >= 0) y[diff]--; + sum += Arrays.mismatch(x, y) * 11 + Arrays.hashCode(x); + long[] p = new long[len], q; + for (int i = 0; i < len; i++) p[i] = r.nextLong(); + q = p.clone(); + if (diff >= 0) q[diff]++; + sum += Arrays.mismatch(p, q) * 7; + } + } + System.out.println(sum); + String latin = "café plain ascii text", wide = "中文 text é"; + System.out.println(latin.hashCode() + " " + wide.hashCode() + " " + "".hashCode()); + System.out.println(Arrays.mismatch(new byte[100], 10, 90, new byte[100], 0, 80)); + byte[] utf8 = "ascii only then é non ascii".getBytes(java.nio.charset.StandardCharsets.UTF_8); + System.out.println(new String(utf8, java.nio.charset.StandardCharsets.UTF_8)); + System.out.println(new String(new byte[] {65, 66, 67}, java.nio.charset.StandardCharsets.UTF_8)); + System.out.println(new String(new byte[] {65, (byte) 0xff, 67}, java.nio.charset.StandardCharsets.UTF_8).length()); + } +} diff --git a/test/java/VirtualInline.java b/test/java/VirtualInline.java new file mode 100644 index 0000000..e400f6e --- /dev/null +++ b/test/java/VirtualInline.java @@ -0,0 +1,46 @@ +// Call sites that run one receiver class long enough to compile, then see others. +public class VirtualInline { + interface Shape { int area(); default int twice() { return area() * 2; } } + static abstract class Base implements Shape { + int w = 3; + abstract int h(); + public int area() { return w * h(); } + int checked(int d) { return 100 / d; } + int viaH() { return h() + 1; } + } + static class Sq extends Base { int h() { return w + 1; } } + static class Tall extends Base { int h() { return w * 10; } public int area() { return -super.area(); } } + static class Zero extends Base { int h() { return 0; } } + + static int direct(Base b) { return b.area() + b.h() + b.viaH(); } + static int iface(Shape s) { return s.area() + s.twice(); } + static int guarded(Base b, int d) { + try { return b.checked(d); } catch (ArithmeticException e) { return -1; } + } + + public static void main(String[] args) { + Base[] all = { new Sq(), new Tall(), new Zero() }; + long sum = 0; + for (int i = 0; i < 20000; i++) sum += direct(all[0]) + iface(all[0]) + guarded(all[0], 1 + i % 3); + System.out.println(sum); + for (int round = 0; round < 3; round++) { + for (Base b : all) { + for (int i = 0; i < 3000; i++) sum += direct(b) + iface(b) + guarded(b, i % 4); + } + } + System.out.println(sum); + try { + direct(null); + } catch (NullPointerException e) { + System.out.println("npe"); + } + try { + iface(null); + } catch (NullPointerException e) { + System.out.println("npe"); + } + for (int i = 0; i < 20000; i++) sum += direct(all[2]) + iface(all[2]); + for (int i = 0; i < 100; i++) sum += direct(all[i % 3]) + iface(all[i % 3]); + System.out.println(sum); + } +} diff --git a/test/zip.test.ts b/test/zip.test.ts new file mode 100644 index 0000000..d64d85f --- /dev/null +++ b/test/zip.test.ts @@ -0,0 +1,121 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { crc32 as zlibCrc32, deflateRawSync, deflateSync } from "node:zlib"; +import { DataFormatError, Inflate } from "../src/inflate.ts"; +import { adler32, crc32 } from "../src/natives/zip.ts"; + +// xorshift, so every run sees the same data +function rng(seed: number) { + let x = seed; + return () => { + x ^= x << 13; x >>>= 0; + x ^= x >>> 17; + x ^= x << 5; x >>>= 0; + return x; + }; +} + +// text-like bytes with long and short repeats, and some noise +function sample(n: number, seed: number): Uint8Array { + const r = rng(seed), out = new Uint8Array(n); + const words = ["alpha", "beta", "gamma", "delta", "java", "class", "zip", "entry", "\n"]; + let i = 0; + while (i < n) { + const k = r() % 10; + if (k < 6) for (const c of words[r() % words.length]) { if (i < n) out[i++] = c.charCodeAt(0); } + else if (k < 8 && i > 300) { const d = 1 + (r() % 300), l = 3 + (r() % 258); for (let j = 0; j < l && i < n; j++, i++) out[i] = out[i - d]; } + else out[i++] = r() & 0xff; + } + return out; +} + +// Feeds compressed in pieces of random sizes into outputs of random sizes, as Java's Inflater does. +function inflateStream(z: Inflate, compressed: Uint8Array, seed: number, maxIn: number, maxOut: number): Uint8Array { + const r = rng(seed), parts: Uint8Array[] = []; + let pos = 0, idle = 0; + while (!z.finished) { + const inLen = Math.min(compressed.length - pos, 1 + (r() % maxIn)); + const out = new Uint8Array(1 + (r() % maxOut)); + const [read, written] = z.run(compressed.subarray(pos, pos + inLen), out, 0, out.length); + pos += read; + parts.push(out.subarray(0, written)); + idle = read || written ? 0 : idle + 1; + assert.ok(idle < 1000, "inflate stalled"); + if (pos >= compressed.length && !read && !written) break; + } + assert.ok(z.finished, "stream not finished"); + return Buffer.concat(parts); +} + +test("crc32 matches zlib at every length around the word size and across splits", () => { + const data = sample(70000, 1); + for (const n of [0, 1, 7, 8, 9, 15, 16, 17, 1000, 65537]) { + const b = data.subarray(0, n); + assert.equal(crc32(0, b) >>> 0, zlibCrc32(b)); + const half = n >> 1; + assert.equal(crc32(crc32(0, b.subarray(0, half)), b.subarray(half)) >>> 0, zlibCrc32(b)); + } +}); + +test("adler32 matches the checksum zlib writes", () => { + const data = sample(20000, 2); + const z = deflateSync(data); + const stored = new DataView(z.buffer, z.byteOffset + z.length - 4, 4).getUint32(0); + assert.equal(adler32(1, data) >>> 0, stored); +}); + +test("inflate matches zlib for any split of input and output", () => { + for (const [size, seed] of [[0, 3], [1, 4], [100, 5], [5000, 6], [70000, 7], [300000, 8]]) { + const data = sample(size, seed); + for (const level of [0, 1, 6, 9]) { + for (const wrap of [true, false]) { + const packed = wrap ? deflateSync(data, { level }) : deflateRawSync(data, { level }); + for (const [maxIn, maxOut] of [[1, 1000], [7, 3], [100, 100], [10000, 70000], [1 << 20, 1 << 20]]) { + if (maxOut < 10 && size > 5000) continue; + const got = inflateStream(new Inflate(!wrap), packed, seed + level, maxIn, maxOut); + assert.ok(Buffer.from(got).equals(data), `size ${size} level ${level} wrap ${wrap} in ${maxIn} out ${maxOut}`); + } + } + } + } +}); + +test("inflate keeps the window across calls and rejects a bad checksum", () => { + const data = sample(100000, 9); + const packed = deflateSync(data); + const z = new Inflate(false); + const got = inflateStream(z, packed, 10, 4000, 500); + assert.ok(Buffer.from(got).equals(data)); + assert.equal(z.adler, adler32(1, data) >>> 0); + const bad = Buffer.from(packed); + bad[bad.length - 1] ^= 1; + assert.throws(() => inflateStream(new Inflate(false), bad, 11, 5000, 5000), DataFormatError); +}); + +test("inflate stops at the end of the stream and leaves the bytes after it", () => { + const data = sample(3000, 12); + const packed = deflateRawSync(data); + const tail = Uint8Array.of(1, 2, 3, 4, 5); + const input = Buffer.concat([packed, tail]); + const z = new Inflate(true); + const out = new Uint8Array(4000); + const [read, written] = z.run(input, out, 0, out.length); + assert.ok(z.finished); + assert.equal(written, data.length); + assert.equal(read, packed.length); +}); + +test("inflate sees the end of the stream when the output fills on the last byte", () => { + for (const wrap of [true, false]) { + for (const size of [1, 10, 5000]) { + const data = sample(size, 13); + const packed = wrap ? deflateSync(data) : deflateRawSync(data); + const z = new Inflate(!wrap); + const out = new Uint8Array(size); + const [read, written] = z.run(packed, out, 0, size); + assert.equal(written, size); + assert.equal(read, packed.length); + assert.ok(z.finished, `size ${size} wrap ${wrap}`); + } + } +});