Refactor: Implement Record Layouts and Optimized Field Access

Introduces a new `RecordLayout` system for efficient and type-safe
record handling.

Key changes:
- `RecordLayout` struct with `O(1)` field lookup using FMap
  optimization.
- Field accessors (`.name`) are now first-class callable values.
- Parser recognizes dot-prefixed identifiers as field accessors.
- Binder and TypeChecker handle field accessors.
- Optimizer transforms field accessor calls into specialized `GetField`
  nodes.
- VM executes `GetField` efficiently by directly accessing record
  values.
- Adds a new `records.myc` example showcasing record features.
- Improves comparison logic for records to use pointer equality for
  layout.
This commit is contained in:
Michael Schimmel
2026-02-26 21:38:20 +01:00
parent 2ad2eb5d43
commit bf74795e01
20 changed files with 548 additions and 63 deletions
+81
View File
@@ -426,4 +426,85 @@ mod tests {
"Definitions should be removed by DCE"
);
}
#[test]
fn test_record_basics() {
let env = Environment::new();
let source = r#"
((fn [user] [(.name user) (.age user)])
{:name "Alice" :age 30})
"#;
let res = env.run_script(source).unwrap();
assert_eq!(format!("{}", res), "[\"Alice\" 30]");
}
#[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();
assert_eq!(format!("{}", res), "99.5");
// 2. Verify optimization to GET_FIELD
let dump = env.dump_ast(source).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();
let source = r#"
(do
(def get-name .name)
; Dynamic call to field accessor
(get-name {:name "Alice"}))
"#;
let res = env.run_script(source).unwrap();
assert_eq!(format!("{}", res), "\"Alice\"");
}
#[test]
fn test_record_constant_folding() {
let env = Environment::new();
// 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);
}
#[test]
fn test_record_errors() {
let env = Environment::new();
// Both cases are reported by the TypeChecker since it can't resolve the call
// for a missing field or a non-record argument.
// 1. Missing field
let res_missing = env.run_script("(.missing {:a 1})");
assert!(res_missing.is_err());
assert!(res_missing.unwrap_err().contains("Invalid arguments"));
// 2. Not a record
let res_not_rec = env.run_script("(.name 123)");
assert!(res_not_rec.is_err());
assert!(res_not_rec.unwrap_err().contains("Invalid arguments"));
}
#[test]
fn test_record_layout_interning() {
let env = Environment::new();
let source = r#"
(do
(def r1 {:a 1 :b 2})
(def r2 {:a 10 :b 20})
; Identical layouts result in identical types
(= r1 r2))
"#;
// This will be false because values differ, but let's just check if it compiles and runs.
// To really test interning, we'd need a way to check if layouts are the same Arc.
let res = env.run_script(source).unwrap();
assert_eq!(format!("{}", res), "false");
}
}