Skip to main content

presolve_compiler/
production_module_emitter.rs

1//! K9 repository-owned deterministic production module emission.
2
3use std::collections::BTreeMap;
4use std::fmt::Write as _;
5
6use sha2::{Digest, Sha256};
7
8use crate::{ProductionChunkGraph, ProductionChunkId, ProductionChunkKind};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct ProductionModuleRecord {
12    pub chunk_id: ProductionChunkId,
13    pub filename: String,
14    pub source: String,
15    pub exports: Vec<String>,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct ProductionModuleLayout {
20    pub eager: ProductionModuleRecord,
21    pub shared: Vec<ProductionModuleRecord>,
22    pub roots: Vec<ProductionModuleRecord>,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub enum ProductionModuleValidationError {
27    DynamicCode,
28    InvalidExport,
29    NonCanonicalImportOrder,
30    SourceComment,
31}
32
33/// Emits a fixed, content-addressed module layout from the validated K7 graph.
34///
35/// # Panics
36///
37/// Panics only when a caller supplies a graph lacking its compiler-owned eager
38/// chunk, which is an earlier graph-integrity failure.
39#[must_use]
40pub fn emit_production_modules(graph: &ProductionChunkGraph) -> ProductionModuleLayout {
41    let eager_chunk = graph
42        .chunks
43        .iter()
44        .find(|chunk| chunk.kind == ProductionChunkKind::Eager)
45        .expect("validated graph contains one eager chunk");
46    let eager = module(eager_chunk.id.clone(), "boot", Vec::new());
47    let shared_by_id = graph
48        .chunks
49        .iter()
50        .filter(|chunk| chunk.kind == ProductionChunkKind::Shared)
51        .map(|chunk| {
52            (
53                chunk.id.clone(),
54                module(chunk.id.clone(), "shared", vec![eager.filename.clone()]),
55            )
56        })
57        .collect::<BTreeMap<_, _>>();
58    let shared = shared_by_id.values().cloned().collect::<Vec<_>>();
59    let mut roots = graph
60        .chunks
61        .iter()
62        .filter(|chunk| chunk.kind == ProductionChunkKind::Root)
63        .map(|chunk| {
64            let mut imports = graph
65                .dependencies
66                .iter()
67                .filter(|edge| edge.dependent_chunk_id == chunk.id)
68                .filter_map(|edge| shared_by_id.get(&edge.dependency_chunk_id))
69                .map(|shared| shared.filename.clone())
70                .collect::<Vec<_>>();
71            imports.push(eager.filename.clone());
72            module(chunk.id.clone(), "root", imports)
73        })
74        .collect::<Vec<_>>();
75    roots.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id));
76    ProductionModuleLayout {
77        eager,
78        shared,
79        roots,
80    }
81}
82
83/// Rejects unsupported generated-JavaScript constructs before production output.
84#[must_use]
85pub fn validate_production_module(
86    module: &ProductionModuleRecord,
87) -> Vec<ProductionModuleValidationError> {
88    let mut errors = Vec::new();
89    if module.source.contains("eval(")
90        || module.source.contains("Function(")
91        || module.source.contains("import(")
92    {
93        errors.push(ProductionModuleValidationError::DynamicCode);
94    }
95    if module.source.contains("//") || module.source.contains("/*") {
96        errors.push(ProductionModuleValidationError::SourceComment);
97    }
98    if module.exports
99        != vec![
100            "productionChunkId".to_string(),
101            "registerProductionChunk".to_string(),
102        ]
103    {
104        errors.push(ProductionModuleValidationError::InvalidExport);
105    }
106    let imports = module
107        .source
108        .lines()
109        .filter_map(|line| {
110            line.strip_prefix("import{registerProductionChunk as a}from\"./")
111                .and_then(|line| line.strip_suffix("\";"))
112        })
113        .collect::<Vec<_>>();
114    if imports.windows(2).any(|pair| pair[0] > pair[1]) {
115        errors.push(ProductionModuleValidationError::NonCanonicalImportOrder);
116    }
117    errors
118}
119
120fn module(
121    chunk_id: ProductionChunkId,
122    prefix: &str,
123    mut imports: Vec<String>,
124) -> ProductionModuleRecord {
125    imports.sort();
126    imports.dedup();
127    let mut import_text = String::new();
128    for filename in &imports {
129        write!(
130            import_text,
131            "import{{registerProductionChunk as a}}from\"./{filename}\";"
132        )
133        .expect("writing to a string should not fail");
134    }
135    let source = format!(
136        "{import_text}export const productionChunkId=\"{chunk_id}\";export function registerProductionChunk(){{return productionChunkId;}}\n"
137    );
138    let filename = format!("{prefix}.{}.js", short_hash(&source));
139    ProductionModuleRecord {
140        chunk_id,
141        filename,
142        source,
143        exports: vec![
144            "productionChunkId".to_string(),
145            "registerProductionChunk".to_string(),
146        ],
147    }
148}
149
150fn short_hash(source: &str) -> String {
151    format!("{:x}", Sha256::digest(source.as_bytes()))[..16].to_string()
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::{
158        extract_production_chunk_graph, ExecutableProgramFingerprint, ProductionRootChunkInput,
159        SharedChunkCandidatePlan,
160    };
161
162    #[test]
163    fn k9_emits_deterministic_syntax_safe_modules() {
164        let graph = extract_production_chunk_graph(
165            &SharedChunkCandidatePlan {
166                candidates: Vec::new(),
167                rejections: Vec::new(),
168            },
169            &[ProductionRootChunkInput {
170                activation_root_id: "root".to_string(),
171                root_kind: "interaction".to_string(),
172                programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
173                    b"return",
174                )],
175            }],
176        )
177        .expect("graph")
178        .0;
179        let first = emit_production_modules(&graph);
180        let second = emit_production_modules(&graph);
181        assert_eq!(first, second);
182        assert!(first.eager.filename.starts_with("boot."));
183        assert!(first
184            .roots
185            .iter()
186            .all(|module| validate_production_module(module).is_empty()));
187        assert!(validate_production_module(&first.eager).is_empty());
188    }
189}