diff --git a/package-lock.json b/package-lock.json index 37071db9..f4fa1062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "13.0.0-beta.4", + "version": "13.0.0-beta.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "13.0.0-beta.4", + "version": "13.0.0-beta.5", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", @@ -14,7 +14,6 @@ "blake2b": "2.1.3", "buffer": "6.0.3", "json-bigint": "1.0.0", - "json-duplicate-key-handle": "1.0.0", "keccak": "3.0.2", "protobufjs": "7.2.4" }, @@ -1489,11 +1488,6 @@ "babylon": "bin/babylon.js" } }, - "node_modules/backslash": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/backslash/-/backslash-0.2.0.tgz", - "integrity": "sha512-Avs+8FUZ1HF/VFP4YWwHQZSGzRPm37ukU1JQYQWijuHhtXdOuAzcZ8PcAzfIw898a8PyBzdn+RtnKA6MzW0X2A==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3700,14 +3694,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-duplicate-key-handle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-duplicate-key-handle/-/json-duplicate-key-handle-1.0.0.tgz", - "integrity": "sha512-OLIxL+UpfwUsqcLX3i6Z51ChTou/Vje+6bSeGUSubj96dF/SfjObDprLy++ZXYH07KITuEzsXS7PX7e/BGf4jw==", - "dependencies": { - "backslash": "^0.2.0" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6765,11 +6751,6 @@ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", "dev": true }, - "backslash": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/backslash/-/backslash-0.2.0.tgz", - "integrity": "sha512-Avs+8FUZ1HF/VFP4YWwHQZSGzRPm37ukU1JQYQWijuHhtXdOuAzcZ8PcAzfIw898a8PyBzdn+RtnKA6MzW0X2A==" - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8488,14 +8469,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "json-duplicate-key-handle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-duplicate-key-handle/-/json-duplicate-key-handle-1.0.0.tgz", - "integrity": "sha512-OLIxL+UpfwUsqcLX3i6Z51ChTou/Vje+6bSeGUSubj96dF/SfjObDprLy++ZXYH07KITuEzsXS7PX7e/BGf4jw==", - "requires": { - "backslash": "^0.2.0" - } - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/package.json b/package.json index e4cc2286..a8e6852b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "13.0.0-beta.4", + "version": "13.0.0-beta.5", "description": "MultiversX SDK for JavaScript and TypeScript", "main": "out/index.js", "types": "out/index.d.js", @@ -31,7 +31,6 @@ "bech32": "1.1.4", "blake2b": "2.1.3", "buffer": "6.0.3", - "json-duplicate-key-handle": "1.0.0", "keccak": "3.0.2", "protobufjs": "7.2.4" }, diff --git a/src/abi/typeFormula.ts b/src/abi/typeFormula.ts new file mode 100644 index 00000000..ca24926b --- /dev/null +++ b/src/abi/typeFormula.ts @@ -0,0 +1,18 @@ +export class TypeFormula { + name: string; + typeParameters: TypeFormula[]; + + constructor(name: string, typeParameters: TypeFormula[]) { + this.name = name; + this.typeParameters = typeParameters; + } + + toString(): string { + if (this.typeParameters.length > 0) { + const typeParameters = this.typeParameters.map((typeParameter) => typeParameter.toString()).join(", "); + return `${this.name}<${typeParameters}>`; + } else { + return this.name; + } + } +} diff --git a/src/abi/typeFormulaParser.spec.ts b/src/abi/typeFormulaParser.spec.ts new file mode 100644 index 00000000..7271b86b --- /dev/null +++ b/src/abi/typeFormulaParser.spec.ts @@ -0,0 +1,25 @@ +import { assert } from "chai"; +import { TypeFormulaParser } from "./typeFormulaParser"; + +describe("test type formula parser", () => { + it("should parse expression", async () => { + const parser = new TypeFormulaParser(); + + const testVectors = [ + ["i64", "i64"], + [" i64 ", "i64"], + ["utf-8 string", "utf-8 string"], + ["MultiResultVec>", "MultiResultVec>"], + ["tuple3>", "tuple3>"], + ["tuple2", "tuple2"], + ["tuple2 ", "tuple2"], + ["tuple, List>", "tuple, List>"], + ]; + + for (const [inputExpression, expectedExpression] of testVectors) { + const typeFormula = parser.parseExpression(inputExpression); + const outputExpression = typeFormula.toString(); + assert.equal(outputExpression, expectedExpression); + } + }); +}); diff --git a/src/abi/typeFormulaParser.ts b/src/abi/typeFormulaParser.ts new file mode 100644 index 00000000..02e7b961 --- /dev/null +++ b/src/abi/typeFormulaParser.ts @@ -0,0 +1,130 @@ +import { TypeFormula } from "./typeFormula"; + +export class TypeFormulaParser { + static BEGIN_TYPE_PARAMETERS = "<"; + static END_TYPE_PARAMETERS = ">"; + static COMMA = ","; + static PUNCTUATION = [ + TypeFormulaParser.COMMA, + TypeFormulaParser.BEGIN_TYPE_PARAMETERS, + TypeFormulaParser.END_TYPE_PARAMETERS, + ]; + + parseExpression(expression: string): TypeFormula { + expression = expression.trim(); + + const tokens = this.tokenizeExpression(expression).filter((token) => token !== TypeFormulaParser.COMMA); + const stack: any[] = []; + + for (const token of tokens) { + if (this.isPunctuation(token)) { + if (this.isEndOfTypeParameters(token)) { + const typeFormula = this.acquireTypeWithParameters(stack); + stack.push(typeFormula); + } else if (this.isBeginningOfTypeParameters(token)) { + // This symbol is pushed as a simple string. + stack.push(token); + } else { + throw new Error(`Unexpected token (punctuation): ${token}`); + } + } else { + // It's a type name. We push it as a simple string. + stack.push(token); + } + } + + if (stack.length !== 1) { + throw new Error(`Unexpected stack length at end of parsing: ${stack.length}`); + } + if (TypeFormulaParser.PUNCTUATION.includes(stack[0])) { + throw new Error("Unexpected root element."); + } + + const item = stack[0]; + + if (item instanceof TypeFormula) { + return item; + } else if (typeof item === "string") { + // Expression contained a simple, non-generic type. + return new TypeFormula(item, []); + } else { + throw new Error(`Unexpected item on stack: ${item}`); + } + } + + private tokenizeExpression(expression: string): string[] { + const tokens: string[] = []; + let currentToken = ""; + + for (const character of expression) { + if (this.isPunctuation(character)) { + if (currentToken) { + // Retain current token + tokens.push(currentToken.trim()); + // Reset current token + currentToken = ""; + } + + // Punctuation character + tokens.push(character); + } else { + currentToken += character; + } + } + + if (currentToken) { + // Retain the last token (if any). + tokens.push(currentToken.trim()); + } + + return tokens; + } + + private acquireTypeWithParameters(stack: any[]): TypeFormula { + const typeParameters = this.acquireTypeParameters(stack); + const typeName = stack.pop(); + const typeFormula = new TypeFormula(typeName, typeParameters.reverse()); + return typeFormula; + } + + private acquireTypeParameters(stack: any[]): TypeFormula[] { + const typeParameters: TypeFormula[] = []; + + while (true) { + const item = stack.pop(); + + if (item === undefined) { + throw new Error("Badly specified type parameters"); + } + + if (this.isBeginningOfTypeParameters(item)) { + // We've acquired all type parameters. + break; + } + + if (item instanceof TypeFormula) { + // Type parameter is a previously-acquired type. + typeParameters.push(item); + } else if (typeof item === "string") { + // Type parameter is a simple, non-generic type. + typeParameters.push(new TypeFormula(item, [])); + } else { + throw new Error(`Unexpected type parameter object in stack: ${item}`); + } + } + + return typeParameters; + } + + private isPunctuation(token: string): boolean { + return TypeFormulaParser.PUNCTUATION.includes(token); + } + + private isEndOfTypeParameters(token: string): boolean { + return token === TypeFormulaParser.END_TYPE_PARAMETERS; + } + + private isBeginningOfTypeParameters(token: string): boolean { + return token === TypeFormulaParser.BEGIN_TYPE_PARAMETERS; + } +} diff --git a/src/smartcontracts/typesystem/typeExpressionParser.spec.ts b/src/smartcontracts/typesystem/typeExpressionParser.spec.ts index 0465a1b5..0aa09ae2 100644 --- a/src/smartcontracts/typesystem/typeExpressionParser.spec.ts +++ b/src/smartcontracts/typesystem/typeExpressionParser.spec.ts @@ -1,7 +1,7 @@ -import * as errors from "../../errors"; import { assert } from "chai"; -import { Type } from "./types"; +import { ErrTypingSystem } from "../../errors"; import { TypeExpressionParser } from "./typeExpressionParser"; +import { Type } from "./types"; describe("test parser", () => { let parser = new TypeExpressionParser(); @@ -102,54 +102,53 @@ describe("test parser", () => { ], }); - type = parser.parse("MultiArg, List>"); assert.deepEqual(type.toJSON(), { - "name": "MultiArg", - "typeParameters": [ + name: "MultiArg", + typeParameters: [ { - "name": "Option", - "typeParameters": [ + name: "Option", + typeParameters: [ { - "name": "u8", - "typeParameters": [] - } - ] + name: "u8", + typeParameters: [], + }, + ], }, { - "name": "List", - "typeParameters": [ + name: "List", + typeParameters: [ { - "name": "u16", - "typeParameters": [] - } - ] - } - ] + name: "u16", + typeParameters: [], + }, + ], + }, + ], }); type = parser.parse("variadic>"); assert.deepEqual(type.toJSON(), { - "name": "variadic", - "typeParameters": [ + name: "variadic", + typeParameters: [ { - "name": "multi", - "typeParameters": [ + name: "multi", + typeParameters: [ { - "name": "array32", - "typeParameters": [] + name: "array32", + typeParameters: [], }, { - "name": "u32", - "typeParameters": [] + name: "u32", + typeParameters: [], }, { - "name": "array64", - "typeParameters": [] - } - ] - } - ] + name: "array64", + typeParameters: [], + }, + ], + }, + ], }); }); @@ -195,8 +194,6 @@ describe("test parser", () => { ], }); - // TODO: In a future PR, replace the JSON-based parsing logic with a better one and enable this test. - // This test currently fails because JSON key de-duplication takes place: i32 is incorrectly de-duplicated by the parser. type = parser.parse("tuple2"); assert.deepEqual(type.toJSON(), { name: "tuple2", @@ -222,7 +219,7 @@ describe("test parser", () => { { name: "u64", typeParameters: [], - } + }, ], }, { @@ -231,13 +228,78 @@ describe("test parser", () => { { name: "u64", typeParameters: [], - } + }, + ], + }, + ], + }); + }); + + it("should parse ", () => { + let type: Type; + type = parser.parse("variadic>"); + assert.deepEqual(type.toJSON(), { + name: "variadic", + typeParameters: [ + { + name: "multi", + typeParameters: [ + { + name: "BigUint", + typeParameters: [], + }, + { + name: "BigUint", + typeParameters: [], + }, + { + name: "u64", + typeParameters: [], + }, + { + name: "BigUint", + typeParameters: [], + }, ], }, ], }); }); + it("should parse multi", () => { + const type = parser.parse("multi"); + + assert.deepEqual(type.toJSON(), { + name: "multi", + typeParameters: [ + { + name: "u8", + typeParameters: [], + }, + { + name: "utf-8 string", + typeParameters: [], + }, + { + name: "u8", + typeParameters: [], + }, + { + name: "utf-8 string", + typeParameters: [], + }, + { + name: "u8", + typeParameters: [], + }, + { + name: "utf-8 string", + typeParameters: [], + }, + ], + }); + }); + it("should handle utf-8 string types which contain spaces", () => { let type: Type; @@ -264,14 +326,12 @@ describe("test parser", () => { }, ], }); - }); it("should not parse expression", () => { - assert.throw(() => parser.parse("<>"), errors.ErrTypingSystem); - assert.throw(() => parser.parse("<"), errors.ErrTypingSystem); - // TODO: In a future PR replace Json Parsing logic with a better one and enable this test - //assert.throw(() => parser.parse("MultiResultVec"), errors.ErrTypingSystem); - assert.throw(() => parser.parse("a, b"), errors.ErrTypingSystem); + assert.throw(() => parser.parse("<>"), ErrTypingSystem); + assert.throw(() => parser.parse("<"), ErrTypingSystem); + assert.throw(() => parser.parse("MultiResultVec"), ErrTypingSystem); + assert.throw(() => parser.parse("a, b"), ErrTypingSystem); }); }); diff --git a/src/smartcontracts/typesystem/typeExpressionParser.ts b/src/smartcontracts/typesystem/typeExpressionParser.ts index 564bf8be..62d2216c 100644 --- a/src/smartcontracts/typesystem/typeExpressionParser.ts +++ b/src/smartcontracts/typesystem/typeExpressionParser.ts @@ -1,95 +1,31 @@ -import * as errors from "../../errors"; +import { TypeFormula } from "../../abi/typeFormula"; +import { TypeFormulaParser } from "../../abi/typeFormulaParser"; +import { ErrTypingSystem } from "../../errors"; import { Type } from "./types"; -const jsonHandler = require("json-duplicate-key-handle"); export class TypeExpressionParser { - parse(expression: string): Type { - let root = this.doParse(expression); - let rootKeys = Object.keys(root); - - if (rootKeys.length != 1) { - throw new errors.ErrTypingSystem(`bad type expression: ${expression}`); - } + private readonly backingTypeFormulaParser: TypeFormulaParser; - let name = rootKeys[0]; - let type = this.nodeToType(name, root[name]); - return type; + constructor() { + this.backingTypeFormulaParser = new TypeFormulaParser(); } - private doParse(expression: string): any { - let jsoned = this.getJsonedString(expression); - + parse(expression: string): Type { try { - return jsonHandler.parse(jsoned); - } catch (error) { - throw new errors.ErrTypingSystem(`cannot parse type expression: ${expression}. internal json: ${jsoned}.`); + return this.doParse(expression); + } catch (e) { + throw new ErrTypingSystem(`Failed to parse type expression: ${expression}. Error: ${e}`); } } - /** - * Converts a raw type expression to a JSON, parsing-friendly format. - * This is a workaround, so that the parser implementation is simpler (thus we actually rely on the JSON parser). - * - * @param expression a string such as: - * - * ``` - * - Option> - * - VarArgs> - * - MultiResultVec - * ``` - */ - private getJsonedString(expression: string) { - let jsoned = ""; - - for (let i = 0; i < expression.length; i++) { - let char = expression.charAt(i); - let previousChar = expression.charAt(i - 1); - let nextChar = expression.charAt(i + 1); - - if (char == "<") { - jsoned += ": {"; - } else if (char == ">") { - if (previousChar != ">") { - jsoned += ": {} }"; - } else { - jsoned += "}"; - } - } else if (char == ",") { - if (nextChar == ">") { - // Skip superfluous comma - } else if (previousChar == ">") { - jsoned += ","; - } else { - jsoned += ": {},"; - } - } else { - jsoned += char; - } - } - - // Split by the delimiters, but exclude the spaces that are found in the middle of "utf-8 string" - let symbolsRegex = /(:|\{|\}|,|\s)/; - let tokens = jsoned - // Hack for Safari compatibility, where we can't use negative lookbehind - .replace(/utf\-8\sstring/ig, "utf-8-string") - .split(symbolsRegex) - .filter((token) => token); - - jsoned = tokens.map((token) => (symbolsRegex.test(token) ? token : `"${token}"`)) - .map((token) => token.replace(/utf\-8\-string/ig, "utf-8 string")) - .join(""); - - if (tokens.length == 1) { - // Workaround for simple, non-generic types. - return `{${jsoned}: {}}`; - } - - return `{${jsoned}}`; + private doParse(expression: string): Type { + const typeFormula = this.backingTypeFormulaParser.parseExpression(expression); + const type = this.typeFormulaToType(typeFormula); + return type; } - private nodeToType(name: string, node: any): Type { - if (name.charAt(name.length - 1) === "1") { name = name.slice(0, -1); } - let typeParameters = Object.keys(node).map((key) => this.nodeToType(key, node[key])); - return new Type(name, typeParameters); + private typeFormulaToType(typeFormula: TypeFormula): Type { + const typeParameters = typeFormula.typeParameters.map((typeFormula) => this.typeFormulaToType(typeFormula)); + return new Type(typeFormula.name, typeParameters); } }