diff --git a/src/misc/index.ts b/src/misc/index.ts index 4346ab0a..c20074e7 100644 --- a/src/misc/index.ts +++ b/src/misc/index.ts @@ -1,4 +1,5 @@ export * from "./location"; -export * from "./utils"; -export * from "./struct_equality"; export * from "./node"; +export * from "./pretty_printing"; +export * from "./struct_equality"; +export * from "./utils"; diff --git a/src/misc/node.ts b/src/misc/node.ts index fe62522c..e2c2d3a0 100644 --- a/src/misc/node.ts +++ b/src/misc/node.ts @@ -1,3 +1,4 @@ +import { PPAble } from "./pretty_printing"; import { StructEqualityComparable } from "./struct_equality"; import { assert } from "./utils"; @@ -10,7 +11,7 @@ let nNodes = 0; * Generic tree node with pretty-printer, optional source tripple information * and structural equality comparison. Useful for building ASTs. */ -export abstract class Node implements StructEqualityComparable { +export abstract class Node implements StructEqualityComparable, PPAble { readonly id: number; readonly src?: Range; diff --git a/src/misc/pretty_printing.ts b/src/misc/pretty_printing.ts new file mode 100644 index 00000000..538ccb0a --- /dev/null +++ b/src/misc/pretty_printing.ts @@ -0,0 +1,99 @@ +import { ASTNode } from "../ast/ast_node"; + +export interface PPAble { + pp(): string; +} + +export type PPIsh = + | PPAble + | ASTNode + | string + | number + | boolean + | bigint + | null + | undefined + | PPIsh[] + | Set + | Map + | Iterable; + +export function isPPAble(value: any): value is PPAble { + return value ? typeof value.pp === "function" : false; +} + +export function pp(value: PPIsh): string { + if (value instanceof ASTNode) { + return value.type + " #" + value.id; + } + + if (value === undefined) { + return ""; + } + + if ( + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" || + typeof value === "bigint" + ) { + return String(value); + } + + if (isPPAble(value)) { + return value.pp(); + } + + if (value instanceof Array) { + return ppArr(value); + } + + if (value instanceof Set) { + return ppSet(value); + } + + if (value instanceof Map) { + return ppMap(value); + } + + if (typeof value[Symbol.iterator] === "function") { + return ppIter(value); + } + + throw new Error("Unhandled value in pp(): " + String(value)); +} + +export function ppArr(array: PPIsh[], separator = ",", start = "[", end = "]"): string { + return start + array.map(pp).join(separator) + end; +} + +export function ppIter(iter: Iterable, separator = ",", start = "[", end = "]"): string { + const parts: string[] = []; + + for (const part of iter) { + parts.push(pp(part)); + } + + return start + parts.join(separator) + end; +} + +export function ppSet(set: Set, separator = ",", start = "{", end = "}"): string { + return ppIter(set, separator, start, end); +} + +export function ppMap( + map: Map, + separator = ",", + keyValueSeparator = ":", + start = "{", + end = "}" +): string { + const parts: string[] = []; + + for (const [name, val] of map.entries()) { + parts.push(pp(name) + keyValueSeparator + pp(val)); + } + + return start + parts.join(separator) + end; +} diff --git a/src/misc/utils.ts b/src/misc/utils.ts index a24babd8..395f84cb 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -1,4 +1,4 @@ -import { TypeNode } from ".."; +import { pp, PPIsh } from ".."; import { ASTNode } from "../ast/ast_node"; export function forAll(iterable: Iterable, cb: (v: T) => boolean): boolean { @@ -14,7 +14,7 @@ export function forAll(iterable: Iterable, cb: (v: T) => boolean): boolean export function assert( condition: boolean, message: string, - ...details: Array + ...details: PPIsh[] ): asserts condition { if (condition) { return; @@ -25,17 +25,10 @@ export function assert( for (let i = 0; i < details.length; i++) { const detail = details[i]; - - let part: string; + const part = pp(detail); if (detail instanceof ASTNode) { - part = detail.type + " #" + detail.id; - nodes.push(detail); - } else if (detail instanceof TypeNode) { - part = detail.pp(); - } else { - part = detail; } message = message.replace(new RegExp("\\{" + i + "\\}", "g"), part); diff --git a/test/unit/misc/pretty_printing.spec.ts b/test/unit/misc/pretty_printing.spec.ts new file mode 100644 index 00000000..d30051d6 --- /dev/null +++ b/test/unit/misc/pretty_printing.spec.ts @@ -0,0 +1,144 @@ +import expect from "expect"; +import { ElementaryTypeName, isPPAble, pp, ppArr, ppIter, ppMap, ppSet } from "../../../src"; + +describe("Utility formatting routines", () => { + describe("isPPAble()", () => { + const cases: Array<[any, boolean]> = [ + [{}, false], + [[], false], + [ + { + name: "test", + pp: () => "PPAble object" + }, + true + ] + ]; + + for (const [value, result] of cases) { + it(`${JSON.stringify(value)} results ${result}`, () => { + expect(isPPAble(value)).toEqual(result); + }); + } + }); + + describe("pp()", () => { + const cases: Array<[any, string]> = [ + [1, "1"], + [BigInt(1), "1"], + ["abc", "abc"], + [true, "true"], + [false, "false"], + [null, "null"], + [undefined, ""], + [ + { + name: "test", + pp: () => "PPAble object" + }, + "PPAble object" + ], + [ + new ElementaryTypeName(1, "0:0:0", "ElementaryTypeName", "uint8", "uint8"), + "ElementaryTypeName #1" + ], + [["x", 1, true, null], "[x,1,true,null]"], + [new Set(["a", 2, false, null]), "{a,2,false,null}"], + [ + new Map([ + ["x", true], + ["y", 100], + ["z", false] + ]), + "{x:true,y:100,z:false}" + ], + [ + (function* gen() { + yield 10; + yield 20; + yield 30; + })(), + "[10,20,30]" + ] + ]; + + for (const [value, result] of cases) { + it(`${value} results ${result}`, () => { + expect(pp(value)).toEqual(result); + }); + } + }); + + describe("ppArr()", () => { + const cases: Array<[any[], Array, string]> = [ + [[1, 2, 3], [], "[1,2,3]"], + [["x", "y", "z"], ["/", "<", ">"], ""] + ]; + + for (const [value, options, result] of cases) { + it(`${value} with options ${JSON.stringify(options)} results ${result}`, () => { + expect(ppArr(value, ...options)).toEqual(result); + }); + } + }); + + describe("ppIter()", () => { + function makeIterable(...array: T[]): Iterable { + return { + *[Symbol.iterator]() { + for (const element of array) { + yield element; + } + } + }; + } + + const cases: Array<[Iterable, Array, string]> = [ + [[1, 2, 3], [], "[1,2,3]"], + [new Set(["x", "y", "z"]), ["/", "<", ">"], ""], + [makeIterable("1x", 2, "y3"), [], "[1x,2,y3]"] + ]; + + for (const [value, options, result] of cases) { + const array = Array.from(value); + + it(`Iterable ${JSON.stringify(array)} with options ${JSON.stringify( + options + )} results ${result}`, () => { + expect(ppIter(value, ...options)).toEqual(result); + }); + } + }); + + describe("ppSet()", () => { + const cases: Array<[Set, Array, string]> = [ + [new Set([1, 2, 3]), [], "{1,2,3}"], + [new Set(["x", "y", "z"]), ["/", "<", ">"], ""] + ]; + + for (const [value, options, result] of cases) { + it(`${value} with options ${JSON.stringify(options)} results ${result}`, () => { + expect(ppSet(value, ...options)).toEqual(result); + }); + } + }); + + describe("ppMap()", () => { + const map = new Map([ + ["a", 0], + ["b", 3], + ["c", 1] + ]); + + const cases: Array<[Map, Array, string]> = [ + [map, [], "{a:0,b:3,c:1}"], + [map, ["/", " => ", "<", ">"], " 0/b => 3/c => 1>"] + ]; + + for (const [value, options, result] of cases) { + it(`${value} with options ${JSON.stringify(options)} results ${result}`, () => { + expect(ppMap(value, ...options)).toEqual(result); + }); + } + }); +});