Skip to content

Commit

Permalink
Add YAML parsing support to WASM runtime (#5785)
Browse files Browse the repository at this point in the history
* runtime: add yaml parsing support to wasm runtime

* runtime: add yaml parsing tests
  • Loading branch information
isum authored Feb 11, 2025
1 parent a3f3da7 commit 82080b1
Show file tree
Hide file tree
Showing 15 changed files with 533 additions and 2 deletions.
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions graph/src/runtime/gas/costs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,10 @@ pub const JSON_FROM_BYTES: GasOp = GasOp {
base_cost: DEFAULT_BASE_COST,
size_mult: DEFAULT_GAS_PER_BYTE * 100,
};

// Deeply nested YAML can take up more than 100 times the memory of the serialized format.
// Multiplying the size cost by 100 accounts for this.
pub const YAML_FROM_BYTES: GasOp = GasOp {
base_cost: DEFAULT_BASE_COST,
size_mult: DEFAULT_GAS_PER_BYTE * 100,
};
12 changes: 11 additions & 1 deletion graph/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,17 @@ pub enum IndexForAscTypeId {
// Subgraph Data Source types
AscEntityTrigger = 4500,

// Reserved discriminant space for a future blockchain type IDs: [4,500, 5,499]
// Reserved discriminant space for YAML type IDs: [5,500, 6,499]
YamlValue = 5500,
YamlTaggedValue = 5501,
YamlTypedMapEntryValueValue = 5502,
YamlTypedMapValueValue = 5503,
YamlArrayValue = 5504,
YamlArrayTypedMapEntryValueValue = 5505,
YamlWrappedValue = 5506,
YamlResultValueBool = 5507,

// Reserved discriminant space for a future blockchain type IDs: [6,500, 7,499]
//
// Generated with the following shell script:
//
Expand Down
68 changes: 68 additions & 0 deletions runtime/test/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1698,3 +1698,71 @@ async fn test_store_ts() {
"Cannot get entity of type `Stats`. The type must be an @entity type",
);
}

async fn test_yaml_parsing(api_version: Version, gas_used: u64) {
let mut module = test_module(
"yamlParsing",
mock_data_source(
&wasm_file_path("yaml_parsing.wasm", api_version.clone()),
api_version.clone(),
),
api_version,
)
.await;

let mut test = |input: &str, expected: &str| {
let ptr: AscPtr<AscString> = module.invoke_export1("handleYaml", input.as_bytes());
let resp: String = module.asc_get(ptr).unwrap();
assert_eq!(resp, expected, "failed on input: {input}");
};

// Test invalid YAML;
test("{a: 1, - b: 2}", "error");

// Test size limit;
test(&"x".repeat(10_000_0001), "error");

// Test nulls;
test("null", "(0) null");

// Test booleans;
test("false", "(1) false");
test("true", "(1) true");

// Test numbers;
test("12345", "(2) 12345");
test("12345.6789", "(2) 12345.6789");

// Test strings;
test("aa bb cc", "(3) aa bb cc");
test("\"aa bb cc\"", "(3) aa bb cc");

// Test arrays;
test("[1, 2, 3, 4]", "(4) [(2) 1, (2) 2, (2) 3, (2) 4]");
test("- 1\n- 2\n- 3\n- 4", "(4) [(2) 1, (2) 2, (2) 3, (2) 4]");

// Test objects;
test("{a: 1, b: 2, c: 3}", "(5) {a: (2) 1, b: (2) 2, c: (2) 3}");
test("a: 1\nb: 2\nc: 3", "(5) {a: (2) 1, b: (2) 2, c: (2) 3}");

// Test tagged values;
test("!AA bb cc", "(6) !AA (3) bb cc");

// Test nesting;
test(
"aa:\n bb:\n - cc: !DD ee",
"(5) {aa: (5) {bb: (4) [(5) {cc: (6) !DD (3) ee}]}}",
);

assert_eq!(module.gas_used(), gas_used, "gas used");
}

#[tokio::test]
async fn yaml_parsing_v0_0_4() {
test_yaml_parsing(API_VERSION_0_0_4, 10462217077171).await;
}

#[tokio::test]
async fn yaml_parsing_v0_0_5() {
test_yaml_parsing(API_VERSION_0_0_5, 10462245390665).await;
}
20 changes: 20 additions & 0 deletions runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "allocator/arena";

import {Bytes, Result} from "../api_version_0_0_5/common/types";
import {debug, YAMLValue} from "../api_version_0_0_5/common/yaml";

export {memory};

declare namespace yaml {
function try_fromBytes(data: Bytes): Result<YAMLValue, boolean>;
}

export function handleYaml(data: Bytes): string {
let result = yaml.try_fromBytes(data);

if (result.isError) {
return "error";
}

return debug(result.value);
}
Binary file not shown.
139 changes: 139 additions & 0 deletions runtime/test/wasm_test/api_version_0_0_5/common/yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {TypedMap} from './types';

export enum YAMLValueKind {
NULL = 0,
BOOL = 1,
NUMBER = 2,
STRING = 3,
ARRAY = 4,
OBJECT = 5,
TAGGED = 6,
}

export class YAMLValue {
kind: YAMLValueKind;
data: u64;

isBool(): boolean {
return this.kind == YAMLValueKind.BOOL;
}

isNumber(): boolean {
return this.kind == YAMLValueKind.NUMBER;
}

isString(): boolean {
return this.kind == YAMLValueKind.STRING;
}

isArray(): boolean {
return this.kind == YAMLValueKind.ARRAY;
}

isObject(): boolean {
return this.kind == YAMLValueKind.OBJECT;
}

isTagged(): boolean {
return this.kind == YAMLValueKind.TAGGED;
}


toBool(): boolean {
assert(this.isBool(), 'YAML value is not a boolean');
return this.data != 0;
}

toNumber(): string {
assert(this.isNumber(), 'YAML value is not a number');
return changetype<string>(this.data as usize);
}

toString(): string {
assert(this.isString(), 'YAML value is not a string');
return changetype<string>(this.data as usize);
}

toArray(): Array<YAMLValue> {
assert(this.isArray(), 'YAML value is not an array');
return changetype<Array<YAMLValue>>(this.data as usize);
}

toObject(): TypedMap<YAMLValue, YAMLValue> {
assert(this.isObject(), 'YAML value is not an object');
return changetype<TypedMap<YAMLValue, YAMLValue>>(this.data as usize);
}

toTagged(): YAMLTaggedValue {
assert(this.isTagged(), 'YAML value is not tagged');
return changetype<YAMLTaggedValue>(this.data as usize);
}
}

export class YAMLTaggedValue {
tag: string;
value: YAMLValue;
}


export function debug(value: YAMLValue): string {
return "(" + value.kind.toString() + ") " + debug_value(value);
}

function debug_value(value: YAMLValue): string {
switch (value.kind) {
case YAMLValueKind.NULL:
return "null";
case YAMLValueKind.BOOL:
return value.toBool() ? "true" : "false";
case YAMLValueKind.NUMBER:
return value.toNumber();
case YAMLValueKind.STRING:
return value.toString();
case YAMLValueKind.ARRAY: {
let arr = value.toArray();

let s = "[";
for (let i = 0; i < arr.length; i++) {
if (i > 0) {
s += ", ";
}
s += debug(arr[i]);
}
s += "]";

return s;
}
case YAMLValueKind.OBJECT: {
let arr = value.toObject().entries.sort((a, b) => {
if (a.key.toString() < b.key.toString()) {
return -1;
}

if (a.key.toString() > b.key.toString()) {
return 1;
}

return 0;
});

let s = "{";
for (let i = 0; i < arr.length; i++) {
if (i > 0) {
s += ", ";
}
s += debug_value(arr[i].key) + ": " + debug(arr[i].value);
}
s += "}";

return s;
}
case YAMLValueKind.TAGGED: {
let tagged = value.toTagged();

return tagged.tag + " " + debug(tagged.value);
}
default:
return "undefined";
}
}
62 changes: 62 additions & 0 deletions runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {debug, YAMLValue, YAMLTaggedValue} from './common/yaml';
import {Bytes, Result, TypedMap, TypedMapEntry, Wrapped} from './common/types';

enum TypeId {
STRING = 0,
UINT8_ARRAY = 6,

YamlValue = 5500,
YamlTaggedValue = 5501,
YamlTypedMapEntryValueValue = 5502,
YamlTypedMapValueValue = 5503,
YamlArrayValue = 5504,
YamlArrayTypedMapEntryValueValue = 5505,
YamlWrappedValue = 5506,
YamlResultValueBool = 5507,
}

export function id_of_type(type_id_index: TypeId): usize {
switch (type_id_index) {
case TypeId.STRING:
return idof<string>();
case TypeId.UINT8_ARRAY:
return idof<Uint8Array>();

case TypeId.YamlValue:
return idof<YAMLValue>();
case TypeId.YamlTaggedValue:
return idof<YAMLTaggedValue>();
case TypeId.YamlTypedMapEntryValueValue:
return idof<TypedMapEntry<YAMLValue, YAMLValue>>();
case TypeId.YamlTypedMapValueValue:
return idof<TypedMap<YAMLValue, YAMLValue>>();
case TypeId.YamlArrayValue:
return idof<Array<YAMLValue>>();
case TypeId.YamlArrayTypedMapEntryValueValue:
return idof<Array<TypedMapEntry<YAMLValue, YAMLValue>>>();
case TypeId.YamlWrappedValue:
return idof<Wrapped<YAMLValue>>();
case TypeId.YamlResultValueBool:
return idof<Result<YAMLValue, boolean>>();
default:
return 0;
}
}

export function allocate(n: usize): usize {
return __alloc(n);
}

declare namespace yaml {
function try_fromBytes(data: Bytes): Result<YAMLValue, boolean>;
}

export function handleYaml(data: Bytes): string {
let result = yaml.try_fromBytes(data);

if (result.isError) {
return "error";
}

return debug(result.value);
}
Binary file not shown.
2 changes: 2 additions & 0 deletions runtime/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ wasm-instrument = { version = "0.2.0", features = ["std", "sign_ext"] }

# AssemblyScript uses sign extensions
parity-wasm = { version = "0.45", features = ["std", "sign_ext"] }

serde_yaml = { workspace = true }
Loading

0 comments on commit 82080b1

Please sign in to comment.