From 7b4b54f8771b8395a43a12c61052507c85ef1f45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:26:24 +0000 Subject: [PATCH 1/7] Initial plan From 58535ed28273b52f1579b92d3fa7ce013f6f5705 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:32:25 +0000 Subject: [PATCH 2/7] feat: add optional and dynamic struct fields Agent-Logs-Url: https://github.com/rotu/structview/sessions/0716fb80-af23-468f-86bd-27d35fdca170 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- README.md | 27 +++ fields.ts | 621 +++++++++++++++++++++++++++++++++------------------- mod_test.ts | 49 +++++ 3 files changed, 474 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index f1f6a34..671c0e9 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,33 @@ for (const dish of myMenu) { } ``` +# Optional and variably sized fields + +The core API still favors explicit offsets for fixed binary layouts, but fields +can also resolve their offset, length, or presence from other properties. This +keeps the current API intact while making packet-style layouts possible in a +declarative way. + +```js +import { bool, defineStruct, optional, string, u16, u8 } from "@rotu/structview" + +class Record extends defineStruct({ + has_name: bool(0), + name_length: u8(1), + name: optional(string(2, "name_length"), "has_name"), + name_end: { + get() { + return 2 + (this.has_name ? this.name_length : 0) + }, + }, + checksum: u16("name_end"), +}) {} +``` + +`optional()` hides a field behind a boolean property or callback, and field +factories such as `string()`, `u16()`, `substruct()`, and `typedArray()` can +now resolve offsets and lengths from property names or accessor functions. + # Gotchas and rough edges 1. Resizable structs are not yet implemented. Resizable `Arraybuffer`s only diff --git a/fields.ts b/fields.ts index eaf0a40..81e4b88 100644 --- a/fields.ts +++ b/fields.ts @@ -5,144 +5,240 @@ import { structBytes, structDataView } from "./core.ts" import type { + AnyStruct, ReadOnlyAccessorDescriptor, StructConstructor, StructPropertyDescriptor, TypedArraySpecies, } from "./types.ts" -/** - * Field for a 8-bit unsigned integer - */ -export function u8(fieldOffset: number): StructPropertyDescriptor { +type FieldValue = T | string | ((struct: AnyStruct) => T) +type BooleanFieldValue = FieldValue +type NumberFieldValue = FieldValue +type OptionalNumberFieldValue = FieldValue | undefined + +function resolveFieldValue( + struct: AnyStruct, + value: FieldValue, +): T { + if (typeof value === "function") { + return value(struct) + } + if (typeof value === "string") { + return Reflect.get(struct, value) + } + return value +} + +function resolveNumberFieldValue( + struct: AnyStruct, + value: NumberFieldValue, + name: string, +): number { + const result = resolveFieldValue(struct, value) + if (typeof result !== "number") { + throw new TypeError(`${name} must resolve to a number`) + } + return result +} + +function resolveOptionalNumberFieldValue( + struct: AnyStruct, + value: OptionalNumberFieldValue, + name: string, +): number | undefined { + if (typeof value === "undefined") { + return undefined + } + const result = resolveFieldValue(struct, value) + if (typeof result !== "number" && typeof result !== "undefined") { + throw new TypeError(`${name} must resolve to a number or undefined`) + } + return result +} + +function resolveBooleanFieldValue( + struct: AnyStruct, + value: BooleanFieldValue, +): boolean { + const result = resolveFieldValue(struct, value) + if (typeof result !== "boolean") { + throw new TypeError("present must resolve to a boolean") + } + return result +} + +function setBooleanFieldValue( + struct: AnyStruct, + value: string | ((struct: AnyStruct, present: boolean) => void), + present: boolean, +) { + if (typeof value === "function") { + value(struct, present) + return + } + if (typeof value === "string") { + Reflect.set(struct, value, present) + return + } + throw new TypeError( + "optional field presence must be settable via a property name or callback", + ) +} + +function dataViewField( + fieldOffset: NumberFieldValue, + fieldGetter: (dv: DataView, fieldOffset: number) => T, + fieldSetter: (dv: DataView, fieldOffset: number, value: T) => void, +): StructPropertyDescriptor { return { get() { - return structDataView(this).getUint8(fieldOffset) + const dv = structDataView(this) + const offset = resolveNumberFieldValue(this, fieldOffset, "fieldOffset") + return fieldGetter(dv, offset) }, set(value) { - structDataView(this).setUint8(fieldOffset, value) + const dv = structDataView(this) + const offset = resolveNumberFieldValue(this, fieldOffset, "fieldOffset") + fieldSetter(dv, offset, value) }, } } + +/** + * Field for a 8-bit unsigned integer + */ +export function u8(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getUint8(offset), + (dv, offset, value) => dv.setUint8(offset, value), + ) +} /** * Field for a little-endian 16-bit unsigned integer */ -export function u16(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getUint16(fieldOffset, true) - }, - set(value) { - structDataView(this).setUint16(fieldOffset, value, true) - }, - } +export function u16(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getUint16(offset, true), + (dv, offset, value) => dv.setUint16(offset, value, true), + ) } /** * Field for a little-endian 32-bit unsigned integer */ -export function u32(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getUint32(fieldOffset, true) - }, - set(value) { - structDataView(this).setUint32(fieldOffset, value, true) - }, - } +export function u32(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getUint32(offset, true), + (dv, offset, value) => dv.setUint32(offset, value, true), + ) } /** * Field for a little-endian 64-bit unsigned integer */ -export function u64(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getBigUint64(fieldOffset, true) - }, - set(value) { - structDataView(this).setBigUint64(fieldOffset, value, true) - }, - } +export function u64(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getBigUint64(offset, true), + (dv, offset, value) => dv.setBigUint64(offset, value, true), + ) } /** * Field for a little-endian 8-bit signed integer */ -export function i8(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getInt8(fieldOffset) - }, - set(value) { - structDataView(this).setInt8(fieldOffset, value) - }, - } +export function i8(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getInt8(offset), + (dv, offset, value) => dv.setInt8(offset, value), + ) } /** * Field for a little-endian 16-bit signed integer */ -export function i16(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getInt16(fieldOffset, true) - }, - set(value) { - structDataView(this).setInt16(fieldOffset, value, true) - }, - } +export function i16(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getInt16(offset, true), + (dv, offset, value) => dv.setInt16(offset, value, true), + ) } /** * Field for a little-endian 32-bit signed integer */ -export function i32(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getInt32(fieldOffset, true) - }, - set(value) { - structDataView(this).setInt32(fieldOffset, value, true) - }, - } +export function i32(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getInt32(offset, true), + (dv, offset, value) => dv.setInt32(offset, value, true), + ) } /** * Field for a little-endian 64-bit signed integer */ -export function i64(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getBigInt64(fieldOffset, true) - }, - set(value) { - structDataView(this).setBigInt64(fieldOffset, value, true) - }, - } +export function i64(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getBigInt64(offset, true), + (dv, offset, value) => dv.setBigInt64(offset, value, true), + ) } /** * Field for a little-endian unsigned integer of arbitrary byte length */ export function biguintle( - fieldOffset: number, - { byteLength }: { byteLength: number }, + fieldOffset: NumberFieldValue, + { byteLength }: { byteLength: NumberFieldValue }, ): StructPropertyDescriptor { - if ( - !Number.isInteger(byteLength) || - !(0 < byteLength) - ) { - throw new TypeError("byteLength must be a positive integer") - } return { get() { + const resolvedFieldOffset = resolveNumberFieldValue( + this, + fieldOffset, + "fieldOffset", + ) + const resolvedByteLength = resolveNumberFieldValue( + this, + byteLength, + "byteLength", + ) + if ( + !Number.isInteger(resolvedByteLength) || + !(0 < resolvedByteLength) + ) { + throw new TypeError("byteLength must resolve to a positive integer") + } let result = 0n const dv = structDataView(this) - for (let i = 0; i < byteLength; ++i) { - result |= BigInt(dv.getUint8(fieldOffset + i)) << BigInt(8 * i) + for (let i = 0; i < resolvedByteLength; ++i) { + result |= BigInt(dv.getUint8(resolvedFieldOffset + i)) << BigInt(8 * i) } return result }, set(value) { + const resolvedFieldOffset = resolveNumberFieldValue( + this, + fieldOffset, + "fieldOffset", + ) + const resolvedByteLength = resolveNumberFieldValue( + this, + byteLength, + "byteLength", + ) + if ( + !Number.isInteger(resolvedByteLength) || + !(0 < resolvedByteLength) + ) { + throw new TypeError("byteLength must resolve to a positive integer") + } const dv = structDataView(this) - for (let i = 0; i < byteLength; ++i) { + for (let i = 0; i < resolvedByteLength; ++i) { dv.setUint8( - fieldOffset + i, + resolvedFieldOffset + i, Number((value >> BigInt(8 * i)) & 0xffn), ) } @@ -154,24 +250,36 @@ export function biguintle( * Field for a little-endian signed integer of arbitrary byte length */ export function bigintle( - offset: number, - options: { byteLength: number }, + offset: NumberFieldValue, + options: { byteLength: NumberFieldValue }, ): StructPropertyDescriptor { const { byteLength } = options return { get() { + const resolvedOffset = resolveNumberFieldValue(this, offset, "fieldOffset") + const resolvedByteLength = resolveNumberFieldValue( + this, + byteLength, + "byteLength", + ) let result = 0n const dv = structDataView(this) - for (let i = 0; i < byteLength; ++i) { - result |= BigInt(dv.getUint8(offset + i)) << BigInt(8 * i) + for (let i = 0; i < resolvedByteLength; ++i) { + result |= BigInt(dv.getUint8(resolvedOffset + i)) << BigInt(8 * i) } - return BigInt.asIntN(byteLength * 8, result) + return BigInt.asIntN(resolvedByteLength * 8, result) }, set(value) { + const resolvedOffset = resolveNumberFieldValue(this, offset, "fieldOffset") + const resolvedByteLength = resolveNumberFieldValue( + this, + byteLength, + "byteLength", + ) const dv = structDataView(this) - for (let i = 0; i < byteLength; ++i) { + for (let i = 0; i < resolvedByteLength; ++i) { dv.setUint8( - offset + i, + resolvedOffset + i, Number((value >> BigInt(8 * i)) & 0xffn), ) } @@ -182,73 +290,88 @@ export function bigintle( /** * Field for a little-endian 16-bit binary float (float16_t) */ -export function f16(fieldOffset: number): StructPropertyDescriptor { +export function f16(fieldOffset: NumberFieldValue): StructPropertyDescriptor { if ( typeof DataView.prototype.getFloat16 !== "function" || typeof DataView.prototype.setFloat16 !== "function" ) { throw new TypeError("float16 is not supported in this environment") } - return { - get() { - return structDataView(this).getFloat16(fieldOffset, true) - }, - set(value) { - structDataView(this).setFloat16(fieldOffset, value, true) - }, - } + return dataViewField( + fieldOffset, + (dv, offset) => dv.getFloat16(offset, true), + (dv, offset, value) => dv.setFloat16(offset, value, true), + ) } /** * Field for a little-endian 32-bit binary float (float32_t) */ -export function f32(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getFloat32(fieldOffset, true) - }, - set(value) { - structDataView(this).setFloat32(fieldOffset, value, true) - }, - } +export function f32(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getFloat32(offset, true), + (dv, offset, value) => dv.setFloat32(offset, value, true), + ) } /** * Field for a little-endian 64-bit binary float (float64_t) */ -export function f64(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getFloat64(fieldOffset, true) - }, - set(value) { - structDataView(this).setFloat64(fieldOffset, value, true) - }, - } +export function f64(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getFloat64(offset, true), + (dv, offset, value) => dv.setFloat64(offset, value, true), + ) } /** * Field for a UTF-8 fixed-length string */ export function string( - fieldOffset: number, - byteLength: number, + fieldOffset: NumberFieldValue, + byteLength: NumberFieldValue, ): StructPropertyDescriptor { const TEXT_DECODER = new TextDecoder() const TEXT_ENCODER = new TextEncoder() return { get() { + const resolvedFieldOffset = resolveNumberFieldValue( + this, + fieldOffset, + "fieldOffset", + ) + const resolvedByteLength = resolveNumberFieldValue( + this, + byteLength, + "byteLength", + ) const str = TEXT_DECODER.decode( - structBytes(this, fieldOffset, fieldOffset + byteLength), + structBytes( + this, + resolvedFieldOffset, + resolvedFieldOffset + resolvedByteLength, + ), ) // trim all trailing null characters return str.replace(/\0+$/, "") }, set(value) { - const bytes = structBytes( + const resolvedFieldOffset = resolveNumberFieldValue( this, fieldOffset, - fieldOffset + byteLength, + "fieldOffset", + ) + const resolvedByteLength = resolveNumberFieldValue( + this, + byteLength, + "byteLength", + ) + const bytes = structBytes( + this, + resolvedFieldOffset, + resolvedFieldOffset + resolvedByteLength, ) bytes.fill(0) TEXT_ENCODER.encodeInto(value, bytes) @@ -260,15 +383,75 @@ export function string( * Field for a boolean stored in a byte (0 = false, nonzero = true) * True will be stored as 1 */ -export function bool(fieldOffset: number): StructPropertyDescriptor { - return { +export function bool(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => Boolean(dv.getUint8(offset)), + (dv, offset, value) => dv.setUint8(offset, value ? 1 : 0), + ) +} + +export function optional( + field: StructPropertyDescriptor & ReadOnlyAccessorDescriptor, + present: + | BooleanFieldValue + | { + present: BooleanFieldValue + setPresent?: string | ((struct: AnyStruct, present: boolean) => void) + }, +): StructPropertyDescriptor & ReadOnlyAccessorDescriptor +export function optional( + field: StructPropertyDescriptor, + present: + | BooleanFieldValue + | { + present: BooleanFieldValue + setPresent?: string | ((struct: AnyStruct, present: boolean) => void) + }, +): StructPropertyDescriptor +export function optional( + field: StructPropertyDescriptor, + present: + | BooleanFieldValue + | { + present: BooleanFieldValue + setPresent?: string | ((struct: AnyStruct, present: boolean) => void) + }, +): StructPropertyDescriptor { + const getter = field.get + if (typeof getter !== "function") { + throw new TypeError("optional() requires a getter") + } + const presentDescriptor = typeof present === "object" + ? present + : { present, setPresent: present } + const descriptor: StructPropertyDescriptor = { get() { - return Boolean(structDataView(this).getUint8(fieldOffset)) - }, - set(value) { - structDataView(this).setUint8(fieldOffset, value ? 1 : 0) + if (!resolveBooleanFieldValue(this, presentDescriptor.present)) { + return undefined + } + return getter.call(this) }, } + if (typeof field.set === "function") { + descriptor.set = function (value) { + if (typeof value === "undefined") { + setBooleanFieldValue( + this, + presentDescriptor.setPresent ?? presentDescriptor.present, + false, + ) + return + } + field.set?.call(this, value) + setBooleanFieldValue( + this, + presentDescriptor.setPresent ?? presentDescriptor.present, + true, + ) + } + } + return descriptor } /** @@ -321,20 +504,31 @@ export function substruct< T extends object, >( ctor: StructConstructor, - byteOffset?: number, - bytelength?: number, + byteOffset?: NumberFieldValue, + bytelength?: OptionalNumberFieldValue, ): StructPropertyDescriptor & ReadOnlyAccessorDescriptor { - return fromDataView( - (dv) => { - const offset2 = dv.byteOffset + (byteOffset ?? 0) - const bytelength2 = bytelength ?? (dv.byteLength - (byteOffset ?? 0)) + return { + get() { + const dv = structDataView(this) + const resolvedByteOffset = resolveOptionalNumberFieldValue( + this, + byteOffset, + "byteOffset", + ) ?? 0 + const resolvedByteLength = resolveOptionalNumberFieldValue( + this, + bytelength, + "byteLength", + ) ?? (dv.byteLength - resolvedByteOffset) + const offset2 = dv.byteOffset + resolvedByteOffset + const bytelength2 = resolvedByteLength return Reflect.construct(ctor, [{ buffer: dv.buffer, byteOffset: offset2, byteLength: bytelength2, }]) }, - ) + } } /** @@ -349,10 +543,10 @@ export function substruct< * @param fieldOffset where the array starts relative to the parent struct */ export function typedArray( - fieldOffset: number, + fieldOffset: NumberFieldValue, kwargs: { /** length or property name for the length of the array */ - readonly length: number | string | undefined + readonly length: OptionalNumberFieldValue /** TypedArray constructor */ readonly species: TypedArraySpecies }, @@ -361,19 +555,27 @@ export function typedArray( return { get() { const dv = structDataView(this) + const resolvedFieldOffset = resolveNumberFieldValue( + this, + fieldOffset, + "fieldOffset", + ) let lengthValue: number | undefined - if (typeof length === "undefined") { + const resolvedLength = resolveOptionalNumberFieldValue( + this, + length, + "length", + ) + if (typeof resolvedLength === "undefined") { lengthValue = Math.floor( - (dv.byteLength - fieldOffset) / species.BYTES_PER_ELEMENT, + (dv.byteLength - resolvedFieldOffset) / species.BYTES_PER_ELEMENT, ) - } else if (typeof length === "number") { - lengthValue = length - } else if (typeof length === "string") { - lengthValue = Reflect.get(this, length) + } else { + lengthValue = resolvedLength } return new species( dv.buffer, - dv.byteOffset + fieldOffset, + dv.byteOffset + resolvedFieldOffset, lengthValue, ) }, @@ -383,126 +585,99 @@ export function typedArray( /** * Field for a big-endian 16-bit unsigned integer */ -export function u16be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getUint16(fieldOffset, false) - }, - set(value) { - structDataView(this).setUint16(fieldOffset, value, false) - }, - } +export function u16be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getUint16(offset, false), + (dv, offset, value) => dv.setUint16(offset, value, false), + ) } /** * Field for a big-endian 32-bit unsigned integer */ -export function u32be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getUint32(fieldOffset, false) - }, - set(value) { - structDataView(this).setUint32(fieldOffset, value, false) - }, - } +export function u32be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getUint32(offset, false), + (dv, offset, value) => dv.setUint32(offset, value, false), + ) } /** * Field for a big-endian 64-bit unsigned integer */ -export function u64be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getBigUint64(fieldOffset, false) - }, - set(value) { - structDataView(this).setBigUint64(fieldOffset, value, false) - }, - } +export function u64be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getBigUint64(offset, false), + (dv, offset, value) => dv.setBigUint64(offset, value, false), + ) } /** * Field for a big-endian 16-bit signed integer */ -export function i16be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getInt16(fieldOffset, false) - }, - set(value) { - structDataView(this).setInt16(fieldOffset, value, false) - }, - } +export function i16be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getInt16(offset, false), + (dv, offset, value) => dv.setInt16(offset, value, false), + ) } /** * Field for a big-endian 32-bit signed integer */ -export function i32be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getInt32(fieldOffset, false) - }, - set(value) { - structDataView(this).setInt32(fieldOffset, value, false) - }, - } +export function i32be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getInt32(offset, false), + (dv, offset, value) => dv.setInt32(offset, value, false), + ) } /** * Field for a big-endian 64-bit signed integer */ -export function i64be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getBigInt64(fieldOffset, false) - }, - set(value) { - structDataView(this).setBigInt64(fieldOffset, value, false) - }, - } +export function i64be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getBigInt64(offset, false), + (dv, offset, value) => dv.setBigInt64(offset, value, false), + ) } /** * Field for a big-endian 16-bit binary float (float16_t) */ -export function f16be(fieldOffset: number): StructPropertyDescriptor { +export function f16be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { if ( typeof DataView.prototype.getFloat16 !== "function" || typeof DataView.prototype.setFloat16 !== "function" ) { throw new TypeError("float16 is not supported in this environment") } - return { - get() { - return structDataView(this).getFloat16(fieldOffset, false) - }, - set(value) { - structDataView(this).setFloat16(fieldOffset, value, false) - }, - } + return dataViewField( + fieldOffset, + (dv, offset) => dv.getFloat16(offset, false), + (dv, offset, value) => dv.setFloat16(offset, value, false), + ) } /** * Field for a big-endian 32-bit binary float (float32_t) */ -export function f32be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getFloat32(fieldOffset, false) - }, - set(value) { - structDataView(this).setFloat32(fieldOffset, value, false) - }, - } +export function f32be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getFloat32(offset, false), + (dv, offset, value) => dv.setFloat32(offset, value, false), + ) } /** * Field for a big-endian 64-bit binary float (float64_t) */ -export function f64be(fieldOffset: number): StructPropertyDescriptor { - return { - get() { - return structDataView(this).getFloat64(fieldOffset, false) - }, - set(value) { - structDataView(this).setFloat64(fieldOffset, value, false) - }, - } +export function f64be(fieldOffset: NumberFieldValue): StructPropertyDescriptor { + return dataViewField( + fieldOffset, + (dv, offset) => dv.getFloat64(offset, false), + (dv, offset, value) => dv.setFloat64(offset, value, false), + ) } diff --git a/mod_test.ts b/mod_test.ts index 44458a4..797b6e8 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -10,6 +10,7 @@ import { i32, i64, i8, + optional, string, substruct, typedArray, @@ -390,6 +391,54 @@ test("typedArray", () => { deepStrictEqual(new Float32Array(buf.buffer.slice(4, 16)), f32s2) }) +test("optional field can follow a presence property", () => { + const buf = new Uint8Array(8) + class S extends defineStruct({ + has_name: bool(0), + name_length: u8(1), + name: optional(string(2, "name_length"), "has_name"), + }) {} + const instance = new S(buf) + + instance.name_length = 3 + deepStrictEqual(instance.name, undefined) + + instance.name = "cat" + deepStrictEqual(instance.has_name, true) + deepStrictEqual(instance.name, "cat") + deepStrictEqual(buf.slice(2, 5), new Uint8Array([99, 97, 116])) + + instance.name = undefined + deepStrictEqual(instance.has_name, false) + deepStrictEqual(instance.name, undefined) +}) + +test("field offsets and lengths can come from other properties", () => { + const buf = new Uint8Array(12) + class S extends defineStruct({ + has_name: bool(0), + name_length: u8(1), + name: optional(string(2, "name_length"), "has_name"), + score_offset: { + get() { + return 2 + (this.has_name ? this.name_length : 0) + }, + }, + score: u16("score_offset"), + }) {} + const instance = new S(buf) + + instance.score = 0x1234 + deepStrictEqual(buf.slice(2, 4), new Uint8Array([0x34, 0x12])) + + instance.name_length = 3 + instance.name = "cat" + instance.score = 0xabcd + deepStrictEqual(instance.score_offset, 5) + deepStrictEqual(instance.score, 0xabcd) + deepStrictEqual(buf.slice(5, 7), new Uint8Array([0xcd, 0xab])) +}) + test("getter-only properties inferred as readonly", () => { class S extends defineStruct({ y: { From 45c30670dcc4c0413daaca204b27a765df274131 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:33:32 +0000 Subject: [PATCH 3/7] chore: polish dynamic field API Agent-Logs-Url: https://github.com/rotu/structview/sessions/0716fb80-af23-468f-86bd-27d35fdca170 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- fields.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/fields.ts b/fields.ts index 81e4b88..11014a2 100644 --- a/fields.ts +++ b/fields.ts @@ -12,14 +12,14 @@ import type { TypedArraySpecies, } from "./types.ts" -type FieldValue = T | string | ((struct: AnyStruct) => T) -type BooleanFieldValue = FieldValue -type NumberFieldValue = FieldValue -type OptionalNumberFieldValue = FieldValue | undefined +type ResolvableValue = T | string | ((struct: AnyStruct) => T) +type BooleanFieldValue = ResolvableValue +type NumberFieldValue = ResolvableValue +type OptionalNumberFieldValue = ResolvableValue | undefined function resolveFieldValue( struct: AnyStruct, - value: FieldValue, + value: ResolvableValue, ): T { if (typeof value === "function") { return value(struct) @@ -88,8 +88,8 @@ function setBooleanFieldValue( function dataViewField( fieldOffset: NumberFieldValue, - fieldGetter: (dv: DataView, fieldOffset: number) => T, - fieldSetter: (dv: DataView, fieldOffset: number, value: T) => void, + fieldGetter: (dv: DataView, offset: number) => T, + fieldSetter: (dv: DataView, offset: number, value: T) => void, ): StructPropertyDescriptor { return { get() { @@ -420,7 +420,9 @@ export function optional( ): StructPropertyDescriptor { const getter = field.get if (typeof getter !== "function") { - throw new TypeError("optional() requires a getter") + throw new TypeError( + "optional() requires a field descriptor with a getter function", + ) } const presentDescriptor = typeof present === "object" ? present From a0566f0d2436553ffbffc90c40167f968327848c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:34:22 +0000 Subject: [PATCH 4/7] chore: clean up dynamic length checks Agent-Logs-Url: https://github.com/rotu/structview/sessions/0716fb80-af23-468f-86bd-27d35fdca170 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- fields.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fields.ts b/fields.ts index 11014a2..d5a64b2 100644 --- a/fields.ts +++ b/fields.ts @@ -207,7 +207,7 @@ export function biguintle( ) if ( !Number.isInteger(resolvedByteLength) || - !(0 < resolvedByteLength) + resolvedByteLength <= 0 ) { throw new TypeError("byteLength must resolve to a positive integer") } @@ -231,7 +231,7 @@ export function biguintle( ) if ( !Number.isInteger(resolvedByteLength) || - !(0 < resolvedByteLength) + resolvedByteLength <= 0 ) { throw new TypeError("byteLength must resolve to a positive integer") } From 983815b031614d9b41e5293c66b7de8a28bfa8cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:35:17 +0000 Subject: [PATCH 5/7] refactor: tidy optional field helpers Agent-Logs-Url: https://github.com/rotu/structview/sessions/0716fb80-af23-468f-86bd-27d35fdca170 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- fields.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/fields.ts b/fields.ts index d5a64b2..a64ffd5 100644 --- a/fields.ts +++ b/fields.ts @@ -57,6 +57,18 @@ function resolveOptionalNumberFieldValue( return result } +function resolvePositiveIntegerFieldValue( + struct: AnyStruct, + value: NumberFieldValue, + name: string, +): number { + const result = resolveNumberFieldValue(struct, value, name) + if (!Number.isInteger(result) || result <= 0) { + throw new TypeError(`${name} must resolve to a positive integer`) + } + return result +} + function resolveBooleanFieldValue( struct: AnyStruct, value: BooleanFieldValue, @@ -200,17 +212,11 @@ export function biguintle( fieldOffset, "fieldOffset", ) - const resolvedByteLength = resolveNumberFieldValue( + const resolvedByteLength = resolvePositiveIntegerFieldValue( this, byteLength, "byteLength", ) - if ( - !Number.isInteger(resolvedByteLength) || - resolvedByteLength <= 0 - ) { - throw new TypeError("byteLength must resolve to a positive integer") - } let result = 0n const dv = structDataView(this) for (let i = 0; i < resolvedByteLength; ++i) { @@ -224,17 +230,11 @@ export function biguintle( fieldOffset, "fieldOffset", ) - const resolvedByteLength = resolveNumberFieldValue( + const resolvedByteLength = resolvePositiveIntegerFieldValue( this, byteLength, "byteLength", ) - if ( - !Number.isInteger(resolvedByteLength) || - resolvedByteLength <= 0 - ) { - throw new TypeError("byteLength must resolve to a positive integer") - } const dv = structDataView(this) for (let i = 0; i < resolvedByteLength; ++i) { dv.setUint8( @@ -424,12 +424,12 @@ export function optional( "optional() requires a field descriptor with a getter function", ) } - const presentDescriptor = typeof present === "object" + const presenceConfig = typeof present === "object" ? present : { present, setPresent: present } const descriptor: StructPropertyDescriptor = { get() { - if (!resolveBooleanFieldValue(this, presentDescriptor.present)) { + if (!resolveBooleanFieldValue(this, presenceConfig.present)) { return undefined } return getter.call(this) @@ -440,7 +440,7 @@ export function optional( if (typeof value === "undefined") { setBooleanFieldValue( this, - presentDescriptor.setPresent ?? presentDescriptor.present, + presenceConfig.setPresent ?? presenceConfig.present, false, ) return @@ -448,7 +448,7 @@ export function optional( field.set?.call(this, value) setBooleanFieldValue( this, - presentDescriptor.setPresent ?? presentDescriptor.present, + presenceConfig.setPresent ?? presenceConfig.present, true, ) } From 7092d9b3609cc1b3dc796541edd3009e66e6c96a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:36:15 +0000 Subject: [PATCH 6/7] fix: tighten optional field typing Agent-Logs-Url: https://github.com/rotu/structview/sessions/0716fb80-af23-468f-86bd-27d35fdca170 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- fields.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fields.ts b/fields.ts index a64ffd5..1d69851 100644 --- a/fields.ts +++ b/fields.ts @@ -257,7 +257,7 @@ export function bigintle( return { get() { const resolvedOffset = resolveNumberFieldValue(this, offset, "fieldOffset") - const resolvedByteLength = resolveNumberFieldValue( + const resolvedByteLength = resolvePositiveIntegerFieldValue( this, byteLength, "byteLength", @@ -271,7 +271,7 @@ export function bigintle( }, set(value) { const resolvedOffset = resolveNumberFieldValue(this, offset, "fieldOffset") - const resolvedByteLength = resolveNumberFieldValue( + const resolvedByteLength = resolvePositiveIntegerFieldValue( this, byteLength, "byteLength", @@ -392,7 +392,7 @@ export function bool(fieldOffset: NumberFieldValue): StructPropertyDescriptor( - field: StructPropertyDescriptor & ReadOnlyAccessorDescriptor, + field: StructPropertyDescriptor & { get(): T; set?: undefined }, present: | BooleanFieldValue | { From 086b2b882f59e81a2ffa5f3f26381c58c71988bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:37:05 +0000 Subject: [PATCH 7/7] chore: clarify optional field errors Agent-Logs-Url: https://github.com/rotu/structview/sessions/0716fb80-af23-468f-86bd-27d35fdca170 Co-authored-by: rotu <119948+rotu@users.noreply.github.com> --- fields.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fields.ts b/fields.ts index 1d69851..302b7ef 100644 --- a/fields.ts +++ b/fields.ts @@ -75,7 +75,9 @@ function resolveBooleanFieldValue( ): boolean { const result = resolveFieldValue(struct, value) if (typeof result !== "boolean") { - throw new TypeError("present must resolve to a boolean") + throw new TypeError( + "optional field presence condition must resolve to a boolean", + ) } return result } @@ -421,7 +423,7 @@ export function optional( const getter = field.get if (typeof getter !== "function") { throw new TypeError( - "optional() requires a field descriptor with a getter function", + "optional() requires a field descriptor with a defined 'get' method", ) } const presenceConfig = typeof present === "object"