Skip to content

Commit

Permalink
test: codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Sep 23, 2020
1 parent b0c55e1 commit f81beff
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 41 deletions.
2 changes: 1 addition & 1 deletion docs/codegen.md
Expand Up @@ -29,7 +29,7 @@ gen.if(
// so if `x` contained some code, it would not be executed.
_`${num} > ${x}`,
() => log("greater"),
() => log("smaller")
() => log("smaller or equal")
)

function log(comparison: string): void {
Expand Down
56 changes: 19 additions & 37 deletions lib/compile/codegen/code.ts
@@ -1,5 +1,5 @@
export class _Code {
private _str: string
private readonly _str: string

constructor(s: string) {
this._str = s
Expand All @@ -9,18 +9,9 @@ export class _Code {
return this._str
}

isQuoted(): boolean {
const len = this._str.length
return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"'
}

emptyStr(): boolean {
return this._str === "" || this._str === '""'
}

add(c: _Code): void {
this._str += c._str
}
}

export const IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i
Expand All @@ -31,17 +22,9 @@ export class Name extends _Code {
if (!IDENTIFIER.test(s)) throw new Error("CodeGen: name must be a valid identifier")
}

isQuoted(): boolean {
return false
}

emptyStr(): boolean {
return false
}

add(_c: _Code): void {
throw new Error("CodeGen: can't add to Name")
}
}

export type Code = _Code | Name
Expand All @@ -53,27 +36,29 @@ export const nil = new _Code("")
type TemplateArg = SafeExpr | string | undefined

export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code {
// TODO benchmark if loop is faster than reduce
// let res = strs[0]
// for (let i = 0; i < args.length; i++) {
// res += interpolate(args[i]) + strs[i + 1]
// }
// return new _Code(res)
return new _Code(strs.reduce((res, s, i) => `${res}${interpolate(args[i - 1])}${s}`))
}

export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code {
return new _Code(
strs.map(safeStringify).reduce((res, s, i) => {
let aStr = interpolateStr(args[i - 1])
if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString()
return typeof aStr === "string"
? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1)
: `${res} + ${aStr} + ${s}`
})
strs
.map(safeStringify)
.reduce((res, s, i) => concat(concat(res, interpolateStr(args[i - 1])), s))
)
}

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}`
}
Expand All @@ -84,9 +69,10 @@ function interpolate(x: TemplateArg): TemplateArg {
: safeStringify(x)
}

function interpolateStr(x: TemplateArg | string[]): TemplateArg {
function interpolateStr(x: TemplateArg | string[]): string | number | boolean | null | undefined {
if (Array.isArray(x)) x = x.join(",")
return interpolate(x)
x = interpolate(x)
return x instanceof _Code ? x.toString() : x
}

export function stringify(x: unknown): Code {
Expand All @@ -102,7 +88,3 @@ function safeStringify(x: unknown): string {
export function getProperty(key: Code | string | number): Code {
return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]`
}

export function keyValue(key: Name, value: SafeExpr, es5?: boolean): Code {
return key === value && !es5 ? key : _`${key}: ${value}`
}
42 changes: 39 additions & 3 deletions lib/compile/codegen/index.ts
Expand Up @@ -12,8 +12,10 @@ enum BlockKind {
Func,
}

// type for expressions that can be safely inserted in code without quotes
export type SafeExpr = Code | number | boolean | null

// type that is either Code of function that adds code to CodeGen instance using its methods
export type Block = Code | (() => void)

export const operators = {
Expand Down Expand Up @@ -61,14 +63,17 @@ export class CodeGen {
return this._out
}

// returns unique name in the internal scope
name(prefix: string): Name {
return this._scope.name(prefix)
}

// reserves unique name in the external scope
scopeName(prefix: string): ValueScopeName {
return this._extScope.name(prefix)
}

// reserves unique name in the external scope and assigns value to it
scopeValue(prefixOrName: ValueScopeName | string, value: NameValue): Name {
const name = this._extScope.value(prefixOrName, value)
const vs = this._values[name.prefix] || (this._values[name.prefix] = new Set())
Expand All @@ -80,6 +85,8 @@ export class CodeGen {
return this._extScope.getValue(prefix, keyOrRef)
}

// return code that assigns values in the external scope to the names that are used internally
// (same names that were returned by gen.scopeName or gen.scopeValue)
scopeRefs(scopeName: Name): Code {
return this._extScope.scopeRefs(scopeName, this._values)
}
Expand All @@ -92,37 +99,44 @@ export class CodeGen {
return name
}

const(nameOrPrefix: Name | string, rhs?: SafeExpr): Name {
// render `const` declaration (`var` in es5 mode)
const(nameOrPrefix: Name | string, rhs: SafeExpr): Name {
return this._def(varKinds.const, nameOrPrefix, rhs)
}

// render `let` declaration with optional assignment (`var` in es5 mode)
let(nameOrPrefix: Name | string, rhs?: SafeExpr): Name {
return this._def(varKinds.let, nameOrPrefix, rhs)
}

// render `var` declaration with optional assignment
var(nameOrPrefix: Name | string, rhs?: SafeExpr): Name {
return this._def(varKinds.var, nameOrPrefix, rhs)
}

// render assignment
assign(name: Code, rhs: SafeExpr): CodeGen {
this._out += `${name} = ${rhs};` + this._n
return this
}

// appends passed SafeExpr to code or executes Block
code(c: Block | SafeExpr): CodeGen {
if (typeof c == "function") c()
else this._out += `${c};${this._n}`

return this
}

// returns code for object literal for the passed argument list of key-value pairs
object(...keyValues: [Name, SafeExpr][]): _Code {
const values = keyValues
.map(([key, value]) => (key === value && !this.opts.es5 ? key : `${key}: ${value}`))
.map(([key, value]) => (key === value && !this.opts.es5 ? key : `${key}:${value}`))
.reduce((c1, c2) => `${c1},${c2}`)
return new _Code(`{${values}}`)
}

// render `if` clause (or statement if `thenBody` and, optionally, `elseBody` are passed)
if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen {
this._blocks.push(BlockKind.If)
this._out += `if(${condition}){` + this._n
Expand All @@ -136,24 +150,29 @@ export class CodeGen {
return this
}

// render `if` clause or statement with negated condition,
// useful to avoid using _ template just to negate the name
ifNot(condition: Code, thenBody?: Block, elseBody?: Block): CodeGen {
const cond = new _Code(condition instanceof Name ? `!${condition}` : `!(${condition})`)
return this.if(cond, thenBody, elseBody)
}

// render `else if` clause - invalid without `if` or after `else` clauses
elseIf(condition: Code): CodeGen {
if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else if" without "if"')
this._out += `}else if(${condition}){` + this._n
return this
}

// render `else` clause - only valid after `if` or `else if` clauses
else(): CodeGen {
if (this._lastBlock !== BlockKind.If) throw new Error('CodeGen: "else" without "if"')
this._lastBlock = BlockKind.Else
this._out += "}else{" + this._n
return this
}

// render the closing brace for `if` statement - checks and updates the stack of previous clauses
endIf(): CodeGen {
// TODO possibly remove empty branches here
const b = this._lastBlock
Expand All @@ -163,13 +182,15 @@ export class CodeGen {
return this
}

// render a generic `for` clause (or statement if `forBody` is passed)
for(iteration: Code, forBody?: Block): CodeGen {
this._blocks.push(BlockKind.For)
this._out += `for(${iteration}){` + this._n
if (forBody) this.code(forBody).endFor()
return this
}

// render `for` statement for a range of values
forRange(
nameOrPrefix: Name | string,
from: SafeExpr,
Expand All @@ -182,6 +203,7 @@ export class CodeGen {
return this._loop(_`for(${varKind} ${i}=${from}; ${i}<${to}; ${i}++){`, i, forBody)
}

// render `for-of` statement (in es5 mode a normal for loop)
forOf(
nameOrPrefix: Name | string,
iterable: SafeExpr,
Expand All @@ -190,7 +212,7 @@ export class CodeGen {
): CodeGen {
const name = this._scope.toName(nameOrPrefix)
if (this.opts.es5) {
const arr = iterable instanceof Name ? iterable : this.var("arr", iterable)
const arr = iterable instanceof Name ? iterable : this.var("_arr", iterable)
return this.forRange("_i", 0, new _Code(`${arr}.length`), (i) => {
this.var(name, new _Code(`${arr}[${i}]`))
forBody(name)
Expand All @@ -199,6 +221,8 @@ export class CodeGen {
return this._loop(_`for(${varKind} ${name} of ${iterable}){`, name, forBody)
}

// render `for-in` statement.
// With option `forInOwn` (set from Ajv option `ownProperties`) render a `for-of` loop for object keys
forIn(
nameOrPrefix: Name | string,
obj: SafeExpr,
Expand All @@ -220,6 +244,7 @@ export class CodeGen {
return this
}

// render closing brace for `for` loop - checks and updates the stack of previous clauses
endFor(): CodeGen {
const b = this._lastBlock
if (b !== BlockKind.For) throw new Error('CodeGen: "endFor" without "for"')
Expand All @@ -228,23 +253,27 @@ export class CodeGen {
return this
}

// render `label` clause
label(label?: Code): CodeGen {
this._out += `${label}:${this._n}`
return this
}

// render `break` statement
break(label?: Code): CodeGen {
this._out += (label ? `break ${label};` : "break;") + this._n
return this
}

// render `return` statement
return(value: Block | SafeExpr): CodeGen {
this._out += "return "
this.code(value)
this._out += ";" + this._n
return this
}

// render `try` statement
try(tryBody: Block, catchCode?: (e: Name) => void, finallyCode?: Block): CodeGen {
if (!catchCode && !finallyCode) throw new Error('CodeGen: "try" without "catch" and "finally"')
this._out += "try{" + this._n
Expand All @@ -262,17 +291,20 @@ export class CodeGen {
return this
}

// render `throw` statement
throw(err: Code): CodeGen {
this._out += `throw ${err};` + this._n
return this
}

// start self-balancing block
block(body?: Block, expectedToClose?: number): CodeGen {
this._blockStarts.push(this._blocks.length)
if (body) this.code(body).endBlock(expectedToClose)
return this
}

// render braces to balance them until the previous gen.block call
endBlock(expectedToClose?: number): CodeGen {
// TODO maybe close blocks one by one, eliminating empty branches
const len = this._blockStarts.pop()
Expand All @@ -286,13 +318,15 @@ export class CodeGen {
return this
}

// render `function` head (or definition if funcBody is passed)
func(name: Name, args: Code = nil, async?: boolean, funcBody?: Block): CodeGen {
this._blocks.push(BlockKind.Func)
this._out += `${async ? "async " : ""}function ${name}(${args}){` + this._n
if (funcBody) this.code(funcBody).endFunc()
return this
}

// render closing brace for function definition
endFunc(): CodeGen {
const b = this._lastBlock
if (b !== BlockKind.Func) throw new Error('CodeGen: "endFunc" without "func"')
Expand All @@ -318,12 +352,14 @@ export class CodeGen {

const andCode = mappend(operators.AND)

// boolean AND (&&) expression with the passed arguments
export function and(...args: Code[]): Code {
return args.reduce(andCode)
}

const orCode = mappend(operators.OR)

// boolean OR (||) expression with the passed arguments
export function or(...args: Code[]): Code {
return args.reduce(orCode)
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -16,6 +16,7 @@
"prettier:write": "prettier --write './**/*.{md,json,yaml,js,ts}'",
"prettier:check": "prettier --list-different './**/*.{md,json,yaml,js,ts}'",
"test-spec": "cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register 'spec/**/*.spec.ts' -R dot",
"test-codegen": "nyc cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register 'spec/codegen.spec.ts' -R spec",
"test-debug": "npm run test-spec -- --inspect-brk",
"test-cov": "nyc npm run test-spec",
"bundle": "rm -rf bundle && node ./scripts/bundle.js",
Expand Down

0 comments on commit f81beff

Please sign in to comment.