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_instance_registry, build_runtime_effect_registry,
10    build_runtime_effect_structural_template_registry, ApplicationSemanticModel,
11    EffectExecutionPolicy, EffectRenderBoundary, ExecutionBoundary, IntermediateRepresentation,
12    IrInstruction, IrInstructionKind, IrValueId, RuntimeEffectRecord, EFFECT_CAPABILITY_REGISTRY,
13};
14
15pub const RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION: u32 = 7;
16
17/// Versioned compiler-generated runtime metadata and effect programs.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct RuntimeEffectArtifact {
20    pub schema_version: u32,
21    pub effects: Vec<RuntimeEffectArtifactEffect>,
22    pub instances: Vec<RuntimeEffectArtifactInstance>,
23    pub structural_templates: Vec<RuntimeEffectArtifactStructuralTemplate>,
24}
25
26/// One instance-qualified V2 effect ownership record. Programs remain on the
27/// declaration record until instance execution context is available.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct RuntimeEffectArtifactInstance {
30    pub effect_instance: String,
31    pub effect: String,
32    pub component_instance: String,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub parent_instance: Option<String>,
35    pub depth: usize,
36    pub declaration_order: u32,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40pub struct RuntimeEffectArtifactStructuralTemplate {
41    /// Template-qualified effect-instance identity. The structural runtime
42    /// replaces only the template-instance prefix with its opaque occurrence
43    /// identity before it can activate this record.
44    pub effect_instance: String,
45    pub template_instance: String,
46    pub effect: String,
47    pub component: String,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub parent_instance: Option<String>,
50    pub structural_region: String,
51    pub depth: usize,
52    pub declaration_order: u32,
53}
54
55/// Runtime metadata and executable capability program for one lowered effect.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57pub struct RuntimeEffectArtifactEffect {
58    pub effect: String,
59    pub execution_function: String,
60    pub initial_trigger_policy: RuntimeEffectArtifactExecutionPolicy,
61    pub initial_trigger: Option<RuntimeEffectArtifactInitialTrigger>,
62    pub action_batch_triggers: Vec<RuntimeEffectArtifactActionTrigger>,
63    pub capability_operations: Vec<RuntimeEffectArtifactCapabilityOperation>,
64    pub execution_boundary: RuntimeEffectArtifactExecutionBoundary,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub declaration_order: Option<u32>,
67    /// Whether this V2 field effect is eligible for its single post-resume run.
68    pub run_on_resume: bool,
69    pub program: RuntimeEffectArtifactProgram,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub cleanup_program: Option<RuntimeEffectArtifactProgram>,
72}
73
74/// Explicit initial-render trigger metadata for one emitted effect.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
76pub struct RuntimeEffectArtifactInitialTrigger {
77    pub render_boundary: RuntimeEffectArtifactRenderBoundary,
78    pub required_computed: Vec<String>,
79    pub prerequisite_batches: Vec<RuntimeEffectArtifactPrerequisiteBatch>,
80    pub effect_batch_index: u32,
81}
82
83/// Explicit completed-action-batch trigger metadata for one emitted effect.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
85pub struct RuntimeEffectArtifactActionTrigger {
86    pub action_batch: String,
87    pub matched_states: Vec<String>,
88    pub required_computed: Vec<String>,
89    pub prerequisite_batches: Vec<RuntimeEffectArtifactPrerequisiteBatch>,
90    pub effect_batch_index: u32,
91}
92
93/// A filtered F9 computed-prerequisite batch available to an effect trigger.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
95pub struct RuntimeEffectArtifactPrerequisiteBatch {
96    pub source_batch_index: u32,
97    pub computed: Vec<String>,
98}
99
100/// Stable compiler operation and runtime-lowering identities.
101#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
102pub struct RuntimeEffectArtifactCapabilityOperation {
103    pub operation: String,
104    pub runtime_lowering: String,
105}
106
107/// One complete, ordered F10/F11 effect function program.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109pub struct RuntimeEffectArtifactProgram {
110    pub instructions: Vec<RuntimeEffectArtifactInstruction>,
111}
112
113/// An effect program instruction. Pure operand instructions reuse the existing
114/// computed-artifact representation; capability operations are explicit roots.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
116#[serde(untagged)]
117pub enum RuntimeEffectArtifactInstruction {
118    Evaluate(RuntimeComputedArtifactInstruction),
119    CapabilityCall {
120        #[serde(rename = "kind")]
121        kind: RuntimeEffectArtifactCapabilityInstructionKind,
122        operation: String,
123        runtime_lowering: String,
124        arguments: Vec<RuntimeComputedArtifactOperand>,
125    },
126    CapabilityAssign {
127        #[serde(rename = "kind")]
128        kind: RuntimeEffectArtifactCapabilityInstructionKind,
129        operation: String,
130        runtime_lowering: String,
131        value: RuntimeComputedArtifactOperand,
132    },
133}
134
135/// Distinguishes the two observable capability instruction forms.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
137#[serde(rename_all = "kebab-case")]
138pub enum RuntimeEffectArtifactCapabilityInstructionKind {
139    CapabilityCall,
140    CapabilityAssign,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
144#[serde(rename_all = "kebab-case")]
145pub enum RuntimeEffectArtifactExecutionPolicy {
146    AfterInitialRenderAndCompletedActionBatch,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
150#[serde(rename_all = "kebab-case")]
151pub enum RuntimeEffectArtifactRenderBoundary {
152    AfterInitialRender,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
156#[serde(rename_all = "kebab-case")]
157pub enum RuntimeEffectArtifactExecutionBoundary {
158    Client,
159    Server,
160}
161
162/// Emit a deterministic effect artifact from F9 plans, F10/F11 IR, and F12 records.
163///
164/// The artifact contains canonical semantic and registry identities only. It
165/// neither retains raw capability paths nor performs dependency discovery or
166/// effect execution.
167#[must_use]
168pub fn build_runtime_effect_artifact(
169    model: &ApplicationSemanticModel,
170    ir: &IntermediateRepresentation,
171) -> RuntimeEffectArtifact {
172    let registry = build_runtime_effect_registry(model, ir);
173    let programs = effect_programs(ir);
174    let effects = registry
175        .records
176        .values()
177        .filter_map(|record| {
178            let programs = programs.get(&record.effect)?;
179            runtime_effect(record, programs.main.clone(), programs.cleanup.clone())
180        })
181        .collect();
182    RuntimeEffectArtifact {
183        schema_version: RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
184        effects,
185        instances: build_runtime_effect_instance_registry(model)
186            .records
187            .into_iter()
188            .map(|record| RuntimeEffectArtifactInstance {
189                effect_instance: record.id.as_str().to_owned(),
190                effect: record.effect.to_string(),
191                component_instance: record.component_instance.as_str().to_owned(),
192                parent_instance: record
193                    .parent_instance
194                    .map(|parent| parent.as_str().to_owned()),
195                depth: record.depth,
196                declaration_order: record.declaration_order,
197            })
198            .collect(),
199        structural_templates: build_runtime_effect_structural_template_registry(model)
200            .into_iter()
201            .map(|record| RuntimeEffectArtifactStructuralTemplate {
202                effect_instance: crate::EffectInstanceId::for_component_instance(
203                    &record.template_instance,
204                    &record.effect,
205                )
206                .as_str()
207                .to_owned(),
208                template_instance: record.template_instance.as_str().to_owned(),
209                effect: record.effect.to_string(),
210                component: model
211                    .component_instance_plan
212                    .instances
213                    .get(&record.template_instance)
214                    .expect("structural effect template belongs to a planned component instance")
215                    .component
216                    .to_string(),
217                parent_instance: record
218                    .parent_instance
219                    .map(|value| value.as_str().to_owned()),
220                structural_region: record.structural_region.as_str().to_owned(),
221                depth: record.depth,
222                declaration_order: record.declaration_order,
223            })
224            .collect(),
225    }
226}
227
228/// Serialize emitted effect runtime metadata as deterministic, pretty JSON.
229///
230/// # Panics
231///
232/// Panics when compiler-owned effect runtime metadata cannot serialize.
233#[must_use]
234pub fn runtime_effect_artifact_json(artifact: &RuntimeEffectArtifact) -> String {
235    serde_json::to_string_pretty(artifact).expect("effect runtime artifact should serialize") + "\n"
236}
237
238fn runtime_effect(
239    record: &RuntimeEffectRecord,
240    program: RuntimeEffectArtifactProgram,
241    cleanup_program: Option<RuntimeEffectArtifactProgram>,
242) -> Option<RuntimeEffectArtifactEffect> {
243    Some(RuntimeEffectArtifactEffect {
244        effect: record.effect.as_str().to_string(),
245        execution_function: record.execution_function.as_str().to_string(),
246        initial_trigger_policy: execution_policy(record.initial_trigger_policy),
247        initial_trigger: record.initial_trigger.as_ref().map(|trigger| {
248            RuntimeEffectArtifactInitialTrigger {
249                render_boundary: render_boundary(trigger.render_boundary),
250                required_computed: semantic_ids(&trigger.required_computed),
251                prerequisite_batches: prerequisite_batches(&trigger.prerequisite_batches),
252                effect_batch_index: trigger.effect_batch_index,
253            }
254        }),
255        action_batch_triggers: record
256            .action_batch_triggers
257            .iter()
258            .map(|trigger| RuntimeEffectArtifactActionTrigger {
259                action_batch: trigger.action_batch.as_str().to_string(),
260                matched_states: semantic_ids(&trigger.matched_states),
261                required_computed: semantic_ids(&trigger.required_computed),
262                prerequisite_batches: prerequisite_batches(&trigger.prerequisite_batches),
263                effect_batch_index: trigger.effect_batch_index,
264            })
265            .collect(),
266        capability_operations: capability_operations(&record.capability_operations)?,
267        execution_boundary: execution_boundary(record.execution_boundary),
268        declaration_order: record.declaration_order,
269        run_on_resume: record.run_on_resume,
270        program,
271        cleanup_program,
272    })
273}
274
275#[derive(Debug, Clone)]
276struct EffectPrograms {
277    main: RuntimeEffectArtifactProgram,
278    cleanup: Option<RuntimeEffectArtifactProgram>,
279}
280
281fn effect_programs(ir: &IntermediateRepresentation) -> BTreeMap<crate::SemanticId, EffectPrograms> {
282    ir.modules
283        .iter()
284        .flat_map(|module| {
285            module.effect_executions.iter().filter_map(|execution| {
286                let function = module
287                    .functions
288                    .iter()
289                    .find(|function| function.id == execution.function)?;
290                let program = runtime_effect_program(function)?;
291                let cleanup = if let Some(cleanup_id) = &execution.cleanup_function {
292                    let function = module
293                        .functions
294                        .iter()
295                        .find(|function| function.id == *cleanup_id)?;
296                    Some(runtime_effect_program(function)?)
297                } else {
298                    None
299                };
300                Some((
301                    execution.effect.clone(),
302                    EffectPrograms {
303                        main: program,
304                        cleanup,
305                    },
306                ))
307            })
308        })
309        .collect()
310}
311
312fn runtime_effect_program(function: &crate::IrFunction) -> Option<RuntimeEffectArtifactProgram> {
313    let instructions = function
314        .blocks
315        .iter()
316        .flat_map(|block| block.instructions.iter())
317        .map(runtime_instruction)
318        .collect::<Option<Vec<_>>>()?;
319    Some(RuntimeEffectArtifactProgram { instructions })
320}
321
322fn runtime_instruction(instruction: &IrInstruction) -> Option<RuntimeEffectArtifactInstruction> {
323    match &instruction.kind {
324        IrInstructionKind::CapabilityCall {
325            operation,
326            arguments,
327        } => Some(RuntimeEffectArtifactInstruction::CapabilityCall {
328            kind: RuntimeEffectArtifactCapabilityInstructionKind::CapabilityCall,
329            operation: operation.0.to_string(),
330            runtime_lowering: runtime_lowering(*operation)?,
331            arguments: arguments.iter().map(runtime_value_operand).collect(),
332        }),
333        IrInstructionKind::CapabilityAssign { operation, value } => {
334            Some(RuntimeEffectArtifactInstruction::CapabilityAssign {
335                kind: RuntimeEffectArtifactCapabilityInstructionKind::CapabilityAssign,
336                operation: operation.0.to_string(),
337                runtime_lowering: runtime_lowering(*operation)?,
338                value: runtime_value_operand(value),
339            })
340        }
341        _ => crate::runtime_computed_artifact::runtime_instruction(instruction)
342            .map(RuntimeEffectArtifactInstruction::Evaluate),
343    }
344}
345
346fn capability_operations(
347    operations: &[crate::CapabilityOperationId],
348) -> Option<Vec<RuntimeEffectArtifactCapabilityOperation>> {
349    operations
350        .iter()
351        .map(|operation| {
352            Some(RuntimeEffectArtifactCapabilityOperation {
353                operation: operation.0.to_string(),
354                runtime_lowering: runtime_lowering(*operation)?,
355            })
356        })
357        .collect()
358}
359
360fn runtime_lowering(operation: crate::CapabilityOperationId) -> Option<String> {
361    EFFECT_CAPABILITY_REGISTRY
362        .operation(operation)
363        .map(|definition| definition.runtime_lowering.0.to_string())
364}
365
366fn runtime_value_operand(value: &IrValueId) -> RuntimeComputedArtifactOperand {
367    RuntimeComputedArtifactOperand::Value {
368        value: value.as_str().to_string(),
369    }
370}
371
372fn semantic_ids(ids: &[crate::SemanticId]) -> Vec<String> {
373    ids.iter().map(ToString::to_string).collect()
374}
375
376fn prerequisite_batches(
377    batches: &[crate::EffectComputedPrerequisiteBatch],
378) -> Vec<RuntimeEffectArtifactPrerequisiteBatch> {
379    batches
380        .iter()
381        .map(|batch| RuntimeEffectArtifactPrerequisiteBatch {
382            source_batch_index: batch.source_batch_index,
383            computed: semantic_ids(&batch.computed),
384        })
385        .collect()
386}
387
388const fn execution_policy(policy: EffectExecutionPolicy) -> RuntimeEffectArtifactExecutionPolicy {
389    match policy {
390        EffectExecutionPolicy::AfterInitialRenderAndCompletedActionBatch => {
391            RuntimeEffectArtifactExecutionPolicy::AfterInitialRenderAndCompletedActionBatch
392        }
393    }
394}
395
396const fn render_boundary(boundary: EffectRenderBoundary) -> RuntimeEffectArtifactRenderBoundary {
397    match boundary {
398        EffectRenderBoundary::AfterInitialRender => {
399            RuntimeEffectArtifactRenderBoundary::AfterInitialRender
400        }
401    }
402}
403
404const fn execution_boundary(boundary: ExecutionBoundary) -> RuntimeEffectArtifactExecutionBoundary {
405    match boundary {
406        ExecutionBoundary::Client => RuntimeEffectArtifactExecutionBoundary::Client,
407        ExecutionBoundary::Server => RuntimeEffectArtifactExecutionBoundary::Server,
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use crate::{
414        build_application_semantic_model, build_runtime_effect_artifact, lower_components_to_ir,
415        optimize_effect_ir, runtime_effect_artifact_json,
416        RuntimeEffectArtifactCapabilityInstructionKind, RuntimeEffectArtifactInstruction,
417        RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION,
418    };
419
420    #[test]
421    #[allow(clippy::too_many_lines)]
422    fn emits_deterministic_effect_programs_from_registry_and_optimized_ir() {
423        let parsed = presolve_parser::parse_file(
424            "src/RuntimeEffectArtifact.tsx",
425            r#"
426@component("x-runtime-effect-artifact")
427class RuntimeEffectArtifact extends Component {
428  count = state(1);
429  title = state("Presolve");
430
431  @computed()
432  get doubled() { return this.count * 2; }
433
434  @action()
435  increment() { this.count += 1; }
436
437  @effect()
438  report() {
439    console.log(1 + 2, this.doubled);
440    document.title = this.title;
441    localStorage.setItem("count", "updated");
442  }
443
444  @action()
445  invalidAction() { this.count += 1; }
446
447  @effect()
448  invalid() { this.invalidAction(); }
449
450  render() { return <p />; }
451}
452"#,
453        );
454        let model = build_application_semantic_model(&parsed);
455        let component = &model.components[0];
456        let report = component.id.effect("report");
457        let doubled = component.id.computed("doubled");
458        let increment = component.id.action_batch("increment");
459        let invalid_action = component.id.action_batch("invalidAction");
460        let artifact = build_runtime_effect_artifact(
461            &model,
462            &optimize_effect_ir(&lower_components_to_ir(&model)).output,
463        );
464        let effect = artifact.effects.first().expect("report artifact");
465        let operations = effect
466            .program
467            .instructions
468            .iter()
469            .filter_map(|instruction| match instruction {
470                RuntimeEffectArtifactInstruction::CapabilityCall { operation, .. }
471                | RuntimeEffectArtifactInstruction::CapabilityAssign { operation, .. } => {
472                    Some(operation.as_str())
473                }
474                RuntimeEffectArtifactInstruction::Evaluate(_) => None,
475            })
476            .collect::<Vec<_>>();
477
478        assert_eq!(
479            artifact.schema_version,
480            RUNTIME_EFFECT_ARTIFACT_SCHEMA_VERSION
481        );
482        assert_eq!(artifact.effects.len(), 1);
483        assert!(artifact.instances.is_empty());
484        assert!(effect.cleanup_program.is_none());
485        assert!(!effect.run_on_resume);
486        assert_eq!(effect.effect, report.as_str());
487        assert_eq!(effect.execution_function, report.as_str());
488        assert_eq!(
489            effect
490                .initial_trigger
491                .as_ref()
492                .expect("initial trigger")
493                .required_computed,
494            vec![doubled.to_string()]
495        );
496        assert_eq!(effect.action_batch_triggers.len(), 2);
497        assert_eq!(
498            effect.action_batch_triggers[0].action_batch,
499            increment.as_str()
500        );
501        assert_eq!(
502            effect.action_batch_triggers[0].required_computed,
503            vec![doubled.to_string()]
504        );
505        assert_eq!(
506            effect.action_batch_triggers[1].action_batch,
507            invalid_action.as_str()
508        );
509        assert_eq!(
510            effect.action_batch_triggers[1].required_computed,
511            vec![doubled.to_string()]
512        );
513        assert_eq!(
514            effect
515                .capability_operations
516                .iter()
517                .map(|operation| operation.operation.as_str())
518                .collect::<Vec<_>>(),
519            vec![
520                "builtin.browser.console.log",
521                "builtin.browser.document.title.assign",
522                "builtin.browser.local_storage.set_item",
523            ]
524        );
525        assert!(effect
526            .capability_operations
527            .iter()
528            .all(|operation| operation.operation == operation.runtime_lowering));
529        assert_eq!(
530            operations,
531            vec![
532                "builtin.browser.console.log",
533                "builtin.browser.document.title.assign",
534                "builtin.browser.local_storage.set_item",
535            ]
536        );
537        assert!(effect.program.instructions.iter().any(|instruction| {
538            matches!(
539                instruction,
540                RuntimeEffectArtifactInstruction::CapabilityCall {
541                    kind: RuntimeEffectArtifactCapabilityInstructionKind::CapabilityCall,
542                    runtime_lowering,
543                    ..
544                } if runtime_lowering == "builtin.browser.console.log"
545            )
546        }));
547
548        let first = runtime_effect_artifact_json(&artifact);
549        let second = runtime_effect_artifact_json(&build_runtime_effect_artifact(
550            &model,
551            &optimize_effect_ir(&lower_components_to_ir(&model)).output,
552        ));
553        assert_eq!(first, second);
554        let json: serde_json::Value = serde_json::from_str(&first).expect("artifact JSON");
555        assert_eq!(json["schema_version"], 7);
556        assert_eq!(
557            json["effects"][0]["program"]["instructions"][2]["kind"],
558            "capability-call"
559        );
560        assert!(json["effects"][0].get("provenance").is_none());
561        assert!(json["effects"][0]["program"]["instructions"]
562            .as_array()
563            .expect("instructions")
564            .iter()
565            .all(|instruction| instruction.get("static_path").is_none()));
566    }
567
568    #[test]
569    fn emits_distinct_v2_effect_instances_for_repeated_component_instances() {
570        let parsed = presolve_parser::parse_file(
571            "src/RepeatedEffectArtifact.tsx",
572            r#"
573@component("x-card") class Card extends Component {
574  @effect() report() { document.title = "card"; }
575  render() { return <article />; }
576}
577@component("x-page") class Page extends Component {
578  render() { return <main><Card /><Card /></main>; }
579}
580"#,
581        );
582        let mut model = build_application_semantic_model(&parsed);
583        let effect_id = model.components[0].id.effect("report");
584        let effect = model.effects.get_mut(&effect_id).expect("Card effect");
585        effect.declaration = crate::EffectDeclaration::V2Field;
586        effect.declaration_order = Some(0);
587        let artifact = build_runtime_effect_artifact(
588            &model,
589            &optimize_effect_ir(&lower_components_to_ir(&model)).output,
590        );
591
592        assert_eq!(artifact.effects.len(), 1);
593        assert_eq!(artifact.instances.len(), 2);
594        assert!(artifact
595            .instances
596            .iter()
597            .all(|record| record.effect == effect_id.as_str()));
598        assert_ne!(
599            artifact.instances[0].effect_instance,
600            artifact.instances[1].effect_instance
601        );
602        assert!(artifact
603            .instances
604            .iter()
605            .all(|record| record.parent_instance.is_some()));
606        assert!(artifact.instances.iter().all(|record| record.depth == 1));
607    }
608}