blob: f3ffa770118ba7e4655e564af55f40cfe99556df [file] [log] [blame]
/**
* @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,
});