Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion src/sema/file_analysis/source_tests/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,13 @@ const SHORT_CIRCUITED_CHAINS: &[&str] = &[
"const N = null; N?.a.b;",
];

const EVALUATED_CHAINS: &[&str] = &["x?.a.b;", "0?.a;", "(null?.a).b;", "null.a;"];
const EVALUATED_CHAINS: &[&str] = &[
"x?.a.b;",
"0?.a;",
"(null?.a).b;",
"null.a;",
"({ a: null }).a?.();",
];

const AFTER_EXITS: &[DeadCalls] = &[
("a.mjs", "function f() { return; a(); }", &["a"], &[]),
Expand Down Expand Up @@ -499,6 +505,88 @@ const EFFECTS: &[DeadCalls] = &[
("a.mjs", "if ((f(), false)) a();", &["a"], &["f"]),
];

const MEMBER_READS_OF_LITERALS: &[DeadCalls] = &[
("a.mjs", "if ({ b: 0 }.b) a(); else b();", &["a"], &["b"]),
("a.mjs", "[1, 0][1] ? a() : b();", &["a"], &["b"]),
("a.mjs", r#""ab".length === 2 || a();"#, &["a"], &[]),
("a.mjs", r#""ab"[0] === "b" && a();"#, &["a"], &[]),
("a.mjs", "[, 1][0] ?? a();", &[], &["a"]),
("a.mjs", "if ([[]][0]) a(); else b();", &["b"], &["a"]),
(
"a.mjs",
"const K = [1, 0][1]; if (K) a(); else b();",
&["a"],
&["b"],
),
("a.mjs", "[a(), 0][1] ? b() : c();", &[], &["a", "b", "c"]),
("a.mjs", "[module, /* @__PURE__ */ a(), 0][2];", &["a"], &[]),
(
"a.mjs",
"if ({ k: (f(), 0) }.k) a(); else b();",
&["a"],
&["f", "b"],
),
(
"a.mjs",
"if ([void f()][0]) a(); else b();",
&["a"],
&["f", "b"],
),
(
"a.mjs",
"const K = [(f(), 1)][0]; if (!K) a(); else b();",
&["a"],
&["f", "b"],
),
(
"a.mjs",
"let v = [1, 0][1]; if (v) a(); else b();",
&["a"],
&["b"],
),
(
"a.mjs",
"switch ([, 1][0]) { case 0: a(); break; default: b(); }",
&["a"],
&["b"],
),
(
"a.mjs",
"if ([null?.a][0]) a(); else b();",
&[],
&["a", "b"],
),
(
"a.mjs",
"if ({ k: null?.a }.k) a(); else b();",
&[],
&["a", "b"],
),
(
"a.mjs",
"if ({ a: 1, b: 0 }.b) a(); else b();",
&[],
&["a", "b"],
),
];

const VALUES_OF_BUN_RUN_ALONE: &[&str] = &[
"{ b: 0 }.b",
"[1, 0][1]",
"[, 1][0]",
r#""ab".length"#,
r#""ab"[0]"#,
r#""ab".length + 1"#,
"`${[1][0]}`",
"![0][0]",
"typeof [1][0]",
"typeof [function () {}][0]",
"(0, [1][0])",
"!(0, [[]][0])",
"!(x && [0][0])",
"[null][0]?.a",
];

const TYPEOF_REQUIRE: &[DeadCalls] = &[
(
"a.cjs",
Expand Down Expand Up @@ -1664,6 +1752,27 @@ fn effects_stay() {
check_dead_calls(EFFECTS);
}

#[test]
fn member_reads_of_literals() {
check_dead_calls(MEMBER_READS_OF_LITERALS);
for &text in VALUES_OF_BUN_RUN_ALONE {
for program in [
format!("m({text});"),
format!("const K = {text}; m(K);"),
format!("m([{text}]);"),
format!("m({{ k: {text} }});"),
] {
analyzed("a.mjs", &program, |s| {
let argument = only_argument(s, call_of(s, "m"));
assert_eq!(constant(s, argument), FoldedValue::Unknown, "{program}");
assert_eq!(data_value(s, argument), FoldedData::Unknown, "{program}");
assert!(!s.analysis.decisions.is_data_literal(argument), "{program}");
assert_nothing_dead(s, &program);
});
}
}
}

#[test]
fn typeof_require() {
check_dead_calls(TYPEOF_REQUIRE);
Expand Down
118 changes: 118 additions & 0 deletions src/sema/file_analysis/source_tests/module_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ use support::*;

use ModuleFormat::{CommonJs, Esm};

/// Programs whose names of CommonJS stand where `bun run` may fold them away. `what-a-file-is-taken-for.test.ts` beside the table runs each under `bun run` and in a build.
const FORMATS_UNDER_BUN_RUN: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../test/aot/fold/what-a-file-is-taken-for.txt"
));
const MIN_FORMATS_UNDER_BUN_RUN: usize = 200;

const FORMATS: &[(&str, &str, ModuleFormat)] = &[
("a.js", r#"export {};"#, Esm),
("a.js", r#"export const a = 1; module.exports = 2;"#, Esm),
Expand Down Expand Up @@ -432,6 +439,33 @@ const BUILD_FACTS: Fold<'static> = Fold {
..Fold::NONE
};

/// The three names that `bun run` replaces where nothing declares them, and `BUILD_FACTS`.
const KNOWN_TO_EVERY_BUILD: Fold<'static> = Fold {
defines: &[
(
Path::Global(&["undefined"]),
Defined::Value(Data::Undefined),
),
(
Path::Global(&["NaN"]),
Defined::Value(Data::Number(f64::NAN)),
),
(
Path::Global(&["Infinity"]),
Defined::Value(Data::Number(f64::INFINITY)),
),
(
Path::Global(&["Bun"]),
Defined::RuntimeValue(RuntimeValueKind::Object),
),
(
Path::Global(&["process", "platform"]),
Defined::Value(Data::String("linux")),
),
],
..Fold::NONE
};

const USER_DEFINES: Fold<'static> = Fold {
defines: &[
(
Expand Down Expand Up @@ -587,6 +621,12 @@ const FORMATS_WITH_USER_DEFINES: &[(&str, &str, ModuleFormat)] = &[
r#"if (process.platform === "linux" && OFF) require("a");"#,
Esm,
),
// Not yet: `bun run` reads the length out of the text that the define gives, and takes this file for an ES module.
(
"a.js",
r#"if (process.env.NODE_ENV.length === 3) require("a");"#,
CommonJs,
),
];

const FORMATS_WITH_BUILD_FACTS: &[(&str, &str, ModuleFormat)] = &[
Expand Down Expand Up @@ -969,6 +1009,8 @@ const CONSTANT_SPECIFIERS: &[&str] = &[
r#"switch (x) { case 0: const p = "ab"; break; default: require(p); }"#,
r#"require((f(), "ab"));"#,
r#"require(void f() || "ab");"#,
r#"require(([0][0], "ab"));"#,
r#"require(void [0][0] || "ab");"#,
];

const UNRECORDED_SPECIFIERS: &[&str] = &[
Expand Down Expand Up @@ -1274,6 +1316,82 @@ fn format_follows_what_the_user_defines() {
}
}

#[test]
fn format_is_what_the_table_says_that_a_build_takes_a_file_for() {
let format_of = |column: &str, line: usize| match column {
"cjs" => CommonJs,
"esm" => Esm,
_ => panic!("line {line}: {column:?} is no format"),
};
let lines = bun_core::strings::split(FORMATS_UNDER_BUN_RUN.as_bytes(), b"\n");
let mut row_count = 0;
for (index, row) in lines.enumerate() {
if row.is_empty() || row.starts_with(b"#") {
continue;
}
let line = index + 1;
let columns = bun_core::strings::split(row, b"\t");
let columns: Vec<&str> = columns
.map(|column| std::str::from_utf8(column).unwrap())
.collect();
let &[bun_run, build, extension, text] = &columns[..] else {
panic!("line {line} has {} columns", columns.len());
};
let bun_run = format_of(bun_run, line);
let format = match build {
"=" => bun_run,
_ => format_of(build, line),
};
assert!(
build == "=" || format != bun_run,
"line {line} names one format twice"
);
let path = format!("a.{extension}");
with_workspace(|workspace| {
let fold = workspace.fold_inputs(&KNOWN_TO_EVERY_BUILD);
let analyzed = workspace.analyze(&Source::new(&path, text), &fold);
assert_eq!(
analyzed.analysis.module_format, format,
"line {line}: {text}"
);
});
row_count += 1;
}
assert!(row_count > MIN_FORMATS_UNDER_BUN_RUN, "{row_count} rows");
}

#[test]
fn member_read_of_a_literal_names_a_file_at_run_time() {
for (path, text) in [
("a.cjs", r#"require(["ab"][0]);"#),
("a.cjs", r#"const p = ["ab"][0]; require(p);"#),
("a.cjs", r#"require({ a: "ab" }.a);"#),
("a.cjs", r#"require("a" + ["b"][0]);"#),
("a.cjs", r#"require(["ab"][0] || x);"#),
("a.cjs", r#"require(1 ? ["ab"][0] : x);"#),
("a.cts", r#"require(["ab"][0] as string);"#),
] {
analyzed(path, text, |s| {
let specifier = only_site(s, text).specifier;
let expected = [ImportSpecifier::Constant(s.workspace.atom("ab"))];
assert!(matches!(specifier, ImportSpecifier::OneOf(_)), "{text}");
assert_eq!(s.analysis.alternatives_of(&specifier), expected, "{text}");
});
}
let joined = r#"require(require(["node:path"][0]).join(__dirname, "a.js"));"#;
analyzed("a.cjs", joined, |s| {
let [_, file] = s.analysis.import_sites else {
panic!("{joined}: {:?}", s.analysis.import_sites);
};
assert_eq!(file.specifier, ImportSpecifier::Unknown, "{joined}");
});
let selected = r#"require((0, [[]][0]) ? "ab" : "cd");"#;
analyzed("a.cjs", selected, |s| {
let expected = ImportSpecifier::Constant(s.workspace.atom("ab"));
assert_eq!(only_site(s, selected).specifier, expected, "{selected}");
});
}

#[test]
fn main_module_comparisons() {
for &(text, is_equality) in MAIN_MODULE_COMPARISONS {
Expand Down
1 change: 1 addition & 0 deletions src/sema/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod folder_names;
mod folder_operators;
mod inputs;
mod known_to_bun_run;
mod literal_members;
mod removed_by_bun_run;
mod run_order;
mod string_affixes;
Expand Down
20 changes: 18 additions & 2 deletions src/sema/fold/data_literals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,33 @@ const MAX_LINEAR_SEARCH_KEYS: usize = 8;
impl<'s> Folder<'_, 's> {
pub(super) fn note_array_literal(&mut self, e: ExprId, items: IdList<ExprId>) {
let hir = self.hir;
if hir.ids(items).all(|item| self.is_data_part(item)) {
let is_data = hir.ids(items).all(|item| self.is_data_part(item));
if is_data && !self.holds_value_of_bun_run_alone(hir.ids(items)) {
self.data_literals.insert(e.idx());
}
}

pub(super) fn note_object_literal(&mut self, e: ExprId, props: Span<PropId>) {
if props.iter().all(|prop| self.is_data_property(prop)) {
let hir = self.hir;
let parts = props.iter().flat_map(|prop| {
let key = match hir.runtime_property_key(prop) {
PropKey::Computed(key) => Some(key),
_ => None,
};
key.into_iter().chain([hir[prop].value])
});
let is_data = props.iter().all(|prop| self.is_data_property(prop));
if is_data && !self.holds_value_of_bun_run_alone(parts) {
self.data_literals.insert(e.idx());
}
}

/// A value that `bun run` alone folds is no data of a build.
fn holds_value_of_bun_run_alone(&self, mut parts: impl Iterator<Item = ExprId>) -> bool {
!self.folded_by_bun_run_alone.is_empty()
&& parts.any(|part| self.folded_by_bun_run_alone.contains(&part))
}

fn is_data_part(&self, e: ExprId) -> bool {
let is_literal = e.idx() < self.hir.exprs.len() && self.data_literals.contains(e.idx());
is_literal || self.is_pure_constant(e)
Expand Down
2 changes: 1 addition & 1 deletion src/sema/fold/dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl<'s> Folder<'_, 's> {
None => None,
};
roots.all.extend(excluded.map(TreeNode::Stmt));
if self.is_truthy_in_bun_run(test).is_some() {
if self.is_truthy_as_test_in_bun_run(test).is_some() {
roots.in_bun_run.extend(excluded.map(TreeNode::Stmt));
}
} else {
Expand Down
Loading
Loading