From 82080b1a3874c5d89b97ca2c75e464c92bc49691 Mon Sep 17 00:00:00 2001 From: Ion Suman <47307091+isum@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:39:23 +0200 Subject: [PATCH] Add YAML parsing support to WASM runtime (#5785) * runtime: add yaml parsing support to wasm runtime * runtime: add yaml parsing tests --- Cargo.lock | 3 +- graph/src/runtime/gas/costs.rs | 7 + graph/src/runtime/mod.rs | 12 +- runtime/test/src/test.rs | 68 +++++++++ .../api_version_0_0_4/yaml_parsing.ts | 20 +++ .../api_version_0_0_4/yaml_parsing.wasm | Bin 0 -> 7414 bytes .../api_version_0_0_5/common/yaml.ts | 139 ++++++++++++++++++ .../api_version_0_0_5/yaml_parsing.ts | 62 ++++++++ .../api_version_0_0_5/yaml_parsing.wasm | Bin 0 -> 11955 bytes runtime/wasm/Cargo.toml | 2 + runtime/wasm/src/asc_abi/class.rs | 78 ++++++++++ runtime/wasm/src/host_exports.rs | 30 ++++ runtime/wasm/src/module/context.rs | 60 ++++++++ runtime/wasm/src/module/instance.rs | 3 + runtime/wasm/src/to_from/external.rs | 51 +++++++ 15 files changed, 533 insertions(+), 2 deletions(-) create mode 100644 runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.ts create mode 100644 runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.wasm create mode 100644 runtime/test/wasm_test/api_version_0_0_5/common/yaml.ts create mode 100644 runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.ts create mode 100644 runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.wasm diff --git a/Cargo.lock b/Cargo.lock index 17573acd02c..b2ae24e0169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -2073,6 +2073,7 @@ dependencies = [ "never", "parity-wasm", "semver", + "serde_yaml", "uuid", "wasm-instrument", "wasmtime", diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs index 6436fc2102d..06decdf03aa 100644 --- a/graph/src/runtime/gas/costs.rs +++ b/graph/src/runtime/gas/costs.rs @@ -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, +}; diff --git a/graph/src/runtime/mod.rs b/graph/src/runtime/mod.rs index f015e1e9563..5622d37c100 100644 --- a/graph/src/runtime/mod.rs +++ b/graph/src/runtime/mod.rs @@ -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: // diff --git a/runtime/test/src/test.rs b/runtime/test/src/test.rs index bad91ef2158..53a84aec5f1 100644 --- a/runtime/test/src/test.rs +++ b/runtime/test/src/test.rs @@ -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 = 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; +} diff --git a/runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.ts b/runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.ts new file mode 100644 index 00000000000..b3efc9ba205 --- /dev/null +++ b/runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.ts @@ -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; +} + +export function handleYaml(data: Bytes): string { + let result = yaml.try_fromBytes(data); + + if (result.isError) { + return "error"; + } + + return debug(result.value); +} diff --git a/runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.wasm b/runtime/test/wasm_test/api_version_0_0_4/yaml_parsing.wasm new file mode 100644 index 0000000000000000000000000000000000000000..cb132344ce3d8d47afe0bd8dc4b6bb29fb55be64 GIT binary patch literal 7414 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 a1^U$OiZPGZK;<1o&|8r5tUvf#82<-&W!*6V literal 0 HcmV?d00001 diff --git a/runtime/test/wasm_test/api_version_0_0_5/common/yaml.ts b/runtime/test/wasm_test/api_version_0_0_5/common/yaml.ts new file mode 100644 index 00000000000..135635475f1 --- /dev/null +++ b/runtime/test/wasm_test/api_version_0_0_5/common/yaml.ts @@ -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(this.data as usize); + } + + toString(): string { + assert(this.isString(), 'YAML value is not a string'); + return changetype(this.data as usize); + } + + toArray(): Array { + assert(this.isArray(), 'YAML value is not an array'); + return changetype>(this.data as usize); + } + + toObject(): TypedMap { + assert(this.isObject(), 'YAML value is not an object'); + return changetype>(this.data as usize); + } + + toTagged(): YAMLTaggedValue { + assert(this.isTagged(), 'YAML value is not tagged'); + return changetype(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"; + } +} diff --git a/runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.ts b/runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.ts new file mode 100644 index 00000000000..c89eb611bb2 --- /dev/null +++ b/runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.ts @@ -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(); + case TypeId.UINT8_ARRAY: + return idof(); + + case TypeId.YamlValue: + return idof(); + case TypeId.YamlTaggedValue: + return idof(); + case TypeId.YamlTypedMapEntryValueValue: + return idof>(); + case TypeId.YamlTypedMapValueValue: + return idof>(); + case TypeId.YamlArrayValue: + return idof>(); + case TypeId.YamlArrayTypedMapEntryValueValue: + return idof>>(); + case TypeId.YamlWrappedValue: + return idof>(); + case TypeId.YamlResultValueBool: + return idof>(); + default: + return 0; + } +} + +export function allocate(n: usize): usize { + return __alloc(n); +} + +declare namespace yaml { + function try_fromBytes(data: Bytes): Result; +} + +export function handleYaml(data: Bytes): string { + let result = yaml.try_fromBytes(data); + + if (result.isError) { + return "error"; + } + + return debug(result.value); +} diff --git a/runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.wasm b/runtime/test/wasm_test/api_version_0_0_5/yaml_parsing.wasm new file mode 100644 index 0000000000000000000000000000000000000000..131ded5d04c976692e98b3798239dfdb9f300ad2 GIT binary patch literal 11955 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 I$v3|K4>s!+W&i*H literal 0 HcmV?d00001 diff --git a/runtime/wasm/Cargo.toml b/runtime/wasm/Cargo.toml index 0e6e5d64100..3e74e9f985e 100644 --- a/runtime/wasm/Cargo.toml +++ b/runtime/wasm/Cargo.toml @@ -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 } diff --git a/runtime/wasm/src/asc_abi/class.rs b/runtime/wasm/src/asc_abi/class.rs index 366ff844b08..1fae1ad9ce0 100644 --- a/runtime/wasm/src/asc_abi/class.rs +++ b/runtime/wasm/src/asc_abi/class.rs @@ -398,6 +398,17 @@ impl AscIndexId for Array> { const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::ArrayBigDecimal; } +impl AscIndexId for Array>> { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlArrayValue; +} + +impl AscIndexId + for Array, AscEnum>>> +{ + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = + IndexForAscTypeId::YamlArrayTypedMapEntryValueValue; +} + /// Represents any `AscValue` since they all fit in 64 bits. #[repr(C)] #[derive(Copy, Clone, Default)] @@ -505,6 +516,10 @@ impl AscIndexId for AscEnum { const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::JsonValue; } +impl AscIndexId for AscEnum { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlValue; +} + pub type AscEnumArray = AscPtr>>>; #[repr(u32)] @@ -613,6 +628,10 @@ impl AscIndexId for AscTypedMapEntry> { const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::TypedMapEntryStringJsonValue; } +impl AscIndexId for AscTypedMapEntry, AscEnum> { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlTypedMapEntryValueValue; +} + pub(crate) type AscTypedMapEntryArray = Array>>; #[repr(C)] @@ -638,6 +657,10 @@ impl AscIndexId for AscTypedMap, AscEnum> { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlTypedMapValueValue; +} + pub type AscEntity = AscTypedMap>; pub(crate) type AscJson = AscTypedMap>; @@ -725,6 +748,10 @@ impl AscIndexId for AscResult>, bool> { const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::ResultJsonValueBool; } +impl AscIndexId for AscResult>, bool> { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlResultValueBool; +} + #[repr(C)] #[derive(AscType, Copy, Clone)] pub struct AscWrapped { @@ -742,3 +769,54 @@ impl AscIndexId for AscWrapped { impl AscIndexId for AscWrapped>> { const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::WrappedJsonValue; } + +impl AscIndexId for AscWrapped>> { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlWrappedValue; +} + +#[repr(u32)] +#[derive(AscType, Clone, Copy)] +pub enum YamlValueKind { + Null, + Bool, + Number, + String, + Array, + Object, + Tagged, +} + +impl Default for YamlValueKind { + fn default() -> Self { + YamlValueKind::Null + } +} + +impl AscValue for YamlValueKind {} + +impl YamlValueKind { + pub(crate) fn get_kind(value: &serde_yaml::Value) -> Self { + use serde_yaml::Value; + + match value { + Value::Null => Self::Null, + Value::Bool(_) => Self::Bool, + Value::Number(_) => Self::Number, + Value::String(_) => Self::String, + Value::Sequence(_) => Self::Array, + Value::Mapping(_) => Self::Object, + Value::Tagged(_) => Self::Tagged, + } + } +} + +#[repr(C)] +#[derive(AscType)] +pub struct AscYamlTaggedValue { + pub tag: AscPtr, + pub value: AscPtr>, +} + +impl AscIndexId for AscYamlTaggedValue { + const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::YamlTaggedValue; +} diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index b9735cc1e0d..bd1c8706c4a 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -1236,6 +1236,36 @@ impl HostExports { .map(|mut tokens| tokens.pop().unwrap()) .context("Failed to decode") } + + pub(crate) fn yaml_from_bytes( + &self, + bytes: &[u8], + gas: &GasCounter, + state: &mut BlockState, + ) -> Result { + const YAML_MAX_SIZE_BYTES: usize = 10_000_000; + + Self::track_gas_and_ops( + gas, + state, + gas::YAML_FROM_BYTES.with_args(complexity::Size, bytes), + "yaml_from_bytes", + )?; + + if bytes.len() > YAML_MAX_SIZE_BYTES { + return Err(DeterministicHostError::Other( + anyhow!( + "YAML size exceeds max size of {} bytes", + YAML_MAX_SIZE_BYTES + ) + .into(), + )); + } + + serde_yaml::from_slice(bytes) + .context("failed to parse YAML from bytes") + .map_err(DeterministicHostError::from) + } } fn string_to_h160(string: &str) -> Result { diff --git a/runtime/wasm/src/module/context.rs b/runtime/wasm/src/module/context.rs index ddf8eba3f1d..03cbf244c23 100644 --- a/runtime/wasm/src/module/context.rs +++ b/runtime/wasm/src/module/context.rs @@ -1188,4 +1188,64 @@ impl WasmInstanceContext<'_> { "`box.profile` has been removed." ))) } + + /// function yaml.fromBytes(bytes: Bytes): YAMLValue + pub fn yaml_from_bytes( + &mut self, + gas: &GasCounter, + bytes_ptr: AscPtr, + ) -> Result>, HostExportError> { + let bytes: Vec = asc_get(self, bytes_ptr, gas)?; + let host_exports = self.as_ref().ctx.host_exports.cheap_clone(); + let ctx = &mut self.as_mut().ctx; + + let yaml_value = host_exports + .yaml_from_bytes(&bytes, gas, &mut ctx.state) + .inspect_err(|_| { + debug!( + &self.as_ref().ctx.logger, + "Failed to parse YAML from byte array"; + "bytes" => truncate_yaml_bytes_for_logging(&bytes), + ); + })?; + + asc_new(self, &yaml_value, gas) + } + + /// function yaml.try_fromBytes(bytes: Bytes): Result + pub fn yaml_try_from_bytes( + &mut self, + gas: &GasCounter, + bytes_ptr: AscPtr, + ) -> Result>, bool>>, HostExportError> { + let bytes: Vec = asc_get(self, bytes_ptr, gas)?; + let host_exports = self.as_ref().ctx.host_exports.cheap_clone(); + let ctx = &mut self.as_mut().ctx; + + let result = host_exports + .yaml_from_bytes(&bytes, gas, &mut ctx.state) + .map_err(|err| { + warn!( + &self.as_ref().ctx.logger, + "Failed to parse YAML from byte array"; + "bytes" => truncate_yaml_bytes_for_logging(&bytes), + "error" => format!("{:#}", err), + ); + + true + }); + + asc_new(self, &result, gas) + } +} + +/// For debugging, it might be useful to know exactly which bytes could not be parsed as YAML, but +/// since we can parse large YAML documents, even one bad mapping could produce terabytes of logs. +/// To avoid this, we only log the first 1024 bytes of the failed YAML source. +fn truncate_yaml_bytes_for_logging(bytes: &[u8]) -> String { + if bytes.len() > 1024 { + return format!("(truncated) 0x{}", hex::encode(&bytes[..1024])); + } + + format!("0x{}", hex::encode(bytes)) } diff --git a/runtime/wasm/src/module/instance.rs b/runtime/wasm/src/module/instance.rs index 55d3e8574d2..63845e81c60 100644 --- a/runtime/wasm/src/module/instance.rs +++ b/runtime/wasm/src/module/instance.rs @@ -468,6 +468,9 @@ impl WasmInstance { link!("json.toF64", json_to_f64, ptr); link!("json.toBigInt", json_to_big_int, ptr); + link!("yaml.fromBytes", yaml_from_bytes, ptr); + link!("yaml.try_fromBytes", yaml_try_from_bytes, ptr); + link!("crypto.keccak256", crypto_keccak_256, ptr); link!("bigInt.plus", big_int_plus, x_ptr, y_ptr); diff --git a/runtime/wasm/src/to_from/external.rs b/runtime/wasm/src/to_from/external.rs index 30740e77696..9bbe0298abc 100644 --- a/runtime/wasm/src/to_from/external.rs +++ b/runtime/wasm/src/to_from/external.rs @@ -506,3 +506,54 @@ impl ToAscObj for EntitySourceOperation { impl AscIndexId for AscEntityTrigger { const INDEX_ASC_TYPE_ID: IndexForAscTypeId = IndexForAscTypeId::AscEntityTrigger; } + +impl ToAscObj> for serde_yaml::Value { + fn to_asc_obj( + &self, + heap: &mut H, + gas: &GasCounter, + ) -> Result, HostExportError> { + use serde_yaml::Value; + + let payload = match self { + Value::Null => EnumPayload(0), + Value::Bool(val) => EnumPayload::from(*val), + Value::Number(val) => asc_new(heap, &val.to_string(), gas)?.into(), + Value::String(val) => asc_new(heap, val, gas)?.into(), + Value::Sequence(val) => asc_new(heap, val.as_slice(), gas)?.into(), + Value::Mapping(val) => asc_new(heap, val, gas)?.into(), + Value::Tagged(val) => asc_new(heap, val.as_ref(), gas)?.into(), + }; + + Ok(AscEnum { + kind: YamlValueKind::get_kind(self), + _padding: 0, + payload, + }) + } +} + +impl ToAscObj, AscEnum>> for serde_yaml::Mapping { + fn to_asc_obj( + &self, + heap: &mut H, + gas: &GasCounter, + ) -> Result, AscEnum>, HostExportError> { + Ok(AscTypedMap { + entries: asc_new(heap, &*self.iter().collect::>(), gas)?, + }) + } +} + +impl ToAscObj for serde_yaml::value::TaggedValue { + fn to_asc_obj( + &self, + heap: &mut H, + gas: &GasCounter, + ) -> Result { + Ok(AscYamlTaggedValue { + tag: asc_new(heap, &self.tag.to_string(), gas)?, + value: asc_new(heap, &self.value, gas)?, + }) + } +}