// The AssemblyScript every code-mode program compiles against, as abi.ts: Json (from socialweb-computer-emdash's // studio abi.as), print, and Run (one atlibs step-protocol event). A string, so browsers and Node load it alike. export const ABI = String.raw` export enum Kind { Null, Bool, Number, String, Array, Object, } export class Json { kind: Kind = Kind.Null; bool: bool = false; num: f64 = 0; str: string = ""; items: Json[] = []; keys: string[] = []; values: Json[] = []; static null(): Json { return new Json(); } static bool(value: bool): Json { const json = new Json(); json.kind = Kind.Bool; json.bool = value; return json; } static number(value: f64): Json { const json = new Json(); json.kind = Kind.Number; json.num = value; return json; } static string(value: string): Json { const json = new Json(); json.kind = Kind.String; json.str = value; return json; } static array(items: Json[]): Json { const json = new Json(); json.kind = Kind.Array; json.items = items; return json; } static object(): Json { const json = new Json(); json.kind = Kind.Object; return json; } static parse(text: string): Json { const parser = new Parser(text); const value = parser.value(); parser.space(); if (parser.at < text.length) throw new Error("trailing characters in JSON"); return value; } get isNull(): bool { return this.kind == Kind.Null; } get isBool(): bool { return this.kind == Kind.Bool; } get isNumber(): bool { return this.kind == Kind.Number; } get isString(): bool { return this.kind == Kind.String; } get isArray(): bool { return this.kind == Kind.Array; } get isObject(): bool { return this.kind == Kind.Object; } // an array's items, an object's keys, a string's characters; 0 for the rest get length(): i32 { if (this.kind == Kind.Array) return this.items.length; if (this.kind == Kind.Object) return this.keys.length; if (this.kind == Kind.String) return this.str.length; return 0; } has(key: string): bool { return this.keys.includes(key); } get(key: string): Json { for (let i = 0; i < this.keys.length; i++) if (this.keys[i] == key) return this.values[i]; return new Json(); } at(index: i32): Json { return index >= 0 && index < this.items.length ? this.items[index] : new Json(); } set(key: string, value: Json): Json { for (let i = 0; i < this.keys.length; i++) { if (this.keys[i] == key) { this.values[i] = value; return this; } } this.keys.push(key); this.values.push(value); return this; } push(value: Json): Json { this.items.push(value); return this; } toString(): string { switch (this.kind) { case Kind.Bool: return this.bool ? "true" : "false"; case Kind.Number: return isFinite(this.num) ? (this.num == Math.floor(this.num) && Math.abs(this.num) < 9007199254740992 ? i64(this.num).toString() : this.num.toString()) : "null"; case Kind.String: return quote(this.str); case Kind.Array: { const parts = new Array(this.items.length); for (let i = 0; i < this.items.length; i++) parts[i] = this.items[i].toString(); return "[" + parts.join(",") + "]"; } case Kind.Object: { const parts = new Array(this.keys.length); for (let i = 0; i < this.keys.length; i++) parts[i] = quote(this.keys[i]) + ":" + this.values[i].toString(); return "{" + parts.join(",") + "}"; } default: return "null"; } } } // Runs of plain characters are copied whole (substring), escapes added between them: the stub runtime // never frees, so building a string a character at a time would cost memory quadratic in its length. function quote(value: string): string { const parts: string[] = ['"']; let from = 0; for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); if (code != 0x22 && code != 0x5c && code >= 0x20) continue; if (i > from) parts.push(value.substring(from, i)); from = i + 1; if (code == 0x22) parts.push('\\"'); else if (code == 0x5c) parts.push("\\\\"); else if (code == 0x0a) parts.push("\\n"); else if (code == 0x0d) parts.push("\\r"); else if (code == 0x09) parts.push("\\t"); else parts.push("\\u" + code.toString(16).padStart(4, "0")); } if (value.length > from) parts.push(value.substring(from)); parts.push('"'); return parts.join(""); } class Parser { at: i32 = 0; constructor(public text: string) {} space(): void { while (this.at < this.text.length) { const code = this.text.charCodeAt(this.at); if (code != 0x20 && code != 0x0a && code != 0x0d && code != 0x09) return; this.at++; } } expect(word: string): void { if (this.text.substr(this.at, word.length) != word) throw new Error("invalid JSON at " + this.at.toString()); this.at += word.length; } value(): Json { this.space(); if (this.at >= this.text.length) throw new Error("unexpected end of JSON"); const code = this.text.charCodeAt(this.at); if (code == 0x7b) return this.object(); if (code == 0x5b) return this.array(); if (code == 0x22) return Json.string(this.string()); if (code == 0x74) { this.expect("true"); return Json.bool(true); } if (code == 0x66) { this.expect("false"); return Json.bool(false); } if (code == 0x6e) { this.expect("null"); return Json.null(); } return this.number(); } take(code: i32): bool { this.space(); if (this.at < this.text.length && this.text.charCodeAt(this.at) == code) { this.at++; return true; } return false; } object(): Json { const json = Json.object(); this.at++; if (this.take(0x7d)) return json; do { this.space(); const key = this.string(); this.space(); this.expect(":"); json.set(key, this.value()); } while (this.take(0x2c)); this.space(); this.expect("}"); return json; } array(): Json { const json = Json.array([]); this.at++; if (this.take(0x5d)) return json; do { json.push(this.value()); } while (this.take(0x2c)); this.space(); this.expect("]"); return json; } string(): string { this.expect('"'); const parts: string[] = []; let from = this.at; while (this.at < this.text.length) { const code = this.text.charCodeAt(this.at); if (code != 0x22 && code != 0x5c) { this.at++; continue; } if (this.at > from) parts.push(this.text.substring(from, this.at)); this.at++; if (code == 0x22) return parts.join(""); const escape = this.text.charCodeAt(this.at++); if (escape == 0x6e) parts.push("\n"); else if (escape == 0x72) parts.push("\r"); else if (escape == 0x74) parts.push("\t"); else if (escape == 0x62) parts.push("\b"); else if (escape == 0x66) parts.push("\f"); else if (escape == 0x75) { parts.push(String.fromCharCode(parseInt(this.text.substr(this.at, 4), 16))); this.at += 4; } else parts.push(String.fromCharCode(escape)); from = this.at; } throw new Error("unterminated string in JSON"); } number(): Json { const start = this.at; while (this.at < this.text.length) { const code = this.text.charCodeAt(this.at); if ((code >= 0x30 && code <= 0x39) || code == 0x2d || code == 0x2b || code == 0x2e || code == 0x65 || code == 0x45) this.at++; else break; } if (start == this.at) throw new Error("invalid JSON at " + start.toString()); return Json.number(parseFloat(this.text.substring(start, this.at))); } } export function alloc(len: i32): usize { return heap.alloc(len); } export function input(ptr: usize, len: i32): Json { return Json.parse(String.UTF8.decodeUnsafe(ptr, len)); } export function output(value: Json): i64 { const bytes = String.UTF8.encode(value.toString()); return (i64(changetype(bytes)) << 32) | i64(bytes.byteLength); } // stdout: print() appends a line; kept across events in the run's state, at most OUT_MAX characters. const OUT_MAX = 65536; let OUT: string[] = []; let OUT_LEN = 0; export function print(text: string): void { if (OUT_LEN >= OUT_MAX) return; const line = text.length + 1 > OUT_MAX - OUT_LEN ? text.substring(0, OUT_MAX - OUT_LEN - 1) : text; OUT.push(line + "\n"); OUT_LEN += line.length + 1; } // One event of the run: the tool's input, the event (start, or a call's result) and the state main kept. export class Run { input: Json; event: Json; state: Json; now: string; calls: Json[] = []; value: Json = Json.null(); finished: bool = false; constructor(step: Json) { this.input = step.get("input"); this.event = step.get("event"); this.now = step.get("now").str; const saved = step.get("state"); this.state = saved.get("s"); const out = saved.get("out").str; OUT = out.length ? [out] : []; OUT_LEN = out.length; } get start(): bool { return this.event.get("type").str == "start"; } // a call's result: id, ok, output or error get id(): string { return this.event.get("id").str; } get ok(): bool { return this.event.get("ok").bool; } get output(): Json { return this.event.get("output"); } get error(): string { return this.event.get("error").str; } // a procedure (input); service: an https origin or a DID, "" for the session's PDS call(id: string, nsid: string, input: Json, service: string = ""): void { const c = Json.object().set("id", Json.string(id)).set("nsid", Json.string(nsid)).set("input", input); if (service.length) c.set("service", Json.string(service)); this.calls.push(c); } // a query (params, string values) query(id: string, nsid: string, params: Json, service: string = ""): void { const c = Json.object().set("id", Json.string(id)).set("nsid", Json.string(nsid)).set("params", params); if (service.length) c.set("service", Json.string(service)); this.calls.push(c); } finish(value: Json): void { this.value = value; this.finished = true; } out(): i64 { const stdout = Json.string(OUT.join("")); const out = Json.object() .set("calls", Json.array(this.calls)) .set("state", Json.object().set("s", this.state).set("out", stdout)) .set("result", Json.object().set("stdout", stdout).set("value", this.value)); if (this.finished) out.set("done", Json.bool(true)); return output(out); } } `; // The entry the host runs: one event in, main(run), the run's output out. export const ENTRY = String.raw`import { Run, input } from "./abi"; import { main } from "./code"; export { alloc } from "./abi"; export function run(ptr: usize, len: i32): i64 { const r = new Run(input(ptr, len)); main(r); return r.out(); } `;