commit 574ccc87bfb857f1325021b697d169f7084aa0cf Author: luaneko Date: Tue Jan 7 02:58:12 2025 +1100 Initial commit diff --git a/async.ts b/async.ts new file mode 100644 index 0000000..ac222b9 --- /dev/null +++ b/async.ts @@ -0,0 +1,204 @@ +function noop() {} + +export interface Deferred extends Promise { + resolve(value: T | PromiseLike): void; + reject(reason?: unknown): void; +} + +export function deferred() { + const { promise, resolve, reject } = Promise.withResolvers(); + const p = promise as Deferred; + return (p.resolve = resolve), (p.reject = reject), p; +} + +export function notifier() { + let p: Deferred | null = null; + + return { + listen(): Promise { + return (p ??= deferred()); + }, + + notify(value: T) { + p &&= (p.resolve(value), null); + }, + }; +} + +export interface Channel { + send: Sender; + recv: Receiver; +} + +export interface Sender { + (value: T): void; + close(reason?: unknown): void; +} + +export interface Receiver { + (): Promise | T | null; + try(): T | null; + close(reason?: unknown): void; +} + +export function channel(): Channel { + const q = new Map(); + let head = 0; + let tail = 0; + let open = true; + let err: unknown = null; + + type Entry = + | { has: true; value: T } + | { has: false; value: Deferred }; + + function send(value: T) { + if (open) { + const i = head++; + let n = q.get(i); + if (!n) q.set(i, (n = { has: true, value })); + else if (!n.has) q.delete(i), n.value.resolve(value); + else n.value = value; + } else { + if (err !== null) throw err; + else return; + } + } + + function recv() { + if (open) { + const i = tail++; + let n = q.get(i); + if (!n) return q.set(i, (n = { has: false, value: deferred() })), n.value; + else if (!n.has) return n.value; + else return q.delete(i), n.value; + } else { + if (err !== null) throw err; + else return null; + } + } + + function try_recv() { + if (open) { + const n = q.get(tail); + if (n?.has) return q.delete(tail++), n.value; + else return null; + } else { + return null; + } + } + + recv.try = try_recv; + send.close = recv.close = function close(reason?: unknown) { + if (!open) return; + else (open = false), (err = reason ?? null); + for (const p of q.values()) { + if (p.has) continue; + else if (err !== null) p.value.reject(err); + else p.value.resolve(null); + } + q.clear(); + }; + + return { send, recv }; +} + +channel.sender = function sender( + f: (recv: Receiver) => void | PromiseLike +): Sender { + const { send, recv } = channel(); + Promise.resolve(f(recv)).then(noop).then(recv.close, recv.close); + return send; +}; + +channel.receiver = function receiver( + f: (send: Sender) => void | PromiseLike +): Receiver { + const { send, recv } = channel(); + Promise.resolve(f(send)).then(noop).then(send.close, send.close); + return recv; +}; + +export function semaphore(count = 1) { + const { send: signal, recv: wait } = channel(); + let n = count; + + async function acquire() { + if (--n < 0) await wait(); + return release; + } + + function release() { + if (n++ < 0) signal(); + } + + function reset(new_count = count) { + n = count = new_count; + } + + acquire.release = release; + acquire.reset = reset; + release[Symbol.dispose] = release; + release satisfies Disposable; + + return acquire; +} + +export interface PoolToken extends Iterable { + readonly value: T; + release(): void; +} + +export function pool(size: number, f: () => T | PromiseLike) { + const lock = semaphore(size); + const all = new Set(); + const free: T[] = []; + const Token = class implements PoolToken { + #value; + #open = true; + + get value() { + if (this.#open) return this.#value; + else throw new TypeError(`borrowed value is already released`); + } + + constructor(value: T) { + this.#value = value; + } + + *[Symbol.iterator]() { + yield this.value; + } + + release() { + this.#open &&= + (lock.release(), all.has(this.#value) && free.push(this.#value), false); + } + + [Symbol.dispose]() { + this.release(); + } + }; + + async function acquire(): Promise> { + await lock(); + try { + let value; + if (free.length !== 0) value = free.pop() as T; + else value = await f(); + return all.add(value), new Token(value); + } catch (e) { + throw (lock.release(), e); + } + } + + function forget(value: T) { + if (all.delete(value)) { + const idx = free.indexOf(value); + if (idx !== -1) free.splice(idx, 1); + } + } + + acquire.forget = forget; + return acquire; +} diff --git a/bytes.ts b/bytes.ts new file mode 100644 index 0000000..685c2db --- /dev/null +++ b/bytes.ts @@ -0,0 +1,60 @@ +import { + decodeBase64, + decodeHex, + encodeBase64, + encodeHex, +} from "@std/encoding"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +export type BinaryLike = string | Uint8Array; + +export function from_utf8(s: BinaryLike): string { + return typeof s === "string" ? s : decoder.decode(s); +} + +export function to_utf8(s: BinaryLike): Uint8Array { + return typeof s === "string" ? encoder.encode(s) : s; +} + +export function encode_utf8(s: BinaryLike, buf: Uint8Array): number { + if (typeof s === "string") return encoder.encodeInto(s, buf).written; + else return buf.set(s), s.length; +} + +export function from_hex(s: BinaryLike): Uint8Array { + return decodeHex(from_utf8(s)); +} + +export function to_hex(b: BinaryLike): string { + return encodeHex(to_utf8(b)); +} + +export function from_base64(s: BinaryLike): Uint8Array { + return decodeBase64(from_utf8(s)); +} + +export function to_base64(b: BinaryLike): string { + return encodeBase64(to_utf8(b)); +} + +export { equals as buf_eq, concat as buf_concat } from "@std/bytes"; + +export function buf_concat_fast(a: Uint8Array, b: Uint8Array) { + const m = a.length; + const c = new Uint8Array(m + b.length); + return c.set(a, 0), c.set(b, m), c; +} + +export function buf_resize(buf: Uint8Array, n: number) { + const res = new Uint8Array(n); + return res.set(buf.subarray(0, n)), res; +} + +export function buf_xor(a: Uint8Array, b: Uint8Array) { + const n = Math.max(a.length, b.length); + const c = new Uint8Array(n); + for (let i = 0; i < n; i++) c[i] = a[i] ^ b[i]; + return c; +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..d378fb7 --- /dev/null +++ b/deno.json @@ -0,0 +1,9 @@ +{ + "name": "@luaneko/lstd", + "version": "0.1.0", + "exports": "./mod.ts", + "imports": { + "@std/bytes": "jsr:@std/bytes@^1.0.4", + "@std/encoding": "jsr:@std/encoding@^1.0.6" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..f156b53 --- /dev/null +++ b/deno.lock @@ -0,0 +1,21 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/bytes@^1.0.4": "1.0.4", + "jsr:@std/encoding@^1.0.6": "1.0.6" + }, + "jsr": { + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/encoding@1.0.6": { + "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/bytes@^1.0.4", + "jsr:@std/encoding@^1.0.6" + ] + } +} diff --git a/events.ts b/events.ts new file mode 100644 index 0000000..e0413dc --- /dev/null +++ b/events.ts @@ -0,0 +1,132 @@ +// deno-lint-ignore-file no-explicit-any +import { jit } from "./jit.ts"; + +export class Emitter void> { + #ls: { f: F; once: boolean }[] = []; + #argc = 0; + + get argc() { + return this.#argc; + } + + get size() { + return this.#ls.length; + } + + constructor() { + this.emit = this.#emit_fast; + } + + on(f: F, once: boolean) { + this.#ls.push({ f, once }); + this.#argc = Math.max(this.#argc, f.length); + + if (once) this.emit = this.#emit_slow; + else this.#emit_fast = this.#compile(); + if (this.emit !== this.#emit_slow) this.emit = this.#emit_fast; + return this; + } + + off(f: F) { + const ls = this.#ls; + let once = false; + + for (let i = ls.length; i-- !== 0; ) { + if (ls[i].f === f) { + (once = ls[i].once), ls.splice(i, 1); + break; + } + } + + this.#argc = ls.length ? Math.max(...ls.map(({ f }) => f.length)) : 0; + if (!once) this.#emit_fast = this.#compile(); + if (this.emit !== this.#emit_slow) this.emit = this.#emit_fast; + return this; + } + + // deno-lint-ignore no-unused-vars + emit(...args: Parameters) { + return false; + } + + listeners() { + return this.#ls.map(({ f }) => f); + } + + #emit_fast = this.#compile(); + #emit_slow(...args: Parameters) { + const ls = this.#ls; + (this.#ls = ls.filter(({ once }) => !once)), (this.emit = this.#emit_fast); + + for (let i = 0, n = ls.length; i < n; i++) + Reflect.apply(ls[i].f, undefined, args); + + return ls.length !== 0; + } + + #compile(): typeof this.emit { + const ls = this.#ls.filter(({ once }) => !once); + const ps = [...Array(this.#argc).keys()].map((i) => jit.raw`a${i}`); + + return jit.compiled`function emit(${jit.fragment(", ", ...ps)}) { + ${jit.map(" ", ls, ({ f }) => { + return jit`${f}(${jit.fragment(", ", ...ps.slice(0, f.length))});`; + })} + return ${jit.literal(ls.length !== 0)}; + }`; + } +} + +export type EventMap = Record void>; + +export class TypedEmitter { + readonly #events: { [K in keyof T]?: Emitter } = Object.create(null); + + on(name: K, f: T[K]) { + return (this.#events[name] ??= new Emitter()).on(f, false), this; + } + + once(name: K, f: T[K]) { + return (this.#events[name] ??= new Emitter()).on(f, true), this; + } + + off(name: K, f: T[K]) { + const events = this.#events; + if (events[name]?.off(f).size === 0) delete events[name]; + return this; + } + + emit(name: K, ...args: Parameters) { + const event = this.#events[name]; + if (event) return event.emit.apply(event, args); + else return false; + } + + listeners(name: K) { + return this.#events[name]?.listeners() ?? []; + } + + static { + this.prototype.emit = function emit( + this: TypedEmitter, + name: string, + a: any, + b: any, + c: any, + d: any, + e: any + ) { + const event = this.#events[name]; + if (!event) return false; + + const { argc } = event; + if (argc < 6) { + return event.emit(a, b, c, d, e); + } else { + const args = Array(argc); + for (let i = 0; i < argc; i++) args[i] = arguments[i + 1]; + return event.emit.apply(undefined, args); + } + } as any; + } +} diff --git a/func.ts b/func.ts new file mode 100644 index 0000000..9dbb747 --- /dev/null +++ b/func.ts @@ -0,0 +1,32 @@ +// deno-lint-ignore-file no-explicit-any +export class Callable { + static { + Object.setPrototypeOf(this.prototype, Function.prototype); + } + + get name() { + return this.constructor.name; + } + + constructor(f: (...args: any) => any) { + let self: any; + + if (typeof f.prototype !== "undefined") { + self = function call(...args: unknown[]) { + return typeof new.target !== "undefined" + ? Reflect.construct(f, args, new.target) + : f(...args); + }; + + self.prototype = f.prototype; + } else { + self = (...args: unknown[]) => f(...args); + } + + f = f.bind(self); + + delete self.name; + delete self.length; + return Object.setPrototypeOf(self, new.target.prototype); + } +} diff --git a/jit.ts b/jit.ts new file mode 100644 index 0000000..394bc27 --- /dev/null +++ b/jit.ts @@ -0,0 +1,113 @@ +export const jit_format = Symbol.for(`re.lua.lstd.jit_format`); + +export interface JitFragment { + [jit_format](c: JitCompiler): void; +} + +export function is_jit(x: unknown): x is JitFragment { + return typeof x === "object" && x !== null && jit_format in x; +} + +export class JitCompiler { + #body = ""; + #args = new Map(); + + write(s: string | JitFragment) { + if (is_jit(s)) s[jit_format](this); + else this.#body += s; + } + + enclose(x: unknown) { + let name = this.#args.get(x); + if (!name) this.#args.set(x, (name = `__x${this.#args.size}`)); + this.write(name); + } + + format(x: unknown) { + is_jit(x) ? this.write(x) : this.enclose(x); + } + + compile() { + return new Function( + ...this.#args.values(), + `"use strict"; return (${this.#body});` + )(...this.#args.keys()); + } +} + +export function jit( + { raw: s }: TemplateStringsArray, + ...xs: unknown[] +): JitFragment { + return { + [jit_format](c) { + for (let i = 0, n = s.length; i < n; i++) { + if (i !== 0) c.format(xs[i - 1]); + c.write(s[i]); + } + }, + }; +} + +jit.compiled = compiled; +jit.raw = raw; +jit.fragment = fragment; +jit.literal = literal; +jit.map = map; +jit.if = condition; + +export function compiled( + s: TemplateStringsArray, + ...xs: unknown[] +): T { + const c = new JitCompiler(); + return c.write(jit(s, ...xs)), c.compile(); +} + +export function raw(s: TemplateStringsArray, ...xs: unknown[]): JitFragment; +export function raw(s: string): JitFragment; +export function raw( + s: TemplateStringsArray | string, + ...xs: unknown[] +): JitFragment { + s = typeof s === "string" ? s : String.raw(s, ...xs); + return { + [jit_format](c) { + c.write(s); + }, + }; +} + +export function fragment( + sep: string | JitFragment, + ...xs: unknown[] +): JitFragment { + return { + [jit_format](c) { + for (let i = 0, n = xs.length; i < n; i++) { + if (i !== 0) c.write(sep); + c.format(xs[i]); + } + }, + }; +} + +export function literal(x: unknown) { + return raw(typeof x === "undefined" ? "undefined" : JSON.stringify(x)); +} + +export function map( + sep: string | JitFragment, + xs: Iterable, + f: (value: T, index: number) => unknown +): JitFragment { + return fragment(sep, ...Iterator.from(xs).map(f)); +} + +export function condition( + test: unknown, + consequent: JitFragment, + alternate: JitFragment = jit`` +): JitFragment { + return test ? consequent : alternate; +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..e69de29