diff --git a/src/shared/shared.ts b/src/shared/shared.ts index aa5b013..26a89f2 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -1,4 +1,8 @@ import type { Descriptor } from './descriptors.ts'; +import { int32, int64, bool } from './descriptors.ts'; +import { Int32 } from './int32.ts'; +import { Int64 } from './int64.ts'; +import { Bool } from './bool.ts'; import { SharedStruct } from './shared-struct.ts'; import { Tuple } from './tuple.ts'; @@ -15,16 +19,34 @@ function align(offset: number, alignment: number): number { return remainder === 0 ? offset : offset + (alignment - remainder); } +function resolveValue(value: unknown): { descriptor: Descriptor; initialValue: unknown } | null { + if (typeof value === 'number') { + if (!Number.isInteger(value) || value > 2_147_483_647 || value < -2_147_483_648) { + throw new RangeError(`Value ${value} out of range for int32. Use int64 with bigint instead.`); + } + return { descriptor: int32 as unknown as Descriptor, initialValue: value }; + } + if (typeof value === 'bigint') { + return { descriptor: int64 as unknown as Descriptor, initialValue: value }; + } + if (typeof value === 'boolean') { + return { descriptor: bool as unknown as Descriptor, initialValue: value }; + } + return null; +} + interface LeafEntry { path: string[]; descriptor: Descriptor; offset: number; + initialValue?: unknown; } interface ArrayLeafEntry { type: 'leaf'; descriptor: Descriptor; offset: number; + initialValue?: unknown; } interface ArrayStructEntry { @@ -51,6 +73,14 @@ function collectLeaves(schema: Record, path: string[], leaves: collectArrayLeaves(value, leaves, cursor); } else if (isStructSchema(value)) { collectLeaves(value as Record, [...path, key], leaves, cursor); + } else { + const resolved = resolveValue(value); + if (resolved !== null) { + const { descriptor, initialValue } = resolved; + cursor.offset = align(cursor.offset, descriptor.byteAlignment); + leaves.push({ path: [...path, key], descriptor, offset: cursor.offset, initialValue }); + cursor.offset += descriptor.byteSize; + } } } } @@ -65,6 +95,14 @@ function collectArrayLeaves(schema: unknown[], leaves: LeafEntry[], cursor: { of collectArrayLeaves(element, leaves, cursor); } else if (isStructSchema(element)) { collectLeaves(element as Record, [], leaves, cursor); + } else { + const resolved = resolveValue(element); + if (resolved !== null) { + const { descriptor, initialValue } = resolved; + cursor.offset = align(cursor.offset, descriptor.byteAlignment); + leaves.push({ path: [], descriptor, offset: cursor.offset, initialValue }); + cursor.offset += descriptor.byteSize; + } } } } @@ -83,6 +121,14 @@ function processArraySchema(schema: unknown[], cursor: { offset: number }): { en const leaves: LeafEntry[] = []; collectLeaves(element as Record, [], leaves, cursor); entries.push({ type: 'struct', schema: element as Record, leaves }); + } else { + const resolved = resolveValue(element); + if (resolved !== null) { + const { descriptor, initialValue } = resolved; + cursor.offset = align(cursor.offset, descriptor.byteAlignment); + entries.push({ type: 'leaf', descriptor, offset: cursor.offset, initialValue }); + cursor.offset += descriptor.byteSize; + } } } return { entries }; @@ -91,7 +137,11 @@ function processArraySchema(schema: unknown[], cursor: { offset: number }): { en function buildTupleFromEntries(entries: ArrayEntry[], buffer: SharedArrayBuffer): Tuple { const elements = entries.map((entry) => { if (entry.type === 'leaf') { - return new entry.descriptor._class(buffer, entry.offset); + const instance = new entry.descriptor._class(buffer, entry.offset); + if ('initialValue' in entry) { + (instance as any).store(entry.initialValue); + } + return instance; } if (entry.type === 'struct') { const leafIndex = { i: 0 }; @@ -116,6 +166,13 @@ function buildStructTree(schema: Record, leaves: LeafEntry[], b fields[key] = buildTupleFromEntries(arrayEntries, buffer); } else if (isStructSchema(value)) { fields[key] = buildStructTree(value as Record, leaves, buffer, leafIndex); + } else if (resolveValue(value) !== null) { + const leaf = leaves[leafIndex.i++]; + const instance = new leaf.descriptor._class(buffer, leaf.offset) as any; + if ('initialValue' in leaf) { + instance.store(leaf.initialValue); + } + fields[key] = instance; } } return new SharedStruct(fields); @@ -131,18 +188,19 @@ function processArraySchemaFromLeaves(schema: unknown[], leaves: LeafEntry[], le const nested = processArraySchemaFromLeaves(element, leaves, leafIndex); entries.push({ type: 'tuple', entries: nested }); } else if (isStructSchema(element)) { - const structLeaves: LeafEntry[] = []; const startIndex = leafIndex.i; // Count leaves for this struct countStructLeaves(element as Record, leaves, leafIndex); const endIndex = leafIndex.i; const subLeaves = leaves.slice(startIndex, endIndex); - const subLeafIndex = { i: 0 }; entries.push({ type: 'struct', schema: element as Record, leaves: subLeaves, }); + } else if (resolveValue(element) !== null) { + const leaf = leaves[leafIndex.i++]; + entries.push({ type: 'leaf', descriptor: leaf.descriptor, offset: leaf.offset, initialValue: leaf.initialValue }); } } return entries; @@ -157,6 +215,8 @@ function countStructLeaves(schema: Record, leaves: LeafEntry[], countArrayLeaves(value, leaves, leafIndex); } else if (isStructSchema(value)) { countStructLeaves(value as Record, leaves, leafIndex); + } else if (resolveValue(value) !== null) { + leafIndex.i++; } } } @@ -169,11 +229,34 @@ function countArrayLeaves(schema: unknown[], leaves: LeafEntry[], leafIndex: { i countArrayLeaves(element, leaves, leafIndex); } else if (isStructSchema(element)) { countStructLeaves(element as Record, leaves, leafIndex); + } else if (resolveValue(element) !== null) { + leafIndex.i++; } } } export function shared(schema: unknown): any { + if (typeof schema === 'number') { + if (!Number.isInteger(schema) || schema > 2_147_483_647 || schema < -2_147_483_648) { + throw new RangeError(`Value ${schema} out of range for int32. Use int64 with bigint instead.`); + } + const instance = new Int32(new SharedArrayBuffer(Int32.byteSize), 0); + instance.store(schema); + return instance; + } + + if (typeof schema === 'bigint') { + const instance = new Int64(new SharedArrayBuffer(Int64.byteSize), 0); + instance.store(schema); + return instance; + } + + if (typeof schema === 'boolean') { + const instance = new Bool(new SharedArrayBuffer(Bool.byteSize), 0); + instance.store(schema); + return instance; + } + if (isDescriptor(schema)) { return schema(); } diff --git a/test/shared/value-shorthand.test.ts b/test/shared/value-shorthand.test.ts new file mode 100644 index 0000000..4d05fe6 --- /dev/null +++ b/test/shared/value-shorthand.test.ts @@ -0,0 +1,67 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { shared } from 'moroutine'; + +describe('shared() value shorthand', () => { + it('shared(0) creates int32 initialized to 0', () => { + const x = shared(0); + assert.equal(x.load(), 0); + }); + + it('shared(42) creates int32 initialized to 42', () => { + const x = shared(42); + assert.equal(x.load(), 42); + }); + + it('shared(-1) creates int32 initialized to -1', () => { + const x = shared(-1); + assert.equal(x.load(), -1); + }); + + it('shared(true) creates bool initialized to true', () => { + const x = shared(true); + assert.equal(x.load(), true); + }); + + it('shared(false) creates bool initialized to false', () => { + const x = shared(false); + assert.equal(x.load(), false); + }); + + it('shared(0n) creates int64 initialized to 0n', () => { + const x = shared(0n); + assert.equal(x.load(), 0n); + }); + + it('shared(99n) creates int64 initialized to 99n', () => { + const x = shared(99n); + assert.equal(x.load(), 99n); + }); + + it('shared(value) throws for out-of-range int32', () => { + assert.throws(() => shared(Number.MAX_SAFE_INTEGER), /out of range|exceeds/i); + assert.throws(() => shared(2_147_483_648), /out of range|exceeds/i); + assert.throws(() => shared(-2_147_483_649), /out of range|exceeds/i); + }); + + it('shared(value) throws for non-integer number', () => { + assert.throws(() => shared(1.5), /out of range|integer/i); + }); + + it('value shorthand in struct schema', () => { + const point = shared({ x: 10, y: 20 }); + assert.deepEqual(point.load(), { x: 10, y: 20 }); + }); + + it('value shorthand in tuple schema', () => { + const t = shared([1, 2n, true]); + assert.deepEqual(t.load(), [1, 2n, true]); + }); + + it('mixed descriptors and values in struct', () => { + const s = shared({ x: 0, y: 0, alive: true }); + assert.deepEqual(s.load(), { x: 0, y: 0, alive: true }); + s.store({ x: 10, y: 20, alive: false }); + assert.deepEqual(s.load(), { x: 10, y: 20, alive: false }); + }); +});