Skip to main content

presolve_compiler/
runtime_form_artifact.rs

1//! I15 public compiler-generated Forms runtime artifact.
2//!
3//! This module is deliberately a projection of immutable compiler products.
4//! It does not inspect source syntax or create runtime state.
5
6use std::collections::BTreeSet;
7
8use serde::Serialize;
9
10use crate::{
11    semantic_type_text, ApplicationSemanticModel, FormControlCompatibility, FormIrOperation,
12    RUNTIME_FORM_REGISTRY_VERSION,
13};
14
15pub const RUNTIME_FORM_ARTIFACT_SCHEMA_VERSION: u32 = 2;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18pub struct RuntimeFormsArtifact {
19    pub schema_version: u32,
20    pub registry_version: u32,
21    pub forms: Vec<RuntimeFormsArtifactForm>,
22    pub instances: Vec<RuntimeFormsArtifactInstance>,
23    /// Instance-qualified executable submit-host records. These are the only
24    /// runtime authority for locating and handling a native submit event.
25    pub hosts: Vec<RuntimeFormsArtifactHost>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29pub struct RuntimeFormsArtifactForm {
30    pub id: String,
31    /// Non-authoritative authored name for developer tooling only.
32    pub debug_name: String,
33    pub fields: Vec<RuntimeFormsArtifactField>,
34    pub bindings: Vec<RuntimeFormsArtifactBinding>,
35    pub validation_rules: Vec<RuntimeFormsArtifactRule>,
36    pub validation_dependencies: Vec<RuntimeFormsArtifactDependency>,
37    pub submission: Option<RuntimeFormsArtifactSubmission>,
38    pub serialization: RuntimeFormsArtifactSerialization,
39    pub reset: RuntimeFormsArtifactReset,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43pub struct RuntimeFormsArtifactField {
44    pub id: String,
45    /// Non-authoritative authored name for developer tooling only.
46    pub debug_name: String,
47    /// Exact compiler-issued serialized leaf path.
48    pub path: Vec<String>,
49    pub semantic_type: String,
50    pub initial_value: crate::SerializableValue,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54pub struct RuntimeFormsArtifactBinding {
55    pub id: String,
56    pub control_anchor: String,
57    pub field: String,
58    pub channel: String,
59    pub normalization: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
63pub struct RuntimeFormsArtifactRule {
64    pub id: String,
65    pub target_field: String,
66    pub kind: String,
67    pub argument: String,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub dependency: Option<String>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73pub struct RuntimeFormsArtifactDependency {
74    pub id: String,
75    pub source_field: String,
76    pub target_field: String,
77    pub rule: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81pub struct RuntimeFormsArtifactSubmission {
82    pub plan: String,
83    pub action_batch: String,
84    pub validation_rules: Vec<String>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88pub struct RuntimeFormsArtifactSerialization {
89    pub plan: String,
90    pub format: String,
91    pub fields: Vec<String>,
92    pub status: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
96pub struct RuntimeFormsArtifactReset {
97    pub plan: String,
98    pub fields: Vec<String>,
99    pub schedule_validation: bool,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
103pub struct RuntimeFormsArtifactInstance {
104    pub id: String,
105    pub form: String,
106    pub component_instance: String,
107    pub field_slots: Vec<RuntimeFormsArtifactFieldSlots>,
108    pub aggregate_validation_slot: String,
109    pub submission_slot: String,
110    pub programs: RuntimeFormsArtifactPrograms,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114pub struct RuntimeFormsArtifactHost {
115    pub id: String,
116    pub host_anchor: String,
117    pub form: String,
118    pub form_instance: String,
119    pub submission_plan: String,
120    pub submit_action: String,
121    pub action_batch: String,
122    pub serialization_plan: String,
123    pub event: String,
124    pub prevent_default: bool,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128pub struct RuntimeFormsArtifactFieldSlots {
129    pub field: String,
130    pub value: String,
131    pub dirty: String,
132    pub touched: String,
133    pub validation: String,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
137pub struct RuntimeFormsArtifactPrograms {
138    pub initialize: Vec<String>,
139    pub input: Vec<RuntimeFormsArtifactFieldProgram>,
140    pub blur: Vec<RuntimeFormsArtifactFieldProgram>,
141    pub reset: Vec<String>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
145pub struct RuntimeFormsArtifactFieldProgram {
146    pub field: String,
147    pub operations: Vec<String>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct RuntimeFormsArtifactValidation {
152    pub diagnostics: Vec<String>,
153    pub is_valid: bool,
154}
155
156/// Project every executable I0-I14 Forms product into the versioned public
157/// artifact. All identity-bearing fields are canonical IDs; authored names are
158/// kept in explicit non-authoritative debug fields only.
159///
160/// # Panics
161///
162/// Panics when a prior I10 or I11 product failed to provide its mandatory
163/// per-Form plan. That is an internal staged-compiler invariant violation.
164#[must_use]
165#[allow(clippy::too_many_lines)]
166pub fn build_runtime_forms_artifact(model: &ApplicationSemanticModel) -> RuntimeFormsArtifact {
167    let forms = model
168        .forms
169        .values()
170        .map(|form| {
171            let fields = model
172                .form_fields
173                .values()
174                .filter(|field| field.owner_form == form.id)
175                .map(|field| RuntimeFormsArtifactField {
176                    id: field.id.to_string(),
177                    debug_name: field.name.clone(),
178                    path: field.path.clone(),
179                    semantic_type: semantic_type_text(&field.semantic_type),
180                    initial_value: field.initial_value.clone(),
181                })
182                .collect::<Vec<_>>();
183            let bindings = model
184                .form_field_bindings
185                .values()
186                .filter(|binding| binding.form == form.id)
187                .map(|binding| RuntimeFormsArtifactBinding {
188                    id: binding.id.to_string(),
189                    control_anchor: binding.control_entity.to_string(),
190                    field: binding.field.to_string(),
191                    channel: format!("{:?}", binding.channel),
192                    normalization: normalization_name(binding.compatibility),
193                })
194                .collect();
195            let validation_rules = model
196                .validation_rules
197                .values()
198                .filter(|rule| rule.owner_form == form.id)
199                .map(|rule| RuntimeFormsArtifactRule {
200                    id: rule.id.to_string(),
201                    target_field: rule.target_field.to_string(),
202                    kind: format!("{:?}", rule.kind),
203                    argument: format!("{:?}", rule.argument),
204                    dependency: rule.dependency.as_ref().map(ToString::to_string),
205                })
206                .collect();
207            let validation_dependencies = model
208                .validation_dependency_plans
209                .dependencies
210                .values()
211                .filter(|dependency| dependency.form == form.id)
212                .map(|dependency| RuntimeFormsArtifactDependency {
213                    id: dependency.id.to_string(),
214                    source_field: dependency.source_field.to_string(),
215                    target_field: dependency.target_field.to_string(),
216                    rule: dependency.dependent_rule.to_string(),
217                })
218                .collect();
219            let submission =
220                model
221                    .submissions
222                    .plan(&form.id)
223                    .map(|plan| RuntimeFormsArtifactSubmission {
224                        plan: plan.id.as_str().to_string(),
225                        action_batch: plan.action_batch.to_string(),
226                        validation_rules: plan
227                            .validation_rules
228                            .iter()
229                            .map(ToString::to_string)
230                            .collect(),
231                    });
232            let serialization = model
233                .serialization
234                .plans
235                .get(&crate::SerializationPlanId::for_form(&form.id))
236                .expect("I10 creates one serialization plan per valid Form");
237            let reset = model
238                .reset
239                .plans
240                .get(&crate::ResetPlanId::for_form(&form.id))
241                .expect("I11 creates one reset plan per valid Form");
242            RuntimeFormsArtifactForm {
243                id: form.id.to_string(),
244                debug_name: form.name.clone(),
245                fields,
246                bindings,
247                validation_rules,
248                validation_dependencies,
249                submission,
250                serialization: RuntimeFormsArtifactSerialization {
251                    plan: serialization.id.as_str().to_string(),
252                    format: format!("{:?}", serialization.format),
253                    fields: serialization
254                        .fields
255                        .iter()
256                        .map(|field| field.field.to_string())
257                        .collect(),
258                    status: format!("{:?}", serialization.status),
259                },
260                reset: RuntimeFormsArtifactReset {
261                    plan: reset.id.as_str().to_string(),
262                    fields: reset
263                        .operations
264                        .iter()
265                        .map(|operation| operation.field.to_string())
266                        .collect(),
267                    schedule_validation: reset.schedule_validation,
268                },
269            }
270        })
271        .collect();
272    let instances = model
273        .optimized_form_ir
274        .optimized
275        .instances
276        .values()
277        .map(|instance| RuntimeFormsArtifactInstance {
278            id: instance.id.to_string(),
279            form: instance.form.to_string(),
280            component_instance: instance.component_instance.to_string(),
281            field_slots: instance
282                .storage
283                .value
284                .iter()
285                .map(|(field, value)| RuntimeFormsArtifactFieldSlots {
286                    field: field.to_string(),
287                    value: value.as_str().to_string(),
288                    dirty: instance.storage.dirty[field].as_str().to_string(),
289                    touched: instance.storage.touched[field].as_str().to_string(),
290                    validation: instance.storage.validation[field].as_str().to_string(),
291                })
292                .collect(),
293            aggregate_validation_slot: instance.storage.aggregate.as_str().to_string(),
294            submission_slot: instance.storage.submission.as_str().to_string(),
295            programs: RuntimeFormsArtifactPrograms {
296                initialize: operations(&instance.initialize),
297                input: field_programs(&instance.input),
298                blur: field_programs(&instance.blur),
299                reset: operations(&instance.reset),
300            },
301        })
302        .collect();
303    let hosts = model
304        .submission_hosts
305        .values()
306        .flat_map(|host| {
307            model
308                .optimized_form_ir
309                .optimized
310                .instances
311                .values()
312                .filter(move |instance| {
313                    instance.form == host.form
314                        && model
315                            .component_instance_plan
316                            .instances
317                            .get(&instance.component_instance)
318                            .is_some_and(|component| component.component == host.component)
319                })
320                .map(move |instance| RuntimeFormsArtifactHost {
321                    id: host.id.to_string(),
322                    host_anchor: runtime_anchor_for_element(
323                        model,
324                        &host.owner_template,
325                        &host.owner_template_element,
326                    )
327                    .expect("valid host has exact template anchor"),
328                    form: host.form.to_string(),
329                    form_instance: instance.id.to_string(),
330                    submission_plan: host.submission_plan.as_str().to_string(),
331                    submit_action: host.submit_action.to_string(),
332                    action_batch: host.action_batch.to_string(),
333                    serialization_plan: host.serialization_plan.as_str().to_string(),
334                    event: host.event.to_string(),
335                    prevent_default: host.prevent_default,
336                })
337        })
338        .collect();
339    let artifact = RuntimeFormsArtifact {
340        schema_version: RUNTIME_FORM_ARTIFACT_SCHEMA_VERSION,
341        registry_version: RUNTIME_FORM_REGISTRY_VERSION,
342        forms,
343        instances,
344        hosts,
345    };
346    debug_assert!(validate_runtime_forms_artifact(&artifact).is_valid);
347    artifact
348}
349
350#[allow(clippy::items_after_statements)]
351fn runtime_anchor_for_element(
352    model: &ApplicationSemanticModel,
353    template_id: &crate::SemanticId,
354    element: &crate::SemanticId,
355) -> Option<String> {
356    let template = model
357        .templates
358        .iter()
359        .find(|template| &template.id == template_id)?;
360    fn find(
361        element: &crate::ElementNode,
362        template: &crate::TemplateNode,
363        path: &str,
364        target: &crate::SemanticId,
365    ) -> Option<String> {
366        if template.id.template_entity("element", path) == *target {
367            return Some(element.id.0.clone());
368        }
369        for (index, child) in element.children.iter().enumerate() {
370            if let crate::TemplateChild::Element(child) = child {
371                if let Some(anchor) = find(child, template, &format!("{path}.{index}"), target) {
372                    return Some(anchor);
373                }
374            }
375        }
376        None
377    }
378    template
379        .root
380        .as_ref()
381        .and_then(|root| find(root, template, "root", element))
382}
383
384#[must_use]
385pub fn validate_runtime_forms_artifact(
386    artifact: &RuntimeFormsArtifact,
387) -> RuntimeFormsArtifactValidation {
388    let mut diagnostics = Vec::new();
389    if artifact.schema_version != RUNTIME_FORM_ARTIFACT_SCHEMA_VERSION {
390        diagnostics.push("unsupported Forms artifact schema".to_string());
391    }
392    let forms = artifact
393        .forms
394        .iter()
395        .map(|form| form.id.as_str())
396        .collect::<BTreeSet<_>>();
397    if forms.len() != artifact.forms.len() {
398        diagnostics.push("duplicate Form definition".to_string());
399    }
400    let instances = artifact
401        .instances
402        .iter()
403        .map(|instance| instance.id.as_str())
404        .collect::<BTreeSet<_>>();
405    if instances.len() != artifact.instances.len() {
406        diagnostics.push("duplicate Form instance".to_string());
407    }
408    for instance in &artifact.instances {
409        if !forms.contains(instance.form.as_str()) {
410            diagnostics.push(format!("unknown Form for instance {}", instance.id));
411        }
412    }
413    for form in &artifact.forms {
414        let paths = form
415            .fields
416            .iter()
417            .map(|field| &field.path)
418            .collect::<Vec<_>>();
419        if paths.iter().any(|path| {
420            !(1..=16).contains(&path.len())
421                || path
422                    .iter()
423                    .any(|segment| !is_canonical_form_field_path_segment(segment))
424        }) {
425            diagnostics.push(format!("invalid Form Field path for {}", form.id));
426        }
427        if paths.iter().enumerate().any(|(index, path)| {
428            paths
429                .iter()
430                .skip(index + 1)
431                .any(|other| form_field_paths_conflict(path, other))
432        }) {
433            diagnostics.push(format!("conflicting Form Field path for {}", form.id));
434        }
435    }
436    for host in &artifact.hosts {
437        if !instances.contains(host.form_instance.as_str())
438            || !forms.contains(host.form.as_str())
439            || host.event != "submit"
440        {
441            diagnostics.push(format!("invalid submission host {}", host.id));
442        }
443    }
444    RuntimeFormsArtifactValidation {
445        is_valid: diagnostics.is_empty(),
446        diagnostics,
447    }
448}
449
450fn is_canonical_form_field_path_segment(segment: &str) -> bool {
451    let mut characters = segment.chars();
452    matches!(characters.next(), Some(character) if character == '_' || character.is_ascii_alphabetic())
453        && characters.all(|character| character == '_' || character.is_ascii_alphanumeric())
454}
455
456fn form_field_paths_conflict(left: &[String], right: &[String]) -> bool {
457    (left.len() <= right.len() && left.iter().zip(right).all(|(left, right)| left == right))
458        || (right.len() <= left.len() && right.iter().zip(left).all(|(left, right)| left == right))
459}
460
461#[must_use]
462/// # Panics
463///
464/// Panics only if an internal Forms artifact cannot serialize, which indicates
465/// a compiler implementation defect.
466pub fn runtime_forms_artifact_json(artifact: &RuntimeFormsArtifact) -> String {
467    serde_json::to_string_pretty(artifact).expect("Forms artifact serializes deterministically")
468        + "\n"
469}
470
471fn normalization_name(compatibility: FormControlCompatibility) -> String {
472    match compatibility {
473        FormControlCompatibility::Compatible(normalization) => format!("{normalization:?}"),
474        FormControlCompatibility::Incompatible => "Incompatible".to_string(),
475    }
476}
477
478fn operations(operations: &[FormIrOperation]) -> Vec<String> {
479    operations
480        .iter()
481        .map(|operation| format!("{operation:?}"))
482        .collect()
483}
484
485fn field_programs(
486    programs: &std::collections::BTreeMap<crate::FieldId, Vec<FormIrOperation>>,
487) -> Vec<RuntimeFormsArtifactFieldProgram> {
488    programs
489        .iter()
490        .map(
491            |(field, program_operations)| RuntimeFormsArtifactFieldProgram {
492                field: field.to_string(),
493                operations: operations(program_operations),
494            },
495        )
496        .collect()
497}
498
499#[cfg(test)]
500mod tests {
501    #[test]
502    fn emits_schema_v2_with_only_canonical_execution_references_and_field_paths() {
503        let parsed = presolve_parser::parse_file(
504            "src/X.tsx",
505            r#"@component("x")class X{@form()form!:Form;@field(this.form)value="";render(){return <input field={this.value}/>;}}"#,
506        );
507        let asm = crate::build_application_semantic_model(&parsed);
508        let artifact = super::build_runtime_forms_artifact(&asm);
509        assert_eq!(artifact.schema_version, 2);
510        assert_eq!(artifact.registry_version, 1);
511        assert_eq!(artifact.forms.len(), 1);
512        assert_eq!(artifact.instances.len(), 1);
513        assert!(super::validate_runtime_forms_artifact(&artifact).is_valid);
514        assert!(super::runtime_forms_artifact_json(&artifact).contains("field-binding"));
515    }
516
517    #[test]
518    fn rejects_noncanonical_and_prefix_conflicting_field_paths() {
519        let parsed = presolve_parser::parse_file(
520            "src/X.tsx",
521            r#"@component("x")class X{@form()form!:Form;@field(this.form)value="";render(){return <input field={this.value}/>;}}"#,
522        );
523        let asm = crate::build_application_semantic_model(&parsed);
524        let mut artifact = super::build_runtime_forms_artifact(&asm);
525        let field = artifact.forms[0].fields[0].clone();
526        artifact.forms[0].fields[0].path = vec!["invalid-path".to_string()];
527        assert!(!super::validate_runtime_forms_artifact(&artifact).is_valid);
528
529        artifact.forms[0].fields[0].path = vec!["address".to_string()];
530        let mut nested = field;
531        nested.path = vec!["address".to_string(), "street".to_string()];
532        artifact.forms[0].fields.push(nested);
533        assert!(!super::validate_runtime_forms_artifact(&artifact).is_valid);
534    }
535
536    #[test]
537    fn forms_products_are_byte_deterministic_when_input_files_are_reversed() {
538        let a = r#"@component("a") class A { @form() @serialize("json") profile!: Form; @field(this.profile) name = ""; @action() @submit(this.profile) save(): void {} render() { return <form form={this.profile}><input field={this.name}/></form>; } }"#;
539        let b = r#"@component("b") class B { @form() @serialize("url-encoded") search!: Form; @field(this.search) query = ""; @action() @submit(this.search) save(): void {} render() { return <form form={this.search}><input field={this.query}/></form>; } }"#;
540        let first = crate::CompilationUnit::parse_sources([("src/A.tsx", a), ("src/B.tsx", b)]);
541        let second = crate::CompilationUnit::parse_sources([("src/B.tsx", b), ("src/A.tsx", a)]);
542        let first = crate::build_application_semantic_model_for_unit(&first);
543        let second = crate::build_application_semantic_model_for_unit(&second);
544
545        assert_eq!(
546            super::runtime_forms_artifact_json(&super::build_runtime_forms_artifact(&first)),
547            super::runtime_forms_artifact_json(&super::build_runtime_forms_artifact(&second)),
548        );
549        assert_eq!(
550            crate::template_manifest_json(&crate::build_template_manifest_from_asm(&first)),
551            crate::template_manifest_json(&crate::build_template_manifest_from_asm(&second)),
552        );
553        assert_eq!(
554            crate::resume_manifest_json(&crate::build_resume_manifest(&first)),
555            crate::resume_manifest_json(&crate::build_resume_manifest(&second)),
556        );
557    }
558}