bitburner-src/src/Script/RamCalculations.ts

385 lines
14 KiB
TypeScript
Raw Normal View History

2019-05-18 00:51:28 +02:00
/**
* Implements RAM Calculation functionality.
*
* Uses the acorn.js library to parse a script's code into an AST and
* recursively walk through that AST, calculating RAM usage along
* the way
*/
import * as walk from "acorn-walk";
2021-10-29 05:04:26 +02:00
import acorn, { parse } from "acorn";
import { RamCalculationErrorCode } from "./RamCalculationErrorCodes";
import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
2022-03-06 05:05:55 +01:00
import { Script } from "./Script";
2022-07-16 05:34:27 +02:00
import { Node } from "../NetscriptJSEvaluator";
FILES: Path rework & typesafety (#479) * Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 16:26:57 +02:00
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
import { root } from "../Paths/Directory";
2022-01-05 22:41:48 +01:00
export interface RamUsageEntry {
type: "ns" | "dom" | "fn" | "misc";
2022-01-05 22:41:48 +01:00
name: string;
cost: number;
}
export interface RamCalculation {
cost: number;
entries?: RamUsageEntry[];
}
// These special strings are used to reference the presence of a given logical
// construct within a user script.
const specialReferenceIF = "__SPECIAL_referenceIf";
const specialReferenceFOR = "__SPECIAL_referenceFor";
const specialReferenceWHILE = "__SPECIAL_referenceWhile";
// The global scope of a script is registered under this key during parsing.
const memCheckGlobalKey = ".__GLOBAL__";
2019-05-18 00:51:28 +02:00
/**
* Parses code into an AST and walks through it recursively to calculate
* RAM usage. Also accounts for imported modules.
FILES: Path rework & typesafety (#479) * Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 16:26:57 +02:00
* @param otherScripts - All other scripts on the server. Used to account for imported scripts
* @param code - The code being parsed */
function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code: string, ns1?: boolean): RamCalculation {
2021-09-05 01:09:30 +02:00
try {
/**
* Maps dependent identifiers to their dependencies.
*
* The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__.
* It depends on all the functions declared in the module, all the global scopes
* of its imports, and any identifiers referenced in this global scope. Each
* function depends on all the identifiers referenced internally.
* We walk the dependency graph to calculate RAM usage, given that some identifiers
* reference Netscript functions which have a RAM cost.
*/
2021-09-25 00:13:20 +02:00
let dependencyMap: { [key: string]: string[] } = {};
2021-09-05 01:09:30 +02:00
// Scripts we've parsed.
const completedParses = new Set();
// Scripts we've discovered that need to be parsed.
2021-09-25 00:13:20 +02:00
const parseQueue: string[] = [];
2021-09-05 01:09:30 +02:00
// Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap.
2021-09-25 00:13:20 +02:00
function parseCode(code: string, moduleName: string): void {
2021-09-05 01:09:30 +02:00
const result = parseOnlyCalculateDeps(code, moduleName);
completedParses.add(moduleName);
// Add any additional modules to the parse queue;
for (let i = 0; i < result.additionalModules.length; ++i) {
if (!completedParses.has(result.additionalModules[i])) {
parseQueue.push(result.additionalModules[i]);
}
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
// Splice all the references in
dependencyMap = Object.assign(dependencyMap, result.dependencyMap);
}
// Parse the initial module, which is the "main" script that is being run
const initialModule = "__SPECIAL_INITIAL_MODULE__";
parseCode(code, initialModule);
// Process additional modules, which occurs if the "main" script has any imports
while (parseQueue.length > 0) {
const nextModule = parseQueue.shift();
2021-09-25 00:13:20 +02:00
if (nextModule === undefined) throw new Error("nextModule should not be undefined");
2022-08-17 23:11:59 +02:00
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue;
FILES: Path rework & typesafety (#479) * Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 16:26:57 +02:00
// Using root as the path base right now. Difficult to implement
const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js");
if (!filename) return { cost: RamCalculationErrorCode.ImportError }; // Invalid import path
const script = otherScripts.get(filename);
FILES: Path rework & typesafety (#479) * Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 16:26:57 +02:00
if (!script) return { cost: RamCalculationErrorCode.ImportError }; // No such file on server
2022-08-17 23:11:59 +02:00
parseCode(script.code, nextModule);
2021-09-05 01:09:30 +02:00
}
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
// are those that start with __SPECIAL_INITIAL_MODULE__.
let ram = RamCostConstants.Base;
const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }];
2021-09-09 05:47:34 +02:00
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
2021-09-05 01:09:30 +02:00
const resolvedRefs = new Set();
2022-07-20 07:13:06 +02:00
const loadedFns: Record<string, boolean> = {};
2021-09-05 01:09:30 +02:00
while (unresolvedRefs.length > 0) {
const ref = unresolvedRefs.shift();
2021-09-25 00:13:20 +02:00
if (ref === undefined) throw new Error("ref should not be undefined");
2021-09-05 01:09:30 +02:00
// Check if this is one of the special keys, and add the appropriate ram cost if so.
if (ref === "hacknet" && !resolvedRefs.has("hacknet")) {
ram += RamCostConstants.HacknetNodes;
detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.HacknetNodes });
2021-09-05 01:09:30 +02:00
}
if (ref === "document" && !resolvedRefs.has("document")) {
ram += RamCostConstants.Dom;
detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.Dom });
2021-09-05 01:09:30 +02:00
}
if (ref === "window" && !resolvedRefs.has("window")) {
ram += RamCostConstants.Dom;
detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.Dom });
2021-12-04 05:06:04 +01:00
}
2021-09-05 01:09:30 +02:00
resolvedRefs.add(ref);
if (ref.endsWith(".*")) {
// A prefix reference. We need to find all matching identifiers.
const prefix = ref.slice(0, ref.length - 2);
2021-09-25 07:26:03 +02:00
for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) {
for (const dep of dependencyMap[ident] || []) {
2021-09-05 01:09:30 +02:00
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
}
}
} else {
// An exact reference. Add all dependencies of this ref.
2021-09-25 07:26:03 +02:00
for (const dep of dependencyMap[ref] || []) {
2021-09-05 01:09:30 +02:00
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
}
}
2021-09-05 01:09:30 +02:00
// Check if this identifier is a function in the workerScript environment.
// If it is, then we need to get its RAM cost.
try {
2022-08-29 08:41:17 +02:00
function applyFuncRam(cost: number | (() => number)): number {
if (typeof cost === "number") {
return cost;
2022-01-05 01:09:34 +01:00
} else if (typeof cost === "function") {
2022-08-29 08:41:17 +02:00
return cost();
2021-09-05 01:09:30 +02:00
} else {
return 0;
}
}
// Only count each function once
2022-07-20 07:13:06 +02:00
if (loadedFns[ref]) {
2021-09-05 01:09:30 +02:00
continue;
}
2022-07-20 07:13:06 +02:00
loadedFns[ref] = true;
// This accounts for namespaces (Bladeburner, CodingContract, etc.)
const findFunc = (
prefix: string,
obj: object,
ref: string,
2022-08-29 08:41:17 +02:00
): { func: () => number | number; refDetail: string } | undefined => {
2022-07-20 07:13:06 +02:00
if (!obj) return;
const elem = Object.entries(obj).find(([key]) => key === ref);
if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) {
return { func: elem[1], refDetail: `${prefix}${ref}` };
}
for (const [key, value] of Object.entries(obj)) {
const found = findFunc(`${key}.`, value, ref);
if (found) return found;
}
return undefined;
};
2022-07-20 07:13:06 +02:00
const details = findFunc("", RamCosts, ref);
const fnRam = applyFuncRam(details?.func ?? 0);
2022-01-05 22:41:48 +01:00
ram += fnRam;
2022-07-20 07:13:06 +02:00
detailedCosts.push({ type: "fn", name: details?.refDetail ?? "", cost: fnRam });
2021-09-05 01:09:30 +02:00
} catch (error) {
2022-10-09 08:32:13 +02:00
console.error(error);
2021-09-05 01:09:30 +02:00
continue;
}
}
if (ram > RamCostConstants.Max) {
ram = RamCostConstants.Max;
detailedCosts.push({ type: "misc", name: "Max Ram Cap", cost: RamCostConstants.Max });
}
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
2021-09-05 01:09:30 +02:00
} catch (error) {
// console.info("parse or eval error: ", error);
// This is not unexpected. The user may be editing a script, and it may be in
// a transitory invalid state.
2022-01-05 22:41:48 +01:00
return { cost: RamCalculationErrorCode.SyntaxError };
2021-09-05 01:09:30 +02:00
}
}
2021-10-29 05:04:26 +02:00
export function checkInfiniteLoop(code: string): number {
const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" });
function nodeHasTrueTest(node: acorn.Node): boolean {
return node.type === "Literal" && (node as any).raw === "true";
}
function hasAwait(ast: acorn.Node): boolean {
let hasAwait = false;
walk.recursive(
ast,
{},
{
AwaitExpression: () => {
hasAwait = true;
},
},
);
return hasAwait;
}
let missingAwaitLine = -1;
walk.recursive(
ast,
{},
{
2022-07-16 05:34:27 +02:00
WhileStatement: (node: Node, st: unknown, walkDeeper: walk.WalkerCallback<any>) => {
if (nodeHasTrueTest(node.test) && !hasAwait(node)) {
2021-10-29 05:04:26 +02:00
missingAwaitLine = (code.slice(0, node.start).match(/\n/g) || []).length + 1;
} else {
2022-07-16 05:34:27 +02:00
node.body && walkDeeper(node.body, st);
2021-10-29 05:04:26 +02:00
}
},
},
);
return missingAwaitLine;
}
2022-07-16 05:34:27 +02:00
interface ParseDepsResult {
dependencyMap: {
[key: string]: Set<string> | undefined;
};
additionalModules: string[];
}
2019-05-18 00:51:28 +02:00
/**
* Helper function that parses a single script. It returns a map of all dependencies,
* which are items in the code's AST that potentially need to be evaluated
* for RAM usage calculations. It also returns an array of additional modules
* that need to be parsed (i.e. are 'import'ed scripts).
*/
2022-07-16 05:34:27 +02:00
function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsResult {
2021-09-05 01:09:30 +02:00
const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" });
// Everything from the global scope goes in ".". Everything else goes in ".function", where only
// the outermost layer of functions counts.
const globalKey = currentModule + memCheckGlobalKey;
2021-09-25 00:13:20 +02:00
const dependencyMap: { [key: string]: Set<string> | undefined } = {};
dependencyMap[globalKey] = new Set<string>();
2021-09-05 01:09:30 +02:00
// If we reference this internal name, we're really referencing that external name.
// Filled when we import names from other modules.
2021-09-25 07:26:03 +02:00
const internalToExternal: { [key: string]: string | undefined } = {};
2021-09-05 01:09:30 +02:00
2021-09-25 07:26:03 +02:00
const additionalModules: string[] = [];
2021-09-05 01:09:30 +02:00
// References get added pessimistically. They are added for thisModule.name, name, and for
// any aliases.
2021-09-25 00:13:20 +02:00
function addRef(key: string, name: string): void {
2021-09-05 01:09:30 +02:00
const s = dependencyMap[key] || (dependencyMap[key] = new Set());
2021-09-25 00:13:20 +02:00
const external = internalToExternal[name];
if (external !== undefined) {
s.add(external);
}
2021-09-05 01:09:30 +02:00
s.add(currentModule + "." + name);
s.add(name); // For builtins like hack.
}
//A list of identifiers that resolve to "native Javascript code"
2021-09-09 05:47:34 +02:00
const objectPrototypeProperties = Object.getOwnPropertyNames(Object.prototype);
2021-09-05 01:09:30 +02:00
2022-07-16 05:34:27 +02:00
interface State {
key: string;
}
2021-09-05 01:09:30 +02:00
// If we discover a dependency identifier, state.key is the dependent identifier.
// walkDeeper is for doing recursive walks of expressions in composites that we handle.
2022-07-16 05:34:27 +02:00
function commonVisitors(): walk.RecursiveVisitors<State> {
2021-09-05 01:09:30 +02:00
return {
2022-07-16 05:34:27 +02:00
Identifier: (node: Node, st: State) => {
2021-09-05 01:09:30 +02:00
if (objectPrototypeProperties.includes(node.name)) {
return;
}
2021-09-05 01:09:30 +02:00
addRef(st.key, node.name);
},
2022-07-16 05:34:27 +02:00
WhileStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
2021-09-05 01:09:30 +02:00
addRef(st.key, specialReferenceWHILE);
node.test && walkDeeper(node.test, st);
node.body && walkDeeper(node.body, st);
},
2022-07-16 05:34:27 +02:00
DoWhileStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
2021-09-05 01:09:30 +02:00
addRef(st.key, specialReferenceWHILE);
node.test && walkDeeper(node.test, st);
node.body && walkDeeper(node.body, st);
},
2022-07-16 05:34:27 +02:00
ForStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
2021-09-05 01:09:30 +02:00
addRef(st.key, specialReferenceFOR);
node.init && walkDeeper(node.init, st);
node.test && walkDeeper(node.test, st);
node.update && walkDeeper(node.update, st);
node.body && walkDeeper(node.body, st);
},
2022-07-16 05:34:27 +02:00
IfStatement: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
2021-09-05 01:09:30 +02:00
addRef(st.key, specialReferenceIF);
node.test && walkDeeper(node.test, st);
node.consequent && walkDeeper(node.consequent, st);
node.alternate && walkDeeper(node.alternate, st);
},
2022-07-16 05:34:27 +02:00
MemberExpression: (node: Node, st: State, walkDeeper: walk.WalkerCallback<State>) => {
2021-09-05 01:09:30 +02:00
node.object && walkDeeper(node.object, st);
node.property && walkDeeper(node.property, st);
},
};
}
2022-07-16 05:34:27 +02:00
walk.recursive<State>(
2021-09-05 01:09:30 +02:00
ast,
{ key: globalKey },
Object.assign(
{
2022-07-16 05:34:27 +02:00
ImportDeclaration: (node: Node, st: State) => {
2021-09-05 01:09:30 +02:00
const importModuleName = node.source.value;
additionalModules.push(importModuleName);
// This module's global scope refers to that module's global scope, no matter how we
// import it.
2021-09-25 00:13:20 +02:00
const set = dependencyMap[st.key];
if (set === undefined) throw new Error("set should not be undefined");
set.add(importModuleName + memCheckGlobalKey);
2021-09-05 01:09:30 +02:00
for (let i = 0; i < node.specifiers.length; ++i) {
const spec = node.specifiers[i];
if (spec.imported !== undefined && spec.local !== undefined) {
// We depend on specific things.
2021-09-09 05:47:34 +02:00
internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name;
2021-09-05 01:09:30 +02:00
} else {
// We depend on everything.
2021-09-25 00:13:20 +02:00
const set = dependencyMap[st.key];
if (set === undefined) throw new Error("set should not be undefined");
set.add(importModuleName + ".*");
}
2021-09-05 01:09:30 +02:00
}
},
2022-07-16 05:34:27 +02:00
FunctionDeclaration: (node: Node) => {
// node.id will be null when using 'export default'. Add a module name indicating the default export.
const key = currentModule + "." + (node.id === null ? "__SPECIAL_DEFAULT_EXPORT__" : node.id.name);
2021-09-05 01:09:30 +02:00
walk.recursive(node, { key: key }, commonVisitors());
},
2021-09-05 01:09:30 +02:00
},
commonVisitors(),
),
);
2021-09-05 01:09:30 +02:00
return { dependencyMap: dependencyMap, additionalModules: additionalModules };
}
2019-05-18 00:51:28 +02:00
/**
* Calculate's a scripts RAM Usage
* @param {string} codeCopy - The script's code
* @param {Script[]} otherScripts - All other scripts on the server.
* Used to account for imported scripts
*/
export function calculateRamUsage(
codeCopy: string,
FILES: Path rework & typesafety (#479) * Added new types for various file paths, all in the Paths folder. * TypeSafety and other helper functions related to these types * Added basic globbing support with * and ?. Currently only implemented for Script/Text, on nano and download terminal commands * Enforcing the new types throughout the codebase, plus whatever rewrites happened along the way * Server.textFiles is now a map * TextFile no longer uses a fn property, now it is filename * Added a shared ContentFile interface for shared functionality between TextFile and Script. * related to ContentFile change above, the player is now allowed to move a text file to a script file and vice versa. * File paths no longer conditionally start with slashes, and all directory names other than root have ending slashes. The player is still able to provide paths starting with / but this now indicates that the player is specifying an absolute path instead of one relative to root. * Singularized the MessageFilename and LiteratureName enums * Because they now only accept correct types, server.writeToXFile functions now always succeed (the only reasons they could fail before were invalid filepath). * Fix several issues with tab completion, which included pretty much a complete rewrite * Changed the autocomplete display options so there's less chance it clips outside the display area. * Turned CompletedProgramName into an enum. * Got rid of programsMetadata, and programs and DarkWebItems are now initialized immediately instead of relying on initializers called from the engine. * For any executable (program, cct, or script file) pathing can be used directly to execute without using the run command (previously the command had to start with ./ and it wasn't actually using pathing).
2023-04-24 16:26:57 +02:00
otherScripts: Map<ScriptFilePath, Script>,
ns1?: boolean,
): RamCalculation {
2021-09-05 01:09:30 +02:00
try {
return parseOnlyRamCalculate(otherScripts, codeCopy, ns1);
2021-09-05 01:09:30 +02:00
} catch (e) {
console.error(`Failed to parse script for RAM calculations:`);
console.error(e);
2022-01-05 22:41:48 +01:00
return { cost: RamCalculationErrorCode.SyntaxError };
2021-09-05 01:09:30 +02:00
}
}