From 21c8fcec451538726b653a9826ab777417bbcc96 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 08 Apr 2026 22:35:23 +0000 Subject: [PATCH] feat: non-atomic shared primitives (Int8-Uint64, Bool) Co-Authored-By: Claude Sonnet 4.6 --- src/index.ts | 8 ++++++++ src/shared/bool.ts | 28 ++++++++++++++++++++++++++++ src/shared/index.ts | 8 ++++++++ src/shared/int16.ts | 28 ++++++++++++++++++++++++++++ src/shared/int64.ts | 28 ++++++++++++++++++++++++++++ src/shared/int8.ts | 28 ++++++++++++++++++++++++++++ src/shared/uint16.ts | 28 ++++++++++++++++++++++++++++ src/shared/uint32.ts | 28 ++++++++++++++++++++++++++++ src/shared/uint64.ts | 28 ++++++++++++++++++++++++++++ src/shared/uint8.ts | 28 ++++++++++++++++++++++++++++ test/shared/bool.test.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test/shared/int16.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/shared/int64.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/shared/int8.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/shared/uint16.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/shared/uint32.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/shared/uint64.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/shared/uint8.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 18 file(s) changed, 559 insertion(s)(+), 0 deletion(s)(-) diff --git a/src/index.ts b/src/index.ts --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,15 @@ export type { Runner } from './runner.ts'; export type { Loadable } from './shared/index.ts'; export { + Int8, + Uint8, + Int16, + Uint16, Int32, + Uint32, + Int64, + Uint64, + Bool, AtomicBool, AtomicInt8, AtomicUint8, diff --git a/src/shared/bool.ts b/src/shared/bool.ts new file mode 100644 --- /dev/null +++ b/src/shared/bool.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Bool implements Loadable { + static readonly byteSize = 1; + static readonly byteAlignment = 1; + private readonly view: Uint8Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(1); + const offset = byteOffset ?? 0; + this.view = new Uint8Array(buf, offset, 1); + } + + load(): boolean { + return this.view[0] !== 0; + } + + store(value: boolean): void { + this.view[0] = value ? 1 : 0; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Bool', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Bool', Bool); diff --git a/src/shared/index.ts b/src/shared/index.ts --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,5 +1,13 @@ export type { Loadable } from './loadable.ts'; +export { Int8 } from './int8.ts'; +export { Uint8 } from './uint8.ts'; +export { Int16 } from './int16.ts'; +export { Uint16 } from './uint16.ts'; export { Int32 } from './int32.ts'; +export { Uint32 } from './uint32.ts'; +export { Int64 } from './int64.ts'; +export { Uint64 } from './uint64.ts'; +export { Bool } from './bool.ts'; export { AtomicBool } from './atomic-bool.ts'; export { AtomicInt8 } from './atomic-int8.ts'; export { AtomicUint8 } from './atomic-uint8.ts'; diff --git a/src/shared/int16.ts b/src/shared/int16.ts new file mode 100644 --- /dev/null +++ b/src/shared/int16.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Int16 implements Loadable { + static readonly byteSize = 2; + static readonly byteAlignment = 2; + private readonly view: Int16Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(2); + const offset = byteOffset ?? 0; + this.view = new Int16Array(buf, offset, 1); + } + + load(): number { + return this.view[0]; + } + + store(value: number): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Int16', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Int16', Int16); diff --git a/src/shared/int64.ts b/src/shared/int64.ts new file mode 100644 --- /dev/null +++ b/src/shared/int64.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Int64 implements Loadable { + static readonly byteSize = 8; + static readonly byteAlignment = 8; + private readonly view: BigInt64Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(8); + const offset = byteOffset ?? 0; + this.view = new BigInt64Array(buf, offset, 1); + } + + load(): bigint { + return this.view[0]; + } + + store(value: bigint): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Int64', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Int64', Int64); diff --git a/src/shared/int8.ts b/src/shared/int8.ts new file mode 100644 --- /dev/null +++ b/src/shared/int8.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Int8 implements Loadable { + static readonly byteSize = 1; + static readonly byteAlignment = 1; + private readonly view: Int8Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(1); + const offset = byteOffset ?? 0; + this.view = new Int8Array(buf, offset, 1); + } + + load(): number { + return this.view[0]; + } + + store(value: number): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Int8', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Int8', Int8); diff --git a/src/shared/uint16.ts b/src/shared/uint16.ts new file mode 100644 --- /dev/null +++ b/src/shared/uint16.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Uint16 implements Loadable { + static readonly byteSize = 2; + static readonly byteAlignment = 2; + private readonly view: Uint16Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(2); + const offset = byteOffset ?? 0; + this.view = new Uint16Array(buf, offset, 1); + } + + load(): number { + return this.view[0]; + } + + store(value: number): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Uint16', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Uint16', Uint16); diff --git a/src/shared/uint32.ts b/src/shared/uint32.ts new file mode 100644 --- /dev/null +++ b/src/shared/uint32.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Uint32 implements Loadable { + static readonly byteSize = 4; + static readonly byteAlignment = 4; + private readonly view: Uint32Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(4); + const offset = byteOffset ?? 0; + this.view = new Uint32Array(buf, offset, 1); + } + + load(): number { + return this.view[0]; + } + + store(value: number): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Uint32', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Uint32', Uint32); diff --git a/src/shared/uint64.ts b/src/shared/uint64.ts new file mode 100644 --- /dev/null +++ b/src/shared/uint64.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Uint64 implements Loadable { + static readonly byteSize = 8; + static readonly byteAlignment = 8; + private readonly view: BigUint64Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(8); + const offset = byteOffset ?? 0; + this.view = new BigUint64Array(buf, offset, 1); + } + + load(): bigint { + return this.view[0]; + } + + store(value: bigint): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Uint64', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Uint64', Uint64); diff --git a/src/shared/uint8.ts b/src/shared/uint8.ts new file mode 100644 --- /dev/null +++ b/src/shared/uint8.ts @@ -0,0 +1,28 @@ +import { registerSync } from './reconstruct.ts'; +import type { Loadable } from './loadable.ts'; + +export class Uint8 implements Loadable { + static readonly byteSize = 1; + static readonly byteAlignment = 1; + private readonly view: Uint8Array; + + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { + const buf = buffer ?? new SharedArrayBuffer(1); + const offset = byteOffset ?? 0; + this.view = new Uint8Array(buf, offset, 1); + } + + load(): number { + return this.view[0]; + } + + store(value: number): void { + this.view[0] = value; + } + + [Symbol.for('moroutine.shared')](): { tag: string; buffer: SharedArrayBuffer; byteOffset: number } { + return { tag: 'Uint8', buffer: this.view.buffer as SharedArrayBuffer, byteOffset: this.view.byteOffset }; + } +} + +registerSync('Uint8', Uint8); diff --git a/test/shared/bool.test.ts b/test/shared/bool.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/bool.test.ts @@ -0,0 +1,53 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Bool } from 'moroutine'; + +describe('Bool', () => { + it('self-allocates and initializes to false', () => { + const v = new Bool(); + assert.equal(v.load(), false); + }); + + it('stores and loads true', () => { + const v = new Bool(); + v.store(true); + assert.equal(v.load(), true); + }); + + it('stores and loads false', () => { + const v = new Bool(); + v.store(true); + v.store(false); + assert.equal(v.load(), false); + }); + + it('round-trips true/false', () => { + const v = new Bool(); + v.store(true); + assert.equal(v.load(), true); + v.store(false); + assert.equal(v.load(), false); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(8); + const v = new Bool(buffer, 1); + v.store(true); + assert.equal(new Uint8Array(buffer, 1, 1)[0], 1); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(1); + const v = new Bool(buffer); + v.store(true); + assert.equal(new Uint8Array(buffer)[0], 1); + }); + + it('exposes static byteSize of 1', () => { + assert.equal(Bool.byteSize, 1); + }); + + it('exposes static byteAlignment of 1', () => { + assert.equal(Bool.byteAlignment, 1); + }); +}); diff --git a/test/shared/int16.test.ts b/test/shared/int16.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/int16.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Int16 } from 'moroutine'; + +describe('Int16', () => { + it('self-allocates and initializes to zero', () => { + const v = new Int16(); + assert.equal(v.load(), 0); + }); + + it('stores and loads a value', () => { + const v = new Int16(); + v.store(42); + assert.equal(v.load(), 42); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(16); + const v = new Int16(buffer, 2); + v.store(99); + assert.equal(new Int16Array(buffer, 2, 1)[0], 99); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(2); + const v = new Int16(buffer); + v.store(7); + assert.equal(new Int16Array(buffer)[0], 7); + }); + + it('exposes static byteSize of 2', () => { + assert.equal(Int16.byteSize, 2); + }); + + it('exposes static byteAlignment of 2', () => { + assert.equal(Int16.byteAlignment, 2); + }); +}); diff --git a/test/shared/int64.test.ts b/test/shared/int64.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/int64.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Int64 } from 'moroutine'; + +describe('Int64', () => { + it('self-allocates and initializes to zero', () => { + const v = new Int64(); + assert.equal(v.load(), 0n); + }); + + it('stores and loads a value', () => { + const v = new Int64(); + v.store(99n); + assert.equal(v.load(), 99n); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(16); + const v = new Int64(buffer, 8); + v.store(42n); + assert.equal(new BigInt64Array(buffer, 8, 1)[0], 42n); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(8); + const v = new Int64(buffer); + v.store(7n); + assert.equal(new BigInt64Array(buffer)[0], 7n); + }); + + it('exposes static byteSize of 8', () => { + assert.equal(Int64.byteSize, 8); + }); + + it('exposes static byteAlignment of 8', () => { + assert.equal(Int64.byteAlignment, 8); + }); +}); diff --git a/test/shared/int8.test.ts b/test/shared/int8.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/int8.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Int8 } from 'moroutine'; + +describe('Int8', () => { + it('self-allocates and initializes to zero', () => { + const v = new Int8(); + assert.equal(v.load(), 0); + }); + + it('stores and loads a value', () => { + const v = new Int8(); + v.store(42); + assert.equal(v.load(), 42); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(8); + const v = new Int8(buffer, 1); + v.store(99); + assert.equal(new Int8Array(buffer, 1, 1)[0], 99); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(1); + const v = new Int8(buffer); + v.store(7); + assert.equal(new Int8Array(buffer)[0], 7); + }); + + it('exposes static byteSize of 1', () => { + assert.equal(Int8.byteSize, 1); + }); + + it('exposes static byteAlignment of 1', () => { + assert.equal(Int8.byteAlignment, 1); + }); +}); diff --git a/test/shared/uint16.test.ts b/test/shared/uint16.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/uint16.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Uint16 } from 'moroutine'; + +describe('Uint16', () => { + it('self-allocates and initializes to zero', () => { + const v = new Uint16(); + assert.equal(v.load(), 0); + }); + + it('stores and loads a value', () => { + const v = new Uint16(); + v.store(42); + assert.equal(v.load(), 42); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(16); + const v = new Uint16(buffer, 2); + v.store(99); + assert.equal(new Uint16Array(buffer, 2, 1)[0], 99); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(2); + const v = new Uint16(buffer); + v.store(7); + assert.equal(new Uint16Array(buffer)[0], 7); + }); + + it('exposes static byteSize of 2', () => { + assert.equal(Uint16.byteSize, 2); + }); + + it('exposes static byteAlignment of 2', () => { + assert.equal(Uint16.byteAlignment, 2); + }); +}); diff --git a/test/shared/uint32.test.ts b/test/shared/uint32.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/uint32.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Uint32 } from 'moroutine'; + +describe('Uint32', () => { + it('self-allocates and initializes to zero', () => { + const v = new Uint32(); + assert.equal(v.load(), 0); + }); + + it('stores and loads a value', () => { + const v = new Uint32(); + v.store(42); + assert.equal(v.load(), 42); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(16); + const v = new Uint32(buffer, 4); + v.store(99); + assert.equal(new Uint32Array(buffer, 4, 1)[0], 99); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(4); + const v = new Uint32(buffer); + v.store(7); + assert.equal(new Uint32Array(buffer)[0], 7); + }); + + it('exposes static byteSize of 4', () => { + assert.equal(Uint32.byteSize, 4); + }); + + it('exposes static byteAlignment of 4', () => { + assert.equal(Uint32.byteAlignment, 4); + }); +}); diff --git a/test/shared/uint64.test.ts b/test/shared/uint64.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/uint64.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Uint64 } from 'moroutine'; + +describe('Uint64', () => { + it('self-allocates and initializes to zero', () => { + const v = new Uint64(); + assert.equal(v.load(), 0n); + }); + + it('stores and loads a value', () => { + const v = new Uint64(); + v.store(99n); + assert.equal(v.load(), 99n); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(16); + const v = new Uint64(buffer, 8); + v.store(42n); + assert.equal(new BigUint64Array(buffer, 8, 1)[0], 42n); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(8); + const v = new Uint64(buffer); + v.store(7n); + assert.equal(new BigUint64Array(buffer)[0], 7n); + }); + + it('exposes static byteSize of 8', () => { + assert.equal(Uint64.byteSize, 8); + }); + + it('exposes static byteAlignment of 8', () => { + assert.equal(Uint64.byteAlignment, 8); + }); +}); diff --git a/test/shared/uint8.test.ts b/test/shared/uint8.test.ts new file mode 100644 --- /dev/null +++ b/test/shared/uint8.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { Uint8 } from 'moroutine'; + +describe('Uint8', () => { + it('self-allocates and initializes to zero', () => { + const v = new Uint8(); + assert.equal(v.load(), 0); + }); + + it('stores and loads a value', () => { + const v = new Uint8(); + v.store(42); + assert.equal(v.load(), 42); + }); + + it('accepts an external buffer and byteOffset', () => { + const buffer = new SharedArrayBuffer(8); + const v = new Uint8(buffer, 1); + v.store(99); + assert.equal(new Uint8Array(buffer, 1, 1)[0], 99); + }); + + it('defaults byteOffset to 0', () => { + const buffer = new SharedArrayBuffer(1); + const v = new Uint8(buffer); + v.store(7); + assert.equal(new Uint8Array(buffer)[0], 7); + }); + + it('exposes static byteSize of 1', () => { + assert.equal(Uint8.byteSize, 1); + }); + + it('exposes static byteAlignment of 1', () => { + assert.equal(Uint8.byteAlignment, 1); + }); +}); -- tangled.sh