Skip to main content

presolve_compiler/
runtime_effect_artifact.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4
5use crate::runtime_computed_artifact::{
6    RuntimeComputedArtifactInstruction, RuntimeComputedArtifactOperand,
7};
8use crate::{
9    build_runtime_effect_registry, ApplicationSemanticModel, EffectExecutionPolicy,
10    EffectRenderBoundary, ExecutionBoundary, IntermediateRepresentation, IrInstruction,
11    IrInstructionKind, IrValueId, RuntimeEffectRecord, EFFECT_CAPABILITY_REGISTRY,
12};
13
14pub const RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION: u32 = 1;
15
16/// Versioned compiler-generated runtime metadata and effect programs.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct RuntimeEffectArtifact {
19    pub schema_version: u32,
20    pub effects: Vec<RuntimeEffectArtifactEffect>,
21}
22
23/// Runtime metadata and executable capability program for one lowered effect.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25pub struct RuntimeEffectArtifactEffect {
26    pub effect: String,
27    pub execution_function: String,
28    pub initial_trigger_policy: RuntimeEffectArtifactExecutionPolicy,
29    pub initial_trigger: Option<RuntimeEffectArtifactInitialTrigger>,
30    pub action_batch_triggers: Vec<RuntimeEffectArtifactActionTrigger>,
31    pub capability_operations: Vec<RuntimeEffectArtifactCapabilityOperation>,
32    pub execution_boundary: RuntimeEffectArtifactExecutionBoundary,
33    pub program: RuntimeEffectArtifactProgram,
34}
35
36/// Explicit initial-render trigger metadata for one emitted effect.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38pub struct RuntimeEffectArtifactInitialTrigger {
39    pub render_boundary: RuntimeEffectArtifactRenderBoundary,
40    pub required_computed: Vec<String>,
41    pub prerequisite_batches: Vec<RuntimeEffectArtifactPrerequisiteBatch>,
42    pub effect_batch_index: u32,
43}
44
45/// Explicit completed-action-batch trigger metadata for one emitted effect.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47pub struct RuntimeEffectArtifactActionTrigger {
48    pub action_batch: String,
49    pub matched_states: Vec<String>,
50    pub required_computed: Vec<String>,
51    pub prerequisite_batches: Vec<RuntimeEffectArtifactPrerequisiteBatch>,
52    pub effect_batch_index: u32,
53}
54
55/// A filtered F9 computed-prerequisite batch available to an effect trigger.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57pub struct RuntimeEffectArtifactPrerequisiteBatch {
58    pub source_batch_index: u32,
59    pub computed: Vec<String>,
60}
61
62/// Stable compiler operation and runtime-lowering identities.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64pub struct RuntimeEffectArtifactCapabilityOperation {
65    pub operation: String,
66    pub runtime_lowering: String,
67}
68
69/// One complete, ordered F10/F11 effect function program.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71pub struct RuntimeEffectArtifactProgram {
72    pub instructions: Vec<RuntimeEffectArtifactInstruction>,
73}
74
75/// An effect program instruction. Pure operand instructions reuse the existing
76/// computed-artifact representation; capability operations are explicit roots.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(untagged)]
79pub enum RuntimeEffectArtifactInstruction {
80    Evaluate(RuntimeComputedArtifactInstruction),
81    CapabilityCall {
82        #[serde(rename = "kind")]
83        kind: RuntimeEffectArtifactCapabilityInstructionKind,
84        operation: String,
85        runtime_lowering: String,
86        arguments: Vec<RuntimeComputedArtifactOperand>,
87    },
88    CapabilityAssign {
89        #[serde(rename = "kind")]
90        kind: RuntimeEffectArtifactCapabilityInstructionKind,
91        operation: String,
92        runtime_lowering: String,
93        value: RuntimeComputedArtifactOperand,
94    },
95}
96
97/// Distinguishes the two observable capability instruction forms.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
99#[serde(rename_all = "kebab-case")]
100pub enum RuntimeEffectArtifactCapabilityInstructionKind {
101    CapabilityCall,
102    CapabilityAssign,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
106#[serde(rename_all = "kebab-case")]
107pub enum RuntimeEffectArtifactExecutionPolicy {
108    AfterInitialRenderAndCompletedActionBatch,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
112#[serde(rename_all = "kebab-case")]
113pub enum RuntimeEffectArtifactRenderBoundary {
114    AfterInitialRender,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
118#[serde(rename_all = "kebab-case")]
119pub enum RuntimeEffectArtifactExecutionBoundary {
120    Client,
121    Server,
122}
123
124/// Emit a deterministic effect artifact from F9 plans, F10/F11 IR, and F12 records.
125///
126/// The artifact contains canonical semantic and registry identities only. It
127/// neither retains raw capability paths nor performs dependency discovery or
128/// effect execution.
129#[must_use]
130pub fn build_runtime_effect_artifact(
131    model: &ApplicationSemanticModel,
132    ir: &IntermediateRepresentation,
133) -> RuntimeEffectArtifact {
134    let registry = build_runtime_effect_registry(model, ir);
135    let programs = effect_programs(ir);
136    let effects = registry
137        .records
138        .values()
139        .filter_map(|record| {
140            let program = programs.get(&record.effect)?.clone();
141            runtime_effect(record, program)
142        })
143        .collect();
144    RuntimeEffectArtifact {
145        schema_version: RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
146        effects,
147    }
148}
149
150/// Serialize emitted effect runtime metadata as deterministic, pretty JSON.
151///
152/// # Panics
153///
154/// Panics when compiler-owned effect runtime metadata cannot serialize.
155#[must_use]
156pub fn runtime_effect_artifact_json(artifact: &RuntimeEffectArtifact) -> String {
157    serde_json::to_string_pretty(artifact).expect("effect runtime artifact should serialize") + "\n"
158}
159
160fn runtime_effect(
161    record: &RuntimeEffectRecord,
162    program: RuntimeEffectArtifactProgram,
163) -> Option<RuntimeEffectArtifactEffect> {
164    Some(RuntimeEffectArtifactEffect {
165        effect: record.effect.as_str().to_string(),
166        execution_function: record.execution_function.as_str().to_string(),
167        initial_trigger_policy: execution_policy(record.initial_trigger_policy),
168        initial_trigger: record.initial_trigger.as_ref().map(|trigger| {
169            RuntimeEffectArtifactInitialTrigger {
170                render_boundary: render_boundary(trigger.render_boundary),
171                required_computed: semantic_ids(&trigger.required_computed),
172                prerequisite_batches: prerequisite_batches(&trigger.prerequisite_batches),
173                effect_batch_index: trigger.effect_batch_index,
174            }
175        }),
176        action_batch_triggers: record
177            .action_batch_triggers
178            .iter()
179            .map(|trigger| RuntimeEffectArtifactActionTrigger {
180                action_batch: trigger.action_batch.as_str().to_string(),
181                matched_states: semantic_ids(&trigger.matched_states),
182                required_computed: semantic_ids(&trigger.required_computed),
183                prerequisite_batches: prerequisite_batches(&trigger.prerequisite_batches),
184                effect_batch_index: trigger.effect_batch_index,
185            })
186            .collect(),
187        capability_operations: capability_operations(&record.capability_operations)?,
188        execution_boundary: execution_boundary(record.execution_boundary),
189        program,
190    })
191}
192
193fn effect_programs(
194    ir: &IntermediateRepresentation,
195) -> BTreeMap<crate::SemanticId, RuntimeEffectArtifactProgram> {
196    ir.modules
197        .iter()
198        .flat_map(|module| {
199            module.effect_executions.iter().filter_map(|execution| {
200                let function = module
201                    .functions
202                    .iter()
203                    .find(|function| function.id == execution.function)?;
204                let instructions = function
205                    .blocks
206                    .iter()
207                    .flat_map(|block| block.instructions.iter())
208                    .map(runtime_instruction)
209                    .collect::<Option<Vec<_>>>()?;
210                Some((
211                    execution.effect.clone(),
212                    RuntimeEffectArtifactProgram { instructions },
213                ))
214            })
215        })
216        .collect()
217}
218
219fn runtime_instruction(instruction: &IrInstruction) -> Option<RuntimeEffectArtifactInstruction> {
220    match &instruction.kind {
221        IrInstructionKind::CapabilityCall {
222            operation,
223            arguments,
224        } => Some(RuntimeEffectArtifactInstruction::CapabilityCall {
225            kind: RuntimeEffectArtifactCapabilityInstructionKind::CapabilityCall,
226            operation: operation.0.to_string(),
227            runtime_lowering: runtime_lowering(*operation)?,
228            arguments: arguments.iter().map(runtime_value_operand).collect(),
229        }),
230        IrInstructionKind::CapabilityAssign { operation, value } => {
231            Some(RuntimeEffectArtifactInstruction::CapabilityAssign {
232                kind: RuntimeEffectArtifactCapabilityInstructionKind::CapabilityAssign,
233                operation: operation.0.to_string(),
234                runtime_lowering: runtime_lowering(*operation)?,
235                value: runtime_value_operand(value),
236            })
237        }
238        _ => crate::runtime_computed_artifact::runtime_instruction(instruction)
239            .map(RuntimeEffectArtifactInstruction::Evaluate),
240    }
241}
242
243fn capability_operations(
244    operations: &[crate::CapabilityOperationId],
245) -> Option<Vec<RuntimeEffectArtifactCapabilityOperation>> {
246    operations
247        .iter()
248        .map(|operation| {
249            Some(RuntimeEffectArtifactCapabilityOperation {
250                operation: operation.0.to_string(),
251                runtime_lowering: runtime_lowering(*operation)?,
252            })
253        })
254        .collect()
255}
256
257fn runtime_lowering(operation: crate::CapabilityOperationId) -> Option<String> {
258    EFFECT_CAPABILITY_REGISTRY
259        .operation(operation)
260        .map(|definition| definition.runtime_lowering.0.to_string())
261}
262
263fn runtime_value_operand(value: &IrValueId) -> RuntimeComputedArtifactOperand {
264    RuntimeComputedArtifactOperand::Value {
265        value: value.as_str().to_string(),
266    }
267}
268
269fn semantic_ids(ids: &[crate::SemanticId]) -> Vec<String> {
270    ids.iter().map(ToString::to_string).collect()
271}
272
273fn prerequisite_batches(
274    batches: &[crate::EffectComputedPrerequisiteBatch],
275) -> Vec<RuntimeEffectArtifactPrerequisiteBatch> {
276    batches
277        .iter()
278        .map(|batch| RuntimeEffectArtifactPrerequisiteBatch {
279            source_batch_index: batch.source_batch_index,
280            computed: semantic_ids(&batch.computed),
281        })
282        .collect()
283}
284
285const fn execution_policy(policy: EffectExecutionPolicy) -> RuntimeEffectArtifactExecutionPolicy {
286    match policy {
287        EffectExecutionPolicy::AfterInitialRenderAndCompletedActionBatch => {
288            RuntimeEffectArtifactExecutionPolicy::AfterInitialRenderAndCompletedActionBatch
289        }
290    }
291}
292
293const fn render_boundary(boundary: EffectRenderBoundary) -> RuntimeEffectArtifactRenderBoundary {
294    match boundary {
295        EffectRenderBoundary::AfterInitialRender => {
296            RuntimeEffectArtifactRenderBoundary::AfterInitialRender
297        }
298    }
299}
300
301const fn execution_boundary(boundary: ExecutionBoundary) -> RuntimeEffectArtifactExecutionBoundary {
302    match boundary {
303        ExecutionBoundary::Client => RuntimeEffectArtifactExecutionBoundary::Client,
304        ExecutionBoundary::Server => RuntimeEffectArtifactExecutionBoundary::Server,
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use crate::{
311        build_application_semantic_model, build_runtime_effect_artifact, lower_components_to_ir,
312        optimize_effect_ir, runtime_effect_artifact_json,
313        RuntimeEffectArtifactCapabilityInstructionKind, RuntimeEffectArtifactInstruction,
314        RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
315    };
316
317    #[test]
318    #[allow(clippy::too_many_lines)]
319    fn emits_deterministic_effect_programs_from_registry_and_optimized_ir() {
320        let parsed = presolve_parser::parse_file(
321            "src/RuntimeEffectArtifact.tsx",
322            r#"
323@component("x-runtime-effect-artifact")
324class RuntimeEffectArtifact extends Component {
325  count = state(1);
326  title = state("Presolve");
327
328  @computed()
329  get doubled() { return this.count * 2; }
330
331  @action()
332  increment() { this.count += 1; }
333
334  @effect()
335  report() {
336    console.log(1 + 2, this.doubled);
337    document.title = this.title;
338    localStorage.setItem("count", "updated");
339  }
340
341  @action()
342  invalidAction() { this.count += 1; }
343
344  @effect()
345  invalid() { this.invalidAction(); }
346
347  render() { return <p />; }
348}
349"#,
350        );
351        let model = build_application_semantic_model(&parsed);
352        let component = &model.components[0];
353        let report = component.id.effect("report");
354        let doubled = component.id.computed("doubled");
355        let increment = component.id.action_batch("increment");
356        let invalid_action = component.id.action_batch("invalidAction");
357        let artifact = build_runtime_effect_artifact(
358            &model,
359            &optimize_effect_ir(&lower_components_to_ir(&model)).output,
360        );
361        let effect = artifact.effects.first().expect("report artifact");
362        let operations = effect
363            .program
364            .instructions
365            .iter()
366            .filter_map(|instruction| match instruction {
367                RuntimeEffectArtifactInstruction::CapabilityCall { operation, .. }
368                | RuntimeEffectArtifactInstruction::CapabilityAssign { operation, .. } => {
369                    Some(operation.as_str())
370                }
371                RuntimeEffectArtifactInstruction::Evaluate(_) => None,
372            })
373            .collect::<Vec<_>>();
374
375        assert_eq!(
376            artifact.schema_version,
377            RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION
378        );
379        assert_eq!(artifact.effects.len(), 1);
380        assert_eq!(effect.effect, report.as_str());
381        assert_eq!(effect.execution_function, report.as_str());
382        assert_eq!(
383            effect
384                .initial_trigger
385                .as_ref()
386                .expect("initial trigger")
387                .required_computed,
388            vec![doubled.to_string()]
389        );
390        assert_eq!(effect.action_batch_triggers.len(), 2);
391        assert_eq!(
392            effect.action_batch_triggers[0].action_batch,
393            increment.as_str()
394        );
395        assert_eq!(
396            effect.action_batch_triggers[0].required_computed,
397            vec![doubled.to_string()]
398        );
399        assert_eq!(
400            effect.action_batch_triggers[1].action_batch,
401            invalid_action.as_str()
402        );
403        assert_eq!(
404            effect.action_batch_triggers[1].required_computed,
405            vec![doubled.to_string()]
406        );
407        assert_eq!(
408            effect
409                .capability_operations
410                .iter()
411                .map(|operation| operation.operation.as_str())
412                .collect::<Vec<_>>(),
413            vec![
414                "builtin.browser.console.log",
415                "builtin.browser.document.title.assign",
416                "builtin.browser.local_storage.set_item",
417            ]
418        );
419        assert!(effect
420            .capability_operations
421            .iter()
422            .all(|operation| operation.operation == operation.runtime_lowering));
423        assert_eq!(
424            operations,
425            vec![
426                "builtin.browser.console.log",
427                "builtin.browser.document.title.assign",
428                "builtin.browser.local_storage.set_item",
429            ]
430        );
431        assert!(effect.program.instructions.iter().any(|instruction| {
432            matches!(
433                instruction,
434                RuntimeEffectArtifactInstruction::CapabilityCall {
435                    kind: RuntimeEffectArtifactCapabilityInstructionKind::CapabilityCall,
436                    runtime_lowering,
437                    ..
438                } if runtime_lowering == "builtin.browser.console.log"
439            )
440        }));
441
442        let first = runtime_effect_artifact_json(&artifact);
443        let second = runtime_effect_artifact_json(&build_runtime_effect_artifact(
444            &model,
445            &optimize_effect_ir(&lower_components_to_ir(&model)).output,
446        ));
447        assert_eq!(first, second);
448        let json: serde_json::Value = serde_json::from_str(&first).expect("artifact JSON");
449        assert_eq!(json["schema_version"], 1);
450        assert_eq!(
451            json["effects"][0]["program"]["instructions"][2]["kind"],
452            "capability-call"
453        );
454        assert!(json["effects"][0].get("provenance").is_none());
455        assert!(json["effects"][0]["program"]["instructions"]
456            .as_array()
457            .expect("instructions")
458            .iter()
459            .all(|instruction| instruction.get("static_path").is_none()));
460    }
461}