From 837930f51380852d6c1dc30e034148cd916b4b69 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 27 Sep 2020 14:36:26 +0100 Subject: [PATCH] refactor: store arrays of items in _Code class --- lib/compile/codegen/code.ts | 180 +++++++++++++++++++++++------------ lib/compile/codegen/index.ts | 46 +++++---- lib/compile/errors.ts | 6 +- spec/codegen.spec.ts | 16 +++- 4 files changed, 162 insertions(+), 86 deletions(-) diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts index a4a49b8cc..c3dfc4ac9 100644 --- a/lib/compile/codegen/code.ts +++ b/lib/compile/codegen/code.ts @@ -1,66 +1,142 @@ -export class _Code { - readonly _str: string - names?: UsedNames +export abstract class _CodeOrName { + expr = false + abstract readonly str: string + abstract readonly names: UsedNames + abstract toString(): string + abstract emptyStr(): boolean +} + +export const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i - constructor(s: string, names?: UsedNames) { - this._str = s - this.names = names +export class Name extends _CodeOrName { + readonly str: string + + constructor(s: string) { + super() + if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier") + this.str = s } toString(): string { - return this._str + return this.str } emptyStr(): boolean { - return this._str === "" || this._str === '""' + return false + } + + get names(): UsedNames { + return {[this.str]: 1} } } -export type UsedNames = Record +export class _Code extends _CodeOrName { + readonly _items: readonly CodeItem[] + private _str?: string + private _names?: UsedNames -export const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i + constructor(code: string | readonly CodeItem[]) { + super() + this._items = typeof code === "string" ? [code] : code + } -export class Name extends _Code { - constructor(s: string) { - super(s) - if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier") + toString(): string { + return this.str } emptyStr(): boolean { - return false + if (this._items.length > 1) return false + const item = this._items[0] + return item === "" || item === '""' + } + + get str(): string { + this._str ??= this._items.reduce((s: string, c: CodeItem) => `${s}${c}`, "") + return this._str + } + + get names(): UsedNames { + if (!this._names) { + this._names = {} + for (const c of this._items) { + if (c instanceof Name) this._names[c.str] = (this._names[c.str] || 0) + 1 + } + } + return this._names } } +export type CodeItem = Name | string | number | boolean | null + +export type UsedNames = Record + export type Code = _Code | Name export type SafeExpr = Code | number | boolean | null export const nil = new _Code("") -type TemplateArg = SafeExpr | string | undefined - -export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code { - const names: UsedNames = {} - return new _Code( - strs.reduce((res, s, i) => { - const arg = args[i - 1] - if (arg instanceof _Code) updateUsedNames(arg, names) - return `${res}${interpolate(arg)}${s}` - }), - names - ) +type CodeArg = SafeExpr | string | undefined + +export function _(strs: TemplateStringsArray, ...args: CodeArg[]): _Code { + const code: CodeItem[] = [strs[0]] + let i = 0 + while (i < args.length) { + addCodeArg(code, args[i]) + code.push(strs[++i]) + } + return new _Code(code) +} + +const plus = new _Code("+") + +export function str(strs: TemplateStringsArray, ...args: (CodeArg | string[])[]): _Code { + const expr: CodeItem[] = [safeStringify(strs[0])] + let i = 0 + while (i < args.length) { + expr.push(plus) + addCodeArg(expr, args[i]) + expr.push(plus, safeStringify(strs[++i])) + } + optimize(expr) + return new _Code(expr) +} + +export function addCodeArg(code: CodeItem[], arg: CodeArg | string[]): void { + if (arg instanceof _CodeOrName) { + if (arg instanceof Name) code.push(arg) + else code.push(...arg._items) + } else { + code.push(interpolate(arg)) + } } -export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code { - const names: UsedNames = {} - return new _Code( - strs.map(safeStringify).reduce((res, s, i) => { - const arg = args[i - 1] - if (arg instanceof _Code) updateUsedNames(arg, names) - return concat(concat(res, interpolateStr(arg)), s) - }), - names - ) +function optimize(expr: CodeItem[]): void { + let i = 1 + while (i < expr.length - 1) { + if (expr[i] === plus) { + const res = mergeExprItems(expr[i - 1], expr[i + 1]) + if (res !== undefined) { + expr.splice(i - 1, 3, res) + continue + } + expr[i++] = "+" + } + i++ + } +} + +function mergeExprItems(a: CodeItem, b: CodeItem): CodeItem | undefined { + if (b === '""') return a + if (a === '""') return b + if (typeof a == "string") { + if (b instanceof Name || a[a.length - 1] !== '"') return + if (typeof b != "string") return `${a.slice(0, -1)}${b}"` + if (b[0] === '"') return a.slice(0, -1) + b.slice(1) + return + } + if (typeof b == "string" && b[0] === '"' && !(a instanceof Name)) return `"${a}${b.slice(1)}` + return } export function updateUsedNames( @@ -69,7 +145,7 @@ export function updateUsedNames( inc: 1 | -1 = 1 ): void { if (src instanceof Name) { - const n = src._str + const n = src.str names[n] = (names[n] || 0) + inc } else if (src.names) { for (const n in src.names) { @@ -79,37 +155,19 @@ export function updateUsedNames( } export function usedNames(e?: SafeExpr): UsedNames | undefined { - if (e instanceof Name) return {[e._str]: 1} - if (e instanceof _Code) return e.names + if (e instanceof _CodeOrName) return e.names return undefined } -function concat(s: string, a: string | number | boolean | null | undefined): string { - return a === '""' - ? s - : s === '""' - ? `${a}` - : typeof a != "string" - ? `${s.slice(0, -1)}${a}"` - : s.endsWith('"') && a[0] === '"' - ? s.slice(0, -1) + a.slice(1) - : `${s} + ${a}` -} - export function strConcat(c1: Code, c2: Code): Code { return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}` } -function interpolate(x: TemplateArg): TemplateArg { - return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null +// TODO do not allow arrays here +function interpolate(x?: string | string[] | number | boolean | null): SafeExpr | string { + return typeof x == "number" || typeof x == "boolean" || x === null ? x - : safeStringify(x) -} - -function interpolateStr(x: TemplateArg | string[]): string | number | boolean | null | undefined { - if (Array.isArray(x)) x = x.join(",") - x = interpolate(x) - return x instanceof _Code ? x._str : x + : safeStringify(Array.isArray(x) ? x.join(",") : x) } export function stringify(x: unknown): Code { diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 3deeb209c..94fb60fa9 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -1,5 +1,16 @@ import type {ScopeValueSets, NameValue, ValueScope, ValueScopeName} from "./scope" -import {_, nil, _Code, Code, Name, UsedNames, usedNames, updateUsedNames} from "./code" +import { + _, + nil, + _Code, + Code, + Name, + UsedNames, + usedNames, + updateUsedNames, + CodeItem, + addCodeArg, +} from "./code" import {Scope} from "./scope" export {_, str, strConcat, nil, getProperty, stringify, Name, Code} from "./code" @@ -170,7 +181,7 @@ export class CodeGen { private readonly opts: CodeGenOptions private readonly _n: string private _out = "" - // nodeCount = 0 + nodeCount = 0 constructor(extScope: ValueScope, opts: CodeGenOptions = {}) { this.opts = opts @@ -254,17 +265,18 @@ export class CodeGen { } // returns code for object literal for the passed argument list of key-value pairs - object(...keyValues: [Name, SafeExpr][]): _Code { - const names: UsedNames = {} - const values = keyValues - .map(([key, value]) => { - updateUsedNames(key, names) - if (key === value) return this.opts.es5 ? `${key}:${value}` : key - if (value instanceof _Code) updateUsedNames(value, names) - return `${key}:${value}` - }) - .reduce((c1, c2) => `${c1},${c2}`) - return new _Code(`{${values}}`, names) + object(...keyValues: [Name, SafeExpr | string][]): _Code { + const code: CodeItem[] = ["{"] + for (const [key, value] of keyValues) { + if (code.length > 1) code.push(",") + code.push(key) + if (key !== value || this.opts.es5) { + code.push(":") + addCodeArg(code, value) + } + } + code.push("}") + return new _Code(code) } // `if` clause (or statement if `thenBody` and, optionally, `elseBody` are passed) @@ -473,7 +485,7 @@ export class CodeGen { } private _nodeCode(node: ParentNode): void { - // this.nodeCount++ + this.nodeCount++ switch (node.kind) { case Node.If: this._out += `if(${node.condition})` @@ -519,7 +531,7 @@ export class CodeGen { } private _leafNodeCode(node: LeafNode): void { - // this.nodeCount++ + this.nodeCount++ let code: string switch (node.kind) { case Node.Def: { @@ -656,8 +668,8 @@ function removeUnusedNames(nodes: ChildNode[], names: UsedNames): void { function unusedName(n: ChildNode, names: UsedNames): boolean { return ( - (n.kind === Node.Def && !names[n.name._str]) || - (n.kind === Node.Assign && n.lhs instanceof Name && !names[n.lhs._str]) + (n.kind === Node.Def && !names[n.name.str]) || + (n.kind === Node.Assign && n.lhs instanceof Name && !names[n.lhs.str]) ) } diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index d3a3055b5..a15e3a41d 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -113,15 +113,15 @@ function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): C } = cxt if (createErrors === false) return _`{}` const {params, message} = error - const keyValues: [Name, SafeExpr][] = [ - [E.keyword, _`${keyword}`], + const keyValues: [Name, SafeExpr | string][] = [ + [E.keyword, keyword], [N.dataPath, strConcat(N.dataPath, errorPath)], [E.schemaPath, str`${errSchemaPath}/${keyword}`], [E.params, params ? params(cxt) : _`{}`], ] if (propertyName) keyValues.push([E.propertyName, propertyName]) if (opts.messages !== false) { - const msg = typeof message == "string" ? _`${message}` : message(cxt) + const msg = typeof message == "string" ? message : message(cxt) keyValues.push([E.message, msg]) } if (opts.verbose) { diff --git a/spec/codegen.spec.ts b/spec/codegen.spec.ts index 037e68f7a..791fb03b3 100644 --- a/spec/codegen.spec.ts +++ b/spec/codegen.spec.ts @@ -62,19 +62,25 @@ describe("code generation", () => { it("creates string expressions with Code", () => { const x = new Name("x") - assertEqual(str`${x}foo${x}bar${x}`, 'x + "foo" + x + "bar" + x') - assertEqual(str`foo${x}${x}bar${x}`, '"foo" + x + x + "bar" + x') + assertEqual(str`${x}foo${x}bar${x}`, 'x+"foo"+x+"bar"+x') + assertEqual(str`foo${x}${x}bar${x}`, '"foo"+x+x+"bar"+x') }) it("connects string expressions removing unnecessary additions", () => { const x = _`"foo" + ${new Name("x")} + "bar"` - const code: Code = str`start ${x} end` - assertEqual(code, '"start foo" + x + "bar end"') + assertEqual(str`start ${x} end`, '"start foo" + x + "bar end"') }) it("connects strings with numbers, booleans and nulls removing unnecessary additions", () => { assertEqual(str`foo ${1} ${true} ${null} bar`, '"foo 1 true null bar"') }) + + it("preserves code", () => { + const data = new Name("data") + const code = _`${data}.replace(/~/g, "~0")` + assertEqual(str`/${code}`, '"/"+data.replace(/~/g, "~0")') + assertEqual(str`/${code}/`, '"/"+data.replace(/~/g, "~0")+"/"') + }) }) describe("CodeGen", () => { @@ -352,7 +358,7 @@ describe("code generation", () => { gen.optimize() assertEqual( gen, - 'function inverse(x0){try{return 1/x0;}catch(e0){console.error("dividing " + x0 + " by 0");throw e0;}}' + 'function inverse(x0){try{return 1/x0;}catch(e0){console.error("dividing "+x0+" by 0");throw e0;}}' ) }) })