Files
newt/serializer.ts
Steve Dunham a0bab1cf5c update serializer.ts to work with current object formats
- numeric tags
- true / false

Will probably ditch this for a repl/lsp approach
2025-11-13 10:36:55 -08:00

270 lines
6.3 KiB
TypeScript

// Experimental serializer / deserializer for modules
// not completely wired in yet, serialization is running, deserialization is not
const END = 0;
const LIST = 1;
const TUPLE = 2;
const INDUCT = 3;
const STRING = 4;
const NUMBER = 5;
const NULL = 6;
const TRUE = 7;
const FALSE = 8;
const te = new TextEncoder();
class DeserializationStream {
pos = 0;
buf: Uint8Array;
constructor(buf: Uint8Array) {
this.buf = buf;
}
readByte() {
return this.buf[this.pos++];
}
readVarint() {
let shift = 0;
let result = 0;
while (true) {
const byte = this.readByte();
result |= (byte & 0x7f) << shift;
if ((byte & 0x80) === 0) break;
shift += 7;
}
return result;
}
readSignedVarint() {
const n = this.readVarint();
return (n >>> 1) ^ -(n & 1);
}
readString() {
const length = this.readVarint();
const bytes = this.buf.slice(this.pos, this.pos + length);
this.pos += length;
return new TextDecoder().decode(bytes);
}
}
export class DecFile {
pool: string[] = [""];
buf: DeserializationStream;
static decode(encoded: Uint8Array) {
return new DecFile(encoded).read()
}
constructor(data: Uint8Array) {
this.buf = new DeserializationStream(data);
this.readPool();
}
readPool() {
while (true) {
let str = this.buf.readString();
if (!str.length) break
this.pool.push(str);
}
}
read(): any {
const type = this.buf.readByte();
switch (type) {
case NULL:
return null;
case LIST: {
const list: any[] = [];
while (this.buf.buf[this.buf.pos] !== END) {
list.push(this.read());
}
this.buf.pos++;
let rval: any = { tag: "Nil", 'h0': null };
while (list.length)
rval = { tag: "_::_", h0: null, h1: list.pop(), h2: rval };
return rval;
}
case TUPLE: {
const tuple: any[] = [];
while (this.buf.buf[this.buf.pos] !== END) {
tuple.push(this.read());
}
this.buf.pos++;
let rval: any = tuple.pop();
while (tuple.length)
rval = { tag: "_,_", h0: null, h1: null, h2: tuple.pop(), h3: rval };
return rval;
}
case STRING:
return this.pool[this.buf.readVarint()];
case NUMBER:
return this.buf.readSignedVarint();
case INDUCT:
const tag = this.buf.readVarint()
const obj: any = { tag };
let i = 0;
while (this.buf.buf[this.buf.pos] !== END) {
obj[`h${i++}`] = this.read();
}
this.buf.pos++;
return obj;
case TRUE:
return true;
case FALSE:
return false;
default:
debugger
throw new Error(`Unknown type: ${type}`);
}
}
}
class SerializationStream {
pos = 0;
buf = new Uint8Array(1024 * 1024);
ensure(size: number) {
if (this.buf.length - this.pos < size) {
const tmp = new Uint8Array(this.buf.length * 1.5);
tmp.set(this.buf);
this.buf = tmp;
}
}
writeByte(n: number) {
this.ensure(1);
this.buf[this.pos++] = n % 256;
}
writeVarint(n: number) {
while (n > 127) {
this.writeByte((n & 0x7f) | 0x80);
n >>= 7;
}
this.writeByte(n & 0x7f);
}
writeSignedVarint(n: number) {
const zigzag = (n << 1) ^ (n >> 31);
this.writeVarint(zigzag);
}
writeString(s: string) {
let data = te.encode(s);
this.ensure(data.byteLength + 4);
this.writeVarint(data.byteLength);
this.buf.set(data, this.pos);
this.pos += data.byteLength;
}
toUint8Array() {
return this.buf.slice(0, this.pos);
}
}
export class EncFile {
poollen = 1;
pool = new SerializationStream();
buf = new SerializationStream();
pmap: Map<string, number> = new Map();
constructor() {
this.pmap.set("",0);
}
static encode(data: any) {
let f = new EncFile()
f.write(data)
f.pool.writeVarint(0)
return f.toUint8Array()
}
writeString(s: string) {
let n = this.pmap.get(s);
if (n === undefined) {
n = this.poollen++;
this.pool.writeString(s);
this.pmap.set(s,n);
}
this.buf.writeVarint(n);
}
write(a: any) {
// shouldn't happen?
if (a == null) {
this.buf.writeByte(NULL);
} else if (a.tag == "_::_") {
this.buf.writeByte(LIST);
for (; a.tag === "_::_"; a = a.h2) {
this.write(a.h1);
}
this.buf.writeByte(END);
} else if (a.tag == "_,_") {
this.buf.writeByte(TUPLE);
for (; a.tag === "_,_"; a = a.h3) {
this.write(a.h2);
}
this.write(a);
this.buf.writeByte(END);
} else if (typeof a === "string") {
this.buf.writeByte(STRING);
this.writeString(a);
} else if (typeof a === "number") {
this.buf.writeByte(NUMBER);
this.buf.writeSignedVarint(a);
} else if (a === true) {
this.buf.writeByte(TRUE);
} else if (a === false) {
this.buf.writeByte(FALSE);
} else if (a.tag !== undefined) {
this.buf.writeByte(INDUCT);
this.buf.writeVarint(a.tag);
// Sometimes keys are skipped
let end = 0
for (let k in a) {
if (k[0] == 'h') end = Math.max(end, +k.slice(1))
}
for (let i = 0; i <= end; i++) {
let key = 'h' + i
let v = a[key] ?? null
this.write(v);
}
this.buf.writeByte(END);
} else {
throw new Error(`handle ${typeof a} ${a} ${Object.keys(a)}`);
}
}
toUint8Array() {
const poolArray = this.pool.toUint8Array();
const bufArray = this.buf.toUint8Array();
const rval = new Uint8Array(poolArray.length + bufArray.length);
rval.set(poolArray);
rval.set(bufArray, poolArray.length);
return rval;
}
}
// This was for testing
function deepEqual(a: any, b: any): boolean {
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (a == null || b == null) return false;
if (typeof a !== "object") return false;
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!deepEqual(a[key], b[key])) return false;
}
return true;
}