Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add DynamicMap type #638

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: {},
});
Loading