"use strict";
// -*- mode: typescript; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Genie
//
// Copyright 2020 The Board of Trustees of the Leland Stanford Junior University
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.importGenie = exports.compile = exports.Compiler = void 0;
const fs_1 = require("fs");
const path = __importStar(require("path"));
const ts = __importStar(require("typescript"));
const metagrammar = __importStar(require("./grammar"));
const metaast = __importStar(require("./meta_ast"));
const COMPILER_OPTIONS = {
    module: ts.ModuleKind.CommonJS,
    target: ts.ScriptTarget.ES2018,
    allowJs: true,
    esModuleInterop: true,
    moduleResolution: ts.ModuleResolutionKind.NodeJs,
    resolveJsonModule: true,
    noUnusedLocals: false,
    strict: false,
};
class Compiler {
    constructor(target) {
        this._files = new Map();
        this._nonTerm = new Set();
        // map a non-terminal to its type declaration, if any
        this._typeMap = new Map();
        // map a type to its key function, if any
        this._keyFnMap = new Map();
        this._target = target;
    }
    async _loadFile(filename) {
        if (this._files.has(filename))
            return;
        const dirname = path.dirname(filename);
        const input = await fs_1.promises.readFile(filename, { encoding: 'utf8' });
        let parsed;
        try {
            parsed = metagrammar.parse(input);
        }
        catch (e) {
            e.fileName = filename;
            throw e;
        }
        this._files.set(filename, parsed);
        const allImports = new Set();
        const self = this;
        parsed.visit(new class extends metaast.NodeVisitor {
            visitImport(stmt) {
                const resolved = path.resolve(dirname, stmt.what);
                allImports.add(resolved);
            }
            visitContextStmt(stmt) {
                for (const symbol of stmt.names)
                    self._nonTerm.add(symbol);
                if (!stmt.type)
                    return;
                for (const symbol of stmt.names)
                    self._typeMap.set(symbol, stmt.type);
            }
            visitNonTerminalStmt(stmt) {
                const symbol = stmt.name;
                self._nonTerm.add(symbol);
                if (stmt.type === undefined || stmt.type === 'any')
                    return;
                const existing = self._typeMap.get(symbol);
                if (!existing || existing === 'any') {
                    self._typeMap.set(symbol, stmt.type);
                    return;
                }
                if (existing !== stmt.type)
                    throw new TypeError(`Invalid conflicting type annotation for non-terminal ${symbol}, have ${existing} want ${stmt.type}`);
            }
            visitKeyFunctionDeclaration(stmt) {
                for (let [type, keyfn] of stmt.decls) {
                    type = type.trim();
                    keyfn = keyfn.trim();
                    const existing = self._keyFnMap.get(type);
                    if (!existing) {
                        self._keyFnMap.set(type, keyfn);
                        continue;
                    }
                    if (existing !== keyfn)
                        throw new TypeError(`Invalid conflicting key function declaration for type ${type}`);
                }
            }
        });
        for (const import_ of allImports)
            await this._loadFile(import_);
    }
    _assignAllTypes() {
        const self = this;
        this.visit(new class extends metaast.NodeVisitor {
            // assign a type to every usage of a non-terminal
            visitNonTerminalRuleHead(node) {
                const symbol = node.category;
                if (!self._nonTerm.has(symbol))
                    throw new TypeError(`Undeclared non-terminal ${symbol}`);
                const type = self._typeMap.get(symbol);
                if (type) {
                    node.type = type;
                    node.keyfn = self._keyFnMap.get(type.trim()) || 'undefined';
                }
                if (node.constraint && node.keyfn === 'undefined')
                    console.log(`WARNING: missing key function for type ${node.type}, which is used in constraint for non-terminal ${symbol}`);
            }
            // also assign a type to every non-terminal declaration, if
            // it doesn't have one already
            visitNonTerminalStmt(stmt) {
                if (stmt.type) {
                    stmt.keyfn = self._keyFnMap.get(stmt.type.trim()) || 'undefined';
                    return;
                }
                const symbol = stmt.name;
                const existing = self._typeMap.get(symbol);
                if (existing) {
                    stmt.type = existing;
                    stmt.keyfn = self._keyFnMap.get(existing.trim()) || 'undefined';
                }
            }
        });
    }
    visit(visitor) {
        for (const parsed of this._files.values())
            parsed.visit(visitor);
    }
    async parse(filename) {
        // load all template files and extract all the type annotations
        await this._loadFile(filename);
        // assign the type annotations to all the uses of the non-terminals
        this._assignAllTypes();
    }
    async process(filename) {
        await this.parse(filename);
        await this._outputAllFiles();
    }
    async _outputAllFiles() {
        for (const [filename, parsed] of this._files)
            await this._outputFile(filename, parsed);
    }
    async _outputFile(filename, parsed) {
        const outputFile = filename + '.' + this._target;
        let output = parsed.codegen();
        if (this._target === 'js') {
            const result = ts.transpileModule(output, {
                compilerOptions: COMPILER_OPTIONS,
                fileName: filename,
                reportDiagnostics: true,
            });
            if (result.diagnostics && result.diagnostics.length > 0) {
                let error = '';
                result.diagnostics.forEach((diagnostic) => {
                    if (diagnostic.file) {
                        const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
                        const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
                        error += `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}\n`;
                    }
                    else {
                        error += ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') + '\n';
                    }
                });
                throw new Error(`TypeScript compilation failed: ${error}`);
            }
            output = result.outputText;
        }
        await fs_1.promises.writeFile(outputFile, output);
    }
}
exports.Compiler = Compiler;
function compile(filename) {
    return new Compiler('ts').process(filename);
}
exports.compile = compile;
async function importGenie(filename, searchPath = '.') {
    filename = path.resolve(searchPath, filename);
    // try loading compiled js first
    let target = 'js';
    try {
        if (filename.endsWith('.js'))
            return (await Promise.resolve().then(() => __importStar(require(filename)))).default;
        else
            return (await Promise.resolve().then(() => __importStar(require(filename + '.' + target)))).default;
    }
    catch (e) {
        if (e.code !== 'MODULE_NOT_FOUND')
            throw e;
    }
    // if that did not work, try compiling the template on-demand
    // we compile to .ts if we're running in ts-node or nyc with the
    // typescript extensions
    // and compile to js ourselves otherwise
    // (in the latter case, type errors won't be reported across modules)
    target = require.extensions['.ts'] ? 'ts' : 'js';
    try {
        await fs_1.promises.access(filename + '.' + target);
    }
    catch (e) {
        if (e.code !== 'ENOENT')
            throw e;
        await new Compiler(target).process(filename);
    }
    return (await Promise.resolve().then(() => __importStar(require(filename + '.' + target)))).default;
}
exports.importGenie = importGenie;
//# sourceMappingURL=index.js.map