diff --git a/cli/asc.d.ts b/cli/asc.d.ts index 3244b4e80b..7957ac7c2f 100644 --- a/cli/asc.d.ts +++ b/cli/asc.d.ts @@ -56,7 +56,7 @@ export interface MemoryStream extends OutputStream { } /** Compiler options. */ -interface CompilerOptions { +export interface CompilerOptions { /** Prints just the compiler's version and exits. */ version?: boolean; /** Prints the help message and exits. */ @@ -146,7 +146,7 @@ interface CompilerOptions { } /** Compiler API options. */ -interface APIOptions { +export interface APIOptions { /** Standard output stream to use. */ stdout?: OutputStream; /** Standard error stream to use. */ diff --git a/cli/asc.js b/cli/asc.js index bf724ff16d..3936ff7768 100644 --- a/cli/asc.js +++ b/cli/asc.js @@ -41,8 +41,9 @@ const mkdirp = require("./util/mkdirp"); const find = require("./util/find"); const binaryen = global.binaryen || (global.binaryen = require("binaryen")); -const EOL = process.platform === "win32" ? "\r\n" : "\n"; -const SEP = process.platform === "win32" ? "\\" : "/"; +const WIN = process.platform === "win32"; +const EOL = WIN ? "\r\n" : "\n"; +const SEP = WIN ? "\\" : "/"; // Sets up an extension with its definition counterpart and relevant regexes. function setupExtension(extension) { @@ -193,11 +194,12 @@ exports.main = function main(argv, options, callback) { if (!stdout) throw Error("'options.stdout' must be specified"); if (!stderr) throw Error("'options.stderr' must be specified"); - const opts = optionsUtil.parse(argv, exports.options); - let args = opts.options; + // Parse command line options but do not populate option defaults yet + const optionsResult = optionsUtil.parse(argv, exports.options, false); + let opts = optionsResult.options; + argv = optionsResult.arguments; - argv = opts.arguments; - if (args.noColors) { + if (opts.noColors) { colorsUtil.stdout.supported = colorsUtil.stderr.supported = false; } else { @@ -205,16 +207,18 @@ exports.main = function main(argv, options, callback) { colorsUtil.stderr = colorsUtil.from(stderr); } - // Check for unknown arguments - if (opts.unknown.length) { - opts.unknown.forEach(arg => { + // Check for unknown options + const unknownOpts = optionsResult.unknown; + if (unknownOpts.length) { + unknownOpts.forEach(arg => { stderr.write(colorsUtil.stderr.yellow("WARNING ") + "Unknown option '" + arg + "'" + EOL); }); } // Check for trailing arguments - if (opts.trailing.length) { - stderr.write(colorsUtil.stderr.yellow("WARNING ") + "Unsupported trailing arguments: " + opts.trailing.join(" ") + EOL); + const trailingArgv = optionsResult.trailing; + if (trailingArgv.length) { + stderr.write(colorsUtil.stderr.yellow("WARNING ") + "Unsupported trailing arguments: " + trailingArgv.join(" ") + EOL); } // Use default callback if none is provided @@ -228,24 +232,24 @@ exports.main = function main(argv, options, callback) { }; // Just print the version if requested - if (args.version) { + if (opts.version) { stdout.write("Version " + exports.version + (isDev ? "-dev" : "") + EOL); return callback(null); } // Use another extension if requested - if (typeof args.extension === "string") { - if (/^\.?[0-9a-zA-Z]{1,14}$/.test(args.extension)) { - extension = setupExtension(args.extension); + if (typeof opts.extension === "string") { + if (/^\.?[0-9a-zA-Z]{1,14}$/.test(opts.extension)) { + extension = setupExtension(opts.extension); } else { - return callback(Error("Invalid extension: " + args.extension)); + return callback(Error("Invalid extension: " + opts.extension)); } } // Print the help message if requested or no source files are provided - if (args.help || !argv.length) { - var out = args.help ? stdout : stderr; - var color = args.help ? colorsUtil.stdout : colorsUtil.stderr; + if (opts.help || !argv.length) { + var out = opts.help ? stdout : stderr; + var color = opts.help ? colorsUtil.stdout : colorsUtil.stderr; out.write([ color.white("SYNTAX"), " " + color.cyan("asc") + " [entryFile ...] [options]", @@ -270,78 +274,62 @@ exports.main = function main(argv, options, callback) { } // Set up base directory - const baseDir = args.baseDir ? path.resolve(args.baseDir) : "."; - const target = args.target; + const baseDir = path.normalize(opts.baseDir || "."); - // Once the baseDir is calculated, we can resolve the config, and its extensions - let asconfig = getAsconfig(args.config, baseDir, readFile); - let asconfigDir = baseDir; + // Load additional options from asconfig.json + let asconfigPath = optionsUtil.resolvePath(opts.config || "asconfig.json", baseDir); + let asconfigFile = path.basename(asconfigPath); + let asconfigDir = path.dirname(asconfigPath); + let asconfig = getAsconfig(asconfigFile, asconfigDir, readFile); const seenAsconfig = new Set(); - seenAsconfig.add(path.join(baseDir, args.config)); + seenAsconfig.add(asconfigPath); + const target = opts.target || "release"; while (asconfig) { - // merge target first, then merge options, then merge extended asconfigs - if (asconfig.targets && asconfig.targets[target]) { - args = optionsUtil.merge(exports.options, asconfig.targets[target], args); - } - if (asconfig.options) { - if (asconfig.options.transform) { - // ensure that a transform's path is relative to the current config - asconfig.options.transform = asconfig.options.transform.map(p => { - if (!path.isAbsolute(p)) { - if (p.startsWith(".")) { - return path.join(asconfigDir, p); - } - return require.resolve(p); - } - return p; - }); + // Merge target first + if (asconfig.targets) { + const targetOptions = asconfig.targets[target]; + if (targetOptions) { + opts = optionsUtil.merge(exports.options, opts, targetOptions, asconfigDir); } - args = optionsUtil.merge(exports.options, args, asconfig.options); + } + // Merge general options + const generalOptions = asconfig.options; + if (generalOptions) { + opts = optionsUtil.merge(exports.options, opts, generalOptions, asconfigDir); } - // entries are added to the compilation + // Append entries if (asconfig.entries) { - for (const entry of asconfig.entries) { - argv.push( - path.isAbsolute(entry) - ? entry - // the entry is relative to the asconfig directory - : path.join(asconfigDir, entry) - ); + for (let entry of asconfig.entries) { + argv.push(path.relative(baseDir, optionsUtil.resolvePath(entry, asconfigDir))); } } - // asconfig "extends" another config, merging options of it's parent + // Look up extended asconfig and repeat if (asconfig.extends) { - asconfigDir = path.isAbsolute(asconfig.extends) - // absolute extension path means we know the exact directory and location - ? path.dirname(asconfig.extends) - // relative means we need to calculate a relative asconfigDir - : path.join(asconfigDir, path.dirname(asconfig.extends)); - const fileName = path.basename(asconfig.extends); - const filePath = path.join(asconfigDir, fileName); - if (seenAsconfig.has(filePath)) { - asconfig = null; - } else { - seenAsconfig.add(filePath); - asconfig = getAsconfig(fileName, asconfigDir, readFile); - } + asconfigPath = optionsUtil.resolvePath(asconfig.extends, asconfigDir, true); + asconfigFile = path.basename(asconfigPath); + asconfigDir = path.dirname(asconfigPath); + if (seenAsconfig.has(asconfigPath)) break; + seenAsconfig.add(asconfigPath); + asconfig = getAsconfig(asconfigFile, asconfigDir, readFile); } else { - asconfig = null; // finished resolving the configuration chain + break; } } - // If showConfig print args and exit - if (args.showConfig) { - stderr.write(JSON.stringify(args, null, 2)); - return callback(null); - } + // Populate option defaults once user-defined options are set + optionsUtil.addDefaults(exports.options, opts); - // This method resolves a path relative to the baseDir instead of process.cwd() - function resolveBasedir(arg) { - return path.resolve(baseDir, arg); + // If showConfig print options and exit + if (opts.showConfig) { + stderr.write(JSON.stringify({ + options: opts, + entries: argv + }, null, 2)); + return callback(null); } // create a unique set of values @@ -349,36 +337,28 @@ exports.main = function main(argv, options, callback) { return [...new Set(values)]; } - // returns a relative path from baseDir - function makeRelative(arg) { - return path.relative(baseDir, arg); - } - - // postprocess we need to get absolute file locations argv - argv = unique(argv.map(resolveBasedir)).map(makeRelative); - // Set up options const compilerOptions = assemblyscript.newOptions(); assemblyscript.setTarget(compilerOptions, 0); - assemblyscript.setNoAssert(compilerOptions, args.noAssert); - assemblyscript.setExportMemory(compilerOptions, !args.noExportMemory); - assemblyscript.setImportMemory(compilerOptions, args.importMemory); - assemblyscript.setInitialMemory(compilerOptions, args.initialMemory >>> 0); - assemblyscript.setMaximumMemory(compilerOptions, args.maximumMemory >>> 0); - assemblyscript.setSharedMemory(compilerOptions, args.sharedMemory); - assemblyscript.setImportTable(compilerOptions, args.importTable); - assemblyscript.setExportTable(compilerOptions, args.exportTable); - assemblyscript.setExplicitStart(compilerOptions, args.explicitStart); - assemblyscript.setMemoryBase(compilerOptions, args.memoryBase >>> 0); - assemblyscript.setTableBase(compilerOptions, args.tableBase >>> 0); - assemblyscript.setSourceMap(compilerOptions, args.sourceMap != null); - assemblyscript.setNoUnsafe(compilerOptions, args.noUnsafe); - assemblyscript.setPedantic(compilerOptions, args.pedantic); - assemblyscript.setLowMemoryLimit(compilerOptions, args.lowMemoryLimit >>> 0); + assemblyscript.setNoAssert(compilerOptions, opts.noAssert); + assemblyscript.setExportMemory(compilerOptions, !opts.noExportMemory); + assemblyscript.setImportMemory(compilerOptions, opts.importMemory); + assemblyscript.setInitialMemory(compilerOptions, opts.initialMemory >>> 0); + assemblyscript.setMaximumMemory(compilerOptions, opts.maximumMemory >>> 0); + assemblyscript.setSharedMemory(compilerOptions, opts.sharedMemory); + assemblyscript.setImportTable(compilerOptions, opts.importTable); + assemblyscript.setExportTable(compilerOptions, opts.exportTable); + assemblyscript.setExplicitStart(compilerOptions, opts.explicitStart); + assemblyscript.setMemoryBase(compilerOptions, opts.memoryBase >>> 0); + assemblyscript.setTableBase(compilerOptions, opts.tableBase >>> 0); + assemblyscript.setSourceMap(compilerOptions, opts.sourceMap != null); + assemblyscript.setNoUnsafe(compilerOptions, opts.noUnsafe); + assemblyscript.setPedantic(compilerOptions, opts.pedantic); + assemblyscript.setLowMemoryLimit(compilerOptions, opts.lowMemoryLimit >>> 0); // Add or override aliases if specified - if (args.use) { - let aliases = args.use; + if (opts.use) { + let aliases = opts.use; for (let i = 0, k = aliases.length; i < k; ++i) { let part = aliases[i]; let p = part.indexOf("="); @@ -392,7 +372,7 @@ exports.main = function main(argv, options, callback) { // Disable default features if specified var features; - if ((features = args.disable) != null) { + if ((features = opts.disable) != null) { if (typeof features === "string") features = features.split(","); for (let i = 0, k = features.length; i < k; ++i) { let name = features[i].trim(); @@ -403,7 +383,7 @@ exports.main = function main(argv, options, callback) { } // Enable experimental features if specified - if ((features = args.enable) != null) { + if ((features = opts.enable) != null) { if (typeof features === "string") features = features.split(","); for (let i = 0, k = features.length; i < k; ++i) { let name = features[i].trim(); @@ -416,12 +396,12 @@ exports.main = function main(argv, options, callback) { // Set up optimization levels var optimizeLevel = 0; var shrinkLevel = 0; - if (args.optimize) { + if (opts.optimize) { optimizeLevel = exports.defaultOptimizeLevel; shrinkLevel = exports.defaultShrinkLevel; } - if (typeof args.optimizeLevel === "number") optimizeLevel = args.optimizeLevel; - if (typeof args.shrinkLevel === "number") shrinkLevel = args.shrinkLevel; + if (typeof opts.optimizeLevel === "number") optimizeLevel = opts.optimizeLevel; + if (typeof opts.shrinkLevel === "number") shrinkLevel = opts.shrinkLevel; optimizeLevel = Math.min(Math.max(optimizeLevel, 0), 3); shrinkLevel = Math.min(Math.max(shrinkLevel, 0), 2); assemblyscript.setOptimizeLevelHints(compilerOptions, optimizeLevel, shrinkLevel); @@ -431,9 +411,9 @@ exports.main = function main(argv, options, callback) { // Set up transforms const transforms = []; - if (args.transform) { + if (opts.transform) { let tsNodeRegistered = false; - let transformArgs = unique(args.transform.map(resolveBasedir)); + let transformArgs = unique(opts.transform); for (let i = 0, k = transformArgs.length; i < k; ++i) { let filename = transformArgs[i].trim(); if (!tsNodeRegistered && filename.endsWith(".ts")) { // ts-node requires .ts specifically @@ -488,10 +468,10 @@ exports.main = function main(argv, options, callback) { }); }); let customLibDirs = []; - if (args.lib) { - let lib = args.lib; - if (typeof lib === "string") lib = lib.trim().split(/\s*,\s*/); - customLibDirs.push(...lib.map(resolveBasedir)); + if (opts.lib) { + let lib = opts.lib; + if (typeof lib === "string") lib = lib.split(","); + customLibDirs.push(...lib.map(p => p.trim())); customLibDirs = unique(customLibDirs); // `lib` and `customLibDirs` may include duplicates for (let i = 0, k = customLibDirs.length; i < k; ++i) { // custom let libDir = customLibDirs[i]; @@ -514,7 +494,7 @@ exports.main = function main(argv, options, callback) { } } } - args.path = args.path || []; + opts.path = opts.path || []; // Maps package names to parent directory var packageMains = new Map(); @@ -567,14 +547,14 @@ exports.main = function main(argv, options, callback) { const isPackageRoot = match[2] === undefined; const filePath = isPackageRoot ? "index" : match[2]; const basePath = packageBases.has(dependeePath) ? packageBases.get(dependeePath) : "."; - if (args.traceResolution) stderr.write("Looking for package '" + packageName + "' file '" + filePath + "' relative to '" + basePath + "'" + EOL); - const absBasePath = path.isAbsolute(basePath) ? basePath : path.join(baseDir, basePath); + if (opts.traceResolution) stderr.write("Looking for package '" + packageName + "' file '" + filePath + "' relative to '" + basePath + "'" + EOL); const paths = []; - for (let parts = absBasePath.split(SEP), i = parts.length, k = SEP == "/" ? 0 : 1; i >= k; --i) { + const parts = path.resolve(baseDir, basePath).split(SEP); + for (let i = parts.length, k = WIN ? 1 : 0; i >= k; --i) { if (parts[i - 1] !== "node_modules") paths.push(parts.slice(0, i).join(SEP) + SEP + "node_modules"); } - for (const currentPath of paths.concat(...args.path).map(p => path.relative(baseDir, p))) { - if (args.traceResolution) stderr.write(" in " + path.join(currentPath, packageName) + EOL); + for (const currentPath of paths.concat(...opts.path).map(p => path.relative(baseDir, p))) { + if (opts.traceResolution) stderr.write(" in " + path.join(currentPath, packageName) + EOL); let mainPath = "assembly"; if (packageMains.has(packageName)) { // use cached mainPath = packageMains.get(packageName); @@ -596,14 +576,14 @@ exports.main = function main(argv, options, callback) { if ((sourceText = readFile(path.join(mainDir, plainName + extension.ext), baseDir)) != null) { sourcePath = libraryPrefix + packageName + "/" + plainName + extension.ext; packageBases.set(sourcePath.replace(extension.re, ""), path.join(currentPath, packageName)); - if (args.traceResolution) stderr.write(" -> " + path.join(mainDir, plainName + extension.ext) + EOL); + if (opts.traceResolution) stderr.write(" -> " + path.join(mainDir, plainName + extension.ext) + EOL); break; } else if (!isPackageRoot) { const indexName = filePath + "/index"; if ((sourceText = readFile(path.join(mainDir, indexName + extension.ext), baseDir)) !== null) { sourcePath = libraryPrefix + packageName + "/" + indexName + extension.ext; packageBases.set(sourcePath.replace(extension.re, ""), path.join(currentPath, packageName)); - if (args.traceResolution) stderr.write(" -> " + path.join(mainDir, indexName + extension.ext) + EOL); + if (opts.traceResolution) stderr.write(" -> " + path.join(mainDir, indexName + extension.ext) + EOL); break; } } @@ -641,7 +621,7 @@ exports.main = function main(argv, options, callback) { // Include runtime template before entry files so its setup runs first { - let runtimeName = String(args.runtime); + let runtimeName = String(opts.runtime); let runtimePath = "rt/index-" + runtimeName; let runtimeText = exports.libraryFiles[runtimePath]; if (runtimeText == null) { @@ -701,7 +681,7 @@ exports.main = function main(argv, options, callback) { } // Print files and exit if listFiles - if (args.listFiles) { + if (opts.listFiles) { // FIXME: not a proper C-like API stderr.write(program.sources.map(s => s.normalizedPath).sort().join(EOL) + EOL); return callback(null); @@ -739,7 +719,7 @@ exports.main = function main(argv, options, callback) { } // Validate the module if requested - if (!args.noValidate) { + if (!opts.noValidate) { stats.validateCount++; let isValid; stats.validateTime += measure(() => { @@ -752,32 +732,32 @@ exports.main = function main(argv, options, callback) { } // Set Binaryen-specific options - if (args.trapMode === "clamp") { + if (opts.trapMode === "clamp") { stats.optimizeCount++; stats.optimizeTime += measure(() => { module.runPass("trap-mode-clamp"); }); - } else if (args.trapMode === "js") { + } else if (opts.trapMode === "js") { stats.optimizeCount++; stats.optimizeTime += measure(() => { module.runPass("trap-mode-js"); }); - } else if (args.trapMode !== "allow") { + } else if (opts.trapMode !== "allow") { module.dispose(); return callback(Error("Unsupported trap mode")); } // Optimize the module - const debugInfo = args.debug; - const usesARC = args.runtime == "half" || args.runtime == "full"; - const converge = args.converge; + const debugInfo = opts.debug; + const usesARC = opts.runtime == "half" || opts.runtime == "full"; + const converge = opts.converge; const runPasses = []; - if (args.runPasses) { - if (typeof args.runPasses === "string") { - args.runPasses = args.runPasses.split(","); + if (opts.runPasses) { + if (typeof opts.runPasses === "string") { + opts.runPasses = opts.runPasses.split(","); } - if (args.runPasses.length) { - args.runPasses.forEach(pass => { + if (opts.runPasses.length) { + opts.runPasses.forEach(pass => { if (runPasses.indexOf(pass = pass.trim()) < 0) runPasses.push(pass); }); @@ -807,30 +787,30 @@ exports.main = function main(argv, options, callback) { }); // Prepare output - if (!args.noEmit) { - if (args.outFile != null) { - if (/\.was?t$/.test(args.outFile) && args.textFile == null) { - args.textFile = args.outFile; - } else if (/\.js$/.test(args.outFile) && args.jsFile == null) { - args.jsFile = args.outFile; - } else if (args.binaryFile == null) { - args.binaryFile = args.outFile; + if (!opts.noEmit) { + if (opts.outFile != null) { + if (/\.was?t$/.test(opts.outFile) && opts.textFile == null) { + opts.textFile = opts.outFile; + } else if (/\.js$/.test(opts.outFile) && opts.jsFile == null) { + opts.jsFile = opts.outFile; + } else if (opts.binaryFile == null) { + opts.binaryFile = opts.outFile; } } let hasStdout = false; - let hasOutput = args.textFile != null - || args.binaryFile != null - || args.jsFile != null - || args.tsdFile != null - || args.idlFile != null; + let hasOutput = opts.textFile != null + || opts.binaryFile != null + || opts.jsFile != null + || opts.tsdFile != null + || opts.idlFile != null; // Write binary - if (args.binaryFile != null) { - let basename = path.basename(args.binaryFile); - let sourceMapURL = args.sourceMap != null - ? args.sourceMap.length - ? args.sourceMap + if (opts.binaryFile != null) { + let basename = path.basename(opts.binaryFile); + let sourceMapURL = opts.sourceMap != null + ? opts.sourceMap.length + ? opts.sourceMap : "./" + basename + ".map" : null; @@ -840,8 +820,8 @@ exports.main = function main(argv, options, callback) { wasm = module.toBinary(sourceMapURL); }); - if (args.binaryFile.length) { - writeFile(args.binaryFile, wasm.output, baseDir); + if (opts.binaryFile.length) { + writeFile(opts.binaryFile, wasm.output, baseDir); } else { writeStdout(wasm.output); hasStdout = true; @@ -849,7 +829,7 @@ exports.main = function main(argv, options, callback) { // Post-process source map if (wasm.sourceMap != null) { - if (args.binaryFile.length) { + if (opts.binaryFile.length) { let map = JSON.parse(wasm.sourceMap); map.sourceRoot = "./" + basename; let contents = []; @@ -860,7 +840,7 @@ exports.main = function main(argv, options, callback) { }); map.sourcesContent = contents; writeFile(path.join( - path.dirname(args.binaryFile), + path.dirname(opts.binaryFile), path.basename(sourceMapURL) ).replace(/^\.\//, ""), JSON.stringify(map), baseDir); } else { @@ -870,14 +850,14 @@ exports.main = function main(argv, options, callback) { } // Write text (also fallback) - if (args.textFile != null || !hasOutput) { + if (opts.textFile != null || !hasOutput) { let wat; - if (args.textFile != null && args.textFile.length) { + if (opts.textFile != null && opts.textFile.length) { stats.emitCount++; stats.emitTime += measure(() => { wat = module.toText(); }); - writeFile(args.textFile, wat, baseDir); + writeFile(opts.textFile, wat, baseDir); } else if (!hasStdout) { stats.emitCount++; stats.emitTime += measure(() => { @@ -888,14 +868,14 @@ exports.main = function main(argv, options, callback) { } // Write WebIDL - if (args.idlFile != null) { + if (opts.idlFile != null) { let idl; - if (args.idlFile.length) { + if (opts.idlFile.length) { stats.emitCount++; stats.emitTime += measure(() => { idl = assemblyscript.buildIDL(program); }); - writeFile(args.idlFile, idl, baseDir); + writeFile(opts.idlFile, idl, baseDir); } else if (!hasStdout) { stats.emitCount++; stats.emitTime += measure(() => { @@ -907,14 +887,14 @@ exports.main = function main(argv, options, callback) { } // Write TypeScript definition - if (args.tsdFile != null) { + if (opts.tsdFile != null) { let tsd; - if (args.tsdFile.length) { + if (opts.tsdFile.length) { stats.emitCount++; stats.emitTime += measure(() => { tsd = assemblyscript.buildTSD(program); }); - writeFile(args.tsdFile, tsd, baseDir); + writeFile(opts.tsdFile, tsd, baseDir); } else if (!hasStdout) { stats.emitCount++; stats.emitTime += measure(() => { @@ -926,14 +906,14 @@ exports.main = function main(argv, options, callback) { } // Write JS (modifies the binary, so must be last) - if (args.jsFile != null) { + if (opts.jsFile != null) { let js; - if (args.jsFile.length) { + if (opts.jsFile.length) { stats.emitCount++; stats.emitTime += measure(() => { js = module.toAsmjs(); }); - writeFile(args.jsFile, js, baseDir); + writeFile(opts.jsFile, js, baseDir); } else if (!hasStdout) { stats.emitCount++; stats.emitTime += measure(() => { @@ -945,7 +925,7 @@ exports.main = function main(argv, options, callback) { } module.dispose(); - if (args.measure) { + if (opts.measure) { printStats(stats, stderr); } diff --git a/cli/asc.json b/cli/asc.json index 9b194d7861..b9de41a05a 100644 --- a/cli/asc.json +++ b/cli/asc.json @@ -21,13 +21,13 @@ "category": "General", "description": "Configuration file to apply. CLI arguments take precedence.", "type": "s", - "default": "asconfig.json" + "cliOnly": true }, "target": { "category": "General", "description": "Target configuration to use. Defaults to 'release'.", "type": "s", - "default": "release" + "cliOnly": true }, "optimize": { @@ -247,7 +247,9 @@ "transform": { "category": "API", "description": "Specifies the path to a custom transform to 'require'.", - "type": "S" + "type": "S", + "isPath": true, + "useNodeResolution": true }, "trapMode": { @@ -285,7 +287,8 @@ }, "extension": { "description": "Specifies an alternative file extension to use.", - "type": "s" + "type": "s", + "cliOnly": true }, "noUnsafe": { "description": [ @@ -320,7 +323,8 @@ "Adds one or multiple paths to custom library components and", "uses exports of all top-level files at this path as globals." ], - "type": "s" + "type": "S", + "isPath": true }, "path": { "description": [ @@ -328,7 +332,8 @@ "to node_modules. Prefers an 'ascMain' entry in a package's", "package.json and falls back to an inner 'assembly/' folder." ], - "type": "S" + "type": "S", + "isPath": true }, "traceResolution": { "description": "Enables tracing of package resolution.", diff --git a/cli/util/options.d.ts b/cli/util/options.d.ts index e12013fef3..6a727628e9 100644 --- a/cli/util/options.d.ts +++ b/cli/util/options.d.ts @@ -58,7 +58,10 @@ interface HelpOptions { export function help(config: Config, options?: HelpOptions): string; /** Merges two sets of options into one, preferring the current over the parent set. */ -export function merge(config: Config, currentOptions: OptionSet, parentOptions: OptionSet): OptionSet; +export function merge(config: Config, currentOptions: OptionSet, parentOptions: OptionSet, parentBaseDir: string): OptionSet; + +/** Resolves a single relative path. Keeps absolute paths, otherwise prepends baseDir. */ +export function resolvePath(path: string, baseDir: string, useNodeResolution?: boolean): string; /** Populates default values on a parsed options result. */ -export function addDefaults(config: Config, options: OptionSet): OptionSet; +export function addDefaults(config: Config, options: OptionSet): void; diff --git a/cli/util/options.js b/cli/util/options.js index 742df4d8cc..1aedac3dc6 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -3,6 +3,7 @@ * @license Apache-2.0 */ +const path = require("path"); const colorsUtil = require("./colors"); // type | meaning @@ -31,7 +32,7 @@ function parse(argv, config, propagateDefaults = true) { if (typeof option.alias === "string") aliases[option.alias] = key; else if (Array.isArray(option.alias)) option.alias.forEach(alias => aliases[alias] = key); } - if (option.default != null) options[key] = option.default; + if (propagateDefaults && option.default != null) options[key] = option.default; }); // iterate over argv @@ -171,22 +172,29 @@ function sanitizeValue(value, type) { } /** Merges two sets of options into one, preferring the current over the parent set. */ -function merge(config, currentOptions, parentOptions) { +function merge(config, currentOptions, parentOptions, parentBaseDir) { const mergedOptions = {}; - for (const [key, { type, mutuallyExclusive }] of Object.entries(config)) { + for (const [key, { type, mutuallyExclusive, isPath, useNodeResolution, cliOnly }] of Object.entries(config)) { let currentValue = sanitizeValue(currentOptions[key], type); let parentValue = sanitizeValue(parentOptions[key], type); if (currentValue == null) { if (parentValue != null) { // only parent value present + if (cliOnly) continue; if (Array.isArray(parentValue)) { let exclude; + if (isPath) { + parentValue = parentValue.map(value => resolvePath(value, parentBaseDir, useNodeResolution)); + } if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) { mergedOptions[key] = parentValue.filter(value => !exclude.includes(value)); } else { mergedOptions[key] = parentValue.slice(); } } else { + if (isPath) { + parentValue = resolvePath(parentValue, parentBaseDir, useNodeResolution); + } mergedOptions[key] = parentValue; } } @@ -200,7 +208,14 @@ function merge(config, currentOptions, parentOptions) { } else { // both current and parent values present if (Array.isArray(currentValue)) { + if (cliOnly) { + mergedOptions[key] = currentValue.slice(); + continue; + } let exclude; + if (isPath) { + parentValue = parentValue.map(value => resolvePath(value, parentBaseDir, useNodeResolution)); + } if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) { mergedOptions[key] = [ ...currentValue, @@ -222,6 +237,17 @@ function merge(config, currentOptions, parentOptions) { exports.merge = merge; +/** Resolves a single possibly relative path. Keeps absolute paths, otherwise prepends baseDir. */ +function resolvePath(p, baseDir, useNodeResolution = false) { + if (path.isAbsolute(p)) return p; + if (useNodeResolution && !p.startsWith(".")) { + return require.resolve(p, { paths: [ baseDir ] }); + } + return path.join(baseDir, p); +} + +exports.resolvePath = resolvePath; + /** Populates default values on a parsed options result. */ function addDefaults(config, options) { for (const [key, { default: defaultValue }] of Object.entries(config)) { @@ -229,7 +255,6 @@ function addDefaults(config, options) { options[key] = defaultValue; } } - return options; } exports.addDefaults = addDefaults; diff --git a/src/compiler.ts b/src/compiler.ts index 4966331552..3d7228765c 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -193,7 +193,8 @@ import { writeF32, writeF64, uniqueMap, - isPowerOf2 + isPowerOf2, + v128_zero } from "./util"; /** Compiler options. */ @@ -3058,10 +3059,11 @@ export class Compiler extends DiagnosticEmitter { if (initExpr) { let precomp = module.runExpression(initExpr, ExpressionRunnerFlags.PreserveSideeffects); if (precomp) { - initExpr = precomp; - let local = new Local(name, -1, type, flow.parentFunction); + initExpr = precomp; // always use precomputed initExpr + let local: Local | null = null; switch (getExpressionType(initExpr)) { case NativeType.I32: { + local = new Local(name, -1, type, flow.parentFunction); local.setConstantIntegerValue( i64_new( getConstValueI32(initExpr), @@ -3072,6 +3074,7 @@ export class Compiler extends DiagnosticEmitter { break; } case NativeType.I64: { + local = new Local(name, -1, type, flow.parentFunction); local.setConstantIntegerValue( i64_new( getConstValueI64Low(initExpr), @@ -3082,33 +3085,33 @@ export class Compiler extends DiagnosticEmitter { break; } case NativeType.F32: { + local = new Local(name, -1, type, flow.parentFunction); local.setConstantFloatValue(getConstValueF32(initExpr), type); break; } case NativeType.F64: { + local = new Local(name, -1, type, flow.parentFunction); local.setConstantFloatValue(getConstValueF64(initExpr), type); break; } - default: { - assert(false); - return module.unreachable(); - } } - // Create a virtual local that doesn't actually exist in WebAssembly - let scopedLocals = flow.scopedLocals; - if (!scopedLocals) flow.scopedLocals = scopedLocals = new Map(); - else if (scopedLocals.has(name)) { - let existing = assert(scopedLocals.get(name)); - this.errorRelated( - DiagnosticCode.Duplicate_identifier_0, - declaration.name.range, - existing.declaration.name.range, - name - ); - return this.module.unreachable(); + if (local) { + // Add as a virtual local that doesn't actually exist in WebAssembly + let scopedLocals = flow.scopedLocals; + if (!scopedLocals) flow.scopedLocals = scopedLocals = new Map(); + else if (scopedLocals.has(name)) { + let existing = assert(scopedLocals.get(name)); + this.errorRelated( + DiagnosticCode.Duplicate_identifier_0, + declaration.name.range, + existing.declaration.name.range, + name + ); + return this.module.unreachable(); + } + scopedLocals.set(name, local); + isStatic = true; } - scopedLocals.set(name, local); - isStatic = true; } } else { this.error( @@ -11035,8 +11038,6 @@ export class Compiler extends DiagnosticEmitter { // helpers -const v128_zero = new Uint8Array(16); - function mangleImportName( element: Element, declaration: DeclarationStatement diff --git a/src/util/index.ts b/src/util/index.ts index da61a0069f..30d097c078 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -8,3 +8,4 @@ export * from "./collections"; export * from "./math"; export * from "./path"; export * from "./text"; +export * from "./vector"; diff --git a/src/util/vector.ts b/src/util/vector.ts new file mode 100644 index 0000000000..eee9d7ad78 --- /dev/null +++ b/src/util/vector.ts @@ -0,0 +1,7 @@ +/** + * @fileoverview Various vector utility. + * @license Apache-2.0 + */ + +/** v128 zero constant. */ +export const v128_zero = new Uint8Array(16); diff --git a/tests/asconfig/.gitignore b/tests/asconfig/.gitignore new file mode 100644 index 0000000000..89d6c1c6b1 --- /dev/null +++ b/tests/asconfig/.gitignore @@ -0,0 +1 @@ +!entry-points/**/*/node_modules/ \ No newline at end of file diff --git a/tests/asconfig/complicated/assembly/index.ts b/tests/asconfig/complicated/assembly/index.ts index ae67e9d6f5..ccc7edfb32 100644 --- a/tests/asconfig/complicated/assembly/index.ts +++ b/tests/asconfig/complicated/assembly/index.ts @@ -1,6 +1,6 @@ -assert(ASC_OPTIMIZE_LEVEL == 3); -assert(ASC_SHRINK_LEVEL == 1); -assert(ASC_FEATURE_SIMD); +assert(ASC_OPTIMIZE_LEVEL == 3, "expected optimize level == 3"); +assert(ASC_SHRINK_LEVEL == 1, "expected shrink level == 1"); +assert(ASC_FEATURE_SIMD, "expected SIMD enabled"); let size = memory.size(); trace("size", 1, size); -assert(size == 30); +assert(size == 30, "expected 30 got " + size.toString()); diff --git a/tests/asconfig/entry-points/node-resolution/asconfig.json b/tests/asconfig/entry-points/node-resolution/asconfig.json new file mode 100644 index 0000000000..93c3e4424d --- /dev/null +++ b/tests/asconfig/entry-points/node-resolution/asconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "entry-points/asconfig.json" +} diff --git a/tests/asconfig/entry-points/node-resolution/assembly/index.ts b/tests/asconfig/entry-points/node-resolution/assembly/index.ts new file mode 100644 index 0000000000..48b64eeb05 --- /dev/null +++ b/tests/asconfig/entry-points/node-resolution/assembly/index.ts @@ -0,0 +1 @@ +assert(answerToLife == 42); diff --git a/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/asconfig.json b/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/asconfig.json new file mode 100644 index 0000000000..f83851ac73 --- /dev/null +++ b/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/asconfig.json @@ -0,0 +1,3 @@ +{ + "entries": ["assembly/globals.ts"] +} diff --git a/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/assembly/globals.ts b/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/assembly/globals.ts new file mode 100644 index 0000000000..ef2ca6b2f9 --- /dev/null +++ b/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/assembly/globals.ts @@ -0,0 +1,3 @@ +// @ts-ignore: decorator +@global const answerToLife = 42; +assert(answerToLife); diff --git a/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/assembly/index.ts b/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/assembly/index.ts new file mode 100644 index 0000000000..8d57706af7 --- /dev/null +++ b/tests/asconfig/entry-points/node-resolution/node_modules/entry-points/assembly/index.ts @@ -0,0 +1,2 @@ + +assert(answerToLife == 42); diff --git a/tests/asconfig/entry-points/node-resolution/package.json b/tests/asconfig/entry-points/node-resolution/package.json new file mode 100644 index 0000000000..7603e9ad4d --- /dev/null +++ b/tests/asconfig/entry-points/node-resolution/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "scripts": { + "test": "node ../../index.js" + } +} diff --git a/tests/asconfig/entry-points/package.json b/tests/asconfig/entry-points/package.json index e835996ed6..e92aaac524 100644 --- a/tests/asconfig/entry-points/package.json +++ b/tests/asconfig/entry-points/package.json @@ -1,6 +1,8 @@ { "private": true, "scripts": { - "test": "node ../index.js && cd nested && npm run test" + "test": "node ../index.js && npm run test:nested && npm run test:node", + "test:nested": "cd nested && npm run test", + "test:node": "cd node-resolution && npm run test" } } diff --git a/tests/asconfig/extends/asconfig.json b/tests/asconfig/extends/asconfig.json index d052236111..e9ef74654a 100644 --- a/tests/asconfig/extends/asconfig.json +++ b/tests/asconfig/extends/asconfig.json @@ -5,7 +5,9 @@ } }, "options": { - "enable": ["simd"] + "enable": ["simd"], + "runtime": "half", + "noEmit": false }, "extends": "./extends.json" } diff --git a/tests/asconfig/extends/expected.json b/tests/asconfig/extends/expected.json new file mode 100644 index 0000000000..b5498f8b40 --- /dev/null +++ b/tests/asconfig/extends/expected.json @@ -0,0 +1,8 @@ +{ + "options": { + "runtime": "half", + "noEmit": false, + "noAssert": true, + "enable": ["simd"] + } +} \ No newline at end of file diff --git a/tests/asconfig/extends/extends.json b/tests/asconfig/extends/extends.json index 44d1411849..21e9a62fa3 100644 --- a/tests/asconfig/extends/extends.json +++ b/tests/asconfig/extends/extends.json @@ -6,6 +6,8 @@ } }, "options": { - "disable": ["simd"] + "disable": ["simd"], + "noEmit": true, + "noAssert": true } } diff --git a/tests/asconfig/extends/package.json b/tests/asconfig/extends/package.json index f98424bae9..fdbb9ed043 100644 --- a/tests/asconfig/extends/package.json +++ b/tests/asconfig/extends/package.json @@ -1,6 +1,6 @@ { "private": true, "scripts": { - "test": "node ../index.js" + "test": "node ../index.js --showConfig && node ../index.js" } } diff --git a/tests/asconfig/index.js b/tests/asconfig/index.js index 55ee7a6d87..6b491ec5ee 100644 --- a/tests/asconfig/index.js +++ b/tests/asconfig/index.js @@ -1,7 +1,11 @@ const asc = require("../../cli/asc"); const loader = require("../../lib/loader"); const args = process.argv.slice(2); +const path = require('path'); +const fs = require("fs"); +/** @type {string} */ +let stderr; /** @type {Uint8Array} */ let binary; asc.main(["assembly/index.ts", "--outFile", "output.wasm", "--explicitStart", ...args], { @@ -11,13 +15,52 @@ asc.main(["assembly/index.ts", "--outFile", "output.wasm", "--explicitStart", .. } else if (name !== "output.wasm.map") { throw Error("Unexpected output file: " + name); } + }, + stderr: { + write(s) { + stderr = s; + } } + }, (err) => { if (err) { console.error(err); + console.error(stderr); process.exit(1); } + const jsonPath = path.join(process.cwd(), "expected.json"); + if (fs.existsSync(jsonPath) && stderr) { + const actualRes = JSON.parse(stderr); + const actual = actualRes.options; + const expected = require(jsonPath).options; + let errored = false; + for (let name of Object.getOwnPropertyNames(expected)) { + if (actual[name] !== expected[name]) { + // If object check just first level + if (typeof actual[name] === 'object' && typeof expected[name] === 'object') { + let error = false; + for (let field of Object.getOwnPropertyNames(actual[name])) { + if (actual[name][field] !== expected[name][field]) { + error = true; + break; + } + } + if (!error) { + continue; + } + } + console.error(name + ": " + actual[name] + " expected " + expected[name]); + errored = true; + } + } + if (errored) { + process.exit(1); + } + process.exit(0); + } + + if (!binary) { console.error("No binary was generated for the asconfig test in " + process.cwd()); process.exit(1); @@ -29,6 +72,7 @@ asc.main(["assembly/index.ts", "--outFile", "output.wasm", "--explicitStart", .. theModule.exports._start(); } catch (err) { console.error("The wasm module _start() function failed in " + process.cwd()); + console.error(err); process.exit(1); } return 0; diff --git a/tests/asconfig/package.json b/tests/asconfig/package.json index 58789240bc..1c6818ce3b 100644 --- a/tests/asconfig/package.json +++ b/tests/asconfig/package.json @@ -1,12 +1,13 @@ { "private": true, "scripts": { - "test": "npm run test:use-consts && npm run test:target && npm run test:entry-points && npm run test:complicated", + "test": "npm run test:use-consts && npm run test:target && npm run test:entry-points && npm run test:complicated && npm run test:extends", "test:use-consts": "cd use-consts && npm run test", "test:entry-points": "cd entry-points && npm run test", "test:respect-inheritence": "cd respect-inheritence && npm run test", "test:target": "cd target && npm run test", "test:cyclical": "cd cyclical && npm run test", - "test:complicated": "cd complicated && npm run test" + "test:complicated": "cd complicated && npm run test", + "test:extends": "cd extends && npm run test" } } diff --git a/tests/asconfig/target/asconfig.json b/tests/asconfig/target/asconfig.json index aaadbf8119..57ba76da13 100644 --- a/tests/asconfig/target/asconfig.json +++ b/tests/asconfig/target/asconfig.json @@ -1,13 +1,17 @@ { "targets": { "release": { + "noAssert": true, + "runtime": "none" + }, + "debug": { "optimizeLevel": 3, "shrinkLevel": 1, - "enable": ["simd"] - }, - "dev": { + "enable": ["simd"], "debug": true } }, - "options": {} + "options": { + "runtime": "stub" + } } diff --git a/tests/asconfig/target/assembly/index.ts b/tests/asconfig/target/assembly/index.ts index 5c03e37224..a3353e3c36 100644 --- a/tests/asconfig/target/assembly/index.ts +++ b/tests/asconfig/target/assembly/index.ts @@ -1,3 +1,3 @@ -assert(ASC_OPTIMIZE_LEVEL == 3); -assert(ASC_SHRINK_LEVEL == 1); -assert(ASC_FEATURE_SIMD); +assert(ASC_OPTIMIZE_LEVEL == 3, "expected optimize level == 3"); +assert(ASC_SHRINK_LEVEL == 1, "expected shrink level == 1"); +assert(ASC_FEATURE_SIMD, "expected SIMD enabled"); diff --git a/tests/asconfig/target/expected.json b/tests/asconfig/target/expected.json new file mode 100644 index 0000000000..d4e935883a --- /dev/null +++ b/tests/asconfig/target/expected.json @@ -0,0 +1,6 @@ +{ + "options": { + "runtime": "none", + "noAssert": true + } +} \ No newline at end of file diff --git a/tests/asconfig/target/package.json b/tests/asconfig/target/package.json index f98424bae9..0d5843d61f 100644 --- a/tests/asconfig/target/package.json +++ b/tests/asconfig/target/package.json @@ -1,6 +1,6 @@ { "private": true, "scripts": { - "test": "node ../index.js" + "test": "node ../index.js --target debug && node ../index.js --showConfig" } }