HarmonyImportGuard.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Haijie Xie @hai-x
  4. */
  5. "use strict";
  6. const { InlinedUsedName } = require("../optimize/InlineExports");
  7. /** @import { Expression } from "estree" */
  8. /** @import Dependency from "../Dependency" */
  9. /** @import Module from "../Module" */
  10. /** @import ModuleGraph from "../ModuleGraph" */
  11. /** @import JavascriptParser from "../javascript/JavascriptParser" */
  12. /** @import { RuntimeSpec } from "../util/runtime" */
  13. /** @import CommonJsFullRequireDependency from "./CommonJsFullRequireDependency" */
  14. /** @import CommonJsRequireDependency from "./CommonJsRequireDependency" */
  15. /** @import HarmonyEvaluatedImportSpecifierDependency from "./HarmonyEvaluatedImportSpecifierDependency" */
  16. /** @import HarmonyImportSpecifierDependency from "./HarmonyImportSpecifierDependency" */
  17. /** @import ImportDependency from "./ImportDependency" */
  18. // Tuple-encoded boolean formula over guard dependencies. Split into per-shape
  19. // aliases so the recursive references resolve through array/object types.
  20. // `v` = dependency-liveness atom, `p` = `"x" in ns` presence atom.
  21. /** @typedef {["v", Dependency]} GuardAtom */
  22. /** @typedef {["p", HarmonyEvaluatedImportSpecifierDependency]} GuardPresenceAtom */
  23. /** @typedef {["?"]} GuardUnknown */
  24. /** @typedef {["!", GuardFormula]} GuardNot */
  25. /** @typedef {["&&" | "||" | "??", GuardFormula, GuardFormula]} GuardLogical */
  26. /** @typedef {GuardAtom | GuardPresenceAtom | GuardUnknown | GuardNot | GuardLogical} GuardFormula */
  27. /** @typedef {{ formula: GuardFormula, value: boolean }} DependencyGuard branch guard: the dependency is live only when the formula evaluates to `value` */
  28. /** @typedef {HarmonyImportSpecifierDependency | CommonJsRequireDependency | CommonJsFullRequireDependency | ImportDependency} GuardableDependency */
  29. /**
  30. * A guard frame pushed onto `parser.state.guardStack` for the duration of one
  31. * conditional branch body. Carries the `"x" in ns` presence guards and/or the
  32. * dead-branch dependency guard for that branch.
  33. * @typedef {object} GuardFrame
  34. * @property {Expression=} test the conditional test (for the lazily built formula)
  35. * @property {number=} depStart dependency count before the test was walked
  36. * @property {boolean=} condition branch truthiness the dependency guard is live for
  37. * @property {GuardFormula | null=} formula memoized dependency-guard formula (null = no knowable atom)
  38. * @property {GuardFormula | null=} presenceFormula memoized presence formula (null = no presence atom)
  39. */
  40. // Tri-state truthiness: UNKNOWN means not statically known.
  41. const UNKNOWN = 0;
  42. const FALSE = 1;
  43. const TRUE = 2;
  44. /**
  45. * @param {number} t tri-state value
  46. * @returns {number} negated tri-state
  47. */
  48. const flip = (t) => (t === TRUE ? FALSE : t === FALSE ? TRUE : UNKNOWN);
  49. /**
  50. * @param {boolean} b boolean
  51. * @returns {number} tri-state value
  52. */
  53. const fromBool = (b) => (b ? TRUE : FALSE);
  54. /**
  55. * Build a liveness formula from a conditional test AST.
  56. * @param {Expression} test conditional test expression
  57. * @param {Map<number, HarmonyImportSpecifierDependency>} depByRangeStart import-specifier deps in the test, keyed by range start
  58. * @returns {GuardFormula | null} formula, or null when it has no knowable atom
  59. */
  60. const buildLivenessFormula = (test, depByRangeStart) => {
  61. /**
  62. * @param {Expression} node ast node
  63. * @returns {GuardFormula} formula node
  64. */
  65. const build = (node) => {
  66. if (node.type === "UnaryExpression" && node.operator === "!") {
  67. return ["!", build(/** @type {Expression} */ (node.argument))];
  68. }
  69. if (
  70. node.type === "LogicalExpression" &&
  71. (node.operator === "&&" ||
  72. node.operator === "||" ||
  73. node.operator === "??")
  74. ) {
  75. return [
  76. node.operator,
  77. build(/** @type {Expression} */ (node.left)),
  78. build(/** @type {Expression} */ (node.right))
  79. ];
  80. }
  81. const dep =
  82. node.range && depByRangeStart.get(/** @type {number} */ (node.range[0]));
  83. return dep ? ["v", dep] : ["?"];
  84. };
  85. const formula = build(test);
  86. return hasAtom(formula) ? formula : null;
  87. };
  88. /**
  89. * @param {GuardFormula} formula formula
  90. * @returns {boolean} true when the formula contains at least one atom
  91. */
  92. const hasAtom = (formula) => {
  93. switch (formula[0]) {
  94. case "v":
  95. case "p":
  96. return true;
  97. case "!":
  98. return hasAtom(formula[1]);
  99. case "&&":
  100. case "||":
  101. case "??":
  102. return hasAtom(formula[1]) || hasAtom(formula[2]);
  103. default:
  104. return false;
  105. }
  106. };
  107. /**
  108. * @param {GuardFormula} formula formula
  109. * @param {ModuleGraph} moduleGraph module graph
  110. * @param {RuntimeSpec} runtime runtime
  111. * @returns {{ t: number, n: number }} truthy `t` and null `n` tri-states
  112. */
  113. const evalLivenessFormula = (formula, moduleGraph, runtime) => {
  114. switch (formula[0]) {
  115. case "v": {
  116. const dep = /** @type {HarmonyImportSpecifierDependency} */ (formula[1]);
  117. const module = moduleGraph.getModule(dep);
  118. if (!module) return { t: UNKNOWN, n: UNKNOWN };
  119. const used = moduleGraph
  120. .getExportsInfo(module)
  121. .getUsedName(dep.getIds(moduleGraph), runtime);
  122. // Only a bare inlined primitive is knowable; a property suffix is not.
  123. if (!(used instanceof InlinedUsedName) || used.suffix.length !== 0) {
  124. return { t: UNKNOWN, n: UNKNOWN };
  125. }
  126. const value = used.value;
  127. const nullish = value.kind === "null" || value.kind === "undefined";
  128. return {
  129. t: fromBool(!nullish && Boolean(value.value)),
  130. n: fromBool(nullish)
  131. };
  132. }
  133. case "!":
  134. // `!x` always yields a boolean → never nullish.
  135. return {
  136. t: flip(evalLivenessFormula(formula[1], moduleGraph, runtime).t),
  137. n: FALSE
  138. };
  139. case "&&": {
  140. const l = evalLivenessFormula(formula[1], moduleGraph, runtime);
  141. const r = evalLivenessFormula(formula[2], moduleGraph, runtime);
  142. const t =
  143. l.t === FALSE || r.t === FALSE
  144. ? FALSE
  145. : l.t === TRUE && r.t === TRUE
  146. ? TRUE
  147. : UNKNOWN;
  148. return { t, n: UNKNOWN };
  149. }
  150. case "||": {
  151. const l = evalLivenessFormula(formula[1], moduleGraph, runtime);
  152. const r = evalLivenessFormula(formula[2], moduleGraph, runtime);
  153. const t =
  154. l.t === TRUE || r.t === TRUE
  155. ? TRUE
  156. : l.t === FALSE && r.t === FALSE
  157. ? FALSE
  158. : UNKNOWN;
  159. return { t, n: UNKNOWN };
  160. }
  161. case "??": {
  162. const l = evalLivenessFormula(formula[1], moduleGraph, runtime);
  163. const r = evalLivenessFormula(formula[2], moduleGraph, runtime);
  164. // `l ?? r` is l when l non-nullish, else r.
  165. const t =
  166. l.n === FALSE ? l.t : l.n === TRUE ? r.t : l.t === r.t ? l.t : UNKNOWN;
  167. return { t, n: UNKNOWN };
  168. }
  169. default:
  170. return { t: UNKNOWN, n: UNKNOWN };
  171. }
  172. };
  173. /**
  174. * Whether any guard proves its branch dead.
  175. * @param {DependencyGuard[]} guards dependency guards
  176. * @param {ModuleGraph} moduleGraph module graph
  177. * @param {RuntimeSpec} runtime runtime
  178. * @returns {boolean} true when a test is provably the opposite of its branch
  179. */
  180. const isDeadByGuards = (guards, moduleGraph, runtime) => {
  181. for (const guard of guards) {
  182. const t = evalLivenessFormula(guard.formula, moduleGraph, runtime).t;
  183. if (t !== UNKNOWN && t !== (guard.value ? TRUE : FALSE)) return true;
  184. }
  185. return false;
  186. };
  187. /**
  188. * Builds (once) the liveness formula for a frame from the import specifier deps
  189. * created while walking the test.
  190. * @param {JavascriptParser} parser the parser
  191. * @param {GuardFrame} frame guard frame
  192. * @returns {GuardFormula | null} formula, or null when it has no knowable atom
  193. */
  194. const buildFrameLivenessFormula = (parser, frame) => {
  195. const deps = /** @type {Module} */ (parser.state.module).dependencies;
  196. /** @type {Map<number, HarmonyImportSpecifierDependency>} */
  197. const depByRangeStart = new Map();
  198. for (let i = /** @type {number} */ (frame.depStart); i < deps.length; i++) {
  199. const dep = /** @type {HarmonyEvaluatedImportSpecifierDependency} */ (
  200. deps[i]
  201. );
  202. // Exclude `"x" in ns` evaluated deps; those feed the presence formula only.
  203. if (
  204. dep.isHarmonyImportSpecifier === true &&
  205. dep.isHarmonyEvaluatedImportSpecifier !== true &&
  206. dep.range
  207. ) {
  208. depByRangeStart.set(dep.range[0], dep);
  209. }
  210. }
  211. return buildLivenessFormula(
  212. /** @type {Expression} */ (frame.test),
  213. depByRangeStart
  214. );
  215. };
  216. /**
  217. * Build a presence formula from a conditional test AST. Statically-decided
  218. * `||`/`??` operands are folded away at build time, so the result contains only
  219. * `"x" in ns` presence atoms (plus `&&`/`!`).
  220. * @param {JavascriptParser} parser the parser
  221. * @param {Expression} test conditional test expression
  222. * @param {Map<number, HarmonyEvaluatedImportSpecifierDependency>} depByRangeStart in-operator deps in the test, keyed by range start
  223. * @returns {GuardFormula | null} formula, or null when it has no presence atom
  224. */
  225. const buildPresenceFormula = (parser, test, depByRangeStart) => {
  226. /**
  227. * @param {Expression} node ast node
  228. * @returns {GuardFormula} formula node
  229. */
  230. const build = (node) => {
  231. if (node.type === "UnaryExpression" && node.operator === "!") {
  232. return ["!", build(/** @type {Expression} */ (node.argument))];
  233. }
  234. if (node.type === "LogicalExpression") {
  235. if (node.operator === "&&") {
  236. return [
  237. "&&",
  238. build(/** @type {Expression} */ (node.left)),
  239. build(/** @type {Expression} */ (node.right))
  240. ];
  241. }
  242. if (node.operator === "||") {
  243. if (parser.evaluateExpression(node.left).asBool() === false) {
  244. return build(/** @type {Expression} */ (node.right));
  245. }
  246. if (parser.evaluateExpression(node.right).asBool() === false) {
  247. return build(/** @type {Expression} */ (node.left));
  248. }
  249. return ["?"];
  250. }
  251. if (node.operator === "??") {
  252. const nullish = parser.evaluateExpression(node.left).asNullish();
  253. if (nullish === true) {
  254. return build(/** @type {Expression} */ (node.right));
  255. }
  256. if (nullish === false) {
  257. return build(/** @type {Expression} */ (node.left));
  258. }
  259. return ["?"];
  260. }
  261. }
  262. const dep =
  263. node.range && depByRangeStart.get(/** @type {number} */ (node.range[0]));
  264. return dep ? ["p", dep] : ["?"];
  265. };
  266. const formula = build(test);
  267. return hasAtom(formula) ? formula : null;
  268. };
  269. /**
  270. * Whether a presence formula guarantees `ns.member` is present. "Must be truthy"
  271. * semantics: the member is guaranteed when some `"member" in ns` atom must hold
  272. * for the branch condition. Statically-dead branches never reach here — the
  273. * parser eliminates them before the branch body is walked.
  274. * @param {GuardFormula} formula presence formula
  275. * @param {string} name namespace binding name
  276. * @param {string} member member key
  277. * @param {boolean} needTruthy whether the formula must be truthy
  278. * @returns {boolean} true when the member is guaranteed present
  279. */
  280. const evalPresenceFormula = (formula, name, member, needTruthy) => {
  281. switch (formula[0]) {
  282. case "p": {
  283. const dep = /** @type {HarmonyEvaluatedImportSpecifierDependency} */ (
  284. formula[1]
  285. );
  286. return (
  287. needTruthy &&
  288. dep.directImport === true &&
  289. dep.name === name &&
  290. dep.ids.length === 1 &&
  291. dep.ids[0] === member
  292. );
  293. }
  294. case "!":
  295. return evalPresenceFormula(formula[1], name, member, !needTruthy);
  296. case "&&":
  297. return (
  298. needTruthy &&
  299. (evalPresenceFormula(formula[1], name, member, true) ||
  300. evalPresenceFormula(formula[2], name, member, true))
  301. );
  302. default:
  303. return false;
  304. }
  305. };
  306. /**
  307. * Builds (once) the presence formula for a frame from the in-operator deps
  308. * created while walking the test.
  309. * @param {JavascriptParser} parser the parser
  310. * @param {GuardFrame} frame guard frame
  311. * @returns {GuardFormula | null} formula, or null when it has no presence atom
  312. */
  313. const buildFramePresenceFormula = (parser, frame) => {
  314. const deps = /** @type {Module} */ (parser.state.module).dependencies;
  315. /** @type {Map<number, HarmonyEvaluatedImportSpecifierDependency>} */
  316. const depByRangeStart = new Map();
  317. for (let i = /** @type {number} */ (frame.depStart); i < deps.length; i++) {
  318. const dep = /** @type {HarmonyEvaluatedImportSpecifierDependency} */ (
  319. deps[i]
  320. );
  321. if (dep.isHarmonyEvaluatedImportSpecifier === true && dep.range) {
  322. depByRangeStart.set(dep.range[0], dep);
  323. }
  324. }
  325. return buildPresenceFormula(
  326. parser,
  327. /** @type {Expression} */ (frame.test),
  328. depByRangeStart
  329. );
  330. };
  331. /**
  332. * Whether an active presence guard proves `ns.member` present, suppressing
  333. * export-presence errors. Each frame is evaluated against its branch condition,
  334. * so the `else` of `if (!("x" in ns))` also guards `ns.x`.
  335. * @param {JavascriptParser} parser the parser
  336. * @param {GuardFrame[]} stack the guard stack
  337. * @param {string} name namespace binding name
  338. * @param {string} member member key
  339. * @returns {boolean} true when a guard proves the member present
  340. */
  341. const isPresentByGuards = (parser, stack, name, member) => {
  342. for (let i = stack.length - 1; i >= 0; i--) {
  343. const frame = stack[i];
  344. if (frame.depStart === undefined) continue;
  345. let formula = frame.presenceFormula;
  346. if (formula === undefined) {
  347. formula = frame.presenceFormula = buildFramePresenceFormula(
  348. parser,
  349. frame
  350. );
  351. }
  352. if (
  353. formula !== null &&
  354. evalPresenceFormula(
  355. formula,
  356. name,
  357. member,
  358. /** @type {boolean} */ (frame.condition)
  359. )
  360. ) {
  361. return true;
  362. }
  363. }
  364. return false;
  365. };
  366. /**
  367. * Tags a freshly created dependency with the active dependency guards.
  368. * @param {JavascriptParser} parser the parser
  369. * @param {GuardableDependency} dep the dependency
  370. */
  371. const attachDependencyGuards = (parser, dep) => {
  372. const stack = /** @type {GuardFrame[] | undefined} */ (
  373. parser.state.guardStack
  374. );
  375. if (stack === undefined || stack.length === 0) return;
  376. /** @type {DependencyGuard[] | undefined} */
  377. let guards;
  378. for (const frame of stack) {
  379. if (frame.depStart === undefined) continue;
  380. let formula = frame.formula;
  381. if (formula === undefined) {
  382. formula = frame.formula = buildFrameLivenessFormula(parser, frame);
  383. }
  384. if (formula === null) continue;
  385. (guards || (guards = [])).push({
  386. formula,
  387. value: /** @type {boolean} */ (frame.condition)
  388. });
  389. }
  390. if (guards !== undefined) dep.branchGuards = guards;
  391. };
  392. module.exports.attachDependencyGuards = attachDependencyGuards;
  393. module.exports.isDeadByGuards = isDeadByGuards;
  394. module.exports.isPresentByGuards = isPresentByGuards;