Skip to content

Commit

Permalink
feat: Add DynamicMap type (#638)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattjohnsonpint authored Dec 7, 2024
1 parent 8482b57 commit f86dcc5
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

- fix: vector package should return generic type in computations [#628](https://github.com/hypermodeinc/modus/pull/628)
- chore: Delete extraneous copy of Anthropic model interface [#631](https://github.com/hypermodeinc/modus/pull/631)
- feat: Add `DynamicMap` type [#638](https://github.com/hypermodeinc/modus/pull/638)

## 2024-11-27 - CLI 0.14.0

Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dotproduct",
"dsname",
"dspc",
"dynamicmap",
"embedder",
"embedders",
"envfiles",
Expand Down
116 changes: 116 additions & 0 deletions sdk/assemblyscript/src/assembly/__tests__/dynamicmap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

import { JSON } from "json-as";
import { expect, it, run } from "as-test";
import { DynamicMap } from "../dynamicmap";


@json
class Obj {
foo: string = "";
}

it("should set values", () => {
const m = new DynamicMap();
m.set("a", 42);
m.set("b", "hello");
m.set("c", [1, 2, 3]);
m.set("d", true);
m.set("e", null);
m.set("f", 3.14);
m.set("g", { foo: "bar" } as Obj);

const json = JSON.stringify(m);
expect(json).toBe(
'{"a":42,"b":"hello","c":[1,2,3],"d":true,"e":null,"f":3.14,"g":{"foo":"bar"}}',
);
});

it("should get values", () => {
const m = JSON.parse<DynamicMap>(
'{"a":42,"b":"hello","c":[1,2,3],"d":true,"e":null,"f":3.14,"g":{"foo":"bar"}}',
);
expect(m.get<i32>("a")).toBe(42);
expect(m.get<string>("b")).toBe("hello");
expect(m.get<i32[]>("c")).toBe([1, 2, 3]);
expect(m.get<bool>("d")).toBe(true);
expect(m.get<Obj | null>("e")).toBe(null);
expect(m.get<f64>("f")).toBe(3.14);

const obj = m.get<Obj>("g");
expect(obj.foo).toBe("bar");
});

it("should get size", () => {
const m = new DynamicMap();
expect(m.size).toBe(0);
m.set("a", 42);
expect(m.size).toBe(1);
m.set("b", "hello");
expect(m.size).toBe(2);
});

it("should test existence of keys", () => {
const m = new DynamicMap();
expect(m.has("a")).toBe(false);
m.set("a", 42);
expect(m.has("a")).toBe(true);
expect(m.has("b")).toBe(false);
m.set("b", "hello");
expect(m.has("b")).toBe(true);
});

it("should delete keys", () => {
const m = new DynamicMap();
m.set("a", 42);
m.set("b", "hello");
expect(m.size).toBe(2);
expect(m.has("a")).toBe(true);
expect(m.has("b")).toBe(true);
m.delete("a");
expect(m.size).toBe(1);
expect(m.has("a")).toBe(false);
expect(m.has("b")).toBe(true);
m.delete("b");
expect(m.size).toBe(0);
expect(m.has("a")).toBe(false);
expect(m.has("b")).toBe(false);
});

it("should clear", () => {
const m = new DynamicMap();
m.set("a", 42);
m.set("b", "hello");
expect(m.size).toBe(2);
m.clear();
expect(m.size).toBe(0);
expect(m.has("a")).toBe(false);
expect(m.has("b")).toBe(false);
});

it("should iterate keys", () => {
const m = new DynamicMap();
m.set("a", 42);
m.set("b", "hello");
m.set("c", [1, 2, 3]);
const keys = m.keys();
expect(keys).toBe(["a", "b", "c"]);
});

it("should iterate raw values", () => {
const m = new DynamicMap();
m.set("a", 42);
m.set("b", "hello");
m.set("c", [1, 2, 3]);
const values = m.values();
expect(values).toBe(["42", '"hello"', "[1,2,3]"]);
});

run();
217 changes: 217 additions & 0 deletions sdk/assemblyscript/src/assembly/dynamicmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

import { JSON } from "json-as";

export class DynamicMap {
private data: Map<string, JSON.Raw> = new Map<string, JSON.Raw>();

public get size(): i32 {
return this.data.size;
}

public has(key: string): bool {
return this.data.has(key);
}

public get<T>(key: string): T {
return JSON.parse<T>(this.data.get(key));
}

public set<T>(key: string, value: T): void {
if (value == null) {
this.data.set(key, "null");
} else {
this.data.set(key, JSON.stringify(value));
}
}

public delete(key: string): bool {
return this.data.delete(key);
}

public clear(): void {
this.data.clear();
}

public keys(): string[] {
return this.data.keys();
}

public values(): JSON.Raw[] {
return this.data.values();
}

__INITIALIZE(): this {
return this;
}

__SERIALIZE(): string {
// This would be ideal, but doesn't work:
// return JSON.stringify(this.data);
// So instead, we have to do construct the JSON manually.
//
// TODO: Update this to use JSON.stringify once it's supported in json-as.
// https://github.com/JairusSW/as-json/issues/98

const segments: string[] = [];
const keys = this.data.keys();
const values = this.data.values();

for (let i = 0; i < this.data.size; i++) {
const key = JSON.stringify(keys[i]);
const value = values[i]; // already in JSON
segments.push(`${key}:${value}`);
}

return `{${segments.join(",")}}`;
}

/* eslint-disable @typescript-eslint/no-unused-vars */
__DESERIALIZE(
data: string,
key_start: i32,
key_end: i32,
value_start: i32,
value_end: i32,
): boolean {
// This would be ideal, but doesn't work:
// this.data = JSON.parse<Map<string, JSON.Raw>>(data);
// So instead, we have to parse the JSON manually.
//
// TODO: Update this to use JSON.parse once it's supported in json-as.
// https://github.com/JairusSW/as-json/issues/98

this.data = parseJsonToRawMap(data);

return true;
}
}

// Everything below this line is a workaround for JSON.parse not working with JSON.Raw values.
// (It was generated via AI and may not be optimized.)

class ParseResult {
constructor(
public result: string,
public nextIndex: i32,
) {}
}

function parseJsonToRawMap(jsonString: string): Map<string, string> {
const map = new Map<string, string>();
let i: i32 = 0;
const length: i32 = jsonString.length;

function skipWhitespace(index: i32, input: string): i32 {
while (
index < input.length &&
(input.charCodeAt(index) === 32 || // space
input.charCodeAt(index) === 9 || // tab
input.charCodeAt(index) === 10 || // newline
input.charCodeAt(index) === 13) // carriage return
) {
index++;
}
return index;
}

function readString(index: i32, input: string): ParseResult {
if (input.charCodeAt(index) !== 34) {
throw new Error("Expected '\"' at position " + index.toString());
}
index++;
let result = "";
while (index < input.length) {
const charCode = input.charCodeAt(index);
if (charCode === 34) {
return new ParseResult(result, index + 1);
}
if (charCode === 92) {
index++;
if (index < input.length) {
result += String.fromCharCode(input.charCodeAt(index));
}
} else {
result += String.fromCharCode(charCode);
}
index++;
}
throw new Error("Unterminated string");
}

function readValue(index: i32, input: string): ParseResult {
const start: i32 = index;
let braceCount: i32 = 0;
let bracketCount: i32 = 0;

while (index < input.length) {
const char = input.charAt(index);
if (
braceCount === 0 &&
bracketCount === 0 &&
(char === "," || char === "}")
) {
break;
}
if (char === "{") braceCount++;
else if (char === "}") braceCount--;
else if (char === "[") bracketCount++;
else if (char === "]") bracketCount--;
index++;
}

const result = input.substring(start, index).trim();
return new ParseResult(result, index);
}

if (jsonString.charCodeAt(i) !== 123) {
throw new Error("Expected '{' at the beginning of JSON");
}
i++;

while (i < length) {
i = skipWhitespace(i, jsonString);

const keyResult = readString(i, jsonString);
const key = keyResult.result;
i = keyResult.nextIndex;

i = skipWhitespace(i, jsonString);
if (jsonString.charCodeAt(i) !== 58) {
throw new Error("Expected ':' after key at position " + i.toString());
}
i++;

i = skipWhitespace(i, jsonString);

const valueResult = readValue(i, jsonString);
const value = valueResult.result;
i = valueResult.nextIndex;

map.set(key, value);

i = skipWhitespace(i, jsonString);
if (jsonString.charCodeAt(i) === 44) {
i++;
} else if (jsonString.charCodeAt(i) === 125) {
i++;
break;
} else {
throw new Error(
"Unexpected character '" +
jsonString.charAt(i) +
"' at position " +
i.toString(),
);
}
}

return map;
}
5 changes: 5 additions & 0 deletions sdk/assemblyscript/src/assembly/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ export { vectors };

import * as auth from "./auth";
export { auth };

import * as utils from "./utils";
export { utils };

export * from "./dynamicmap";
16 changes: 16 additions & 0 deletions sdk/assemblyscript/src/tests/dynamicmap.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright 2024 Hypermode Inc.
* Licensed under the terms of the Apache License, Version 2.0
* See the LICENSE file that accompanied this code for further details.
*
* SPDX-FileCopyrightText: 2024 Hypermode Inc. <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

import { readFileSync } from "fs";
import { instantiate } from "../build/dynamicmap.spec.js";
const binary = readFileSync("./build/dynamicmap.spec.wasm");
const module = new WebAssembly.Module(binary);
instantiate(module, {
env: {},
});

0 comments on commit f86dcc5

Please sign in to comment.