From 8ad25f146022b089719cbe8be49fec1653596471 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Mon, 16 May 2022 04:16:47 +0800 Subject: [PATCH 01/18] Blank slate --- .gitignore | 2 + bindings/web/rune/LICENSE_APACHE.md | 42 +- bindings/web/rune/README.md | 14 +- bindings/web/rune/package.json | 18 +- bindings/web/rune/src/Runefile.ts | 168 --- bindings/web/rune/src/Runtime.test.ts | 97 -- bindings/web/rune/src/Runtime.ts | 373 ----- bindings/web/rune/src/Shape.test.ts | 20 - bindings/web/rune/src/Shape.ts | 90 -- bindings/web/rune/src/Tensor.test.ts | 43 - bindings/web/rune/src/Tensor.ts | 151 -- .../web/rune/src/builtin/RandomCapability.ts | 11 - .../web/rune/src/builtin/WebcamCapability.ts | 24 - bindings/web/rune/src/builtin/index.ts | 12 - bindings/web/rune/src/facade.ts | 313 ----- bindings/web/rune/src/index.ts | 45 - bindings/web/rune/tsconfig.json | 9 +- bindings/web/rune/yarn.lock | 1237 ++++++++++++++++- 18 files changed, 1278 insertions(+), 1391 deletions(-) delete mode 100644 bindings/web/rune/src/Runefile.ts delete mode 100644 bindings/web/rune/src/Runtime.test.ts delete mode 100644 bindings/web/rune/src/Runtime.ts delete mode 100644 bindings/web/rune/src/Shape.test.ts delete mode 100644 bindings/web/rune/src/Shape.ts delete mode 100644 bindings/web/rune/src/Tensor.test.ts delete mode 100644 bindings/web/rune/src/Tensor.ts delete mode 100644 bindings/web/rune/src/builtin/RandomCapability.ts delete mode 100644 bindings/web/rune/src/builtin/WebcamCapability.ts delete mode 100644 bindings/web/rune/src/builtin/index.ts delete mode 100644 bindings/web/rune/src/facade.ts diff --git a/.gitignore b/.gitignore index 3a2972c887..40223a01bd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ examples/.DS_Store tarpaulin-report.html .ipynb_checkpoints/ build/ + +.parcel-cache/ diff --git a/bindings/web/rune/LICENSE_APACHE.md b/bindings/web/rune/LICENSE_APACHE.md index 1b5ec8b78e..038d25d682 100644 --- a/bindings/web/rune/LICENSE_APACHE.md +++ b/bindings/web/rune/LICENSE_APACHE.md @@ -92,33 +92,33 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION meet the following conditions: (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions diff --git a/bindings/web/rune/README.md b/bindings/web/rune/README.md index 278f85e91a..90f0e285b7 100644 --- a/bindings/web/rune/README.md +++ b/bindings/web/rune/README.md @@ -4,8 +4,8 @@ A package that lets you run Runes in the browser. ## Getting Started -The easiest way to get started is by following [*Lesson 4: Integrating With The -Browser*][lesson-4] from our tutorial series. +The easiest way to get started is by following [_Lesson 4: Integrating With The +Browser_][lesson-4] from our tutorial series. This will walk you through creating a React application which initializes the Rune runtime and executes it every time a button is pressed. @@ -25,16 +25,16 @@ import path (i.e. you import from `@hotg-ai/rune/builtins` instead of `@hotg-ai/rune/dist/builtins`). As a precaution, the `package.json` in this folder sets `"private": true` to -make sure you don't accidentally run `yarn publish` +make sure you don't accidentally run `yarn publish` ## License This project is licensed under either of - * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE.md) or - http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT.md) or - http://opensource.org/licenses/MIT) +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE.md) or + http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT.md) or + http://opensource.org/licenses/MIT) at your option. diff --git a/bindings/web/rune/package.json b/bindings/web/rune/package.json index 23a74a5d11..b003ba3d1f 100644 --- a/bindings/web/rune/package.json +++ b/bindings/web/rune/package.json @@ -6,22 +6,28 @@ "homepage": "https://hotg.dev/", "author": "The Rune Developers ", "license": "MIT OR Apache-2.0", - "main": "index.js", - "types": "index.d.ts", + "source": "src/index.ts", + "module": "dist/index.js", + "types": "dist/index.d.ts", "private": true, "scripts": { - "build": "tsc", - "watch": "tsc --watch", + "build": "parcel build", + "watch": "parcel watch", "test": "jest", + "ci": "tsc --noEmit && yarn build && yarn test", "fmt": "prettier --write .", - "release": "tsc && cd dist && cp ../*.md . && sed 's/\"private\": true,/\"private\": false,/g' ../package.json > package.json && yarn publish", "generate-runefile-types": "json2ts ../../../crates/compiler/runefile-schema.json --output src/Runefile.ts" }, - "dependencies": {}, + "dependencies": { + "@hotg-ai/rune-wit-files": "^0.3.1" + }, "devDependencies": { + "@parcel/packager-ts": "2.5.0", + "@parcel/transformer-typescript-types": "2.5.0", "@types/jest": "^27.0.0", "jest": "^27.0.6", "json-schema-to-typescript": "^10.1.5", + "parcel": "^2.5.0", "prettier": "^2.5.1", "ts-jest": "^27.0.4", "ts-node": "^10.4.0", diff --git a/bindings/web/rune/src/Runefile.ts b/bindings/web/rune/src/Runefile.ts deleted file mode 100644 index f8653bd869..0000000000 --- a/bindings/web/rune/src/Runefile.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* tslint:disable */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ - -/** - * The top level Runefile type. - */ -export type Document = DocumentV1; -/** - * - * A specification for finding a dependency. - * - * The full syntax is `base@version#sub_path` where - * - * - `base` is a URL or the name of a repository on GitHub (e.g. `hotg-ai/rune` - * or `https://github.com/hotg-ai/rune`) - * - `version` is an optional field specifying the version (e.g. as a git tag) - * - `sub_path` is an optional field which is useful when pointing to - * repositories with multiple relevant items because it lets you specify - * which directory the specified item is in. - * - */ -export type Path = string; -/** - * A stage in the Rune's pipeline. - */ -export type Stage = ModelStage | ProcBlockStage | CapabilityStage | OutStage; -/** - * Something that could be either a reference to a resource (`$resource`) or a plain string (`./path`). - */ -export type Argument = ResourceName | number; -/** - * - * A reference to some [`ResourceDeclaration`]. It typically looks like - * `$RESOURCE_NAME`. - * - */ -export type ResourceName = string; -/** - * - * The name of a tensor. - * - * Typically something like "stage", or "stage.2" if the stage has multiple outputs. - * - */ -export type Input = string; -/** - * How the resource should be treated inside the Rune. - */ -export type ResourceType = "string" | "binary"; - -/** - * Version 1 of the `Runefile.yml` format. - */ -export interface DocumentV1 { - /** - * The base image that defines the interface between a Rune and its runtime. - * - * This should always be `"runicos/base"`. - */ - image: Path; - /** - * The various stages in the Runefile's pipeline. - */ - pipeline: { - [k: string]: Stage; - }; - /** - * Any resources that can be accessed by pipeline stages. - */ - resources?: { - [k: string]: ResourceDeclaration; - }; - /** - * The version number. Must always be `"1"`. - */ - version: number; - [k: string]: unknown; -} -/** - * A ML model which will be executed by the runtime. - */ -export interface ModelStage { - args?: { - [k: string]: Argument; - }; - /** - * Tensors to use as input to this model. - */ - inputs?: Input[]; - /** - * The model to use, or a resource which specifies the model to use. - */ - model: ResourceName; - /** - * The tensors that this model outputs. - */ - outputs?: Type[]; - [k: string]: unknown; -} -/** - * The element type and dimensions for a particular tensor. - */ -export interface Type { - dimensions?: number[]; - type: string; - [k: string]: unknown; -} -/** - * A stage which executes a procedural block. - */ -export interface ProcBlockStage { - args?: { - [k: string]: Argument; - }; - inputs?: Input[]; - outputs?: Type[]; - /** - * A [`Path`] that Rune can use to locate the proc block. - */ - "proc-block": string; - [k: string]: unknown; -} -/** - * A stage which reads inputs from the runtime. - */ -export interface CapabilityStage { - args?: { - [k: string]: Argument; - }; - /** - * What type of capability to use ("IMAGE", "SOUND", etc.). - */ - capability: string; - outputs?: Type[]; - [k: string]: unknown; -} -/** - * A stage which passes outputs back to the runtime. - */ -export interface OutStage { - args?: { - [k: string]: Argument; - }; - inputs?: Input[]; - /** - * The type of output (e.g. "SERIAL"). - */ - out: string; - [k: string]: unknown; -} -/** - * The declaration for a resource, typically something like a wordlist or environment variable. - */ -export interface ResourceDeclaration { - /** - * A resource who's default value is specified inline. - */ - inline?: string | null; - /** - * A resource who's default value is meant to be loaded from a file. - */ - path?: string | null; - type?: ResourceType & string; -} diff --git a/bindings/web/rune/src/Runtime.test.ts b/bindings/web/rune/src/Runtime.test.ts deleted file mode 100644 index ce4fab193d..0000000000 --- a/bindings/web/rune/src/Runtime.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import child_process from "child_process"; -import path from "path"; -import fs from "fs"; -import { Runtime, Capability, Output } from "./Runtime"; - -const decoder = new TextDecoder("utf8"); - -describe("Runtime", () => { - const noopRune = buildExample("noop"); - - it("can load the noop Rune", async () => { - const imports = { - createCapability: () => new RawCapability(), - createOutput: () => new SpyOutput([]), - createModel: () => { throw new Error(); }, - log: (msg: any) => { }, - }; - - const runtime = await Runtime.load(noopRune, imports); - - expect(runtime).not.toBeNull(); - }); - - it("can run the noop Rune", async () => { - const calls: Uint8Array[] = []; - const imports = { - createCapability: () => new RawCapability([ - 1, 0, 0, 0, - 2, 0, 0, 0, - 3, 0, 0, 0, - 4, 0, 0, 0, - ]), - createOutput: () => new SpyOutput(calls), - createModel: () => { throw new Error(); }, - log: (msg: any) => { }, - }; - const runtime = await Runtime.load(noopRune, imports); - - runtime.call(); - - expect(calls).toHaveLength(1); - const output = decoder.decode(calls[0]); - expect(JSON.parse(output)).toEqual({ - channel: 1, - dimensions: [4,], - elements: [1, 2, 3, 4,], - type_name: "i32", - }); - }); -}); - -class RawCapability implements Capability { - data: Uint8Array = new Uint8Array(); - - constructor(data?: number[]) { - if (data) { - this.data = Uint8Array.from(data); - } - } - - setParameter(name: string, value: number): void { - throw new Error("Method not implemented."); - } - generate(dest: Uint8Array): void { - dest.set(this.data); - } -} - -class SpyOutput implements Output { - received: Uint8Array[]; - constructor(received: Uint8Array[]) { - this.received = received; - } - - consume(data: Uint8Array): void { - this.received.push(data); - } -} - -function buildExample(name: string): ArrayBuffer { - const gitOutput = child_process.execSync("git rev-parse --show-toplevel"); - const repoRoot = decoder.decode(gitOutput).trim(); - - const exampleDir = path.join(repoRoot, "examples", name); - const runefile = path.join(exampleDir, "Runefile.yml"); - - child_process.execSync(`cargo run --bin rune --quiet -- build ${runefile} --quiet --unstable --rune-repo-dir ${repoRoot}`, { - cwd: repoRoot, - env: { - RUST_LOG: "warning", - ...process.env - }, - }); - const rune = path.join(exampleDir, name + ".rune"); - - return fs.readFileSync(rune); -} diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts deleted file mode 100644 index 53fc913dd6..0000000000 --- a/bindings/web/rune/src/Runtime.ts +++ /dev/null @@ -1,373 +0,0 @@ -import Shape from "./Shape"; - -/** - * Something which consumes outputs generated by the Rune. - */ -export interface Output { - consume(data: Uint8Array): void; -} - -/** - * Inputs provided by the application. - */ -export interface Capability { - generate(dest: Uint8Array): void; - setParameter(name: string, value: number): void; -} - -/** - * Functions required by the Rune runtime. - */ -export interface Imports { - createOutput(type: number): Output; - createCapability(type: number): Capability; - createModel(mimetype: string, model: ArrayBuffer): Promise; - log(message: string | StructuredLogMessage): void; -} - -/** - * Something which can run inference on a model. - */ -export interface Model { - transform( - inputArray: Uint8Array[], - inputDimensions: Shape[], - outputArray: Uint8Array[], - outputDimensions: Shape[], - ): void; -} - -type TensorDescriptor = { - dimensions: string, -}; - -type ModelInfo = { - id: number, - modelSize: number, - inputs?: TensorDescriptor[], - outputs?: TensorDescriptor[], -}; - -/** - * Public interface exposed by the WebAssembly module. - */ -interface Exports extends WebAssembly.Exports { - memory: WebAssembly.Memory; - _manifest(): void; - _call(capability_type: number, input_type: number, capability_index: number): void; -} - -export class Runtime { - instance: WebAssembly.Instance; - - constructor(instance: WebAssembly.Instance) { - this.instance = instance; - } - - static async load(wasm: ArrayBuffer, imports: Imports) { - let memory: WebAssembly.Memory; - - const { hostFunctions, finaliseModels } = importsToHostFunctions( - imports, - () => memory, - ); - const { instance } = await WebAssembly.instantiate(wasm, hostFunctions); - - const exports = instance.exports; - if (!isRuneExports(exports)) { - throw new Error("Invalid Rune exports"); - } - memory = exports.memory; - exports._manifest(); - - // now we've asked for all the models to be loaded, let's wait until - // they are done before continuing - await finaliseModels(); - return new Runtime(instance); - } - - manifest() { - return this.exports._manifest(); - } - - call() { - this.exports._call(0, 0, 0); - } - - get exports() { - // Note: checked inside Runtime.load() and exports will never change. - const { exports } = this.instance; - - if (isRuneExports(exports)) { - return exports; - } else { - throw Error(); - } - } -} - -type Dict = Partial>; - -/** - * Generate a bunch of host functions backed by the supplied @param imports. - */ -function importsToHostFunctions( - imports: Imports, - getMemory: () => WebAssembly.Memory, -) { - const memory = () => { - const m = getMemory(); - if (!m) - throw new Error("WebAssembly memory wasn't initialized"); - - return new Uint8Array(m.buffer); - }; - - const ids = counter(); - const outputs: Dict = {}; - const capabilities: Dict = {}; - const pendingModels: Promise<[number, Model]>[] = []; - const models: Record = {}; - const modelsDescription: Record = {}; - const utf8 = new TextDecoder(); - const decoder = new TextDecoder("utf8"); - - // Annoyingly, this needs to be an object literal instead of a class. - const env = { - _debug(msg: number, len: number) { - const raw = memory().subarray(msg, msg + len); - const decoded = utf8.decode(raw); - const parsed = tryParseJSON(decoded); - - function tryParseJSON(input: string): any | undefined { - try { - return JSON.parse(input); - } catch { - return; - } - } - - if (isStructuredLogMessage(parsed)) { - imports.log(parsed); - - if (parsed.level == "ERROR") { - // Translate all errors inside the Rune into exceptions, - // aborting execution. - throw new Error(parsed.message); - } - } else { - imports.log(decoded); - } - }, - - request_output(type: number) { - const output = imports.createOutput(type); - const id = ids(); - - outputs[id] = output; - return id; - }, - - consume_output(id: number, buffer: number, len: number) { - const output = outputs[id]; - if (output) { - const data = memory().subarray(buffer, buffer + len); - output.consume(data); - } - else { - throw new Error("Invalid output"); - } - }, - - request_capability(type: number) { - const capability = imports.createCapability(type); - const id = ids(); - - capabilities[id] = capability; - return id; - }, - - request_capability_set_param(id: number, - keyPtr: number, - keyLength: number, - valuePtr: number, - valueLength: number, - valueType: number) { - const keyBytes = memory().subarray(keyPtr, keyPtr + keyLength); - const key = decoder.decode(keyBytes); - const bytes = memory().subarray(valuePtr, valuePtr + valueLength).slice(0); - const value = decodeValue(valueType, bytes); - - const capability = capabilities[id]; - - if (!capability) { - throw new Error(`Tried to set "${key}" to ${value} but capability ${id} doesn't exist`); - } - - capability.setParameter(key, value); - }, - - request_provider_response(buffer: number, len: number, id: number) { - const cap = capabilities[id]; - if (!cap) { - throw new Error("Invalid capability"); - } - const dest = memory().subarray(buffer, buffer + len); - - cap.generate(dest); - }, - - rune_model_load(mimetype: number, mimetype_len: number, model: number, model_len: number, input_descriptors: number, input_len: number, output_descriptors: number, output_len: number) { - const mime = decoder.decode(memory().subarray(mimetype, mimetype + mimetype_len)); - const model_data = memory().subarray(model, model + model_len); - - //inputs - let o = memory().subarray(input_descriptors, input_descriptors + 8 * input_len); - let inputs = []; - for (let i = 0; i < input_len; i++) { - const inputs_pointer = new Uint32Array(new Uint8Array([o[i * 8], o[i * 8 + 1], o[i * 8 + 2], o[i * 8 + 3]]).buffer)[0]; - const inputs_length = new Uint32Array(new Uint8Array([o[i * 8 + 4], o[i * 8 + 5], o[i * 8 + 6], o[i * 8 + 7]]).buffer)[0]; - const inputs_string = decoder.decode(memory().subarray(inputs_pointer, inputs_pointer + inputs_length)); - inputs.push({ "dimensions": inputs_string }); - } - //outputs - o = memory().subarray(output_descriptors, output_descriptors + 8 * output_len); - let outputs = []; - for (let i = 0; i < output_len; i++) { - const outputs_pointer = new Uint32Array(new Uint8Array([o[i * 8], o[i * 8 + 1], o[i * 8 + 2], o[i * 8 + 3]]).buffer)[0]; - const outputs_length = new Uint32Array(new Uint8Array([o[i * 8 + 4], o[i * 8 + 5], o[i * 8 + 6], o[i * 8 + 7]]).buffer)[0]; - const outputs_string = decoder.decode(memory().subarray(outputs_pointer, outputs_pointer + outputs_length)); - outputs.push({ "dimensions": outputs_string }); - } - - const pending = imports.createModel(mime, model_data); - const id = ids(); - - pendingModels.push(pending.then(model => [id, model])); - modelsDescription[id] = { id, inputs, outputs, "modelSize": model_len }; - return id; - }, - - async rune_model_infer(id: number, inputs: number, outputs: number) { - const model = models[id]; - let modelsDes = modelsDescription[id]; - - let inputArray = []; - let inputDimensions = []; - - for (let i = 0; i < modelsDes!.inputs!.length; i++) { - let dimensions = Shape.parse(modelsDes!.inputs![i].dimensions); - - let o = memory().subarray(inputs + i * 4, inputs + 4 + i * 4); - const pointer = new Uint32Array(new Uint8Array([o[0], o[1], o[2], o[3]]).buffer)[0]; - inputArray.push(memory().subarray(pointer, pointer + dimensions.byteSize)); - inputDimensions.push(dimensions); - } - - let outputArray = []; - let outputDimensions = []; - for (let i = 0; i < modelsDes!.outputs!.length; i++) { - let dimensions = Shape.parse(modelsDes!.outputs![i].dimensions); - let o = memory().subarray(outputs + i * 4, outputs + 4 + i * 4); - const pointer = new Uint32Array(new Uint8Array([o[0], o[1], o[2], o[3]]).buffer)[0]; - outputArray.push(memory().subarray(pointer, pointer + dimensions.byteSize)); - outputDimensions.push(dimensions); - } - model.transform(inputArray, inputDimensions, outputArray, outputDimensions); - return id; - }, - - tfm_model_invoke(id: number, inputPtr: number, inputLen: number, outputPtr: number, outputLen: number) { - deprecated("tfm_model_invoke()", "0.5"); - }, - tfm_preload_model(data: number, len: number, numInputs: number, numOutputs: number) { - deprecated("tfm_preload_model()", "0.5"); - }, - }; - - async function synchroniseModelLoading() { - const loadedModels = await Promise.all(pendingModels); - pendingModels.length = 0; - loadedModels.forEach(([id, model]) => { - models[id] = model; - }); - } - return { - hostFunctions: { env }, - finaliseModels: synchroniseModelLoading, - }; -} - -function counter() { - let value = 0; - return () => { value++; return value - 1; }; -} - -function isRuneExports(obj: any): obj is Exports { - return (obj && - obj.memory instanceof WebAssembly.Memory && - obj._call instanceof Function && - obj._manifest instanceof Function); -} - -export function isStructuredLogMessage(obj?: any): obj is StructuredLogMessage { - return obj - && typeof obj.level == 'string' - && typeof obj.message == 'string' - && typeof obj.target == 'string' - && typeof obj.module_path == 'string' - && typeof obj.file == 'string' - && typeof obj.line == 'number'; -} - -export type StructuredLogMessage = { - level: string, - message: string, - target: string, - module_path: string, - file: string, - line: number, -}; - -interface TypedArray extends ArrayBuffer { - readonly buffer: ArrayBuffer; -} - -//this function can convert any TypedArray to any other kind of TypedArray : -function convertTypedArray(src: TypedArray, constructor: any): T { - // Instantiate a buffer (zeroed out) and copy the bytes from "src" into it. - const buffer = new constructor(src.byteLength); - buffer.set(src.buffer); - return buffer[0] as T; -} - - -function deprecated(feature: string, version: string) { - throw new Error(`This runtime no longer supports Runes using "${feature}". Please rebuild with Rune ${version}`); -} - -function decodeValue(valueType: number, raw: Uint8Array): number { - const { buffer, byteOffset, byteLength } = raw; - const bytes = buffer.slice(byteOffset, byteOffset + byteLength); - - switch (valueType) { - case 1: - const i32s = new Int32Array(bytes); - return i32s[0]; - case 2: - const f32s = new Float32Array(bytes); - return f32s[0]; - case 5: - return raw[0]; - case 6: - const i16s = new Int16Array(bytes); - return i16s[0]; - case 7: - const i8s = new Int8Array(bytes); - return i8s[0]; - - default: - throw new Error(`Unknown value type, ${valueType}, with binary representation, ${raw}`); - } -} - diff --git a/bindings/web/rune/src/Shape.test.ts b/bindings/web/rune/src/Shape.test.ts deleted file mode 100644 index fca966378f..0000000000 --- a/bindings/web/rune/src/Shape.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Shape from "./Shape"; - -describe("Shape", () => { - it("can parse u8[1, 2,3]", () => { - const text = "u8[1, 2,3]"; - - const got = Shape.parse(text); - - expect(got).toEqual(new Shape("u8", [1, 2, 3])); - }); - - const knownShapes = ["u8[1]", "f32[2, 4, 6, 8]"] - - test.each(knownShapes)(`can round-trip %p`, input => { - const parsed = Shape.parse(input); - const stringified = parsed.toString(); - - expect(stringified).toEqual(input); - }); -}); diff --git a/bindings/web/rune/src/Shape.ts b/bindings/web/rune/src/Shape.ts deleted file mode 100644 index e4e296e29b..0000000000 --- a/bindings/web/rune/src/Shape.ts +++ /dev/null @@ -1,90 +0,0 @@ - -/** - * A description of a tensor. - */ -export default class Shape { - static ByteSize = { - "f64": 8, - "i64": 8, - "u64": 8, - "f32": 4, - "i32": 4, - "u32": 4, - "u16": 2, - "i16": 2, - "u8": 1, - "i8": 1 - } as const; - - /** - * The element type. - */ - readonly type: string; - /** - * The tensor's dimensions. - */ - readonly dimensions: readonly number[]; - - constructor(type: string, values: number[]) { - this.type = type; - this.dimensions = [...values]; - } - - /** - * Parse a string like "u8[1, 2, 3]" into a Shape. - */ - static parse(text: string): Shape { - const pattern = /^([\w\d]+)\[(\d+(?:,\s*\d+)*)\]$/; - const match = pattern.exec(text.replace(" ", "")); - - if (!match) { - throw new Error(); - } - - const [_, typeName, dims] = match; - - checkElementType(typeName, text); - - return new Shape(typeName, dims.split(",").map(d => parseInt(d.trim()))); - } - - /** - * The number of dimensions this tensor has. - */ - get rank(): number { - return this.dimensions.length; - } - - /** - * The number of elements in this tensor. - */ - get tensorSize(): number { - return this.dimensions.reduce((product, dim) => product * dim, 1); - } - - /** - * The number of bytes used to store this tensor's elements. - */ - get byteSize(): number { - const sizes: Record = Shape.ByteSize; - const elementSize = sizes[this.type] || 1; - return this.tensorSize * elementSize; - } - - toString(): string { - const { type, dimensions } = this; - const dims = dimensions.join(", "); - return `${type}[${dims}]`; - } -} - - -function checkElementType(typeName: string, input: string) { - const knownElements = Object.keys(Shape.ByteSize); - - if (typeName in Shape.ByteSize) { - return; - } - - console.warn(`The "${typeName}" in "${input}" isn't one of the known element types (${knownElements})`); -} diff --git a/bindings/web/rune/src/Tensor.test.ts b/bindings/web/rune/src/Tensor.test.ts deleted file mode 100644 index 892c858d63..0000000000 --- a/bindings/web/rune/src/Tensor.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Shape, Tensor } from "."; - -describe("Tensor", () => { - it("can be round tripped as a Uint8Array", () => { - const shape = new Shape("u8", [2, 3]); - const tensor = new Tensor(shape, new Uint8Array([1, 2, 3, 4, 5, 6])); - - const typed = tensor.asTypedArray("u8"); - - expect(Array.from(typed)).toEqual([1, 2, 3, 4, 5, 6]); - }); - - it("can be viewed as a Float32Array", () => { - const raw = new Uint8Array([0, 0, 64, 64]); - const shape = new Shape("f32", [1]); - - const tensor = new Tensor(shape, raw); - const typed = tensor.asTypedArray("f32"); - - expect(Array.from(typed)).toEqual([3.0]); - }); - - it("can be a slice from a larger buffer", () => { - const numbers = [1, 2, 3, 4, 5, 6, 7, 8]; - const buffer = new Float32Array(numbers); - const section = buffer.subarray(3, 6); - const shape = new Shape("f32", [3]); - - const tensor = new Tensor(shape, new Uint8Array(section.buffer, section.byteOffset, section.byteLength)); - const typed = tensor.asTypedArray("f32"); - - expect(Array.from(typed)).toEqual(numbers.slice(3, 6)); - }); - - it("can be constructed from a typed array", () => { - const values = [1, 2, 3, 4, -5, -6]; - const raw = new Int16Array(values); - - const tensor = Tensor.fromTypedArray("i16", [6], raw); - - expect(Array.from(tensor.asTypedArray("i16"))).toEqual(values); - }); -}); diff --git a/bindings/web/rune/src/Tensor.ts b/bindings/web/rune/src/Tensor.ts deleted file mode 100644 index 86cb53fa55..0000000000 --- a/bindings/web/rune/src/Tensor.ts +++ /dev/null @@ -1,151 +0,0 @@ -import Shape from "./Shape"; - -// Some versions of Safari doesn't support BigUint64Array and friends, and -// it's not possible to polyfill these types because bigint is a builtin type. -// -// This workaround lets us use them when possible and throws an exception at -// runtime when they aren't. -const BigUint64ArrayShim = global.BigUint64Array ?? class { constructor() { throw new Error("BigUint64Array is not supported on this device"); } }; -const BigInt64ArrayShim = global.BigInt64Array ?? class { constructor() { throw new Error("BigInt64Array is not supported on this device"); } }; - -const typedArrayConstructors = { - "f64": Float64Array, - "i64": BigInt64ArrayShim, - "u64": BigUint64ArrayShim, - "f32": Float32Array, - "i32": Int32Array, - "u32": Uint32Array, - "u16": Uint16Array, - "i16": Int16Array, - "u8": Uint8ClampedArray, - "i8": Int8Array, -} as const; - -type TypedArrayConstructors = typeof typedArrayConstructors; - -export type TypedArrays = { - [Key in keyof TypedArrayConstructors]: InstanceType; -} - -/** - * An opaque tensor. - */ -export default class Tensor { - /** - * The raw bytes containing the tensor data. - */ - public readonly elements: Uint8Array; - /** - * The tensor's shape (element type and dimensions). - */ - public readonly shape: Shape; - - constructor(shape: Shape, elements: Uint8Array) { - this.shape = shape; - this.elements = elements; - } - - /** - * Construct a new Tensor from a typed array containing its flattened - * elements in row-major order. - * - * @param elementType The type of the element - * @param dimensions The tensor's dimensions - * @param elements The elements - * @returns - */ - public static fromTypedArray( - elementType: S, - dimensions: readonly number[], - elements: TypedArrays[S], - ): Tensor { - const { buffer, byteLength, byteOffset } = elements; - const shape = new Shape(elementType, [...dimensions]); - return new Tensor(shape, new Uint8Array(buffer, byteOffset, byteLength)); - } - - /** - * View this tensor's data as an array of 64-bit floats. - * - * This will fail if this isn't a f64 tensor. - */ - public asTypedArray(elementType: "f64"): Float64Array; - /** - * View this tensor's data as an array of 64-bit signed integers. - * - * This will fail if this isn't a i64 tensor. It may also fail on - * versions of Safari because they don't support BigInt64Array. - */ - public asTypedArray(elementType: "i64"): BigInt64Array; - /** - * View this tensor's data as an array of 64-bit unsigned integers. - * - * This will fail if this isn't a u64 tensor. It may also fail on - * versions of Safari because they don't support BigUint64Array. - */ - public asTypedArray(elementType: "u64"): BigUint64Array; - /** - * View this tensor's data as an array of 32-bit floats. - * - * This will fail if this isn't a f32 tensor. - */ - public asTypedArray(elementType: "f32"): Float32Array; - /** - * View this tensor's data as an array of 32-bit signed integers. - * - * This will fail if this isn't a i32 tensor. - */ - public asTypedArray(elementType: "i32"): Int32Array; - /** - * View this tensor's data as an array of 32-bit unsigned integers. - * - * This will fail if this isn't a u32 tensor. - */ - public asTypedArray(elementType: "u32"): Uint32Array; - /** - * View this tensor's data as an array of 16-bit signed integers. - * - * This will fail if this isn't a i16 tensor. - */ - public asTypedArray(elementType: "i16"): Int16Array; - /** - * View this tensor's data as an array of 16-bit unsigned integers. - * - * This will fail if this isn't a u16 tensor. - */ - public asTypedArray(elementType: "u16"): Uint16Array; - /** - * View this tensor's data as an array of 8-bit signed integers. - * - * This will fail if this isn't a i8 tensor. - */ - public asTypedArray(elementType: "i8"): Int8Array; - /** - * View this tensor's data as an array of 8-bit unsigned integers. - * - * This will fail if this isn't a u8 tensor. - */ - public asTypedArray(elementType: "u8"): Uint8ClampedArray; - - public asTypedArray(elementType: keyof typeof typedArrayConstructors): ArrayBuffer { - if (this.shape.type != elementType) { - throw new Error(`Attempting to interpret a ${this.shape.toString()} as a ${elementType} tensor`); - } - - const { buffer, byteOffset, byteLength } = this.elements; - const length = byteLength / Shape.ByteSize[this.shape.type]; - const constructor = typedArrayConstructors[elementType]; - - return new constructor(buffer, byteOffset, length); - } - - public get elementType(): string { - return this.shape.type; - } - - public get dimensions(): readonly number[] { - return this.shape.dimensions; - } -} - -const x = Tensor.fromTypedArray diff --git a/bindings/web/rune/src/builtin/RandomCapability.ts b/bindings/web/rune/src/builtin/RandomCapability.ts deleted file mode 100644 index 1fbef661b3..0000000000 --- a/bindings/web/rune/src/builtin/RandomCapability.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Capability } from "../Runtime"; - -export class RandomCapability implements Capability { - setParameter(name: string, value: number): void { - // Note: we don't have any configurable settings - } - - generate(dest: Uint8Array): void { - window.crypto.getRandomValues(dest); - } -} diff --git a/bindings/web/rune/src/builtin/WebcamCapability.ts b/bindings/web/rune/src/builtin/WebcamCapability.ts deleted file mode 100644 index e592cb5f54..0000000000 --- a/bindings/web/rune/src/builtin/WebcamCapability.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Capability } from "../Runtime"; - -type Properties = { - width: number, - height: number, -}; - -export class WebcamCapability implements Capability { - lastImage: any; - properties: Properties = { - width: 320, - height: 320, - }; - - generate(dest: Uint8Array): void { - // TODO: Figure out how to read from the webcam. - throw new Error("Method not implemented."); - } - - setParameter(name: string, value: number): void { - const properties: Record = this.properties; - properties[name] = value; - } -} diff --git a/bindings/web/rune/src/builtin/index.ts b/bindings/web/rune/src/builtin/index.ts deleted file mode 100644 index 81408585f9..0000000000 --- a/bindings/web/rune/src/builtin/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { RandomCapability } from "./RandomCapability"; -export { WebcamCapability } from "./WebcamCapability"; - -/** - * Mimetypes for the model formats known by rune. - */ -export const mimetypes = { - tflite: "application/tflite-model", - tensorflow: "application/tf-model", - tfjs: "application/tfjs-model", - onnx: "application/onnx-model", -} as const; diff --git a/bindings/web/rune/src/facade.ts b/bindings/web/rune/src/facade.ts deleted file mode 100644 index c4c3b070ee..0000000000 --- a/bindings/web/rune/src/facade.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { Capabilities, CapabilityType, Outputs, Shape } from "."; -import { Capability, Imports, Model, Output, Runtime, StructuredLogMessage } from "./Runtime"; -import Tensor from "./Tensor"; - -type ModelConstructor = (model: ArrayBuffer) => Promise; -type Logger = (message: string | StructuredLogMessage) => void; - -export type InputDescription = { - type: CapabilityType, - args: Partial>, -}; - -/** - * A function that returns the desired input, either as a tensor or the raw - * byte buffer. - */ -export type ReadInput = (input: InputDescription) => Tensor; - -/** - * A function which can be used to evaluate a Rune. - */ -export type Evaluate = (r: ReadInput) => Result; - -/** - * A builder object which can be used to initialize the Rune runtime. - */ -export class Builder { - private modelHandlers: Partial> = {}; - private log: Logger = () => { }; - - /** - * Set a handler that will be called every time the Rune logs a message. - */ - public onDebug(handler: Logger): this { - this.log = handler; - return this; - } - - /** - * Add support for a new type of model. - * @param mimetype The "mimetype" that specifies which type of model being - * handled. - * @param constructor A constructor which will load the model. - * @returns - */ - public withModelHandler(mimetype: string, constructor: ModelConstructor): this { - this.modelHandlers[mimetype] = constructor; - - return this; - } - - public async build(rune: ArrayBuffer | string): Promise { - if (typeof rune == "string") { - const response = await fetch(rune); - rune = await response.arrayBuffer(); - } - const { modelHandlers, log } = this; - - const imports = new ImportsObject(modelHandlers, log); - let runtime: Runtime | undefined = await Runtime.load(rune, imports); - - return readInputs => { - if (!runtime) { - throw new Error("A previous call to this Rune has failed, leaving it in an invalid state"); - } - - imports.setInputs(readInputs); - - try { - runtime.call(); - } catch (e) { - // We encountered an error while invoking the Rune, typically by - // throwing an exception from one of our host functions. JS - // exceptions abort execution without unwinding the - // WebAssembly/Rust stack so we need to assume the runtime is - // FUBAR. - runtime = undefined; - throw e; - } - - let outputs = [...imports.outputs]; - imports.outputs.length = 0; - - return { outputs }; - }; - } -} - -export type Result = { - outputs: OutputValue[], -}; - -/** - * A tensor value generated by the SERIAL output. - */ -export type OutputValue = { - /** - * An integer specifying which SERIAL output this is attached to. - */ - channel: number, - /** - * The tensor's dimensions. - */ - dimensions: number[], - /** - * The elements in this tensor, flattened into a single array in row-major - * order. - */ - elements: string[] | number[], - /** - * The Rust name for this tensor's element type. - */ - type_name: string, -} - -class ImportsObject implements Imports { - private decoder = new TextDecoder("utf8"); - outputs: Array = []; - private modelHandlers: Partial>; - private logger: Logger; - private capabilities: LazyCapability[] = []; - - constructor( - modelHandlers: Partial>, - logger: Logger, - ) { - this.modelHandlers = modelHandlers; - this.logger = logger; - } - - setInputs(readInput: ReadInput) { - const inputs = this.capabilities.map(c => c.description()).map(readInput); - - for (let i = 0; i < this.capabilities.length; i++) { - this.capabilities[i].value = inputs[i]; - } - } - - createOutput(type: number): Output { - const { decoder, outputs } = this; - switch (type) { - case Outputs.tensor: - return tensorOutput(decoder, outputs); - case Outputs.serial: - return serialOutput(decoder, outputs); - - default: - throw new Error(`Unsupported output type: ${type}`); - } - } - - createCapability(type: number): Capability { - const pair = Object.entries(Capabilities).find(pair => pair[1] == type); - if (!pair) { - throw new Error(`Unable to handle capability number ${type}`); - } - - const capabilityType = pair[0]; - const cap = new LazyCapability(capabilityType as CapabilityType); - this.capabilities.push(cap); - - return cap; - } - - createModel(mimetype: string, model: ArrayBuffer): Promise { - const handler = this.modelHandlers[mimetype]; - - if (!handler) { - throw new Error(`No handler registered for "${mimetype}" models`); - } - - return handler(model); - } - - log(message: string | StructuredLogMessage): void { - this.logger(message); - } -} - -function tensorOutput(decoder: TextDecoder, outputs: Array): Output { - return { - consume: ({ buffer, byteLength, byteOffset }: Uint8Array) => { - const shapeLength = new Uint32Array(buffer, byteOffset, 1)[0]; - const shapeBytes = new Uint8Array(buffer, byteOffset + 4, shapeLength); - const shape = Shape.parse(decoder.decode(shapeBytes)); - const { type, dimensions } = shape; - const elements = new Uint8Array(buffer, byteOffset + 4 + shapeLength, byteLength - 4 - shapeLength); - const tensor = new Tensor(shape, elements); - outputs.push({ - channel: -1, - dimensions: [...dimensions], - type_name: type, - elements: tensorAsNumberArray(tensor), - }) - } - } -} - -function tensorAsNumberArray(tensor: Tensor): number[] { - const { elementType } = tensor; - - switch (elementType) { - case "f32": - const floats = tensor.asTypedArray(elementType); - return Array.from(floats); - case "u8": - const u8s = tensor.asTypedArray(elementType); - return Array.from(u8s); - case "u16": - const u16s = tensor.asTypedArray(elementType); - return Array.from(u16s); - case "u32": - const u32s = tensor.asTypedArray(elementType); - return Array.from(u32s); - case "i8": - const i8s = tensor.asTypedArray(elementType); - return Array.from(i8s); - case "i16": - const i16s = tensor.asTypedArray(elementType); - return Array.from(i16s); - case "i32": - const i32s = tensor.asTypedArray(elementType); - return Array.from(i32s); - - default: - throw new Error( - `Unable to convert a ${tensor.shape.toString()} to a list of numbers` - ); - } -} - -function serialOutput(decoder: TextDecoder, outputs: Array): Output { - // We want the end user to receive all outputs as a return value, but - // Runes are designed using a callback-based API (it's better for - // performance). This will create an output which will stash all - // generated values away in a list so they can be returned at the end. - - return { - consume(data: Uint8Array) { - const json = decoder.decode(data); - const deserialized = JSON.parse(json); - - if (isOutputValue(deserialized)) { - outputs.push(deserialized); - } else if (Array.isArray(deserialized) && deserialized.every(isOutputValue)) { - outputs.push(...deserialized); - } else { - throw new SerialDeserializeError(json, deserialized); - } - } - } -} - -function isNumberArray(value: any): value is number[] { - return Array.isArray(value) && value.every(v => typeof v === "number"); -} - -function isStringArray(value: any): value is string[] { - return Array.isArray(value) && value.every(v => typeof v === "string"); -} - -function isOutputValue(value?: any): value is OutputValue { - if (!value) { - return false; - } - - const { channel, dimensions, elements, type_name } = value; - - return typeof channel === "number" && - isNumberArray(dimensions) && - (isNumberArray(elements) || isStringArray(elements)) && - typeof type_name === "string"; -} - -class LazyCapability implements Capability { - type: CapabilityType; - value?: Tensor; - args: Record = {}; - - constructor(type: CapabilityType) { - this.type = type; - } - - description(): InputDescription { - return { - type: this.type, - args: this.args, - }; - } - - generate(dest: Uint8Array): void { - if (!this.value) { - throw new Error(); - } - - dest.set(this.value.elements); - } - - setParameter(name: string, value: number): void { - this.args[name] = value; - } -} - -class SerialDeserializeError extends Error { - readonly json: string; - readonly deserialized?: any; - - constructor(json: string, deserialized: any | undefined) { - super("Unable to deserialize the SERIAL output"); - this.json = json; - this.deserialized = deserialized; - } -} diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index 023fa3eb7e..e69de29bb2 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -1,45 +0,0 @@ -export { InputDescription, OutputValue, ReadInput, Result, Builder, Evaluate } from "./facade"; -export { default as Shape } from "./Shape"; -export { default as Tensor } from "./Tensor"; - -import { Builder } from "./facade"; - -/** - * A map of capability names to their identifies. - */ -export const Capabilities = { - "rand": 1, - "sound": 2, - "accel": 3, - "image": 4, - "raw": 5, - "float-image": 6, -} as const; - -/** - * A map of output names to their identifies. - */ -export const Outputs = { - "serial": 1, - "tensor": 5, -} as const; - -/** - * The name of all known capabilities. - */ -export type CapabilityType = keyof typeof Capabilities; - -/** - * The name of all known outputs. - */ -export type OutputType = keyof typeof Outputs; - -/** - * Use a high level builder API to initialize the Rune runtime. - * - * Check out the "Runtime" module if you need tighter control over the runtime - * or want to avoid unnecessary indirection/copies. - */ -export function builder(): Builder { - return new Builder(); -} diff --git a/bindings/web/rune/tsconfig.json b/bindings/web/rune/tsconfig.json index e6cd439071..d9558d1603 100644 --- a/bindings/web/rune/tsconfig.json +++ b/bindings/web/rune/tsconfig.json @@ -2,11 +2,8 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "noEmit": true }, - "exclude": [ - "jest.config.ts", - "dist/*", - "node_modules/*" - ] + "exclude": ["jest.config.ts"] } diff --git a/bindings/web/rune/yarn.lock b/bindings/web/rune/yarn.lock index 4d4e00f714..97059451f9 100644 --- a/bindings/web/rune/yarn.lock +++ b/bindings/web/rune/yarn.lock @@ -12,7 +12,7 @@ call-me-maybe "^1.0.1" js-yaml "^4.1.0" -"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== @@ -308,6 +308,13 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@hotg-ai/rune-wit-files@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@hotg-ai/rune-wit-files/-/rune-wit-files-0.3.1.tgz#c7e925ebd7450df2db2043068bd7de8cc8d1a3ae" + integrity sha512-IzFYxeEzlFuTG8XwBGlI1qkAHMbsUX+VbpPybnUOubXt+suNhYmMEqXJZJfMbGjyOPpJRAusBHPXcoLmoFHS0A== + dependencies: + parcel "^2.4.1" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -498,6 +505,664 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@lezer/common@^0.15.0", "@lezer/common@^0.15.7": + version "0.15.12" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.12.tgz#2f21aec551dd5fd7d24eb069f90f54d5bc6ee5e9" + integrity sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig== + +"@lezer/lr@^0.15.4": + version "0.15.8" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-0.15.8.tgz#1564a911e62b0a0f75ca63794a6aa8c5dc63db21" + integrity sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg== + dependencies: + "@lezer/common" "^0.15.0" + +"@mischnic/json-sourcemap@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz#38af657be4108140a548638267d02a2ea3336507" + integrity sha512-dQb3QnfNqmQNYA4nFSN/uLaByIic58gOXq4Y4XqLOWmOrw73KmJPt/HLyG0wvn1bnR6mBKs/Uwvkh+Hns1T0XA== + dependencies: + "@lezer/common" "^0.15.7" + "@lezer/lr" "^0.15.4" + json5 "^2.2.1" + +"@parcel/bundler-default@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.5.0.tgz#1f0b6d4893bb1a24f49fc7254a423134fb03741e" + integrity sha512-7CJzE17SirCXjcRgBcnqWO/5EOA1raq/3OIKtT4cxbjpDQGHZpjpEEZiMNRpEpdNMxDSlsG8mAkXTYGL2VVWRw== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/cache@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.5.0.tgz#957620b1b26bfd4f9bd7256ea25ef86e7d6f2816" + integrity sha512-3kOO3cZQv0FAKhrMHGLdb4Qtzpmy78Q6jPN3u8eCY4yqeDTnyQBZvWNHoyCm5WlmL8y6Q6REYMbETLxSH1ggAQ== + dependencies: + "@parcel/fs" "2.5.0" + "@parcel/logger" "2.5.0" + "@parcel/utils" "2.5.0" + lmdb "2.2.4" + +"@parcel/codeframe@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.5.0.tgz#de73dcd69a36e9d0fed1f4361cabfd83df13244a" + integrity sha512-qafqL8Vu2kr932cCWESoDEEoAeKVi7/xdzTBuhzEJng1AfmRT0rCbt/P4ao3RjiDyozPSjXsHOqM6GDZcto4eQ== + dependencies: + chalk "^4.1.0" + +"@parcel/compressor-raw@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.5.0.tgz#8675d7474b84920e1e4682a5bbd9b417ebfc0bc5" + integrity sha512-I5Zs+2f1ue4sTPdfT8BNsLfTZl48sMWLk2Io3elUJjH/SS9kO7ut5ChkuJtt77ZS35m0OF+ZCt3ICTJdnDG8eA== + dependencies: + "@parcel/plugin" "2.5.0" + +"@parcel/config-default@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.5.0.tgz#31caa12f6d37f3ae1df68e639dc276039f927603" + integrity sha512-r30V61958SONvP9I8KV8s44ZOFq0H219VyFjPysraSabHjZ+KMaCTQOuqaDtUMa272sHUQkBcZxKYj5jYPJlZg== + dependencies: + "@parcel/bundler-default" "2.5.0" + "@parcel/compressor-raw" "2.5.0" + "@parcel/namer-default" "2.5.0" + "@parcel/optimizer-css" "2.5.0" + "@parcel/optimizer-htmlnano" "2.5.0" + "@parcel/optimizer-image" "2.5.0" + "@parcel/optimizer-svgo" "2.5.0" + "@parcel/optimizer-terser" "2.5.0" + "@parcel/packager-css" "2.5.0" + "@parcel/packager-html" "2.5.0" + "@parcel/packager-js" "2.5.0" + "@parcel/packager-raw" "2.5.0" + "@parcel/packager-svg" "2.5.0" + "@parcel/reporter-dev-server" "2.5.0" + "@parcel/resolver-default" "2.5.0" + "@parcel/runtime-browser-hmr" "2.5.0" + "@parcel/runtime-js" "2.5.0" + "@parcel/runtime-react-refresh" "2.5.0" + "@parcel/runtime-service-worker" "2.5.0" + "@parcel/transformer-babel" "2.5.0" + "@parcel/transformer-css" "2.5.0" + "@parcel/transformer-html" "2.5.0" + "@parcel/transformer-image" "2.5.0" + "@parcel/transformer-js" "2.5.0" + "@parcel/transformer-json" "2.5.0" + "@parcel/transformer-postcss" "2.5.0" + "@parcel/transformer-posthtml" "2.5.0" + "@parcel/transformer-raw" "2.5.0" + "@parcel/transformer-react-refresh-wrap" "2.5.0" + "@parcel/transformer-svg" "2.5.0" + +"@parcel/core@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.5.0.tgz#13f60be9124a6a3e33aff32715acfc5ebade9dd2" + integrity sha512-dygDmPsfAYJKTnUftcbEzjCik7AAaPbFvJW8ETYz8diyjkAG9y6hvCAZIrJE5pNOjFzg32en4v4UWv8Sqlzl9g== + dependencies: + "@mischnic/json-sourcemap" "^0.1.0" + "@parcel/cache" "2.5.0" + "@parcel/diagnostic" "2.5.0" + "@parcel/events" "2.5.0" + "@parcel/fs" "2.5.0" + "@parcel/graph" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/logger" "2.5.0" + "@parcel/package-manager" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + "@parcel/workers" "2.5.0" + abortcontroller-polyfill "^1.1.9" + base-x "^3.0.8" + browserslist "^4.6.6" + clone "^2.1.1" + dotenv "^7.0.0" + dotenv-expand "^5.1.0" + json5 "^2.2.0" + msgpackr "^1.5.4" + nullthrows "^1.1.1" + semver "^5.7.1" + +"@parcel/css-darwin-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-darwin-arm64/-/css-darwin-arm64-1.8.3.tgz#cd74e2441c89743b22ab61e88e8151222dad591c" + integrity sha512-qh/Ig6GfVjGoiGSWjIYDo6Ghwmyy/9BXvYS1l3R+Bp50F300cq84Czfl6wxaL+aFmghdHzhjJuGfWmZlcYliPA== + +"@parcel/css-darwin-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-darwin-x64/-/css-darwin-x64-1.8.3.tgz#4f11b3e35fc3ef14b608fef324f6b1eff902d671" + integrity sha512-gTUIoRgwyYr4UuH7sSn3gOuMlIshJBOJLmjL+E/mR5lqdYabguiKiRORvkrnb/gHBmOUF9re0RcTaFmJ2VOAlg== + +"@parcel/css-linux-arm-gnueabihf@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm-gnueabihf/-/css-linux-arm-gnueabihf-1.8.3.tgz#f176838487b686da5734d9f52bb56c051dda610e" + integrity sha512-4P1r0BvL9dPz70py2xLg/jEvWJmKNyokPgafyrDP+GbpPTfH5NYJJkVRGo/TkKsp3Rv8SJhV9fdlpFKC6BI92A== + +"@parcel/css-linux-arm64-gnu@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm64-gnu/-/css-linux-arm64-gnu-1.8.3.tgz#8a9cbacf3bd730400ceb2a6736c5e1741986ba2b" + integrity sha512-1fUy94eaqdzum+C7bsYVF2AgxjLGR/qppArn/4HTQyydHR5QeV+Uoyqo5vdnO5Vclj8eQwlgR9OyAOlmzXxFDA== + +"@parcel/css-linux-arm64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm64-musl/-/css-linux-arm64-musl-1.8.3.tgz#35c5f1128469458d71b675b42cbd2f06ddfe5797" + integrity sha512-ct1QRK5gAP8sO22NZ7RULZQB7dbHpou+WMa4z0LJb+Fho13a1JNw931vNHbeI5cRr1fCTDq76pz/+Valgetzcw== + +"@parcel/css-linux-x64-gnu@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-x64-gnu/-/css-linux-x64-gnu-1.8.3.tgz#fa87fd53aa0481e028f6da0ecad94794f58207aa" + integrity sha512-pg/mahoogzjbaZcW76rrTZ64tEu8Wok4Gm0sW/dXHJEJD2QVJ6GxLP4UVNBuhaV0GrNFHggp9pcdhTtLGkKl/g== + +"@parcel/css-linux-x64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-linux-x64-musl/-/css-linux-x64-musl-1.8.3.tgz#b0df1a3f2edbcf77c056ad03fc78f3cf9449caf1" + integrity sha512-4Iwawy28HQ2yAgbuyR60bgO+8oE+OiWpE02eNjbgqnDpTsfmXFMt4l5OYgZwJJ7DlaZqm+/yO8RPMd+EzwtNzg== + +"@parcel/css-win32-x64-msvc@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css-win32-x64-msvc/-/css-win32-x64-msvc-1.8.3.tgz#8719e890a5c0969ca26968b15ffa8e2a20dbe10a" + integrity sha512-vnHUdzIVjqONa5ALFzMJ3ZHt6NiaYTHW/lqzP+AR4l+bq+UTXD2Q75/RgirY5NYwdfy1VPy/jI82jAtLOCymkw== + +"@parcel/css@^1.8.1": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@parcel/css/-/css-1.8.3.tgz#9f06319011a957568e98ede0cf657cb38ea24247" + integrity sha512-6qUN4iicr8f9Q6UUZttwwHMzrb65BRX46PHWq0icA4KEmvmfR9cSYlp/hJH8F4stg3Wncx12Bnw+EuPf5OAEPQ== + dependencies: + detect-libc "^1.0.3" + optionalDependencies: + "@parcel/css-darwin-arm64" "1.8.3" + "@parcel/css-darwin-x64" "1.8.3" + "@parcel/css-linux-arm-gnueabihf" "1.8.3" + "@parcel/css-linux-arm64-gnu" "1.8.3" + "@parcel/css-linux-arm64-musl" "1.8.3" + "@parcel/css-linux-x64-gnu" "1.8.3" + "@parcel/css-linux-x64-musl" "1.8.3" + "@parcel/css-win32-x64-msvc" "1.8.3" + +"@parcel/diagnostic@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.5.0.tgz#8c6891924e04b625d50176aae141d24dc8dddf87" + integrity sha512-KiMGGRpEV7wl5gjcxBKcgX84a+cG+IEn94gwy5LK3lENR09nuKShqqgKGAmj/17CobJgw1QNP94/H4Md+oxIWg== + dependencies: + "@mischnic/json-sourcemap" "^0.1.0" + nullthrows "^1.1.1" + +"@parcel/events@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.5.0.tgz#5e108a01a5aa3075038d2a2081fde0432d2559e7" + integrity sha512-Gc2LPwL1H34Ony5MENbKZg7wvCscZ4x9y7Fu92sfbdWpLo3K13hVtsX3TMIIgYt3B7R7OmO8yR880U2T+JfVkQ== + +"@parcel/fs-search@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/fs-search/-/fs-search-2.5.0.tgz#d96b7c46c2326398e52c9c14cdd07559d598436d" + integrity sha512-uBONkz9ZCNSOqbPGWJY3MNl+pqBTfvzHH9+4UhzHEHPArvK2oD0+syYPVE60+zGrxybXTESYMCJp4bHvH6Z2hA== + dependencies: + detect-libc "^1.0.3" + +"@parcel/fs@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.5.0.tgz#2bcb6ccf43826f2bfca9e1ca644be3bf5252c400" + integrity sha512-YYr14BWtx/bJ+hu6PPQQ6G/3omOTWgVqEw+UFI3iQH3P6+e0LRXW/Ja1yAcJeepGcTwIP0opnXZBQOm8PBQ2SA== + dependencies: + "@parcel/fs-search" "2.5.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + "@parcel/watcher" "^2.0.0" + "@parcel/workers" "2.5.0" + +"@parcel/graph@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-2.5.0.tgz#bd8898d555366a4b261766e22c8652ad869efaff" + integrity sha512-qa2VtG08dJyTaWrxYAkMIlkoDRSPoiqLDNxxHKplkcxAjXBUw0/AkWaz82VO5r1G6jfOj+nM30ajH9uygZYwbw== + dependencies: + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/hash@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/hash/-/hash-2.5.0.tgz#f2a05f7090f8f27ce8b53afd6272183763101ba7" + integrity sha512-47JL0XpB7UvIW6Ijf8vv+yVMt9dLvB/lRlBHFmAkmovisueVMVbYD7smxVZnCSehD8UH8BcymKbMzyL5dimgoQ== + dependencies: + detect-libc "^1.0.3" + xxhash-wasm "^0.4.2" + +"@parcel/logger@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.5.0.tgz#c618b780b80984d821c5bc53f27527fd540f4d0f" + integrity sha512-pT1L3ceH6trL1N3I3r2HawPjz/PCubOo/Kazu7IeXsMsKVjj1a6AeieZHzkNZIbhiGPtm/cHbBNLz2zTWDLeOA== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/events" "2.5.0" + +"@parcel/markdown-ansi@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.5.0.tgz#e0751d6c8fcd0aa4c8ee0a08d27e9d4d64705410" + integrity sha512-ixkNF3KWIqxMlfxTe9Gb2cp/uNmklQev8VEUxujMVxmUfGyQs4859zdJIQlIinabWYhArhsXATkVf3MzCUN6TQ== + dependencies: + chalk "^4.1.0" + +"@parcel/namer-default@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.5.0.tgz#1e1950a74aca825a753c9aa8e8c37dfb46ef7ef3" + integrity sha512-ahGQqHJzsWE5Qux8zXMAU+lyNBOl+ZpcOFzRGE2DWOsmAlytsHl7DBVCQvzUyNBFg1/HmIj+7D4efv2kjR7rTg== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/node-resolver-core@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.5.0.tgz#4aaf5c8eb57b56d1257ca02cae5b88be790be6bd" + integrity sha512-XQvpguiIwQcu75cscLDFOVhjsjuPzXbuMaaZ7XxxUEl0PscIgu/GfKYxTfTruN3cRl+CaQH6qBAMfjLaFng6lQ== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/optimizer-css@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.5.0.tgz#4f64bd0aa29727802b29eaea31aedfbb15ead5e9" + integrity sha512-J00bLF+4SsnKc+YbYrNuBr44/zz3cg++CoXteXhH27PxP1rScGQx36Rui8WORgil5mlX2VYN79DuqJC7V3Ynbg== + dependencies: + "@parcel/css" "^1.8.1" + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + browserslist "^4.6.6" + nullthrows "^1.1.1" + +"@parcel/optimizer-htmlnano@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.5.0.tgz#ffd8b3ef16300f957209cf20e7908a81d6f5b4af" + integrity sha512-Fr0zPqgxoNaOVdROAjNGDWCts3+wByNQ82Mxhu8Tzc25A2cPjcr1H2sa/TE3hf79c92DxdKf2FaC1ZOgR5YPdg== + dependencies: + "@parcel/plugin" "2.5.0" + htmlnano "^2.0.0" + nullthrows "^1.1.1" + posthtml "^0.16.5" + svgo "^2.4.0" + +"@parcel/optimizer-image@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.5.0.tgz#c80a463bf1dd82782a1f80504c8d660ec7917bc0" + integrity sha512-nbo2pdnAt21WLGjzTpsE8ZEL0xNoP7c3wBj9y70Pysmasg1SrRVCbfE8jTy+lHBQwq2yjC6lV/Usv+9lfA7S/w== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + "@parcel/workers" "2.5.0" + detect-libc "^1.0.3" + +"@parcel/optimizer-svgo@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.5.0.tgz#b1c809aa2fbf9229dc3cb62bab399b4576fd8b35" + integrity sha512-pgZqwU0RLc/wr4WcQY/W1GJmddnEANDEpz1mdppUOqBz1EfTQ7zh5NgUA3hV1i05Hbecp3mHSvXJPV0mhNOl5Q== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + svgo "^2.4.0" + +"@parcel/optimizer-terser@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.5.0.tgz#16b3320b34135edac69751ab2f3537a346133086" + integrity sha512-PZ3UHBGfjE49/Jloopsd38Hxg4qzsrdepWP53mCuVP7Aw605Y4QtYuB1ho3VV0oXfKQVq+uI7lVIBsuW4K6vqA== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + terser "^5.2.0" + +"@parcel/package-manager@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.5.0.tgz#9c82236e4e0fa158008b5bc5298def1085913b30" + integrity sha512-zTuF55/lITUjw9dUU/X0HiF++589xbPXw/zUiG9T6s8BQThLvrxAhYP89S719pw7cTqDimGkTxnIuK+a0djEkg== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/fs" "2.5.0" + "@parcel/logger" "2.5.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + "@parcel/workers" "2.5.0" + semver "^5.7.1" + +"@parcel/packager-css@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.5.0.tgz#54e36cfee9b32a8be05db7e3a2c37b28f26fa0d1" + integrity sha512-c0mGBFdVSPhAxaX3+zN8KEIqOOUhkIPKbZex1pnGYfy03Qe2/Mb4nyt5DAGlw9gjka1UCHIN/wszLmKC8YyUeg== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/packager-html@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.5.0.tgz#c390ca232753d6df73cdae7eff6f96ab6c973600" + integrity sha512-ZFGUPRMWKrm8kQHdkEJ5S22C05qpSymx+o+57EfuNjCrGyj3M59WyGYYXYJ175bFYZ/jp5yy+VxMh6fZefe+Pw== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + posthtml "^0.16.5" + +"@parcel/packager-js@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.5.0.tgz#3a696207587f57bf5e0c93b2e36db0758f896bea" + integrity sha512-aJAKOTgXdxO3V9O7+2DCVOtne128WwXmUAOVThnMRo7f3zMVSAR7Mxc9pEsuTzPfj8UBXgFBRfdJUSCgsMxiSw== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + globals "^13.2.0" + nullthrows "^1.1.1" + +"@parcel/packager-raw@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.5.0.tgz#ce0103c26667c93e5c04eda92691363e93aecb1a" + integrity sha512-aHV0oogeiqxhxS1lsttw15EvG3DDWK3FV7+F+7hoaAy+xg89K56NTp6j43Jtw9iyU1/HnZRGBE2hF3C7N73oKw== + dependencies: + "@parcel/plugin" "2.5.0" + +"@parcel/packager-svg@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.5.0.tgz#c6c62cc534ca4107bb724d81dc872ed64faa304d" + integrity sha512-XSMFn30K/kpjcPpQqt88GmPJsNUSVL3RNeigXkIAcLpfO6Tb2eV4iOt4yVCagaDrRJ19alXut0TxjMm5bm41/g== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + posthtml "^0.16.4" + +"@parcel/packager-ts@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/packager-ts/-/packager-ts-2.5.0.tgz#3b4af9ab67e19213239c88d211cfad162c62424f" + integrity sha512-BrH2Gum5EKlWJEJ92dFrH7QTSc7A7LxyElv6c2LPc5sI3z52JDdjQsUMEHqm5Fz25D79Ca/xzVvTWQaYA7XyRA== + dependencies: + "@parcel/plugin" "2.5.0" + +"@parcel/plugin@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.5.0.tgz#ae24d9a709581483e0d494a9e09100f0e40956cf" + integrity sha512-obtb6/Gql6YFQ86bdv75A2Noabx8679reFZeyfKKf0L7Lppx4DFQetXwM9XVy7Gx6hJ1Ekm3UMuuIyVJk33YHQ== + dependencies: + "@parcel/types" "2.5.0" + +"@parcel/reporter-cli@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.5.0.tgz#f64ab15f5faef9c017ea67bf3378343684f267f3" + integrity sha512-miJt2YbRJBmYSVeoUWUj8YL85Pwj1CmGQB0/btqhulGLH/Fvkbv6T4sJ4gl4l5xIt9mJQsZ70pOWwa8BId3rWw== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + chalk "^4.1.0" + term-size "^2.2.1" + +"@parcel/reporter-dev-server@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.5.0.tgz#043daa2116358d8f806a89d4a7385fe9555a089f" + integrity sha512-wvxAiW42AxJ3B8jtvowJcP4/cTV8zY48SfKg61YKYu1yUO+TtyJIjHQzDW2XuT34cIGFY97Gr0i+AVu44RyUuQ== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + +"@parcel/resolver-default@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.5.0.tgz#b107c59b4f8bbb013091916f349f5fc58e5dfab9" + integrity sha512-39PkZpVr/+iYS11u+lA84vIsKm/yisltTVmUjlYsDnExiuV1c8OSbSdYZ3JMx+7CYPE0bWbosX2AGilIwIMWpQ== + dependencies: + "@parcel/node-resolver-core" "2.5.0" + "@parcel/plugin" "2.5.0" + +"@parcel/runtime-browser-hmr@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.5.0.tgz#5da8b803cc6bd8a0aac143521ea709f2d13a403f" + integrity sha512-oPAo8Zf06gXCpt41nyvK7kv2HH1RrHAGgOqttyjStwAFlm5MZKs7BgtJzO58LfJN8g3sMY0cNdG17fB/4f8q6Q== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + +"@parcel/runtime-js@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.5.0.tgz#270369beef008f72e2c0814022f573817a12dba1" + integrity sha512-gPC2PbNAiooULP71wF5twe4raekuXsR1Hw/ahITDoqsZdXHzG3CkoCjYL3CkmBGiKQgMMocCyN1E2oBzAH8Kyw== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/runtime-react-refresh@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.5.0.tgz#fc74342d77848ea61f364246df70673e83b5430f" + integrity sha512-+8RuDKFdFYIQTrXG4MRhG9XqkkYEHn0zxKyOJ/IkDDfSEhY0na+EyhrneFUwIvDX63gLPkxceXAg0gwBqXPK/Q== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + react-refresh "^0.9.0" + +"@parcel/runtime-service-worker@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.5.0.tgz#609ea02b27cae378f7d9f54820384f7e3494a749" + integrity sha512-STuDlU0fPXeWpAmbayY7o04F0eHy6FTOFeT5KQ0PTxtdEa3Ey8QInP/NVE52Yv0aVQtesWukGrNEFCERlkbFRw== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/source-map@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@parcel/source-map/-/source-map-2.0.2.tgz#9aa0b00518cee31d5634de6e9c924a5539b142c1" + integrity sha512-NnUrPYLpYB6qyx2v6bcRPn/gVigmGG6M6xL8wIg/i0dP1GLkuY1nf+Hqdf63FzPTqqT7K3k6eE5yHPQVMO5jcA== + dependencies: + detect-libc "^1.0.3" + +"@parcel/transformer-babel@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.5.0.tgz#f7f7563a2be9e8bccf7ef48dc61ef8b7be1c0ff0" + integrity sha512-EFb866C9jCoBHIcebWF7goAcYj1wkObx0GDxshlazFtvym1RM27xSWWjRYyqb5+HNOxB3voaNvQOVjcD+DXjCA== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + browserslist "^4.6.6" + json5 "^2.2.0" + nullthrows "^1.1.1" + semver "^5.7.0" + +"@parcel/transformer-css@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.5.0.tgz#8cbe2bd7299a8ef7a965b315da488dcbba1c43d3" + integrity sha512-p8FOvKWWSbS6H8PbD9a0KZqyaKNpSD2BUTzSRYnNj3TBUv7/ZXaP6Om295XTQ/MPht1o7XTQzvfpF/7yEhr02Q== + dependencies: + "@parcel/css" "^1.8.1" + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + browserslist "^4.6.6" + nullthrows "^1.1.1" + +"@parcel/transformer-html@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.5.0.tgz#665fecbcb05cf1a4148e752ab99bfaeabfa31051" + integrity sha512-iEjNyAF0wQmY3DMw7FS+UzoOMng76UsSngh+WWA1E5lv5XyqrP8Mk2QLTJp1nWetUhSLhZr58LGmPYBTB4l9ZQ== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/plugin" "2.5.0" + nullthrows "^1.1.1" + posthtml "^0.16.5" + posthtml-parser "^0.10.1" + posthtml-render "^3.0.0" + semver "^5.7.1" + +"@parcel/transformer-image@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.5.0.tgz#c0523ea88fb4b6ae18fc7c65b08a5ba6dcd23abd" + integrity sha512-vVEXTHZl8m/9yopgK0dWHLOQX2zOnghq6pZnWdWVG6fsvXZln7kP1YN5iwWDoADQYkiKzP+Ymn6UwP9pZpHFzA== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/workers" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/transformer-js@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.5.0.tgz#268a6d34898d7c6515c5a64bae535d2c1a7f57a0" + integrity sha512-Cp8Ic+Au3OcskCRZszmo47z3bqcZ7rfPv2xZYXpXY2TzEc3IV0bKje57bZektoY8LW9LkYM9iBO/WhkVoT6LIg== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/utils" "2.5.0" + "@parcel/workers" "2.5.0" + "@swc/helpers" "^0.3.6" + browserslist "^4.6.6" + detect-libc "^1.0.3" + nullthrows "^1.1.1" + regenerator-runtime "^0.13.7" + semver "^5.7.1" + +"@parcel/transformer-json@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.5.0.tgz#9406b8f0cdd58e65f20fd381a75ece64d346858d" + integrity sha512-661sByA7TkR6Lmxt+hqV4h2SAt+7lgc58DzmUYArpEl1fQnMuQuaB0kQeHzi6fDD2+2G6o7EC+DuwBZKa479TA== + dependencies: + "@parcel/plugin" "2.5.0" + json5 "^2.2.0" + +"@parcel/transformer-postcss@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.5.0.tgz#bff3a36d5a1eb1af4b8c73b9fe5dc50804e449fa" + integrity sha512-IPNlWElekdQHMTBqhdwJNBCQomuYyo7xgNBdnTrt9VJ+R5ihy6n7ZJSWIAJXAH9VZxETTtunfrzRtgkmtjTeZQ== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + clone "^2.1.1" + nullthrows "^1.1.1" + postcss-value-parser "^4.2.0" + semver "^5.7.1" + +"@parcel/transformer-posthtml@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.5.0.tgz#d4c6558b0443ce94ec5ca823c265ebdf05f3af08" + integrity sha512-AZxg1XD8OXOS4bEGEmBBR+X9T9qoFdVsbVUg498zzejYSka1ZQHF7TgLI/+pUnE+ZVYNIp7/G0xXqsRVKMKmdQ== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + nullthrows "^1.1.1" + posthtml "^0.16.5" + posthtml-parser "^0.10.1" + posthtml-render "^3.0.0" + semver "^5.7.1" + +"@parcel/transformer-raw@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.5.0.tgz#5561945e2fd220ac38c0a21aad72175377d048bc" + integrity sha512-I3zjE1u9+Wj90Qqs1V2FTm6iC6SAyOVUthwVZkZey+qbQG/ok682Ez2XjLu7MyQCo9BJNwF/nfOa1hHr3MaJEQ== + dependencies: + "@parcel/plugin" "2.5.0" + +"@parcel/transformer-react-refresh-wrap@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.5.0.tgz#e1ef71218efb21a78677e8770fb6bcf753caf35c" + integrity sha512-VPqVBxhTN4OQwcjsdyxrv+smjAm4s6dbSWAplgPwdOITMv+a0tjhhJU37WnRC+xxTrbEqRcOt96JvGOkPb8i7g== + dependencies: + "@parcel/plugin" "2.5.0" + "@parcel/utils" "2.5.0" + react-refresh "^0.9.0" + +"@parcel/transformer-svg@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.5.0.tgz#78fd321e395923f720a886cb5b5c4fae7101c6b3" + integrity sha512-zCGJcrCpICFe0Q/dgjQZfW7sYFkbJEC7NGT4zEJnMo8Cm/kq8Qh6+2ApX6c+vv5Q0WZn5Ic+N0OvxIMkvgdC/w== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/plugin" "2.5.0" + nullthrows "^1.1.1" + posthtml "^0.16.5" + posthtml-parser "^0.10.1" + posthtml-render "^3.0.0" + semver "^5.7.1" + +"@parcel/transformer-typescript-types@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.5.0.tgz#5f22ed88f1d439a95abd976b9d94f67b961c4541" + integrity sha512-O+v+vEvgQDj5U1O8C12nYeU9kYOdYaznobWgE21WYSPEV2JD9ppaJVTDoNTI5Lx58gmjc1hndY169o6N6RaV6A== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/plugin" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/ts-utils" "2.5.0" + nullthrows "^1.1.1" + +"@parcel/ts-utils@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/ts-utils/-/ts-utils-2.5.0.tgz#fc3b80c85f284f0b0889695ef59c17642cede6f2" + integrity sha512-YITx84Olg27PDxvJlXzzPVgqTtW3tEqQFh+wE2g7+Mwk4Q8vd/jL+mjDBF/5LEnGCk2WvjkcuBK/QOv7Y+YDsg== + dependencies: + nullthrows "^1.1.1" + +"@parcel/types@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.5.0.tgz#e3818d4358f849ac2593605b98366b8e156ab533" + integrity sha512-bA0fhG6aXSGYEVo5Dt96x6lseUQHeVZVzgmiRdZsvb614Gvx22ItfaKhPmAVbM9vzbObZDHl9l9G2Ovw8Xve4g== + dependencies: + "@parcel/cache" "2.5.0" + "@parcel/diagnostic" "2.5.0" + "@parcel/fs" "2.5.0" + "@parcel/package-manager" "2.5.0" + "@parcel/source-map" "^2.0.0" + "@parcel/workers" "2.5.0" + utility-types "^3.10.0" + +"@parcel/utils@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.5.0.tgz#96d2c7e7226128cc84418ba41770b38aff23ca20" + integrity sha512-kaLGXtQuOOH55KZqXdYDvczhh3mk2eeTVqrrXuuihGjbLKYFlUW2tFDm+5r2s9nCPwTQxOO43ZEOCKSnia+e4w== + dependencies: + "@parcel/codeframe" "2.5.0" + "@parcel/diagnostic" "2.5.0" + "@parcel/hash" "2.5.0" + "@parcel/logger" "2.5.0" + "@parcel/markdown-ansi" "2.5.0" + "@parcel/source-map" "^2.0.0" + chalk "^4.1.0" + +"@parcel/watcher@^2.0.0": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.5.tgz#f913a54e1601b0aac972803829b0eece48de215b" + integrity sha512-x0hUbjv891omnkcHD7ZOhiyyUqUUR6MNjq89JhEI3BxppeKWAm6NPQsqqRrAkCJBogdT/o/My21sXtTI9rJIsw== + dependencies: + node-addon-api "^3.2.1" + node-gyp-build "^4.3.0" + +"@parcel/workers@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.5.0.tgz#c7f1a4bcd491c7422212724dedbcf7d1e980146e" + integrity sha512-/Ow5OKJWs+9OzV3Jy4J++VnbNx0j3ls/M1CGVBLiBWyCada9DMtquYoBQ4Sk6Uam50BKkIFYetGOeXPNQyyMjg== + dependencies: + "@parcel/diagnostic" "2.5.0" + "@parcel/logger" "2.5.0" + "@parcel/types" "2.5.0" + "@parcel/utils" "2.5.0" + chrome-trace-event "^1.0.2" + nullthrows "^1.1.1" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -512,11 +1177,23 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@swc/helpers@^0.3.6": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.3.13.tgz#b9af856aaa3804fefdd1544632dde35b7b6ff978" + integrity sha512-A1wswJhnqaLRn8uYVQ8YiNTtY5i/JIPmV08EXXjjTresIkUVUEUaFv/wXVhGXfRNYMvHPkuoMR1Nb6NgpxGjNg== + dependencies: + tslib "^2.4.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -632,6 +1309,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b" integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/prettier@^2.1.5": version "2.4.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.2.tgz#4c62fae93eb479660c3bd93f9d24d561597a8281" @@ -659,6 +1341,11 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== +abortcontroller-polyfill@^1.1.9: + version "1.7.3" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" + integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -687,6 +1374,11 @@ acorn@^8.2.4, acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -826,6 +1518,18 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base-x@^3.0.8: + version "3.0.9" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + dependencies: + safe-buffer "^5.0.1" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -857,6 +1561,17 @@ browserslist@^4.17.5: node-releases "^2.0.1" picocolors "^1.0.0" +browserslist@^4.6.6: + version "4.20.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" + integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== + dependencies: + caniuse-lite "^1.0.30001332" + electron-to-chromium "^1.4.118" + escalade "^3.1.1" + node-releases "^2.0.3" + picocolors "^1.0.0" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -901,6 +1616,11 @@ caniuse-lite@^1.0.30001286: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz#0e690039f62e91c3ea581673d716890512e7ec52" integrity sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ== +caniuse-lite@^1.0.30001332: + version "1.0.30001341" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498" + integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA== + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -910,7 +1630,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -923,6 +1643,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + ci-info@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" @@ -953,6 +1678,11 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -994,6 +1724,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1006,6 +1746,17 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -1020,6 +1771,37 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -1086,6 +1868,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -1101,6 +1888,20 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -1108,6 +1909,37 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dotenv-expand@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" + integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== + +electron-to-chromium@^1.4.118: + version "1.4.137" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" + integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== + electron-to-chromium@^1.4.17: version "1.4.38" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.38.tgz#10ea58d73d36b13e78d5024f3b74a352d3958d01" @@ -1123,6 +1955,23 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" + integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: version "0.10.53" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" @@ -1317,6 +2166,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" + integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== + get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" @@ -1351,6 +2205,13 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +globals@^13.2.0: + version "13.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== + dependencies: + type-fest "^0.20.2" + graceful-fs@^4.2.4: version "4.2.9" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" @@ -1385,6 +2246,25 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +htmlnano@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.2.tgz#3e3170941e2446a86211196d740272ebca78f878" + integrity sha512-+ZrQFS4Ub+zd+/fWwfvoYCEGNEa0/zrpys6CyXxvZDwtL7Pl+pOtRkiujyvBQ7Lmfp7/iEPxtOFgxWA16Gkj3w== + dependencies: + cosmiconfig "^7.0.1" + posthtml "^0.16.5" + timsort "^0.3.0" + +htmlparser2@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5" + integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.2" + domutils "^2.8.0" + entities "^3.0.1" + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -1414,6 +2294,14 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1440,6 +2328,11 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + is-core-module@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" @@ -1469,6 +2362,11 @@ is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" +is-json@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff" + integrity sha1-a+Fm0USCihMdaGiRuYPfYsOUkf8= + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -2003,6 +2901,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-ref-parser@^9.0.6: version "9.0.9" resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f" @@ -2043,6 +2946,11 @@ json5@2.x, json5@^2.1.2: dependencies: minimist "^1.2.5" +json5@^2.2.0, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -2061,6 +2969,22 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lmdb@2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.2.4.tgz#6494d5a1d1db152e0be759edcfa06893e4cbdb53" + integrity sha512-gto+BB2uEob8qRiTlOq+R3uX0YNHsX9mjxj9Sbdue/LIKqu6IlZjrsjKeGyOMquc/474GEqFyX2pdytpydp0rQ== + dependencies: + msgpackr "^1.5.4" + nan "^2.14.2" + node-gyp-build "^4.2.3" + ordered-binary "^1.2.4" + weak-lru-cache "^1.2.2" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -2073,6 +2997,11 @@ lodash.memoize@4.x: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + lodash@^4.17.20, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2111,6 +3040,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + memoizee@^0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72" @@ -2177,6 +3111,57 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +msgpackr-extract-darwin-arm64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-1.1.0.tgz#d590dffac6b90edc3ab53392f7ec5668ed94638c" + integrity sha512-s1kHoT12tS2cCQOv+Wl3I+/cYNJXBPtwQqGA+dPYoXmchhXiE0Nso+BIfvQ5PxbmAyjj54Q5o7PnLTqVquNfZA== + +msgpackr-extract-darwin-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-1.1.0.tgz#568cbdf5e819ac120659c02b0dbaabf483523ee3" + integrity sha512-yx/H/i12IKg4eWGu/eKdKzJD4jaYvvujQSaVmeOMCesbSQnWo5X6YR9TFjoiNoU9Aexk1KufzL9gW+1DozG1yw== + +msgpackr-extract-linux-arm64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-1.1.0.tgz#c0a30e6687cea4f79115f5762c5fdff90e4a20d4" + integrity sha512-AxFle3fHNwz2V4CYDIGFxI6o/ZuI0lBKg0uHI8EcCMUmDE5mVAUWYge5WXmORVvb8sVWyVgFlmi3MTu4Ve6tNQ== + +msgpackr-extract-linux-arm@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-1.1.0.tgz#38e8db873b6b3986558bde4d7bb15eacc8743a9e" + integrity sha512-0VvSCqi12xpavxl14gMrauwIzHqHbmSChUijy/uo3mpjB1Pk4vlisKpZsaOZvNJyNKj0ACi5jYtbWnnOd7hYGw== + +msgpackr-extract-linux-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-1.1.0.tgz#8c44ca5211d9fa6af77be64a8e687c0be0491ce7" + integrity sha512-O+XoyNFWpdB8oQL6O/YyzffPpmG5rTNrr1nKLW70HD2ENJUhcITzbV7eZimHPzkn8LAGls1tBaMTHQezTBpFOw== + +msgpackr-extract-win32-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-1.1.0.tgz#7bf9bd258e334668842c7532e5e40a60ca3325d7" + integrity sha512-6AJdM5rNsL4yrskRfhujVSPEd6IBpgvsnIT/TPowKNLQ62iIdryizPY2PJNFiW3AJcY249AHEiDBXS1cTDPxzA== + +msgpackr-extract@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.1.4.tgz#665037c1470f225d01d2d735dad0334fff5faae6" + integrity sha512-WQbHvsThprXh+EqZYy+SQFEs7z6bNM7a0vgirwUfwUcphWGT2mdPcpyLCNiRsN6w5q5VKJUMblHY+tNEyceb9Q== + dependencies: + node-gyp-build-optional-packages "^4.3.2" + optionalDependencies: + msgpackr-extract-darwin-arm64 "1.1.0" + msgpackr-extract-darwin-x64 "1.1.0" + msgpackr-extract-linux-arm "1.1.0" + msgpackr-extract-linux-arm64 "1.1.0" + msgpackr-extract-linux-x64 "1.1.0" + msgpackr-extract-win32-x64 "1.1.0" + +msgpackr@^1.5.4: + version "1.5.7" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.5.7.tgz#53b3fd0e7afdf4184a594881a18832df9422b660" + integrity sha512-Hsa80i8W4BiObSMHslfnwC+CC1CYHZzoXJZn0+3EvoCEOgt3c5QlXhdcjgFk2aZxMgpV8aUFZqJyQUCIp4UrzA== + optionalDependencies: + msgpackr-extract "^1.1.4" + mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -2186,6 +3171,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nan@^2.14.2: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2201,6 +3191,21 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= +node-addon-api@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-gyp-build-optional-packages@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.2.tgz#82de9bdf9b1ad042457533afb2f67469dc2264bb" + integrity sha512-P5Ep3ISdmwcCkZIaBaQamQtWAG0facC89phWZgi5Z3hBU//J6S48OIvyZWSPPf6yQMklLZiqoosWAZUj7N+esA== + +node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" + integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -2211,6 +3216,11 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" + integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== + normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -2223,6 +3233,18 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nth-check@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + +nullthrows@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== + nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" @@ -2259,6 +3281,11 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +ordered-binary@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.2.5.tgz#6208c45067eae9d14b8f44791a1d7037adad9147" + integrity sha512-djRmZoEpOGvIRW7ufsCDHtvcUa18UC9TxnPbHhSVFZHsoyg0dtut1bWtBZ/fmxdPN62oWXrV6adM7NoWU+CneA== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -2278,6 +3305,43 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +parcel@^2.4.1, parcel@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.5.0.tgz#b6f01c665b6085e4eb58957ff11bc8f4027bd8f7" + integrity sha512-er0mj/BaMjWyzQ/jedLUi/LNAuQcFT8lCvoNqANF+jTaX9rohaBwxIvKVJVAZgyCnmyfbbldp496wPMW0R0+CA== + dependencies: + "@parcel/config-default" "2.5.0" + "@parcel/core" "2.5.0" + "@parcel/diagnostic" "2.5.0" + "@parcel/events" "2.5.0" + "@parcel/fs" "2.5.0" + "@parcel/logger" "2.5.0" + "@parcel/package-manager" "2.5.0" + "@parcel/reporter-cli" "2.5.0" + "@parcel/reporter-dev-server" "2.5.0" + "@parcel/utils" "2.5.0" + chalk "^4.1.0" + commander "^7.0.0" + get-port "^4.2.0" + v8-compile-cache "^2.0.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parse5@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -2303,6 +3367,11 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -2325,6 +3394,40 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +posthtml-parser@^0.10.1: + version "0.10.2" + resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.10.2.tgz#df364d7b179f2a6bf0466b56be7b98fd4e97c573" + integrity sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg== + dependencies: + htmlparser2 "^7.1.1" + +posthtml-parser@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.11.0.tgz#25d1c7bf811ea83559bc4c21c189a29747a24b7a" + integrity sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw== + dependencies: + htmlparser2 "^7.1.1" + +posthtml-render@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-3.0.0.tgz#97be44931496f495b4f07b99e903cc70ad6a3205" + integrity sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA== + dependencies: + is-json "^2.0.1" + +posthtml@^0.16.4, posthtml@^0.16.5: + version "0.16.6" + resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.16.6.tgz#e2fc407f67a64d2fa3567afe770409ffdadafe59" + integrity sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ== + dependencies: + posthtml-parser "^0.11.0" + posthtml-render "^3.0.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -2357,7 +3460,7 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -punycode@^2.1.1: +punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -2367,6 +3470,16 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-refresh@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" + integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== + +regenerator-runtime@^0.13.7: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -2379,6 +3492,11 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -2405,6 +3523,11 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -2429,6 +3552,11 @@ semver@7.x, semver@^7.3.2: dependencies: lru-cache "^6.0.0" +semver@^5.7.0, semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -2461,7 +3589,7 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -source-map-support@^0.5.6: +source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -2484,11 +3612,23 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +source-map@~0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + stack-utils@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" @@ -2564,11 +3704,29 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svgo@^2.4.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +term-size@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -2577,6 +3735,16 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" +terser@^5.2.0: + version "5.13.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.13.1.tgz#66332cdc5a01b04a224c9fad449fc1a18eaa1799" + integrity sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA== + dependencies: + acorn "^8.5.0" + commander "^2.20.0" + source-map "~0.8.0-beta.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -2613,6 +3781,11 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" next-tick "1" +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -2639,6 +3812,13 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -2678,6 +3858,11 @@ ts-node@^10.4.0: make-error "^1.1.1" yn "3.1.1" +tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -2690,6 +3875,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -2722,6 +3912,16 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + +v8-compile-cache@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + v8-to-istanbul@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" @@ -2752,6 +3952,16 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" +weak-lru-cache@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" + integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -2774,6 +3984,15 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" @@ -2834,6 +4053,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xxhash-wasm@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79" + integrity sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -2844,6 +4068,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@20.x, yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" From c73e60496ac13e2ba1e87e3133f8ed19e3956a6a Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Mon, 16 May 2022 04:17:06 +0800 Subject: [PATCH 02/18] Started working on a new runtime --- bindings/web/rune/src/Runtime.ts | 9 + bindings/web/rune/src/index.test.ts | 13 + bindings/web/rune/src/index.ts | 10 + bindings/web/rune/src/loader/RuneLoader.ts | 44 +++ bindings/web/rune/src/loader/index.ts | 27 ++ bindings/web/rune/src/logging.ts | 158 ++++++++++ .../web/rune/src/proc_blocks/HostFunctions.ts | 293 ++++++++++++++++++ .../web/rune/src/proc_blocks/ProcBlock.ts | 88 ++++++ bindings/web/rune/src/proc_blocks/index.ts | 76 +++++ 9 files changed, 718 insertions(+) create mode 100644 bindings/web/rune/src/Runtime.ts create mode 100644 bindings/web/rune/src/index.test.ts create mode 100644 bindings/web/rune/src/loader/RuneLoader.ts create mode 100644 bindings/web/rune/src/loader/index.ts create mode 100644 bindings/web/rune/src/logging.ts create mode 100644 bindings/web/rune/src/proc_blocks/HostFunctions.ts create mode 100644 bindings/web/rune/src/proc_blocks/ProcBlock.ts create mode 100644 bindings/web/rune/src/proc_blocks/index.ts diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts new file mode 100644 index 0000000000..7e2bf6cf9c --- /dev/null +++ b/bindings/web/rune/src/Runtime.ts @@ -0,0 +1,9 @@ +import { Node, Runtime as RuntimeInterface } from "./loader"; + +export class Runtime implements RuntimeInterface { + constructor(private graph: Record) {} + + infer(): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts new file mode 100644 index 0000000000..187b88c6f1 --- /dev/null +++ b/bindings/web/rune/src/index.test.ts @@ -0,0 +1,13 @@ +import { consoleLogger, RuneLoader } from "."; + +describe("Integration Tests", () => { + it("can load the sine Rune", async () => { + const loader = new RuneLoader(); + + const runtime = await loader + .withLogger(consoleLogger) + .load(new ArrayBuffer(0)); + + runtime.infer(); + }); +}); diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index e69de29bb2..9d851204da 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -0,0 +1,10 @@ +export * from "./loader"; +export * from "./proc_blocks"; +export { consoleLogger } from "./logging"; +export type { Logger } from "./logging"; + +import {runtime_v1} from "@hotg-ai/rune-wit-files"; + +export type Tensor = runtime_v1.Tensor; +export type ElementType = runtime_v1.ElementType; +export type Dimensions = runtime_v1.Dimensions; diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/loader/RuneLoader.ts new file mode 100644 index 0000000000..b066e9e254 --- /dev/null +++ b/bindings/web/rune/src/loader/RuneLoader.ts @@ -0,0 +1,44 @@ +import type { ModelHandler, Runtime } from "."; +import { consoleLogger, Logger, StructuredLogger } from "../logging"; + +export class RuneLoader { + public static default: RuneLoader = new RuneLoader() + .withLogger(consoleLogger); + + private modelHandlers: Record = {}; + private logger: Logger = { log: () => {}, isEnabled: () => false }; + + /** + * Set the logger that will be used whenever the Rune emits a message. + */ + public withLogger(logger: Logger | Logger["log"]): this { + if (typeof logger == "function") { + // As a convenience, we let people pass in a logging function if + // they don't care about isEnabled(). + this.logger = { log: logger, isEnabled: m => m.level != "trace" }; + } else { + this.logger = logger; + } + + return this; + } + + public withModelHandler(modelType: string, handler: ModelHandler): this { + this.modelHandlers[modelType] = handler; + return this; + } + + /** + * Load the Rune, instantiating a Runtime that can be used to interact with + * it. + * + * @param rune + */ + public async load(rune: ArrayBuffer): Promise { + const log = new StructuredLogger(this.logger, RuneLoader.name); + + log.info("Loading the Rune", { bytes: rune.byteLength }); + + throw new Error(); + } +} diff --git a/bindings/web/rune/src/loader/index.ts b/bindings/web/rune/src/loader/index.ts new file mode 100644 index 0000000000..fcc8b4cf5d --- /dev/null +++ b/bindings/web/rune/src/loader/index.ts @@ -0,0 +1,27 @@ +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; + +export { RuneLoader } from "./RuneLoader"; + +export interface Model {} + +/** + * A callback that can load models. + */ +export type ModelHandler = ( + model: ArrayBuffer, + args: Record +) => Promise; + +export interface Node { + infer( + inputs: Record + ): Promise>; +} + +export type Pipeline = { + graph: Record; +}; + +export interface Runtime { + infer(): Promise; +} diff --git a/bindings/web/rune/src/logging.ts b/bindings/web/rune/src/logging.ts new file mode 100644 index 0000000000..801e77786a --- /dev/null +++ b/bindings/web/rune/src/logging.ts @@ -0,0 +1,158 @@ +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; + +export type LogLevel = Lowercase; +export type LogValue = number | boolean | string | null; +export type LogPayload = Record; + +/** + * Metadata associated with a single logging event. + */ +export type LogMetadata = { + /** + * The log event's verbosity. + */ + level: LogLevel; + /** + * The name of the section of code ("span") this event was emitted from. + */ + span: string; + /** + * A string describing the part of the system where the span or event that + * this metadata describes occurred. + * + * Typically, this is the module path, but alternate targets may be set when + * spans or events are constructed. + */ + target: string; + file?: string; + line?: number; + module?: string; +}; + +/** + * An object that will receive log messages. + */ +export interface Logger { + /** + * Log a message. + * + * @param metadata Information about the log message and where it came from. + * @param message The message. + * @param data Structured data. + */ + log(metadata: LogMetadata, message: string, data: LogPayload): void; + + /** + * Check if a message *would* be logged based on its metadata. + */ + isEnabled(metadata: LogMetadata): boolean; +} + +/** + * A structured logger intended for use within JavaScript. + */ +export class StructuredLogger { + /** + * Create a new structured logger. + * + * @param backend The logging backend messages are sent to. + * @param component The name of the component being logged. + */ + constructor(private backend: Logger, public component: string) {} + + trace(msg: string, payload?: LogPayload) { + this.log("trace", msg, payload); + } + + debug(msg: string, payload?: LogPayload) { + this.log("debug", msg, payload); + } + + info(msg: string, payload?: LogPayload) { + this.log("info", msg, payload); + } + + warn(msg: string, payload?: LogPayload) { + this.log("warn", msg, payload); + } + + error(msg: string, payload?: LogPayload) { + this.log("error", msg, payload); + } + + fatal(msg: string, payload?: LogPayload) { + this.log("fatal", msg, payload); + } + + /** + * Enter a named span. + */ + span(name: string): Span { + return new Span(this.backend, this.component, name); + } + + protected metadata(level: LogLevel): LogMetadata { + return { + level, + // Note: We aren't within a span by default. + span: "", + target: this.component, + }; + } + + log(level: LogLevel, msg: string, payload?: LogPayload) { + const meta = this.metadata(level); + + if (this.backend.isEnabled(meta)) { + this.backend.log(meta, msg, payload || {}); + } + } +} + +export class Span extends StructuredLogger { + constructor(backend: Logger, component: string, public name: string) { + super(backend, component); + } + + /** + * Time how long an operation takes. + */ + timeit(thunk: () => R, level: LogLevel = "debug"): R { + const start = Date.now(); + this.log(level, "started"); + + try { + return thunk(); + } finally { + this.log(level, "completed", { duration: Date.now() - start }); + } + } + + protected metadata(level: LogLevel): LogMetadata { + const meta = super.metadata(level); + meta.span = this.name; + return meta; + } +} + +/** + * A simple logger implementation that does the equivalent of console.log() for + * each log level. + */ +export function consoleLogger( + metadata: LogMetadata, + message: string, + data: Record +): void { + const { level } = metadata; + + const fatal = (msg: string, ...args: any[]) => { + console.error(msg, ...args); + throw new Error(msg); + }; + + const logger = level == "fatal" ? fatal : console[level]; + + logger(message, metadata, data); + throw new Error("Method not implemented."); +} diff --git a/bindings/web/rune/src/proc_blocks/HostFunctions.ts b/bindings/web/rune/src/proc_blocks/HostFunctions.ts new file mode 100644 index 0000000000..59109eaa2f --- /dev/null +++ b/bindings/web/rune/src/proc_blocks/HostFunctions.ts @@ -0,0 +1,293 @@ +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +import type { + ArgumentHint, + ArgumentMetadata, + Metadata, + SupportedShapes, + TensorHint, + TensorMetadata, + TensorDescriptor, +} from "."; +import { Logger, LogLevel, LogMetadata, StructuredLogger } from "../logging"; + +const logLevels: Record = { + [runtime_v1.LogLevel.Trace]: "trace", + [runtime_v1.LogLevel.Debug]: "debug", + [runtime_v1.LogLevel.Info]: "info", + [runtime_v1.LogLevel.Warn]: "warn", + [runtime_v1.LogLevel.Error]: "error", + [runtime_v1.LogLevel.Fatal]: "fatal", +}; + +export class HostFunctions implements runtime_v1.RuntimeV1 { + metadata?: Metadata; + graph?: GraphContext; + kernel?: KernelContext; + + constructor(private logger: Logger) {} + + metadataNew(name: string, version: string): runtime_v1.Metadata { + return new MetadataBuilder({ + name, + version, + arguments: [], + inputs: [], + outputs: [], + tags: [], + }); + } + + argumentMetadataNew(name: string): runtime_v1.ArgumentMetadata { + return new ArgumentMetadataBuilder({ name, hints: [] }); + } + + tensorMetadataNew(name: string): runtime_v1.TensorMetadata { + return new TensorMetadataBuilder({ name, hints: [] }); + } + + interpretAsImage(): TensorHint { + return { type: "media-hint", media: "image" }; + } + + interpretAsAudio(): TensorHint { + return { type: "media-hint", media: "audio" }; + } + + supportedShapes( + supportedElementTypes: runtime_v1.ElementType[], + dimensions: runtime_v1.Dimensions + ): SupportedShapes { + return { + type: "supported-shapes", + supportedElementTypes, + dimensions, + }; + } + + interpretAsNumberInRange(min: string, max: string): ArgumentHint { + return { type: "number-in-range", min, max }; + } + + interpretAsStringInEnum(stringEnum: string[]): ArgumentHint { + return { type: "string-enum", possibleValues: stringEnum }; + } + + nonNegativeNumber(): ArgumentHint { + return { type: "non-negative-number" }; + } + + supportedArgumentType(hint: runtime_v1.ArgumentType): ArgumentHint { + return { type: "supported-argument-type", argumentType: hint }; + } + + registerNode(metadata: runtime_v1.Metadata): void { + if (metadata instanceof MetadataBuilder) { + this.metadata = metadata.meta; + } + } + + graphContextForNode(nodeId: string): runtime_v1.GraphContext | null { + return this.graph || null; + } + + kernelContextForNode(nodeId: string): runtime_v1.KernelContext | null { + return this.kernel || null; + } + + private translateMetadata(meta: runtime_v1.LogMetadata): LogMetadata { + const { level, name, target, file, line, module } = meta; + return { + span: name, + target, + file, + line, + module, + level: logLevels[level], + }; + } + + isEnabled(meta: runtime_v1.LogMetadata): boolean { + return this.logger.isEnabled(this.translateMetadata(meta)); + } + + log( + metadata: runtime_v1.LogMetadata, + message: string, + data: runtime_v1.LogValueMap + ): void { + const payload = data.map(([key, value]) => { + return value.tag == "null" ? [key, null] : [key, value.val]; + }); + + const meta = this.translateMetadata(metadata); + + this.logger.log(meta, message, Object.fromEntries(payload)); + } + + modelLoad( + modelFormat: string, + model: Uint8Array, + args: [string, string][] + ): runtime_v1.Result { + throw new Error("Method not implemented."); + } +} + +class MetadataBuilder implements runtime_v1.Metadata { + constructor(public meta: Metadata) {} + + setDescription(description: string): void { + this.meta.description = description; + } + + setRepository(url: string): void { + if (url) { + this.meta.repository = url; + } + } + + setHomepage(url: string): void { + if (url) { + this.meta.homepage = url; + } + } + + addTag(tag: string): void { + this.meta.tags.push(tag); + } + + addArgument(arg: runtime_v1.ArgumentMetadata): void { + if (arg instanceof ArgumentMetadataBuilder) { + this.meta.arguments.push(arg.meta); + } + } + + addInput(metadata: runtime_v1.TensorMetadata): void { + if (metadata instanceof TensorMetadataBuilder) { + this.meta.inputs.push(metadata.meta); + } + } + + addOutput(metadata: runtime_v1.TensorMetadata): void { + if (metadata instanceof TensorMetadataBuilder) { + this.meta.outputs.push(metadata.meta); + } + } +} + +class ArgumentMetadataBuilder implements runtime_v1.ArgumentMetadata { + constructor(public meta: ArgumentMetadata) {} + + setDescription(description: string): void { + this.meta.description = description; + } + + setDefaultValue(defaultValue: string): void { + this.meta.defaultValue; + } + + addHint(hint: runtime_v1.ArgumentHint): void { + if (isArgumentHint(hint)) { + this.meta.hints.push(hint); + } + } +} + +class TensorMetadataBuilder implements runtime_v1.TensorMetadata { + constructor(public meta: TensorMetadata) {} + + setDescription(description: string): void { + this.meta.description = description; + } + + addHint(hint: runtime_v1.TensorHint): void { + if (isTensorHint(hint)) { + this.meta.hints.push(hint); + } + } +} + +function isArgumentHint(value?: any): value is ArgumentHint { + const types: Array = [ + "non-negative-number", + "number-in-range", + "string-enum", + "supported-argument-type", + ]; + + return types.includes(value?.type); +} + +function isTensorHint(value?: any): value is TensorHint { + const types: Array = ["media-hint", "supported-shapes"]; + + return types.includes(value?.type); +} + +export class GraphContext implements runtime_v1.GraphContext { + inputs: TensorDescriptor[] = []; + outputs: TensorDescriptor[] = []; + + constructor(private args: Record) {} + + getArgument(name: string): string | null { + if (name in this.args) { + return this.args[name]; + } else { + return null; + } + } + + addInputTensor( + name: string, + elementType: runtime_v1.ElementType, + dimensions: runtime_v1.Dimensions + ): void { + this.inputs.push({ name, elementType, dimensions }); + } + + addOutputTensor( + name: string, + elementType: runtime_v1.ElementType, + dimensions: runtime_v1.Dimensions + ): void { + this.outputs.push({ name, elementType, dimensions }); + } +} + +export class KernelContext implements runtime_v1.KernelContext { + public outputs: Record = {}; + + constructor( + private args: Record, + private inputs: Record + ) {} + + getArgument(name: string): string | null { + if (name in this.args) { + return this.args[name]; + } else { + return null; + } + } + + getInputTensor(name: string): runtime_v1.Tensor | null { + if (name in this.inputs) { + return this.inputs[name]; + } else { + return null; + } + } + + setOutputTensor(name: string, tensor: runtime_v1.Tensor): void { + this.outputs[name] = tensor; + } + + getGlobalInput(name: string): runtime_v1.Tensor | null { + throw new Error("Method not implemented."); + } + + setGlobalOutput(name: string, tensor: runtime_v1.Tensor): void { + throw new Error("Method not implemented."); + } +} diff --git a/bindings/web/rune/src/proc_blocks/ProcBlock.ts b/bindings/web/rune/src/proc_blocks/ProcBlock.ts new file mode 100644 index 0000000000..516de2f934 --- /dev/null +++ b/bindings/web/rune/src/proc_blocks/ProcBlock.ts @@ -0,0 +1,88 @@ +import { proc_block_v1, runtime_v1 } from "@hotg-ai/rune-wit-files"; +import type { Metadata, TensorDescriptor } from "."; +import { Logger, StructuredLogger } from "../logging"; +import { GraphContext, HostFunctions, KernelContext } from "./HostFunctions"; + +type ProcBlockBuffer = Parameters[0]; + +type Tensors = { + inputs: TensorDescriptor[]; + outputs: TensorDescriptor[]; +}; + +/** + * An executable proc-block. + */ +export class ProcBlock { + private constructor( + private hostFunctions: HostFunctions, + private instance: proc_block_v1.ProcBlockV1, + ) {} + + static async load( + procBlock: ProcBlockBuffer, + logger: Logger + ): Promise { + const log = new StructuredLogger(logger, "ProcBlock"); + + const span = log.span("load"); + span.info("Loading the proc-block"); + const start = Date.now(); + + const wrapper = new proc_block_v1.ProcBlockV1(); + const imports: any = {}; + + const hostFunctions = new HostFunctions(logger); + runtime_v1.addRuntimeV1ToImports( + imports, + hostFunctions, + (name) => wrapper.instance.exports[name] + ); + + await wrapper.instantiate(procBlock, imports); + + span.debug("Finished loading the proc-block", { + durationMs: Date.now() - start, + }); + + return new ProcBlock(hostFunctions, wrapper); + } + + /** + * Extract metadata from the proc-block. + */ + metadata(): Metadata | undefined { + this.hostFunctions.metadata = undefined; + this.instance.registerMetadata(); + return this.hostFunctions.metadata; + } + + /** + * Given the provided set of arguments, what would this proc-block's input + * and output tensors be? + */ + graph(args: Record): Tensors { + const ctx = new GraphContext(args); + this.hostFunctions.graph = ctx; + this.instance.graph(""); + const { inputs, outputs } = ctx; + return { inputs, outputs }; + } + + /** + * Evaluate this proc-block. + * + * @param args Key-value arguments that control the proc-block's behaviour. + * @param inputs Input tensors. + * @returns + */ + evaluate( + args: Record, + inputs: Record + ): Record { + const ctx = new KernelContext(args, inputs); + this.hostFunctions.kernel = ctx; + this.instance.kernel(""); + return ctx.outputs; + } +} diff --git a/bindings/web/rune/src/proc_blocks/index.ts b/bindings/web/rune/src/proc_blocks/index.ts new file mode 100644 index 0000000000..061d61fe7d --- /dev/null +++ b/bindings/web/rune/src/proc_blocks/index.ts @@ -0,0 +1,76 @@ +export { ProcBlock } from "./ProcBlock"; + +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; + +export type Metadata = { + name: string; + version: string; + description?: string; + repository?: string; + homepage?: string; + tags: string[]; + arguments: ArgumentMetadata[]; + inputs: TensorMetadata[]; + outputs: TensorMetadata[]; +}; + +/** + * Information about a tensor's name and constraints about its general shape. + */ +export type TensorDescriptor = { + name: string; + elementType: runtime_v1.ElementType; + dimensions: runtime_v1.Dimensions; +}; + +export type TensorMetadata = { + name: string; + description?: string; + hints: TensorHint[]; +}; + +export type ArgumentMetadata = { + name: string; + description?: string; + defaultValue?: string; + hints: ArgumentHint[]; +}; + +export type NumberInRange = { + type: "number-in-range"; + min: string; + max: string; +}; + +export type StringEnum = { + type: "string-enum"; + possibleValues: string[]; +}; + +export type NonNegativeNumber = { + type: "non-negative-number"; +}; + +export type SupportedArgumentType = { + type: "supported-argument-type"; + argumentType: runtime_v1.ArgumentType; +}; + +export type ArgumentHint = + | NumberInRange + | StringEnum + | NonNegativeNumber + | SupportedArgumentType; + +export type MediaHint = { + type: "media-hint"; + media: "image" | "audio"; +}; + +export type SupportedShapes = { + type: "supported-shapes"; + supportedElementTypes: runtime_v1.ElementType[]; + dimensions: runtime_v1.Dimensions; +}; + +export type TensorHint = MediaHint | SupportedShapes; From 254e81bb2beb2717f66ea357040bef2758a4a3f9 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Mon, 16 May 2022 07:44:53 +0800 Subject: [PATCH 03/18] Started wiring the runtime up to the pipeline --- bindings/web/rune/package.json | 5 +- bindings/web/rune/src/Runefile.ts | 168 +++++++++++++++ bindings/web/rune/src/Runtime.ts | 105 +++++++++- bindings/web/rune/src/__fixtures__/sine.zip | Bin 0 -> 20590 bytes bindings/web/rune/src/index.test.ts | 36 +++- bindings/web/rune/src/index.ts | 6 +- bindings/web/rune/src/loader/Pipeline.ts | 25 +++ bindings/web/rune/src/loader/RuneLoader.ts | 191 +++++++++++++++++- bindings/web/rune/src/loader/index.ts | 25 ++- bindings/web/rune/src/logging.ts | 37 ++-- .../web/rune/src/proc_blocks/ProcBlock.ts | 9 +- bindings/web/rune/src/proc_blocks/index.ts | 5 + bindings/web/rune/yarn.lock | 81 +++++++- 13 files changed, 636 insertions(+), 57 deletions(-) create mode 100644 bindings/web/rune/src/Runefile.ts create mode 100644 bindings/web/rune/src/__fixtures__/sine.zip create mode 100644 bindings/web/rune/src/loader/Pipeline.ts diff --git a/bindings/web/rune/package.json b/bindings/web/rune/package.json index b003ba3d1f..a3f499f425 100644 --- a/bindings/web/rune/package.json +++ b/bindings/web/rune/package.json @@ -19,12 +19,15 @@ "generate-runefile-types": "json2ts ../../../crates/compiler/runefile-schema.json --output src/Runefile.ts" }, "dependencies": { - "@hotg-ai/rune-wit-files": "^0.3.1" + "@hotg-ai/rune-wit-files": "^0.3.1", + "js-yaml": "^4.1.0", + "jszip": "^3.9.1" }, "devDependencies": { "@parcel/packager-ts": "2.5.0", "@parcel/transformer-typescript-types": "2.5.0", "@types/jest": "^27.0.0", + "@types/js-yaml": "^4.0.5", "jest": "^27.0.6", "json-schema-to-typescript": "^10.1.5", "parcel": "^2.5.0", diff --git a/bindings/web/rune/src/Runefile.ts b/bindings/web/rune/src/Runefile.ts new file mode 100644 index 0000000000..f8653bd869 --- /dev/null +++ b/bindings/web/rune/src/Runefile.ts @@ -0,0 +1,168 @@ +/* tslint:disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * The top level Runefile type. + */ +export type Document = DocumentV1; +/** + * + * A specification for finding a dependency. + * + * The full syntax is `base@version#sub_path` where + * + * - `base` is a URL or the name of a repository on GitHub (e.g. `hotg-ai/rune` + * or `https://github.com/hotg-ai/rune`) + * - `version` is an optional field specifying the version (e.g. as a git tag) + * - `sub_path` is an optional field which is useful when pointing to + * repositories with multiple relevant items because it lets you specify + * which directory the specified item is in. + * + */ +export type Path = string; +/** + * A stage in the Rune's pipeline. + */ +export type Stage = ModelStage | ProcBlockStage | CapabilityStage | OutStage; +/** + * Something that could be either a reference to a resource (`$resource`) or a plain string (`./path`). + */ +export type Argument = ResourceName | number; +/** + * + * A reference to some [`ResourceDeclaration`]. It typically looks like + * `$RESOURCE_NAME`. + * + */ +export type ResourceName = string; +/** + * + * The name of a tensor. + * + * Typically something like "stage", or "stage.2" if the stage has multiple outputs. + * + */ +export type Input = string; +/** + * How the resource should be treated inside the Rune. + */ +export type ResourceType = "string" | "binary"; + +/** + * Version 1 of the `Runefile.yml` format. + */ +export interface DocumentV1 { + /** + * The base image that defines the interface between a Rune and its runtime. + * + * This should always be `"runicos/base"`. + */ + image: Path; + /** + * The various stages in the Runefile's pipeline. + */ + pipeline: { + [k: string]: Stage; + }; + /** + * Any resources that can be accessed by pipeline stages. + */ + resources?: { + [k: string]: ResourceDeclaration; + }; + /** + * The version number. Must always be `"1"`. + */ + version: number; + [k: string]: unknown; +} +/** + * A ML model which will be executed by the runtime. + */ +export interface ModelStage { + args?: { + [k: string]: Argument; + }; + /** + * Tensors to use as input to this model. + */ + inputs?: Input[]; + /** + * The model to use, or a resource which specifies the model to use. + */ + model: ResourceName; + /** + * The tensors that this model outputs. + */ + outputs?: Type[]; + [k: string]: unknown; +} +/** + * The element type and dimensions for a particular tensor. + */ +export interface Type { + dimensions?: number[]; + type: string; + [k: string]: unknown; +} +/** + * A stage which executes a procedural block. + */ +export interface ProcBlockStage { + args?: { + [k: string]: Argument; + }; + inputs?: Input[]; + outputs?: Type[]; + /** + * A [`Path`] that Rune can use to locate the proc block. + */ + "proc-block": string; + [k: string]: unknown; +} +/** + * A stage which reads inputs from the runtime. + */ +export interface CapabilityStage { + args?: { + [k: string]: Argument; + }; + /** + * What type of capability to use ("IMAGE", "SOUND", etc.). + */ + capability: string; + outputs?: Type[]; + [k: string]: unknown; +} +/** + * A stage which passes outputs back to the runtime. + */ +export interface OutStage { + args?: { + [k: string]: Argument; + }; + inputs?: Input[]; + /** + * The type of output (e.g. "SERIAL"). + */ + out: string; + [k: string]: unknown; +} +/** + * The declaration for a resource, typically something like a wordlist or environment variable. + */ +export interface ResourceDeclaration { + /** + * A resource who's default value is specified inline. + */ + inline?: string | null; + /** + * A resource who's default value is meant to be loaded from a file. + */ + path?: string | null; + type?: ResourceType & string; +} diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index 7e2bf6cf9c..4d4617fac1 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -1,9 +1,104 @@ -import { Node, Runtime as RuntimeInterface } from "./loader"; +import type { Tensor } from "."; +import { Runtime as RuntimeInterface } from "./loader"; +import { Pipeline } from "./loader/Pipeline"; +import { StructuredLogger, Logger } from "./logging"; +import { TensorDescriptor } from "./proc_blocks"; -export class Runtime implements RuntimeInterface { - constructor(private graph: Record) {} +type TensorId = number; +type NodeId = number; - infer(): Promise { - throw new Error("Method not implemented."); +class Runtime implements RuntimeInterface { + private tensors: Record = {}; + + constructor(private pipeline: Pipeline, private logger: StructuredLogger) {} + + public async infer(): Promise { + const span = this.logger.span("infer"); + const start = Date.now(); + span.info("Started evaluating the Rune"); + + for (const id of this.pipeline.evaluationOrder) { + const start = Date.now(); + await this.evaluate(id); + + span.debug("Evaluated node", { + durationMs: Date.now() - start, + name: this.pipeline.nodeInfo[id].name, + }); + } + + span.debug("Evaluation complete", { durationMs: Date.now() - start }); + } + + public inputs(): Record { + const entries = this.pipeline.inputs + .map((id) => this.pipeline.nodeInfo[id]) + .map((info) => { + const [id] = Object.values(info.inputs); + return [info.name, this.pipeline.tensors[id]]; + }); + + return Object.fromEntries(entries); } + + public setInput(name: string, tensor: Tensor) { + const node = Object.values(this.pipeline.nodeInfo).find( + (n) => n.name == name + ); + + if (!node) { + this.logger.error("Trying to set an input on an unknown node", { name }); + return; + } + + const entries = Object.entries(node.inputs); + + if (entries.length != 1) { + this.logger.error( + "Unable to set the input for a node with multiple input tensors", + { + name, + inputTensors: entries.length, + } + ); + return; + } + + const [_, id] = entries[0]; + this.tensors[id] = tensor; + } + + private async evaluate(id: NodeId) { + const node = this.pipeline.nodes[id]; + const info = this.pipeline.nodeInfo[id]; + + const inputs = this.getTensorsById(info.inputs); + const outputs = await node.infer(inputs, info.args); + this.tensors = { ...this.tensors, ...outputs }; + } + + private getTensorsById( + ids: Record + ): Record { + const tensors: Record = {}; + + for (const [name, id] of Object.entries(ids)) { + if (id in this.tensors) { + tensors[id] = this.tensors[id]; + } else { + throw new Error( + `Tried to look up a non-existent tensor with ID ${id} ("${name}")` + ); + } + } + + return tensors; + } +} + +export function createRuntime( + pipeline: Pipeline, + logger: Logger +): RuntimeInterface { + return new Runtime(pipeline, new StructuredLogger(logger, "Runtime")); } diff --git a/bindings/web/rune/src/__fixtures__/sine.zip b/bindings/web/rune/src/__fixtures__/sine.zip new file mode 100644 index 0000000000000000000000000000000000000000..739c86a28a523d3074d30aaa482ab305e0d7496c GIT binary patch literal 20590 zcma&MW2`Pb)c$#I+qP}nwr!qm+qP}nwr%Td+dkWz=Y40AN&cA+({!~>(_HO`mG!F> zWk5io0soU0sT)-PxAFfJ5CB|&t-Yz4jWdIqDii<&%RbQff6Ubb1^@(l0RjMkUjA1o z{wIJ600uw;;D~ZnWe#;6-ogLt{ZA9-{}phyv@`o(5$-1|C7*fIKA-x|%)JP!nteAUA5l{;O>AE6p=4KRI9Qq##K>NTm76_tFp zf6|M;n`aifJlooUF;CL@Q8YYV-jRtmJrM(={17ci{3v9JgFN!t+T zXZGvHPyrL614P*0tbjkueMPI>1`e)vgJ!iV6+Ax}IlP=P!)&*Vs~d289cD*!u~eFu zmOw=TDsn|!(B>_IlIak5LJxbN3)}PXZ6i0sE8VG2#p###JP-2Pd7RP)vi4(o1{%XB zSvVei|KQ2JQj8zsk;MscVYSc%SbVMEfYX_^g}#T#H;?d~E%%Cqxm_HH8bCY~dVNz8$s!im7Lqe1 z`=?ygAsvKQ<6F7Gx<2kko6`-(wgff`j7R$~vR}9E>*bVf_`m4+-lXYF{K7}8c0P?3 zJEhN;L>tWfF68cd@rJD^q4b@mgAlq)3b@{|L;TZWv?OR9EYq<(+WX@tvHU#lG*QrX zE5I?PiF0%}Aa5LCnn3!@c|-UxZ|KCwt?_0S9ywhsX$OKgEnrj04E>jk9ORn@mA4JScU~T3#_9teUL&jt?+;6$Jefr^8 z{3FAz$K9S*v)}KJdZaC4|Arrlbw0(V`8QyNM-!Q3s;LYhL(gqDYJ_KDuq1Acxqdnb~q6tNMu*-uLyDp z_Trdh%$3GVGKfe#DQU905oBp(vnkD=F)dBPPpX>IjXZ5gb?xjL32ZC`RJD)T+?S~o zaNd)Jn7<8i{6RE5r)gCGU^;j;&dh6!S7fmY{w)bNRIh1X`MVU3mA4gNUSB^pMGeMF z2{X_TAm0_l6-8G?7%^2pVjix$`5FmL%f_tRP>iF&t$MVk2nRjIHWSz&^TgM$Knmn| z$MnmE46uR_U?-k}l3(8rW%oDO{BBjtPVkhXbRtGTS7<%LalmV_ z4CY)}L@>vxHj?TE$?p3jzkKLJvF5jpZ`$Ubap;)#0FQCF!f=z&d}=G}?+iNZ=DY3T zQUh7!ZNKpE)gOEqD$iq>Dx7|cE!(&BAKBy*HNsc3360%mVd(QuIFr+r`~t5QLMoEI zQMerfxPF~k-I|6-hYm*0gDk}Aq;O032B7g{&-Wx&hs5RjEZ6Yio}!~&ll6v9(4YMV zM~9qwe`N8ff%grGJXJ$sd}Nxse0;8nO_AgiBZuvvN+v2f?UvH`4kGs8=NKXI^1}9O zdDX9yy&4XtI(nhrAMsm(TS}y;PD%u)Pi+2@;eLizqRnn!uJYYO;Gy;`LI7r>eo%*N zWKMr#4dpIt)bl260In_&>e9u`sY>}kD%0_Y?`AZvpNr>g8U@q9xqdt0JT;3}qnwnygpw+6`c1amlinKT#~eBP*T#=P z7&rd~bsRTWs5%rXT#%z!J^zp8i3hPOcr7qWUTYUt>&I8Uwiqw{qi!4dm-bGKc5i;E z+PHNq>p@0_R`si_w#Fr#c0S9&d$yl=?e1^9I{i4*5ZcJ=YPAo19Yx1aEBgh-FxDQP z1@Dy`sL#|awX3mqRit0@o|o!CGuDSS(_S?34=z+$f7rVfSUIUH&ogTLga3YBSb{r{ zjtCU{HrEHk|IqXQ8^5ss(bK`n-o(Jz#@@vG|1X30|AV-6?=o7l7yxkC1OOoaA1eO= zorRt8e^Jc(AIty8{tpz7==jFyuDbRU+*zN_X8XdBZ)hR9m?mrk>+KPJIqtI!%ImcS_S4l#^aT~u{}D%iwO#&9 zP!@Qti)~YopO?Mc<$oVon^d^{S@7{TIvsq!xvAwj{bSyKYw1qmF6%}STGym!V0P73 zj$wYfUO&=nPWW24v%KRs*GPRf_dfYo@2Y)X)_n_s;i$K6if1)1-%dc0as7-S=k|7Y zS-<~PqqxQdhh4Uv09n4$N=~VD;kIsXQGbKK-@C2b+g!VYtZh}T?fvC?X6W{I=Bu1q zm!Qh&ChPBVUhekay89S(-eexPnmhU18dU?m+Fd@rPeY9e+*_Pr4T}q-K+Dl&~;iBMJpXNXD&-_i#VUbyw2sY8&D^wlN+n3070)BJ8?frgZFGTTzR5v3HU$4iB-AUPQgTBJ=;;U-(nszqGst_){`vk>rPmkB|k7 z#Nt;=ZM(>gHn-2UQxJFw(9&yhUs%RCiX_LV)I)sgJTRyab4zBxV5Pc$? z596;(_0)0iohqCT)2~A1(s93)Dm)u%P>o8xH7{LwJS0Yqdm>C3)gQ+|XQYblt|=oP zB9ORrb+VaQ853_LcW4t8l9){#YvxRcLEtXIj7Eo0N_;e*sI>pt*O?a2_91_SS)U9l zzF!R9B_AC61Z-6UG6GH{n=lnuIan{0+V`NmD%I0XVu7(`F}OTh=P`DH=<&T5La02H zF|xyGl{<7lcDOg5*UDQH=!aik=v9b(P_JPlrbB0;IH#e=bwmvij-!FV7Zg(a8t0w* zEWO0)RF6czM8A?Widtz==c3C6uQW-8@Je2wiiH!}YamM0ACfnT6ofJ+jWIYZyAY|K zF+hp{j))Xh@BLE2owTxWsx2~r|A9WNXa2;afkooCxvwycv0RWs;EQ?*JS9BRc-xY+ z`Pyxd%lRH2&-={>>Ae@XfZ`)})Tk3B+ zxGfEO9bISoK8`La`-JE1n;#fTgOFm7JC`gdQvUR)>U!I4HweG)BO|1;e*&J%Mr#V{7(&Cct(k0oi zn>4F+5U(K>s**%_$TP+<4S_{diBm&E z;v6!Y4d)bwkbtt3u7VQ8w&4osX(ztYDsZCQ^As=}SfJz$#r4*w2bM<<%OQrH{B37c zm74_bkEZb!C@k5M!5|PZ^7|m=>wR-vikB3Sc#=4z;7pY@Xn0CaI;{^9eB&MzuQ5tuR<`{a_>(bm=;2{C%ZOErs#9Fj=T_d!2+7yfl3 z4aJSB%4)C+zP{tqR;ZDhKVbQZnuQ9yur&=r(v&OFw|w$9lrWLV<}O9@UA$Cr2L8l! z=a@buhLX?RO`!NNJmY*;tfN+z_gc2*snn$Sfye35GZc~qW556dLd&6o4)9_sCX~9f z8S!~4>eEX#715rtPNwz8J;*aXZJT@cax$nb=P$-@WI4uCl1)r1c9iN253hYz|G0w& za&Q7@o~AQrmbyJ9xH&-USH~4Zn+#}I!!K3O!zIL+XVi)hi#v~|$;M&HQfpvAGjf^J zkMNG{+Y_{7cAz)+(?er6_f!72gr5gya|xwI!~jtf)({|sv~tZnklI=d0g)Y>{k1jd zr4`Or@Fg+^p8`Etta@l0jG~<1S<(%QJjTI5^#o<`CUrhB?#iPp;@+e~LUnIBHHcKf zpN%V0zp68BxUaCop7h(Rk`H~z8&^#Q<+$4{FPfk~C@Rv*e4x(~r)AnCh-1DrA#7DJ zX1#HdhCr5=V|S!-HoV|Y#8`O2Viu;xEZk`w6ALAW7O)iI@IITYO6nkBQlhI&>NGWB z3gZ=sRYqi6Evs}jmcn{8%p?O>1BTp{MXq%S!xn6n6blF$e`HNmP?i4GlDoD~`aVy` zzHehXE1enoz~M@gAjg_-)Sn4)v*TMKyXpqV4d80fT)p!xKFq4&fpnHunV4EAJNC!f z&$-I3eslM6tvA;88RKzSjHvOMRQq)f{)bEO^S{*Ve>pVTV8@+N$bh7>NaarUS%AIj5^^l?#CiW^|aBl_h+JW-o z7bLl$IUWob4FRa;$OF!d2}nR((6;Q(gxR3SwaBO-{p3lTnAFgyDj*3Jm`V|k++&Q? zYCNe{sT$73RCZ`et!kdJYPTZC0Y))H6bphZ!A(+%zyS%XLkl?sh3FKOj7`YU3FNPx zik;aAZaB4m^rUh!!2$Fuk)#T$& zd_aJdaKK8ogO=!!DEQf~qYZ3AfE4h+3f6-bpd15+ zWOvRXl;&E+X(Qx;h=Cz<`U%es$(u{>O`nK!fJHZ1X5)vf4ylihjxKGbOkmR#r&z~k z)J`vJ1-HS(Hjq%oz6yHBG?RS8Fj=qL>HV+L`p7~#WVHdwqp^QhGq8#`6^)Lpyk;%QXL=#Q^f4_;Mt?Oo0MH*EdP z^@Jb(sjJ}7)$eT@=#74GdANtZK|dUDh|YlC^GmqP0Xp&-7VNR-fOyw!jJSg)*kgbH zg|YCZ0k-sM7@#^FP?H*vVs{(K?xP++8#-@j__i`Ud^X-9sg6ZJu)b{oonJk}c0+Wq z>=(w#aosNVHq(EDkpIvPmA{IQcEDoaD&S)*`sv`APNLaXO}rLK^36jj@%gG5hX&RG z<6O-d3FdCvP2CabjOso#Ap}l8qNcq^D8Bz-?Y1Rqo6>$nr0}tFPxSpPXzCPB}T6@`7N>y=i=xf-d7J2nO*332=XMnj^ja+l)-xAqC=jVhvdfL7vjEookw~TWOheDzgwTyB+)(h z=b{d1pAk83fxkuR!M!urnhrrfiOQlM){svA$^eSptIuNrsy zk3waHTG|-ukEC=e9&6SYHK1xRK!ciuWBH7BS;~xKFmGsnM3Z@w$nVbhMd6Opkd9o8e9( z8#nvSKl5F|(E;T4bw3oAKTv*@LKD=pt!NDeE>K^5vm$Nmh>^SAF@a#Xe1wM^8A0Nr zB%+{XVPR>ZQ?A9??*3T+WZauONsHz_wdKrzgWNFoPQDFdii0EXHzLa0$-q%aB<+p{ z=*XXh`k}u`Y0;CY%LCCrpI1l zrO~cg+s41PD+0*^W#`)Q;@U<@2w@3lAXqI*>IpO#{V34C!Tk?$nc+u<2+NgxY5(7Y zVNbTvbOH-zrJbiGsGj^}9kMr%F9gcQX^F)O9J%4xy^+H=h?41n0#lH9Dvxr$allx- zJc_7~ThzEOc5imvXTUUv%Q|JRpZ(>G>zgbOlKT<33FF$(l_+w^VI;vve|+zQ4r(`U zj^sm)7Ir(%FCRg^w(E>hzmWD9Hu{}5J1L&<42aDq_&$y+17z6kr?`EdOb^pJcMrD5 z;Mz|t!kh!NvjhGaN`TDXfREt)KHKKkN-vUI!q!;Un*k&P!BpOS4cHEiND?mAhJLvs z6!H-VbGr#=<#i~njr9c3z=11=FO0;N(R~901|`Q}H?};7ybCy5GaLHa$3+|xib<#^y+GT+ z&p{`B4B!VNAuh?&+^dJ;5y&Q!B+}xY25^xw*vt-#-9|aM;hG<+ZR(hljgrqeT1YZw z<`6Ed5jxAp-wZtb-yQxxfH#o9m=XlpHf26DEs9GrU|I5r$OnisHkP3m z%*7x!P#9$7qST~Q?`$0pcgs(hLyU5P!>}Mrsr2T@SDcUaj~tNW*c#>;>?wMkEEGo$ zlwF2Q%QJIJ)NPI!>EZ^QzaYmw>#p1IxZN@Psp-yy1Z?l(>aM2P=|}jQA|g=6yLh2) z*N6o9NosD9VqMjy0YK!jD^lYveB28vM&l6y;ajB8-{WmS#CH|}0s5wt9C=_JxPpc3 z*>mDBi_m^4SQO3|F&maymDdlnO{(8Qd5b*U! zC?-d`is#S!$g^$4d^04goXSYQ6w+NVXMYlcg+OuBP@QZh!I@%xvOT*cMg+wCU_Tzf zE~~vcN}VzT-UKT2btoERF|9y&#K1TiTm?zOS#pp3Kjixkq4F4_=PDcu6!)Q=Gf&by zoXXwK$57)DpfY@$)GvtDGt`7(C*UY5zsz_@SaE~}>helfPmNG6~C__QhwL)4Ru&l5op1J3tuTZPKQqo=} z1!7ruTPw1aJnnCbi)G$#ib=#|Ic*HCj=oaMLc`TCv5y2EnNfU-#GH|2mrydAK`qcS z9Jo}_adKYIyWhsy_Ym3l2C(NifMwSm+UP^o7di5eP#5K%a`aw7Q~b(Fc@g|lLSW$i z-cv}WS&|jXMp-IPS3&gRUbtD&qiygj7}=W5^!3H%aDc$eqBDeEhnq98g19mXMjq!- z; zS!xzCfK^9om0b}ujap`ERZ?ozwzOJ)sxq}=#K?(+*L#>_YfH)=2JU6!HUr>@GdVqj z*P}<-JMs>z3?dHYT{ZPRfDFO$Ch-XER3wde=6I3%pOFmz>~SOYzvVQau*qb#(?7JG zBgfHdr(`mD#*eb8@2agYEo2HT?@p<_)|1V$ecEIOjmI>yKMm4-=9B9gKE={`&Bt8i zdo@XCbe?&W(Q1!DWDJb&ZmB*rA_-bzsM*OCe z!?O6b%mi)6%d+^@%mTKPe=>Nr%>2fZWMum^&Geg&smSM5GZnNRS(6zwG8J?lX_M>K zGv~D*(a8NZ&HP4_>Y3ifQ{UNS@=P9QssC}wdJj+rZ zT85T>573zueSC*DFD~AJCq_bXa#fE14zbKa}sV+a2|StUg$jmWFE5 z6VO5f13U51zol02&A8ZeecB)C#7w;3pF2DqEgs*BS|T3hHi|20&~%E|jj~xpH;UOV zgex1byXjDFYo0-rA{SUs^MZ>U{$mL9#}(6{3pyQf;jVj(JEz`a*U*J|>hiuQ_o|U8 z=Zax-`tXTDuUbob#=l;|Q)z~-hQ4j``t+s>{!ZNKSrYRHMenZcgzl%eVCQxCS+DJm zLIt5c@by*}_3pFMW)GpY3yFCygTt~uILoIXMeMOHY}x&WeXl^V^qD1(iE5X~=0M-b za~kaTe4&GJm0BO}2>6qjp&^!Ue_(MRG%mJ{HCO!HScLK6T$=p`aAZz+`(4z*PvnAb z)NYGfr)U#b=|S)A0XAPro?jaPJ}B`M-)j#PUrmj%V_o~5^_OL`C57Eqy9)(IPG*Z# zE~^A6MDK>ArA6(SOftJ|nLU}^l6+a2A38UM`mUT37ttW3y3CRQS4FpRyN&lBOPul;IqCZbe8jha30v+XqsZN^$`6h6p{KA zCxL7azzv}$wihte>{$E`Zo17u_?Lt5s1K;p7XlJRoX%CR!K=Ti7 zfjtq;2Sh;|)&nkLApx@Xn^x5b=^};RM;v45%gHs!VxNKS2Cnpd)JrfB8*=5a za1rPml)+*>5go*LRpy2W7&}v7U&Q5d>>5J6o*h@!n~BF(EOj7871$c-ou^gTQmrK< zwGtF^XJ&cZ_phSL(()fOVdh>+){-z5b6H#>cM!jkvpFaCR*d1Tj;z7f=#%qnoRe=+ z^qh=Hi1=X(-$)E`oL($>W_Sv$L8=i#sbr`FQUv(&CE2ieI&F*kO`1iis^5BEXG)wh zt_Y`Dp;0ynq-9)HS!YB^Ww8AD)EZ_?AB%Lznk&5V3Qr=V-$_VbS)Z5jCXtBH?4G{oH;1YP)X$aG?u=Z{D)~c^WtFN9; zFY(A}T&nd|*(e_NLz}b?xhD@U#XuYNYx&DTtMe z5#ua~0}y7=IzVhu;HA)uo6(A#A+4fkGF(X8brMo zozre69vH78VJi|&t}1Uv_VFIQac~`j+AOxGdO^C%V&vrB6JN8`;0TjBIrmj9lo#yo zl3W5TvGZbejY_NQ#m=^nv|^RLP+CTc4q^x$W{inf!hZl0iA52h`hjCh zmJK6vo}M%G@~4Q;>T>QT?O>mk?G1PLj%a8l(qI<@up0U?HP1ey;!A;5%S-5Rxu$f? zQY*o*6zcb~fC2Xhr^MrPF&053P<(z?(FF>>>1zEca#YMbOUzW|W!?d=zaWhPEt+=N zBz_(S;ZzHV)g?rzVwa%xe$vhC{CD?BJDyt(jMcao)j&MO&fcQLM{n1EAd) zwQ?mJoQl3yaRh(#NxG3#Y-8Yd2+CR-xn5HT))`%lU=l1*i~X>Z>K33cab!8w6ek>^ zjEt~kNd4-jgkd(K8GP~P%k?ZD8bG89BdMWz1~wpa413YyxtL8vH{y_tOjk>3%KFPR zmTWOMD0-GZH_|7UNO*3d$o$tr-NUNHE3`wa$3LigZPLMf1dqHVxg%pol`^~BJ+|V8 zs-=$xl+`_QX67yT8tiS|Eop0B5c%3#qe3b2wMLsT5+hOt*Afa8bAvndm^kD;Y4xE5 z)&CA`Z>+9McP2iOGQFUJ$g$mVEJB-G70Ghq+90@vWS60b0<>+)CP&Wh%%Zu>kPBZo zj4_mN65=fKs<vk>8cvV71xSUD)aD74}{sw2c;>Z%Q>@5<98J?9zDB?_3PWWU` zp7JSYl@_dP&XL57lRtn2TWZ2fPzO!XGgGuxs8IGealx}oxQWFy%^x!YNI68XZ>IK% zF`~CCCO9st!kUIb^Scw0>sF4LHCQ+qQUA8Pu=Wgbdba0sVYiAgnb={{A>^=?H=E2P zguG!E#KW88bOAg$)TVyGc?s2|FTw^g3#~Yjz~PnJEkG6Y71P$}(vZfyNzL>5Fb70I z8UGOiP)LBt4*@85z=o%i0jaa1aM_bix#(ad-J$Ffqbesr4cktJqly!d`qbfx9CgYE zcug}|ww9hD$e;&(-)iRr5Ect;SFFvk-NO#F6)pse5Q=P^84Q5T8#`MtA!-K$%n7jW zJ9t*b7w(G9nLRU_ScwO=QC!fIHR*?c^r&p;pe43#y7;fyt5iYeyw{P zQ}JjJhj@y72R=ORfS{L7#}A_w1PY3w7?>^ySf8>S3CjblP3R{Ra7X7-pUfOf%2Dk) znTTZ1pmUHON9xec=7G|drwZZ=q!p+b@h$!3Lnxg&3+C9-Pmi> zE`(g8apT{UwUuEZ2VWHxxKAQNiCYGAlz*`k?E}0?bu9LYBlXJ$j)8ebNLNoVe*k0h zVgdDMM(m#zkX^_xJP-ZgYxYVReJZ{OmNVw^S^DsQHFxM6HLuH!@AldD#)LXO4uJv?? z?$aYSN{0DBfHSLM?xB6AU=$_W)g63S@?db3GHdYjnIiX<602X*?twf#Eg6MmnVZZe z!VxsiKf5{ON3YU9eW5;g`L}=o0^!@}QDTNBVV_lqpzK(X z9uKu>2E-_wyGC>F;Rt7*L{0-Rb7qYKqW72b$l50!OxvjLs# zKIkPmx_dMdzND)o1D;F`c0D{VY#a&fWWK34VyQ4E2zAR$_Cp7g91A_b%G` zM{XNj2&Ug3P8|}zlQJ);Aw+5S>dqpl+9pJVN8s$!yV!5NlOqPZdUOVPeDZbg%_ zqpf?f>RUe#J!)spXiCxj8H|r`?!;_xQT~g8L~T5M49Z9Mbd0)IUkNY&J)6yMvxt__ zcKtjixb+V|`-SxTxu5?epO7y(kc(fNs^&t=-A@qqS81R~Arff8 zq^DwBqH9-dwEzKDadbfm4Wz?)Z>n2dv}>|T+{zLruyz$;>$Ad2(c=Fq1eRK98LQKG zLjYq|H2>ZMI*Ct7`vOBok4Zk57FNt$fg~MMzLfN+PekKL;RE$)L zm}DdeCxVXC4VF4UMMce7DB4g>hqTTh%I-Q8*MKO)va76VZ9e52v5=f^wfH_~n6e7; z&N3FLjLD>Qp~7g;!w2QxVG?Fjr|usvMS&w+^YTkvVRa`y;Gpg7TVgdy3{qc1Y780? zqguySB|@whZYR;Q7s5S(!U=R-*SZTs2nk|07~BD4SBSMT##4tHh!Ryo5y}ihFj?TV zLPGQC{v~5z0zps!D%`ko+GZT}w&-e4D1y>Wxnwq>OlN@RAe2+{PC?s*OV=y6bC8A^ zL>fiqGXAt;>OSnQlF5y?N#j0|zUvs6`qacih_-;qI%L&*UmO+XWEPa3c#(AZ!@P_~ zj?2bnoAo)aTyyushYHjA%o}3{uzQv>cABPkwr=TIZ6LD3b#8=3y*1AZM2^VoXIJpe zFWtA-BG$4D8)U7$0Zh4JGq!<=$=g-y=Cv#AsS}!YiC?WegnZidiY--^Wop+i5qM9m zV2OIQFb7w~M0QEZ{&RG3XjZU7N~!_pq6?EYr`#ZFXc4S<09QdMC>jJm;zm;9;*P-b zRS#JC!AOZhk?7k|(bcfHOMWDv!K>aH8vL16B}-_{AL3 z4U_Qtd}cw3IzzFLrS}zz5mZuQQn!Jf1t&C0;TU(=HWd)vS#QKDij*atv}T4$%5Tg< zNR))j;})An7D{eI&aN-=mrEE#yoJSz(-!eIb$qy2_(3xfwptFwsN0pgt_29mi><5; zO-!secJzG)(5U7QxG??uQdf+C*%(oEluIdi5|9X!6;$p3SOp=hB<;5HWm&gu;FT`v z_M-Yo0CC0<&WiklRGT@7I7U$POtRsQWkr;fO>0a~9h3bTbgLP=>-hQkmmx|?5nDIB zl{sCM031t+^kK*1L1iqc@YC-1>B`^debsRnMS3dtuxFnwMmXsq8HFkLy+(IcRbxqG zNc?AbK3t2uq3O>AHi$s<68eY3uVDfa{hO`J3KHZ2V9a1Ge9wYXnN*jN?@K(WB%~uE zVBP9DpLPg`@w15KO^moK9;f??7EcoY@(Fi42!lTO_H&(N-yED{JD?vSY9AwiIJ&-a z#x*1%N`RvEjzx15u}7%i+tgvl)x$CU*lLrOGUyBemk~VZ%s=PYgcU)*l?}t@teDz< zxj6gfN+P}=QpYKP0Ifdc>Wt$&n|OO$bAiL2VGc~7=fuRP{r3yLWj=;qjooXgO^6_I zX@W8uJ`pSE^hrn+SZas-1Fm4=tU*}i@*2*`d#$3`lI0NI!8 zJ`@y=n=~X)2NkN|Z;&4{Sh(y-&wod3a*v561GLPk&B0tHfpEKJaV_qQ84tcGgY)bg%I$H*p2BWBXUs^y!tMGya2(naBa6Th+7j$Z0mF5p%0nOSwHw=IrgA?t`ZlTurf12I;QHIQvmL^lY{l zEYpQd$}%nuz2o{1Qi->KlMEmz>7fOsmWGp`3dQy#JCrki`|#_`q^~Z~7x^?ah(fWg zfYZqT3KzkUp-a=J7!?i)kA_myvNkRp3$xI+G%Oqn)6ljwE}RPUNYaBVg=$uc>}Hgj zx)ovp>?Mh@?gNJ3^c^19KJhuggF)=SpKG=OrQu`YQa%~9{uu~SBHeEG6i8?{b7A?M zfib8HlTag-Is0=oQ>5%M_T`}1y{`DKa`b5>_)?%U2j19eovcyaZ{&#Yq)DNqR|@Al zk8p8Mt$(#m?ysUPQHth)2yz1&V^Q%ftf!}%S|<0vTD`9pv_tlw?$3RgdV_QnlAhP~ zQ_O#<#4A8wiGs}RPBRh1(Q6LiL^#H@^Z6sme7TcMm#D%kyhqzzacT7#v_30eHaviK z_>k|c43I-`dj($8Zxlv9QACERBbr1^1!$znf0|>8M4gqeJsI(-GR<%iyY_AM?%g!+ z(*y}OC<%?$bXlFeWD&-g_$Cww2rb2~Wm*>4(D_>S50W*Uw_yc@KQ0o0!IUAjC4Hee z2^%NI6>!D9bEQMr`!D|Ko@Y^Jew^pP$Zfr9#V22HCtg2}!0@sMI4vw~dab6iQyrjz z;TIJnrPFELy)c_9a?6Iu44Xsbp+qWi=7HitMQCs-XgJEK&Ecq@Y$vMC;i;QRXrWPZ zhUXN+$JE6gd^k>DT6n((QB}x z3I*8$+nZQ6g45MMZnt&9R-_2y=1;R(nr+4igPY}CQ6-v}kQ~ysIcSwx+wrP|^KH|o zv&vv)6_uS_x3GXyHEfz&34PHXvpGf%ARE9jN7@tQ$sNT#Q&Z%86{n@}sQh|C3clRc zrS?RY<~J}_D&PjNK-2ezX`(ve+q&q8UqkNNlYCZ+o%Y2dwQI|1z&W^5>&XRIr@Dl? zb&;`7Aw%G}8hTGdxT##EQCL1w%Hf~*=KE*B>Gji*J8ovLHwgie@Lbtg>hLn{%> z0r%iXdX+n-+hI`RUuJ9cR~P1C!?~J)E+B!3X1^Y-kAyrZK8A9qr;`hb|5zYv6gbBp z>n^n`E}8XWx57tmjI)0!zKOvFiW)G#gZ8f^WndnZ6V0=(dL~{*NV{4H5>|R{8yV8R z@UsY$YLIy&#LdTs;K1%RK0Od5w@6_VlW8~zzi$t@Bp3SpKu?~LOP>OQwE-KFm6(<7 zF;Z(E(^=$$0h?Z03SJ{yxPhY7WXK>qO|M2T5J~lFnLf3YmEmdiXei#AVsr*ulm^QR zW%Pk3+I{L94U=QTpi}FFJH*5L&Ro5-xoKB}~-&u;4ihw*N4gCY!PZ-;N3< zSj_(^1$hu)*E7ihDLtVMj}%*IuRQepGdsa#hF-xz>VJ)g+#Ys{XEw`q(J2Q?fS12o<|nH4W4oC*22 z1Z|>1@lrTtpET@rBV!A^cr3w;M!x^0py1bKb&aKT3q~`>zE8DOi-V@POAoPIN16LII^TuT#x= zGv;7mAqSppoJ4=8VTFYqH1n3EIbp(~3@4kD?H(A50QVt(ukAv`ei0>BCiZ>V&mDVEn#A})C+W;cf4N!a#!#TFOT zBgcWp7LdmfCO z5|M;6EzW%K2k4?a{bPPbhb%YK(vy*tCI93aCgAj(xOI4#?!T|3YQ?A}W^3V#UZN+0 zAtMVA>b}BS-BT_h%(!16oL2KbX}E8?_i0GJtz#8UC}~i{`a>Xb<`Dj?Kq#6wt~N@G zRIrAs?9T&^c|+@&Q;S0fD?=<%_JgdTO$~6xa;Re26^w64^A>AGP-r4|HA$Lbb<5C4 z|R*eT0RpchYtZl<@QqqrWN>LQjZTg7mFZqBvB_e zzH7>B@{ag;fPIv@`>*SXb2*CfjYKl9NTaHSS2R1a0Br6`+nbWLaJyQ=Gh%3D#oo6I z4CBTOo}kQ}Lz{d6U6BjZFG4PlVutNNfd?a0QlcMNJ4cILCCWJ{#z&LhyiUNlHK=pB z(Hsnq$D=e}J7P|HwQ>Nb8g3vEH^^y*V4x2Wa(N4e{^JqpOHPnq@`gR0-?$Aziz1oo zb-rrq?xVE*D7X_@k1f1|M44qvLr$Lf^86$9l4Z(qvfyuU0sA1Kgl2&b z;7i4)h>}KUjzWsdH_(V>OCRVF@64Z8R`ENMEN z6pxKlwPPIeq^TTp6IMpf4db2Tl}L|z!T8Vdys{I#1XAqEKpD` z;zV?=??063*KzhlMb_~P{sV*P`=@#|N%_~r#~HK2AIIfWX3Au2SWu%Vv)Jia#%TWK z9M2<5ldL<8!x*7{N+;9mdW9{^;azi-G~N!eEfECXG%VV9?8r*)?)J5VVC(l{t5Lq& zQ-5pt>{?vXvT@jW9hQgp1+L3qRh^qxow@U9TjbBu4}(XbtrN%Y;zhW zC8M;wF)622z!&x+QHutZ{yw^|HsUw7xKxlzsi+&cglc2Pcz!(zOF@h$TMTo37}Bd1 zW@-<%_;$6C>Z4W2ZVdN9>K3jY<|rb18DJ3yrm`r}M zX@CQGcDnN!!*Mv_?xtx`%Y|X!V9(Z?aJqe?MVOy{GX~{mT%ebReHBk zs<=!oj>T~)hFHqa9}nAjctXXyiZka0R*HjhL8+|D1(wycgU3v4eYNxUcaF_I8e=Tz zl zS%VI$Zmunjw~fC#O}V)gXlw2%XtY+=blrE@ZvIORpJH^*(^m-H0+w(8PTL;OhHty4 z!kv{R6!zL~<@m0?Z1&b>f4cGpTi^3$`FwYLLvO#rW_Px5ApTk7a6H#!CKG^0z9EsA z!~_~({3r^$p-2V)Kvl|4yM_3fCq3C27D8(Y$$mec$o@xeR^(vLXQQW3%2+^v|)|3+yNC9~3C`_Fm38J>{Z{ z_WD?R4~^9#@N3@s{&{w3h8j!2qy4`Mx$|%+^tKP+mNQByM@h1FB$2UZ4b$$(GG#(y zNRzRQF*GsRW|X6dEGc0al{I4Q+h7PIG|4t2#x9d(>}z9Yp3Zfi=e^#i_dL&g{r>s= z{`h^b`@XLGzx%pA+m1qQ=}#lyCpUkhP{d3lpE}V$Iu4U-{b4w_mW4$E1-QelET{X- zxemun{qX>Dfv?b~(E|w{OD+Q7kA-*J_q4=|E!kkEG67UbGld`~Q;JIqf6ENDb=^v{ zb|imZLVY-#KO~g0b{U7nxgH}mgcr@d-N`w}Z=#>U!VsbO5!)l{^y-frB3~Mtxx3@) zxG35hvPN{e3msZ}NP`>TZDCI2(1?D3w!H zNl6@~G>1g=K%&57Laj{*J4%Wu3n-ACv5!2#xm^Pi#s1m_IofU#CO4@Um$i?p^S$w= zYx}&SZ~W8B2e`+c^`?io@0*+NeW&*>ua0;W4JAWGBAkZB{8*~#tSwa63>`HG5}6~O z4kA~inbTHS z7yu+)(dGQ`FZfU%{5OwnT3We?KB_x*b@gZ%%F`s%aA7CgZL3mf%K*Tg z`KTT1ZvV)$Bjg4GcJm#%Wde*cZwecHhVX-l2I?q(*xWIVXe7fC$1b0E)v8ros&E=ViTgI*B*U!K3H)+ z7Z@6+SP_ve|)B_9WUb z>9vw_S=QFpRBJT;X4*~sj9#T)r9r@HD$3W}*OxHN=w%F<5NnHS8)~zHxQyIdDw_ET znL2EY8D&(}ve0K{KB2t4y?j%LdwY9_bci`cISn~k?pOo_fu3j3l4&$rP!Y0-RU}um z#?+%C2nHCJu6kO~+Ds)CkxAD^VVxjO=wU`xauuyA2#G|pkaEbiVf+w&m|0a_QC-!I zKp{}5CC|*Up8g(&c6C-xRwc~E+{rw|+`N5vN2_(`GbhwD*sNY(;YlZb=J#zp09OuU zs|mD^=gPmjhA+p{eLo6XX#^ah6@ym+K;tw=%;x07X57K_Ii%wW%m8^mti>R!K7< zz;O(v*t?fC74?3_M2!5q?&u@eY2iSf3wJFCH-9$)PRkf<5ib}s(x$`Mb-B?>bH0Gf zG_r<>Z1tXe6TiF3Z|z$`9wSEei~{4=tDZ(>m8lOW$FYo~wL3aSTl=%`<*GrE@czNl z$Hyr#!tkdl`6rAjZNJP$xi#~?2q4lSU*zV!Xd8`X-=!i0L7IymWS)z zFI4Ni^x%#h2ODfvLGWxDxzQ+ZWl-0wbgpgWB@{a_;9A3!8QUFg0c}s<>epBKTV!MGWv3HXot#f2OIj#y=m)mNdLZ~`^f~wbRcg{g9%@FZ2DS+!=WndW|Yy> z>XCpY!7<#zTV^q6acDlRIORK;5W3#9DyaY(@h{Zvlfkh`joeLG9JFM)v(Sc7vXQ7@ z!fovXg?wcRJ|Y)j>FAl_1Ek7aQ~0+}1(R~L{?@BSY2Wsh_xR|^Da#H^CpkNs(u_K; z%}n1pU)y=D=8SD_j)~KCId34wI|Cx?6%Qt!24%xfxy6+hLNaEaw>a$Jsf>v>^LJ28 zp~Yr#b-&lA?Tgm8oxPpaJXBZ~n_iz1X>X?0b=~(3EKqe+z7W(#h2xLn!B^Xy7+H3e z#zV7y@fCq`M<7%Baj8Xf1@i?lFVjfH8Qry0_&@nt;Wkj5<1*ru(oxAW5lk&YL=;oXISnBljcB@YL{nD^@;9Ft% znhW|H)7((COKmhwI(Xz)u9XbI1%8-Z=zC!^fU7?zp!pehb?08cr}^5 zokkxu@#Bc0kLDSq-EZOI4v`R+o@72M`z_6w9O*FXnMOHi&1i-1HYv7I5~kM;u<(crvR?hLJj@BBve1 z_@njixAB(QXq#S_NgJ@wMw}RY$lTF_tyEQ_)2ZfCHdCVPTXugwbwi{fJHsry&!+0f zL>(ey*Xs^KEG0TFi6CCqsN|9!rC;h$_4aol72HkQ3{^4e*Dt+K%qQT@c#k%m8YVHC zE>-#sq?IQsfxuDr26QbWbw+yg*N#gaLh}hwOeO*9{Ru;neQfB*l385L{A9)0?Y!FM zqer>-Sfk;?LS?Vo#GPW72E%KHb=!Hq*g#*6|~_% zCbh%zFK*3`+0EC9j98Vo-_bd)t1(apyN`R4oY=ySe?PWoVyTr&b$lLTaU%W?wCy(s zu|(01#wkl50LyKhx-8~tO{A@uae6F7TbQFr$< zwtctR0qXFhJXL) zTt{^8b!lC(msR$oVs23PHOnM>cRK+WIn|vPdT4!b%C}| za^F(n2N_OpjwL=bv$DJp({?m;Q7ueC!NzX zuRm!9limmS(mx5=1$&Mq@Fw-}%&4k4X|1ko??;E_{A1JL!dnr)DLWl3X=zo946kUD zf6cfcTyQ>C$}X-<{;vOT2gTp$Ye;A`p>8?r@6tMxoYgyogB|Zks^@R;C`}z7)+LW9 zH1#(b@_WjNWC^?!k}*skOi9@Ye?V>8FDgn=)P8j#JokF3c7zq+(W^Zl)GogQg$GCr zW|>+lMisRm-|urrx4h)8pH^vCsg)aC%R_te)->!8%zb)K+VQ>mk&-F5rIC(D2GL)n zC61bD{Yz!Fixdd(;@y+(%bcsAtZ@aX>TLIJIwhs7?W z+B!6OrPsf)R=v(BOm(nf;Yr!{tY7F{xp+fj*2aueCAWOT;GtWfWI`mpX8gr!v2$G_pdNMzU&8oru{SQFWK%#;NPfr|E)UF zf2$6+hwmrs=l?J9|588yyTrDi68{DN{K)+q^ylZ+|9!^(%H{vjboPU6YrdZ*?mzVo U_`v}3@d19Ev>#s^ZT@`y1AYA&_W%F@ literal 0 HcmV?d00001 diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index 187b88c6f1..b1ed33910b 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -1,13 +1,45 @@ -import { consoleLogger, RuneLoader } from "."; +import fs from "fs"; +import path from "path"; +import { consoleLogger, RuneLoader, Node, ElementType, Tensor } from "."; +import { Tensors } from "./proc_blocks"; describe("Integration Tests", () => { + const sine = new Uint8Array( + fs.readFileSync(path.join(__dirname, "__fixtures__", "sine.zip")) + ); + it("can load the sine Rune", async () => { const loader = new RuneLoader(); const runtime = await loader + .withModelHandler("tensorflow-lite", async () => new DummySineModel()) .withLogger(consoleLogger) - .load(new ArrayBuffer(0)); + .load(sine); runtime.infer(); }); }); + +class DummySineModel implements Node { + async graph(): Promise { + const tensor = { + elementType: ElementType.F32, + dimensions: { + tag: "fixed", + val: Uint32Array.from([1]), + }, + } as const; + + return { + inputs: [{ ...tensor, name: "input" }], + outputs: [{ ...tensor, name: "output" }], + }; + } + + infer( + inputs: Record, + args: Record, + ): Promise> { + throw new Error("Method not implemented."); + } +} diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index 9d851204da..906a0373ac 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -1,10 +1,10 @@ export * from "./loader"; -export * from "./proc_blocks"; +// export * from "./proc_blocks"; export { consoleLogger } from "./logging"; export type { Logger } from "./logging"; -import {runtime_v1} from "@hotg-ai/rune-wit-files"; +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; export type Tensor = runtime_v1.Tensor; -export type ElementType = runtime_v1.ElementType; +export const ElementType = runtime_v1.ElementType; export type Dimensions = runtime_v1.Dimensions; diff --git a/bindings/web/rune/src/loader/Pipeline.ts b/bindings/web/rune/src/loader/Pipeline.ts new file mode 100644 index 0000000000..bf82e58134 --- /dev/null +++ b/bindings/web/rune/src/loader/Pipeline.ts @@ -0,0 +1,25 @@ +import type { Node } from "."; +import { TensorDescriptor } from "../proc_blocks"; +import { DocumentV1 } from "../Runefile"; + +type NodeId = number; +type TensorId = number; + +export type Pipeline = { + nodes: Record; + nodeInfo: Record; + evaluationOrder: NodeId[]; + inputs: NodeId[]; + tensors: Record; +}; + +type NodeInfo = { + name: string; + args: Record; + inputs: Record; + outputs: Record; +}; + +export function determinePipeline(doc: DocumentV1): Pipeline { + throw new Error(); +} diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/loader/RuneLoader.ts index b066e9e254..aae31eee69 100644 --- a/bindings/web/rune/src/loader/RuneLoader.ts +++ b/bindings/web/rune/src/loader/RuneLoader.ts @@ -1,9 +1,23 @@ +import JSZip from "jszip"; +import yaml from "js-yaml"; import type { ModelHandler, Runtime } from "."; import { consoleLogger, Logger, StructuredLogger } from "../logging"; +import { + CapabilityStage, + DocumentV1, + ModelStage, + OutStage, + ProcBlockStage, + Stage, +} from "../Runefile"; +import { createRuntime } from "../Runtime"; +import { ProcBlock } from "../proc_blocks"; +import { determinePipeline } from "./Pipeline"; export class RuneLoader { - public static default: RuneLoader = new RuneLoader() - .withLogger(consoleLogger); + public static default: RuneLoader = new RuneLoader().withLogger( + consoleLogger + ); private modelHandlers: Record = {}; private logger: Logger = { log: () => {}, isEnabled: () => false }; @@ -15,7 +29,7 @@ export class RuneLoader { if (typeof logger == "function") { // As a convenience, we let people pass in a logging function if // they don't care about isEnabled(). - this.logger = { log: logger, isEnabled: m => m.level != "trace" }; + this.logger = { log: logger, isEnabled: (m) => m.level != "trace" }; } else { this.logger = logger; } @@ -34,11 +48,178 @@ export class RuneLoader { * * @param rune */ - public async load(rune: ArrayBuffer): Promise { + public async load(rune: Uint8Array): Promise { const log = new StructuredLogger(this.logger, RuneLoader.name); log.info("Loading the Rune", { bytes: rune.byteLength }); - throw new Error(); + const zip = new JSZip(); + await zip.loadAsync(rune); + + const f = zip.file("Runefile.yml"); + if (!f) { + throw new Error("No Runefile.yml found"); + } + const src = await f.async("string"); + const runefile = yaml.load(src); + + if (!isRunefile(runefile)) { + throw new Error("Invalid Runefile"); + } + + log.debug("Parsed the Runefile", { length: src.length }); + + const nodes = splitByStageType(runefile); + const procBlocks = await instantiateProcBlocks( + nodes.procBlock, + zip, + log.span("instantiate-proc-blocks") + ); + const models = await loadModels( + nodes.model, + zip, + log.span("instantiate-models"), + this.modelHandlers + ); + + const pipeline = determinePipeline(runefile); + + return createRuntime(pipeline, this.logger); + } +} + +function isRunefile(value?: any): value is DocumentV1 { + return value && value.version == "1" && value.pipeline && value.image; +} + +type Stages = { + capability: Record; + procBlock: Record; + model: Record; + out: Record; +}; + +function splitByStageType(runefile: DocumentV1): Stages { + const nodes: Stages = { capability: {}, procBlock: {}, model: {}, out: {} }; + + for (const [name, stage] of Object.entries(runefile.pipeline)) { + if (isProcBlockStage(stage)) { + nodes.procBlock[name] = stage; + } else if (isModelStage(stage)) { + nodes.model[name] = stage; + } else if (isCapabilityStage(stage)) { + nodes.capability[name] = stage; + } else if (isOutStage(stage)) { + nodes.out[name] = stage; + } } + + return nodes; +} + +async function instantiateProcBlocks( + stages: Record, + zip: JSZip, + log: StructuredLogger +): Promise> { + const start = Date.now(); + + const promises = Object.entries(stages).map(async ([name, stage]) => { + const filename = stage["proc-block"]; + log.debug("Reading proc-block", { name, filename }); + + const file = zip.file(filename); + + if (!file) { + throw new Error(`The Rune doesn't contain "${filename}"`); + } + + const data = await file.async("arraybuffer"); + return [name, await ProcBlock.load(data, log.backend)]; + }); + + const procBlocks = Object.fromEntries(await Promise.all(promises)); + + log.debug("Finished instantiating all proc-blocks", { + count: Object.keys(procBlocks).length, + durationMs: Date.now() - start, + }); + + return procBlocks; +} + +async function loadModels( + stages: Record, + zip: JSZip, + log: StructuredLogger, + modelHandlers: Record +): Promise> { + const start = Date.now(); + + const promises = Object.entries(stages).map(async ([name, stage]) => { + const format = stage.args?.["model-format"] || "tensorflow-lite"; + const filename = stage.model; + log.debug("Loading model", { name, format, filename }); + + const file = zip.file(filename); + + if (!file) { + throw new Error(`The Rune doesn't contain "${filename}"`); + } + + if (!(format in modelHandlers)) { + throw new Error( + `No handler was registered for the "${format}" model on the "${name}" node` + ); + } + + const handler = modelHandlers[format]; + + const data = await file.async("arraybuffer"); + const model = await handler(data, translateArgs(stage.args)); + + log.debug("Loaded model", { name, length: data.byteLength }); + + return [name, model]; + }); + + const models = Object.fromEntries(await Promise.all(promises)); + + log.debug("Finished instantiating all models", { + count: Object.keys(models).length, + durationMs: Date.now() - start, + }); + + return models; +} + +function isModelStage(stage: Stage): stage is ModelStage { + return "model" in stage; +} + +function isCapabilityStage(stage: Stage): stage is CapabilityStage { + return "capability" in stage; +} + +function isProcBlockStage(stage: Stage): stage is ProcBlockStage { + return "proc-block" in stage; +} + +function isOutStage(stage: Stage): stage is OutStage { + return "out" in stage; +} + +function translateArgs( + args?: Record +): Record { + if (!args) { + return {}; + } + + const entries = Object.entries(args).map(([key, value]) => [ + key, + value.toString(), + ]); + + return Object.fromEntries(entries); } diff --git a/bindings/web/rune/src/loader/index.ts b/bindings/web/rune/src/loader/index.ts index fcc8b4cf5d..d7a3793c68 100644 --- a/bindings/web/rune/src/loader/index.ts +++ b/bindings/web/rune/src/loader/index.ts @@ -1,27 +1,36 @@ import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +import type { Tensor } from ".."; +import type { TensorDescriptor, Tensors } from "../proc_blocks"; export { RuneLoader } from "./RuneLoader"; -export interface Model {} - /** * A callback that can load models. */ export type ModelHandler = ( model: ArrayBuffer, args: Record -) => Promise; +) => Promise; export interface Node { + graph(args: Record): Promise; infer( - inputs: Record + inputs: Record, + args: Record, ): Promise>; } -export type Pipeline = { - graph: Record; -}; - export interface Runtime { + /** + * Run the entire Rune pipeline. + */ infer(): Promise; + /** + * Get all named inputs. + */ + inputs(): Record; + /** + * Set an input tensor by name. + */ + setInput(name: string, tensor: Tensor): void; } diff --git a/bindings/web/rune/src/logging.ts b/bindings/web/rune/src/logging.ts index 801e77786a..2c61503843 100644 --- a/bindings/web/rune/src/logging.ts +++ b/bindings/web/rune/src/logging.ts @@ -30,20 +30,24 @@ export type LogMetadata = { }; /** - * An object that will receive log messages. + * A logger that receives structured events. */ export interface Logger { /** - * Log a message. + * Log an event. * * @param metadata Information about the log message and where it came from. * @param message The message. - * @param data Structured data. + * @param payload Additional, structured data that adds context to the log + * message. */ - log(metadata: LogMetadata, message: string, data: LogPayload): void; + log(metadata: LogMetadata, message: string, payload: LogPayload): void; /** * Check if a message *would* be logged based on its metadata. + * + * This is an optimisation that consumers can use to avoid doing expensive + * computations or logging large events that would just be thrown away. */ isEnabled(metadata: LogMetadata): boolean; } @@ -58,7 +62,7 @@ export class StructuredLogger { * @param backend The logging backend messages are sent to. * @param component The name of the component being logged. */ - constructor(private backend: Logger, public component: string) {} + constructor(public readonly backend: Logger, public component: string) {} trace(msg: string, payload?: LogPayload) { this.log("trace", msg, payload); @@ -87,7 +91,7 @@ export class StructuredLogger { /** * Enter a named span. */ - span(name: string): Span { + span(name: string): StructuredLogger { return new Span(this.backend, this.component, name); } @@ -109,25 +113,11 @@ export class StructuredLogger { } } -export class Span extends StructuredLogger { +class Span extends StructuredLogger { constructor(backend: Logger, component: string, public name: string) { super(backend, component); } - /** - * Time how long an operation takes. - */ - timeit(thunk: () => R, level: LogLevel = "debug"): R { - const start = Date.now(); - this.log(level, "started"); - - try { - return thunk(); - } finally { - this.log(level, "completed", { duration: Date.now() - start }); - } - } - protected metadata(level: LogLevel): LogMetadata { const meta = super.metadata(level); meta.span = this.name; @@ -142,7 +132,7 @@ export class Span extends StructuredLogger { export function consoleLogger( metadata: LogMetadata, message: string, - data: Record + payload: Record ): void { const { level } = metadata; @@ -153,6 +143,5 @@ export function consoleLogger( const logger = level == "fatal" ? fatal : console[level]; - logger(message, metadata, data); - throw new Error("Method not implemented."); + logger(message, { metadata, payload }); } diff --git a/bindings/web/rune/src/proc_blocks/ProcBlock.ts b/bindings/web/rune/src/proc_blocks/ProcBlock.ts index 516de2f934..25fbc5c787 100644 --- a/bindings/web/rune/src/proc_blocks/ProcBlock.ts +++ b/bindings/web/rune/src/proc_blocks/ProcBlock.ts @@ -1,22 +1,17 @@ import { proc_block_v1, runtime_v1 } from "@hotg-ai/rune-wit-files"; -import type { Metadata, TensorDescriptor } from "."; +import type { Metadata, Tensors } from "."; import { Logger, StructuredLogger } from "../logging"; import { GraphContext, HostFunctions, KernelContext } from "./HostFunctions"; type ProcBlockBuffer = Parameters[0]; -type Tensors = { - inputs: TensorDescriptor[]; - outputs: TensorDescriptor[]; -}; - /** * An executable proc-block. */ export class ProcBlock { private constructor( private hostFunctions: HostFunctions, - private instance: proc_block_v1.ProcBlockV1, + private instance: proc_block_v1.ProcBlockV1 ) {} static async load( diff --git a/bindings/web/rune/src/proc_blocks/index.ts b/bindings/web/rune/src/proc_blocks/index.ts index 061d61fe7d..9c217f80d5 100644 --- a/bindings/web/rune/src/proc_blocks/index.ts +++ b/bindings/web/rune/src/proc_blocks/index.ts @@ -74,3 +74,8 @@ export type SupportedShapes = { }; export type TensorHint = MediaHint | SupportedShapes; + +export type Tensors = { + inputs: TensorDescriptor[]; + outputs: TensorDescriptor[]; +}; diff --git a/bindings/web/rune/yarn.lock b/bindings/web/rune/yarn.lock index 97059451f9..39a529d973 100644 --- a/bindings/web/rune/yarn.lock +++ b/bindings/web/rune/yarn.lock @@ -1289,6 +1289,11 @@ jest-diff "^27.0.0" pretty-format "^27.0.0" +"@types/js-yaml@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + "@types/json-schema@^7.0.6": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -1746,6 +1751,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cosmiconfig@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" @@ -2294,6 +2304,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2323,7 +2338,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2392,6 +2407,11 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2951,6 +2971,16 @@ json5@^2.2.0, json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +jszip@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.9.1.tgz#784e87f328450d1e8151003a9c67733e2b901051" + integrity sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -2969,6 +2999,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -3305,6 +3342,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parcel@^2.4.1, parcel@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.5.0.tgz#b6f01c665b6085e4eb58957ff11bc8f4027bd8f7" @@ -3447,6 +3489,11 @@ pretty-format@^27.0.0, pretty-format@^27.4.6: ansi-styles "^5.0.0" react-is "^17.0.1" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3475,6 +3522,19 @@ react-refresh@^0.9.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== +readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + regenerator-runtime@^0.13.7: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -3528,7 +3588,7 @@ safe-buffer@^5.0.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.1: +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -3562,6 +3622,11 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -3653,6 +3718,13 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -3912,6 +3984,11 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + utility-types@^3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" From d58f12179db07789a62f677356c9e279765dc2fd Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Mon, 16 May 2022 07:58:04 +0800 Subject: [PATCH 04/18] Created a pipeline test --- bindings/web/rune/src/Runtime.ts | 2 +- bindings/web/rune/src/index.test.ts | 2 +- bindings/web/rune/src/loader/RuneLoader.ts | 5 +- bindings/web/rune/src/loader/index.ts | 2 +- bindings/web/rune/src/loader/pipeline.test.ts | 64 +++++++++++++++++++ .../src/loader/{Pipeline.ts => pipeline.ts} | 5 +- 6 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 bindings/web/rune/src/loader/pipeline.test.ts rename bindings/web/rune/src/loader/{Pipeline.ts => pipeline.ts} (71%) diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index 4d4617fac1..bcc8ac1866 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -1,6 +1,6 @@ import type { Tensor } from "."; import { Runtime as RuntimeInterface } from "./loader"; -import { Pipeline } from "./loader/Pipeline"; +import { Pipeline } from "./loader/pipeline"; import { StructuredLogger, Logger } from "./logging"; import { TensorDescriptor } from "./proc_blocks"; diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index b1ed33910b..dabde7c7af 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -38,7 +38,7 @@ class DummySineModel implements Node { infer( inputs: Record, - args: Record, + args: Record ): Promise> { throw new Error("Method not implemented."); } diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/loader/RuneLoader.ts index aae31eee69..33d52dd620 100644 --- a/bindings/web/rune/src/loader/RuneLoader.ts +++ b/bindings/web/rune/src/loader/RuneLoader.ts @@ -12,7 +12,7 @@ import { } from "../Runefile"; import { createRuntime } from "../Runtime"; import { ProcBlock } from "../proc_blocks"; -import { determinePipeline } from "./Pipeline"; +import { determinePipeline } from "./pipeline"; export class RuneLoader { public static default: RuneLoader = new RuneLoader().withLogger( @@ -62,6 +62,7 @@ export class RuneLoader { } const src = await f.async("string"); const runefile = yaml.load(src); + console.log(src); if (!isRunefile(runefile)) { throw new Error("Invalid Runefile"); @@ -82,7 +83,7 @@ export class RuneLoader { this.modelHandlers ); - const pipeline = determinePipeline(runefile); + const pipeline = determinePipeline(runefile, this.logger); return createRuntime(pipeline, this.logger); } diff --git a/bindings/web/rune/src/loader/index.ts b/bindings/web/rune/src/loader/index.ts index d7a3793c68..32c1fabe66 100644 --- a/bindings/web/rune/src/loader/index.ts +++ b/bindings/web/rune/src/loader/index.ts @@ -16,7 +16,7 @@ export interface Node { graph(args: Record): Promise; infer( inputs: Record, - args: Record, + args: Record ): Promise>; } diff --git a/bindings/web/rune/src/loader/pipeline.test.ts b/bindings/web/rune/src/loader/pipeline.test.ts new file mode 100644 index 0000000000..8055dff3f9 --- /dev/null +++ b/bindings/web/rune/src/loader/pipeline.test.ts @@ -0,0 +1,64 @@ +import yaml from "js-yaml"; +import { consoleLogger, Logger, StructuredLogger } from "../logging"; +import { DocumentV1 } from "../Runefile"; +import { determinePipeline } from "./pipeline"; + +describe("pipeline", () => { + const src = ` + version: 1 + image: runicos/base + pipeline: + rand: + capability: RAW + outputs: + - type: F32 + dimensions: + - 1 + - 1 + args: + length: "4" + mod360: + proc-block: proc_blocks/mod360 + inputs: + - rand + outputs: + - type: F32 + dimensions: + - 1 + - 1 + args: + modulus: "360" + sine: + model: models/sine + inputs: + - mod360 + outputs: + - type: F32 + dimensions: + - 1 + - 1 + serial: + out: serial + inputs: + - sine + resources: {}`; + const runefile = yaml.load(src) as DocumentV1; + + it("can determine the pipeline for sine", () => { + const logger = { log: consoleLogger, isEnabled: () => true }; + + const pipeline = determinePipeline(runefile, logger); + + // Note: we can't compare nodes for equality because they are objects + const { nodes, ...rest } = pipeline; + + expect(rest).toMatchObject({ + nodeInfo: { 1: { name: "asf" } }, + evaluationOrder: [42], + inputs: [42], + tensors: { + 91: { name: "asdf" }, + }, + }); + }); +}); diff --git a/bindings/web/rune/src/loader/Pipeline.ts b/bindings/web/rune/src/loader/pipeline.ts similarity index 71% rename from bindings/web/rune/src/loader/Pipeline.ts rename to bindings/web/rune/src/loader/pipeline.ts index bf82e58134..a40777fd1a 100644 --- a/bindings/web/rune/src/loader/Pipeline.ts +++ b/bindings/web/rune/src/loader/pipeline.ts @@ -1,4 +1,5 @@ import type { Node } from "."; +import { Logger, StructuredLogger } from "../logging"; import { TensorDescriptor } from "../proc_blocks"; import { DocumentV1 } from "../Runefile"; @@ -20,6 +21,8 @@ type NodeInfo = { outputs: Record; }; -export function determinePipeline(doc: DocumentV1): Pipeline { +export function determinePipeline(doc: DocumentV1, logBackend: Logger): Pipeline { + const logger = new StructuredLogger(logBackend, "determinePipeline"); + throw new Error(); } From fecb07aba74417b84035a3bf0ec3cab92065b129 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Mon, 16 May 2022 07:58:47 +0800 Subject: [PATCH 05/18] Updated GitHub Actions to test the new @hotg-ai/rune package --- .github/workflows/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 19396a4aaa..4d7af2b238 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: - name: Setup bazel uses: jwlawson/actions-setup-bazel@v1 with: - bazel-version: '3.7.2' + bazel-version: "3.7.2" - uses: maxim-lobanov/setup-xcode@v1 if: runner.os == 'macOS' with: @@ -148,7 +148,7 @@ jobs: - name: Setup Rust run: rustup show - name: check @hotg-ai/rune - run: yarn install && yarn build && yarn test + run: yarn install && yarn ci working-directory: bindings/web/rune - name: check @hotg-ai/rune-tfjs-v3 run: yarn install && yarn build && yarn test @@ -159,4 +159,3 @@ jobs: - name: check @hotg-ai/rune-tflite run: yarn install && yarn build working-directory: bindings/web/tflite - From 8a066bc0ae57b7122c7a31fe79b32c9780ad0f3f Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Thu, 19 May 2022 10:16:08 +0800 Subject: [PATCH 06/18] Made sure we handle proc-block errors appropriately --- .../web/rune/src/proc_blocks/ProcBlock.ts | 98 ++++++++++++++++--- 1 file changed, 85 insertions(+), 13 deletions(-) diff --git a/bindings/web/rune/src/proc_blocks/ProcBlock.ts b/bindings/web/rune/src/proc_blocks/ProcBlock.ts index 25fbc5c787..fbc58dcba7 100644 --- a/bindings/web/rune/src/proc_blocks/ProcBlock.ts +++ b/bindings/web/rune/src/proc_blocks/ProcBlock.ts @@ -3,7 +3,7 @@ import type { Metadata, Tensors } from "."; import { Logger, StructuredLogger } from "../logging"; import { GraphContext, HostFunctions, KernelContext } from "./HostFunctions"; -type ProcBlockBuffer = Parameters[0]; +type ProcBlockBinary = Parameters[0]; /** * An executable proc-block. @@ -14,33 +14,30 @@ export class ProcBlock { private instance: proc_block_v1.ProcBlockV1 ) {} - static async load( - procBlock: ProcBlockBuffer, - logger: Logger - ): Promise { + static async load(wasm: ProcBlockBinary, logger: Logger): Promise { const log = new StructuredLogger(logger, "ProcBlock"); const span = log.span("load"); span.info("Loading the proc-block"); const start = Date.now(); - const wrapper = new proc_block_v1.ProcBlockV1(); + const procBlock = new proc_block_v1.ProcBlockV1(); const imports: any = {}; const hostFunctions = new HostFunctions(logger); runtime_v1.addRuntimeV1ToImports( imports, hostFunctions, - (name) => wrapper.instance.exports[name] + (name) => procBlock.instance.exports[name] ); - await wrapper.instantiate(procBlock, imports); + await procBlock.instantiate(wasm, imports); span.debug("Finished loading the proc-block", { durationMs: Date.now() - start, }); - return new ProcBlock(hostFunctions, wrapper); + return new ProcBlock(hostFunctions, procBlock); } /** @@ -59,7 +56,12 @@ export class ProcBlock { graph(args: Record): Tensors { const ctx = new GraphContext(args); this.hostFunctions.graph = ctx; - this.instance.graph(""); + const result = this.instance.graph(""); + + if (result.tag == "err") { + handleGraphError(result.val); + } + const { inputs, outputs } = ctx; return { inputs, outputs }; } @@ -72,12 +74,82 @@ export class ProcBlock { * @returns */ evaluate( - args: Record, - inputs: Record + inputs: Record, + args: Record ): Record { const ctx = new KernelContext(args, inputs); this.hostFunctions.kernel = ctx; - this.instance.kernel(""); + + const result = this.instance.kernel(""); + + if (result.tag == "err") { + handleKernelError(result.val); + } + return ctx.outputs; } } + +function handleGraphError(err: proc_block_v1.GraphError): never { + switch (err.tag) { + case "invalid-argument": + const { name, reason } = err.val; + handleInvalidArgument(name, reason); + + case "missing-context": + throw new Error("The proc-block couldn't access the context object"); + + case "other": + throw new Error(err.val); + } +} + +function handleKernelError(err: proc_block_v1.KernelError): never { + switch (err.tag) { + case "invalid-input": + const { name, reason } = err.val; + handleInvalidInput(name, reason); + + default: + handleGraphError(err); + } +} + +function handleInvalidInput( + name: string, + reason: proc_block_v1.BadInputReason +): never { + switch (reason.tag) { + case "invalid-value": + throw new Error( + `The "${name}" input had an invalid value: ${reason.val}` + ); + + case "unsupported-shape": + throw new Error(`The "${name}" input had a the wrong shape`); + + case "not-found": + throw new Error(`The "${name}" input wasn't set`); + + case "other": + throw new Error(`The "${name}" input was invalid: ${reason.val}`); + } +} + +function handleInvalidArgument( + name: string, + reason: proc_block_v1.BadArgumentReason +): never { + switch (reason.tag) { + case "invalid-value": + throw new Error( + `The "${name}" argument had an invalid value: ${reason.val}` + ); + + case "not-found": + throw new Error(`The "${name}" argument wasn't set`); + + case "other": + throw new Error(`The "${name}" argument was invalid: ${reason.val}`); + } +} From e8aefccdcf066bbebf31805c589106307988cbab Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Thu, 19 May 2022 11:55:39 +0800 Subject: [PATCH 07/18] Implementing determinePipeline() --- bindings/web/rune/src/Runtime.ts | 21 +- bindings/web/rune/src/index.test.ts | 5 +- bindings/web/rune/src/index.ts | 1 + bindings/web/rune/src/loader/RuneLoader.ts | 9 +- bindings/web/rune/src/loader/pipeline.test.ts | 48 ++++- bindings/web/rune/src/loader/pipeline.ts | 193 +++++++++++++++++- 6 files changed, 245 insertions(+), 32 deletions(-) diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index bcc8ac1866..238f108488 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -4,8 +4,8 @@ import { Pipeline } from "./loader/pipeline"; import { StructuredLogger, Logger } from "./logging"; import { TensorDescriptor } from "./proc_blocks"; +type NodeId = string; type TensorId = number; -type NodeId = number; class Runtime implements RuntimeInterface { private tensors: Record = {}; @@ -15,19 +15,19 @@ class Runtime implements RuntimeInterface { public async infer(): Promise { const span = this.logger.span("infer"); const start = Date.now(); - span.info("Started evaluating the Rune"); + span.info("Started running the Rune"); for (const id of this.pipeline.evaluationOrder) { + const { name } = this.pipeline.nodeInfo[id]; + span.debug("Executing node", { name, id }); const start = Date.now(); + await this.evaluate(id); - span.debug("Evaluated node", { - durationMs: Date.now() - start, - name: this.pipeline.nodeInfo[id].name, - }); + span.debug("Node executed", { durationMs: Date.now() - start, name }); } - span.debug("Evaluation complete", { durationMs: Date.now() - start }); + span.debug("Rune complete", { durationMs: Date.now() - start }); } public inputs(): Record { @@ -68,12 +68,17 @@ class Runtime implements RuntimeInterface { this.tensors[id] = tensor; } + public getOutputs(name: string): Tensor[] | undefined { + throw new Error("Not Implemented"); + } + private async evaluate(id: NodeId) { const node = this.pipeline.nodes[id]; const info = this.pipeline.nodeInfo[id]; - const inputs = this.getTensorsById(info.inputs); + const outputs = await node.infer(inputs, info.args); + this.tensors = { ...this.tensors, ...outputs }; } diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index dabde7c7af..4c1a8722aa 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import { consoleLogger, RuneLoader, Node, ElementType, Tensor } from "."; +import { RuneLoader, Node, ElementType, Tensor } from "."; import { Tensors } from "./proc_blocks"; describe("Integration Tests", () => { @@ -13,10 +13,9 @@ describe("Integration Tests", () => { const runtime = await loader .withModelHandler("tensorflow-lite", async () => new DummySineModel()) - .withLogger(consoleLogger) .load(sine); - runtime.infer(); + await runtime.infer(); }); }); diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index 906a0373ac..a25ccaea39 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -7,4 +7,5 @@ import { runtime_v1 } from "@hotg-ai/rune-wit-files"; export type Tensor = runtime_v1.Tensor; export const ElementType = runtime_v1.ElementType; +export type ElementType = runtime_v1.ElementType; export type Dimensions = runtime_v1.Dimensions; diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/loader/RuneLoader.ts index 33d52dd620..662296906a 100644 --- a/bindings/web/rune/src/loader/RuneLoader.ts +++ b/bindings/web/rune/src/loader/RuneLoader.ts @@ -13,6 +13,7 @@ import { import { createRuntime } from "../Runtime"; import { ProcBlock } from "../proc_blocks"; import { determinePipeline } from "./pipeline"; +import type { Node } from "."; export class RuneLoader { public static default: RuneLoader = new RuneLoader().withLogger( @@ -62,7 +63,6 @@ export class RuneLoader { } const src = await f.async("string"); const runefile = yaml.load(src); - console.log(src); if (!isRunefile(runefile)) { throw new Error("Invalid Runefile"); @@ -83,7 +83,12 @@ export class RuneLoader { this.modelHandlers ); - const pipeline = determinePipeline(runefile, this.logger); + const pipeline = await determinePipeline( + runefile, + procBlocks, + models, + this.logger + ); return createRuntime(pipeline, this.logger); } diff --git a/bindings/web/rune/src/loader/pipeline.test.ts b/bindings/web/rune/src/loader/pipeline.test.ts index 8055dff3f9..cf60f7f8ce 100644 --- a/bindings/web/rune/src/loader/pipeline.test.ts +++ b/bindings/web/rune/src/loader/pipeline.test.ts @@ -1,7 +1,8 @@ import yaml from "js-yaml"; +import { ElementType } from ".."; import { consoleLogger, Logger, StructuredLogger } from "../logging"; import { DocumentV1 } from "../Runefile"; -import { determinePipeline } from "./pipeline"; +import { determinePipeline, Pipeline } from "./pipeline"; describe("pipeline", () => { const src = ` @@ -44,21 +45,50 @@ describe("pipeline", () => { resources: {}`; const runefile = yaml.load(src) as DocumentV1; - it("can determine the pipeline for sine", () => { + it("can determine the pipeline for sine", async () => { const logger = { log: consoleLogger, isEnabled: () => true }; - const pipeline = determinePipeline(runefile, logger); + const pipeline = await determinePipeline(runefile, {}, {}, logger); // Note: we can't compare nodes for equality because they are objects const { nodes, ...rest } = pipeline; - expect(rest).toMatchObject({ - nodeInfo: { 1: { name: "asf" } }, - evaluationOrder: [42], - inputs: [42], + const expected: Omit = { + nodeInfo: { + "0": { + name: "rand", + inputs: { + "0": 1, + }, + outputs: { + "0": 2, + }, + args: { length: "4" }, + }, + "1": { + name: "mod360", + inputs: { + "0": 3, + }, + outputs: { + "0": 4, + }, + args: {}, + }, + }, + evaluationOrder: ["42"], + inputs: ["42"], tensors: { - 91: { name: "asdf" }, + 42: { + elementType: ElementType.F32, + dimensions: { + tag: "fixed", + val: Uint32Array.from([1, 1]), + }, + }, }, - }); + outputTensors: [42], + }; + expect(rest).toMatchObject(expected); }); }); diff --git a/bindings/web/rune/src/loader/pipeline.ts b/bindings/web/rune/src/loader/pipeline.ts index a40777fd1a..1f90439e7b 100644 --- a/bindings/web/rune/src/loader/pipeline.ts +++ b/bindings/web/rune/src/loader/pipeline.ts @@ -1,28 +1,201 @@ +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; import type { Node } from "."; +import { Dimensions, ElementType } from ".."; import { Logger, StructuredLogger } from "../logging"; -import { TensorDescriptor } from "../proc_blocks"; -import { DocumentV1 } from "../Runefile"; +import { TensorDescriptor, Tensors } from "../proc_blocks"; +import { CapabilityStage, DocumentV1, Input, Stage } from "../Runefile"; -type NodeId = number; +type NodeId = string; type TensorId = number; +type PortId = [NodeId, number]; + +export type TensorShape = { + elementType: runtime_v1.ElementType; + dimensions: Dimensions; +}; export type Pipeline = { nodes: Record; nodeInfo: Record; evaluationOrder: NodeId[]; inputs: NodeId[]; - tensors: Record; + tensors: Record; + outputTensors: TensorId[]; }; type NodeInfo = { - name: string; - args: Record; - inputs: Record; - outputs: Record; + readonly name: string; + readonly args: Readonly>; + readonly inputs: Record; + readonly outputs: Record; +}; + +type Edge = { + previous: PortId; + next: PortId; }; -export function determinePipeline(doc: DocumentV1, logBackend: Logger): Pipeline { +interface ProcBlockLike { + graph(args: Record): Tensors; + evaluate( + inputs: Record, + args: Record + ): Record; +} + +export async function determinePipeline( + doc: DocumentV1, + procBlocks: Record, + models: Record, + logBackend: Logger +): Promise { const logger = new StructuredLogger(logBackend, "determinePipeline"); + logger.debug("Deriving the pipeline"); + + const nodePorts = { + ...(await ports(doc, models)), + ...(await ports(doc, procBlocks)), + ...inputPorts(doc), + }; + + const tensors = discoverTensors(nodePorts); + const edges = discoverEdges(doc); + + console.log(JSON.stringify({nodePorts, tensors, edges}, null, 2)); + + return { + evaluationOrder: [], + inputs: [], + nodeInfo: {}, + nodes: {}, + tensors: {}, + outputTensors: [], + }; +} + +function inputPorts(doc: DocumentV1): Record { + const ports: Record = {}; + + for (const [name, stage] of Object.entries(doc.pipeline)) { + if (!isInputStage(stage)) { + continue; + } + + const outputs = stage.outputs?.map(({ type, dimensions }, i) => { + const dims: Dimensions = dimensions + ? { tag: "fixed", val: Uint32Array.from(dimensions) } + : { tag: "dynamic" }; + + const elementType = elementTypeFromName(type); + return { + name: i.toString(), + elementType, + dimensions: dims, + }; + }); + ports[name] = { inputs: [], outputs: outputs! }; + } + + return ports; +} + +const elementNames: Partial> = { + u8: runtime_v1.ElementType.U8, + i8: runtime_v1.ElementType.I8, + u16: runtime_v1.ElementType.U16, + i16: runtime_v1.ElementType.I16, + u32: runtime_v1.ElementType.U32, + i32: runtime_v1.ElementType.I32, + f32: runtime_v1.ElementType.F32, + u64: runtime_v1.ElementType.U64, + i64: runtime_v1.ElementType.I64, + f64: runtime_v1.ElementType.F64, + utf8: runtime_v1.ElementType.Utf8, +}; + +function elementTypeFromName(name: string): ElementType { + name = name.toLowerCase(); + const type = elementNames[name]; + + if (!type) { + throw new Error(`Unknown element type, "${name}"`); + } + + return type; +} + +async function ports( + doc: DocumentV1, + nodes: Record< + string, + { graph(args: Record): Tensors | Promise } + > +): Promise> { + const ports: Record = {}; + + for (const [name, node] of Object.entries(nodes)) { + const args = stageArguments(doc.pipeline[name]); + ports[name] = await node.graph(args); + } + + return ports; +} + +function stageArguments({ args }: Stage): Record { + if (!args) { + return {}; + } + + const stringified: Record = {}; + + for (const [key, value] of Object.entries(args)) { + stringified[key] = value.toString(); + } + + return stringified; +} + +function isInputStage(stage: Stage): stage is CapabilityStage { + return "capability" in stage; +} + +function discoverEdges(doc: DocumentV1): Edge[] { + const edges: Edge[] = []; + + for (const [name, stage] of Object.entries(doc.pipeline)) { + if (isInputStage(stage) || !stage.inputs) { + continue; + } + + stage.inputs.forEach((input, ix) => { + const previous = parsePortId(input); + edges.push({ previous, next: [name, ix] }); + }); + } + + return edges; +} + +function parsePortId(value: string): PortId { + const match = value.match(/^[\w\d_-]+(?:\.(\d+))?$/); + + if (!match) { + throw new Error(`Unable to parse the input, "${value}"`); + } + + const name = match[0]; + const index = match[1] ? parseInt(match[1]) : 0; + + return [name, index]; +} - throw new Error(); +function discoverTensors( + nodePorts: Record +): Array<{ port: PortId; descriptor: TensorDescriptor }> { + return Object.entries(nodePorts).flatMap(([name, tensors]) => + tensors.inputs.map((tensor, ix) => ({ + port: [name, ix], + descriptor: tensor, + })) + ); } From 1f89e4d9f5b469badbf9a790e5d6fb202f3a40c3 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Thu, 19 May 2022 19:09:01 +0800 Subject: [PATCH 08/18] Updated sine.zip to use the input proc-blocks --- bindings/web/rune/src/__fixtures__/sine.zip | Bin 20590 -> 70297 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bindings/web/rune/src/__fixtures__/sine.zip b/bindings/web/rune/src/__fixtures__/sine.zip index 739c86a28a523d3074d30aaa482ab305e0d7496c..775334e60dfcd05e2f9013380317588df0cd3b22 100644 GIT binary patch literal 70297 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 U_`v}3@d19Ev>#s^ZT@`y1AYA&_W%F@ From 47535ac8b273d057e48fe3642f84d964e01332ee Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Fri, 20 May 2022 11:46:19 +0800 Subject: [PATCH 09/18] Hacking away at the pipeline code --- bindings/web/rune/src/Runtime.ts | 5 +- bindings/web/rune/src/index.test.ts | 12 + bindings/web/rune/src/loader/RuneLoader.ts | 43 ++- bindings/web/rune/src/loader/index.ts | 2 +- bindings/web/rune/src/loader/pipeline.test.ts | 62 +++- bindings/web/rune/src/loader/pipeline.ts | 319 +++++++++++++----- 6 files changed, 334 insertions(+), 109 deletions(-) diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index 238f108488..16dae51560 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -30,7 +30,7 @@ class Runtime implements RuntimeInterface { span.debug("Rune complete", { durationMs: Date.now() - start }); } - public inputs(): Record { + public get inputs(): Record { const entries = this.pipeline.inputs .map((id) => this.pipeline.nodeInfo[id]) .map((info) => { @@ -61,11 +61,14 @@ class Runtime implements RuntimeInterface { inputTensors: entries.length, } ); + console.log({name, tensor, entries, pipeline: this.pipeline.nodeInfo}); + throw new Error(); return; } const [_, id] = entries[0]; this.tensors[id] = tensor; + console.log(this.tensors); } public getOutputs(name: string): Tensor[] | undefined { diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index 4c1a8722aa..9e6d6fcbe1 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -1,4 +1,5 @@ import fs from "fs"; +import { load } from "js-yaml"; import path from "path"; import { RuneLoader, Node, ElementType, Tensor } from "."; import { Tensors } from "./proc_blocks"; @@ -15,7 +16,18 @@ describe("Integration Tests", () => { .withModelHandler("tensorflow-lite", async () => new DummySineModel()) .load(sine); + for (const name of Object.keys(runtime.inputs)) { + runtime.setInput(name, { + buffer: new Uint8Array(), + dimensions: new Uint32Array(), + elementType: ElementType.F32, + }); + } + await runtime.infer(); + + console.log(runtime); + expect(false).toBeTruthy(); }); }); diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/loader/RuneLoader.ts index 662296906a..dac904a2e6 100644 --- a/bindings/web/rune/src/loader/RuneLoader.ts +++ b/bindings/web/rune/src/loader/RuneLoader.ts @@ -72,7 +72,7 @@ export class RuneLoader { const nodes = splitByStageType(runefile); const procBlocks = await instantiateProcBlocks( - nodes.procBlock, + nodes, zip, log.span("instantiate-proc-blocks") ); @@ -123,28 +123,45 @@ function splitByStageType(runefile: DocumentV1): Stages { return nodes; } +function stagesBackedByProcBlocks(stages: Stages) { + const procBlocks: Array<{ name: string; path: string }> = []; + + for (const [name, stage] of Object.entries(stages.procBlock)) { + const path = stage["proc-block"]; + procBlocks.push({ name, path }); + } + + for (const [name, stage] of Object.entries(stages.capability)) { + const path = stage.capability; + procBlocks.push({ name, path }); + } + + return procBlocks; +} + async function instantiateProcBlocks( - stages: Record, + stages: Stages, zip: JSZip, log: StructuredLogger ): Promise> { const start = Date.now(); - const promises = Object.entries(stages).map(async ([name, stage]) => { - const filename = stage["proc-block"]; - log.debug("Reading proc-block", { name, filename }); + const entries = stagesBackedByProcBlocks(stages).map( + async ({ name, path }) => { + log.debug("Reading proc-block", { name, path }); - const file = zip.file(filename); + const file = zip.file(path); - if (!file) { - throw new Error(`The Rune doesn't contain "${filename}"`); - } + if (!file) { + throw new Error(`The Rune doesn't contain "${path}"`); + } - const data = await file.async("arraybuffer"); - return [name, await ProcBlock.load(data, log.backend)]; - }); + const data = await file.async("arraybuffer"); + return [name, await ProcBlock.load(data, log.backend)]; + } + ); - const procBlocks = Object.fromEntries(await Promise.all(promises)); + const procBlocks = Object.fromEntries(await Promise.all(entries)); log.debug("Finished instantiating all proc-blocks", { count: Object.keys(procBlocks).length, diff --git a/bindings/web/rune/src/loader/index.ts b/bindings/web/rune/src/loader/index.ts index 32c1fabe66..5337c9c57b 100644 --- a/bindings/web/rune/src/loader/index.ts +++ b/bindings/web/rune/src/loader/index.ts @@ -28,7 +28,7 @@ export interface Runtime { /** * Get all named inputs. */ - inputs(): Record; + inputs: Record; /** * Set an input tensor by name. */ diff --git a/bindings/web/rune/src/loader/pipeline.test.ts b/bindings/web/rune/src/loader/pipeline.test.ts index cf60f7f8ce..318567e537 100644 --- a/bindings/web/rune/src/loader/pipeline.test.ts +++ b/bindings/web/rune/src/loader/pipeline.test.ts @@ -1,6 +1,9 @@ +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; import yaml from "js-yaml"; -import { ElementType } from ".."; +import { Node } from "."; +import { ElementType, Tensor } from ".."; import { consoleLogger, Logger, StructuredLogger } from "../logging"; +import { TensorDescriptor, Tensors } from "../proc_blocks"; import { DocumentV1 } from "../Runefile"; import { determinePipeline, Pipeline } from "./pipeline"; @@ -45,10 +48,33 @@ describe("pipeline", () => { resources: {}`; const runefile = yaml.load(src) as DocumentV1; - it("can determine the pipeline for sine", async () => { + const f32_1x1 = { + elementType: ElementType.F32, + dimensions: { tag: "fixed", val: Uint32Array.from([1]) }, + } as const; + + it.skip("can determine the pipeline for sine", async () => { const logger = { log: consoleLogger, isEnabled: () => true }; + const procBlocks = { + rand: dummyProcBlock([], [{ name: "output", ...f32_1x1 }]), + mod360: dummyProcBlock( + [{ name: "input", ...f32_1x1 }], + [{ name: "output", ...f32_1x1 }] + ), + }; + const models = { + sine: dummyNode( + [{ name: "input", ...f32_1x1 }], + [{ name: "output", ...f32_1x1 }] + ), + }; - const pipeline = await determinePipeline(runefile, {}, {}, logger); + const pipeline = await determinePipeline( + runefile, + procBlocks, + models, + logger + ); // Note: we can't compare nodes for equality because they are objects const { nodes, ...rest } = pipeline; @@ -87,8 +113,36 @@ describe("pipeline", () => { }, }, }, - outputTensors: [42], + outputTensors: {}, }; expect(rest).toMatchObject(expected); }); }); + +function dummyProcBlock( + inputs: TensorDescriptor[], + outputs: TensorDescriptor[] +) { + return { + graph: (): Tensors => { + return { inputs, outputs }; + }, + evaluate: () => { + throw new Error(); + }, + }; +} + +function dummyNode( + inputs: TensorDescriptor[], + outputs: TensorDescriptor[] +): Node { + return { + graph: async (): Promise => { + return { inputs, outputs }; + }, + infer: () => { + throw new Error(); + }, + }; +} diff --git a/bindings/web/rune/src/loader/pipeline.ts b/bindings/web/rune/src/loader/pipeline.ts index 1f90439e7b..3250920c9f 100644 --- a/bindings/web/rune/src/loader/pipeline.ts +++ b/bindings/web/rune/src/loader/pipeline.ts @@ -3,7 +3,15 @@ import type { Node } from "."; import { Dimensions, ElementType } from ".."; import { Logger, StructuredLogger } from "../logging"; import { TensorDescriptor, Tensors } from "../proc_blocks"; -import { CapabilityStage, DocumentV1, Input, Stage } from "../Runefile"; +import { + CapabilityStage, + DocumentV1, + Input, + ModelStage, + OutStage, + ProcBlockStage, + Stage, +} from "../Runefile"; type NodeId = string; type TensorId = number; @@ -20,7 +28,7 @@ export type Pipeline = { evaluationOrder: NodeId[]; inputs: NodeId[]; tensors: Record; - outputTensors: TensorId[]; + outputTensors: Record ; }; type NodeInfo = { @@ -30,11 +38,6 @@ type NodeInfo = { readonly outputs: Record; }; -type Edge = { - previous: PortId; - next: PortId; -}; - interface ProcBlockLike { graph(args: Record): Tensors; evaluate( @@ -52,51 +55,220 @@ export async function determinePipeline( const logger = new StructuredLogger(logBackend, "determinePipeline"); logger.debug("Deriving the pipeline"); - const nodePorts = { - ...(await ports(doc, models)), - ...(await ports(doc, procBlocks)), - ...inputPorts(doc), - }; - - const tensors = discoverTensors(nodePorts); - const edges = discoverEdges(doc); - - console.log(JSON.stringify({nodePorts, tensors, edges}, null, 2)); - - return { - evaluationOrder: [], - inputs: [], - nodeInfo: {}, - nodes: {}, - tensors: {}, - outputTensors: [], - }; + const resolver = new PipelineResolver(doc, procBlocks, models); + return await resolver.pipeline(); } -function inputPorts(doc: DocumentV1): Record { - const ports: Record = {}; +type TensorInfo = { + parent: NodeId; + index: number; + shape: TensorDescriptor; + isGlobalInput: boolean; +}; + +class PipelineResolver { + inputNodes: NodeId[] = []; + inputsAndOutputs: Record = {}; + tensors: TensorInfo[] = []; + outputNodes: NodeId[] = []; + tensorInputs: Record> = {}; + outputTensors: Record = {}; + + constructor( + private doc: DocumentV1, + private procBlocks: Record, + private models: Record + ) {} + + async pipeline(): Promise { + await this.registerStages(); + this.registerTensors(); + this.resolveInputs(); + this.resolveOutputTensors(); + + return { + evaluationOrder: ["rand", "mod360", "sine"], + inputs: this.inputNodes, + nodeInfo: this.nodeInfo(), + nodes: this.nodes(), + outputTensors: this.outputTensors, + tensors: this.tensorShapes(), + }; + } - for (const [name, stage] of Object.entries(doc.pipeline)) { - if (!isInputStage(stage)) { - continue; + resolveOutputTensors() { + for (const node of this.outputNodes) { + const inputNames = stageInputs(this.doc.pipeline[node]); + + const inputs: TensorId[] = inputNames + .map(parsePortId) + .map(([upstreamNode, outputIndex]) => { + const ix = this.tensors.findIndex((t) => { + return t.parent == upstreamNode && t.index == outputIndex; + }); + if (typeof ix != "number") { + throw new Error( + `Unable to find the "${upstreamNode}.${outputIndex}" input used by "${node}"` + ); + } + + return ix; + }); + + this.outputTensors[node] = inputs; } + } - const outputs = stage.outputs?.map(({ type, dimensions }, i) => { - const dims: Dimensions = dimensions - ? { tag: "fixed", val: Uint32Array.from(dimensions) } - : { tag: "dynamic" }; + nodes(): Record { + const nodes = { ...this.models }; - const elementType = elementTypeFromName(type); - return { - name: i.toString(), - elementType, - dimensions: dims, + for (const [name, procBlock] of Object.entries(this.procBlocks)) { + nodes[name] = { + graph: (args) => Promise.resolve(procBlock.graph(args)), + infer: (inputs, args) => + Promise.resolve(procBlock.evaluate(inputs, args)), }; - }); - ports[name] = { inputs: [], outputs: outputs! }; + } + + return nodes; + } + + tensorShapes(): Record { + const entries = this.tensors.map((t, id) => [id, t.shape]); + return Object.fromEntries(entries); + } + + nodeInfo(): Record { + const nodes: Record = {}; + + for (const [name, stage] of Object.entries(this.doc.pipeline)) { + if (isOutStage(stage)) { + // TODO: handle output nodes + continue; + } + + const args = stageArguments(stage); + const inputs = this.tensorInputs[name]; + nodes[name] = { name, args, inputs, outputs: {} }; + } + + return nodes; + } + + async registerStages() { + for (const [name, stage] of Object.entries(this.doc.pipeline)) { + const args = stageArguments(stage); + + if (isCapabilityStage(stage)) { + this.inputNodes.push(name); + const graph = this.graph(name, args); + console.log("[Capability]", name, graph); + this.inputsAndOutputs[name] = graph; + } else if (isProcBlockStage(stage)) { + this.inputsAndOutputs[name] = this.graph(name, args); + } else if (isModelStage(stage)) { + this.inputsAndOutputs[name] = await this.modelGraph(name, args); + } else { + this.outputNodes.push(name); + } + } + } + + graph(node: string, args: Record): Tensors { + if (node in this.procBlocks) { + return this.procBlocks[node].graph(args); + } else { + throw new Error(`No "${node}" proc-block registered`); + } + } + + async modelGraph( + node: string, + args: Record + ): Promise { + if (node in this.models) { + return await this.models[node].graph(args); + } else { + throw new Error(`No "${node}" model registered`); + } + } + + registerTensors() { + for (const node in this.inputsAndOutputs) { + const { outputs } = this.inputsAndOutputs[node]; + + if (node in this.inputNodes) { + // Input nodes aren't connected to anything, so we need to allocate + // their input tensors explicitly. + const {inputs} = this.inputsAndOutputs[node]; + if (inputs.length != 1) { + throw new Error(); + } + this.tensors.push({parent: node, index: 0, shape: inputs[0], isGlobalInput: true}); + } + + outputs.forEach((shape, index) => { + this.tensors.push({ parent: node, index, shape, isGlobalInput: false }); + }); + } } - return ports; + /** + * For each stage, find the TensorId of its inputs and map them to the name + * used by the stage. + */ + resolveInputs() { + for (const [node, stage] of Object.entries(this.doc.pipeline)) { + if (isOutStage(stage)) { + // Outputs are handed separately. + continue; + } + + if (isCapabilityStage(stage)) { + const ix = this.tensors.findIndex(t => t.isGlobalInput && t.parent == node); + this.tensorInputs[node] = {[node]: ix! }; + continue; + } + + const inputs: TensorId[] = stageInputs(stage) + .map(parsePortId) + .map(([upstreamNode, outputIndex]) => { + const ix = this.tensors.findIndex((t) => { + return t.parent == upstreamNode && t.index == outputIndex && !t.isGlobalInput; + }); + if (typeof ix != "number") { + throw new Error( + `Unable to find the "${upstreamNode}.${outputIndex}" input used by "${node}"` + ); + } + + return ix; + }); + + const namedInputs: Record = {}; + + for (let i = 0; i < inputs.length; i++) { + // Note: we assume the proc-block declared in the same order as they + // are used in the Runefile + const tensorId = inputs[i]; + const { name } = this.inputsAndOutputs[node].inputs[i]; + namedInputs[name] = tensorId; + } + + this.tensorInputs[node] = namedInputs; + } + } +} + +function stageInputs(stage: Stage): string[] { + if ( + (isProcBlockStage(stage) || isModelStage(stage) || isOutStage(stage)) && + stage.inputs + ) { + return stage.inputs; + } + + return []; } const elementNames: Partial> = { @@ -124,23 +296,6 @@ function elementTypeFromName(name: string): ElementType { return type; } -async function ports( - doc: DocumentV1, - nodes: Record< - string, - { graph(args: Record): Tensors | Promise } - > -): Promise> { - const ports: Record = {}; - - for (const [name, node] of Object.entries(nodes)) { - const args = stageArguments(doc.pipeline[name]); - ports[name] = await node.graph(args); - } - - return ports; -} - function stageArguments({ args }: Stage): Record { if (!args) { return {}; @@ -155,27 +310,6 @@ function stageArguments({ args }: Stage): Record { return stringified; } -function isInputStage(stage: Stage): stage is CapabilityStage { - return "capability" in stage; -} - -function discoverEdges(doc: DocumentV1): Edge[] { - const edges: Edge[] = []; - - for (const [name, stage] of Object.entries(doc.pipeline)) { - if (isInputStage(stage) || !stage.inputs) { - continue; - } - - stage.inputs.forEach((input, ix) => { - const previous = parsePortId(input); - edges.push({ previous, next: [name, ix] }); - }); - } - - return edges; -} - function parsePortId(value: string): PortId { const match = value.match(/^[\w\d_-]+(?:\.(\d+))?$/); @@ -189,13 +323,18 @@ function parsePortId(value: string): PortId { return [name, index]; } -function discoverTensors( - nodePorts: Record -): Array<{ port: PortId; descriptor: TensorDescriptor }> { - return Object.entries(nodePorts).flatMap(([name, tensors]) => - tensors.inputs.map((tensor, ix) => ({ - port: [name, ix], - descriptor: tensor, - })) - ); +function isModelStage(stage: Stage): stage is ModelStage { + return "model" in stage; +} + +function isCapabilityStage(stage: Stage): stage is CapabilityStage { + return "capability" in stage; +} + +function isProcBlockStage(stage: Stage): stage is ProcBlockStage { + return "proc-block" in stage; +} + +function isOutStage(stage: Stage): stage is OutStage { + return "out" in stage; } From 1a62aa3243a7bd816b06cdea56f8340e911f2bf8 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Thu, 26 May 2022 07:26:36 +0800 Subject: [PATCH 10/18] Implemented a "tree-walker" style runtime --- bindings/web/rune/src/Runtime.ts | 2 +- bindings/web/rune/src/Runtime2.test.ts | 111 +++++++++ bindings/web/rune/src/Runtime2.ts | 216 ++++++++++++++++++ bindings/web/rune/src/index.test.ts | 19 +- bindings/web/rune/src/loader/RuneLoader.ts | 60 +---- bindings/web/rune/src/loader/pipeline.test.ts | 2 +- bindings/web/rune/src/loader/pipeline.ts | 47 ++-- bindings/web/rune/src/utils.ts | 76 ++++++ 8 files changed, 449 insertions(+), 84 deletions(-) create mode 100644 bindings/web/rune/src/Runtime2.test.ts create mode 100644 bindings/web/rune/src/Runtime2.ts create mode 100644 bindings/web/rune/src/utils.ts diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index 16dae51560..115e860440 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -61,7 +61,7 @@ class Runtime implements RuntimeInterface { inputTensors: entries.length, } ); - console.log({name, tensor, entries, pipeline: this.pipeline.nodeInfo}); + console.log({ name, tensor, entries, pipeline: this.pipeline.nodeInfo }); throw new Error(); return; } diff --git a/bindings/web/rune/src/Runtime2.test.ts b/bindings/web/rune/src/Runtime2.test.ts new file mode 100644 index 0000000000..4f78b72dbe --- /dev/null +++ b/bindings/web/rune/src/Runtime2.test.ts @@ -0,0 +1,111 @@ +import yaml from "js-yaml"; +import { TensorDescriptor, Tensors } from "./proc_blocks"; +import { DocumentV1 } from "./Runefile"; +import { Node } from "./loader"; +import { Runtime, create } from "./Runtime2"; +import { ElementType, Tensor } from "."; +import { floatTensor } from "./utils"; + +describe("Runtime2", () => { + const src = ` + version: 1 + image: runicos/base + pipeline: + rand: + capability: RAW + outputs: + - type: F32 + dimensions: + - 1 + - 1 + args: + length: "4" + mod360: + proc-block: proc_blocks/mod360 + inputs: + - rand + outputs: + - type: F32 + dimensions: + - 1 + - 1 + args: + modulus: "360" + sine: + model: models/sine + inputs: + - mod360 + outputs: + - type: F32 + dimensions: + - 1 + - 1 + serial: + out: serial + inputs: + - sine + resources: {}`; + const runefile = yaml.load(src) as DocumentV1; + + const f32_1x1 = { + elementType: ElementType.F32, + dimensions: { tag: "fixed", val: Uint32Array.from([1]) }, + } as const; + + const rand = dummyProcBlock([], [{ name: "output", ...f32_1x1 }], { + output: floatTensor(1), + }); + const mod360 = dummyProcBlock( + [{ name: "input", ...f32_1x1 }], + [{ name: "output", ...f32_1x1 }], + { output: floatTensor(2) } + ); + + const sine = dummyNode( + [{ name: "input", ...f32_1x1 }], + [{ name: "output", ...f32_1x1 }], + { output: floatTensor(3) } + ); + + it("can run the sine Rune", async () => { + const procBlocks = { rand, mod360 }; + const models = { sine }; + + const runtime: Runtime = create(runefile, procBlocks, models); + + runtime.setInput("rand", floatTensor(0)); + + await runtime.infer(); + + const outputs = runtime.outputTensors; + expect(outputs).toMatchObject({ + serial: [floatTensor(3)], + }); + }); +}); + +function dummyProcBlock( + inputs: TensorDescriptor[], + outputs: TensorDescriptor[], + results: Record +) { + return { + graph: (): Tensors => { + return { inputs, outputs }; + }, + evaluate: () => results, + }; +} + +function dummyNode( + inputs: TensorDescriptor[], + outputs: TensorDescriptor[], + results: Record +): Node { + return { + graph: async (): Promise => { + return { inputs, outputs }; + }, + infer: () => Promise.resolve(results), + }; +} diff --git a/bindings/web/rune/src/Runtime2.ts b/bindings/web/rune/src/Runtime2.ts new file mode 100644 index 0000000000..490a79ed1f --- /dev/null +++ b/bindings/web/rune/src/Runtime2.ts @@ -0,0 +1,216 @@ +import { Node } from "./loader"; +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +import { TensorDescriptor, Tensors } from "./proc_blocks"; +import { DocumentV1, Stage } from "./Runefile"; +import { Tensor } from "."; +import { + isCapabilityStage, + isOutStage, + stageArguments, + stageInputs, +} from "./utils"; + +type TensorId = number; +type NodeId = string; + +interface ProcBlockLike { + graph(args: Record): Tensors; + evaluate( + inputs: Record, + args: Record + ): Record; +} + +export function create( + doc: DocumentV1, + procBlocks: Record, + models: Record +): Runtime { + const pb = procBlockNodes(procBlocks); + const nodes: Record = { ...models, ...pb }; + + return new Runtime(doc, nodes); +} + +type NamedTensor = { + parentNode: NodeId; + outputIndex: number; +} & Tensor; + +export class Runtime { + private inputTensors: Record = {}; + outputTensors: Record = {}; + private tensors: NamedTensor[] = []; + + constructor(private doc: DocumentV1, private nodes: Record) {} + + public async infer(): Promise { + // drop any existing tensors + this.tensors = []; + this.outputTensors = {}; + + for (const [name, stage] of Object.entries(this.doc.pipeline)) { + if (isOutStage(stage)) { + // Output stages are the terminal nodes in our DAG. They aren't + // actually backed by anything, so we just need to (recursively) + // evaluate the node's inputs. + await this.evaluatePrerequisites(stage); + + const inputs = stageInputs(stage).map(({ node, index }) => { + const tensor = this.findTensor(node, index); + if (!tensor) { + throw new Error(); + } + return tensor; + }); + + this.outputTensors[name] = await Promise.all(inputs); + } + } + } + + public get inputs(): string[] { + const inputs: string[] = []; + + for (const [name, stage] of Object.entries(this.doc.pipeline)) { + if (isCapabilityStage(stage)) { + inputs.push(name); + } + } + + return inputs; + } + + public setInput(node: string, tensor: Tensor) { + this.inputTensors[node] = tensor; + } + + private async evaluatePrerequisites(stage: Stage) { + const inputNames = stageInputs(stage).map((input) => input.node); + + const deduplicatedNames = new Set(inputNames); + + const promises: Array> = []; + deduplicatedNames.forEach((name) => { + const alreadyEvaluated = this.tensors.some((t) => t.parentNode == name); + + if (!alreadyEvaluated) { + promises.push(this.evaluateNode(name)); + } + }); + + await Promise.all(promises); + } + + private findTensor( + parentNode: string, + outputIndex: number + ): NamedTensor | undefined { + return this.tensors.find( + (t) => t.parentNode == parentNode && t.outputIndex == outputIndex + ); + } + + private async evaluateNode(name: string) { + if (!(name in this.nodes)) { + throw new Error(`No "${name}" node registered`); + } + if (!(name in this.doc.pipeline)) { + throw new Error(`The Runefile doesn't contain a "${name}" node`); + } + + const stage = this.doc.pipeline[name]; + await this.evaluatePrerequisites(stage); + + const node = this.nodes[name]; + const args = stageArguments(this.doc.pipeline[name]); + + // Fixme: this could be computed once and cached. + const { inputs: inputDescriptors, outputs: outputDescriptors } = + await node.graph(args); + + const inputs = isCapabilityStage(stage) + ? this.getInputTensorsForInputNode(name, inputDescriptors) + : this.getNonInputNodeInputTensors(stage, inputDescriptors); + + const outputs = await node.infer(inputs, args); + + const outputTensors = outputDescriptors.map(({ name: outputName }, i) => ({ + parentNode: name, + outputIndex: i, + ...outputs[outputName], + })); + this.tensors.push(...outputTensors); + } + + private getInputTensorsForInputNode( + name: string, + inputs: TensorDescriptor[] + ): Record { + if (!(name in this.inputTensors)) { + throw new Error(`The "${name}" input tensor wasn't set`); + } + + let tensorName; + + switch (inputs.length) { + case 0: + tensorName = name; + break; + case 1: + tensorName = inputs[0].name; + break; + default: + throw new Error( + `The "${name}" node is an input and should only accept 1 tensor, found ${inputs.length}` + ); + } + + return { [tensorName]: this.inputTensors[name] }; + } + + private getNonInputNodeInputTensors( + stage: Stage, + inputDescriptors: TensorDescriptor[] + ): Record { + const inputs = stageInputs(stage).map(({ node, index }, inputIndex) => { + const tensor = this.findTensor(node, index); + if (!tensor) { + throw new Error(`The "${node}.${index}" tensor hasn't been evaluated`); + } + const inputName = inputDescriptors[inputIndex].name; + return [inputName, tensor] as const; + }); + + return Object.fromEntries(inputs); + } +} + +function procBlockNodes( + procBlocks: Record +): Record { + const nodes: Record = {}; + + for (const [name, procBlock] of Object.entries(procBlocks)) { + nodes[name] = new ProcBlockNode(procBlock); + } + + return nodes; +} + +class ProcBlockNode implements Node { + constructor(private procBlock: ProcBlockLike) {} + + graph(args: Record): Promise { + const tensors = this.procBlock.graph(args); + return Promise.resolve(tensors); + } + + infer( + inputs: Record, + args: Record + ): Promise> { + const outputs = this.procBlock.evaluate(inputs, args); + return Promise.resolve(outputs); + } +} diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index 9e6d6fcbe1..af519de543 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import { load } from "js-yaml"; import path from "path"; import { RuneLoader, Node, ElementType, Tensor } from "."; import { Tensors } from "./proc_blocks"; +import { floatTensor } from "./utils"; describe("Integration Tests", () => { const sine = new Uint8Array( @@ -16,18 +16,15 @@ describe("Integration Tests", () => { .withModelHandler("tensorflow-lite", async () => new DummySineModel()) .load(sine); - for (const name of Object.keys(runtime.inputs)) { - runtime.setInput(name, { - buffer: new Uint8Array(), - dimensions: new Uint32Array(), - elementType: ElementType.F32, - }); - } + runtime.setInput("rand", floatTensor(1)); await runtime.infer(); console.log(runtime); - expect(false).toBeTruthy(); + const outputs = runtime.outputTensors; + expect(outputs).toMatchObject({ + asd: [floatTensor(5)], + }); }); }); @@ -47,10 +44,10 @@ class DummySineModel implements Node { }; } - infer( + async infer( inputs: Record, args: Record ): Promise> { - throw new Error("Method not implemented."); + return { output: inputs["input"] }; } } diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/loader/RuneLoader.ts index dac904a2e6..9b4356bb3b 100644 --- a/bindings/web/rune/src/loader/RuneLoader.ts +++ b/bindings/web/rune/src/loader/RuneLoader.ts @@ -1,6 +1,6 @@ import JSZip from "jszip"; import yaml from "js-yaml"; -import type { ModelHandler, Runtime } from "."; +import type { ModelHandler } from "."; import { consoleLogger, Logger, StructuredLogger } from "../logging"; import { CapabilityStage, @@ -8,12 +8,18 @@ import { ModelStage, OutStage, ProcBlockStage, - Stage, } from "../Runefile"; -import { createRuntime } from "../Runtime"; import { ProcBlock } from "../proc_blocks"; -import { determinePipeline } from "./pipeline"; +import { create, Runtime } from "../Runtime2"; import type { Node } from "."; +import { + isCapabilityStage, + isModelStage, + isOutStage, + isProcBlockStage, + isRunefile, + stageArguments, +} from "../utils"; export class RuneLoader { public static default: RuneLoader = new RuneLoader().withLogger( @@ -83,21 +89,10 @@ export class RuneLoader { this.modelHandlers ); - const pipeline = await determinePipeline( - runefile, - procBlocks, - models, - this.logger - ); - - return createRuntime(pipeline, this.logger); + return create(runefile, procBlocks, models); } } -function isRunefile(value?: any): value is DocumentV1 { - return value && value.version == "1" && value.pipeline && value.image; -} - type Stages = { capability: Record; procBlock: Record; @@ -199,7 +194,7 @@ async function loadModels( const handler = modelHandlers[format]; const data = await file.async("arraybuffer"); - const model = await handler(data, translateArgs(stage.args)); + const model = await handler(data, stageArguments(stage)); log.debug("Loaded model", { name, length: data.byteLength }); @@ -215,34 +210,3 @@ async function loadModels( return models; } - -function isModelStage(stage: Stage): stage is ModelStage { - return "model" in stage; -} - -function isCapabilityStage(stage: Stage): stage is CapabilityStage { - return "capability" in stage; -} - -function isProcBlockStage(stage: Stage): stage is ProcBlockStage { - return "proc-block" in stage; -} - -function isOutStage(stage: Stage): stage is OutStage { - return "out" in stage; -} - -function translateArgs( - args?: Record -): Record { - if (!args) { - return {}; - } - - const entries = Object.entries(args).map(([key, value]) => [ - key, - value.toString(), - ]); - - return Object.fromEntries(entries); -} diff --git a/bindings/web/rune/src/loader/pipeline.test.ts b/bindings/web/rune/src/loader/pipeline.test.ts index 318567e537..b4bce7ae6d 100644 --- a/bindings/web/rune/src/loader/pipeline.test.ts +++ b/bindings/web/rune/src/loader/pipeline.test.ts @@ -7,7 +7,7 @@ import { TensorDescriptor, Tensors } from "../proc_blocks"; import { DocumentV1 } from "../Runefile"; import { determinePipeline, Pipeline } from "./pipeline"; -describe("pipeline", () => { +describe.skip("pipeline", () => { const src = ` version: 1 image: runicos/base diff --git a/bindings/web/rune/src/loader/pipeline.ts b/bindings/web/rune/src/loader/pipeline.ts index 3250920c9f..a54ec5e41e 100644 --- a/bindings/web/rune/src/loader/pipeline.ts +++ b/bindings/web/rune/src/loader/pipeline.ts @@ -1,6 +1,12 @@ import { runtime_v1 } from "@hotg-ai/rune-wit-files"; import type { Node } from "."; import { Dimensions, ElementType } from ".."; +import { + isCapabilityStage, + isModelStage, + isOutStage, + isProcBlockStage, +} from "../utils"; import { Logger, StructuredLogger } from "../logging"; import { TensorDescriptor, Tensors } from "../proc_blocks"; import { @@ -28,7 +34,7 @@ export type Pipeline = { evaluationOrder: NodeId[]; inputs: NodeId[]; tensors: Record; - outputTensors: Record ; + outputTensors: Record; }; type NodeInfo = { @@ -115,7 +121,7 @@ class PipelineResolver { return ix; }); - this.outputTensors[node] = inputs; + this.outputTensors[node] = inputs; } } @@ -200,11 +206,16 @@ class PipelineResolver { if (node in this.inputNodes) { // Input nodes aren't connected to anything, so we need to allocate // their input tensors explicitly. - const {inputs} = this.inputsAndOutputs[node]; + const { inputs } = this.inputsAndOutputs[node]; if (inputs.length != 1) { throw new Error(); } - this.tensors.push({parent: node, index: 0, shape: inputs[0], isGlobalInput: true}); + this.tensors.push({ + parent: node, + index: 0, + shape: inputs[0], + isGlobalInput: true, + }); } outputs.forEach((shape, index) => { @@ -225,8 +236,10 @@ class PipelineResolver { } if (isCapabilityStage(stage)) { - const ix = this.tensors.findIndex(t => t.isGlobalInput && t.parent == node); - this.tensorInputs[node] = {[node]: ix! }; + const ix = this.tensors.findIndex( + (t) => t.isGlobalInput && t.parent == node + ); + this.tensorInputs[node] = { [node]: ix! }; continue; } @@ -234,7 +247,11 @@ class PipelineResolver { .map(parsePortId) .map(([upstreamNode, outputIndex]) => { const ix = this.tensors.findIndex((t) => { - return t.parent == upstreamNode && t.index == outputIndex && !t.isGlobalInput; + return ( + t.parent == upstreamNode && + t.index == outputIndex && + !t.isGlobalInput + ); }); if (typeof ix != "number") { throw new Error( @@ -322,19 +339,3 @@ function parsePortId(value: string): PortId { return [name, index]; } - -function isModelStage(stage: Stage): stage is ModelStage { - return "model" in stage; -} - -function isCapabilityStage(stage: Stage): stage is CapabilityStage { - return "capability" in stage; -} - -function isProcBlockStage(stage: Stage): stage is ProcBlockStage { - return "proc-block" in stage; -} - -function isOutStage(stage: Stage): stage is OutStage { - return "out" in stage; -} diff --git a/bindings/web/rune/src/utils.ts b/bindings/web/rune/src/utils.ts new file mode 100644 index 0000000000..e21426a0d5 --- /dev/null +++ b/bindings/web/rune/src/utils.ts @@ -0,0 +1,76 @@ +import { ElementType, Tensor } from "."; +import { + CapabilityStage, + DocumentV1, + ModelStage, + OutStage, + ProcBlockStage, + Stage, +} from "./Runefile"; + +export function isModelStage(stage: Stage): stage is ModelStage { + return "model" in stage; +} + +export function isCapabilityStage(stage: Stage): stage is CapabilityStage { + return "capability" in stage; +} + +export function isProcBlockStage(stage: Stage): stage is ProcBlockStage { + return "proc-block" in stage; +} + +export function isOutStage(stage: Stage): stage is OutStage { + return "out" in stage; +} + +export function isRunefile(value?: any): value is DocumentV1 { + return value && value.version == "1" && value.pipeline && value.image; +} + +export function stageArguments({ args }: Stage): Record { + if (!args) { + return {}; + } + + const entries = Object.entries(args).map(([key, value]) => [ + key, + value.toString(), + ]); + + return Object.fromEntries(entries); +} + +export type InputName = { + node: string; + index: number; +}; + +export function stageInputs(stage: Stage): InputName[] { + if (isCapabilityStage(stage) || !stage.inputs) { + return []; + } + + return stage.inputs.map(parsePortId); +} + +function parsePortId(value: string): InputName { + const match = value.match(/^[\w\d_-]+(?:\.(\d+))?$/); + + if (!match) { + throw new Error(`Unable to parse the input, "${value}"`); + } + + const node = match[0]; + const index = match[1] ? parseInt(match[1]) : 0; + + return { node, index }; +} + +export function floatTensor(value: number): Tensor { + return { + elementType: ElementType.F32, + dimensions: Uint32Array.from([1, 1]), + buffer: new Uint8Array(Float32Array.from([value])), + }; +} From d930bb24e0f2b8d3deeaa124d2a185c51e8e8fe1 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Thu, 26 May 2022 07:56:59 +0800 Subject: [PATCH 11/18] Switching in the alternate Runtime implementation --- .../web/rune/src/{loader => }/RuneLoader.ts | 21 +- .../src/{Runtime2.test.ts => Runtime.test.ts} | 14 +- bindings/web/rune/src/Runtime.ts | 257 +++++++++---- bindings/web/rune/src/Runtime2.ts | 216 ----------- bindings/web/rune/src/index.test.ts | 4 +- bindings/web/rune/src/index.ts | 35 +- bindings/web/rune/src/loader/index.ts | 36 -- bindings/web/rune/src/loader/pipeline.test.ts | 148 -------- bindings/web/rune/src/loader/pipeline.ts | 341 ------------------ bindings/web/rune/src/utils.ts | 5 +- 10 files changed, 240 insertions(+), 837 deletions(-) rename bindings/web/rune/src/{loader => }/RuneLoader.ts (92%) rename bindings/web/rune/src/{Runtime2.test.ts => Runtime.test.ts} (90%) delete mode 100644 bindings/web/rune/src/Runtime2.ts delete mode 100644 bindings/web/rune/src/loader/index.ts delete mode 100644 bindings/web/rune/src/loader/pipeline.test.ts delete mode 100644 bindings/web/rune/src/loader/pipeline.ts diff --git a/bindings/web/rune/src/loader/RuneLoader.ts b/bindings/web/rune/src/RuneLoader.ts similarity index 92% rename from bindings/web/rune/src/loader/RuneLoader.ts rename to bindings/web/rune/src/RuneLoader.ts index 9b4356bb3b..830936b947 100644 --- a/bindings/web/rune/src/loader/RuneLoader.ts +++ b/bindings/web/rune/src/RuneLoader.ts @@ -1,17 +1,16 @@ import JSZip from "jszip"; import yaml from "js-yaml"; -import type { ModelHandler } from "."; -import { consoleLogger, Logger, StructuredLogger } from "../logging"; +import type { ModelHandler, Node } from "."; +import { consoleLogger, Logger, StructuredLogger } from "./logging"; import { CapabilityStage, DocumentV1, ModelStage, OutStage, ProcBlockStage, -} from "../Runefile"; -import { ProcBlock } from "../proc_blocks"; -import { create, Runtime } from "../Runtime2"; -import type { Node } from "."; +} from "./Runefile"; +import { ProcBlock } from "./proc_blocks"; +import { create } from "./Runtime"; import { isCapabilityStage, isModelStage, @@ -19,7 +18,15 @@ import { isProcBlockStage, isRunefile, stageArguments, -} from "../utils"; +} from "./utils"; +import { Tensor } from "."; + +export interface Runtime { + readonly inputs: string[]; + readonly outputTensors: Readonly>; + infer(): Promise; + setInput(node: string, tensor: Tensor): void; +} export class RuneLoader { public static default: RuneLoader = new RuneLoader().withLogger( diff --git a/bindings/web/rune/src/Runtime2.test.ts b/bindings/web/rune/src/Runtime.test.ts similarity index 90% rename from bindings/web/rune/src/Runtime2.test.ts rename to bindings/web/rune/src/Runtime.test.ts index 4f78b72dbe..8d62f9b980 100644 --- a/bindings/web/rune/src/Runtime2.test.ts +++ b/bindings/web/rune/src/Runtime.test.ts @@ -1,8 +1,8 @@ import yaml from "js-yaml"; import { TensorDescriptor, Tensors } from "./proc_blocks"; import { DocumentV1 } from "./Runefile"; -import { Node } from "./loader"; -import { Runtime, create } from "./Runtime2"; +import { Node } from "."; +import { Runtime, create } from "./Runtime"; import { ElementType, Tensor } from "."; import { floatTensor } from "./utils"; @@ -53,18 +53,18 @@ describe("Runtime2", () => { } as const; const rand = dummyProcBlock([], [{ name: "output", ...f32_1x1 }], { - output: floatTensor(1), + output: floatTensor([1]), }); const mod360 = dummyProcBlock( [{ name: "input", ...f32_1x1 }], [{ name: "output", ...f32_1x1 }], - { output: floatTensor(2) } + { output: floatTensor([2]) } ); const sine = dummyNode( [{ name: "input", ...f32_1x1 }], [{ name: "output", ...f32_1x1 }], - { output: floatTensor(3) } + { output: floatTensor([3]) } ); it("can run the sine Rune", async () => { @@ -73,13 +73,13 @@ describe("Runtime2", () => { const runtime: Runtime = create(runefile, procBlocks, models); - runtime.setInput("rand", floatTensor(0)); + runtime.setInput("rand", floatTensor([0])); await runtime.infer(); const outputs = runtime.outputTensors; expect(outputs).toMatchObject({ - serial: [floatTensor(3)], + serial: [floatTensor([3])], }); }); }); diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index 115e860440..b357fb8d6c 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -1,112 +1,217 @@ -import type { Tensor } from "."; -import { Runtime as RuntimeInterface } from "./loader"; -import { Pipeline } from "./loader/pipeline"; -import { StructuredLogger, Logger } from "./logging"; -import { TensorDescriptor } from "./proc_blocks"; +import { Node } from "."; +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +import { TensorDescriptor, Tensors } from "./proc_blocks"; +import { DocumentV1, Stage } from "./Runefile"; +import { Tensor } from "."; +import { + isCapabilityStage, + isOutStage, + stageArguments, + stageInputs, +} from "./utils"; -type NodeId = string; type TensorId = number; +type NodeId = string; + +interface ProcBlockLike { + graph(args: Record): Tensors; + evaluate( + inputs: Record, + args: Record + ): Record; +} -class Runtime implements RuntimeInterface { - private tensors: Record = {}; +export function create( + doc: DocumentV1, + procBlocks: Record, + models: Record +): Runtime { + const pb = procBlockNodes(procBlocks); + const nodes: Record = { ...models, ...pb }; - constructor(private pipeline: Pipeline, private logger: StructuredLogger) {} + return new Runtime(doc, nodes); +} - public async infer(): Promise { - const span = this.logger.span("infer"); - const start = Date.now(); - span.info("Started running the Rune"); +type NamedTensor = { + parentNode: NodeId; + outputIndex: number; +} & Tensor; - for (const id of this.pipeline.evaluationOrder) { - const { name } = this.pipeline.nodeInfo[id]; - span.debug("Executing node", { name, id }); - const start = Date.now(); +export class Runtime { + outputTensors: Record = {}; + private inputTensors: Record = {}; + private tensors: NamedTensor[] = []; - await this.evaluate(id); + constructor(private doc: DocumentV1, private nodes: Record) {} - span.debug("Node executed", { durationMs: Date.now() - start, name }); + public async infer(): Promise { + // drop any existing tensors + this.tensors = []; + this.outputTensors = {}; + + for (const [name, stage] of Object.entries(this.doc.pipeline)) { + if (isOutStage(stage)) { + // Output stages are the terminal nodes in our DAG. They aren't + // actually backed by anything, so we just need to (recursively) + // evaluate the node's inputs. + await this.evaluatePrerequisites(stage); + + const inputs = stageInputs(stage).map(({ node, index }) => { + const tensor = this.findTensor(node, index); + if (!tensor) { + throw new Error(); + } + return tensor; + }); + + this.outputTensors[name] = await Promise.all(inputs); + } } + } - span.debug("Rune complete", { durationMs: Date.now() - start }); + public get inputs(): string[] { + const inputs: string[] = []; + + for (const [name, stage] of Object.entries(this.doc.pipeline)) { + if (isCapabilityStage(stage)) { + inputs.push(name); + } + } + + return inputs; } - public get inputs(): Record { - const entries = this.pipeline.inputs - .map((id) => this.pipeline.nodeInfo[id]) - .map((info) => { - const [id] = Object.values(info.inputs); - return [info.name, this.pipeline.tensors[id]]; - }); + public setInput(node: string, tensor: Tensor) { + this.inputTensors[node] = tensor; + } + + private async evaluatePrerequisites(stage: Stage) { + const inputNames = stageInputs(stage).map((input) => input.node); + + const deduplicatedNames = new Set(inputNames); - return Object.fromEntries(entries); + const promises: Array> = []; + deduplicatedNames.forEach((name) => { + const alreadyEvaluated = this.tensors.some((t) => t.parentNode == name); + + if (!alreadyEvaluated) { + promises.push(this.evaluateNode(name)); + } + }); + + await Promise.all(promises); } - public setInput(name: string, tensor: Tensor) { - const node = Object.values(this.pipeline.nodeInfo).find( - (n) => n.name == name + private findTensor( + parentNode: string, + outputIndex: number + ): NamedTensor | undefined { + return this.tensors.find( + t => t.parentNode == parentNode && t.outputIndex == outputIndex ); + } - if (!node) { - this.logger.error("Trying to set an input on an unknown node", { name }); - return; + private async evaluateNode(name: string) { + if (!(name in this.nodes)) { + throw new Error(`No "${name}" node registered`); } - - const entries = Object.entries(node.inputs); - - if (entries.length != 1) { - this.logger.error( - "Unable to set the input for a node with multiple input tensors", - { - name, - inputTensors: entries.length, - } - ); - console.log({ name, tensor, entries, pipeline: this.pipeline.nodeInfo }); - throw new Error(); - return; + if (!(name in this.doc.pipeline)) { + throw new Error(`The Runefile doesn't contain a "${name}" node`); } - const [_, id] = entries[0]; - this.tensors[id] = tensor; - console.log(this.tensors); - } + const stage = this.doc.pipeline[name]; + await this.evaluatePrerequisites(stage); - public getOutputs(name: string): Tensor[] | undefined { - throw new Error("Not Implemented"); - } + const node = this.nodes[name]; + const args = stageArguments(this.doc.pipeline[name]); + + // Fixme: this could be computed once and cached. + const { inputs: inputDescriptors, outputs: outputDescriptors } = + await node.graph(args); - private async evaluate(id: NodeId) { - const node = this.pipeline.nodes[id]; - const info = this.pipeline.nodeInfo[id]; - const inputs = this.getTensorsById(info.inputs); + const inputs = isCapabilityStage(stage) + ? this.getInputTensorsForInputNode(name, inputDescriptors) + : this.getNonInputNodeInputTensors(stage, inputDescriptors); - const outputs = await node.infer(inputs, info.args); + const outputs = await node.infer(inputs, args); - this.tensors = { ...this.tensors, ...outputs }; + const outputTensors = outputDescriptors.map(({ name: outputName }, i) => ({ + parentNode: name, + outputIndex: i, + ...outputs[outputName], + })); + this.tensors.push(...outputTensors); } - private getTensorsById( - ids: Record + private getInputTensorsForInputNode( + name: string, + inputs: TensorDescriptor[] ): Record { - const tensors: Record = {}; + if (!(name in this.inputTensors)) { + throw new Error(`The "${name}" input tensor wasn't set`); + } + + let tensorName; - for (const [name, id] of Object.entries(ids)) { - if (id in this.tensors) { - tensors[id] = this.tensors[id]; - } else { + switch (inputs.length) { + case 0: + tensorName = name; + break; + case 1: + tensorName = inputs[0].name; + break; + default: throw new Error( - `Tried to look up a non-existent tensor with ID ${id} ("${name}")` + `The "${name}" node is an input and should only accept 1 tensor, found ${inputs.length}` ); - } } - return tensors; + return { [tensorName]: this.inputTensors[name] }; } + + private getNonInputNodeInputTensors( + stage: Stage, + inputDescriptors: TensorDescriptor[] + ): Record { + const inputs = stageInputs(stage).map(({ node, index }, inputIndex) => { + const tensor = this.findTensor(node, index); + if (!tensor) { + throw new Error(`The "${node}.${index}" tensor hasn't been evaluated`); + } + + const inputName = inputDescriptors[inputIndex].name; + return [inputName, tensor] as const; + }); + + return Object.fromEntries(inputs); + } +} + +function procBlockNodes( + procBlocks: Record +): Record { + const nodes: Record = {}; + + for (const [name, procBlock] of Object.entries(procBlocks)) { + nodes[name] = new ProcBlockNode(procBlock); + } + + return nodes; } -export function createRuntime( - pipeline: Pipeline, - logger: Logger -): RuntimeInterface { - return new Runtime(pipeline, new StructuredLogger(logger, "Runtime")); +class ProcBlockNode implements Node { + constructor(private procBlock: ProcBlockLike) {} + + graph(args: Record): Promise { + const tensors = this.procBlock.graph(args); + return Promise.resolve(tensors); + } + + infer( + inputs: Record, + args: Record + ): Promise> { + const outputs = this.procBlock.evaluate(inputs, args); + return Promise.resolve(outputs); + } } diff --git a/bindings/web/rune/src/Runtime2.ts b/bindings/web/rune/src/Runtime2.ts deleted file mode 100644 index 490a79ed1f..0000000000 --- a/bindings/web/rune/src/Runtime2.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Node } from "./loader"; -import { runtime_v1 } from "@hotg-ai/rune-wit-files"; -import { TensorDescriptor, Tensors } from "./proc_blocks"; -import { DocumentV1, Stage } from "./Runefile"; -import { Tensor } from "."; -import { - isCapabilityStage, - isOutStage, - stageArguments, - stageInputs, -} from "./utils"; - -type TensorId = number; -type NodeId = string; - -interface ProcBlockLike { - graph(args: Record): Tensors; - evaluate( - inputs: Record, - args: Record - ): Record; -} - -export function create( - doc: DocumentV1, - procBlocks: Record, - models: Record -): Runtime { - const pb = procBlockNodes(procBlocks); - const nodes: Record = { ...models, ...pb }; - - return new Runtime(doc, nodes); -} - -type NamedTensor = { - parentNode: NodeId; - outputIndex: number; -} & Tensor; - -export class Runtime { - private inputTensors: Record = {}; - outputTensors: Record = {}; - private tensors: NamedTensor[] = []; - - constructor(private doc: DocumentV1, private nodes: Record) {} - - public async infer(): Promise { - // drop any existing tensors - this.tensors = []; - this.outputTensors = {}; - - for (const [name, stage] of Object.entries(this.doc.pipeline)) { - if (isOutStage(stage)) { - // Output stages are the terminal nodes in our DAG. They aren't - // actually backed by anything, so we just need to (recursively) - // evaluate the node's inputs. - await this.evaluatePrerequisites(stage); - - const inputs = stageInputs(stage).map(({ node, index }) => { - const tensor = this.findTensor(node, index); - if (!tensor) { - throw new Error(); - } - return tensor; - }); - - this.outputTensors[name] = await Promise.all(inputs); - } - } - } - - public get inputs(): string[] { - const inputs: string[] = []; - - for (const [name, stage] of Object.entries(this.doc.pipeline)) { - if (isCapabilityStage(stage)) { - inputs.push(name); - } - } - - return inputs; - } - - public setInput(node: string, tensor: Tensor) { - this.inputTensors[node] = tensor; - } - - private async evaluatePrerequisites(stage: Stage) { - const inputNames = stageInputs(stage).map((input) => input.node); - - const deduplicatedNames = new Set(inputNames); - - const promises: Array> = []; - deduplicatedNames.forEach((name) => { - const alreadyEvaluated = this.tensors.some((t) => t.parentNode == name); - - if (!alreadyEvaluated) { - promises.push(this.evaluateNode(name)); - } - }); - - await Promise.all(promises); - } - - private findTensor( - parentNode: string, - outputIndex: number - ): NamedTensor | undefined { - return this.tensors.find( - (t) => t.parentNode == parentNode && t.outputIndex == outputIndex - ); - } - - private async evaluateNode(name: string) { - if (!(name in this.nodes)) { - throw new Error(`No "${name}" node registered`); - } - if (!(name in this.doc.pipeline)) { - throw new Error(`The Runefile doesn't contain a "${name}" node`); - } - - const stage = this.doc.pipeline[name]; - await this.evaluatePrerequisites(stage); - - const node = this.nodes[name]; - const args = stageArguments(this.doc.pipeline[name]); - - // Fixme: this could be computed once and cached. - const { inputs: inputDescriptors, outputs: outputDescriptors } = - await node.graph(args); - - const inputs = isCapabilityStage(stage) - ? this.getInputTensorsForInputNode(name, inputDescriptors) - : this.getNonInputNodeInputTensors(stage, inputDescriptors); - - const outputs = await node.infer(inputs, args); - - const outputTensors = outputDescriptors.map(({ name: outputName }, i) => ({ - parentNode: name, - outputIndex: i, - ...outputs[outputName], - })); - this.tensors.push(...outputTensors); - } - - private getInputTensorsForInputNode( - name: string, - inputs: TensorDescriptor[] - ): Record { - if (!(name in this.inputTensors)) { - throw new Error(`The "${name}" input tensor wasn't set`); - } - - let tensorName; - - switch (inputs.length) { - case 0: - tensorName = name; - break; - case 1: - tensorName = inputs[0].name; - break; - default: - throw new Error( - `The "${name}" node is an input and should only accept 1 tensor, found ${inputs.length}` - ); - } - - return { [tensorName]: this.inputTensors[name] }; - } - - private getNonInputNodeInputTensors( - stage: Stage, - inputDescriptors: TensorDescriptor[] - ): Record { - const inputs = stageInputs(stage).map(({ node, index }, inputIndex) => { - const tensor = this.findTensor(node, index); - if (!tensor) { - throw new Error(`The "${node}.${index}" tensor hasn't been evaluated`); - } - const inputName = inputDescriptors[inputIndex].name; - return [inputName, tensor] as const; - }); - - return Object.fromEntries(inputs); - } -} - -function procBlockNodes( - procBlocks: Record -): Record { - const nodes: Record = {}; - - for (const [name, procBlock] of Object.entries(procBlocks)) { - nodes[name] = new ProcBlockNode(procBlock); - } - - return nodes; -} - -class ProcBlockNode implements Node { - constructor(private procBlock: ProcBlockLike) {} - - graph(args: Record): Promise { - const tensors = this.procBlock.graph(args); - return Promise.resolve(tensors); - } - - infer( - inputs: Record, - args: Record - ): Promise> { - const outputs = this.procBlock.evaluate(inputs, args); - return Promise.resolve(outputs); - } -} diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index af519de543..93efc1b3d7 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -16,14 +16,14 @@ describe("Integration Tests", () => { .withModelHandler("tensorflow-lite", async () => new DummySineModel()) .load(sine); - runtime.setInput("rand", floatTensor(1)); + runtime.setInput("rand", floatTensor([1])); await runtime.infer(); console.log(runtime); const outputs = runtime.outputTensors; expect(outputs).toMatchObject({ - asd: [floatTensor(5)], + asd: [floatTensor([5])], }); }); }); diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index a25ccaea39..0b5c976642 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -1,11 +1,42 @@ -export * from "./loader"; -// export * from "./proc_blocks"; export { consoleLogger } from "./logging"; export type { Logger } from "./logging"; +export { RuneLoader } from "./RuneLoader"; import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +import { TensorDescriptor, Tensors } from "./proc_blocks"; export type Tensor = runtime_v1.Tensor; export const ElementType = runtime_v1.ElementType; export type ElementType = runtime_v1.ElementType; export type Dimensions = runtime_v1.Dimensions; + +/** + * A callback that can load models. + */ +export type ModelHandler = ( + model: ArrayBuffer, + args: Record +) => Promise; + +export interface Node { + graph(args: Record): Promise; + infer( + inputs: Record, + args: Record + ): Promise>; +} + +export interface Runtime { + /** + * Run the entire Rune pipeline. + */ + infer(): Promise; + /** + * Get all named inputs. + */ + inputs: Record; + /** + * Set an input tensor by name. + */ + setInput(name: string, tensor: Tensor): void; +} diff --git a/bindings/web/rune/src/loader/index.ts b/bindings/web/rune/src/loader/index.ts deleted file mode 100644 index 5337c9c57b..0000000000 --- a/bindings/web/rune/src/loader/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { runtime_v1 } from "@hotg-ai/rune-wit-files"; -import type { Tensor } from ".."; -import type { TensorDescriptor, Tensors } from "../proc_blocks"; - -export { RuneLoader } from "./RuneLoader"; - -/** - * A callback that can load models. - */ -export type ModelHandler = ( - model: ArrayBuffer, - args: Record -) => Promise; - -export interface Node { - graph(args: Record): Promise; - infer( - inputs: Record, - args: Record - ): Promise>; -} - -export interface Runtime { - /** - * Run the entire Rune pipeline. - */ - infer(): Promise; - /** - * Get all named inputs. - */ - inputs: Record; - /** - * Set an input tensor by name. - */ - setInput(name: string, tensor: Tensor): void; -} diff --git a/bindings/web/rune/src/loader/pipeline.test.ts b/bindings/web/rune/src/loader/pipeline.test.ts deleted file mode 100644 index b4bce7ae6d..0000000000 --- a/bindings/web/rune/src/loader/pipeline.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { runtime_v1 } from "@hotg-ai/rune-wit-files"; -import yaml from "js-yaml"; -import { Node } from "."; -import { ElementType, Tensor } from ".."; -import { consoleLogger, Logger, StructuredLogger } from "../logging"; -import { TensorDescriptor, Tensors } from "../proc_blocks"; -import { DocumentV1 } from "../Runefile"; -import { determinePipeline, Pipeline } from "./pipeline"; - -describe.skip("pipeline", () => { - const src = ` - version: 1 - image: runicos/base - pipeline: - rand: - capability: RAW - outputs: - - type: F32 - dimensions: - - 1 - - 1 - args: - length: "4" - mod360: - proc-block: proc_blocks/mod360 - inputs: - - rand - outputs: - - type: F32 - dimensions: - - 1 - - 1 - args: - modulus: "360" - sine: - model: models/sine - inputs: - - mod360 - outputs: - - type: F32 - dimensions: - - 1 - - 1 - serial: - out: serial - inputs: - - sine - resources: {}`; - const runefile = yaml.load(src) as DocumentV1; - - const f32_1x1 = { - elementType: ElementType.F32, - dimensions: { tag: "fixed", val: Uint32Array.from([1]) }, - } as const; - - it.skip("can determine the pipeline for sine", async () => { - const logger = { log: consoleLogger, isEnabled: () => true }; - const procBlocks = { - rand: dummyProcBlock([], [{ name: "output", ...f32_1x1 }]), - mod360: dummyProcBlock( - [{ name: "input", ...f32_1x1 }], - [{ name: "output", ...f32_1x1 }] - ), - }; - const models = { - sine: dummyNode( - [{ name: "input", ...f32_1x1 }], - [{ name: "output", ...f32_1x1 }] - ), - }; - - const pipeline = await determinePipeline( - runefile, - procBlocks, - models, - logger - ); - - // Note: we can't compare nodes for equality because they are objects - const { nodes, ...rest } = pipeline; - - const expected: Omit = { - nodeInfo: { - "0": { - name: "rand", - inputs: { - "0": 1, - }, - outputs: { - "0": 2, - }, - args: { length: "4" }, - }, - "1": { - name: "mod360", - inputs: { - "0": 3, - }, - outputs: { - "0": 4, - }, - args: {}, - }, - }, - evaluationOrder: ["42"], - inputs: ["42"], - tensors: { - 42: { - elementType: ElementType.F32, - dimensions: { - tag: "fixed", - val: Uint32Array.from([1, 1]), - }, - }, - }, - outputTensors: {}, - }; - expect(rest).toMatchObject(expected); - }); -}); - -function dummyProcBlock( - inputs: TensorDescriptor[], - outputs: TensorDescriptor[] -) { - return { - graph: (): Tensors => { - return { inputs, outputs }; - }, - evaluate: () => { - throw new Error(); - }, - }; -} - -function dummyNode( - inputs: TensorDescriptor[], - outputs: TensorDescriptor[] -): Node { - return { - graph: async (): Promise => { - return { inputs, outputs }; - }, - infer: () => { - throw new Error(); - }, - }; -} diff --git a/bindings/web/rune/src/loader/pipeline.ts b/bindings/web/rune/src/loader/pipeline.ts deleted file mode 100644 index a54ec5e41e..0000000000 --- a/bindings/web/rune/src/loader/pipeline.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { runtime_v1 } from "@hotg-ai/rune-wit-files"; -import type { Node } from "."; -import { Dimensions, ElementType } from ".."; -import { - isCapabilityStage, - isModelStage, - isOutStage, - isProcBlockStage, -} from "../utils"; -import { Logger, StructuredLogger } from "../logging"; -import { TensorDescriptor, Tensors } from "../proc_blocks"; -import { - CapabilityStage, - DocumentV1, - Input, - ModelStage, - OutStage, - ProcBlockStage, - Stage, -} from "../Runefile"; - -type NodeId = string; -type TensorId = number; -type PortId = [NodeId, number]; - -export type TensorShape = { - elementType: runtime_v1.ElementType; - dimensions: Dimensions; -}; - -export type Pipeline = { - nodes: Record; - nodeInfo: Record; - evaluationOrder: NodeId[]; - inputs: NodeId[]; - tensors: Record; - outputTensors: Record; -}; - -type NodeInfo = { - readonly name: string; - readonly args: Readonly>; - readonly inputs: Record; - readonly outputs: Record; -}; - -interface ProcBlockLike { - graph(args: Record): Tensors; - evaluate( - inputs: Record, - args: Record - ): Record; -} - -export async function determinePipeline( - doc: DocumentV1, - procBlocks: Record, - models: Record, - logBackend: Logger -): Promise { - const logger = new StructuredLogger(logBackend, "determinePipeline"); - logger.debug("Deriving the pipeline"); - - const resolver = new PipelineResolver(doc, procBlocks, models); - return await resolver.pipeline(); -} - -type TensorInfo = { - parent: NodeId; - index: number; - shape: TensorDescriptor; - isGlobalInput: boolean; -}; - -class PipelineResolver { - inputNodes: NodeId[] = []; - inputsAndOutputs: Record = {}; - tensors: TensorInfo[] = []; - outputNodes: NodeId[] = []; - tensorInputs: Record> = {}; - outputTensors: Record = {}; - - constructor( - private doc: DocumentV1, - private procBlocks: Record, - private models: Record - ) {} - - async pipeline(): Promise { - await this.registerStages(); - this.registerTensors(); - this.resolveInputs(); - this.resolveOutputTensors(); - - return { - evaluationOrder: ["rand", "mod360", "sine"], - inputs: this.inputNodes, - nodeInfo: this.nodeInfo(), - nodes: this.nodes(), - outputTensors: this.outputTensors, - tensors: this.tensorShapes(), - }; - } - - resolveOutputTensors() { - for (const node of this.outputNodes) { - const inputNames = stageInputs(this.doc.pipeline[node]); - - const inputs: TensorId[] = inputNames - .map(parsePortId) - .map(([upstreamNode, outputIndex]) => { - const ix = this.tensors.findIndex((t) => { - return t.parent == upstreamNode && t.index == outputIndex; - }); - if (typeof ix != "number") { - throw new Error( - `Unable to find the "${upstreamNode}.${outputIndex}" input used by "${node}"` - ); - } - - return ix; - }); - - this.outputTensors[node] = inputs; - } - } - - nodes(): Record { - const nodes = { ...this.models }; - - for (const [name, procBlock] of Object.entries(this.procBlocks)) { - nodes[name] = { - graph: (args) => Promise.resolve(procBlock.graph(args)), - infer: (inputs, args) => - Promise.resolve(procBlock.evaluate(inputs, args)), - }; - } - - return nodes; - } - - tensorShapes(): Record { - const entries = this.tensors.map((t, id) => [id, t.shape]); - return Object.fromEntries(entries); - } - - nodeInfo(): Record { - const nodes: Record = {}; - - for (const [name, stage] of Object.entries(this.doc.pipeline)) { - if (isOutStage(stage)) { - // TODO: handle output nodes - continue; - } - - const args = stageArguments(stage); - const inputs = this.tensorInputs[name]; - nodes[name] = { name, args, inputs, outputs: {} }; - } - - return nodes; - } - - async registerStages() { - for (const [name, stage] of Object.entries(this.doc.pipeline)) { - const args = stageArguments(stage); - - if (isCapabilityStage(stage)) { - this.inputNodes.push(name); - const graph = this.graph(name, args); - console.log("[Capability]", name, graph); - this.inputsAndOutputs[name] = graph; - } else if (isProcBlockStage(stage)) { - this.inputsAndOutputs[name] = this.graph(name, args); - } else if (isModelStage(stage)) { - this.inputsAndOutputs[name] = await this.modelGraph(name, args); - } else { - this.outputNodes.push(name); - } - } - } - - graph(node: string, args: Record): Tensors { - if (node in this.procBlocks) { - return this.procBlocks[node].graph(args); - } else { - throw new Error(`No "${node}" proc-block registered`); - } - } - - async modelGraph( - node: string, - args: Record - ): Promise { - if (node in this.models) { - return await this.models[node].graph(args); - } else { - throw new Error(`No "${node}" model registered`); - } - } - - registerTensors() { - for (const node in this.inputsAndOutputs) { - const { outputs } = this.inputsAndOutputs[node]; - - if (node in this.inputNodes) { - // Input nodes aren't connected to anything, so we need to allocate - // their input tensors explicitly. - const { inputs } = this.inputsAndOutputs[node]; - if (inputs.length != 1) { - throw new Error(); - } - this.tensors.push({ - parent: node, - index: 0, - shape: inputs[0], - isGlobalInput: true, - }); - } - - outputs.forEach((shape, index) => { - this.tensors.push({ parent: node, index, shape, isGlobalInput: false }); - }); - } - } - - /** - * For each stage, find the TensorId of its inputs and map them to the name - * used by the stage. - */ - resolveInputs() { - for (const [node, stage] of Object.entries(this.doc.pipeline)) { - if (isOutStage(stage)) { - // Outputs are handed separately. - continue; - } - - if (isCapabilityStage(stage)) { - const ix = this.tensors.findIndex( - (t) => t.isGlobalInput && t.parent == node - ); - this.tensorInputs[node] = { [node]: ix! }; - continue; - } - - const inputs: TensorId[] = stageInputs(stage) - .map(parsePortId) - .map(([upstreamNode, outputIndex]) => { - const ix = this.tensors.findIndex((t) => { - return ( - t.parent == upstreamNode && - t.index == outputIndex && - !t.isGlobalInput - ); - }); - if (typeof ix != "number") { - throw new Error( - `Unable to find the "${upstreamNode}.${outputIndex}" input used by "${node}"` - ); - } - - return ix; - }); - - const namedInputs: Record = {}; - - for (let i = 0; i < inputs.length; i++) { - // Note: we assume the proc-block declared in the same order as they - // are used in the Runefile - const tensorId = inputs[i]; - const { name } = this.inputsAndOutputs[node].inputs[i]; - namedInputs[name] = tensorId; - } - - this.tensorInputs[node] = namedInputs; - } - } -} - -function stageInputs(stage: Stage): string[] { - if ( - (isProcBlockStage(stage) || isModelStage(stage) || isOutStage(stage)) && - stage.inputs - ) { - return stage.inputs; - } - - return []; -} - -const elementNames: Partial> = { - u8: runtime_v1.ElementType.U8, - i8: runtime_v1.ElementType.I8, - u16: runtime_v1.ElementType.U16, - i16: runtime_v1.ElementType.I16, - u32: runtime_v1.ElementType.U32, - i32: runtime_v1.ElementType.I32, - f32: runtime_v1.ElementType.F32, - u64: runtime_v1.ElementType.U64, - i64: runtime_v1.ElementType.I64, - f64: runtime_v1.ElementType.F64, - utf8: runtime_v1.ElementType.Utf8, -}; - -function elementTypeFromName(name: string): ElementType { - name = name.toLowerCase(); - const type = elementNames[name]; - - if (!type) { - throw new Error(`Unknown element type, "${name}"`); - } - - return type; -} - -function stageArguments({ args }: Stage): Record { - if (!args) { - return {}; - } - - const stringified: Record = {}; - - for (const [key, value] of Object.entries(args)) { - stringified[key] = value.toString(); - } - - return stringified; -} - -function parsePortId(value: string): PortId { - const match = value.match(/^[\w\d_-]+(?:\.(\d+))?$/); - - if (!match) { - throw new Error(`Unable to parse the input, "${value}"`); - } - - const name = match[0]; - const index = match[1] ? parseInt(match[1]) : 0; - - return [name, index]; -} diff --git a/bindings/web/rune/src/utils.ts b/bindings/web/rune/src/utils.ts index e21426a0d5..78567793ab 100644 --- a/bindings/web/rune/src/utils.ts +++ b/bindings/web/rune/src/utils.ts @@ -67,10 +67,11 @@ function parsePortId(value: string): InputName { return { node, index }; } -export function floatTensor(value: number): Tensor { +export function floatTensor(values: number[]): Tensor { + const floats = Float32Array.from(values); return { elementType: ElementType.F32, dimensions: Uint32Array.from([1, 1]), - buffer: new Uint8Array(Float32Array.from([value])), + buffer: new Uint8Array(floats.buffer), }; } From b17ac28759a8dc52c112ef0c10433fc4de3f21be Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Thu, 26 May 2022 21:07:56 +0800 Subject: [PATCH 12/18] We can run the sine Rune! --- bindings/web/rune/src/Runtime.ts | 20 ++++++++++++++------ bindings/web/rune/src/index.test.ts | 6 ++---- bindings/web/rune/src/index.ts | 22 ++++------------------ 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index b357fb8d6c..6db0078d37 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -10,7 +10,6 @@ import { stageInputs, } from "./utils"; -type TensorId = number; type NodeId = string; interface ProcBlockLike { @@ -59,12 +58,21 @@ export class Runtime { const inputs = stageInputs(stage).map(({ node, index }) => { const tensor = this.findTensor(node, index); if (!tensor) { - throw new Error(); + throw new Error( + `The "${node}.${index}" tensor wasn't found (needed by "${name}")` + ); } return tensor; }); - this.outputTensors[name] = await Promise.all(inputs); + const results = await Promise.all(inputs); + this.outputTensors[name] = results.map( + ({ buffer, dimensions, elementType }) => ({ + buffer, + dimensions, + elementType, + }) + ); } } } @@ -107,7 +115,7 @@ export class Runtime { outputIndex: number ): NamedTensor | undefined { return this.tensors.find( - t => t.parentNode == parentNode && t.outputIndex == outputIndex + (t) => t.parentNode == parentNode && t.outputIndex == outputIndex ); } @@ -135,10 +143,10 @@ export class Runtime { const outputs = await node.infer(inputs, args); - const outputTensors = outputDescriptors.map(({ name: outputName }, i) => ({ + const outputTensors = outputDescriptors.map((descriptor, i) => ({ + ...outputs[descriptor.name], parentNode: name, outputIndex: i, - ...outputs[outputName], })); this.tensors.push(...outputTensors); } diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index 93efc1b3d7..b37baf0926 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -20,10 +20,8 @@ describe("Integration Tests", () => { await runtime.infer(); - console.log(runtime); - const outputs = runtime.outputTensors; - expect(outputs).toMatchObject({ - asd: [floatTensor([5])], + expect(runtime.outputTensors).toEqual({ + serial: [floatTensor([1])], }); }); }); diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index 0b5c976642..95d81e24eb 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -1,9 +1,10 @@ +import { runtime_v1 } from "@hotg-ai/rune-wit-files"; + export { consoleLogger } from "./logging"; export type { Logger } from "./logging"; export { RuneLoader } from "./RuneLoader"; - -import { runtime_v1 } from "@hotg-ai/rune-wit-files"; -import { TensorDescriptor, Tensors } from "./proc_blocks"; +export type { Runtime } from "./RuneLoader"; +import { Tensors } from "./proc_blocks"; export type Tensor = runtime_v1.Tensor; export const ElementType = runtime_v1.ElementType; @@ -25,18 +26,3 @@ export interface Node { args: Record ): Promise>; } - -export interface Runtime { - /** - * Run the entire Rune pipeline. - */ - infer(): Promise; - /** - * Get all named inputs. - */ - inputs: Record; - /** - * Set an input tensor by name. - */ - setInput(name: string, tensor: Tensor): void; -} From f881d66c56a8567c443db3fc85e57af1cc6a9124 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Fri, 27 May 2022 21:59:03 +0800 Subject: [PATCH 13/18] Wired up a proper logger and started preparing for release --- bindings/web/rune/package.json | 5 +- bindings/web/rune/src/Rune.ts | 40 +++ bindings/web/rune/src/RuneLoader.ts | 235 +++++++--------- bindings/web/rune/src/Runtime.test.ts | 9 +- bindings/web/rune/src/Runtime.ts | 93 ++++-- bindings/web/rune/src/__test__/index.ts | 175 ++++++++++++ bindings/web/rune/src/index.test.ts | 50 +++- bindings/web/rune/src/index.ts | 57 +++- bindings/web/rune/src/logging.ts | 147 ---------- .../web/rune/src/proc_blocks/HostFunctions.ts | 27 +- .../web/rune/src/proc_blocks/ProcBlock.ts | 48 +++- bindings/web/rune/src/proc_blocks/index.ts | 96 +++++++ bindings/web/rune/src/utils.ts | 16 +- bindings/web/rune/yarn.lock | 264 +++++++++++++++++- 14 files changed, 897 insertions(+), 365 deletions(-) create mode 100644 bindings/web/rune/src/Rune.ts create mode 100644 bindings/web/rune/src/__test__/index.ts delete mode 100644 bindings/web/rune/src/logging.ts diff --git a/bindings/web/rune/package.json b/bindings/web/rune/package.json index a3f499f425..2a139ff4f8 100644 --- a/bindings/web/rune/package.json +++ b/bindings/web/rune/package.json @@ -14,14 +14,15 @@ "build": "parcel build", "watch": "parcel watch", "test": "jest", - "ci": "tsc --noEmit && yarn build && yarn test", + "ci": "tsc --noEmit && parcel build && yarn test", "fmt": "prettier --write .", "generate-runefile-types": "json2ts ../../../crates/compiler/runefile-schema.json --output src/Runefile.ts" }, "dependencies": { "@hotg-ai/rune-wit-files": "^0.3.1", "js-yaml": "^4.1.0", - "jszip": "^3.9.1" + "jszip": "^3.9.1", + "pino": "^7.11.0" }, "devDependencies": { "@parcel/packager-ts": "2.5.0", diff --git a/bindings/web/rune/src/Rune.ts b/bindings/web/rune/src/Rune.ts new file mode 100644 index 0000000000..d13bf119a8 --- /dev/null +++ b/bindings/web/rune/src/Rune.ts @@ -0,0 +1,40 @@ +import { Logger, pino } from "pino"; +import type { ModelHandler, Runtime } from "."; +import { RuneLoader } from "./RuneLoader"; + +/** + * A builder object that lets you configure how a Rune is loaded. + */ +export class Rune { + private modelHandlers: Record = {}; + private logger: Logger = pino({ level: "silent", enabled: false }); + + /** + * Set the logger that will be used during the loading process and by the + * Rune runtime. + */ + public withLogger(logger: Logger): this { + this.logger = logger; + return this; + } + + /** + * Register a model handler based on the "model-format" argument attached to + * a model node. + */ + public withModelHandler(modelType: string, handler: ModelHandler): this { + this.modelHandlers[modelType] = handler; + return this; + } + + /** + * Load the Rune, instantiating a Runtime that can be used to interact with + * it. + * + * @param rune + */ + public async load(rune: Uint8Array): Promise { + const loader = new RuneLoader(this.modelHandlers, this.logger); + return await loader.load(rune); + } +} diff --git a/bindings/web/rune/src/RuneLoader.ts b/bindings/web/rune/src/RuneLoader.ts index 830936b947..62b99b5f6f 100644 --- a/bindings/web/rune/src/RuneLoader.ts +++ b/bindings/web/rune/src/RuneLoader.ts @@ -1,7 +1,7 @@ import JSZip from "jszip"; import yaml from "js-yaml"; -import type { ModelHandler, Node } from "."; -import { consoleLogger, Logger, StructuredLogger } from "./logging"; +import { Logger, pino } from "pino"; +import type { ModelHandler, Node, Runtime, Tensor } from "."; import { CapabilityStage, DocumentV1, @@ -19,57 +19,33 @@ import { isRunefile, stageArguments, } from "./utils"; -import { Tensor } from "."; -export interface Runtime { - readonly inputs: string[]; - readonly outputTensors: Readonly>; - infer(): Promise; - setInput(node: string, tensor: Tensor): void; -} export class RuneLoader { - public static default: RuneLoader = new RuneLoader().withLogger( - consoleLogger - ); - - private modelHandlers: Record = {}; - private logger: Logger = { log: () => {}, isEnabled: () => false }; - - /** - * Set the logger that will be used whenever the Rune emits a message. - */ - public withLogger(logger: Logger | Logger["log"]): this { - if (typeof logger == "function") { - // As a convenience, we let people pass in a logging function if - // they don't care about isEnabled(). - this.logger = { log: logger, isEnabled: (m) => m.level != "trace" }; - } else { - this.logger = logger; - } + logger: Logger; - return this; + constructor( + private modelHandlers: Record, + private rootLogger: Logger + ) { + this.logger = rootLogger.child({ name: "RuneLoader" }); } - public withModelHandler(modelType: string, handler: ModelHandler): this { - this.modelHandlers[modelType] = handler; - return this; - } - - /** - * Load the Rune, instantiating a Runtime that can be used to interact with - * it. - * - * @param rune - */ - public async load(rune: Uint8Array): Promise { - const log = new StructuredLogger(this.logger, RuneLoader.name); - - log.info("Loading the Rune", { bytes: rune.byteLength }); + async load(rune: Uint8Array): Promise { + this.logger.info({ bytes: rune.byteLength }, "Loading the Rune"); const zip = new JSZip(); await zip.loadAsync(rune); + const runefile = await this.parseRunefile(zip); + + const nodes = splitByStageType(runefile); + const procBlocks = await this.instantiateProcBlocks(nodes, zip); + const models = await this.loadModels(nodes.model, zip, this.modelHandlers); + + return create(runefile, procBlocks, models, this.rootLogger); + } + async parseRunefile(zip: JSZip): Promise { const f = zip.file("Runefile.yml"); if (!f) { throw new Error("No Runefile.yml found"); @@ -81,22 +57,94 @@ export class RuneLoader { throw new Error("Invalid Runefile"); } - log.debug("Parsed the Runefile", { length: src.length }); + this.logger.debug({ length: src.length }, "Parsed the Runefile"); - const nodes = splitByStageType(runefile); - const procBlocks = await instantiateProcBlocks( - nodes, - zip, - log.span("instantiate-proc-blocks") + return runefile; + } + + async instantiateProcBlocks( + stages: Stages, + zip: JSZip + ): Promise> { + const start = Date.now(); + + const entries = stagesBackedByProcBlocks(stages).map( + async ({ name, path }) => { + this.logger.debug({ procBlock: name, path }, "Reading proc-block"); + + const file = zip.file(path); + + if (!file) { + throw new Error(`The Rune doesn't contain "${path}"`); + } + + const data = await file.async("arraybuffer"); + const procBlock = await ProcBlock.load( + data, + this.rootLogger.child({ procBlock: name }) + ); + return [ name, procBlock ] as const; + } ); - const models = await loadModels( - nodes.model, - zip, - log.span("instantiate-models"), - this.modelHandlers + + const procBlocks = Object.fromEntries(await Promise.all(entries)); + + this.logger.debug({ + count: Object.keys(procBlocks).length, + durationMs: Date.now() - start, + }, "Finished instantiating all proc-blocks"); + + return procBlocks; + } + + async loadModels( + stages: Record, + zip: JSZip, + modelHandlers: Record + ): Promise> { + const start = Date.now(); + + const promises = Object.entries(stages).map(async ([name, stage]) => { + const format = stage.args?.["model-format"] || "tensorflow-lite"; + const filename = stage.model; + this.logger.debug({ model: name, format, filename }, "Loading model"); + + const file = zip.file(filename); + + if (!file) { + throw new Error(`The Rune doesn't contain "${filename}"`); + } + + if (!(format in modelHandlers)) { + throw new Error( + `No handler was registered for the "${format}" model on the "${name}" node` + ); + } + + const handler = modelHandlers[format]; + + const data = await file.async("arraybuffer"); + const model = await handler(data, stageArguments(stage), this.rootLogger); + + this.logger.debug( + { model: name, length: data.byteLength }, + "Loaded model" + ); + + return [name, model]; + }); + + const models = Object.fromEntries(await Promise.all(promises)); + + this.logger.debug( + { + count: Object.keys(models).length, + durationMs: Date.now() - start, + }, + "Finished instantiating all models" ); - return create(runefile, procBlocks, models); + return models; } } @@ -140,80 +188,3 @@ function stagesBackedByProcBlocks(stages: Stages) { return procBlocks; } - -async function instantiateProcBlocks( - stages: Stages, - zip: JSZip, - log: StructuredLogger -): Promise> { - const start = Date.now(); - - const entries = stagesBackedByProcBlocks(stages).map( - async ({ name, path }) => { - log.debug("Reading proc-block", { name, path }); - - const file = zip.file(path); - - if (!file) { - throw new Error(`The Rune doesn't contain "${path}"`); - } - - const data = await file.async("arraybuffer"); - return [name, await ProcBlock.load(data, log.backend)]; - } - ); - - const procBlocks = Object.fromEntries(await Promise.all(entries)); - - log.debug("Finished instantiating all proc-blocks", { - count: Object.keys(procBlocks).length, - durationMs: Date.now() - start, - }); - - return procBlocks; -} - -async function loadModels( - stages: Record, - zip: JSZip, - log: StructuredLogger, - modelHandlers: Record -): Promise> { - const start = Date.now(); - - const promises = Object.entries(stages).map(async ([name, stage]) => { - const format = stage.args?.["model-format"] || "tensorflow-lite"; - const filename = stage.model; - log.debug("Loading model", { name, format, filename }); - - const file = zip.file(filename); - - if (!file) { - throw new Error(`The Rune doesn't contain "${filename}"`); - } - - if (!(format in modelHandlers)) { - throw new Error( - `No handler was registered for the "${format}" model on the "${name}" node` - ); - } - - const handler = modelHandlers[format]; - - const data = await file.async("arraybuffer"); - const model = await handler(data, stageArguments(stage)); - - log.debug("Loaded model", { name, length: data.byteLength }); - - return [name, model]; - }); - - const models = Object.fromEntries(await Promise.all(promises)); - - log.debug("Finished instantiating all models", { - count: Object.keys(models).length, - durationMs: Date.now() - start, - }); - - return models; -} diff --git a/bindings/web/rune/src/Runtime.test.ts b/bindings/web/rune/src/Runtime.test.ts index 8d62f9b980..a7a66a7e8c 100644 --- a/bindings/web/rune/src/Runtime.test.ts +++ b/bindings/web/rune/src/Runtime.test.ts @@ -5,8 +5,11 @@ import { Node } from "."; import { Runtime, create } from "./Runtime"; import { ElementType, Tensor } from "."; import { floatTensor } from "./utils"; +import { testLogger } from "./__test__"; + +describe("Runtime", () => { + let logger = testLogger(); -describe("Runtime2", () => { const src = ` version: 1 image: runicos/base @@ -71,13 +74,13 @@ describe("Runtime2", () => { const procBlocks = { rand, mod360 }; const models = { sine }; - const runtime: Runtime = create(runefile, procBlocks, models); + const runtime: Runtime = create(runefile, procBlocks, models, logger); runtime.setInput("rand", floatTensor([0])); await runtime.infer(); - const outputs = runtime.outputTensors; + const outputs = runtime.outputs; expect(outputs).toMatchObject({ serial: [floatTensor([3])], }); diff --git a/bindings/web/rune/src/Runtime.ts b/bindings/web/rune/src/Runtime.ts index 6db0078d37..5c68a4b6a2 100644 --- a/bindings/web/rune/src/Runtime.ts +++ b/bindings/web/rune/src/Runtime.ts @@ -9,6 +9,7 @@ import { stageArguments, stageInputs, } from "./utils"; +import { Logger } from "pino"; type NodeId = string; @@ -23,12 +24,13 @@ interface ProcBlockLike { export function create( doc: DocumentV1, procBlocks: Record, - models: Record + models: Record, + logger: Logger ): Runtime { const pb = procBlockNodes(procBlocks); const nodes: Record = { ...models, ...pb }; - return new Runtime(doc, nodes); + return new Runtime(doc, nodes, logger); } type NamedTensor = { @@ -37,44 +39,61 @@ type NamedTensor = { } & Tensor; export class Runtime { - outputTensors: Record = {}; + outputs: Record = {}; private inputTensors: Record = {}; private tensors: NamedTensor[] = []; - - constructor(private doc: DocumentV1, private nodes: Record) {} + private logger: Logger; + + constructor( + private doc: DocumentV1, + private nodes: Record, + logger: Logger + ) { + this.logger = logger.child({ name: "Runtime" }); + } public async infer(): Promise { + this.logger.debug("Starting inference"); + const start = Date.now(); + // drop any existing tensors this.tensors = []; - this.outputTensors = {}; + this.outputs = {}; for (const [name, stage] of Object.entries(this.doc.pipeline)) { - if (isOutStage(stage)) { - // Output stages are the terminal nodes in our DAG. They aren't - // actually backed by anything, so we just need to (recursively) - // evaluate the node's inputs. - await this.evaluatePrerequisites(stage); - - const inputs = stageInputs(stage).map(({ node, index }) => { - const tensor = this.findTensor(node, index); - if (!tensor) { - throw new Error( - `The "${node}.${index}" tensor wasn't found (needed by "${name}")` - ); - } - return tensor; - }); - - const results = await Promise.all(inputs); - this.outputTensors[name] = results.map( - ({ buffer, dimensions, elementType }) => ({ - buffer, - dimensions, - elementType, - }) - ); + if (!isOutStage(stage)) { + continue; } + + this.logger.debug({ node: name }, "Evaluating output node"); + + // Output stages are the terminal nodes in our DAG. They aren't + // actually backed by anything, so we just need to (recursively) + // evaluate the node's inputs. + await this.evaluatePrerequisites(stage); + + const inputs = stageInputs(stage).map(({ node, index }) => { + const tensor = this.findTensor(node, index); + if (!tensor) { + throw new Error( + `The "${node}.${index}" tensor wasn't found (needed by "${name}")` + ); + } + return tensor; + }); + + const results = await Promise.all(inputs); + this.outputs[name] = results.map( + ({ buffer, dimensions, elementType }) => ({ + buffer, + dimensions, + elementType, + }) + ); } + + const durationMs = Date.now() - start; + this.logger.debug({ durationMs }, "Inference completed successfully"); } public get inputs(): string[] { @@ -97,6 +116,10 @@ export class Runtime { const inputNames = stageInputs(stage).map((input) => input.node); const deduplicatedNames = new Set(inputNames); + this.logger.debug( + { prerequisites: Array.from(deduplicatedNames.values()) }, + "Evaluating prerequisites" + ); const promises: Array> = []; deduplicatedNames.forEach((name) => { @@ -120,6 +143,9 @@ export class Runtime { } private async evaluateNode(name: string) { + this.logger.debug({ node: name }, "Evaluating a node"); + const start = Date.now(); + if (!(name in this.nodes)) { throw new Error(`No "${name}" node registered`); } @@ -143,6 +169,10 @@ export class Runtime { const outputs = await node.infer(inputs, args); + const durationMs = Date.now() - start; + this.logger.debug({ durationMs, node: name }, "Node evaluated"); + this.logger.trace({ inputs, outputs, args, node: name }); + const outputTensors = outputDescriptors.map((descriptor, i) => ({ ...outputs[descriptor.name], parentNode: name, @@ -207,6 +237,9 @@ function procBlockNodes( return nodes; } +/** + * An adapter class that makes each ProcBlock method asynchronous. + */ class ProcBlockNode implements Node { constructor(private procBlock: ProcBlockLike) {} diff --git a/bindings/web/rune/src/__test__/index.ts b/bindings/web/rune/src/__test__/index.ts new file mode 100644 index 0000000000..d2faf9f5c4 --- /dev/null +++ b/bindings/web/rune/src/__test__/index.ts @@ -0,0 +1,175 @@ +import pino, { Logger } from "pino"; +import { ElementType, Tensor } from ".."; +import { isTensor } from "../utils"; + +function stringArray(buffer: ArrayBuffer, byteOffset: number, length: number) { + const reader = new DataView(buffer, byteOffset, length); + const decoder = new TextDecoder(); + const strings: string[] = []; + + let offset = 0; + while (offset < reader.byteLength) { + const length = reader.getUint32(offset, true); + const utf8 = new Uint8Array(buffer, byteOffset + offset, length); + strings.push(decoder.decode(utf8)); + offset += 4 + length; + } + + return strings; +} + +function typedArray(constructor: { + new ( + buffer: ArrayBufferLike, + byteOffset: number, + length: number + ): ArrayLike; + readonly BYTES_PER_ELEMENT: number; +}): (b: ArrayBuffer, off: number, len: number) => T[] { + return (b, off, len) => + Array.from(new constructor(b, off, len / constructor.BYTES_PER_ELEMENT)); +} + +type NumericTensor = { + elementType: "u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "f32" | "f64"; + dimensions: number[]; + elements: number[]; +}; + +type StringTensor = { + elementType: "utf8"; + dimensions: number[]; + elements: string[]; +}; + +type BigIntTensor = { + elementType: "u64" | "i64"; + dimensions: number[]; + elements: bigint[]; +}; + +type FormattedTensor = NumericTensor | StringTensor | BigIntTensor; + +export function formatTensor(tensor: Tensor): FormattedTensor { + const dimensions = Array.from(tensor.dimensions); + + const { buffer, byteOffset, byteLength } = tensor.buffer; + + switch (tensor.elementType) { + case ElementType.U8: + return { + elementType: "u8", + dimensions, + elements: typedArray(Uint8Array)(buffer, byteOffset, byteLength), + }; + case ElementType.I8: + return { + elementType: "i8", + dimensions, + elements: typedArray(Int8Array)(buffer, byteOffset, byteLength), + }; + case ElementType.U16: + return { + elementType: "u16", + dimensions, + elements: typedArray(Uint16Array)(buffer, byteOffset, byteLength), + }; + case ElementType.I16: + return { + elementType: "i16", + dimensions, + elements: typedArray(Int16Array)(buffer, byteOffset, byteLength), + }; + case ElementType.U32: + return { + elementType: "u32", + dimensions, + elements: typedArray(Uint32Array)(buffer, byteOffset, byteLength), + }; + case ElementType.I32: + return { + elementType: "i32", + dimensions, + elements: typedArray(Int32Array)(buffer, byteOffset, byteLength), + }; + case ElementType.F32: + return { + elementType: "f32", + dimensions, + elements: typedArray(Float32Array)(buffer, byteOffset, byteLength), + }; + case ElementType.F64: + return { + elementType: "f64", + dimensions, + elements: typedArray(Float64Array)(buffer, byteOffset, byteLength), + }; + case ElementType.U64: + return { + elementType: "u64", + dimensions, + elements: typedArray(BigUint64Array)(buffer, byteOffset, byteLength), + }; + case ElementType.I64: + return { + elementType: "i64", + dimensions, + elements: typedArray(BigInt64Array)(buffer, byteOffset, byteLength), + }; + case ElementType.Utf8: + return { + elementType: "utf8", + dimensions, + elements: stringArray(buffer, byteOffset, byteLength), + }; + } +} + +export function testLogger(): Logger { + const bindings = () => { + const { currentTestName } = expect.getState(); + return { test: currentTestName }; + }; + + const humanReadableTensors = (object: any): any => { + if (isTensor(object)) { + return formatTensor(object); + } + + if (typeof object == "object") { + const formatted: any = {}; + + for (const key in object) { + formatted[key] = humanReadableTensors(object[key]); + } + + return formatted; + } + + return object; + }; + + const logger = pino({ + level: "trace", + nestedKey: "payload", + timestamp: false, + formatters: { + bindings, + log: humanReadableTensors, + level: (label) => ({ level: label }), + }, + }); + + beforeEach(() => { + const { currentTestName } = expect.getState(); + logger.info({ test: currentTestName }, "Starting Test"); + }); + + afterEach(() => { + const { currentTestName } = expect.getState(); + logger.info({ test: currentTestName }, "Completed Test"); + logger.flush(); + }); + + return logger; +} diff --git a/bindings/web/rune/src/index.test.ts b/bindings/web/rune/src/index.test.ts index b37baf0926..f62729a4aa 100644 --- a/bindings/web/rune/src/index.test.ts +++ b/bindings/web/rune/src/index.test.ts @@ -1,38 +1,46 @@ import fs from "fs"; import path from "path"; -import { RuneLoader, Node, ElementType, Tensor } from "."; +import { Node, ElementType, Tensor, Rune } from "."; import { Tensors } from "./proc_blocks"; import { floatTensor } from "./utils"; +import { testLogger } from "./__test__"; describe("Integration Tests", () => { + let logger = testLogger(); + const sine = new Uint8Array( fs.readFileSync(path.join(__dirname, "__fixtures__", "sine.zip")) ); it("can load the sine Rune", async () => { - const loader = new RuneLoader(); + const loader = new Rune(); const runtime = await loader .withModelHandler("tensorflow-lite", async () => new DummySineModel()) + .withLogger(logger) .load(sine); runtime.setInput("rand", floatTensor([1])); await runtime.infer(); - expect(runtime.outputTensors).toEqual({ - serial: [floatTensor([1])], + expect(runtime.outputs).toEqual({ + serial: [floatTensor([Math.sin(1)])], }); }); }); +/** + * A "model" that executes sine() against each element in the tensor it is + * given. + */ class DummySineModel implements Node { async graph(): Promise { const tensor = { elementType: ElementType.F32, dimensions: { tag: "fixed", - val: Uint32Array.from([1]), + val: Uint32Array.from([1, 1]), }, } as const; @@ -46,6 +54,36 @@ class DummySineModel implements Node { inputs: Record, args: Record ): Promise> { - return { output: inputs["input"] }; + const { + input: { + buffer: { buffer, byteLength, byteOffset }, + dimensions, + elementType, + }, + } = inputs; + + if (elementType != ElementType.F32) { + throw new Error("Invalid element type"); + } + + const floats = new Float32Array( + buffer, + byteOffset, + byteLength / Float32Array.BYTES_PER_ELEMENT + ); + + const result = floats.map(Math.sin); + + return { + output: { + elementType: ElementType.F32, + dimensions, + buffer: new Uint8Array( + result.buffer, + result.byteOffset, + result.byteLength + ), + }, + }; } } diff --git a/bindings/web/rune/src/index.ts b/bindings/web/rune/src/index.ts index 95d81e24eb..6983657b08 100644 --- a/bindings/web/rune/src/index.ts +++ b/bindings/web/rune/src/index.ts @@ -1,28 +1,71 @@ import { runtime_v1 } from "@hotg-ai/rune-wit-files"; - -export { consoleLogger } from "./logging"; -export type { Logger } from "./logging"; -export { RuneLoader } from "./RuneLoader"; -export type { Runtime } from "./RuneLoader"; +import { Logger } from "pino"; import { Tensors } from "./proc_blocks"; +export { Rune } from "./Rune"; +// export * from "./proc_blocks"; + export type Tensor = runtime_v1.Tensor; export const ElementType = runtime_v1.ElementType; export type ElementType = runtime_v1.ElementType; export type Dimensions = runtime_v1.Dimensions; /** - * A callback that can load models. + * A callback that can be used to load models. + * + * The callback is given the model's bytes, arguments that were associated with + * this particular node, and a logger that should be used for logging. */ export type ModelHandler = ( model: ArrayBuffer, - args: Record + args: Record, + logger: Logger ) => Promise; +/** + * A node in the Rune pipeline. + */ export interface Node { + /** + * Given the provided set of arguments, what are do node's input and output + * tensors look like? + */ graph(args: Record): Promise; + + /** + * Evaluate this node. + * + * @param inputs The node's input tensors. + * @param args Arguments that may alter this node's behaviour. + */ infer( inputs: Record, args: Record ): Promise>; } + +/** + * An instantiated Rune. + */ +export interface Runtime { + /** + * The name of all input nodes. + */ + readonly inputs: readonly string[]; + + /** + * The Rune's output tensors, grouped by the output node they are associated + * with. + */ + readonly outputs: Readonly>; + + /** + * Run the Rune's pipeline. + */ + infer(): Promise; + + /** + * Set the tensor to be used for a particular node's input. + */ + setInput(node: string, tensor: Tensor): void; +} diff --git a/bindings/web/rune/src/logging.ts b/bindings/web/rune/src/logging.ts deleted file mode 100644 index 2c61503843..0000000000 --- a/bindings/web/rune/src/logging.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { runtime_v1 } from "@hotg-ai/rune-wit-files"; - -export type LogLevel = Lowercase; -export type LogValue = number | boolean | string | null; -export type LogPayload = Record; - -/** - * Metadata associated with a single logging event. - */ -export type LogMetadata = { - /** - * The log event's verbosity. - */ - level: LogLevel; - /** - * The name of the section of code ("span") this event was emitted from. - */ - span: string; - /** - * A string describing the part of the system where the span or event that - * this metadata describes occurred. - * - * Typically, this is the module path, but alternate targets may be set when - * spans or events are constructed. - */ - target: string; - file?: string; - line?: number; - module?: string; -}; - -/** - * A logger that receives structured events. - */ -export interface Logger { - /** - * Log an event. - * - * @param metadata Information about the log message and where it came from. - * @param message The message. - * @param payload Additional, structured data that adds context to the log - * message. - */ - log(metadata: LogMetadata, message: string, payload: LogPayload): void; - - /** - * Check if a message *would* be logged based on its metadata. - * - * This is an optimisation that consumers can use to avoid doing expensive - * computations or logging large events that would just be thrown away. - */ - isEnabled(metadata: LogMetadata): boolean; -} - -/** - * A structured logger intended for use within JavaScript. - */ -export class StructuredLogger { - /** - * Create a new structured logger. - * - * @param backend The logging backend messages are sent to. - * @param component The name of the component being logged. - */ - constructor(public readonly backend: Logger, public component: string) {} - - trace(msg: string, payload?: LogPayload) { - this.log("trace", msg, payload); - } - - debug(msg: string, payload?: LogPayload) { - this.log("debug", msg, payload); - } - - info(msg: string, payload?: LogPayload) { - this.log("info", msg, payload); - } - - warn(msg: string, payload?: LogPayload) { - this.log("warn", msg, payload); - } - - error(msg: string, payload?: LogPayload) { - this.log("error", msg, payload); - } - - fatal(msg: string, payload?: LogPayload) { - this.log("fatal", msg, payload); - } - - /** - * Enter a named span. - */ - span(name: string): StructuredLogger { - return new Span(this.backend, this.component, name); - } - - protected metadata(level: LogLevel): LogMetadata { - return { - level, - // Note: We aren't within a span by default. - span: "", - target: this.component, - }; - } - - log(level: LogLevel, msg: string, payload?: LogPayload) { - const meta = this.metadata(level); - - if (this.backend.isEnabled(meta)) { - this.backend.log(meta, msg, payload || {}); - } - } -} - -class Span extends StructuredLogger { - constructor(backend: Logger, component: string, public name: string) { - super(backend, component); - } - - protected metadata(level: LogLevel): LogMetadata { - const meta = super.metadata(level); - meta.span = this.name; - return meta; - } -} - -/** - * A simple logger implementation that does the equivalent of console.log() for - * each log level. - */ -export function consoleLogger( - metadata: LogMetadata, - message: string, - payload: Record -): void { - const { level } = metadata; - - const fatal = (msg: string, ...args: any[]) => { - console.error(msg, ...args); - throw new Error(msg); - }; - - const logger = level == "fatal" ? fatal : console[level]; - - logger(message, { metadata, payload }); -} diff --git a/bindings/web/rune/src/proc_blocks/HostFunctions.ts b/bindings/web/rune/src/proc_blocks/HostFunctions.ts index 59109eaa2f..91625b6816 100644 --- a/bindings/web/rune/src/proc_blocks/HostFunctions.ts +++ b/bindings/web/rune/src/proc_blocks/HostFunctions.ts @@ -1,4 +1,5 @@ import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +import { Logger, Level, levels } from "pino"; import type { ArgumentHint, ArgumentMetadata, @@ -8,9 +9,8 @@ import type { TensorMetadata, TensorDescriptor, } from "."; -import { Logger, LogLevel, LogMetadata, StructuredLogger } from "../logging"; -const logLevels: Record = { +const logLevels: Record = { [runtime_v1.LogLevel.Trace]: "trace", [runtime_v1.LogLevel.Debug]: "debug", [runtime_v1.LogLevel.Info]: "info", @@ -24,7 +24,7 @@ export class HostFunctions implements runtime_v1.RuntimeV1 { graph?: GraphContext; kernel?: KernelContext; - constructor(private logger: Logger) {} + constructor(private logger: Logger) { } metadataNew(name: string, version: string): runtime_v1.Metadata { return new MetadataBuilder({ @@ -94,20 +94,10 @@ export class HostFunctions implements runtime_v1.RuntimeV1 { return this.kernel || null; } - private translateMetadata(meta: runtime_v1.LogMetadata): LogMetadata { - const { level, name, target, file, line, module } = meta; - return { - span: name, - target, - file, - line, - module, - level: logLevels[level], - }; - } - isEnabled(meta: runtime_v1.LogMetadata): boolean { - return this.logger.isEnabled(this.translateMetadata(meta)); + const requestedLevel = logLevels[meta.level]; + const threshold = this.logger.levelVal; + return levels.values[requestedLevel] > threshold; } log( @@ -119,9 +109,10 @@ export class HostFunctions implements runtime_v1.RuntimeV1 { return value.tag == "null" ? [key, null] : [key, value.val]; }); - const meta = this.translateMetadata(metadata); + const level = logLevels[metadata.level]; + const log = this.logger[level]; - this.logger.log(meta, message, Object.fromEntries(payload)); + log({ metadata, payload: Object.fromEntries(payload) }, message); } modelLoad( diff --git a/bindings/web/rune/src/proc_blocks/ProcBlock.ts b/bindings/web/rune/src/proc_blocks/ProcBlock.ts index fbc58dcba7..2a575ab2be 100644 --- a/bindings/web/rune/src/proc_blocks/ProcBlock.ts +++ b/bindings/web/rune/src/proc_blocks/ProcBlock.ts @@ -1,6 +1,6 @@ import { proc_block_v1, runtime_v1 } from "@hotg-ai/rune-wit-files"; +import { Logger } from "pino"; import type { Metadata, Tensors } from "."; -import { Logger, StructuredLogger } from "../logging"; import { GraphContext, HostFunctions, KernelContext } from "./HostFunctions"; type ProcBlockBinary = Parameters[0]; @@ -11,20 +11,30 @@ type ProcBlockBinary = Parameters[0]; export class ProcBlock { private constructor( private hostFunctions: HostFunctions, - private instance: proc_block_v1.ProcBlockV1 + private instance: proc_block_v1.ProcBlockV1, + private logger: Logger ) {} - static async load(wasm: ProcBlockBinary, logger: Logger): Promise { - const log = new StructuredLogger(logger, "ProcBlock"); + /** + * Load a ProcBlock from a WebAssembly module. + * + * @param wasm Something that can be used to instantiate a WebAssembly module. + * @param rootLogger A logger that this ProcBlock can use. + * @returns + */ + static async load(wasm: ProcBlockBinary, rootLogger: Logger): Promise { + // Note: We want the host functions logger to have a different "name" field + // to the ProcBlock object. + const hostFunctionsLogger = rootLogger.child({name: "HostFunctions"}); + const logger = rootLogger.child({ name: "ProcBlock" }); - const span = log.span("load"); - span.info("Loading the proc-block"); + logger.info("Loading the proc-block"); const start = Date.now(); const procBlock = new proc_block_v1.ProcBlockV1(); const imports: any = {}; - const hostFunctions = new HostFunctions(logger); + const hostFunctions = new HostFunctions(hostFunctionsLogger); runtime_v1.addRuntimeV1ToImports( imports, hostFunctions, @@ -33,19 +43,22 @@ export class ProcBlock { await procBlock.instantiate(wasm, imports); - span.debug("Finished loading the proc-block", { - durationMs: Date.now() - start, - }); + const durationMs = Date.now() - start; + rootLogger.debug({ durationMs }, "Finished loading the proc-block"); - return new ProcBlock(hostFunctions, procBlock); + return new ProcBlock(hostFunctions, procBlock, logger); } /** * Extract metadata from the proc-block. */ - metadata(): Metadata | undefined { + metadata(): Metadata { this.hostFunctions.metadata = undefined; this.instance.registerMetadata(); + + if (!this.hostFunctions.metadata) { + throw new Error("The proc-block didn't register any metadata"); + } return this.hostFunctions.metadata; } @@ -54,6 +67,8 @@ export class ProcBlock { * and output tensors be? */ graph(args: Record): Tensors { + this.logger.debug({ args }, "Calling the graph function"); + const ctx = new GraphContext(args); this.hostFunctions.graph = ctx; const result = this.instance.graph(""); @@ -77,12 +92,21 @@ export class ProcBlock { inputs: Record, args: Record ): Record { + this.logger.debug( + { args, inputs: Object.keys(inputs) }, + "Evaluating a proc-block" + ); + const ctx = new KernelContext(args, inputs); this.hostFunctions.kernel = ctx; const result = this.instance.kernel(""); if (result.tag == "err") { + this.logger.error( + { error: result.val }, + "Evaluating the proc-block failed" + ); handleKernelError(result.val); } diff --git a/bindings/web/rune/src/proc_blocks/index.ts b/bindings/web/rune/src/proc_blocks/index.ts index 9c217f80d5..91fd0c7713 100644 --- a/bindings/web/rune/src/proc_blocks/index.ts +++ b/bindings/web/rune/src/proc_blocks/index.ts @@ -2,15 +2,45 @@ export { ProcBlock } from "./ProcBlock"; import { runtime_v1 } from "@hotg-ai/rune-wit-files"; +/** + * Proc-block metadata. + */ export type Metadata = { + /** + * The proc-block's human-friendly name. + */ name: string; + /** + * A semver-compliant version number. + */ version: string; + /** + * A long-form description of what this proc-block does, formatted as markdown. + */ description?: string; + /** + * A link to the proc-block's source code. + */ repository?: string; + /** + * A link to some web page associated with the proc-block. + */ homepage?: string; + /** + * Arbitrary tags that can be used for filtering and searching. + */ tags: string[]; + /** + * Arguments this proc-block accepts. + */ arguments: ArgumentMetadata[]; + /** + * The tensors this proc-block will expect as inputs. + */ inputs: TensorMetadata[]; + /** + * The tensors this proc-block will produce as outputs. + */ outputs: TensorMetadata[]; }; @@ -18,63 +48,129 @@ export type Metadata = { * Information about a tensor's name and constraints about its general shape. */ export type TensorDescriptor = { + /** + * The name associated with this tensor. + */ name: string; + /** + * The type of elements this tensor will contain. + */ elementType: runtime_v1.ElementType; + /** + * Constraints on the tensor's dimensions (a 2D tensor with fixed dimensions, + * a 1D tensor of arbitrary length, a tensor that can have any number of + * dimensions it wants, etc.). + */ dimensions: runtime_v1.Dimensions; }; +/** + * Metadata about a particular tensor. + */ export type TensorMetadata = { + /** + * The name used by the proc-block when referring to this tensor. + */ name: string; + /** + * A long-form description of this tensor, formatted as markdown. + */ description?: string; hints: TensorHint[]; }; +/** + * Metadata around a proc-block argument. + */ export type ArgumentMetadata = { + /** + * The name the proc-block will expect to find. + */ name: string; + /** + * A long-form description of what this argument does, formatted as markdown. + */ description?: string; + /** + * The value used by this if this argument isn't provided. + */ defaultValue?: string; + /** + * Arbitrary hints that can be used to understand more about the argument. + */ hints: ArgumentHint[]; }; +/** + * The argument has a numeric value within a particular range. + */ export type NumberInRange = { type: "number-in-range"; min: string; max: string; }; +/** + * The argument should have one of the values within a set of possible values. + */ export type StringEnum = { type: "string-enum"; possibleValues: string[]; }; +/** + * The argument is a non-negative number. + */ export type NonNegativeNumber = { type: "non-negative-number"; }; +/** + * The "type" of argument this may take. + * + * You can use this as a suggestion when trying to choose which widget would be + * most appropriate when a user is inputting the argument value (e.g. you might + * want to use a