From b77f7637472c3a37ecb5f527f0fbce613c42ebe4 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 23 Mar 2021 00:00:16 -0400 Subject: [PATCH 01/16] Add initial parser --- packages/parse-mapping-lookup/.gitignore | 1 + packages/parse-mapping-lookup/.madgerc | 13 ++ packages/parse-mapping-lookup/README.md | 9 ++ packages/parse-mapping-lookup/package.json | 84 +++++++++++ packages/parse-mapping-lookup/src/ast.ts | 111 ++++++++++++++ packages/parse-mapping-lookup/src/index.ts | 7 + .../parse-mapping-lookup/src/parser.spec.ts | 87 +++++++++++ packages/parse-mapping-lookup/src/parser.ts | 139 ++++++++++++++++++ packages/parse-mapping-lookup/tsconfig.json | 39 +++++ packages/parse-mapping-lookup/typedoc.js | 7 + yarn.lock | 15 ++ 11 files changed, 512 insertions(+) create mode 100644 packages/parse-mapping-lookup/.gitignore create mode 100644 packages/parse-mapping-lookup/.madgerc create mode 100644 packages/parse-mapping-lookup/README.md create mode 100644 packages/parse-mapping-lookup/package.json create mode 100644 packages/parse-mapping-lookup/src/ast.ts create mode 100644 packages/parse-mapping-lookup/src/index.ts create mode 100644 packages/parse-mapping-lookup/src/parser.spec.ts create mode 100644 packages/parse-mapping-lookup/src/parser.ts create mode 100644 packages/parse-mapping-lookup/tsconfig.json create mode 100644 packages/parse-mapping-lookup/typedoc.js diff --git a/packages/parse-mapping-lookup/.gitignore b/packages/parse-mapping-lookup/.gitignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/packages/parse-mapping-lookup/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/parse-mapping-lookup/.madgerc b/packages/parse-mapping-lookup/.madgerc new file mode 100644 index 00000000000..cd2b9575208 --- /dev/null +++ b/packages/parse-mapping-lookup/.madgerc @@ -0,0 +1,13 @@ +{ + "excludeRegExp": [ + "\\.\\.", + "test" + ], + "fileExtensions": ["ts"], + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + }, + "tsConfig": "./tsconfig.json" +} diff --git a/packages/parse-mapping-lookup/README.md b/packages/parse-mapping-lookup/README.md new file mode 100644 index 00000000000..317a276e9ac --- /dev/null +++ b/packages/parse-mapping-lookup/README.md @@ -0,0 +1,9 @@ +# `@truffle/parse-mapping-lookup` + +## Usage + +```typescript +import { parse } from "@truffle/parse-mapping-lookup"; + +parse("ledger.balances[0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e]"); +``` diff --git a/packages/parse-mapping-lookup/package.json b/packages/parse-mapping-lookup/package.json new file mode 100644 index 00000000000..4fe836aa1ee --- /dev/null +++ b/packages/parse-mapping-lookup/package.json @@ -0,0 +1,84 @@ +{ + "name": "@truffle/parse-mapping-lookup", + "version": "0.1.0", + "description": "Parse pointers to mapping/struct innards (e.g. m[0])", + "keywords": [ + "smart-contract", + "truffle" + ], + "author": "g. nicholas d'andrea ", + "homepage": "https://github.com/trufflesuite/truffle#readme", + "license": "MIT", + "main": "dist/src/index.js", + "directories": { + "dist": "dist" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/trufflesuite/truffle.git", + "directory": "packages/parse-mapping-lookup" + }, + "scripts": { + "build": "yarn build:dist && yarn build:docs", + "clean": "rm -rf ./dist", + "prepare": "yarn build", + "build:dist": "ttsc", + "build:docs": "typedoc", + "madge": "madge ./src/index.ts", + "test": "jest --verbose --detectOpenHandles --forceExit --passWithNoTests" + }, + "devDependencies": { + "@types/jest": "^26.0.20", + "@types/node": "12.12.21", + "jest": "^26.5.2", + "madge": "^3.11.0", + "ts-jest": "^26.4.1", + "ts-node": "^9.1.1", + "tsconfig-paths": "^3.9.0", + "ttypescript": "^1.5.7", + "typedoc": "^0.20.19", + "typescript": "^4.1.4", + "typescript-transform-paths": "^2.1.0" + }, + "bugs": { + "url": "https://github.com/trufflesuite/truffle/issues" + }, + "dependencies": { + "parjs": "^0.12.7" + }, + "jest": { + "moduleFileExtensions": [ + "ts", + "js", + "json", + "node" + ], + "testEnvironment": "node", + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "globals": { + "ts-jest": { + "tsConfig": "/tsconfig.json", + "diagnostics": true + } + }, + "moduleNameMapper": { + "^@truffle/parse-mapping-lookup/(.*)": "/src/$1", + "^@truffle/parse-mapping-lookup$": "/src", + "^test/(.*)": "/test/$1" + }, + "testMatch": [ + "/src/**/test/*\\.(ts|js)", + "/test/**/test/*\\.(ts|js)", + "/src/**/*\\.(spec|test)\\.(ts|js)", + "/test/**/*\\.(spec|test)\\.(ts|js)" + ] + } +} diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts new file mode 100644 index 00000000000..e3e04346db5 --- /dev/null +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -0,0 +1,111 @@ +import debugModule from "debug"; +const debug = debugModule("parse-mapping-lookup:ast"); + +/* + * Expression + */ + +export interface Expression { + kind: "expression"; + root: Identifier; + pointer: Pointer; +} + +export const expression = (options: { + root: Identifier; + pointer: Pointer; +}): Expression => { + const { root, pointer } = options; + return { + kind: "expression", + root, + pointer + }; +}; + +/* + * Identifier + */ + +export interface Identifier { + kind: "identifier"; + name: string; +} + +export const identifier = (options: { name: string }): Identifier => { + const { name } = options; + return { + kind: "identifier", + name + }; +}; + +/* + * Pointer + */ + +export interface Pointer { + kind: "pointer"; + path: Step[]; +} + +export type Step = Access | Lookup; + +export const pointer = (options: { path: Step[] }): Pointer => { + const { path } = options; + return { + kind: "pointer", + path + }; +}; + +/* + * Lookup + */ + +export interface Lookup { + kind: "lookup"; + property: Identifier; +} + +export const lookup = (options: { property: Identifier }): Lookup => { + const { property } = options; + return { + kind: "lookup", + property + }; +}; + +/* + * Access + */ + +export interface Access { + kind: "access"; + index: Literal; +} + +export const access = (options: { index: Literal }): Access => { + const { index } = options; + return { + kind: "access", + index + }; +}; + +/* + * Literal + */ + +export interface Literal { + kind: "literal"; + value: string; +} + +export const literal = (options: { value: string }): Literal => { + const { value } = options; + return { + kind: "literal", + value + }; +}; diff --git a/packages/parse-mapping-lookup/src/index.ts b/packages/parse-mapping-lookup/src/index.ts new file mode 100644 index 00000000000..d1b6342fe87 --- /dev/null +++ b/packages/parse-mapping-lookup/src/index.ts @@ -0,0 +1,7 @@ +import debugModule from "debug"; +const debug = debugModule("parse-mapping-lookup"); + +export { parse } from "./parser"; + +import * as Ast from "./ast"; +export { Ast }; diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts new file mode 100644 index 00000000000..45c867b13e2 --- /dev/null +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -0,0 +1,87 @@ +import { parse } from "@truffle/parse-mapping-lookup/parser"; + +import { + expression, + access, + lookup, + identifier, + pointer, + literal +} from "@truffle/parse-mapping-lookup/ast"; + +const testCases = [ + { + expression: `m[0]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [access({ index: literal({ value: "0" }) })] + }) + }) + }, + { + expression: `m[0][1]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [ + access({ index: literal({ value: "0" }) }), + access({ index: literal({ value: "1" }) }) + ] + }) + }) + }, + { + expression: `m["hello"]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [access({ index: literal({ value: "hello" }) })] + }) + }) + }, + { + expression: `m["\\""]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [access({ index: literal({ value: '"' }) })] + }) + }) + }, + { + expression: `s.m[0]`, + result: expression({ + root: identifier({ name: "s" }), + pointer: pointer({ + path: [ + lookup({ property: identifier({ name: "m" }) }), + access({ index: literal({ value: "0" }) }) + ] + }) + }) + }, + { + expression: `m[0].k[1]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [ + access({ index: literal({ value: "0" }) }), + lookup({ property: identifier({ name: "k" }) }), + access({ index: literal({ value: "1" }) }) + ] + }) + }) + } +]; + +describe("@truffle/parse-mapping-lookup", () => { + for (const { expression, result: expected } of testCases) { + it(`parses: ${expression}`, () => { + const result = parse(expression); + + expect(result).toEqual(expected); + }); + } +}); diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts new file mode 100644 index 00000000000..e75e26f921d --- /dev/null +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -0,0 +1,139 @@ +import debugModule from "debug"; +const debug = debugModule("parse-mapping-lookup:parser"); + +import { + regexp, + anyCharOf, + noCharOf, + stringLen, + string, + int, + float +} from "parjs"; +import { + qthen, + map, + many, + stringify, + between, + or, + then +} from "parjs/combinators"; + +import { + access, + expression, + identifier, + literal, + lookup, + pointer +} from "./ast"; + +/* + * Identifier + */ + +const identifierP = regexp(/[a-zA-Z][a-zA-Z0-9]*/).pipe( + map(([name]) => identifier({ name })) +); + +/* + * Literal + */ + +namespace Strings { + // borrowed from parjs JSON example + // https://github.com/GregRos/parjs/blob/master/src/examples/json.ts + + const escapeChars = { + '"': `"`, + "\\": "\\", + "/": "/", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t" + }; + + const escapeCharP = anyCharOf(Object.keys(escapeChars).join()).pipe( + map(char => escapeChars[char] as string) + ); + + // A unicode escape sequence is "u" followed by exactly 4 hex digits + const unicodeEscapeP = string("u").pipe( + qthen( + stringLen(4).pipe( + map(str => parseInt(str, 16)), + map(x => String.fromCharCode(x)) + ) + ) + ); + + // Any escape sequence begins with a \ + const escapeP = string("\\").pipe( + qthen(escapeCharP.pipe(or(unicodeEscapeP))) + ); + + // Here we process regular characters vs escape sequences + const stringEntriesP = escapeP.pipe(or(noCharOf('"'))); + + // Repeat the char/escape to get a sequence, and then put between quotes to get a string + export const stringP = stringEntriesP.pipe(many(), stringify(), between('"')); +} + +const numberP = int().pipe(or(float())); + +const stringP = Strings.stringP; + +const literalP = numberP.pipe( + or(stringP), + map(value => literal({ value: value.toString() })) +); + +/* + * Lookup + */ + +const lookupP = string(".").pipe( + then(identifierP), + map(([_, property]) => lookup({ property })) +); + +/* + * Access + */ + +const accessP = literalP.pipe( + between(string("["), string("]")), + map(index => access({ index })) +); + +/* + * Pointer + */ + +const stepP = lookupP.pipe(or(accessP)); + +const pointerP = stepP.pipe( + then(stepP.pipe(many())), + map(([first, rest]) => pointer({ path: [first, ...rest] })) +); + +/* + * Expression + */ + +const expressionP = identifierP.pipe( + then(pointerP), + map(([root, pointer]) => expression({ root, pointer })) +); + +export const parse = (expression: string) => { + const result = expressionP.parse(expression); + + if (result.isOk) { + return result.value; + } else { + throw new Error("Parse error"); + } +}; diff --git a/packages/parse-mapping-lookup/tsconfig.json b/packages/parse-mapping-lookup/tsconfig.json new file mode 100644 index 00000000000..a41063460f1 --- /dev/null +++ b/packages/parse-mapping-lookup/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "sourceMap": true, + "declaration": true, + "esModuleInterop": true, + "lib": ["esnext"], + "skipLibCheck": true, + "target": "es6", + "moduleResolution": "node", + "downlevelIteration": true, + "allowSyntheticDefaultImports": true, + "module": "commonjs", + "outDir": "./dist", + "strictBindCallApply": true, + "strictNullChecks": true, + "paths": { + "@truffle/parse-mapping-lookup": ["./src"], + "@truffle/parse-mapping-lookup/*": ["./src/*"], + "test/*": ["./test/*"] + }, + "rootDir": ".", + "baseUrl": ".", + "types": [ + "jest", + "node" + ], + "plugins": [ + { "transform": "typescript-transform-paths" }, + { "transform": "typescript-transform-paths", "afterDeclarations": true } + ] + }, + "include": [ + "src/**/*", + "test/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/parse-mapping-lookup/typedoc.js b/packages/parse-mapping-lookup/typedoc.js new file mode 100644 index 00000000000..1a72dc813d6 --- /dev/null +++ b/packages/parse-mapping-lookup/typedoc.js @@ -0,0 +1,7 @@ +module.exports = { + entryPoints: ["src/index.ts"], + categorizeByGroup: false, + readme: "none", + plugin: ["none"], + out: "dist/docs" +}; diff --git a/yarn.lock b/yarn.lock index 90b491b8292..7f30cc9d8c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8585,6 +8585,13 @@ change-case@^4.1.1: snake-case "^3.0.4" tslib "^2.0.3" +char-info@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/char-info/-/char-info-0.3.2.tgz#d4c4d034245c864d1ab17152cd31746b3bd4f0d0" + integrity sha512-6P6KW8crZx+N857wZalB4FreR+PhheSLmAk22c8zbQsFhsDxZP1aTDfmOjrzddgp1IBOl53b0Z8kDQrwh4B//A== + dependencies: + node-interval-tree "^1.3.3" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -22666,6 +22673,14 @@ parents@^1.0.0, parents@^1.0.1: dependencies: path-platform "~0.11.15" +parjs@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/parjs/-/parjs-0.12.7.tgz#9dde4975dbbb2f48d4b484524cb359d02395fb72" + integrity sha512-aKw+vsSMBdZNdxcetIwi5mnmFckuwxTz7dKu07wEzrNjKYtvhRlrlla3b4GAEmGMyAWhrocLrUcbSGvmMZMU3Q== + dependencies: + char-info "^0.3.1" + lodash "^4.17.13" + parse-asn1@^5.0.0: version "5.1.5" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" From 3b7e98c3fcba0a033d98e20f2b7a25276e23164c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 22 Apr 2021 21:29:47 -0400 Subject: [PATCH 02/16] Handle _ and $ --- packages/parse-mapping-lookup/src/parser.spec.ts | 6 +++--- packages/parse-mapping-lookup/src/parser.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index 45c867b13e2..ba10f919f16 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -62,13 +62,13 @@ const testCases = [ }) }, { - expression: `m[0].k[1]`, + expression: `m$[0]._k[1]`, result: expression({ - root: identifier({ name: "m" }), + root: identifier({ name: "m$" }), pointer: pointer({ path: [ access({ index: literal({ value: "0" }) }), - lookup({ property: identifier({ name: "k" }) }), + lookup({ property: identifier({ name: "_k" }) }), access({ index: literal({ value: "1" }) }) ] }) diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index e75e26f921d..7833a09ad4d 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -33,7 +33,7 @@ import { * Identifier */ -const identifierP = regexp(/[a-zA-Z][a-zA-Z0-9]*/).pipe( +const identifierP = regexp(/[a-zA-Z_$][a-zA-Z0-9_$]*/).pipe( map(([name]) => identifier({ name })) ); From 003641dbd51050c544688925dc2b4db778fd2350 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Fri, 23 Apr 2021 18:20:17 -0400 Subject: [PATCH 03/16] Use more explicit names than Access and Lookup --- packages/parse-mapping-lookup/src/ast.ts | 24 ++++++++++--------- .../parse-mapping-lookup/src/parser.spec.ts | 24 +++++++++---------- packages/parse-mapping-lookup/src/parser.ts | 18 +++++++------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index e3e04346db5..3f34bfa33a6 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -49,7 +49,7 @@ export interface Pointer { path: Step[]; } -export type Step = Access | Lookup; +export type Step = IndexAccess | MemberLookup; export const pointer = (options: { path: Step[] }): Pointer => { const { path } = options; @@ -60,35 +60,37 @@ export const pointer = (options: { path: Step[] }): Pointer => { }; /* - * Lookup + * MemberLookup */ -export interface Lookup { - kind: "lookup"; +export interface MemberLookup { + kind: "member-lookup"; property: Identifier; } -export const lookup = (options: { property: Identifier }): Lookup => { +export const memberLookup = (options: { + property: Identifier; +}): MemberLookup => { const { property } = options; return { - kind: "lookup", + kind: "member-lookup", property }; }; /* - * Access + * IndexAccess */ -export interface Access { - kind: "access"; +export interface IndexAccess { + kind: "index-access"; index: Literal; } -export const access = (options: { index: Literal }): Access => { +export const indexAccess = (options: { index: Literal }): IndexAccess => { const { index } = options; return { - kind: "access", + kind: "index-access", index }; }; diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index ba10f919f16..ab1a1de7496 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -2,8 +2,8 @@ import { parse } from "@truffle/parse-mapping-lookup/parser"; import { expression, - access, - lookup, + indexAccess, + memberLookup, identifier, pointer, literal @@ -15,7 +15,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [access({ index: literal({ value: "0" }) })] + path: [indexAccess({ index: literal({ value: "0" }) })] }) }) }, @@ -25,8 +25,8 @@ const testCases = [ root: identifier({ name: "m" }), pointer: pointer({ path: [ - access({ index: literal({ value: "0" }) }), - access({ index: literal({ value: "1" }) }) + indexAccess({ index: literal({ value: "0" }) }), + indexAccess({ index: literal({ value: "1" }) }) ] }) }) @@ -36,7 +36,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [access({ index: literal({ value: "hello" }) })] + path: [indexAccess({ index: literal({ value: "hello" }) })] }) }) }, @@ -45,7 +45,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [access({ index: literal({ value: '"' }) })] + path: [indexAccess({ index: literal({ value: '"' }) })] }) }) }, @@ -55,8 +55,8 @@ const testCases = [ root: identifier({ name: "s" }), pointer: pointer({ path: [ - lookup({ property: identifier({ name: "m" }) }), - access({ index: literal({ value: "0" }) }) + memberLookup({ property: identifier({ name: "m" }) }), + indexAccess({ index: literal({ value: "0" }) }) ] }) }) @@ -67,9 +67,9 @@ const testCases = [ root: identifier({ name: "m$" }), pointer: pointer({ path: [ - access({ index: literal({ value: "0" }) }), - lookup({ property: identifier({ name: "_k" }) }), - access({ index: literal({ value: "1" }) }) + indexAccess({ index: literal({ value: "0" }) }), + memberLookup({ property: identifier({ name: "_k" }) }), + indexAccess({ index: literal({ value: "1" }) }) ] }) }) diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index 7833a09ad4d..208b4f32302 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -21,11 +21,11 @@ import { } from "parjs/combinators"; import { - access, + indexAccess, expression, identifier, literal, - lookup, + memberLookup, pointer } from "./ast"; @@ -91,28 +91,28 @@ const literalP = numberP.pipe( ); /* - * Lookup + * MemberLookup */ -const lookupP = string(".").pipe( +const memberLookupP = string(".").pipe( then(identifierP), - map(([_, property]) => lookup({ property })) + map(([_, property]) => memberLookup({ property })) ); /* - * Access + * IndexAccess */ -const accessP = literalP.pipe( +const indexAccessP = literalP.pipe( between(string("["), string("]")), - map(index => access({ index })) + map(index => indexAccess({ index })) ); /* * Pointer */ -const stepP = lookupP.pipe(or(accessP)); +const stepP = memberLookupP.pipe(or(indexAccessP)); const pointerP = stepP.pipe( then(stepP.pipe(many())), From 8a6588f19898305548c71ca16fbe5685f4397ca7 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Fri, 23 Apr 2021 22:06:04 -0400 Subject: [PATCH 04/16] Expand support for different kinds of literals - Ensure Literal interface includes `type` field as discriminator - Add some generics infrasturcture for building AST constructors - Add support for boolean literals --- packages/parse-mapping-lookup/src/ast.ts | 91 +++++++++++++++++-- .../parse-mapping-lookup/src/parser.spec.ts | 22 +++-- packages/parse-mapping-lookup/src/parser.ts | 24 ++++- 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index 3f34bfa33a6..6b6e4e5e7c4 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -95,19 +95,98 @@ export const indexAccess = (options: { index: Literal }): IndexAccess => { }; }; +namespace LiteralGenerics { + type Literals = { + [type: string]: { + interface: { + kind: "literal"; + type: string; + value: any; + }; + value: any; + } + }; + + type LiteralName = string & keyof L; + type Literal> = L[N]; + + type Definition> = {}; + type Definitions = { + [N in LiteralName]: Definition; + }; + + type Constructor> = + (options: { value: Literal["value"] }) => Literal["interface"]; + + export type ConstructorName> = + `${N}Literal`; + type UnionToIntersection = + (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never; + export type Constructors = UnionToIntersection<{ + [N in LiteralName]: { + [K in ConstructorName]: Constructor; + } + }[LiteralName]>; + + const makeConstructor = >( + type: N + ): Constructor => ({ value }) => ({ + kind: "literal", + type, + value + }); + + export const makeConstructors = ( + definitions: Definitions + ): Constructors => Object.keys(definitions) + .map(type => ({ [`${type}Literal`]: makeConstructor(type) })) + .reduce((a, b) => ({ ...a, ...b }), {}) as Constructors; +} + /* * Literal */ export interface Literal { kind: "literal"; + type: string; + value: any; +} + +export interface BooleanLiteral extends Literal { + type: "boolean"; + value: boolean; +} + +export interface NumberLiteral extends Literal { + type: "number"; value: string; } -export const literal = (options: { value: string }): Literal => { - const { value } = options; - return { - kind: "literal", - value +export interface StringLiteral extends Literal { + type: "string"; + value: string; +} + +export const { + booleanLiteral, + numberLiteral, + stringLiteral +} = LiteralGenerics.makeConstructors<{ + boolean: { + interface: BooleanLiteral; + value: boolean; }; -}; + number: { + interface: NumberLiteral; + value: string; + }; + string: { + interface: StringLiteral; + value: string; + }; +}>({ + boolean: {}, + number: {}, + string: {} +} as const); diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index ab1a1de7496..fadc571a5e5 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -6,7 +6,9 @@ import { memberLookup, identifier, pointer, - literal + numberLiteral, + stringLiteral, + booleanLiteral } from "@truffle/parse-mapping-lookup/ast"; const testCases = [ @@ -15,7 +17,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: literal({ value: "0" }) })] + path: [indexAccess({ index: numberLiteral({ value: "0" }) })] }) }) }, @@ -25,8 +27,8 @@ const testCases = [ root: identifier({ name: "m" }), pointer: pointer({ path: [ - indexAccess({ index: literal({ value: "0" }) }), - indexAccess({ index: literal({ value: "1" }) }) + indexAccess({ index: numberLiteral({ value: "0" }) }), + indexAccess({ index: numberLiteral({ value: "1" }) }) ] }) }) @@ -36,7 +38,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: literal({ value: "hello" }) })] + path: [indexAccess({ index: stringLiteral({ value: "hello" }) })] }) }) }, @@ -45,7 +47,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: literal({ value: '"' }) })] + path: [indexAccess({ index: stringLiteral({ value: '"' }) })] }) }) }, @@ -56,20 +58,20 @@ const testCases = [ pointer: pointer({ path: [ memberLookup({ property: identifier({ name: "m" }) }), - indexAccess({ index: literal({ value: "0" }) }) + indexAccess({ index: numberLiteral({ value: "0" }) }) ] }) }) }, { - expression: `m$[0]._k[1]`, + expression: `m$[false]._k[true]`, result: expression({ root: identifier({ name: "m$" }), pointer: pointer({ path: [ - indexAccess({ index: literal({ value: "0" }) }), + indexAccess({ index: booleanLiteral({ value: false }) }), memberLookup({ property: identifier({ name: "_k" }) }), - indexAccess({ index: literal({ value: "1" }) }) + indexAccess({ index: booleanLiteral({ value: true }) }) ] }) }) diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index 208b4f32302..c08e0c6a078 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -24,7 +24,9 @@ import { indexAccess, expression, identifier, - literal, + numberLiteral, + stringLiteral, + booleanLiteral, memberLookup, pointer } from "./ast"; @@ -81,13 +83,25 @@ namespace Strings { export const stringP = stringEntriesP.pipe(many(), stringify(), between('"')); } -const numberP = int().pipe(or(float())); +const numberP = int().pipe( + or(float()), + map(value => numberLiteral({ value: value.toString() })) +); + +const stringP = Strings.stringP.pipe( + map(value => stringLiteral({ value })) +); -const stringP = Strings.stringP; +const booleanP = string("true").pipe( + or(string("false")), + map((value: "true" | "false") => booleanLiteral({ value: value === "true" })) +); const literalP = numberP.pipe( - or(stringP), - map(value => literal({ value: value.toString() })) + or( + stringP, + booleanP + ) ); /* From 0de488576a8d903dc84bd01e5b56606e5818657a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Fri, 23 Apr 2021 23:17:15 -0400 Subject: [PATCH 05/16] Handle Solidity escape sequences --- .../parse-mapping-lookup/src/parser.spec.ts | 11 +++++++++ packages/parse-mapping-lookup/src/parser.ts | 23 +++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index fadc571a5e5..f6b480d4ccf 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -75,6 +75,17 @@ const testCases = [ ] }) }) + }, + { + expression: `m["\\x41"]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [ + indexAccess({ index: stringLiteral({ value: "A" }) }) + ] + }) + }) } ]; diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index c08e0c6a078..ea4e289e337 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -48,19 +48,32 @@ namespace Strings { // https://github.com/GregRos/parjs/blob/master/src/examples/json.ts const escapeChars = { - '"': `"`, + "\n": "\n", "\\": "\\", - "/": "/", + "'": "'", + '"': '"', + "b": "\b", "f": "\f", "n": "\n", "r": "\r", - "t": "\t" + "t": "\t", + "v": "\v", + "/": "/", }; const escapeCharP = anyCharOf(Object.keys(escapeChars).join()).pipe( map(char => escapeChars[char] as string) ); + const hexEscapeP = string("x").pipe( + qthen( + stringLen(2).pipe( + map(str => parseInt(str, 16)), + map(x => String.fromCharCode(x)) + ) + ) + ); + // A unicode escape sequence is "u" followed by exactly 4 hex digits const unicodeEscapeP = string("u").pipe( qthen( @@ -73,7 +86,9 @@ namespace Strings { // Any escape sequence begins with a \ const escapeP = string("\\").pipe( - qthen(escapeCharP.pipe(or(unicodeEscapeP))) + qthen( + escapeCharP.pipe(or(unicodeEscapeP, hexEscapeP)) + ) ); // Here we process regular characters vs escape sequences From 9bd698b3aecefcec1084b85babb349ead90bd291 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Fri, 23 Apr 2021 23:49:07 -0400 Subject: [PATCH 06/16] Add support for hex literals --- packages/parse-mapping-lookup/src/ast.ts | 15 +++++++-- .../parse-mapping-lookup/src/parser.spec.ts | 32 +++++++++++++++++-- packages/parse-mapping-lookup/src/parser.ts | 24 ++++++++++---- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index 6b6e4e5e7c4..469550592d2 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -168,10 +168,16 @@ export interface StringLiteral extends Literal { value: string; } +export interface HexLiteral extends Literal { + type: "hex"; + value: string; +} + export const { booleanLiteral, numberLiteral, - stringLiteral + stringLiteral, + hexLiteral } = LiteralGenerics.makeConstructors<{ boolean: { interface: BooleanLiteral; @@ -185,8 +191,13 @@ export const { interface: StringLiteral; value: string; }; + hex: { + interface: HexLiteral; + value: string; + } }>({ boolean: {}, number: {}, - string: {} + string: {}, + hex: {} } as const); diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index f6b480d4ccf..c9573c1623b 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -8,7 +8,8 @@ import { pointer, numberLiteral, stringLiteral, - booleanLiteral + booleanLiteral, + hexLiteral } from "@truffle/parse-mapping-lookup/ast"; const testCases = [ @@ -86,13 +87,38 @@ const testCases = [ ] }) }) + }, + { + expression: `m[`, + errors: true + }, + { + expression: `m[hex"deadbeef"]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [ + indexAccess({ index: hexLiteral({ value: "0xdeadbeef" }) }) + ] + }) + }) + }, + { + expression: `m[hex"deadbee"]`, + errors: true } ]; describe("@truffle/parse-mapping-lookup", () => { - for (const { expression, result: expected } of testCases) { + for (const { expression, errors = false, result: expected } of testCases) { it(`parses: ${expression}`, () => { - const result = parse(expression); + let result; + try { + result = parse(expression); + } catch (error) { + expect(errors).toEqual(true); + return; + } expect(result).toEqual(expected); }); diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index ea4e289e337..0d64ac5e7ee 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -7,13 +7,16 @@ import { noCharOf, stringLen, string, + digit, int, float } from "parjs"; import { qthen, map, + exactly, many, + manyBetween, stringify, between, or, @@ -27,6 +30,7 @@ import { numberLiteral, stringLiteral, booleanLiteral, + hexLiteral, memberLookup, pointer } from "./ast"; @@ -98,24 +102,32 @@ namespace Strings { export const stringP = stringEntriesP.pipe(many(), stringify(), between('"')); } -const numberP = int().pipe( +const numberLiteralP = int().pipe( or(float()), map(value => numberLiteral({ value: value.toString() })) ); -const stringP = Strings.stringP.pipe( +const stringLiteralP = Strings.stringP.pipe( map(value => stringLiteral({ value })) ); -const booleanP = string("true").pipe( +const booleanLiteralP = string("true").pipe( or(string("false")), map((value: "true" | "false") => booleanLiteral({ value: value === "true" })) ); -const literalP = numberP.pipe( +const hexLiteralP = digit(16).pipe(exactly(2)).pipe( + map(pair => pair.join("")), + manyBetween(string(`hex"`), string(`"`)), + map(pairs => pairs.join("")), + map(value => hexLiteral({ value: `0x${value}` })) +); + +const literalP = numberLiteralP.pipe( or( - stringP, - booleanP + stringLiteralP, + booleanLiteralP, + hexLiteralP ) ); From a11b8f1576ad333e5dd5a0433a60bda7d75d02a3 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sat, 24 Apr 2021 00:28:07 -0400 Subject: [PATCH 07/16] [squashme] Simplify LiteralGenerics a bit --- packages/parse-mapping-lookup/src/ast.ts | 41 +++++++++--------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index 469550592d2..b267ed6eae2 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -98,17 +98,18 @@ export const indexAccess = (options: { index: Literal }): IndexAccess => { namespace LiteralGenerics { type Literals = { [type: string]: { - interface: { - kind: "literal"; - type: string; - value: any; - }; + kind: "literal"; + type: string; value: any; } }; type LiteralName = string & keyof L; - type Literal> = L[N]; + type Literal> = { + kind: "literal"; + type: N; + value: L[N]["value"]; + }; type Definition> = {}; type Definitions = { @@ -116,7 +117,7 @@ namespace LiteralGenerics { }; type Constructor> = - (options: { value: Literal["value"] }) => Literal["interface"]; + (options: Pick, "value">) => Literal; export type ConstructorName> = `${N}Literal`; @@ -130,8 +131,8 @@ namespace LiteralGenerics { const makeConstructor = >( type: N - ): Constructor => ({ value }) => ({ - kind: "literal", + ): Constructor => ({ value }: Pick, "value">) => ({ + kind: "literal" as const, type, value }); @@ -177,24 +178,12 @@ export const { booleanLiteral, numberLiteral, stringLiteral, - hexLiteral + hexLiteral, } = LiteralGenerics.makeConstructors<{ - boolean: { - interface: BooleanLiteral; - value: boolean; - }; - number: { - interface: NumberLiteral; - value: string; - }; - string: { - interface: StringLiteral; - value: string; - }; - hex: { - interface: HexLiteral; - value: string; - } + boolean: BooleanLiteral; + number: NumberLiteral; + string: StringLiteral; + hex: HexLiteral; }>({ boolean: {}, number: {}, From 01198c9c36dd5f584cf11590253bb89e5cdc7fc7 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sat, 24 Apr 2021 00:48:32 -0400 Subject: [PATCH 08/16] [squashme] Separate it() blocks for erroroneous expressions --- .../parse-mapping-lookup/src/parser.spec.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index c9573c1623b..0fc8b9315ea 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -111,16 +111,17 @@ const testCases = [ describe("@truffle/parse-mapping-lookup", () => { for (const { expression, errors = false, result: expected } of testCases) { - it(`parses: ${expression}`, () => { - let result; - try { - result = parse(expression); - } catch (error) { - expect(errors).toEqual(true); - return; - } - - expect(result).toEqual(expected); - }); + if (errors) { + it(`fails to parse: ${expression}`, () => { + expect(() => { + return parse(expression); + }).toThrow(); + }); + } else { + it(`parses: ${expression}`, () => { + const result = parse(expression); + expect(result).toEqual(expected); + }); + } } }); From ffee51654d735baca2f032d83c23a440ae9aa726 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Sat, 24 Apr 2021 00:48:54 -0400 Subject: [PATCH 09/16] Add support for enum literals --- packages/parse-mapping-lookup/src/ast.ts | 14 +++++- .../parse-mapping-lookup/src/parser.spec.ts | 44 ++++++++++++++++++- packages/parse-mapping-lookup/src/parser.ts | 31 ++++++++++++- 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index b267ed6eae2..6a7eccbb946 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -174,19 +174,31 @@ export interface HexLiteral extends Literal { value: string; } +export interface EnumLiteral extends Literal { + type: "enum"; + value: { + contract?: Identifier; + enumeration: Identifier; + member: Identifier; + }; +} + export const { booleanLiteral, numberLiteral, stringLiteral, hexLiteral, + enumLiteral } = LiteralGenerics.makeConstructors<{ boolean: BooleanLiteral; number: NumberLiteral; string: StringLiteral; hex: HexLiteral; + enum: EnumLiteral; }>({ boolean: {}, number: {}, string: {}, - hex: {} + hex: {}, + enum: {} } as const); diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index 0fc8b9315ea..ba89c93e48c 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -9,7 +9,8 @@ import { numberLiteral, stringLiteral, booleanLiteral, - hexLiteral + hexLiteral, + enumLiteral } from "@truffle/parse-mapping-lookup/ast"; const testCases = [ @@ -106,6 +107,47 @@ const testCases = [ { expression: `m[hex"deadbee"]`, errors: true + }, + { + expression: `m[Direction.North]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [ + indexAccess({ + index: enumLiteral({ + value: { + enumeration: identifier({ name: "Direction" }), + member: identifier({ name: "North" }) + } + }) + }) + ] + }) + }) + }, + { + expression: `m[Geography.Direction.North]`, + result: expression({ + root: identifier({ name: "m" }), + pointer: pointer({ + path: [ + indexAccess({ + index: enumLiteral({ + value: { + contract: identifier({ name: "Geography" }), + enumeration: identifier({ name: "Direction" }), + member: identifier({ name: "North" }) + } + }) + }) + ] + }) + }) + }, + { + expression: `m[North]`, + errors: true } ]; diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index 0d64ac5e7ee..c445162f452 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -15,6 +15,7 @@ import { qthen, map, exactly, + maybe, many, manyBetween, stringify, @@ -31,6 +32,7 @@ import { stringLiteral, booleanLiteral, hexLiteral, + enumLiteral, memberLookup, pointer } from "./ast"; @@ -123,11 +125,38 @@ const hexLiteralP = digit(16).pipe(exactly(2)).pipe( map(value => hexLiteral({ value: `0x${value}` })) ); +const enumLiteralP = identifierP.pipe( + then( + string(".").pipe( + qthen(identifierP) + ), + string(".").pipe( + qthen(identifierP), + maybe() + ) + ), +).pipe( + map(args => { + if (args[2]) { + const [contract, enumeration, member] = args; + return enumLiteral({ + value: { contract, enumeration, member } + }); + } else { + const [enumeration, member] = args; + return enumLiteral({ + value: { enumeration, member } + }); + } + }) +); + const literalP = numberLiteralP.pipe( or( stringLiteralP, booleanLiteralP, - hexLiteralP + hexLiteralP, + enumLiteralP ) ); From a2db4bd7297814a84b6ba6ef952ba645bddfaf0e Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 27 Apr 2021 22:58:13 -0400 Subject: [PATCH 10/16] Upgrade prettier to ^2.2.1 --- package.json | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ca4713b965f..2ebe31ab17d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lerna-update-wizard": "^0.16.0", "lint-staged": "^10.4.2", "nyc": "^13.0.1", - "prettier": "^2.0.5", + "prettier": "^2.2.1", "prs-merged-since": "^1.1.0" }, "workspaces": { diff --git a/yarn.lock b/yarn.lock index 7f30cc9d8c8..99507ad6aac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23595,6 +23595,11 @@ prettier@^2.1.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== +prettier@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" From 90e7d63ceee7c3ff05f8d9dc27cab15114518a5b Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 27 Apr 2021 22:59:45 -0400 Subject: [PATCH 11/16] Add eslintrc for package --- packages/parse-mapping-lookup/.eslintrc.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/parse-mapping-lookup/.eslintrc.json diff --git a/packages/parse-mapping-lookup/.eslintrc.json b/packages/parse-mapping-lookup/.eslintrc.json new file mode 100644 index 00000000000..3e111aad02c --- /dev/null +++ b/packages/parse-mapping-lookup/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": ["**/*.spec.ts"], + "env": { + "jest": true + } + } + ] +} From c743bc6d5533c8de32537707f21fd27ac98e878e Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Tue, 27 Apr 2021 23:00:14 -0400 Subject: [PATCH 12/16] Reduce complexity of literals --- packages/parse-mapping-lookup/src/ast.ts | 75 +++++++----------- .../parse-mapping-lookup/src/parser.spec.ts | 62 +++------------ packages/parse-mapping-lookup/src/parser.ts | 78 ++----------------- 3 files changed, 46 insertions(+), 169 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index 6a7eccbb946..ce8f46debe1 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -101,7 +101,7 @@ namespace LiteralGenerics { kind: "literal"; type: string; value: any; - } + }; }; type LiteralName = string & keyof L; @@ -116,18 +116,24 @@ namespace LiteralGenerics { [N in LiteralName]: Definition; }; - type Constructor> = - (options: Pick, "value">) => Literal; - - export type ConstructorName> = - `${N}Literal`; - type UnionToIntersection = - (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never; - export type Constructors = UnionToIntersection<{ - [N in LiteralName]: { - [K in ConstructorName]: Constructor; - } - }[LiteralName]>; + type Constructor> = ( + options: Pick, "value"> + ) => Literal; + + // prettier-ignore + export type ConstructorName> = `${N}Literal`; + type UnionToIntersection = ( + U extends any ? (k: U) => void : never + ) extends (k: infer I) => void + ? I + : never; + export type Constructors = UnionToIntersection< + { + [N in LiteralName]: { + [K in ConstructorName]: Constructor; + }; + }[LiteralName] + >; const makeConstructor = >( type: N @@ -139,9 +145,10 @@ namespace LiteralGenerics { export const makeConstructors = ( definitions: Definitions - ): Constructors => Object.keys(definitions) - .map(type => ({ [`${type}Literal`]: makeConstructor(type) })) - .reduce((a, b) => ({ ...a, ...b }), {}) as Constructors; + ): Constructors => + Object.keys(definitions) + .map(type => ({ [`${type}Literal`]: makeConstructor(type) })) + .reduce((a, b) => ({ ...a, ...b }), {}) as Constructors; } /* @@ -154,51 +161,23 @@ export interface Literal { value: any; } -export interface BooleanLiteral extends Literal { - type: "boolean"; - value: boolean; -} - -export interface NumberLiteral extends Literal { - type: "number"; - value: string; -} - export interface StringLiteral extends Literal { type: "string"; value: string; } -export interface HexLiteral extends Literal { +export interface ValueLiteral extends Literal { type: "hex"; value: string; } -export interface EnumLiteral extends Literal { - type: "enum"; - value: { - contract?: Identifier; - enumeration: Identifier; - member: Identifier; - }; -} - export const { - booleanLiteral, - numberLiteral, stringLiteral, - hexLiteral, - enumLiteral + valueLiteral } = LiteralGenerics.makeConstructors<{ - boolean: BooleanLiteral; - number: NumberLiteral; string: StringLiteral; - hex: HexLiteral; - enum: EnumLiteral; + value: ValueLiteral; }>({ - boolean: {}, - number: {}, string: {}, - hex: {}, - enum: {} + value: {} } as const); diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index ba89c93e48c..ee3519b6b82 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -6,11 +6,8 @@ import { memberLookup, identifier, pointer, - numberLiteral, stringLiteral, - booleanLiteral, - hexLiteral, - enumLiteral + valueLiteral } from "@truffle/parse-mapping-lookup/ast"; const testCases = [ @@ -19,19 +16,16 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: numberLiteral({ value: "0" }) })] + path: [indexAccess({ index: valueLiteral({ value: "0" }) })] }) }) }, { - expression: `m[0][1]`, + expression: `m[0x0]`, result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [ - indexAccess({ index: numberLiteral({ value: "0" }) }), - indexAccess({ index: numberLiteral({ value: "1" }) }) - ] + path: [indexAccess({ index: valueLiteral({ value: "0x0" }) })] }) }) }, @@ -60,7 +54,7 @@ const testCases = [ pointer: pointer({ path: [ memberLookup({ property: identifier({ name: "m" }) }), - indexAccess({ index: numberLiteral({ value: "0" }) }) + indexAccess({ index: valueLiteral({ value: "0" }) }) ] }) }) @@ -71,9 +65,9 @@ const testCases = [ root: identifier({ name: "m$" }), pointer: pointer({ path: [ - indexAccess({ index: booleanLiteral({ value: false }) }), + indexAccess({ index: valueLiteral({ value: "false" }) }), memberLookup({ property: identifier({ name: "_k" }) }), - indexAccess({ index: booleanLiteral({ value: true }) }) + indexAccess({ index: valueLiteral({ value: "true" }) }) ] }) }) @@ -83,9 +77,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [ - indexAccess({ index: stringLiteral({ value: "A" }) }) - ] + path: [indexAccess({ index: stringLiteral({ value: "A" }) })] }) }) }, @@ -98,16 +90,10 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [ - indexAccess({ index: hexLiteral({ value: "0xdeadbeef" }) }) - ] + path: [indexAccess({ index: valueLiteral({ value: `hex"deadbeef"` }) })] }) }) }, - { - expression: `m[hex"deadbee"]`, - errors: true - }, { expression: `m[Direction.North]`, result: expression({ @@ -115,39 +101,13 @@ const testCases = [ pointer: pointer({ path: [ indexAccess({ - index: enumLiteral({ - value: { - enumeration: identifier({ name: "Direction" }), - member: identifier({ name: "North" }) - } - }) - }) - ] - }) - }) - }, - { - expression: `m[Geography.Direction.North]`, - result: expression({ - root: identifier({ name: "m" }), - pointer: pointer({ - path: [ - indexAccess({ - index: enumLiteral({ - value: { - contract: identifier({ name: "Geography" }), - enumeration: identifier({ name: "Direction" }), - member: identifier({ name: "North" }) - } + index: valueLiteral({ + value: "Direction.North" }) }) ] }) }) - }, - { - expression: `m[North]`, - errors: true } ]; diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index c445162f452..d211a3f52b6 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -1,23 +1,11 @@ import debugModule from "debug"; const debug = debugModule("parse-mapping-lookup:parser"); -import { - regexp, - anyCharOf, - noCharOf, - stringLen, - string, - digit, - int, - float -} from "parjs"; +import { regexp, anyCharOf, noCharOf, stringLen, string } from "parjs"; import { qthen, map, - exactly, - maybe, many, - manyBetween, stringify, between, or, @@ -28,11 +16,8 @@ import { indexAccess, expression, identifier, - numberLiteral, stringLiteral, - booleanLiteral, - hexLiteral, - enumLiteral, + valueLiteral, memberLookup, pointer } from "./ast"; @@ -64,7 +49,7 @@ namespace Strings { "r": "\r", "t": "\t", "v": "\v", - "/": "/", + "/": "/" }; const escapeCharP = anyCharOf(Object.keys(escapeChars).join()).pipe( @@ -92,9 +77,7 @@ namespace Strings { // Any escape sequence begins with a \ const escapeP = string("\\").pipe( - qthen( - escapeCharP.pipe(or(unicodeEscapeP, hexEscapeP)) - ) + qthen(escapeCharP.pipe(or(unicodeEscapeP, hexEscapeP))) ); // Here we process regular characters vs escape sequences @@ -104,61 +87,16 @@ namespace Strings { export const stringP = stringEntriesP.pipe(many(), stringify(), between('"')); } -const numberLiteralP = int().pipe( - or(float()), - map(value => numberLiteral({ value: value.toString() })) -); - const stringLiteralP = Strings.stringP.pipe( map(value => stringLiteral({ value })) ); -const booleanLiteralP = string("true").pipe( - or(string("false")), - map((value: "true" | "false") => booleanLiteral({ value: value === "true" })) +const valueLiteralP = noCharOf("]").pipe( + many(), + map(value => valueLiteral({ value: value.join("") })) ); -const hexLiteralP = digit(16).pipe(exactly(2)).pipe( - map(pair => pair.join("")), - manyBetween(string(`hex"`), string(`"`)), - map(pairs => pairs.join("")), - map(value => hexLiteral({ value: `0x${value}` })) -); - -const enumLiteralP = identifierP.pipe( - then( - string(".").pipe( - qthen(identifierP) - ), - string(".").pipe( - qthen(identifierP), - maybe() - ) - ), -).pipe( - map(args => { - if (args[2]) { - const [contract, enumeration, member] = args; - return enumLiteral({ - value: { contract, enumeration, member } - }); - } else { - const [enumeration, member] = args; - return enumLiteral({ - value: { enumeration, member } - }); - } - }) -); - -const literalP = numberLiteralP.pipe( - or( - stringLiteralP, - booleanLiteralP, - hexLiteralP, - enumLiteralP - ) -); +const literalP = stringLiteralP.pipe(or(valueLiteralP)); /* * MemberLookup From e47bc35e4b17cdc85051bab4b3a76ee41444770b Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 28 Apr 2021 02:03:40 -0400 Subject: [PATCH 13/16] Simultaneously define parsers and AST --- packages/parse-mapping-lookup/src/ast.ts | 192 ++---------------- packages/parse-mapping-lookup/src/grammar.ts | 75 +++++++ packages/parse-mapping-lookup/src/index.ts | 2 +- .../src/meta/constructors.ts | 36 ++++ .../parse-mapping-lookup/src/meta/forms.ts | 47 +++++ .../parse-mapping-lookup/src/meta/index.ts | 19 ++ .../parse-mapping-lookup/src/meta/parsers.ts | 97 +++++++++ .../parse-mapping-lookup/src/parser.spec.ts | 37 ++-- packages/parse-mapping-lookup/src/parser.ts | 156 ++------------ packages/parse-mapping-lookup/src/string.ts | 63 ++++++ 10 files changed, 385 insertions(+), 339 deletions(-) create mode 100644 packages/parse-mapping-lookup/src/grammar.ts create mode 100644 packages/parse-mapping-lookup/src/meta/constructors.ts create mode 100644 packages/parse-mapping-lookup/src/meta/forms.ts create mode 100644 packages/parse-mapping-lookup/src/meta/index.ts create mode 100644 packages/parse-mapping-lookup/src/meta/parsers.ts create mode 100644 packages/parse-mapping-lookup/src/string.ts diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index ce8f46debe1..7da19354172 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -1,183 +1,25 @@ import debugModule from "debug"; const debug = debugModule("parse-mapping-lookup:ast"); -/* - * Expression - */ +import { Forms, definitions } from "./grammar"; +import { Node, makeConstructors } from "./meta"; -export interface Expression { - kind: "expression"; - root: Identifier; - pointer: Pointer; -} +export type Identifier = Node; +export type StringLiteral = Node; +export type ValueLiteral = Node; +export type IndexAccess = Node; +export type MemberLookup = Node; +export type Pointer = Node; +export type Expression = Node; -export const expression = (options: { - root: Identifier; - pointer: Pointer; -}): Expression => { - const { root, pointer } = options; - return { - kind: "expression", - root, - pointer - }; -}; - -/* - * Identifier - */ - -export interface Identifier { - kind: "identifier"; - name: string; -} - -export const identifier = (options: { name: string }): Identifier => { - const { name } = options; - return { - kind: "identifier", - name - }; -}; - -/* - * Pointer - */ - -export interface Pointer { - kind: "pointer"; - path: Step[]; -} - -export type Step = IndexAccess | MemberLookup; - -export const pointer = (options: { path: Step[] }): Pointer => { - const { path } = options; - return { - kind: "pointer", - path - }; -}; - -/* - * MemberLookup - */ - -export interface MemberLookup { - kind: "member-lookup"; - property: Identifier; -} - -export const memberLookup = (options: { - property: Identifier; -}): MemberLookup => { - const { property } = options; - return { - kind: "member-lookup", - property - }; -}; - -/* - * IndexAccess - */ - -export interface IndexAccess { - kind: "index-access"; - index: Literal; -} - -export const indexAccess = (options: { index: Literal }): IndexAccess => { - const { index } = options; - return { - kind: "index-access", - index - }; -}; - -namespace LiteralGenerics { - type Literals = { - [type: string]: { - kind: "literal"; - type: string; - value: any; - }; - }; - - type LiteralName = string & keyof L; - type Literal> = { - kind: "literal"; - type: N; - value: L[N]["value"]; - }; - - type Definition> = {}; - type Definitions = { - [N in LiteralName]: Definition; - }; - - type Constructor> = ( - options: Pick, "value"> - ) => Literal; - - // prettier-ignore - export type ConstructorName> = `${N}Literal`; - type UnionToIntersection = ( - U extends any ? (k: U) => void : never - ) extends (k: infer I) => void - ? I - : never; - export type Constructors = UnionToIntersection< - { - [N in LiteralName]: { - [K in ConstructorName]: Constructor; - }; - }[LiteralName] - >; - - const makeConstructor = >( - type: N - ): Constructor => ({ value }: Pick, "value">) => ({ - kind: "literal" as const, - type, - value - }); - - export const makeConstructors = ( - definitions: Definitions - ): Constructors => - Object.keys(definitions) - .map(type => ({ [`${type}Literal`]: makeConstructor(type) })) - .reduce((a, b) => ({ ...a, ...b }), {}) as Constructors; -} - -/* - * Literal - */ - -export interface Literal { - kind: "literal"; - type: string; - value: any; -} - -export interface StringLiteral extends Literal { - type: "string"; - value: string; -} - -export interface ValueLiteral extends Literal { - type: "hex"; - value: string; -} +const constructors = makeConstructors({ definitions }); export const { + identifier, stringLiteral, - valueLiteral -} = LiteralGenerics.makeConstructors<{ - string: StringLiteral; - value: ValueLiteral; -}>({ - string: {}, - value: {} -} as const); + valueLiteral, + indexAccess, + memberLookup, + pointer, + expression +} = constructors; diff --git a/packages/parse-mapping-lookup/src/grammar.ts b/packages/parse-mapping-lookup/src/grammar.ts new file mode 100644 index 00000000000..fc788d9b3d8 --- /dev/null +++ b/packages/parse-mapping-lookup/src/grammar.ts @@ -0,0 +1,75 @@ +import { string, regexp, noCharOf } from "parjs"; +import { between, map, then, qthen, many, or } from "parjs/combinators"; + +import { Definitions } from "./meta"; + +import { solidityString } from "./string"; + +export type Forms = { + identifier: { + name: { type: string }; + }; + stringLiteral: { + contents: { type: string }; + }; + valueLiteral: { + contents: { type: string }; + }; + indexAccess: { + index: { kind: "stringLiteral" | "valueLiteral" }; + }; + memberLookup: { + property: { kind: "identifier" }; + }; + pointer: { + path: Array<{ kind: "memberLookup" | "indexAccess" }>; + }; + expression: { + root: { kind: "identifier" }; + pointer: { kind: "pointer" }; + }; +}; + +export const definitions: Definitions = { + identifier: ({ construct }) => + regexp(/[a-zA-Z_$][a-zA-Z0-9_$]*/).pipe( + map(([name]) => construct({ name })) + ), + + stringLiteral: ({ construct }) => + solidityString.pipe(map(contents => construct({ contents }))), + + valueLiteral: ({ construct }) => + noCharOf("]").pipe( + many(), + map(characters => construct({ contents: characters.join("") })) + ), + + indexAccess: ({ construct, tie }) => + tie("stringLiteral").pipe( + or(tie("valueLiteral")), + between(string("["), string("]")), + map(index => construct({ index })) + ), + + memberLookup: ({ construct, tie }) => + string(".").pipe( + qthen(tie("identifier")), + map(property => construct({ property })) + ), + + pointer: ({ construct, tie }) => { + const stepP = tie("memberLookup").pipe(or(tie("indexAccess"))); + + return stepP.pipe( + then(stepP.pipe(many())), + map(([first, rest]) => construct({ path: [first, ...rest] })) + ); + }, + + expression: ({ construct, tie }) => + tie("identifier").pipe( + then(tie("pointer")), + map(([root, pointer]) => construct({ root, pointer })) + ) +}; diff --git a/packages/parse-mapping-lookup/src/index.ts b/packages/parse-mapping-lookup/src/index.ts index d1b6342fe87..e2e1e490f28 100644 --- a/packages/parse-mapping-lookup/src/index.ts +++ b/packages/parse-mapping-lookup/src/index.ts @@ -1,7 +1,7 @@ import debugModule from "debug"; const debug = debugModule("parse-mapping-lookup"); -export { parse } from "./parser"; +export { parseExpression } from "./parser"; import * as Ast from "./ast"; export { Ast }; diff --git a/packages/parse-mapping-lookup/src/meta/constructors.ts b/packages/parse-mapping-lookup/src/meta/constructors.ts new file mode 100644 index 00000000000..c3b42407d75 --- /dev/null +++ b/packages/parse-mapping-lookup/src/meta/constructors.ts @@ -0,0 +1,36 @@ +import type { Forms, FormKind, Node } from "./forms"; + +export type Constructors = { + [K in FormKind]: Constructor; +}; + +export type Constructor> = ( + fields: Omit, "kind"> +) => Node; + +export type MakeConstructorOptions> = { + kind: K; +}; + +const makeConstructor = >( + options: MakeConstructorOptions +): Constructor => { + const { kind } = options; + return fields => ({ kind, ...fields } as Node); +}; + +export type Definition> = any; +export type MakeConstructorsOptions = { + definitions: { + [K in FormKind]: Definition; + }; +}; + +export const makeConstructors = ( + options: MakeConstructorsOptions +): Constructors => { + const { definitions } = options; + return Object.keys(definitions) + .map(kind => ({ [kind]: makeConstructor({ kind }) })) + .reduce((a, b) => ({ ...a, ...b }), {}) as Constructors; +}; diff --git a/packages/parse-mapping-lookup/src/meta/forms.ts b/packages/parse-mapping-lookup/src/meta/forms.ts new file mode 100644 index 00000000000..ea11416c6f6 --- /dev/null +++ b/packages/parse-mapping-lookup/src/meta/forms.ts @@ -0,0 +1,47 @@ +export type Forms = { + [kind: string]: { + [name: string]: { kind: string } | { kind: string }[] | { type: any }; + }; +}; + +export type FormKind = string & keyof F; +export type Form> = F[K]; + +export type FormFields> = Form; + +export type FormFieldName> = string & + keyof FormFields; + +export type FormField< + F extends Forms, + K extends FormKind, + N extends FormFieldName +> = FormFields[N]; + +export type FormFieldKind< + F extends Forms, + K extends FormKind, + _N extends FormFieldName, + T extends any +> = "kind" extends keyof T ? Node> : never; + +type _FormFieldNode< + F extends Forms, + K extends FormKind, + N extends FormFieldName, + T extends any +> = T extends (infer I)[] + ? _FormFieldNode[] + : "type" extends keyof T + ? T["type"] + : FormFieldKind; + +export type FormFieldNode< + F extends Forms, + K extends FormKind, + N extends FormFieldName +> = _FormFieldNode>; + +export type Node> = { kind: K } & { + [N in FormFieldName]: FormFieldNode; +}; diff --git a/packages/parse-mapping-lookup/src/meta/index.ts b/packages/parse-mapping-lookup/src/meta/index.ts new file mode 100644 index 00000000000..7c77506719b --- /dev/null +++ b/packages/parse-mapping-lookup/src/meta/index.ts @@ -0,0 +1,19 @@ +import type { Node, Forms, FormKind } from "./forms"; +export { Node }; + +import type * as Constructors from "./constructors"; +import { makeConstructors } from "./constructors"; +export { makeConstructors }; + +import type * as Parsers from "./parsers"; +import { makeParsers } from "./parsers"; +export { makeParsers }; + +export type Definition< + F extends Forms, + K extends FormKind +> = Constructors.Definition & Parsers.Definition; + +export type Definitions = { + [K in FormKind]: Definition; +}; diff --git a/packages/parse-mapping-lookup/src/meta/parsers.ts b/packages/parse-mapping-lookup/src/meta/parsers.ts new file mode 100644 index 00000000000..cccebfdac93 --- /dev/null +++ b/packages/parse-mapping-lookup/src/meta/parsers.ts @@ -0,0 +1,97 @@ +import type { Parjser } from "parjs"; + +import type { Forms, FormKind, Node } from "./forms"; + +import type { Constructors, Constructor } from "./constructors"; + +export type ParserCombinator> = Parjser< + Node +>; + +export type Tie = >( + kind: K +) => ParserCombinator; + +export type Definition> = (options: { + construct: Constructor; + tie: Tie; +}) => ParserCombinator; + +export type MakeParserCombinatorOptions< + F extends Forms, + K extends FormKind +> = { + kind: K; + definition: Definition; + construct: Constructor; + tie: Tie; +}; + +const makeParserCombinator = >( + options: MakeParserCombinatorOptions +): ParserCombinator => { + const { definition: parser, tie, construct } = options; + + return parser({ construct, tie }); +}; + +// prettier-ignore +type ParserName> = /* eslint-disable no-undef */ `parse${Capitalize}`; + +export type Parsers = UnionToIntersection< + { + [K in FormKind]: { + [N in ParserName]: ParserCombinator["parse"]; + }; + }[FormKind] +>; + +export type MakeParsersOptions = { + definitions: { + [K in FormKind]: Definition; + }; + constructors: Constructors; +}; + +export const makeParsers = ( + options: MakeParsersOptions +): Parsers => { + const { definitions, constructors } = options; + const combinators: Partial> = {}; + + function tie>(kind: K) { + if (kind in combinators) { + // @ts-ignore + return combinators[kind]; + } + + const definition = definitions[kind]; + // @ts-ignore + const construct = constructors[kind]; + + // @ts-ignore + combinators[kind] = makeParserCombinator({ + kind, + definition, + tie, + construct + }); + // @ts-ignore + return combinators[kind]; + } + + return Object.keys(definitions) + .map(kind => [kind, `parse${kind.charAt(0).toUpperCase() + kind.slice(1)}`]) + .map(([kind, parserName]) => { + const combinator = tie(kind); + const parser = combinator.parse.bind(combinator); + return { [parserName]: parser }; + }) + .reduce((a, b) => ({ ...a, ...b }), {}) as Parsers; +}; + +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I +) => void + ? I + : never; diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index ee3519b6b82..d08864760f7 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -1,5 +1,4 @@ -import { parse } from "@truffle/parse-mapping-lookup/parser"; - +import { parseExpression } from "@truffle/parse-mapping-lookup/parser"; import { expression, indexAccess, @@ -16,7 +15,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: valueLiteral({ value: "0" }) })] + path: [indexAccess({ index: valueLiteral({ contents: "0" }) })] }) }) }, @@ -25,7 +24,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: valueLiteral({ value: "0x0" }) })] + path: [indexAccess({ index: valueLiteral({ contents: "0x0" }) })] }) }) }, @@ -34,7 +33,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: stringLiteral({ value: "hello" }) })] + path: [indexAccess({ index: stringLiteral({ contents: "hello" }) })] }) }) }, @@ -43,7 +42,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: stringLiteral({ value: '"' }) })] + path: [indexAccess({ index: stringLiteral({ contents: '"' }) })] }) }) }, @@ -54,7 +53,7 @@ const testCases = [ pointer: pointer({ path: [ memberLookup({ property: identifier({ name: "m" }) }), - indexAccess({ index: valueLiteral({ value: "0" }) }) + indexAccess({ index: valueLiteral({ contents: "0" }) }) ] }) }) @@ -65,9 +64,9 @@ const testCases = [ root: identifier({ name: "m$" }), pointer: pointer({ path: [ - indexAccess({ index: valueLiteral({ value: "false" }) }), + indexAccess({ index: valueLiteral({ contents: "false" }) }), memberLookup({ property: identifier({ name: "_k" }) }), - indexAccess({ index: valueLiteral({ value: "true" }) }) + indexAccess({ index: valueLiteral({ contents: "true" }) }) ] }) }) @@ -77,7 +76,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: stringLiteral({ value: "A" }) })] + path: [indexAccess({ index: stringLiteral({ contents: "A" }) })] }) }) }, @@ -90,7 +89,9 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: valueLiteral({ value: `hex"deadbeef"` }) })] + path: [ + indexAccess({ index: valueLiteral({ contents: `hex"deadbeef"` }) }) + ] }) }) }, @@ -101,9 +102,7 @@ const testCases = [ pointer: pointer({ path: [ indexAccess({ - index: valueLiteral({ - value: "Direction.North" - }) + index: valueLiteral({ contents: "Direction.North" }) }) ] }) @@ -115,14 +114,14 @@ describe("@truffle/parse-mapping-lookup", () => { for (const { expression, errors = false, result: expected } of testCases) { if (errors) { it(`fails to parse: ${expression}`, () => { - expect(() => { - return parse(expression); - }).toThrow(); + const result = parseExpression(expression); + expect(result.isOk).toBeFalsy(); }); } else { it(`parses: ${expression}`, () => { - const result = parse(expression); - expect(result).toEqual(expected); + const result = parseExpression(expression); + expect(result.isOk).toBeTruthy(); + expect(result.value).toEqual(expected); }); } } diff --git a/packages/parse-mapping-lookup/src/parser.ts b/packages/parse-mapping-lookup/src/parser.ts index d211a3f52b6..3f24e49d2ab 100644 --- a/packages/parse-mapping-lookup/src/parser.ts +++ b/packages/parse-mapping-lookup/src/parser.ts @@ -1,147 +1,15 @@ import debugModule from "debug"; const debug = debugModule("parse-mapping-lookup:parser"); -import { regexp, anyCharOf, noCharOf, stringLen, string } from "parjs"; -import { - qthen, - map, - many, - stringify, - between, - or, - then -} from "parjs/combinators"; - -import { - indexAccess, - expression, - identifier, - stringLiteral, - valueLiteral, - memberLookup, - pointer -} from "./ast"; - -/* - * Identifier - */ - -const identifierP = regexp(/[a-zA-Z_$][a-zA-Z0-9_$]*/).pipe( - map(([name]) => identifier({ name })) -); - -/* - * Literal - */ - -namespace Strings { - // borrowed from parjs JSON example - // https://github.com/GregRos/parjs/blob/master/src/examples/json.ts - - const escapeChars = { - "\n": "\n", - "\\": "\\", - "'": "'", - '"': '"', - "b": "\b", - "f": "\f", - "n": "\n", - "r": "\r", - "t": "\t", - "v": "\v", - "/": "/" - }; - - const escapeCharP = anyCharOf(Object.keys(escapeChars).join()).pipe( - map(char => escapeChars[char] as string) - ); - - const hexEscapeP = string("x").pipe( - qthen( - stringLen(2).pipe( - map(str => parseInt(str, 16)), - map(x => String.fromCharCode(x)) - ) - ) - ); - - // A unicode escape sequence is "u" followed by exactly 4 hex digits - const unicodeEscapeP = string("u").pipe( - qthen( - stringLen(4).pipe( - map(str => parseInt(str, 16)), - map(x => String.fromCharCode(x)) - ) - ) - ); - - // Any escape sequence begins with a \ - const escapeP = string("\\").pipe( - qthen(escapeCharP.pipe(or(unicodeEscapeP, hexEscapeP))) - ); - - // Here we process regular characters vs escape sequences - const stringEntriesP = escapeP.pipe(or(noCharOf('"'))); - - // Repeat the char/escape to get a sequence, and then put between quotes to get a string - export const stringP = stringEntriesP.pipe(many(), stringify(), between('"')); -} - -const stringLiteralP = Strings.stringP.pipe( - map(value => stringLiteral({ value })) -); - -const valueLiteralP = noCharOf("]").pipe( - many(), - map(value => valueLiteral({ value: value.join("") })) -); - -const literalP = stringLiteralP.pipe(or(valueLiteralP)); - -/* - * MemberLookup - */ - -const memberLookupP = string(".").pipe( - then(identifierP), - map(([_, property]) => memberLookup({ property })) -); - -/* - * IndexAccess - */ - -const indexAccessP = literalP.pipe( - between(string("["), string("]")), - map(index => indexAccess({ index })) -); - -/* - * Pointer - */ - -const stepP = memberLookupP.pipe(or(indexAccessP)); - -const pointerP = stepP.pipe( - then(stepP.pipe(many())), - map(([first, rest]) => pointer({ path: [first, ...rest] })) -); - -/* - * Expression - */ - -const expressionP = identifierP.pipe( - then(pointerP), - map(([root, pointer]) => expression({ root, pointer })) -); - -export const parse = (expression: string) => { - const result = expressionP.parse(expression); - - if (result.isOk) { - return result.value; - } else { - throw new Error("Parse error"); - } -}; +import type { ParjsResult } from "parjs"; + +import { Forms, definitions } from "./grammar"; +import * as constructors from "./ast"; +import type { Expression } from "./ast"; +import { makeParsers } from "./meta"; + +export const { + parseExpression +}: { + parseExpression(input: string): ParjsResult; +} = makeParsers({ definitions, constructors }); diff --git a/packages/parse-mapping-lookup/src/string.ts b/packages/parse-mapping-lookup/src/string.ts new file mode 100644 index 00000000000..d13fa96ad54 --- /dev/null +++ b/packages/parse-mapping-lookup/src/string.ts @@ -0,0 +1,63 @@ +/** + * Logic for parsing Solidity strings + * + * This borrows from and repurposes the + * [parjs JSON example](https://github.com/GregRos/parjs/blob/master/src/examples/json.ts). + * + * @packageDocumentation + */ + +import { string, anyCharOf, noCharOf, stringLen } from "parjs"; +import { between, map, qthen, many, or, stringify } from "parjs/combinators"; + +const escapeChars = { + "\n": "\n", + "\\": "\\", + "'": "'", + '"': '"', + "b": "\b", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t", + "v": "\v", + "/": "/" +}; + +const escapeCharP = anyCharOf(Object.keys(escapeChars).join()).pipe( + map(char => escapeChars[char] as string) +); + +const hexEscapeP = string("x").pipe( + qthen( + stringLen(2).pipe( + map(str => parseInt(str, 16)), + map(x => String.fromCharCode(x)) + ) + ) +); + +// A unicode escape sequence is "u" followed by exactly 4 hex digits +const unicodeEscapeP = string("u").pipe( + qthen( + stringLen(4).pipe( + map(str => parseInt(str, 16)), + map(x => String.fromCharCode(x)) + ) + ) +); + +// Any escape sequence begins with a \ +const escapeP = string("\\").pipe( + qthen(escapeCharP.pipe(or(unicodeEscapeP, hexEscapeP))) +); + +// Here we process regular characters vs escape sequences +const stringEntriesP = escapeP.pipe(or(noCharOf('"'))); + +// Repeat the char/escape to get a sequence, and then put between quotes to get a string +export const solidityString = stringEntriesP.pipe( + many(), + stringify(), + between('"') +); From a7f6ce7e921e020364ab12671bfe1d3c500c0cbf Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 28 Apr 2021 19:23:24 -0400 Subject: [PATCH 14/16] Remove Literal prefix --- packages/parse-mapping-lookup/src/ast.ts | 8 +++--- packages/parse-mapping-lookup/src/grammar.ts | 14 +++++----- .../parse-mapping-lookup/src/parser.spec.ts | 26 +++++++++---------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/packages/parse-mapping-lookup/src/ast.ts b/packages/parse-mapping-lookup/src/ast.ts index 7da19354172..af450017f3c 100644 --- a/packages/parse-mapping-lookup/src/ast.ts +++ b/packages/parse-mapping-lookup/src/ast.ts @@ -5,8 +5,8 @@ import { Forms, definitions } from "./grammar"; import { Node, makeConstructors } from "./meta"; export type Identifier = Node; -export type StringLiteral = Node; -export type ValueLiteral = Node; +export type String = Node; +export type Value = Node; export type IndexAccess = Node; export type MemberLookup = Node; export type Pointer = Node; @@ -16,8 +16,8 @@ const constructors = makeConstructors({ definitions }); export const { identifier, - stringLiteral, - valueLiteral, + string, + value, indexAccess, memberLookup, pointer, diff --git a/packages/parse-mapping-lookup/src/grammar.ts b/packages/parse-mapping-lookup/src/grammar.ts index fc788d9b3d8..717a64489ff 100644 --- a/packages/parse-mapping-lookup/src/grammar.ts +++ b/packages/parse-mapping-lookup/src/grammar.ts @@ -9,14 +9,14 @@ export type Forms = { identifier: { name: { type: string }; }; - stringLiteral: { + string: { contents: { type: string }; }; - valueLiteral: { + value: { contents: { type: string }; }; indexAccess: { - index: { kind: "stringLiteral" | "valueLiteral" }; + index: { kind: "string" | "value" }; }; memberLookup: { property: { kind: "identifier" }; @@ -36,18 +36,18 @@ export const definitions: Definitions = { map(([name]) => construct({ name })) ), - stringLiteral: ({ construct }) => + string: ({ construct }) => solidityString.pipe(map(contents => construct({ contents }))), - valueLiteral: ({ construct }) => + value: ({ construct }) => noCharOf("]").pipe( many(), map(characters => construct({ contents: characters.join("") })) ), indexAccess: ({ construct, tie }) => - tie("stringLiteral").pipe( - or(tie("valueLiteral")), + tie("string").pipe( + or(tie("value")), between(string("["), string("]")), map(index => construct({ index })) ), diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index d08864760f7..4d38a96b898 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -5,8 +5,8 @@ import { memberLookup, identifier, pointer, - stringLiteral, - valueLiteral + string, + value } from "@truffle/parse-mapping-lookup/ast"; const testCases = [ @@ -15,7 +15,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: valueLiteral({ contents: "0" }) })] + path: [indexAccess({ index: value({ contents: "0" }) })] }) }) }, @@ -24,7 +24,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: valueLiteral({ contents: "0x0" }) })] + path: [indexAccess({ index: value({ contents: "0x0" }) })] }) }) }, @@ -33,7 +33,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: stringLiteral({ contents: "hello" }) })] + path: [indexAccess({ index: string({ contents: "hello" }) })] }) }) }, @@ -42,7 +42,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: stringLiteral({ contents: '"' }) })] + path: [indexAccess({ index: string({ contents: '"' }) })] }) }) }, @@ -53,7 +53,7 @@ const testCases = [ pointer: pointer({ path: [ memberLookup({ property: identifier({ name: "m" }) }), - indexAccess({ index: valueLiteral({ contents: "0" }) }) + indexAccess({ index: value({ contents: "0" }) }) ] }) }) @@ -64,9 +64,9 @@ const testCases = [ root: identifier({ name: "m$" }), pointer: pointer({ path: [ - indexAccess({ index: valueLiteral({ contents: "false" }) }), + indexAccess({ index: value({ contents: "false" }) }), memberLookup({ property: identifier({ name: "_k" }) }), - indexAccess({ index: valueLiteral({ contents: "true" }) }) + indexAccess({ index: value({ contents: "true" }) }) ] }) }) @@ -76,7 +76,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [indexAccess({ index: stringLiteral({ contents: "A" }) })] + path: [indexAccess({ index: string({ contents: "A" }) })] }) }) }, @@ -89,9 +89,7 @@ const testCases = [ result: expression({ root: identifier({ name: "m" }), pointer: pointer({ - path: [ - indexAccess({ index: valueLiteral({ contents: `hex"deadbeef"` }) }) - ] + path: [indexAccess({ index: value({ contents: `hex"deadbeef"` }) })] }) }) }, @@ -102,7 +100,7 @@ const testCases = [ pointer: pointer({ path: [ indexAccess({ - index: valueLiteral({ contents: "Direction.North" }) + index: value({ contents: "Direction.North" }) }) ] }) From b25bbd74d0432decf525dd8d686377e07292f3a6 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 28 Apr 2021 19:55:59 -0400 Subject: [PATCH 15/16] Improve reporting for parse errors - Define custom type names for each form kind in the AST - Update tests to look for one-of `{ trace, value }` --- .../parse-mapping-lookup/src/meta/parsers.ts | 7 ++-- .../parse-mapping-lookup/src/parser.spec.ts | 36 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/parse-mapping-lookup/src/meta/parsers.ts b/packages/parse-mapping-lookup/src/meta/parsers.ts index cccebfdac93..9de60f0f2b6 100644 --- a/packages/parse-mapping-lookup/src/meta/parsers.ts +++ b/packages/parse-mapping-lookup/src/meta/parsers.ts @@ -30,9 +30,12 @@ export type MakeParserCombinatorOptions< const makeParserCombinator = >( options: MakeParserCombinatorOptions ): ParserCombinator => { - const { definition: parser, tie, construct } = options; + const { kind, definition: parser, tie, construct } = options; - return parser({ construct, tie }); + const combinator = parser({ construct, tie }); + return Object.assign(combinator, { + type: `ast:${kind}` + }); }; // prettier-ignore diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index 4d38a96b898..0cdc32b8c43 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -12,7 +12,7 @@ import { const testCases = [ { expression: `m[0]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [indexAccess({ index: value({ contents: "0" }) })] @@ -21,7 +21,7 @@ const testCases = [ }, { expression: `m[0x0]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [indexAccess({ index: value({ contents: "0x0" }) })] @@ -30,7 +30,7 @@ const testCases = [ }, { expression: `m["hello"]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [indexAccess({ index: string({ contents: "hello" }) })] @@ -39,7 +39,7 @@ const testCases = [ }, { expression: `m["\\""]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [indexAccess({ index: string({ contents: '"' }) })] @@ -48,7 +48,7 @@ const testCases = [ }, { expression: `s.m[0]`, - result: expression({ + value: expression({ root: identifier({ name: "s" }), pointer: pointer({ path: [ @@ -60,7 +60,7 @@ const testCases = [ }, { expression: `m$[false]._k[true]`, - result: expression({ + value: expression({ root: identifier({ name: "m$" }), pointer: pointer({ path: [ @@ -73,7 +73,7 @@ const testCases = [ }, { expression: `m["\\x41"]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [indexAccess({ index: string({ contents: "A" }) })] @@ -82,11 +82,13 @@ const testCases = [ }, { expression: `m[`, - errors: true + trace: { + position: 2 + } }, { expression: `m[hex"deadbeef"]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [indexAccess({ index: value({ contents: `hex"deadbeef"` }) })] @@ -95,7 +97,7 @@ const testCases = [ }, { expression: `m[Direction.North]`, - result: expression({ + value: expression({ root: identifier({ name: "m" }), pointer: pointer({ path: [ @@ -109,14 +111,22 @@ const testCases = [ ]; describe("@truffle/parse-mapping-lookup", () => { - for (const { expression, errors = false, result: expected } of testCases) { - if (errors) { + for (const testCase of testCases) { + const { expression } = testCase; + if (testCase.trace) { + const { trace: expected } = testCase; + it(`fails to parse: ${expression}`, () => { const result = parseExpression(expression); - expect(result.isOk).toBeFalsy(); + expect(result.isOk).toBe(false); + expect( + // @ts-ignore + result.trace + ).toMatchObject(expected); }); } else { it(`parses: ${expression}`, () => { + const { value: expected } = testCase; const result = parseExpression(expression); expect(result.isOk).toBeTruthy(); expect(result.value).toEqual(expected); From f8966ace8d7774d353fd3a487f6aa2192aef53cd Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Wed, 28 Apr 2021 19:57:31 -0400 Subject: [PATCH 16/16] Prevent empty index access (`[]`) --- packages/parse-mapping-lookup/src/grammar.ts | 4 ++-- packages/parse-mapping-lookup/src/parser.spec.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/parse-mapping-lookup/src/grammar.ts b/packages/parse-mapping-lookup/src/grammar.ts index 717a64489ff..2f1720819b7 100644 --- a/packages/parse-mapping-lookup/src/grammar.ts +++ b/packages/parse-mapping-lookup/src/grammar.ts @@ -41,8 +41,8 @@ export const definitions: Definitions = { value: ({ construct }) => noCharOf("]").pipe( - many(), - map(characters => construct({ contents: characters.join("") })) + then(noCharOf("]").pipe(many())), + map(([first, rest]) => construct({ contents: [first, ...rest].join("") })) ), indexAccess: ({ construct, tie }) => diff --git a/packages/parse-mapping-lookup/src/parser.spec.ts b/packages/parse-mapping-lookup/src/parser.spec.ts index 0cdc32b8c43..580805469f1 100644 --- a/packages/parse-mapping-lookup/src/parser.spec.ts +++ b/packages/parse-mapping-lookup/src/parser.spec.ts @@ -86,6 +86,12 @@ const testCases = [ position: 2 } }, + { + expression: `m[1].s[]`, + trace: { + position: 7 + } + }, { expression: `m[hex"deadbeef"]`, value: expression({