MichaReiser/speedy.js

View on GitHub
packages/compiler/src/code-generation/per-file/per-file-code-generator.ts

Summary

Maintainability
A
0 mins
Test Coverage
import * as assert from "assert";
import * as debug from "debug";
import * as fs from "fs";
import * as llvm from "llvm-node";
import * as path from "path";
import * as ts from "typescript";

import {CompilationContext} from "../../compilation-context";
import {wasmOpt} from "../../external-tools/binaryen-opt";
import {s2wasm} from "../../external-tools/binaryen-s2wasm";
import {wasmAs} from "../../external-tools/binaryen-wasm-as";
import {LLVMLink} from "../../external-tools/llvm-link";
import {llc} from "../../external-tools/llvm-llc";
import {optimize, optimizeLinked} from "../../external-tools/llvm-opt";
import {BuildDirectory} from "../build-directory";
import {CodeGenerationContext} from "../code-generation-context";
import {CodeGenerator} from "../code-generator";
import {DefaultCodeGenerationContextFactory} from "../default-code-generation-context-factory";
import {FunctionBuilder} from "../util/function-builder";
import {createResolvedFunctionFromSignature} from "../value/resolved-function";
import {NoopSourceFileRewriter} from "./llvm-emit-source-file-rewriter";
import {PerFileCodeGeneratorSourceFileRewriter} from "./per-file-code-generator-source-file-rewriter";
import {PerFileSourceFileRewirter} from "./per-file-source-file-rewriter";
import {WastMetaData} from "./wast-meta-data";

const WASM_TRIPLE = "wasm32-unknown-unknown";
const LOG = debug("code-generation/per-file-code-generator");

interface SourceFileState {
    context: CodeGenerationContext;
    sourceFileRewriter: PerFileSourceFileRewirter;
    requestEmitHelper: (emitHelper: ts.EmitHelper) => void;
}

/**
 * Code Generator that creates a dedicate WASM Module for each source file
 */
export class PerFileCodeGenerator implements CodeGenerator {
    private sourceFileStates = new Map<string, SourceFileState>();

    constructor(private context: llvm.LLVMContext, private codeGenerationContextFactory = new DefaultCodeGenerationContextFactory()) {
    }

    beginSourceFile(sourceFile: ts.SourceFile, compilationContext: CompilationContext, requestEmitHelper: (emitHelper: ts.EmitHelper) => void) {
        const context = this.createContext(sourceFile, compilationContext);
        const emitsWasm = !compilationContext.compilerOptions.emitLLVM;
        this.sourceFileStates.set(sourceFile.fileName, {
            context,
            requestEmitHelper,
            sourceFileRewriter: emitsWasm ? new PerFileCodeGeneratorSourceFileRewriter(context) : new NoopSourceFileRewriter()
        });
    }

    generateEntryFunction(functionDeclaration: ts.FunctionDeclaration): ts.FunctionDeclaration {
        const state = this.getSourceFileState(functionDeclaration);
        const context = state.context;

        // function is async, therefore, return type is a promise of T. We compile it down to a function just returning T
        const signature = context.typeChecker.getSignatureFromDeclaration(functionDeclaration);
        const resolvedFunction = createResolvedFunctionFromSignature(signature, context.compilationContext);
        const mangledName = `_${resolvedFunction.functionName}`;
        const builder = FunctionBuilder.create(resolvedFunction, context)
            .name(mangledName)
            .externalLinkage();

        builder.define(resolvedFunction.definition!);
        context.addEntryFunction(mangledName);

        return state.sourceFileRewriter.rewriteEntryFunction(mangledName, functionDeclaration, state.requestEmitHelper);
    }

    completeSourceFile(sourceFile: ts.SourceFile): ts.SourceFile {
        const state = this.getSourceFileState(sourceFile);
        const context = state.context;

        if (context.module.empty || context.getEntryFunctionNames().length === 0) {
            return sourceFile;
        }

        if (context.requiresGc) {
            context.module.getOrInsertFunction("speedyJsGc", llvm.FunctionType.get(llvm.Type.getVoidTy(context.llvmContext), [], false));
        }

        LOG(`Emit module for source file ${sourceFile.fileName}.`);
        // llvm.verifyModule(context.module);
        this .writeModule(sourceFile, state);
        LOG(`Module for source file ${sourceFile.fileName} emitted.`);

        return state.sourceFileRewriter.rewriteSourceFile(sourceFile, state.requestEmitHelper);
    }

    completeCompilation() {
        this.sourceFileStates.clear();
    }

    private writeModule(sourceFile: ts.SourceFile, state: SourceFileState) {
        const context = state.context;
        const buildDirectory = BuildDirectory.createTempBuildDirectory();
        const plainFileName = path.basename(sourceFile.fileName.replace(".ts", ""));

        if (context.compilationContext.compilerOptions.emitLLVM) {
            const llvmIR = context.module.print();
            context.compilationContext.compilerHost.writeFile(getOutputFileName(sourceFile, context, ".ll"), llvmIR, false);
        } else {
            const transforms = PerFileCodeGenerator.createTransformationChain(context);
            const transformationContext = {
                sourceFile,
                codeGenerationContext: context,
                buildDirectory,
                plainFileName,
                sourceFileRewriter: state.sourceFileRewriter
            };

            const biteCodeFileName = buildDirectory.getTempFileName(`${plainFileName}.bc`);
            llvm.writeBitcodeToFile(context.module, biteCodeFileName);

            transforms.reduce((inputFileName, transformation) => transformation.transform(inputFileName, transformationContext), biteCodeFileName);
        }

        buildDirectory.remove();
    }

    private static createTransformationChain(context: CodeGenerationContext) {
        const optimizationLevel = context.compilationContext.compilerOptions.optimizationLevel;

        const transforms: TransformationStep[] = [
            LinkTransformationStep.createRuntimeLinking()
        ];

        if (optimizationLevel !== "0") {
            transforms.push(new OptimizationTransformationStep());
        }

        transforms.push(
            LinkTransformationStep.createSharedLibsLinking(),
            new LinkTimeOptimizationTransformationStep());

        if (context.compilationContext.compilerOptions.saveBc) {
            transforms.push(new CopyFileToOutputDirectoryTransformationStep(".bc", true));
        }

        transforms.push(
            new LLCTransformationStep(),
            new S2WasmTransformationStep()
        );

        if (context.compilationContext.compilerOptions.binaryenOpt && optimizationLevel !== "0") {
            transforms.push(new BinaryenOptTransformationStep());
        }

        if (context.compilationContext.compilerOptions.saveWast) {
            transforms.push(new CopyFileToOutputDirectoryTransformationStep(".wast"));
        }

        transforms.push(new WasmToAsTransformationStep());

        return transforms;
    }

    private getSourceFileState(node: ts.Node): SourceFileState {
        const sourceFile = node.getSourceFile();
        const state = this.sourceFileStates.get(sourceFile.fileName);

        // tslint:disable-next-line:max-line-length
        assert(state, `First call beginSourceFile before calling any other functions on the code generator (state missing for source file ${sourceFile.fileName}).`);

        return state!;
    }

    private createContext(sourceFile: ts.SourceFile, compilationContext: CompilationContext): CodeGenerationContext {
        const relativePath = path.relative(compilationContext.rootDir, sourceFile.fileName);
        const module = new llvm.Module(sourceFile.moduleName || relativePath, this.context);
        module.sourceFileName = relativePath;

        const target = llvm.TargetRegistry.lookupTarget(WASM_TRIPLE);
        const targetMachine = target.createTargetMachine(WASM_TRIPLE, "generic");
        module.dataLayout = targetMachine.createDataLayout();
        module.targetTriple = WASM_TRIPLE;

        return this.codeGenerationContextFactory.createContext(compilationContext, module);
    }
}

interface TransformationContext {
    plainFileName: string;
    buildDirectory: BuildDirectory;
    sourceFile: ts.SourceFile;
    codeGenerationContext: CodeGenerationContext;
    sourceFileRewriter: PerFileSourceFileRewirter;
}

interface TransformationStep {
    /**
     * Performs the transformation
     * @param inputFileName the input file name
     * @return the name of the output file name
     */
    transform(inputFileName: string, transformationContext: TransformationContext): string;
}

class OptimizationTransformationStep implements TransformationStep {
    transform(inputFileName: string, {plainFileName, buildDirectory, codeGenerationContext}: TransformationContext): string {
        const optimizedFileName = buildDirectory.getTempFileName(`${plainFileName}-opt.bc`);
        const optimizationLevel = codeGenerationContext.compilationContext.compilerOptions.optimizationLevel;

        return optimize(inputFileName, optimizedFileName, optimizationLevel);
    }
}

class LinkTransformationStep implements TransformationStep {

    static createRuntimeLinking() {
        return new LinkTransformationStep(true);
    }

    static createSharedLibsLinking() {
        return new LinkTransformationStep(false);
    }

    private constructor(private runtime: boolean) {

    }

    transform(inputFileName: string, {plainFileName, buildDirectory, codeGenerationContext}: TransformationContext): string {
        const llvmLinker = new LLVMLink(buildDirectory);
        const entryFunctions = codeGenerationContext.getEntryFunctionNames();

        llvmLinker.addByteCodeFile(inputFileName);

        if (this.runtime) {
            llvmLinker.addRuntime(codeGenerationContext.compilationContext.compilerOptions.unsafe);
        } else {
            llvmLinker.addSharedLibs();
        }

        return llvmLinker.link(buildDirectory.getTempFileName(`${plainFileName}-linked.o`), entryFunctions);
    }
}

class LinkTimeOptimizationTransformationStep implements TransformationStep {
    transform(inputFileName: string, {plainFileName, buildDirectory, codeGenerationContext}: TransformationContext): string {
        const optimizedFileName = buildDirectory.getTempFileName(`${plainFileName}-lopt.bc`);
        const entryFunctionNames = codeGenerationContext.getEntryFunctionNames();

        return optimizeLinked(inputFileName, entryFunctionNames, optimizedFileName, codeGenerationContext.compilationContext.compilerOptions.optimizationLevel);
    }
}

class CopyFileToOutputDirectoryTransformationStep implements TransformationStep {
    constructor(private extension: string, private binary = false) {}

    transform(inputFileName: string, {plainFileName, buildDirectory, sourceFile, codeGenerationContext}: TransformationContext): string {
        const outputFileName = getOutputFileName(sourceFile, codeGenerationContext, this.extension);

        if (this.binary) {
            // ts writeFile seems not to support binary output, therefore use fs directly
            fs.writeFileSync(outputFileName, fs.readFileSync(inputFileName));
        } else {
            // has the advantage that it can be intercepted by the in memory compiler
            codeGenerationContext.compilationContext.compilerHost.writeFile(outputFileName, ts.sys.readFile(inputFileName), false);
        }
        return inputFileName;
    }
}

class LLCTransformationStep implements TransformationStep {
    transform(inputFileName: string, { plainFileName, buildDirectory }: TransformationContext): string {
        return llc(inputFileName, buildDirectory.getTempFileName(`${plainFileName}.s`));
    }
}

class S2WasmTransformationStep implements TransformationStep {
    transform(inputFileName: string, {buildDirectory, plainFileName, codeGenerationContext, sourceFileRewriter}: TransformationContext): string {
        const compilerOptions = codeGenerationContext.compilationContext.compilerOptions;
        const wastFileName = s2wasm(inputFileName, buildDirectory.getTempFileName(`${plainFileName}.wast`), compilerOptions);
        const glue = S2WasmTransformationStep.getGlueMetadata(wastFileName);
        sourceFileRewriter.setWastMetaData(glue);
        return wastFileName;
    }

    // glue code https://github.com/kripken/emscripten/blob/5387c1e5f1f55b69c41b94cf044062c59e052f0b/emscripten.py#L1415
    private static getGlueMetadata(wastFileName: string): WastMetaData {
        const wastContent = ts.sys.readFile(wastFileName);
        const parts = wastContent.split("\n;; METADATA:");

        const metaDataRaw = parts[1];
        return JSON.parse(metaDataRaw);
    }
}

class BinaryenOptTransformationStep implements TransformationStep {
    transform(inputFileName: string, { buildDirectory, plainFileName, codeGenerationContext }: TransformationContext): string {
        const outputFile = buildDirectory.getTempFileName(`${plainFileName}-opt.wast`);
        return wasmOpt(inputFileName, outputFile);
    }
}

class WasmToAsTransformationStep implements TransformationStep {
    transform(inputFileName: string, { buildDirectory, plainFileName, sourceFileRewriter, sourceFile, codeGenerationContext}: TransformationContext): string {
        const wasmFileName = buildDirectory.getTempFileName(`${plainFileName}.wasm`);
        wasmAs(inputFileName, wasmFileName);

        const wasmFileWriter = codeGenerationContext.compilationContext.compilerOptions.wasmFileWriter;
        const finalWasmFileName = getOutputFileName(sourceFile, codeGenerationContext);
        const wasmFetchExpression = wasmFileWriter.writeWasmFile(finalWasmFileName, fs.readFileSync(wasmFileName), codeGenerationContext);
        sourceFileRewriter.setWasmUrl(wasmFetchExpression);

        return wasmFileName;
    }
}

function getOutputFileName(sourceFile: ts.SourceFile, context: CodeGenerationContext, fileExtension = ".wasm") {
    const withNewExtension = sourceFile.fileName.replace(".ts", fileExtension);
    if (context.compilationContext.compilerOptions.outDir) {
        const relativeName = path.relative(context.compilationContext.rootDir, withNewExtension);
        return path.join(context.compilationContext.compilerOptions.outDir, relativeName);
    }

    return withNewExtension;
}