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#[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#[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 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#[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 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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
95pub struct RuntimeEffectArtifactPrerequisiteBatch {
96 pub source_batch_index: u32,
97 pub computed: Vec<String>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
102pub struct RuntimeEffectArtifactCapabilityOperation {
103 pub operation: String,
104 pub runtime_lowering: String,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109pub struct RuntimeEffectArtifactProgram {
110 pub instructions: Vec<RuntimeEffectArtifactInstruction>,
111}
112
113#[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#[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#[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#[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}