From 010026761688fad39bfdc36bda91e28c09f7e7bf Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:55:06 -0700 Subject: [PATCH 1/2] Include dialect in module cache keys. Prevent stale module artifacts from bypassing dialect-gated optimizer fixes by keying cached outputs on explicit dialect semantics. --- src/compiler/diskcache.rs | 68 ++++++++++++++++++++++++------ src/tests/compiler/module_cache.rs | 3 +- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/compiler/diskcache.rs b/src/compiler/diskcache.rs index ba189f450..8e1228fc6 100644 --- a/src/compiler/diskcache.rs +++ b/src/compiler/diskcache.rs @@ -4,12 +4,25 @@ use crate::compiler::clvm::sha256tree_from_atom; use crate::compiler::comptypes::{CompileErr, CompileForm, CompilerOpts}; use crate::compiler::sexp::decode_string; -fn cache_key(cf: &CompileForm) -> String { - let mut include_fingerprints = Vec::new(); +fn cache_key(opts: Rc, cf: &CompileForm) -> String { + let dialect = opts.dialect(); + let mut key_material = b"module-cache-v2".to_vec(); + + if let Some(stepping) = dialect.stepping { + key_material.push(1); + key_material.extend_from_slice(&stepping.to_le_bytes()); + } else { + key_material.push(0); + } + key_material.push(u8::from(dialect.strict)); + key_material.push(u8::from(dialect.int_fix)); + key_material.push(u8::from(dialect.extra_numeric_constants)); + key_material.push(u8::from(dialect.cse_dominance)); + for include in cf.include_forms.iter() { - include_fingerprints.extend_from_slice(&include.fingerprint); + key_material.extend_from_slice(&include.fingerprint); } - hex::encode(sha256tree_from_atom(&include_fingerprints)) + hex::encode(sha256tree_from_atom(&key_material)) } /// Try to get an element from the cache, exposing errors. @@ -30,7 +43,7 @@ pub fn try_element_from_cache( cf: &CompileForm, export_path: &str, ) -> Option { - let key = cache_key(cf); + let key = cache_key(opts.clone(), cf); let hex_file_name = format!(".chialisp/{key}/{export_path}"); opts.read_new_file(cf.loc().file.to_string(), hex_file_name.clone()) .ok() @@ -43,7 +56,7 @@ pub fn set_cache_element_error( export_path: &str, export_hex: &str, ) -> Result<(), CompileErr> { - let key = cache_key(cf); + let key = cache_key(opts.clone(), cf); let hex_file_name = format!(".chialisp/{key}/{export_path}"); opts.write_new_file(&hex_file_name, export_hex.as_bytes())?; Ok(()) @@ -63,14 +76,16 @@ pub fn set_cache_element( /// Exposes the cache-key segment used under `.chialisp//` (tests and tooling only). #[cfg(test)] -pub fn module_cache_key_hex(cf: &CompileForm) -> String { - cache_key(cf) +pub fn module_cache_key_hex(opts: Rc, cf: &CompileForm) -> String { + cache_key(opts, cf) } #[cfg(test)] mod tests { use super::*; + use crate::compiler::compiler::DefaultCompilerOpts; use crate::compiler::comptypes::{BodyForm, CompileForm, IncludeDesc, IncludeProcessType}; + use crate::compiler::dialect::AcceptedDialect; use crate::compiler::sexp::SExp; use crate::compiler::srcloc::Srcloc; @@ -84,14 +99,18 @@ mod tests { } } + fn default_opts(filename: &str) -> Rc { + Rc::new(DefaultCompilerOpts::new(filename)) + } + #[test] fn cache_key_stable_for_empty_includes() { let loc = Srcloc::start(&"a.clsp".to_string()); let cf = empty_compileform(loc); - let k = cache_key(&cf); + let k = cache_key(default_opts("a.clsp"), &cf); assert_eq!( k, - "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a" + "bb1dfe276a7165264b23349a3d349c470f451aa964f89c172bfb5bc8fdf9d548" ); } @@ -112,15 +131,36 @@ mod tests { fingerprint: fp, }; cf.include_forms.push(desc(fp(&[1, 2, 3]))); - let k1 = cache_key(&cf); + let k1 = cache_key(default_opts("b.clsp"), &cf); cf.include_forms.push(desc(fp(&[4, 5]))); - let k2 = cache_key(&cf); + let k2 = cache_key(default_opts("b.clsp"), &cf); assert_ne!(k1, k2); cf.include_forms.truncate(1); - let k1_again = cache_key(&cf); + let k1_again = cache_key(default_opts("b.clsp"), &cf); assert_eq!(k1, k1_again); } + #[test] + fn cache_key_changes_with_cse_dominance() { + let loc = Srcloc::start(&"cse.clsp".to_string()); + let cf = empty_compileform(loc); + let dialect_before = AcceptedDialect { + stepping: Some(26), + strict: true, + int_fix: true, + extra_numeric_constants: false, + cse_dominance: false, + }; + let dialect_after = AcceptedDialect { + cse_dominance: true, + ..dialect_before.clone() + }; + let before = default_opts("cse.clsp").set_dialect(dialect_before); + let after = default_opts("cse.clsp").set_dialect(dialect_after); + + assert_ne!(cache_key(before, &cf), cache_key(after, &cf)); + } + #[test] fn cache_key_main_fingerprint_style() { let loc = Srcloc::start(&"c.clsp".to_string()); @@ -135,7 +175,7 @@ mod tests { kind: Some(IncludeProcessType::Compiled), fingerprint: main_fp, }); - let k = cache_key(&cf); + let k = cache_key(default_opts("c.clsp"), &cf); assert!(!k.is_empty()); assert_ne!( k, diff --git a/src/tests/compiler/module_cache.rs b/src/tests/compiler/module_cache.rs index e676189c0..218eec2bc 100644 --- a/src/tests/compiler/module_cache.rs +++ b/src/tests/compiler/module_cache.rs @@ -67,7 +67,8 @@ fn function_export(name: &[u8], as_name: Option>) -> Export { } fn cache_dir_for(cf: &CompileForm) -> String { - format!(".chialisp/{}/", module_cache_key_hex(cf)) + let opts: Rc = Rc::new(DefaultCompilerOpts::new(TEST_CLSP)); + format!(".chialisp/{}/", module_cache_key_hex(opts, cf)) } #[test] From a8f575a94768433cd0217cb62c436698d48bf0d2 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:39:36 -0700 Subject: [PATCH 2/2] Align module cache lookup dialect. Use the same effective module dialect for cache reads and writes so dialect-aware cache keys still hit valid module artifacts. --- src/compiler/compiler.rs | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/compiler/compiler.rs b/src/compiler/compiler.rs index 27b0cec7e..0aa17f1c2 100644 --- a/src/compiler/compiler.rs +++ b/src/compiler/compiler.rs @@ -387,14 +387,7 @@ fn populate_export_map( /// /// (export foo) /// (export bar) -pub fn compile_module( - context: &mut BasicCompileContext, - mut opts: Rc, - standalone_constants: &HashSet>, - mut program: CompileForm, - exports: &[Export], -) -> Result { - let loc = program.loc(); +fn module_compile_opts(opts: Rc) -> Rc { let mut dialect = opts.dialect(); // Module style is new, so there will never be a time when we don't want every // bug fix that existed before it was released. @@ -405,7 +398,19 @@ pub fn compile_module( dialect.stepping = Some(26); } } - opts = opts.set_optimize(true).set_dialect(dialect); + + opts.set_optimize(true).set_dialect(dialect) +} + +pub fn compile_module( + context: &mut BasicCompileContext, + mut opts: Rc, + standalone_constants: &HashSet>, + mut program: CompileForm, + exports: &[Export], +) -> Result { + let loc = program.loc(); + opts = module_compile_opts(opts); if exports.is_empty() { return Err(CompileErr( @@ -844,20 +849,14 @@ pub fn compile_pre_forms( )), FrontendOutput::Module(mut cf, exports) => { add_main_fingerprint(&mut cf, pre_forms); + let opts = module_compile_opts(opts); + // If we can read from cache, use that. We'll use the actual form of the compileform // and opts (the dialect) to determine a cache hit. if let Some(result) = try_from_cache(opts.clone(), &cf, &exports)? { return Ok(result); } - // cl23 always reflects optimization. - let dialect = opts.dialect(); - let opts = if let Some(stepping) = dialect.stepping.as_ref() { - opts.set_optimize(*stepping > 21) - } else { - opts - }; - // We make a dependency graph of constants and functions. There must // be a solveable hierarchy for constants, that is some must be top // level constants that are not depended on. Thse will not be tabled