From 92c7b7f81bab5234852592a9bf46dfdc690c4726 Mon Sep 17 00:00:00 2001 From: Jonah Scheinerman Date: Thu, 31 May 2018 17:58:04 -0400 Subject: [PATCH] Codegen verification (#30) * Codegen verification * Test verification on travis * Revert test --- codegen/common.ts | 18 +++++++++++------ codegen/emit.ts | 44 ++++++++++++++++++++++++++++++++---------- codegen/genCommon.ts | 14 +++++++------- codegen/genTests.ts | 40 +++++++++++++++++++------------------- codegen/genTypes.ts | 32 +++++++++++++++--------------- codegen/main.ts | 46 -------------------------------------------- codegen/produce.ts | 3 +++ codegen/spec.ts | 41 +++++++++++++++++++++++++++++++++++++++ codegen/verify.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ package.json | 8 +++++--- 10 files changed, 182 insertions(+), 108 deletions(-) delete mode 100644 codegen/main.ts create mode 100644 codegen/produce.ts create mode 100644 codegen/spec.ts create mode 100644 codegen/verify.ts diff --git a/codegen/common.ts b/codegen/common.ts index 2ff931b..f60f0d8 100644 --- a/codegen/common.ts +++ b/codegen/common.ts @@ -1,9 +1,13 @@ -export interface CommonOperatorCodeGenOptions { +export interface CodeGenSpec extends CommonSpec { + operators: PartialOperatorSpec[]; +} + +export interface CommonSpec { minExponent: number; maxExponent: number; } -export interface OperatorCodeGenOptions extends CommonOperatorCodeGenOptions { +export interface PartialOperatorSpec { fileNamePrefix: string; uncurriedTypeNamePrefix: string; curriedTypeNamePrefix: string; @@ -12,7 +16,9 @@ export interface OperatorCodeGenOptions extends CommonOperatorCodeGenOptions { compute: (left: number, right: number) => number; } -export function getExponents({ minExponent, maxExponent }: CommonOperatorCodeGenOptions): number[] { +export interface OperatorSpec extends CommonSpec, PartialOperatorSpec {} + +export function getExponents({ minExponent, maxExponent }: CommonSpec): number[] { const exponents: number[] = []; for (let exponent = minExponent; exponent <= maxExponent; exponent++) { exponents.push(exponent); @@ -20,7 +26,7 @@ export function getExponents({ minExponent, maxExponent }: CommonOperatorCodeGen return exponents; } -export function isExponent(exponent: number, { minExponent, maxExponent }: CommonOperatorCodeGenOptions): boolean { +export function isExponent(exponent: number, { minExponent, maxExponent }: CommonSpec): boolean { return exponent >= minExponent && exponent <= maxExponent && exponent === Math.floor(exponent); } @@ -46,9 +52,9 @@ export function genImport(symbols: string[], source: string): string { return `import { ${symbols.join(", ")} } from "${source}";`; } -export function genUncurriedTypeName(options: OperatorCodeGenOptions, left?: string | number, right?: string | number) { +export function genUncurriedTypeName(spec: OperatorSpec, left?: string | number, right?: string | number) { const args = left !== undefined && right !== undefined ? `<${left}, ${right}>` : ""; - return `${options.uncurriedTypeNamePrefix}Exponents${args}`; + return `${spec.uncurriedTypeNamePrefix}Exponents${args}`; } export function genValueName(value: number): string { diff --git a/codegen/emit.ts b/codegen/emit.ts index 875f2ad..ce186de 100644 --- a/codegen/emit.ts +++ b/codegen/emit.ts @@ -1,27 +1,51 @@ import { writeFile } from "fs"; -import { CommonOperatorCodeGenOptions, OperatorCodeGenOptions } from "./common"; import { genCommonTypes } from "./genCommon"; import { genOperatorTests } from "./genTests"; import { genOperatorTypes } from "./genTypes"; +import { codeGenSpec } from "./spec"; -const PATH_PREFIX = "src/exponent/"; +const PATH_PREFIX = "src/exponent"; -export function emitCommonTypes(options: CommonOperatorCodeGenOptions) { - emitFile("common.ts", genCommonTypes(options)); +export interface EmitPlan { + path: string; + source: string; } -export function emitOperator(options: OperatorCodeGenOptions) { - const { fileNamePrefix } = options; - emitFile(`${fileNamePrefix}.ts`, genOperatorTypes(options)); - emitFile(`__test__/${fileNamePrefix}Spec.ts`, genOperatorTests(options)); +export function emit(callback?: () => void) { + const emits: EmitPlan[] = getEmitPlans(); + + let index = -1; + const nextEmit = () => { + index++; + const isLastEmit = index === emits.length - 1; + emitFile(emits[index], isLastEmit ? callback : nextEmit); + }; + nextEmit(); +} + +export function getEmitPlans(): EmitPlan[] { + const { operators, ...common } = codeGenSpec; + const emits: EmitPlan[] = [{ path: `${PATH_PREFIX}/common.ts`, source: genCommonTypes(codeGenSpec) }]; + operators.forEach(operator => { + const operatorSpec = { ...operator, ...common }; + const { fileNamePrefix } = operator; + emits.push( + { path: `${PATH_PREFIX}/${fileNamePrefix}.ts`, source: genOperatorTypes(operatorSpec) }, + { path: `${PATH_PREFIX}/__test__/${fileNamePrefix}Spec.ts`, source: genOperatorTests(operatorSpec) }, + ); + }); + return emits; } -function emitFile(path: string, content: string) { - writeFile(PATH_PREFIX + path, content, error => { +function emitFile({ path, source }: EmitPlan, callback?: () => void) { + writeFile(path, source, error => { if (error) { console.error(`There was an error writing to ${path}`); } else { console.log(`Generated ${path}`); + if (callback) { + callback(); + } } }); } diff --git a/codegen/genCommon.ts b/codegen/genCommon.ts index ecac2bb..d6dc0c6 100644 --- a/codegen/genCommon.ts +++ b/codegen/genCommon.ts @@ -1,11 +1,11 @@ -import { CommonOperatorCodeGenOptions, genFileHeader, getExponents } from "./common"; +import { CommonSpec, genFileHeader, getExponents } from "./common"; -export function genCommonTypes(options: CommonOperatorCodeGenOptions): string { +export function genCommonTypes(spec: CommonSpec): string { return [ ...genFileHeader(false), - ...genExtremaType("Min", options.minExponent), - ...genExtremaType("Max", options.maxExponent), - ...genUnionType(options), + ...genExtremaType("Min", spec.minExponent), + ...genExtremaType("Max", spec.maxExponent), + ...genUnionType(spec), ...genErrorType(), ].join("\n"); } @@ -17,8 +17,8 @@ function genExtremaType(prefix: string, exponent: number): string[] { return [type, value, ""]; } -function genUnionType(options: CommonOperatorCodeGenOptions): string[] { - const exponents = getExponents(options).join(" | "); +function genUnionType(spec: CommonSpec): string[] { + const exponents = getExponents(spec).join(" | "); return [`export type Exponent = ${exponents};`, ""]; } diff --git a/codegen/genTests.ts b/codegen/genTests.ts index 10f2a8b..9a28497 100644 --- a/codegen/genTests.ts +++ b/codegen/genTests.ts @@ -5,54 +5,54 @@ import { genValueName, getExponents, isExponent, - OperatorCodeGenOptions, + OperatorSpec, } from "./common"; -export function genOperatorTests(options: OperatorCodeGenOptions): string { - const lines: string[] = [...genFileHeader(), ...genImports(options)]; - const exponents = getExponents(options); +export function genOperatorTests(spec: OperatorSpec): string { + const lines: string[] = [...genFileHeader(), ...genImports(spec)]; + const exponents = getExponents(spec); for (const left of exponents) { for (const right of exponents) { - lines.push(...genTest(options, left, right)); + lines.push(...genTest(spec, left, right)); lines.push(""); } } return lines.join("\n"); } -function genImports(options: OperatorCodeGenOptions): string[] { +function genImports(spec: OperatorSpec): string[] { return [ - genImport([genUncurriedTypeName(options)], `../${options.fileNamePrefix}`), + genImport([genUncurriedTypeName(spec)], `../${spec.fileNamePrefix}`), genImport(["IsArithmeticError"], "../utils"), "", ]; } -function genTest(options: OperatorCodeGenOptions, left: number, right: number): string[] { - const result = options.compute(left, right); - if (isExponent(result, options)) { - return genValueTest(options, left, right, result); +function genTest(spec: OperatorSpec, left: number, right: number): string[] { + const result = spec.compute(left, right); + if (isExponent(result, spec)) { + return genValueTest(spec, left, right, result); } else { - return genErrorTest(options, left, right); + return genErrorTest(spec, left, right); } } -function genValueTest(options: OperatorCodeGenOptions, left: number, right: number, result: number): string[] { - const typeName = genTestBaseName(options, left, right); +function genValueTest(spec: OperatorSpec, left: number, right: number, result: number): string[] { + const typeName = genTestBaseName(spec, left, right); return [ - `type ${typeName} = ${genUncurriedTypeName(options, left, right)};`, + `type ${typeName} = ${genUncurriedTypeName(spec, left, right)};`, `const ${typeName}: ${typeName} = ${result};`, ]; } -function genErrorTest(options: OperatorCodeGenOptions, left: number, right: number): string[] { - const typeName = `${genTestBaseName(options, left, right)}IsError`; +function genErrorTest(spec: OperatorSpec, left: number, right: number): string[] { + const typeName = `${genTestBaseName(spec, left, right)}IsError`; return [ - `type ${typeName} = IsArithmeticError<${genUncurriedTypeName(options, left, right)}>;`, + `type ${typeName} = IsArithmeticError<${genUncurriedTypeName(spec, left, right)}>;`, `const ${typeName}: ${typeName} = true;`, ]; } -function genTestBaseName(options: OperatorCodeGenOptions, left: number, right: number) { - return `${options.testTypeNamePrefix}Of${genValueName(left)}And${genValueName(right)}`; +function genTestBaseName(spec: OperatorSpec, left: number, right: number) { + return `${spec.testTypeNamePrefix}Of${genValueName(left)}And${genValueName(right)}`; } diff --git a/codegen/genTypes.ts b/codegen/genTypes.ts index e158a9e..039366c 100644 --- a/codegen/genTypes.ts +++ b/codegen/genTypes.ts @@ -6,15 +6,15 @@ import { getExponents, indent, isExponent, - OperatorCodeGenOptions, + OperatorSpec, } from "./common"; -export function genOperatorTypes(options: OperatorCodeGenOptions): string { - const exponents = getExponents(options); - let lines: string[] = [...genFileHeader(), ...genImports(), ...genUncurriedType(options, exponents)]; +export function genOperatorTypes(spec: OperatorSpec): string { + const exponents = getExponents(spec); + let lines: string[] = [...genFileHeader(), ...genImports(), ...genUncurriedType(spec, exponents)]; for (const left of exponents) { - if (!(left in options.specialCases)) { - lines.push(...genCurriedType(options, exponents, left)); + if (!(left in spec.specialCases)) { + lines.push(...genCurriedType(spec, exponents, left)); } } return lines.join("\n"); @@ -24,17 +24,17 @@ function genImports(): string[] { return [genImport(["ArithmeticError", "Exponent"], "./common"), ""]; } -function genUncurriedType(options: OperatorCodeGenOptions, exponents: number[]): string[] { - const lines = [`export type ${genUncurriedTypeName(options, "L extends Exponent", "R extends Exponent")}`]; +function genUncurriedType(spec: OperatorSpec, exponents: number[]): string[] { + const lines = [`export type ${genUncurriedTypeName(spec, "L extends Exponent", "R extends Exponent")}`]; let first = true; for (const left of exponents) { const operator = first ? "=" : ":"; const prefix = indent(`${operator} L extends ${left} ?`); first = false; - if (left in options.specialCases) { - lines.push(`${prefix} ${options.specialCases[left]}`); + if (left in spec.specialCases) { + lines.push(`${prefix} ${spec.specialCases[left]}`); } else { - lines.push(`${prefix} ${genCurriedTypeName(options, left)}`); + lines.push(`${prefix} ${genCurriedTypeName(spec, left)}`); } } lines.push(genErrorCase()); @@ -42,12 +42,12 @@ function genUncurriedType(options: OperatorCodeGenOptions, exponents: number[]): return lines; } -function genCurriedType(options: OperatorCodeGenOptions, exponents: number[], left: number): string[] { - const lines = [`export type ${genCurriedTypeName(options, left)}`]; +function genCurriedType(spec: OperatorSpec, exponents: number[], left: number): string[] { + const lines = [`export type ${genCurriedTypeName(spec, left)}`]; let first = true; for (const right of exponents) { - const result = options.compute(left, right); - if (isExponent(result, options)) { + const result = spec.compute(left, right); + if (isExponent(result, spec)) { const operator = first ? "=" : ":"; first = false; lines.push(indent(`${operator} N extends ${right} ? ${result}`)); @@ -58,7 +58,7 @@ function genCurriedType(options: OperatorCodeGenOptions, exponents: number[], le return lines; } -function genCurriedTypeName({ curriedTypeNamePrefix }: OperatorCodeGenOptions, value: number): string { +function genCurriedTypeName({ curriedTypeNamePrefix }: OperatorSpec, value: number): string { return `${curriedTypeNamePrefix}${genValueName(value)}`; } diff --git a/codegen/main.ts b/codegen/main.ts deleted file mode 100644 index 759bf43..0000000 --- a/codegen/main.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CommonOperatorCodeGenOptions } from "./common"; -import { emitCommonTypes, emitOperator } from "./emit"; - -const common: CommonOperatorCodeGenOptions = { - minExponent: -5, - maxExponent: 5, -}; - -emitCommonTypes(common); - -emitOperator({ - ...common, - fileNamePrefix: "addition", - uncurriedTypeNamePrefix: "Add", - curriedTypeNamePrefix: "Add", - testTypeNamePrefix: "Sum", - specialCases: { - [0]: "R", - }, - compute: (left, right) => left + right, -}); - -emitOperator({ - ...common, - fileNamePrefix: "multiplication", - uncurriedTypeNamePrefix: "Multiply", - curriedTypeNamePrefix: "MultiplyBy", - testTypeNamePrefix: "Product", - specialCases: { - [0]: "0", - [1]: "R", - }, - compute: (left, right) => left * right, -}); - -emitOperator({ - ...common, - fileNamePrefix: "division", - uncurriedTypeNamePrefix: "Divide", - curriedTypeNamePrefix: "DividedBy", - testTypeNamePrefix: "Quotient", - specialCases: { - [0]: "(R extends 0 ? ArithmeticError : 0)", - }, - compute: (left, right) => left / right, -}); diff --git a/codegen/produce.ts b/codegen/produce.ts new file mode 100644 index 0000000..26fae92 --- /dev/null +++ b/codegen/produce.ts @@ -0,0 +1,3 @@ +import { emit } from "./emit"; + +emit(); diff --git a/codegen/spec.ts b/codegen/spec.ts new file mode 100644 index 0000000..f025e0f --- /dev/null +++ b/codegen/spec.ts @@ -0,0 +1,41 @@ +import { CodeGenSpec } from "./common"; + +const maxExponent = 5; + +export const codeGenSpec: CodeGenSpec = { + minExponent: -maxExponent, + maxExponent, + operators: [ + { + fileNamePrefix: "addition", + uncurriedTypeNamePrefix: "Add", + curriedTypeNamePrefix: "Add", + testTypeNamePrefix: "Sum", + specialCases: { + [0]: "R", + }, + compute: (left, right) => left + right, + }, + { + fileNamePrefix: "multiplication", + uncurriedTypeNamePrefix: "Multiply", + curriedTypeNamePrefix: "MultiplyBy", + testTypeNamePrefix: "Product", + specialCases: { + [0]: "0", + [1]: "R", + }, + compute: (left, right) => left * right, + }, + { + fileNamePrefix: "division", + uncurriedTypeNamePrefix: "Divide", + curriedTypeNamePrefix: "DividedBy", + testTypeNamePrefix: "Quotient", + specialCases: { + [0]: "(R extends 0 ? ArithmeticError : 0)", + }, + compute: (left, right) => left / right, + }, + ], +}; diff --git a/codegen/verify.ts b/codegen/verify.ts new file mode 100644 index 0000000..2901905 --- /dev/null +++ b/codegen/verify.ts @@ -0,0 +1,44 @@ +import * as fs from "fs"; +import { EmitPlan, getEmitPlans } from "./emit"; + +function verify() { + const emits = getEmitPlans(); + + let index = -1; + const nextDiff = () => { + index++; + if (index === emits.length) { + return; + } + verifyEmitDiff(emits[index], nextDiff); + }; + nextDiff(); +} + +function verifyEmitDiff(emit: EmitPlan, callback: () => void) { + readFile(emit.path, content => { + if (emit.source !== content) { + console.error( + `A change to an auto-generated file was made. Revert the change to '${ + emit.path + }' or rerun 'yarn codegen:produce'.`, + ); + process.exit(1); + } + callback(); + }); +} + +function readFile(path: string, callback: (content: string) => void) { + fs.readFile(path, { encoding: "UTF8" }, (error, content) => { + if (error) { + console.error(`Could not read file ${path}.`); + console.error(error); + process.exit(1); + } else { + callback(content); + } + }); +} + +verify(); diff --git a/package.json b/package.json index ef70f87..f68c9ce 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,16 @@ "version": "0.0.1", "license": "MIT", "scripts": { - "codegen": "npm-run-all -s compile:codegen node:codegen", + "codegen:produce": "npm-run-all -s compile:codegen node:codegen:produce", + "codegen:verify": "npm-run-all -s compile:codegen node:codegen:verify", "compile:src": "tsc -p src", "compile:codegen": "tsc -p codegen", "lint": "tslint -p src/tsconfig.json -c tslint.json '{src,codegen}/**/*.ts'", "lint:fix": "yarn lint --fix", - "node:codegen": "ts-node codegen/main", + "node:codegen:produce": "ts-node codegen/produce", + "node:codegen:verify": "ts-node codegen/verify", "test": "jest --config jest.config.js", - "verify": "npm-run-all -s codegen compile:src lint test" + "verify": "npm-run-all -s codegen:verify compile:src lint test" }, "devDependencies": { "@types/jest": "^22.2.3",