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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct RuntimeEffectArtifact {
19 pub schema_version: u32,
20 pub effects: Vec<RuntimeEffectArtifactEffect>,
21}
22
23#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57pub struct RuntimeEffectArtifactPrerequisiteBatch {
58 pub source_batch_index: u32,
59 pub computed: Vec<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64pub struct RuntimeEffectArtifactCapabilityOperation {
65 pub operation: String,
66 pub runtime_lowering: String,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71pub struct RuntimeEffectArtifactProgram {
72 pub instructions: Vec<RuntimeEffectArtifactInstruction>,
73}
74
75#[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#[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#[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#[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}