Skip to main content

hydra_compiler/
compiler.rs

1//! ActionCompiler — generates deterministic AST from normalized sequences.
2
3use serde::{Deserialize, Serialize};
4
5use crate::ast::{ActionNode, ParamExpr};
6use crate::normalizer::{NormalizedParam, NormalizedSequence};
7
8/// A compiled action ready for zero-token execution
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CompiledAction {
11    pub id: String,
12    pub signature: String,
13    pub ast: ActionNode,
14    pub required_variables: Vec<String>,
15    pub compiled_at: String,
16    pub source_occurrences: u32,
17    pub source_success_rate: f64,
18}
19
20/// Compiles normalized sequences into executable ASTs
21pub struct ActionCompiler;
22
23impl ActionCompiler {
24    /// Compile a normalized sequence into an executable AST
25    pub fn compile(
26        normalized: &NormalizedSequence,
27        occurrences: u32,
28        success_rate: f64,
29    ) -> CompiledAction {
30        let required_variables: Vec<String> = normalized.variables.keys().cloned().collect();
31
32        let ast = if normalized.actions.len() == 1 {
33            Self::compile_single_action(&normalized.actions[0])
34        } else {
35            Self::compile_sequence(normalized)
36        };
37
38        CompiledAction {
39            id: uuid::Uuid::new_v4().to_string(),
40            signature: normalized.signature.clone(),
41            ast,
42            required_variables,
43            compiled_at: chrono::Utc::now().to_rfc3339(),
44            source_occurrences: occurrences,
45            source_success_rate: success_rate,
46        }
47    }
48
49    fn compile_single_action(action: &crate::normalizer::NormalizedAction) -> ActionNode {
50        ActionNode::Action {
51            tool: action.tool.clone(),
52            params: action
53                .params
54                .iter()
55                .map(|(k, v)| (k.clone(), Self::param_to_expr(v)))
56                .collect(),
57        }
58    }
59
60    fn compile_sequence(normalized: &NormalizedSequence) -> ActionNode {
61        let nodes: Vec<ActionNode> = normalized
62            .actions
63            .iter()
64            .map(|a| Self::compile_single_action(a))
65            .collect();
66
67        ActionNode::Sequence(nodes)
68    }
69
70    fn param_to_expr(param: &NormalizedParam) -> ParamExpr {
71        match param {
72            NormalizedParam::Literal(v) => ParamExpr::Literal(v.clone()),
73            NormalizedParam::Variable { name } => ParamExpr::Variable(name.clone()),
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::normalizer::{
82        InferredType, NormalizedAction, NormalizedParam, NormalizedSequence, VariableInfo,
83    };
84    use std::collections::HashMap;
85
86    fn simple_sequence() -> NormalizedSequence {
87        NormalizedSequence {
88            actions: vec![
89                NormalizedAction {
90                    tool: "git_add".into(),
91                    params: HashMap::from([(
92                        "path".into(),
93                        NormalizedParam::Literal(serde_json::json!(".")),
94                    )]),
95                },
96                NormalizedAction {
97                    tool: "git_commit".into(),
98                    params: HashMap::from([(
99                        "message".into(),
100                        NormalizedParam::Variable {
101                            name: "var_0".into(),
102                        },
103                    )]),
104                },
105            ],
106            variables: HashMap::from([(
107                "var_0".into(),
108                VariableInfo {
109                    name: "var_0".into(),
110                    sample_values: vec![serde_json::json!("fix: bug")],
111                    inferred_type: InferredType::String,
112                },
113            )]),
114            signature: "git_add→git_commit".into(),
115        }
116    }
117
118    #[test]
119    fn test_compile_simple() {
120        let norm = simple_sequence();
121        let compiled = ActionCompiler::compile(&norm, 5, 1.0);
122        assert_eq!(compiled.signature, "git_add→git_commit");
123        assert_eq!(compiled.required_variables, vec!["var_0"]);
124        assert_eq!(compiled.source_occurrences, 5);
125        assert_eq!(compiled.ast.action_count(), 2);
126    }
127
128    #[test]
129    fn test_compile_single_action() {
130        let norm = NormalizedSequence {
131            actions: vec![NormalizedAction {
132                tool: "deploy".into(),
133                params: HashMap::from([(
134                    "env".into(),
135                    NormalizedParam::Literal(serde_json::json!("prod")),
136                )]),
137            }],
138            variables: HashMap::new(),
139            signature: "deploy".into(),
140        };
141        let compiled = ActionCompiler::compile(&norm, 3, 1.0);
142        assert_eq!(compiled.ast.action_count(), 1);
143        assert!(compiled.required_variables.is_empty());
144    }
145
146    #[test]
147    fn test_compiled_serializable() {
148        let norm = simple_sequence();
149        let compiled = ActionCompiler::compile(&norm, 5, 1.0);
150        let json = serde_json::to_string(&compiled).unwrap();
151        assert!(json.contains("git_add"));
152        assert!(json.contains("git_commit"));
153    }
154
155    #[test]
156    fn test_compiled_action_has_id() {
157        let norm = simple_sequence();
158        let compiled = ActionCompiler::compile(&norm, 1, 0.9);
159        assert!(!compiled.id.is_empty());
160    }
161
162    #[test]
163    fn test_compiled_action_has_timestamp() {
164        let norm = simple_sequence();
165        let compiled = ActionCompiler::compile(&norm, 1, 1.0);
166        assert!(!compiled.compiled_at.is_empty());
167    }
168
169    #[test]
170    fn test_compiled_preserves_success_rate() {
171        let norm = simple_sequence();
172        let compiled = ActionCompiler::compile(&norm, 10, 0.85);
173        assert_eq!(compiled.source_success_rate, 0.85);
174        assert_eq!(compiled.source_occurrences, 10);
175    }
176
177    #[test]
178    fn test_compiled_action_serde_roundtrip() {
179        let norm = simple_sequence();
180        let compiled = ActionCompiler::compile(&norm, 3, 1.0);
181        let json = serde_json::to_string(&compiled).unwrap();
182        let restored: CompiledAction = serde_json::from_str(&json).unwrap();
183        assert_eq!(restored.signature, "git_add→git_commit");
184        assert_eq!(restored.ast.action_count(), 2);
185    }
186
187    #[test]
188    fn test_compile_no_variables() {
189        let norm = NormalizedSequence {
190            actions: vec![NormalizedAction {
191                tool: "test".into(),
192                params: HashMap::from([("key".into(), NormalizedParam::Literal(serde_json::json!("val")))]),
193            }],
194            variables: HashMap::new(),
195            signature: "test".into(),
196        };
197        let compiled = ActionCompiler::compile(&norm, 1, 1.0);
198        assert!(compiled.required_variables.is_empty());
199    }
200}