Refactor: Use new_inner for substitution maps

The `SubstitutionMap::new_inner()` method provides a more robust way to
create nested substitution maps, ensuring that only global substitutions
are inherited. This prevents potential issues with overlapping local and
upvalue addresses in nested scopes, such as during lambda inlining.

This change also includes:
- A new `try_fold_record` method in `folder.rs` to optimize record
  literals into constant values.
- Updates to `inliner.rs` to recognize `Value::Record` for inlining.
- Various test cases to verify record optimization and constant folding.
This commit is contained in:
Michael Schimmel
2026-02-28 16:01:56 +01:00
parent 580c0893c1
commit 3f5d2620ec
5 changed files with 104 additions and 17 deletions
+45 -8
View File
@@ -482,17 +482,17 @@ mod tests {
#[test]
fn test_record_optimized_access() {
let env = Environment::new();
let source = "(.price {:id 1 :price 99.5})";
// 1. Check result
let res = env.run_script(source).unwrap();
let source_eval = "(.price {:id 1 :price 99.5})";
// 1. Check result (will be fully folded to a constant by the new optimization)
let res = env.run_script(source_eval).unwrap();
assert_eq!(format!("{}", res), "99.5");
// 2. Verify optimization to GET_FIELD
let dump = env.dump_ast(source).unwrap();
// 2. Verify optimization to GET_FIELD when the record contains non-constants
let source_ast = "(fn [id] (.price {:id id :price 99.5}))";
let dump = env.dump_ast(source_ast).unwrap();
assert!(dump.contains("GetField: .price"), "Should be optimized to GetField. Dump:\n{}", dump);
}
#[test]
fn test_first_class_field_accessor() {
let env = Environment::new();
@@ -512,7 +512,44 @@ mod tests {
// Optimizer should fold (.x {:x 10}) into 10
let source = "(.x {:x 10 :y 20})";
let dump = env.dump_ast(source).unwrap();
assert!(dump.contains("Constant: 10"), "Should be constant folded. Dump:\n{}", dump);
assert!(dump.contains("Constant: 10"), "Should evaluate GetField at compile time.");
}
#[test]
fn test_record_literal_constant_folding() {
let env = Environment::new();
let source = " {:a 1 :b 2} ";
let dump = env.dump_ast(source).unwrap();
// Ensure the record definition itself is folded into a Constant.
assert!(dump.contains("Constant: {:a 1, :b 2}"), "Should transform a pure record literal into a constant value.");
assert!(!dump.contains("Record {"), "Should not leave a runtime Record node in the AST.");
}
#[test]
fn test_record_inlining_in_while_loop() {
let env_ast = Environment::new();
// Ensure that constants (like the config record) are inherited into inner lambda scopes (like the body of a while loop).
let source = r#"
(do
(def loop-config {:start 0 :limit 10})
(def loop-idx 0)
(while (< loop-idx (.limit loop-config))
(assign loop-idx (+ loop-idx 1)))
loop-idx
)
"#;
let dump = env_ast.dump_ast(source).unwrap();
// The optimizer should inline `loop-config`, resolving `(.limit loop-config)` to `10`.
// The GetField and the Get for 'loop-config' should vanish inside the condition.
assert!(!dump.contains("GetField: .limit"), "The record field should be completely inlined.");
assert!(dump.contains("Constant: 10"), "The limit should be resolved to a constant 10.");
// The result of running it should obviously still be correct.
let env_run = Environment::new();
let res = env_run.run_script(source).unwrap();
assert_eq!(format!("{}", res), "10");
}
#[test]