| /** |
| * @license |
| * Copyright 2010 The Emscripten Authors |
| * SPDX-License-Identifier: MIT |
| */ |
| |
| // General JS utilities - things that might be useful in any JS project. |
| // Nothing specific to Emscripten appears here. |
| |
| import * as url from 'node:url'; |
| import * as path from 'node:path'; |
| import * as fs from 'node:fs'; |
| import * as vm from 'node:vm'; |
| import assert from 'node:assert'; |
| |
| export function safeQuote(x) { |
| return x.replace(/"/g, '\\"').replace(/'/g, "\\'"); |
| } |
| |
| export function dump(item) { |
| let funcData; |
| try { |
| if (typeof item == 'object' && item != null && item.funcData) { |
| funcData = item.funcData; |
| item.funcData = null; |
| } |
| return '// ' + JSON.stringify(item, null, ' ').replace(/\n/g, '\n// '); |
| } catch { |
| const ret = []; |
| for (const [i, j] of Object.entries(item)) { |
| if (typeof j == 'string' || typeof j == 'number') { |
| ret.push(`${i}: ${j}`); |
| } else { |
| ret.push(`${i}: [?]`); |
| } |
| } |
| return ret.join(',\n'); |
| } finally { |
| if (funcData) item.funcData = funcData; |
| } |
| } |
| |
| let warnings = false; |
| |
| export function warningOccured() { |
| return warnings; |
| } |
| |
| let currentFile = []; |
| |
| export function pushCurrentFile(f) { |
| currentFile.push(f); |
| } |
| |
| export function popCurrentFile() { |
| currentFile.pop(); |
| } |
| |
| function errorPrefix(lineNo) { |
| if (!currentFile.length) return ''; |
| const filename = currentFile[currentFile.length - 1]; |
| if (lineNo) { |
| return `${filename}:${lineNo}: `; |
| } else { |
| return `${filename}: `; |
| } |
| } |
| |
| export function warn(msg, lineNo) { |
| warnings = true; |
| printErr(`warning: ${errorPrefix(lineNo)}${msg}`); |
| } |
| |
| const seenWarnings = new Set(); |
| |
| export function warnOnce(msg) { |
| if (!seenWarnings.has(msg)) { |
| seenWarnings.add(msg); |
| warn(msg); |
| } |
| } |
| |
| let abortExecution = false; |
| |
| export function errorOccured() { |
| return abortExecution; |
| } |
| |
| export function error(msg, lineNo) { |
| abortExecution = true; |
| process.exitCode = 1; |
| printErr(`error: ${errorPrefix(lineNo)}${msg}`); |
| } |
| |
| function range(size) { |
| return Array.from(Array(size).keys()); |
| } |
| |
| export function mergeInto(obj, other, options = null) { |
| if (options) { |
| // check for unintended symbol redefinition |
| if (options.noOverride) { |
| for (const key of Object.keys(other)) { |
| if (obj.hasOwnProperty(key)) { |
| error( |
| `Symbol re-definition in JavaScript library: ${key}. Do not use noOverride if this is intended`, |
| ); |
| return; |
| } |
| } |
| } |
| |
| // check if sig is missing for added functions |
| if (options.checkSig) { |
| for (const [key, value] of Object.entries(other)) { |
| if (typeof value === 'function' && !other.hasOwnProperty(key + '__sig')) { |
| error(`__sig is missing for function: ${key}. Do not use checkSig if this is intended`); |
| return; |
| } |
| } |
| } |
| } |
| |
| if (!options || !options.allowMissing) { |
| for (const ident of Object.keys(other)) { |
| if (isDecorator(ident)) { |
| const index = ident.lastIndexOf('__'); |
| const basename = ident.slice(0, index); |
| if (!(basename in obj) && !(basename in other)) { |
| error(`Missing library element '${basename}' for library config '${ident}'`); |
| } |
| } |
| } |
| } |
| |
| for (const key of Object.keys(other)) { |
| if (isDecorator(key)) { |
| if (key.endsWith('__sig')) { |
| if (obj.hasOwnProperty(key)) { |
| const oldsig = obj[key]; |
| const newsig = other[key]; |
| if (oldsig == newsig) { |
| warn(`signature redefinition for: ${key}`); |
| } else { |
| error(`signature redefinition for: ${key}. (old=${oldsig} vs new=${newsig})`); |
| } |
| } |
| } |
| |
| const index = key.lastIndexOf('__'); |
| const decoratorName = key.slice(index); |
| const type = typeof other[key]; |
| |
| // Specific type checking for `__deps` which is expected to be an array |
| // (not just any old `object`) |
| if (decoratorName === '__deps') { |
| const deps = other[key]; |
| if (!Array.isArray(deps)) { |
| error( |
| `JS library directive ${key}=${deps} is of type '${type}', but it should be an array`, |
| ); |
| } |
| for (let dep of deps) { |
| if (dep && typeof dep !== 'string' && typeof dep !== 'function') { |
| error( |
| `__deps entries must be of type 'string' or 'function' not '${typeof dep}': ${key}`, |
| ); |
| } |
| } |
| } else { |
| // General type checking for all other decorators |
| const decoratorTypes = { |
| __sig: 'string', |
| __proxy: 'string', |
| __asm: 'boolean', |
| __postset: ['string', 'function'], |
| __docs: 'string', |
| __nothrow: 'boolean', |
| __noleakcheck: 'boolean', |
| __internal: 'boolean', |
| __user: 'boolean', |
| __async: 'boolean', |
| __i53abi: 'boolean', |
| }; |
| const expected = decoratorTypes[decoratorName]; |
| if (type !== expected && !expected.includes(type)) { |
| error(`Decorator (${key}} has wrong type. Expected '${expected}' not '${type}'`); |
| } |
| } |
| } |
| } |
| |
| return Object.assign(obj, other); |
| } |
| |
| // Symbols that start with '$' are not exported to the wasm module. |
| // They are intended to be called exclusively by JS code. |
| export function isJsOnlySymbol(symbol) { |
| return symbol[0] == '$'; |
| } |
| |
| export const decoratorSuffixes = [ |
| '__sig', |
| '__proxy', |
| '__asm', |
| '__deps', |
| '__postset', |
| '__docs', |
| '__nothrow', |
| '__noleakcheck', |
| '__internal', |
| '__user', |
| '__async', |
| '__i53abi', |
| ]; |
| |
| export function isDecorator(ident) { |
| return decoratorSuffixes.some((suffix) => ident.endsWith(suffix)); |
| } |
| |
| export function readFile(filename) { |
| return fs.readFileSync(filename, 'utf8'); |
| } |
| |
| // Use import.meta.dirname here once we drop support for node v18. |
| const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); |
| |
| export const srcDir = __dirname; |
| |
| // Returns an absolute path for a file, resolving it relative to this script |
| // (i.e. relative to the src/ directory). |
| export function localFile(filename) { |
| assert(!path.isAbsolute(filename)); |
| return path.join(srcDir, filename); |
| } |
| |
| // Helper function for JS library files that can be used to read files |
| // relative to the src/ directory. |
| function read(filename) { |
| if (!path.isAbsolute(filename)) { |
| filename = localFile(filename); |
| } |
| return readFile(filename); |
| } |
| |
| export function printErr(...args) { |
| console.error(...args); |
| } |
| |
| export function debugLog(...args) { |
| if (VERBOSE) printErr(...args); |
| } |
| |
| class Profiler { |
| ids = []; |
| lastTime = 0; |
| |
| constructor() { |
| this.start('overall') |
| this.startTime = performance.now(); |
| } |
| |
| log(msg) { |
| const depth = this.ids.length; |
| const indent = ' '.repeat(depth) |
| printErr('[prof] ' + indent + msg); |
| } |
| |
| start(id) { |
| this.log(`-> ${id}`) |
| const now = performance.now(); |
| this.ids.push([id, now]); |
| } |
| |
| stop(id) { |
| const [poppedId, startTime] = this.ids.pop(); |
| assert(id === poppedId); |
| const now = performance.now(); |
| const duration = now - startTime; |
| this.log(`<- ${id} [${duration.toFixed(1)} ms]`) |
| } |
| |
| terminate() { |
| while (this.ids.length) { |
| const lastID = this.ids[this.ids.length - 1][0]; |
| this.stop(lastID); |
| } |
| // const overall = performance.now() - this.startTime |
| // printErr(`overall total: ${overall.toFixed(1)} ms`); |
| } |
| } |
| |
| class NullProfiler { |
| start(_id) {} |
| stop(_id) {} |
| terminate() {} |
| } |
| |
| // Enable JS compiler profiling if EMPROFILE is "2". This mode reports profile |
| // data to stderr. |
| const EMPROFILE = process.env.EMPROFILE == '2'; |
| |
| export const timer = EMPROFILE ? new Profiler() : new NullProfiler(); |
| |
| if (EMPROFILE) { |
| process.on('exit', () => timer.terminate()); |
| } |
| |
| /** |
| * Context in which JS library code is evaluated. This is distinct from the |
| * global scope of the compiler itself which avoids exposing all of the compiler |
| * internals to user JS library code. |
| */ |
| export const compileTimeContext = vm.createContext({ |
| process, |
| console, |
| }); |
| |
| /** |
| * A symbols to the macro context. |
| * This will makes the symbols available to JS library code at build time. |
| */ |
| export function addToCompileTimeContext(object) { |
| Object.assign(compileTimeContext, object); |
| } |
| |
| export function applySettings(obj) { |
| // Make settings available both in the current / global context |
| // and also in the macro execution contexted. |
| Object.assign(globalThis, obj); |
| addToCompileTimeContext(obj); |
| } |
| |
| export function loadSettingsFile(f) { |
| timer.start('loadSettingsFile') |
| const settings = {}; |
| vm.runInNewContext(readFile(f), settings, {filename: f}); |
| applySettings(settings); |
| timer.stop('loadSettingsFile') |
| return settings; |
| } |
| |
| export function loadDefaultSettings() { |
| const rtn = loadSettingsFile(localFile('settings.js')); |
| Object.assign(rtn, loadSettingsFile(localFile('settings_internal.js'))); |
| return rtn; |
| } |
| |
| export function runInMacroContext(code, options) { |
| compileTimeContext['__filename'] = options.filename; |
| compileTimeContext['__dirname'] = path.dirname(options.filename); |
| return vm.runInContext(code, compileTimeContext, options); |
| } |
| |
| addToCompileTimeContext({ |
| assert, |
| decoratorSuffixes, |
| error, |
| isDecorator, |
| isJsOnlySymbol, |
| mergeInto, |
| read, |
| warn, |
| warnOnce, |
| printErr, |
| range, |
| }); |