~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-code-mode

0
0
main
389 lines · 10.8 KB Raw History
// 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<string>(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<string>(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(<i32>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(<usize>len);
}

export function input(ptr: usize, len: i32): Json {
  return Json.parse(String.UTF8.decodeUnsafe(ptr, <usize>len));
}

export function output(value: Json): i64 {
  const bytes = String.UTF8.encode(value.toString());
  return (i64(changetype<usize>(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();
}
`;