Skip to content

Commit

Permalink
refactor: store arrays of items in _Code class
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Sep 27, 2020
1 parent 6737330 commit 837930f
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 86 deletions.
180 changes: 119 additions & 61 deletions 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<string, number | undefined>
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<string, number | undefined>

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(
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
46 changes: 29 additions & 17 deletions 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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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})`
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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])
)
}

Expand Down
6 changes: 3 additions & 3 deletions lib/compile/errors.ts
Expand Up @@ -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) {
Expand Down
16 changes: 11 additions & 5 deletions spec/codegen.spec.ts
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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;}}'
)
})
})
Expand Down

0 comments on commit 837930f

Please sign in to comment.