1#![allow(
8 clippy::case_sensitive_file_extension_comparisons,
9 clippy::cast_possible_truncation,
10 clippy::missing_errors_doc,
11 clippy::needless_pass_by_value,
12 clippy::semicolon_if_nothing_returned,
13 clippy::too_many_lines
14)]
15
16use std::collections::{BTreeMap, BTreeSet};
17use std::fmt;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::Arc;
20
21use sha2::{Digest as _, Sha256};
22
23use crate::{
24 build_application_semantic_model_for_unit, tooling_products::build_tooling_query_snapshot_v1,
25 tooling_products::ToolingQuerySnapshotV1, CompilationUnit,
26};
27
28pub const WORKSPACE_GRAPH_SCHEMA_VERSION: u32 = 1;
29pub const WORKSPACE_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
30pub const COMPILER_SESSION_SCHEMA_VERSION: u32 = 1;
31pub const INCREMENTAL_PLAN_SCHEMA_VERSION: u32 = 1;
32pub const PRODUCT_CACHE_INSPECTION_SCHEMA_VERSION: u32 = 1;
33pub const WORKSPACE_CONFIGURATION_SCHEMA_VERSION: u32 = 1;
35pub const INCREMENTAL_COMPILATION_PLAN_V1_SCHEMA: &str = "presolve.incremental-compilation-plan.v1";
37pub const INCREMENTAL_EXECUTION_REPORT_V1_SCHEMA: &str = "presolve.incremental-execution-report.v1";
39
40static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub struct Digest(String);
44
45impl Digest {
46 #[must_use]
47 pub fn sha256(bytes: impl AsRef<[u8]>) -> Self {
48 Self(format!("sha256:{:x}", Sha256::digest(bytes.as_ref())))
49 }
50
51 #[must_use]
52 pub fn as_str(&self) -> &str {
53 &self.0
54 }
55}
56
57impl fmt::Display for Digest {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 self.0.fmt(f)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct ContractVersion(String);
65impl ContractVersion {
66 #[must_use]
67 pub fn new(value: impl Into<String>) -> Self {
68 Self(value.into())
69 }
70 #[must_use]
71 pub fn as_str(&self) -> &str {
72 &self.0
73 }
74}
75impl fmt::Display for ContractVersion {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 self.0.fmt(f)
78 }
79}
80
81macro_rules! typed_id {
82 ($name:ident) => {
83 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84 pub struct $name(String);
85 impl $name {
86 #[must_use]
87 pub fn as_str(&self) -> &str {
88 &self.0
89 }
90 }
91 impl fmt::Display for $name {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 self.0.fmt(f)
94 }
95 }
96 };
97}
98typed_id!(WorkspaceId);
99typed_id!(SourceUnitId);
100typed_id!(SourceRevisionId);
101typed_id!(WorkspaceSnapshotId);
102typed_id!(IncrementalPlanId);
103typed_id!(ProductKey);
104typed_id!(WorkspaceProductKey);
105typed_id!(CompilerSessionId);
106typed_id!(CompilationAttemptId);
107
108fn hashed_id(prefix: &str, domain: &str, fields: &[&[u8]]) -> String {
109 let mut bytes = Vec::new();
110 bytes.extend_from_slice(domain.as_bytes());
111 for field in fields {
112 bytes.push(0);
113 bytes.extend_from_slice(field);
114 }
115 format!("{prefix}:{}", Digest::sha256(bytes))
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub struct WorkspacePath(String);
120
121impl WorkspacePath {
122 pub fn new(path: impl AsRef<str>) -> Result<Self, PlatformFailure> {
123 let raw = path.as_ref();
124 if raw.is_empty()
125 || raw.contains('\0')
126 || raw.starts_with('/')
127 || raw.starts_with('\\')
128 || is_windows_absolute(raw)
129 {
130 return Err(PlatformFailure::path(
131 "workspace path must be non-empty and relative",
132 raw,
133 ));
134 }
135 let mut parts = Vec::new();
136 let separated = raw.replace('\\', "/");
137 for part in separated.split('/') {
138 match part {
139 "" | "." => {}
140 ".." => {
141 if parts.pop().is_none() {
142 return Err(PlatformFailure::path(
143 "workspace path escapes its root",
144 raw,
145 ));
146 }
147 }
148 value => parts.push(value),
149 }
150 }
151 if parts.is_empty() {
152 return Err(PlatformFailure::path(
153 "workspace path must name a file",
154 raw,
155 ));
156 }
157 Ok(Self(parts.join("/")))
158 }
159 #[must_use]
160 pub fn as_str(&self) -> &str {
161 &self.0
162 }
163}
164impl fmt::Display for WorkspacePath {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 self.0.fmt(f)
167 }
168}
169fn is_windows_absolute(path: &str) -> bool {
170 path.len() > 2 && path.as_bytes()[1] == b':' && path.as_bytes()[2] == b'\\'
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
174pub enum SourceLanguage {
175 TypeScript,
176 TypeScriptJsx,
177 JavaScript,
178 JavaScriptJsx,
179}
180impl SourceLanguage {
181 #[must_use]
182 pub const fn as_str(self) -> &'static str {
183 match self {
184 Self::TypeScript => "typescript",
185 Self::TypeScriptJsx => "typescript_jsx",
186 Self::JavaScript => "javascript",
187 Self::JavaScriptJsx => "javascript_jsx",
188 }
189 }
190 fn from_path(path: &WorkspacePath) -> Result<Self, PlatformFailure> {
191 if path.0.ends_with(".tsx") {
192 Ok(Self::TypeScriptJsx)
193 } else if path.0.ends_with(".ts") {
194 Ok(Self::TypeScript)
195 } else if path.0.ends_with(".jsx") {
196 Ok(Self::JavaScriptJsx)
197 } else if path.0.ends_with(".js") {
198 Ok(Self::JavaScript)
199 } else {
200 Err(PlatformFailure::new(
201 PlatformFailureCode::UnsupportedSourceLanguage,
202 "only .ts, .tsx, .js, and .jsx workspace inputs are supported",
203 ))
204 }
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
209pub enum ProductKind {
210 Parse,
211 SourceSemantic,
212 ComponentGraph,
213 ApplicationSemanticModel,
214 LoweredProgram,
215 HtmlArtifact,
216 RuntimeArtifact,
217 TemplateManifest,
218 ResumeManifest,
219 InspectionArtifact,
220 DiagnosticSet,
221}
222impl ProductKind {
223 #[must_use]
224 pub const fn as_str(self) -> &'static str {
225 match self {
226 Self::Parse => "parse",
227 Self::SourceSemantic => "source_semantic",
228 Self::ComponentGraph => "component_graph",
229 Self::ApplicationSemanticModel => "application_semantic_model",
230 Self::LoweredProgram => "lowered_program",
231 Self::HtmlArtifact => "html_artifact",
232 Self::RuntimeArtifact => "runtime_artifact",
233 Self::TemplateManifest => "template_manifest",
234 Self::ResumeManifest => "resume_manifest",
235 Self::InspectionArtifact => "inspection_artifact",
236 Self::DiagnosticSet => "diagnostic_set",
237 }
238 }
239}
240#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
241pub enum ApplicationProductKind {
242 ApplicationSemanticModel,
243 ApplicationDiagnostics,
244 BuildManifest,
245}
246impl ApplicationProductKind {
247 #[must_use]
248 pub const fn as_str(self) -> &'static str {
249 match self {
250 Self::ApplicationSemanticModel => "application_semantic_model",
251 Self::ApplicationDiagnostics => "application_diagnostics",
252 Self::BuildManifest => "build_manifest",
253 }
254 }
255}
256#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
257pub enum WorkspaceDependencyKind {
258 Compile,
259 Semantic,
260 Artifact,
261}
262impl WorkspaceDependencyKind {
263 #[must_use]
264 pub const fn as_str(self) -> &'static str {
265 match self {
266 Self::Compile => "compile",
267 Self::Semantic => "semantic",
268 Self::Artifact => "artifact",
269 }
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WorkspaceConfiguration {
275 pub source_roots: Vec<WorkspacePath>,
276 pub feature_flags: Vec<String>,
277 pub target_profile: String,
278 pub platform_options: Vec<(String, String)>,
279}
280impl Default for WorkspaceConfiguration {
281 fn default() -> Self {
282 Self {
283 source_roots: vec![WorkspacePath("src".into())],
284 feature_flags: Vec::new(),
285 target_profile: "default".into(),
286 platform_options: Vec::new(),
287 }
288 }
289}
290impl WorkspaceConfiguration {
291 fn canonical(&self) -> String {
292 let roots = self
293 .source_roots
294 .iter()
295 .map(ToString::to_string)
296 .collect::<Vec<_>>();
297 let mut flags = self.feature_flags.clone();
298 flags.sort();
299 flags.dedup();
300 let mut options = self.platform_options.clone();
301 options.sort();
302 options.dedup();
303 format!(
304 "roots={}\\nflags={}\\ntarget={}\\noptions={}",
305 roots.join(","),
306 flags.join(","),
307 self.target_profile,
308 options
309 .into_iter()
310 .map(|(k, v)| format!("{k}={v}"))
311 .collect::<Vec<_>>()
312 .join(",")
313 )
314 }
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct PlatformValidationError {
320 pub code: &'static str,
321 pub message: String,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct PlatformDecodeError {
327 pub code: &'static str,
328 pub pointer: String,
329 pub message: String,
330}
331
332pub fn validate_workspace_configuration_v1(
334 configuration: &WorkspaceConfiguration,
335) -> Result<(), PlatformValidationError> {
336 if configuration.target_profile.is_empty() || configuration.target_profile.contains('\0') {
337 return Err(PlatformValidationError {
338 code: "invalid_workspace_configuration",
339 message: "target profile must be non-empty".into(),
340 });
341 }
342 if configuration.source_roots.is_empty() {
343 return Err(PlatformValidationError {
344 code: "invalid_workspace_configuration",
345 message: "at least one source root is required".into(),
346 });
347 }
348 let mut roots = BTreeSet::new();
349 for root in &configuration.source_roots {
350 WorkspacePath::new(root.as_str()).map_err(|error| PlatformValidationError {
351 code: "invalid_workspace_configuration",
352 message: error.message,
353 })?;
354 if !roots.insert(root.clone()) {
355 return Err(PlatformValidationError {
356 code: "invalid_workspace_configuration",
357 message: "source roots must be unique".into(),
358 });
359 }
360 }
361 Ok(())
362}
363
364pub fn canonical_workspace_configuration_json_v1(
366 configuration: &WorkspaceConfiguration,
367) -> Result<Vec<u8>, PlatformSerializationError> {
368 validate_workspace_configuration_v1(configuration).map_err(|error| {
369 PlatformSerializationError {
370 message: error.message,
371 }
372 })?;
373 let flags = configuration
374 .feature_flags
375 .iter()
376 .map(|value| quote(value))
377 .collect::<Vec<_>>()
378 .join(",");
379 let roots = configuration
380 .source_roots
381 .iter()
382 .map(|value| quote(value.as_str()))
383 .collect::<Vec<_>>()
384 .join(",");
385 let options = configuration
386 .platform_options
387 .iter()
388 .map(|(key, value)| format!("{{\"key\":{},\"value\":{}}}", quote(key), quote(value)))
389 .collect::<Vec<_>>()
390 .join(",");
391 Ok(json_document(format!("{{\"schema_version\":{},\"source_roots\":[{}],\"compiler_flags\":[{}],\"target_profile\":{},\"platform_options\":[{}]}}", WORKSPACE_CONFIGURATION_SCHEMA_VERSION, roots, flags, quote(&configuration.target_profile), options)))
392}
393
394pub fn workspace_configuration_fingerprint_v1(
396 configuration: &WorkspaceConfiguration,
397) -> Result<Digest, PlatformValidationError> {
398 validate_workspace_configuration_v1(configuration)?;
399 Ok(Digest::sha256(configuration.canonical()))
400}
401
402pub fn derive_workspace_id_v1(
404 configuration: &WorkspaceConfiguration,
405) -> Result<WorkspaceId, PlatformValidationError> {
406 validate_workspace_configuration_v1(configuration)?;
407 Ok(workspace_id(configuration))
408}
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct WorkspaceSource {
411 pub path: String,
412 pub source: String,
413 pub language: Option<SourceLanguage>,
414}
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct WorkspaceInput {
417 pub configuration: WorkspaceConfiguration,
418 pub sources: Vec<WorkspaceSource>,
419 pub compiler_contract: ContractVersion,
420}
421impl WorkspaceInput {
422 #[must_use]
423 pub fn new(sources: Vec<WorkspaceSource>) -> Self {
424 Self {
425 configuration: WorkspaceConfiguration::default(),
426 sources,
427 compiler_contract: ContractVersion::new(format!(
428 "presolve-compiler:{}",
429 env!("CARGO_PKG_VERSION")
430 )),
431 }
432 }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq)]
436struct NormalizedInput {
437 path: WorkspacePath,
438 source: String,
439 language: SourceLanguage,
440 source_unit_id: SourceUnitId,
441 source_revision_id: SourceRevisionId,
442 source_digest: Digest,
443}
444fn workspace_id(configuration: &WorkspaceConfiguration) -> WorkspaceId {
445 WorkspaceId(hashed_id(
446 "workspace",
447 "workspace-config-v1",
448 &[configuration.canonical().as_bytes()],
449 ))
450}
451fn source_unit_id(workspace: &WorkspaceId, path: &WorkspacePath) -> SourceUnitId {
452 SourceUnitId(hashed_id(
453 "source",
454 "source-unit-v1",
455 &[workspace.as_str().as_bytes(), path.as_str().as_bytes()],
456 ))
457}
458fn source_revision_id(
459 unit: &SourceUnitId,
460 source: &str,
461 language: SourceLanguage,
462) -> SourceRevisionId {
463 SourceRevisionId(hashed_id(
464 "revision",
465 "source-revision-v1",
466 &[
467 unit.as_str().as_bytes(),
468 source.len().to_string().as_bytes(),
469 source.as_bytes(),
470 language.as_str().as_bytes(),
471 ],
472 ))
473}
474fn normalize(
475 input: &WorkspaceInput,
476) -> Result<(WorkspaceId, Digest, Vec<NormalizedInput>), PlatformFailure> {
477 let workspace = workspace_id(&input.configuration);
478 let configuration_fingerprint = Digest::sha256(input.configuration.canonical());
479 let mut seen = BTreeSet::new();
480 let mut sources = Vec::new();
481 for source in &input.sources {
482 let path = WorkspacePath::new(&source.path)?;
483 if !seen.insert(path.clone()) {
484 return Err(PlatformFailure {
485 code: PlatformFailureCode::DuplicateWorkspacePath,
486 message: format!("duplicate normalized workspace path: {path}"),
487 workspace_path: Some(path),
488 source_unit_id: None,
489 product_key: None,
490 });
491 }
492 let language = match source.language {
493 Some(language) => language,
494 None => SourceLanguage::from_path(&path)?,
495 };
496 let unit = source_unit_id(&workspace, &path);
497 let revision = source_revision_id(&unit, &source.source, language);
498 sources.push(NormalizedInput {
499 path,
500 source: source.source.clone(),
501 language,
502 source_unit_id: unit,
503 source_revision_id: revision,
504 source_digest: Digest::sha256(source.source.as_bytes()),
505 });
506 }
507 sources.sort_by(|a, b| a.path.cmp(&b.path));
508 Ok((workspace, configuration_fingerprint, sources))
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct SnapshotUnit {
513 pub source_unit_id: SourceUnitId,
514 pub path: WorkspacePath,
515 pub source_revision_id: SourceRevisionId,
516 pub source_digest: Digest,
517 pub source_length: u64,
518 pub language: SourceLanguage,
519}
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct WorkspaceSnapshot {
522 pub schema_version: u32,
523 pub workspace_id: WorkspaceId,
524 pub snapshot_id: WorkspaceSnapshotId,
525 pub compiler_contract: ContractVersion,
526 pub configuration_fingerprint: Digest,
527 pub units: Vec<SnapshotUnit>,
528}
529impl WorkspaceSnapshot {
530 pub fn from_input(input: &WorkspaceInput) -> Result<Self, PlatformFailure> {
531 let (workspace, fingerprint, inputs) = normalize(input)?;
532 Ok(Self::from_normalized(
533 workspace,
534 fingerprint,
535 input.compiler_contract.clone(),
536 &inputs,
537 ))
538 }
539 fn from_normalized(
540 workspace_id: WorkspaceId,
541 configuration_fingerprint: Digest,
542 compiler_contract: ContractVersion,
543 inputs: &[NormalizedInput],
544 ) -> Self {
545 let units = inputs
546 .iter()
547 .map(|input| SnapshotUnit {
548 source_unit_id: input.source_unit_id.clone(),
549 path: input.path.clone(),
550 source_revision_id: input.source_revision_id.clone(),
551 source_digest: input.source_digest.clone(),
552 source_length: input.source.len() as u64,
553 language: input.language,
554 })
555 .collect::<Vec<_>>();
556 let identity = canonical_snapshot_identity(&workspace_id, &compiler_contract, &units);
557 Self {
558 schema_version: WORKSPACE_SNAPSHOT_SCHEMA_VERSION,
559 workspace_id,
560 snapshot_id: WorkspaceSnapshotId(hashed_id(
561 "snapshot",
562 "workspace-snapshot-v1",
563 &[identity.as_bytes()],
564 )),
565 compiler_contract,
566 configuration_fingerprint,
567 units,
568 }
569 }
570 pub fn validate(&self) -> Result<(), PlatformFailure> {
571 if self.schema_version != WORKSPACE_SNAPSHOT_SCHEMA_VERSION {
572 return Err(PlatformFailure::new(
573 PlatformFailureCode::UnsupportedSchemaVersion,
574 "unsupported workspace snapshot schema version",
575 ));
576 }
577 let mut previous = None;
578 for unit in &self.units {
579 if previous
580 .as_ref()
581 .is_some_and(|value: &WorkspacePath| value >= &unit.path)
582 {
583 return Err(PlatformFailure::new(
584 PlatformFailureCode::InvalidWorkspaceSnapshot,
585 "snapshot units are not uniquely ordered by path",
586 ));
587 }
588 if source_unit_id(&self.workspace_id, &unit.path) != unit.source_unit_id {
589 return Err(PlatformFailure::new(
590 PlatformFailureCode::InvalidWorkspaceSnapshot,
591 "snapshot source-unit identity mismatch",
592 ));
593 }
594 previous = Some(unit.path.clone());
595 }
596 let expected = WorkspaceSnapshotId(hashed_id(
597 "snapshot",
598 "workspace-snapshot-v1",
599 &[canonical_snapshot_identity(
600 &self.workspace_id,
601 &self.compiler_contract,
602 &self.units,
603 )
604 .as_bytes()],
605 ));
606 if expected != self.snapshot_id {
607 return Err(PlatformFailure::new(
608 PlatformFailureCode::InvalidWorkspaceSnapshot,
609 "snapshot identity mismatch",
610 ));
611 }
612 Ok(())
613 }
614 pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
615 self.validate()
616 .map_err(PlatformSerializationError::validation)?;
617 Ok(json_document(snapshot_json(self)))
618 }
619}
620
621pub fn decode_workspace_snapshot_json_v1(
624 bytes: &[u8],
625) -> Result<WorkspaceSnapshot, PlatformDecodeError> {
626 let value = decode_json(bytes)?;
627 let object = object_fields(
628 value,
629 &[
630 "schema_version",
631 "workspace_id",
632 "snapshot_id",
633 "compiler_contract",
634 "configuration_fingerprint",
635 "units",
636 ],
637 "",
638 )?;
639 let schema = number(object.get("schema_version"), "/schema_version")?;
640 if schema != u64::from(WORKSPACE_SNAPSHOT_SCHEMA_VERSION) {
641 return Err(decode_error(
642 "unsupported_schema_version",
643 "/schema_version",
644 "unsupported workspace snapshot schema version",
645 ));
646 }
647 let units = array(object.get("units"), "/units")?
648 .iter()
649 .enumerate()
650 .map(|(index, value)| {
651 let item = object_fields(
652 value.clone(),
653 &[
654 "source_unit_id",
655 "path",
656 "source_revision_id",
657 "source_digest",
658 "source_length",
659 "language",
660 ],
661 &format!("/units/{index}"),
662 )?;
663 Ok(SnapshotUnit {
664 source_unit_id: SourceUnitId(
665 string(
666 item.get("source_unit_id"),
667 &format!("/units/{index}/source_unit_id"),
668 )?
669 .into(),
670 ),
671 path: WorkspacePath::new(string(
672 item.get("path"),
673 &format!("/units/{index}/path"),
674 )?)
675 .map_err(|_| {
676 decode_error(
677 "invalid_workspace_path",
678 &format!("/units/{index}/path"),
679 "invalid workspace path",
680 )
681 })?,
682 source_revision_id: SourceRevisionId(
683 string(
684 item.get("source_revision_id"),
685 &format!("/units/{index}/source_revision_id"),
686 )?
687 .into(),
688 ),
689 source_digest: Digest(
690 string(
691 item.get("source_digest"),
692 &format!("/units/{index}/source_digest"),
693 )?
694 .into(),
695 ),
696 source_length: number(
697 item.get("source_length"),
698 &format!("/units/{index}/source_length"),
699 )?,
700 language: decode_language(string(
701 item.get("language"),
702 &format!("/units/{index}/language"),
703 )?)?,
704 })
705 })
706 .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
707 let snapshot = WorkspaceSnapshot {
708 schema_version: WORKSPACE_SNAPSHOT_SCHEMA_VERSION,
709 workspace_id: WorkspaceId(string(object.get("workspace_id"), "/workspace_id")?.into()),
710 snapshot_id: WorkspaceSnapshotId(string(object.get("snapshot_id"), "/snapshot_id")?.into()),
711 compiler_contract: ContractVersion::new(string(
712 object.get("compiler_contract"),
713 "/compiler_contract",
714 )?),
715 configuration_fingerprint: Digest(
716 string(
717 object.get("configuration_fingerprint"),
718 "/configuration_fingerprint",
719 )?
720 .into(),
721 ),
722 units,
723 };
724 snapshot
725 .validate()
726 .map_err(|error| decode_error("invalid_workspace_snapshot", "", error.message))?;
727 if snapshot
728 .to_canonical_json()
729 .map_err(|error| decode_error("invalid_workspace_snapshot", "", error.message))?
730 != bytes
731 {
732 return Err(decode_error(
733 "noncanonical_json",
734 "",
735 "workspace snapshot is not canonical",
736 ));
737 }
738 Ok(snapshot)
739}
740
741pub fn decode_workspace_graph_json_v1(bytes: &[u8]) -> Result<WorkspaceGraph, PlatformDecodeError> {
744 let value = decode_json(bytes)?;
745 let object = object_fields(
746 value,
747 &[
748 "schema_version",
749 "workspace_id",
750 "snapshot_id",
751 "compiler_contract",
752 "units",
753 "dependency_edges",
754 "application_products",
755 ],
756 "",
757 )?;
758 if number(object.get("schema_version"), "/schema_version")?
759 != u64::from(WORKSPACE_GRAPH_SCHEMA_VERSION)
760 {
761 return Err(decode_error(
762 "unsupported_schema_version",
763 "/schema_version",
764 "unsupported workspace graph schema version",
765 ));
766 }
767 let units = array(object.get("units"), "/units")?
768 .iter()
769 .enumerate()
770 .map(|(index, value)| {
771 let item = object_fields(
772 value.clone(),
773 &[
774 "source_unit_id",
775 "path",
776 "source_revision_id",
777 "source_digest",
778 "source_length",
779 "language",
780 "products",
781 ],
782 &format!("/units/{index}"),
783 )?;
784 let products = array(item.get("products"), &format!("/units/{index}/products"))?
785 .iter()
786 .enumerate()
787 .map(|(product_index, value)| {
788 let p = object_fields(
789 value.clone(),
790 &[
791 "product_kind",
792 "product_key",
793 "producer_contract",
794 "content_digest",
795 ],
796 &format!("/units/{index}/products/{product_index}"),
797 )?;
798 Ok(UnitProductRef {
799 product_kind: decode_product_kind(string(p.get("product_kind"), "")?)?,
800 product_key: ProductKey(string(p.get("product_key"), "")?.into()),
801 producer_contract: ContractVersion::new(string(
802 p.get("producer_contract"),
803 "",
804 )?),
805 content_digest: Digest(string(p.get("content_digest"), "")?.into()),
806 })
807 })
808 .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
809 Ok(WorkspaceUnit {
810 source_unit_id: SourceUnitId(string(item.get("source_unit_id"), "")?.into()),
811 path: WorkspacePath::new(string(item.get("path"), "")?).map_err(|_| {
812 decode_error("invalid_workspace_path", "", "invalid workspace path")
813 })?,
814 source_revision_id: SourceRevisionId(
815 string(item.get("source_revision_id"), "")?.into(),
816 ),
817 source_digest: Digest(string(item.get("source_digest"), "")?.into()),
818 source_length: number(item.get("source_length"), "")?,
819 language: decode_language(string(item.get("language"), "")?)?,
820 products,
821 })
822 })
823 .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
824 let edges = array(object.get("dependency_edges"), "/dependency_edges")?
825 .iter()
826 .map(|value| {
827 let edge = object_fields(
828 value.clone(),
829 &["source", "kind", "target"],
830 "/dependency_edges",
831 )?;
832 Ok(WorkspaceDependencyEdge {
833 source: SourceUnitId(string(edge.get("source"), "")?.into()),
834 kind: decode_dependency_kind(string(edge.get("kind"), "")?)?,
835 target: SourceUnitId(string(edge.get("target"), "")?.into()),
836 })
837 })
838 .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
839 let applications = array(object.get("application_products"), "/application_products")?
840 .iter()
841 .map(|value| {
842 let product = object_fields(
843 value.clone(),
844 &[
845 "product_kind",
846 "product_key",
847 "producer_contract",
848 "content_digest",
849 ],
850 "/application_products",
851 )?;
852 Ok(ApplicationProductRef {
853 product_kind: decode_application_product_kind(string(
854 product.get("product_kind"),
855 "",
856 )?)?,
857 product_key: WorkspaceProductKey(string(product.get("product_key"), "")?.into()),
858 producer_contract: ContractVersion::new(string(
859 product.get("producer_contract"),
860 "",
861 )?),
862 content_digest: Digest(string(product.get("content_digest"), "")?.into()),
863 })
864 })
865 .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
866 let graph = WorkspaceGraph {
867 schema_version: WORKSPACE_GRAPH_SCHEMA_VERSION,
868 workspace_id: WorkspaceId(string(object.get("workspace_id"), "/workspace_id")?.into()),
869 snapshot_id: WorkspaceSnapshotId(string(object.get("snapshot_id"), "/snapshot_id")?.into()),
870 compiler_contract: ContractVersion::new(string(
871 object.get("compiler_contract"),
872 "/compiler_contract",
873 )?),
874 units,
875 dependency_edges: edges,
876 application_products: applications,
877 };
878 graph
879 .validate()
880 .map_err(|error| decode_error("invalid_workspace_graph", "", error.message))?;
881 if graph
882 .to_canonical_json()
883 .map_err(|error| decode_error("invalid_workspace_graph", "", error.message))?
884 != bytes
885 {
886 return Err(decode_error(
887 "noncanonical_json",
888 "",
889 "workspace graph is not canonical",
890 ));
891 }
892 Ok(graph)
893}
894fn decode_product_kind(value: &str) -> Result<ProductKind, PlatformDecodeError> {
895 match value {
896 "parse" => Ok(ProductKind::Parse),
897 "source_semantic" => Ok(ProductKind::SourceSemantic),
898 "component_graph" => Ok(ProductKind::ComponentGraph),
899 "application_semantic_model" => Ok(ProductKind::ApplicationSemanticModel),
900 "lowered_program" => Ok(ProductKind::LoweredProgram),
901 "html_artifact" => Ok(ProductKind::HtmlArtifact),
902 "runtime_artifact" => Ok(ProductKind::RuntimeArtifact),
903 "template_manifest" => Ok(ProductKind::TemplateManifest),
904 "resume_manifest" => Ok(ProductKind::ResumeManifest),
905 "inspection_artifact" => Ok(ProductKind::InspectionArtifact),
906 "diagnostic_set" => Ok(ProductKind::DiagnosticSet),
907 _ => Err(decode_error("invalid_enum", "", "unsupported product kind")),
908 }
909}
910fn decode_application_product_kind(
911 value: &str,
912) -> Result<ApplicationProductKind, PlatformDecodeError> {
913 match value {
914 "application_semantic_model" => Ok(ApplicationProductKind::ApplicationSemanticModel),
915 "application_diagnostics" => Ok(ApplicationProductKind::ApplicationDiagnostics),
916 "build_manifest" => Ok(ApplicationProductKind::BuildManifest),
917 _ => Err(decode_error(
918 "invalid_enum",
919 "",
920 "unsupported application product kind",
921 )),
922 }
923}
924fn decode_dependency_kind(value: &str) -> Result<WorkspaceDependencyKind, PlatformDecodeError> {
925 match value {
926 "compile" => Ok(WorkspaceDependencyKind::Compile),
927 "semantic" => Ok(WorkspaceDependencyKind::Semantic),
928 "artifact" => Ok(WorkspaceDependencyKind::Artifact),
929 _ => Err(decode_error(
930 "invalid_enum",
931 "",
932 "unsupported dependency kind",
933 )),
934 }
935}
936
937fn decode_json(bytes: &[u8]) -> Result<serde_json::Value, PlatformDecodeError> {
938 if !bytes.ends_with(b"\n") || bytes.ends_with(b"\n\n") {
939 return Err(decode_error(
940 "noncanonical_json",
941 "",
942 "document must end with exactly one newline",
943 ));
944 }
945 serde_json::from_slice(bytes)
946 .map_err(|_| decode_error("invalid_json", "", "invalid canonical JSON"))
947}
948fn object_fields(
949 value: serde_json::Value,
950 expected: &[&str],
951 pointer: &str,
952) -> Result<serde_json::Map<String, serde_json::Value>, PlatformDecodeError> {
953 let serde_json::Value::Object(object) = value else {
954 return Err(decode_error("invalid_type", pointer, "expected object"));
955 };
956 if object.len() != expected.len() || expected.iter().any(|field| !object.contains_key(*field)) {
957 return Err(decode_error(
958 "invalid_schema",
959 pointer,
960 "missing or unknown object field",
961 ));
962 }
963 Ok(object)
964}
965fn string<'a>(
966 value: Option<&'a serde_json::Value>,
967 pointer: &str,
968) -> Result<&'a str, PlatformDecodeError> {
969 value
970 .and_then(serde_json::Value::as_str)
971 .ok_or_else(|| decode_error("invalid_type", pointer, "expected string"))
972}
973fn number(value: Option<&serde_json::Value>, pointer: &str) -> Result<u64, PlatformDecodeError> {
974 value
975 .and_then(serde_json::Value::as_u64)
976 .ok_or_else(|| decode_error("invalid_type", pointer, "expected unsigned integer"))
977}
978fn array<'a>(
979 value: Option<&'a serde_json::Value>,
980 pointer: &str,
981) -> Result<&'a Vec<serde_json::Value>, PlatformDecodeError> {
982 value
983 .and_then(serde_json::Value::as_array)
984 .ok_or_else(|| decode_error("invalid_type", pointer, "expected array"))
985}
986fn decode_language(value: &str) -> Result<SourceLanguage, PlatformDecodeError> {
987 match value {
988 "typescript" => Ok(SourceLanguage::TypeScript),
989 "typescript_jsx" => Ok(SourceLanguage::TypeScriptJsx),
990 "javascript" => Ok(SourceLanguage::JavaScript),
991 "javascript_jsx" => Ok(SourceLanguage::JavaScriptJsx),
992 _ => Err(decode_error(
993 "invalid_enum",
994 "/language",
995 "unsupported source language",
996 )),
997 }
998}
999fn decode_error(
1000 code: &'static str,
1001 pointer: &str,
1002 message: impl Into<String>,
1003) -> PlatformDecodeError {
1004 PlatformDecodeError {
1005 code,
1006 pointer: pointer.into(),
1007 message: message.into(),
1008 }
1009}
1010fn canonical_snapshot_identity(
1011 workspace: &WorkspaceId,
1012 contract: &ContractVersion,
1013 units: &[SnapshotUnit],
1014) -> String {
1015 let units = units
1016 .iter()
1017 .map(|unit| {
1018 format!(
1019 "{{\"source_unit_id\":{},\"source_revision_id\":{}}}",
1020 quote(unit.source_unit_id.as_str()),
1021 quote(unit.source_revision_id.as_str())
1022 )
1023 })
1024 .collect::<Vec<_>>()
1025 .join(",");
1026 format!(
1027 "{{\"workspace_id\":{},\"compiler_contract\":{},\"units\":[{units}]}}",
1028 quote(workspace.as_str()),
1029 quote(contract.as_str())
1030 )
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Eq)]
1034pub struct UnitProductRef {
1035 pub product_kind: ProductKind,
1036 pub product_key: ProductKey,
1037 pub producer_contract: ContractVersion,
1038 pub content_digest: Digest,
1039}
1040#[derive(Debug, Clone, PartialEq, Eq)]
1041pub struct WorkspaceUnit {
1042 pub source_unit_id: SourceUnitId,
1043 pub path: WorkspacePath,
1044 pub source_revision_id: SourceRevisionId,
1045 pub source_digest: Digest,
1046 pub source_length: u64,
1047 pub language: SourceLanguage,
1048 pub products: Vec<UnitProductRef>,
1049}
1050#[derive(Debug, Clone, PartialEq, Eq)]
1051pub struct WorkspaceDependencyEdge {
1052 pub source: SourceUnitId,
1053 pub kind: WorkspaceDependencyKind,
1054 pub target: SourceUnitId,
1055}
1056#[derive(Debug, Clone, PartialEq, Eq)]
1057pub struct ApplicationProductRef {
1058 pub product_kind: ApplicationProductKind,
1059 pub product_key: WorkspaceProductKey,
1060 pub producer_contract: ContractVersion,
1061 pub content_digest: Digest,
1062}
1063#[derive(Debug, Clone, PartialEq, Eq)]
1064pub struct WorkspaceGraph {
1065 pub schema_version: u32,
1066 pub workspace_id: WorkspaceId,
1067 pub snapshot_id: WorkspaceSnapshotId,
1068 pub compiler_contract: ContractVersion,
1069 pub units: Vec<WorkspaceUnit>,
1070 pub dependency_edges: Vec<WorkspaceDependencyEdge>,
1071 pub application_products: Vec<ApplicationProductRef>,
1072}
1073impl WorkspaceGraph {
1074 #[must_use]
1075 pub fn unit(&self, id: &SourceUnitId) -> Option<&WorkspaceUnit> {
1076 self.units.iter().find(|unit| &unit.source_unit_id == id)
1077 }
1078 #[must_use]
1079 pub fn unit_by_path(&self, path: &WorkspacePath) -> Option<&WorkspaceUnit> {
1080 self.units.iter().find(|unit| &unit.path == path)
1081 }
1082 pub fn direct_dependencies<'a>(
1083 &'a self,
1084 id: &'a SourceUnitId,
1085 ) -> impl Iterator<Item = &'a WorkspaceDependencyEdge> + 'a {
1086 self.dependency_edges
1087 .iter()
1088 .filter(move |edge| &edge.source == id)
1089 }
1090 pub fn direct_dependents<'a>(
1091 &'a self,
1092 id: &'a SourceUnitId,
1093 ) -> impl Iterator<Item = &'a WorkspaceDependencyEdge> + 'a {
1094 self.dependency_edges
1095 .iter()
1096 .filter(move |edge| &edge.target == id)
1097 }
1098 pub fn validate(&self) -> Result<(), PlatformFailure> {
1099 if self.schema_version != WORKSPACE_GRAPH_SCHEMA_VERSION {
1100 return Err(PlatformFailure::new(
1101 PlatformFailureCode::UnsupportedSchemaVersion,
1102 "unsupported workspace graph schema version",
1103 ));
1104 }
1105 let mut paths = BTreeSet::new();
1106 let mut ids = BTreeSet::new();
1107 let mut prior = None;
1108 for unit in &self.units {
1109 if !paths.insert(unit.path.clone())
1110 || !ids.insert(unit.source_unit_id.clone())
1111 || prior
1112 .as_ref()
1113 .is_some_and(|path: &WorkspacePath| path >= &unit.path)
1114 {
1115 return Err(PlatformFailure::new(
1116 PlatformFailureCode::InvalidWorkspaceGraph,
1117 "workspace graph units must be unique and ordered",
1118 ));
1119 }
1120 if source_unit_id(&self.workspace_id, &unit.path) != unit.source_unit_id {
1121 return Err(PlatformFailure::new(
1122 PlatformFailureCode::InvalidWorkspaceGraph,
1123 "workspace graph source-unit identity mismatch",
1124 ));
1125 }
1126 let mut kinds = BTreeSet::new();
1127 for product in &unit.products {
1128 if !kinds.insert(product.product_kind)
1129 || !product.product_key.as_str().starts_with("product:sha256:")
1130 {
1131 return Err(PlatformFailure::new(
1132 PlatformFailureCode::InvalidWorkspaceGraph,
1133 "workspace graph product key mismatch",
1134 ));
1135 }
1136 }
1137 prior = Some(unit.path.clone());
1138 }
1139 let mut edges = BTreeSet::new();
1140 let mut previous = None;
1141 for edge in &self.dependency_edges {
1142 if !ids.contains(&edge.source)
1143 || !ids.contains(&edge.target)
1144 || !edges.insert((edge.source.clone(), edge.kind, edge.target.clone()))
1145 || previous.as_ref().is_some_and(
1146 |value: &(SourceUnitId, WorkspaceDependencyKind, SourceUnitId)| {
1147 value >= &(edge.source.clone(), edge.kind, edge.target.clone())
1148 },
1149 )
1150 {
1151 return Err(PlatformFailure::new(
1152 PlatformFailureCode::InvalidWorkspaceGraph,
1153 "workspace graph edges must be valid, unique, and ordered",
1154 ));
1155 }
1156 previous = Some((edge.source.clone(), edge.kind, edge.target.clone()));
1157 }
1158 Ok(())
1159 }
1160 pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
1161 self.validate()
1162 .map_err(PlatformSerializationError::validation)?;
1163 Ok(json_document(graph_json(self)))
1164 }
1165}
1166fn product_key(
1167 unit: &SourceUnitId,
1168 revision: &SourceRevisionId,
1169 kind: ProductKind,
1170 contract: &ContractVersion,
1171 configuration: &Digest,
1172) -> ProductKey {
1173 ProductKey(hashed_id(
1174 "product",
1175 "product-key-v1",
1176 &[
1177 unit.as_str().as_bytes(),
1178 revision.as_str().as_bytes(),
1179 kind.as_str().as_bytes(),
1180 contract.as_str().as_bytes(),
1181 configuration.as_str().as_bytes(),
1182 ],
1183 ))
1184}
1185fn workspace_product_key(
1186 workspace: &WorkspaceId,
1187 snapshot: &WorkspaceSnapshotId,
1188 kind: ApplicationProductKind,
1189 contract: &ContractVersion,
1190 configuration: &Digest,
1191) -> WorkspaceProductKey {
1192 WorkspaceProductKey(hashed_id(
1193 "workspace-product",
1194 "workspace-product-key-v1",
1195 &[
1196 workspace.as_str().as_bytes(),
1197 snapshot.as_str().as_bytes(),
1198 kind.as_str().as_bytes(),
1199 contract.as_str().as_bytes(),
1200 configuration.as_str().as_bytes(),
1201 ],
1202 ))
1203}
1204
1205#[derive(Debug, Clone, PartialEq, Eq)]
1206pub struct WorkspaceChangeSet {
1207 pub baseline_snapshot_id: WorkspaceSnapshotId,
1208 pub candidate_snapshot_id: WorkspaceSnapshotId,
1209 pub configuration_changed: bool,
1210 pub added: Vec<SourceUnitId>,
1211 pub removed: Vec<SourceUnitId>,
1212 pub modified: Vec<SourceUnitId>,
1213 pub unchanged: Vec<SourceUnitId>,
1214}
1215#[must_use]
1216pub fn compare_snapshots(
1217 baseline: &WorkspaceSnapshot,
1218 candidate: &WorkspaceSnapshot,
1219) -> WorkspaceChangeSet {
1220 let old = baseline
1221 .units
1222 .iter()
1223 .map(|unit| (unit.source_unit_id.clone(), unit))
1224 .collect::<BTreeMap<_, _>>();
1225 let new = candidate
1226 .units
1227 .iter()
1228 .map(|unit| (unit.source_unit_id.clone(), unit))
1229 .collect::<BTreeMap<_, _>>();
1230 let mut added = Vec::new();
1231 let mut removed = Vec::new();
1232 let mut modified = Vec::new();
1233 let mut unchanged = Vec::new();
1234 for unit in &candidate.units {
1235 match old.get(&unit.source_unit_id) {
1236 None => added.push(unit.source_unit_id.clone()),
1237 Some(previous) if previous.source_revision_id != unit.source_revision_id => {
1238 modified.push(unit.source_unit_id.clone())
1239 }
1240 Some(_) => unchanged.push(unit.source_unit_id.clone()),
1241 }
1242 }
1243 for unit in &baseline.units {
1244 if !new.contains_key(&unit.source_unit_id) {
1245 removed.push(unit.source_unit_id.clone());
1246 }
1247 }
1248 WorkspaceChangeSet {
1249 baseline_snapshot_id: baseline.snapshot_id.clone(),
1250 candidate_snapshot_id: candidate.snapshot_id.clone(),
1251 configuration_changed: baseline.workspace_id != candidate.workspace_id
1252 || baseline.configuration_fingerprint != candidate.configuration_fingerprint
1253 || baseline.compiler_contract != candidate.compiler_contract,
1254 added,
1255 removed,
1256 modified,
1257 unchanged,
1258 }
1259}
1260
1261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1262pub enum IncrementalMode {
1263 NoOp,
1264 Incremental,
1265 Full,
1266}
1267impl IncrementalMode {
1268 #[must_use]
1269 pub const fn as_str(self) -> &'static str {
1270 match self {
1271 Self::NoOp => "no_op",
1272 Self::Incremental => "incremental",
1273 Self::Full => "full",
1274 }
1275 }
1276}
1277#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1278pub enum InvalidationReason {
1279 Added,
1280 Modified,
1281 RemovedDependency,
1282 CompileDependent,
1283 SemanticDependent,
1284 ArtifactDependent,
1285 ConfigurationChanged,
1286 CompilerContractChanged,
1287}
1288impl InvalidationReason {
1289 #[must_use]
1290 pub const fn as_str(self) -> &'static str {
1291 match self {
1292 Self::Added => "added",
1293 Self::Modified => "modified",
1294 Self::RemovedDependency => "removed_dependency",
1295 Self::CompileDependent => "compile_dependent",
1296 Self::SemanticDependent => "semantic_dependent",
1297 Self::ArtifactDependent => "artifact_dependent",
1298 Self::ConfigurationChanged => "configuration_changed",
1299 Self::CompilerContractChanged => "compiler_contract_changed",
1300 }
1301 }
1302}
1303#[derive(Debug, Clone, PartialEq, Eq)]
1304pub struct InvalidatedUnit {
1305 pub source_unit_id: SourceUnitId,
1306 pub reason: InvalidationReason,
1307 pub caused_by: Vec<SourceUnitId>,
1308}
1309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1310pub enum IncrementalStageKind {
1311 Remove,
1312 Parse,
1313 Analyze,
1314 AssembleApplicationModel,
1315 Lower,
1316 Emit,
1317 Validate,
1318 Commit,
1319}
1320impl IncrementalStageKind {
1321 const fn as_str(self) -> &'static str {
1322 match self {
1323 Self::Remove => "remove",
1324 Self::Parse => "parse",
1325 Self::Analyze => "analyze",
1326 Self::AssembleApplicationModel => "assemble_application_model",
1327 Self::Lower => "lower",
1328 Self::Emit => "emit",
1329 Self::Validate => "validate",
1330 Self::Commit => "commit",
1331 }
1332 }
1333}
1334#[derive(Debug, Clone, PartialEq, Eq)]
1335pub struct IncrementalStage {
1336 pub stage_index: u32,
1337 pub kind: IncrementalStageKind,
1338 pub units: Vec<SourceUnitId>,
1339}
1340#[derive(Debug, Clone, PartialEq, Eq)]
1341pub struct ExpectedCommit {
1342 pub snapshot_id: WorkspaceSnapshotId,
1343 pub workspace_graph_schema_version: u32,
1344}
1345#[derive(Debug, Clone, PartialEq, Eq)]
1346pub struct IncrementalPlan {
1347 pub schema_version: u32,
1348 pub plan_id: IncrementalPlanId,
1349 pub baseline_snapshot_id: WorkspaceSnapshotId,
1350 pub candidate_snapshot_id: WorkspaceSnapshotId,
1351 pub mode: IncrementalMode,
1352 pub changes: WorkspaceChangeSet,
1353 pub invalidated_units: Vec<InvalidatedUnit>,
1354 pub stages: Vec<IncrementalStage>,
1355 pub retained_products: Vec<ProductKey>,
1356 pub discarded_products: Vec<ProductKey>,
1357 pub expected_commit: ExpectedCommit,
1358}
1359impl IncrementalPlan {
1360 pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
1361 if self.schema_version != INCREMENTAL_PLAN_SCHEMA_VERSION {
1362 return Err(PlatformSerializationError {
1363 message: "unsupported incremental plan schema version".into(),
1364 });
1365 }
1366 Ok(json_document(incremental_plan_json(self)))
1367 }
1368}
1369
1370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1373pub enum IncrementalCompilationModeV1 {
1374 Cold,
1375 NoChange,
1376 Incremental,
1377 CleanFallback,
1378}
1379impl IncrementalCompilationModeV1 {
1380 #[must_use]
1381 pub const fn as_str(self) -> &'static str {
1382 match self {
1383 Self::Cold => "cold",
1384 Self::NoChange => "no_change",
1385 Self::Incremental => "incremental",
1386 Self::CleanFallback => "clean_fallback",
1387 }
1388 }
1389}
1390
1391#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1393pub enum IncrementalInputChangeKindV1 {
1394 ConfigurationChanged,
1395 SourceDeleted,
1396 SourceAdded,
1397 SourceContentChanged,
1398}
1399impl IncrementalInputChangeKindV1 {
1400 #[must_use]
1401 pub const fn as_str(self) -> &'static str {
1402 match self {
1403 Self::ConfigurationChanged => "configuration_changed",
1404 Self::SourceDeleted => "source_deleted",
1405 Self::SourceAdded => "source_added",
1406 Self::SourceContentChanged => "source_content_changed",
1407 }
1408 }
1409}
1410
1411#[derive(Debug, Clone, PartialEq, Eq)]
1412pub struct IncrementalInputChangeV1 {
1413 pub kind: IncrementalInputChangeKindV1,
1414 pub identity: String,
1416}
1417
1418#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1420pub enum IncrementalFallbackReasonV1 {
1421 NoBaseline,
1422 ServiceRestart,
1423 ConfigurationChanged,
1424 CompilerVersionMismatch,
1425 BuildProtocolMismatch,
1426 ProductSchemaMismatch,
1427 MalformedBaselineGraph,
1428 IncompleteDependencyAuthority,
1429 UnresolvedCanonicalIdentity,
1430 SourceUniverseMembershipUnmodeled,
1431 ReuseProductRejected,
1432 IncrementalInvariantFailure,
1433}
1434impl IncrementalFallbackReasonV1 {
1435 #[must_use]
1436 pub const fn code(self) -> &'static str {
1437 match self {
1438 Self::NoBaseline => "L5F000_NO_BASELINE",
1439 Self::ServiceRestart => "L5F001_SERVICE_RESTART",
1440 Self::ConfigurationChanged => "L5F002_CONFIGURATION_CHANGED",
1441 Self::CompilerVersionMismatch => "L5F003_COMPILER_VERSION_MISMATCH",
1442 Self::BuildProtocolMismatch => "L5F004_BUILD_PROTOCOL_MISMATCH",
1443 Self::ProductSchemaMismatch => "L5F005_PRODUCT_SCHEMA_MISMATCH",
1444 Self::MalformedBaselineGraph => "L5F006_MALFORMED_BASELINE_GRAPH",
1445 Self::IncompleteDependencyAuthority => "L5F007_INCOMPLETE_DEPENDENCY_AUTHORITY",
1446 Self::UnresolvedCanonicalIdentity => "L5F008_UNRESOLVED_CANONICAL_IDENTITY",
1447 Self::SourceUniverseMembershipUnmodeled => {
1448 "L5F009_SOURCE_UNIVERSE_MEMBERSHIP_UNMODELED"
1449 }
1450 Self::ReuseProductRejected => "L5F010_REUSE_PRODUCT_REJECTED",
1451 Self::IncrementalInvariantFailure => "L5F011_INCREMENTAL_INVARIANT_FAILURE",
1452 }
1453 }
1454}
1455
1456#[derive(Debug, Clone, PartialEq, Eq)]
1458pub struct IncrementalCompilationPlanV1 {
1459 pub schema: &'static str,
1460 pub baseline_publication_identity: Option<String>,
1461 pub candidate_configuration_fingerprint: Digest,
1462 pub candidate_source_universe_fingerprint: Digest,
1463 pub input_changes: Vec<IncrementalInputChangeV1>,
1464 pub directly_invalidated: Vec<SourceUnitId>,
1465 pub invalidation_closure: Vec<SourceUnitId>,
1466 pub reusable_product_identities: Vec<ProductKey>,
1467 pub recompute_work_units: Vec<SourceUnitId>,
1468 pub fallback_reasons: Vec<IncrementalFallbackReasonV1>,
1469 pub mode: IncrementalCompilationModeV1,
1470 pub plan_fingerprint: Digest,
1471}
1472impl IncrementalCompilationPlanV1 {
1473 pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
1474 if self.schema != INCREMENTAL_COMPILATION_PLAN_V1_SCHEMA {
1475 return Err(PlatformSerializationError {
1476 message: "unsupported L5 incremental compilation plan schema".into(),
1477 });
1478 }
1479 let expected = Digest::sha256(l5_plan_bytes_without_fingerprint(self));
1480 if expected != self.plan_fingerprint {
1481 return Err(PlatformSerializationError {
1482 message: "L5 incremental compilation plan fingerprint mismatch".into(),
1483 });
1484 }
1485 Ok(json_document(l5_plan_json(self)))
1486 }
1487}
1488
1489#[derive(Debug, Clone)]
1492pub struct CanonicalReusableProductV1 {
1493 pub source_unit_id: SourceUnitId,
1494 pub source_revision_id: SourceRevisionId,
1495 pub product_key: ProductKey,
1496 parsed: Arc<presolve_parser::ParsedFile>,
1497}
1498impl CanonicalReusableProductV1 {
1499 fn parsed(&self) -> &presolve_parser::ParsedFile {
1500 self.parsed.as_ref()
1501 }
1502}
1503
1504#[derive(Debug, Clone)]
1505pub struct IncrementalCompileWorkspaceRequestV1 {
1506 pub workspace: WorkspaceInput,
1507 pub cancellation: CancellationToken,
1508 pub plan: IncrementalCompilationPlanV1,
1509 pub reusable_products: Vec<CanonicalReusableProductV1>,
1510}
1511
1512#[derive(Debug, Clone)]
1513pub struct IncrementalCompilationOutcomeV1 {
1514 pub outcome: CompilationOutcome,
1515 pub reused_product_identities: Vec<ProductKey>,
1516 pub recomputed_work_units: Vec<SourceUnitId>,
1517 pub rejected_reuse_product_identities: Vec<ProductKey>,
1518}
1519#[must_use]
1520pub fn plan_incremental(
1521 baseline: Option<(&WorkspaceSnapshot, &WorkspaceGraph)>,
1522 candidate: &WorkspaceSnapshot,
1523 forced_full: bool,
1524) -> IncrementalPlan {
1525 let empty_id = WorkspaceSnapshotId(
1526 "snapshot:sha256:0000000000000000000000000000000000000000000000000000000000000000".into(),
1527 );
1528 let Some((before, graph)) = baseline else {
1529 return build_full_plan(
1530 empty_id.clone(),
1531 candidate,
1532 WorkspaceChangeSet {
1533 baseline_snapshot_id: empty_id.clone(),
1534 candidate_snapshot_id: candidate.snapshot_id.clone(),
1535 configuration_changed: true,
1536 added: candidate
1537 .units
1538 .iter()
1539 .map(|u| u.source_unit_id.clone())
1540 .collect(),
1541 removed: Vec::new(),
1542 modified: Vec::new(),
1543 unchanged: Vec::new(),
1544 },
1545 candidate,
1546 );
1547 };
1548 let changes = compare_snapshots(before, candidate);
1549 if forced_full || changes.configuration_changed || graph.validate().is_err() {
1550 return build_full_plan(before.snapshot_id.clone(), candidate, changes, candidate);
1551 }
1552 if before.snapshot_id == candidate.snapshot_id {
1553 return build_plan(
1554 before.snapshot_id.clone(),
1555 candidate,
1556 changes,
1557 IncrementalMode::NoOp,
1558 Vec::new(),
1559 Vec::new(),
1560 Vec::new(),
1561 );
1562 }
1563 let candidate_paths = candidate
1564 .units
1565 .iter()
1566 .map(|u| (u.source_unit_id.clone(), u.path.clone()))
1567 .collect::<BTreeMap<_, _>>();
1568 let mut causes = BTreeMap::<SourceUnitId, BTreeSet<SourceUnitId>>::new();
1569 let mut reasons = BTreeMap::<SourceUnitId, InvalidationReason>::new();
1570 for id in &changes.added {
1571 causes.entry(id.clone()).or_default().insert(id.clone());
1572 reasons.insert(id.clone(), InvalidationReason::Added);
1573 }
1574 for id in &changes.modified {
1575 causes.entry(id.clone()).or_default().insert(id.clone());
1576 reasons.insert(id.clone(), InvalidationReason::Modified);
1577 }
1578 let mut frontier = changes.modified.clone();
1579 frontier.extend(changes.removed.clone());
1580 while let Some(changed) = frontier.pop() {
1581 for edge in graph.direct_dependents(&changed) {
1582 if !candidate_paths.contains_key(&edge.source) {
1583 continue;
1584 }
1585 let reason = match edge.kind {
1586 WorkspaceDependencyKind::Compile => InvalidationReason::CompileDependent,
1587 WorkspaceDependencyKind::Semantic => InvalidationReason::SemanticDependent,
1588 WorkspaceDependencyKind::Artifact => InvalidationReason::ArtifactDependent,
1589 };
1590 let added = causes
1591 .entry(edge.source.clone())
1592 .or_default()
1593 .insert(changed.clone());
1594 reasons.entry(edge.source.clone()).or_insert(reason);
1595 if added {
1596 frontier.push(edge.source.clone());
1597 }
1598 }
1599 }
1600 let mut invalidated = causes
1601 .into_iter()
1602 .map(|(id, causes)| InvalidatedUnit {
1603 reason: reasons[&id],
1604 source_unit_id: id,
1605 caused_by: causes.into_iter().collect(),
1606 })
1607 .collect::<Vec<_>>();
1608 invalidated.sort_by_key(|item| candidate_paths.get(&item.source_unit_id).cloned());
1609 let impacted = invalidated
1610 .iter()
1611 .map(|item| item.source_unit_id.clone())
1612 .collect::<Vec<_>>();
1613 let discarded = graph
1614 .units
1615 .iter()
1616 .filter(|unit| {
1617 impacted.contains(&unit.source_unit_id)
1618 || changes.removed.contains(&unit.source_unit_id)
1619 })
1620 .flat_map(|unit| {
1621 unit.products
1622 .iter()
1623 .map(|product| product.product_key.clone())
1624 })
1625 .collect::<Vec<_>>();
1626 build_plan(
1627 before.snapshot_id.clone(),
1628 candidate,
1629 changes,
1630 IncrementalMode::Incremental,
1631 invalidated,
1632 impacted,
1633 discarded,
1634 )
1635}
1636
1637#[must_use]
1641pub fn source_universe_fingerprint_v1(snapshot: &WorkspaceSnapshot) -> Digest {
1642 let mut bytes = Vec::new();
1643 for unit in &snapshot.units {
1644 for value in [
1645 unit.source_unit_id.as_str(),
1646 unit.language.as_str(),
1647 unit.source_revision_id.as_str(),
1648 ] {
1649 bytes.extend_from_slice(&(value.len() as u64).to_be_bytes());
1650 bytes.extend_from_slice(value.as_bytes());
1651 }
1652 }
1653 Digest::sha256(bytes)
1654}
1655
1656#[must_use]
1660pub fn plan_incremental_compilation_v1(
1661 baseline: Option<(&WorkspaceSnapshot, &WorkspaceGraph)>,
1662 candidate: &WorkspaceSnapshot,
1663) -> IncrementalCompilationPlanV1 {
1664 let mut changes = Vec::new();
1665 let mut direct = Vec::new();
1666 let mut closure = Vec::new();
1667 let mut reusable = Vec::new();
1668 let mut work = Vec::new();
1669 let mut fallback = Vec::new();
1670 let mut mode = IncrementalCompilationModeV1::Cold;
1671 let baseline_publication_identity =
1672 baseline.map(|(snapshot, _)| snapshot.snapshot_id.to_string());
1673
1674 if let Some((before, graph)) = baseline {
1675 let old = before
1676 .units
1677 .iter()
1678 .map(|unit| (unit.source_unit_id.clone(), unit))
1679 .collect::<BTreeMap<_, _>>();
1680 let current = candidate
1681 .units
1682 .iter()
1683 .map(|unit| (unit.source_unit_id.clone(), unit))
1684 .collect::<BTreeMap<_, _>>();
1685 if before.configuration_fingerprint != candidate.configuration_fingerprint {
1686 changes.push(IncrementalInputChangeV1 {
1687 kind: IncrementalInputChangeKindV1::ConfigurationChanged,
1688 identity: "configuration".into(),
1689 });
1690 fallback.push(IncrementalFallbackReasonV1::ConfigurationChanged);
1691 }
1692 for unit in &before.units {
1693 if !current.contains_key(&unit.source_unit_id) {
1694 changes.push(IncrementalInputChangeV1 {
1695 kind: IncrementalInputChangeKindV1::SourceDeleted,
1696 identity: unit.source_unit_id.to_string(),
1697 });
1698 }
1699 }
1700 for unit in &candidate.units {
1701 match old.get(&unit.source_unit_id) {
1702 None => changes.push(IncrementalInputChangeV1 {
1703 kind: IncrementalInputChangeKindV1::SourceAdded,
1704 identity: unit.source_unit_id.to_string(),
1705 }),
1706 Some(previous) if previous.source_revision_id != unit.source_revision_id => {
1707 changes.push(IncrementalInputChangeV1 {
1708 kind: IncrementalInputChangeKindV1::SourceContentChanged,
1709 identity: unit.source_unit_id.to_string(),
1710 });
1711 direct.push(unit.source_unit_id.clone());
1712 }
1713 Some(_) => {}
1714 }
1715 }
1716 changes.sort_by(|left, right| {
1717 left.kind
1718 .cmp(&right.kind)
1719 .then_with(|| left.identity.as_bytes().cmp(right.identity.as_bytes()))
1720 });
1721 if graph.validate().is_err() || graph.snapshot_id != before.snapshot_id {
1722 fallback.push(IncrementalFallbackReasonV1::MalformedBaselineGraph);
1723 }
1724 if changes.iter().any(|change| {
1725 matches!(
1726 change.kind,
1727 IncrementalInputChangeKindV1::SourceAdded
1728 | IncrementalInputChangeKindV1::SourceDeleted
1729 )
1730 }) {
1731 fallback.push(IncrementalFallbackReasonV1::SourceUniverseMembershipUnmodeled);
1732 }
1733 if before.compiler_contract != candidate.compiler_contract {
1734 fallback.push(IncrementalFallbackReasonV1::CompilerVersionMismatch);
1735 }
1736 if fallback.is_empty() {
1737 let mut invalidated = direct.iter().cloned().collect::<BTreeSet<_>>();
1738 let mut frontier = direct.clone();
1739 while let Some(current_id) = frontier.pop() {
1740 for edge in graph.direct_dependents(¤t_id) {
1741 if invalidated.insert(edge.source.clone()) {
1742 frontier.push(edge.source.clone());
1743 }
1744 }
1745 }
1746 closure = invalidated.into_iter().collect();
1747 work.clone_from(&closure);
1748 let candidate_by_id = candidate
1749 .units
1750 .iter()
1751 .map(|unit| (unit.source_unit_id.clone(), unit))
1752 .collect::<BTreeMap<_, _>>();
1753 for unit in &graph.units {
1754 let Some(candidate_unit) = candidate_by_id.get(&unit.source_unit_id) else {
1755 continue;
1756 };
1757 if closure.contains(&unit.source_unit_id)
1758 || candidate_unit.source_revision_id != unit.source_revision_id
1759 {
1760 continue;
1761 }
1762 for product in &unit.products {
1763 if product.product_kind == ProductKind::Parse {
1764 reusable.push(product.product_key.clone());
1765 }
1766 }
1767 }
1768 mode = if changes.is_empty() {
1769 IncrementalCompilationModeV1::NoChange
1770 } else if reusable.is_empty() {
1771 fallback.push(IncrementalFallbackReasonV1::IncompleteDependencyAuthority);
1774 IncrementalCompilationModeV1::CleanFallback
1775 } else {
1776 IncrementalCompilationModeV1::Incremental
1777 };
1778 }
1779 if !fallback.is_empty() {
1780 mode = IncrementalCompilationModeV1::CleanFallback;
1781 direct.clear();
1782 closure.clear();
1783 reusable.clear();
1784 work = candidate
1785 .units
1786 .iter()
1787 .map(|unit| unit.source_unit_id.clone())
1788 .collect();
1789 }
1790 }
1791 fallback.sort();
1792 fallback.dedup();
1793 direct.sort();
1794 direct.dedup();
1795 closure.sort();
1796 closure.dedup();
1797 reusable.sort();
1798 reusable.dedup();
1799 work.sort();
1800 work.dedup();
1801 let mut plan = IncrementalCompilationPlanV1 {
1802 schema: INCREMENTAL_COMPILATION_PLAN_V1_SCHEMA,
1803 baseline_publication_identity,
1804 candidate_configuration_fingerprint: candidate.configuration_fingerprint.clone(),
1805 candidate_source_universe_fingerprint: source_universe_fingerprint_v1(candidate),
1806 input_changes: changes,
1807 directly_invalidated: direct,
1808 invalidation_closure: closure,
1809 reusable_product_identities: reusable,
1810 recompute_work_units: work,
1811 fallback_reasons: fallback,
1812 mode,
1813 plan_fingerprint: Digest::sha256([]),
1814 };
1815 plan.plan_fingerprint = Digest::sha256(l5_plan_bytes_without_fingerprint(&plan));
1816 plan
1817}
1818
1819pub fn force_clean_fallback_v1(
1823 plan: &mut IncrementalCompilationPlanV1,
1824 candidate: &WorkspaceSnapshot,
1825 reason: IncrementalFallbackReasonV1,
1826) {
1827 plan.mode = IncrementalCompilationModeV1::CleanFallback;
1828 plan.directly_invalidated.clear();
1829 plan.invalidation_closure.clear();
1830 plan.reusable_product_identities.clear();
1831 plan.recompute_work_units = candidate
1832 .units
1833 .iter()
1834 .map(|unit| unit.source_unit_id.clone())
1835 .collect();
1836 plan.fallback_reasons.push(reason);
1837 plan.fallback_reasons.sort();
1838 plan.fallback_reasons.dedup();
1839 plan.plan_fingerprint = Digest::sha256(l5_plan_bytes_without_fingerprint(plan));
1840}
1841fn build_full_plan(
1842 baseline: WorkspaceSnapshotId,
1843 candidate: &WorkspaceSnapshot,
1844 changes: WorkspaceChangeSet,
1845 _snapshot: &WorkspaceSnapshot,
1846) -> IncrementalPlan {
1847 let invalidated = candidate
1848 .units
1849 .iter()
1850 .map(|unit| InvalidatedUnit {
1851 source_unit_id: unit.source_unit_id.clone(),
1852 reason: InvalidationReason::ConfigurationChanged,
1853 caused_by: Vec::new(),
1854 })
1855 .collect();
1856 build_plan(
1857 baseline,
1858 candidate,
1859 changes,
1860 IncrementalMode::Full,
1861 invalidated,
1862 candidate
1863 .units
1864 .iter()
1865 .map(|u| u.source_unit_id.clone())
1866 .collect(),
1867 Vec::new(),
1868 )
1869}
1870fn build_plan(
1871 baseline: WorkspaceSnapshotId,
1872 candidate: &WorkspaceSnapshot,
1873 changes: WorkspaceChangeSet,
1874 mode: IncrementalMode,
1875 invalidated: Vec<InvalidatedUnit>,
1876 impacted: Vec<SourceUnitId>,
1877 mut discarded: Vec<ProductKey>,
1878) -> IncrementalPlan {
1879 let mut stages = Vec::new();
1880 let mut push = |kind, units: Vec<SourceUnitId>| {
1881 if !units.is_empty()
1882 || matches!(
1883 kind,
1884 IncrementalStageKind::Validate
1885 | IncrementalStageKind::Commit
1886 | IncrementalStageKind::AssembleApplicationModel
1887 )
1888 {
1889 let index = stages.len() as u32;
1890 stages.push(IncrementalStage {
1891 stage_index: index,
1892 kind,
1893 units,
1894 });
1895 }
1896 };
1897 push(IncrementalStageKind::Remove, changes.removed.clone());
1898 if !matches!(mode, IncrementalMode::NoOp) {
1899 push(IncrementalStageKind::Parse, impacted.clone());
1900 push(IncrementalStageKind::Analyze, impacted.clone());
1901 push(IncrementalStageKind::AssembleApplicationModel, Vec::new());
1902 push(IncrementalStageKind::Lower, impacted.clone());
1903 push(IncrementalStageKind::Emit, impacted.clone());
1904 }
1905 push(IncrementalStageKind::Validate, Vec::new());
1906 push(IncrementalStageKind::Commit, Vec::new());
1907 discarded.sort();
1908 discarded.dedup();
1909 let identity = format!(
1910 "{}|{}|{}|{:?}",
1911 baseline,
1912 candidate.snapshot_id,
1913 mode.as_str(),
1914 stages
1915 );
1916 let plan_id = IncrementalPlanId(hashed_id(
1917 "incremental-plan",
1918 "incremental-plan-v1",
1919 &[identity.as_bytes()],
1920 ));
1921 IncrementalPlan {
1922 schema_version: INCREMENTAL_PLAN_SCHEMA_VERSION,
1923 plan_id,
1924 baseline_snapshot_id: baseline,
1925 candidate_snapshot_id: candidate.snapshot_id.clone(),
1926 mode,
1927 changes,
1928 invalidated_units: invalidated,
1929 stages,
1930 retained_products: Vec::new(),
1931 discarded_products: discarded,
1932 expected_commit: ExpectedCommit {
1933 snapshot_id: candidate.snapshot_id.clone(),
1934 workspace_graph_schema_version: WORKSPACE_GRAPH_SCHEMA_VERSION,
1935 },
1936 }
1937}
1938
1939pub trait CachedProduct: Send + Sync {
1940 fn kind(&self) -> ProductKind;
1941 fn digest(&self) -> &Digest;
1942}
1943#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1944pub enum CacheEntryState {
1945 AttemptLocal,
1946 Committed,
1947}
1948#[derive(Debug, Clone, PartialEq, Eq)]
1949pub struct CacheLimits {
1950 pub maximum_entries: usize,
1951 pub maximum_weight: u64,
1952}
1953impl Default for CacheLimits {
1954 fn default() -> Self {
1955 Self {
1956 maximum_entries: 1024,
1957 maximum_weight: 268_435_456,
1958 }
1959 }
1960}
1961pub struct CacheEntry {
1962 pub key: ProductKey,
1963 pub content_digest: Digest,
1964 pub state: CacheEntryState,
1965 pub weight: u64,
1966 pub last_access: u64,
1967 pub product: Arc<dyn CachedProduct>,
1968}
1969pub struct ProductCache {
1970 entries: BTreeMap<ProductKey, CacheEntry>,
1971 total_weight: u64,
1972 limits: CacheLimits,
1973 next_access: u64,
1974}
1975impl ProductCache {
1976 #[must_use]
1977 pub fn new(limits: CacheLimits) -> Self {
1978 Self {
1979 entries: BTreeMap::new(),
1980 total_weight: 0,
1981 limits,
1982 next_access: 0,
1983 }
1984 }
1985 pub fn get(&mut self, key: &ProductKey) -> Option<Arc<dyn CachedProduct>> {
1986 let entry = self.entries.get_mut(key)?;
1987 if entry.product.kind() != product_kind_from_key(key, &entry.key)
1988 || entry.product.digest() != &entry.content_digest
1989 {
1990 self.total_weight -= entry.weight;
1991 self.entries.remove(key);
1992 return None;
1993 }
1994 self.next_access += 1;
1995 entry.last_access = self.next_access;
1996 Some(Arc::clone(&entry.product))
1997 }
1998 pub fn insert(&mut self, entry: CacheEntry) {
1999 if let Some(old) = self.entries.insert(entry.key.clone(), entry) {
2000 self.total_weight -= old.weight;
2001 }
2002 self.total_weight = self.entries.values().map(|entry| entry.weight).sum();
2003 }
2004 fn promote_and_evict(&mut self) {
2005 for entry in self.entries.values_mut() {
2006 if entry.state == CacheEntryState::AttemptLocal {
2007 entry.state = CacheEntryState::Committed;
2008 }
2009 }
2010 while self.entries.len() > self.limits.maximum_entries
2011 || self.total_weight > self.limits.maximum_weight
2012 {
2013 let candidate = self
2014 .entries
2015 .iter()
2016 .filter(|(_, entry)| entry.state == CacheEntryState::Committed)
2017 .min_by_key(|(key, entry)| (entry.last_access, (*key).clone()))
2018 .map(|(key, _)| key.clone());
2019 let Some(key) = candidate else { break };
2020 if let Some(entry) = self.entries.remove(&key) {
2021 self.total_weight -= entry.weight;
2022 }
2023 }
2024 }
2025 #[must_use]
2026 pub fn inspection(&self) -> ProductCacheInspection {
2027 let mut entries = self
2028 .entries
2029 .values()
2030 .map(|entry| CacheInspectionEntry {
2031 product_key: entry.key.clone(),
2032 product_kind: entry.product.kind(),
2033 content_digest: entry.content_digest.clone(),
2034 state: entry.state,
2035 weight: entry.weight,
2036 })
2037 .collect::<Vec<_>>();
2038 entries.sort_by_key(|e| e.product_key.clone());
2039 ProductCacheInspection {
2040 schema_version: PRODUCT_CACHE_INSPECTION_SCHEMA_VERSION,
2041 entry_count: entries.len(),
2042 total_weight: self.total_weight,
2043 limits: self.limits.clone(),
2044 entries,
2045 }
2046 }
2047}
2048fn product_kind_from_key(_: &ProductKey, _: &ProductKey) -> ProductKind {
2049 ProductKind::Parse
2050}
2051#[derive(Debug, Clone, PartialEq, Eq)]
2052pub struct CacheInspectionEntry {
2053 pub product_key: ProductKey,
2054 pub product_kind: ProductKind,
2055 pub content_digest: Digest,
2056 pub state: CacheEntryState,
2057 pub weight: u64,
2058}
2059#[derive(Debug, Clone, PartialEq, Eq)]
2060pub struct ProductCacheInspection {
2061 pub schema_version: u32,
2062 pub entry_count: usize,
2063 pub total_weight: u64,
2064 pub limits: CacheLimits,
2065 pub entries: Vec<CacheInspectionEntry>,
2066}
2067impl ProductCacheInspection {
2068 #[must_use]
2069 pub fn to_canonical_json(&self) -> Vec<u8> {
2070 json_document(cache_json(self))
2071 }
2072}
2073
2074#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2075pub enum CompilerSessionStatus {
2076 Empty,
2077 Ready,
2078 Compiling,
2079 Failed,
2080 Closed,
2081}
2082#[derive(Debug, Clone)]
2083pub struct CommittedWorkspaceState {
2084 pub snapshot: Arc<WorkspaceSnapshot>,
2085 pub graph: Arc<WorkspaceGraph>,
2086 pub committed_product_keys: Vec<ProductKey>,
2087 pub committed_workspace_product_keys: Vec<WorkspaceProductKey>,
2088}
2089#[derive(Debug, Clone)]
2090pub struct CompilationAttemptState {
2091 pub attempt_id: CompilationAttemptId,
2092 pub baseline_snapshot_id: Option<WorkspaceSnapshotId>,
2093 pub candidate_snapshot: Arc<WorkspaceSnapshot>,
2094 pub plan: Arc<IncrementalPlan>,
2095 pub phase: AttemptPhase,
2096}
2097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2098pub enum AttemptPhase {
2099 Planned,
2100 Executing,
2101 Validating,
2102 Committing,
2103}
2104pub struct CompilerSessionState {
2105 pub session_id: CompilerSessionId,
2106 pub workspace_id: WorkspaceId,
2107 pub compiler_contract: ContractVersion,
2108 pub status: CompilerSessionStatus,
2109 pub committed: Option<CommittedWorkspaceState>,
2110 pub active_attempt: Option<CompilationAttemptState>,
2111 pub cache: ProductCache,
2112 pub next_attempt: u64,
2113}
2114#[derive(Debug, Clone)]
2115pub struct CancellationToken(Arc<AtomicBool>);
2116impl CancellationToken {
2117 #[must_use]
2118 pub fn new() -> Self {
2119 Self(Arc::new(AtomicBool::new(false)))
2120 }
2121 pub fn cancel(&self) {
2122 self.0.store(true, Ordering::Release);
2123 }
2124 #[must_use]
2125 pub fn is_cancelled(&self) -> bool {
2126 self.0.load(Ordering::Acquire)
2127 }
2128}
2129impl Default for CancellationToken {
2130 fn default() -> Self {
2131 Self::new()
2132 }
2133}
2134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2135pub enum RequestedCompilationMode {
2136 Automatic,
2137 Full,
2138}
2139#[derive(Debug, Clone)]
2140pub struct CompileWorkspaceRequest {
2141 pub workspace: WorkspaceInput,
2142 pub mode: RequestedCompilationMode,
2143 pub cancellation: CancellationToken,
2144}
2145#[derive(Debug, Clone)]
2146pub struct CommittedCompilation {
2147 pub snapshot: Arc<WorkspaceSnapshot>,
2148 pub graph: Arc<WorkspaceGraph>,
2149 pub plan: Arc<IncrementalPlan>,
2150 pub query_snapshot: Arc<ToolingQuerySnapshotV1>,
2153 pub reusable_products: Vec<CanonicalReusableProductV1>,
2156}
2157#[derive(Debug, Clone)]
2158pub struct RejectedCompilation {
2159 pub message: String,
2160}
2161#[derive(Debug, Clone)]
2162pub struct CancelledCompilation {
2163 pub message: String,
2164}
2165#[derive(Debug, Clone)]
2166pub enum CompilationOutcome {
2167 Committed(CommittedCompilation),
2168 Rejected(RejectedCompilation),
2169 Cancelled(CancelledCompilation),
2170 PlatformFailure(PlatformFailure),
2171}
2172impl CompilerSessionState {
2173 #[must_use]
2174 pub fn new(
2175 workspace: WorkspaceId,
2176 compiler_contract: ContractVersion,
2177 limits: CacheLimits,
2178 ) -> Self {
2179 let session = NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed);
2180 Self {
2181 session_id: CompilerSessionId(format!("session:{session}")),
2182 workspace_id: workspace,
2183 compiler_contract,
2184 status: CompilerSessionStatus::Empty,
2185 committed: None,
2186 active_attempt: None,
2187 cache: ProductCache::new(limits),
2188 next_attempt: 1,
2189 }
2190 }
2191 #[must_use]
2192 pub fn committed_snapshot(&self) -> Option<&WorkspaceSnapshot> {
2193 self.committed.as_ref().map(|state| state.snapshot.as_ref())
2194 }
2195 #[must_use]
2196 pub fn committed_graph(&self) -> Option<&WorkspaceGraph> {
2197 self.committed.as_ref().map(|state| state.graph.as_ref())
2198 }
2199 #[must_use]
2200 pub const fn status(&self) -> CompilerSessionStatus {
2201 self.status
2202 }
2203 pub fn close(&mut self) {
2204 self.status = CompilerSessionStatus::Closed;
2205 self.active_attempt = None;
2206 self.committed = None;
2207 self.cache = ProductCache::new(CacheLimits::default());
2208 }
2209 pub fn compile_workspace(&mut self, request: CompileWorkspaceRequest) -> CompilationOutcome {
2210 if self.status == CompilerSessionStatus::Closed {
2211 return CompilationOutcome::PlatformFailure(PlatformFailure::new(
2212 PlatformFailureCode::SessionClosed,
2213 "compiler session is closed",
2214 ));
2215 }
2216 if self.active_attempt.is_some() {
2217 return CompilationOutcome::PlatformFailure(PlatformFailure::new(
2218 PlatformFailureCode::ConcurrentAttempt,
2219 "compiler session already has an active attempt",
2220 ));
2221 }
2222 let snapshot = match WorkspaceSnapshot::from_input(&request.workspace) {
2223 Ok(snapshot) => snapshot,
2224 Err(error) => return self.fail(error),
2225 };
2226 if request.cancellation.is_cancelled() {
2227 return self.cancel();
2228 }
2229 let plan = Arc::new(plan_incremental(
2230 self.committed
2231 .as_ref()
2232 .map(|state| (state.snapshot.as_ref(), state.graph.as_ref())),
2233 &snapshot,
2234 request.mode == RequestedCompilationMode::Full,
2235 ));
2236 let attempt_id = CompilationAttemptId(format!(
2237 "attempt:{}:{}",
2238 self.session_id.as_str().trim_start_matches("session:"),
2239 self.next_attempt
2240 ));
2241 self.next_attempt += 1;
2242 self.status = CompilerSessionStatus::Compiling;
2243 self.active_attempt = Some(CompilationAttemptState {
2244 attempt_id,
2245 baseline_snapshot_id: self
2246 .committed
2247 .as_ref()
2248 .map(|state| state.snapshot.snapshot_id.clone()),
2249 candidate_snapshot: Arc::new(snapshot.clone()),
2250 plan: Arc::clone(&plan),
2251 phase: AttemptPhase::Executing,
2252 });
2253 let (graph, query_snapshot, reusable_products, _, _) =
2254 match build_graph_with_reuse(&request.workspace, &snapshot, &[], &BTreeSet::new()) {
2255 Ok(result) => result,
2256 Err(error) => return self.fail(error),
2257 };
2258 if request.cancellation.is_cancelled() {
2259 return self.cancel();
2260 }
2261 if let Err(error) = snapshot.validate().and_then(|()| graph.validate()) {
2262 return self.fail(error);
2263 }
2264 if let Some(attempt) = self.active_attempt.as_mut() {
2265 attempt.phase = AttemptPhase::Committing;
2266 } else {
2267 return self.fail(PlatformFailure::new(
2268 PlatformFailureCode::CommitInvariantFailed,
2269 "active compiler attempt disappeared before commit",
2270 ));
2271 }
2272 let committed = CommittedWorkspaceState {
2273 snapshot: Arc::new(snapshot),
2274 graph: Arc::new(graph),
2275 committed_product_keys: Vec::new(),
2276 committed_workspace_product_keys: Vec::new(),
2277 };
2278 self.workspace_id = committed.snapshot.workspace_id.clone();
2279 self.compiler_contract = committed.snapshot.compiler_contract.clone();
2280 self.committed = Some(committed.clone());
2281 self.active_attempt = None;
2282 self.status = CompilerSessionStatus::Ready;
2283 self.cache.promote_and_evict();
2284 CompilationOutcome::Committed(CommittedCompilation {
2285 snapshot: committed.snapshot,
2286 graph: committed.graph,
2287 plan,
2288 query_snapshot: Arc::new(query_snapshot),
2289 reusable_products,
2290 })
2291 }
2292
2293 pub fn compile_workspace_incremental_v1(
2298 &mut self,
2299 request: IncrementalCompileWorkspaceRequestV1,
2300 ) -> IncrementalCompilationOutcomeV1 {
2301 if self.status == CompilerSessionStatus::Closed {
2302 return IncrementalCompilationOutcomeV1 {
2303 outcome: CompilationOutcome::PlatformFailure(PlatformFailure::new(
2304 PlatformFailureCode::SessionClosed,
2305 "compiler session is closed",
2306 )),
2307 reused_product_identities: Vec::new(),
2308 recomputed_work_units: Vec::new(),
2309 rejected_reuse_product_identities: Vec::new(),
2310 };
2311 }
2312 let snapshot = match WorkspaceSnapshot::from_input(&request.workspace) {
2313 Ok(snapshot) => snapshot,
2314 Err(error) => {
2315 return IncrementalCompilationOutcomeV1 {
2316 outcome: self.fail(error),
2317 reused_product_identities: Vec::new(),
2318 recomputed_work_units: Vec::new(),
2319 rejected_reuse_product_identities: Vec::new(),
2320 };
2321 }
2322 };
2323 if request.cancellation.is_cancelled() {
2324 return IncrementalCompilationOutcomeV1 {
2325 outcome: self.cancel(),
2326 reused_product_identities: Vec::new(),
2327 recomputed_work_units: Vec::new(),
2328 rejected_reuse_product_identities: Vec::new(),
2329 };
2330 }
2331 if request.plan.to_canonical_json().is_err()
2332 || request.plan.candidate_configuration_fingerprint
2333 != snapshot.configuration_fingerprint
2334 || request.plan.candidate_source_universe_fingerprint
2335 != source_universe_fingerprint_v1(&snapshot)
2336 {
2337 return IncrementalCompilationOutcomeV1 {
2338 outcome: self.fail(PlatformFailure::new(
2339 PlatformFailureCode::InvalidIncrementalPlan,
2340 "L5 incremental plan does not match the canonical candidate",
2341 )),
2342 reused_product_identities: Vec::new(),
2343 recomputed_work_units: Vec::new(),
2344 rejected_reuse_product_identities: Vec::new(),
2345 };
2346 }
2347 let attempt_id = CompilationAttemptId(format!(
2348 "attempt:{}:{}",
2349 self.session_id.as_str().trim_start_matches("session:"),
2350 self.next_attempt
2351 ));
2352 self.next_attempt += 1;
2353 self.status = CompilerSessionStatus::Compiling;
2354 let legacy_plan = Arc::new(plan_incremental(None, &snapshot, true));
2355 self.active_attempt = Some(CompilationAttemptState {
2356 attempt_id,
2357 baseline_snapshot_id: request
2358 .plan
2359 .baseline_publication_identity
2360 .as_ref()
2361 .map(|value| WorkspaceSnapshotId(value.clone())),
2362 candidate_snapshot: Arc::new(snapshot.clone()),
2363 plan: Arc::clone(&legacy_plan),
2364 phase: AttemptPhase::Executing,
2365 });
2366 let invalidated = request
2367 .plan
2368 .invalidation_closure
2369 .iter()
2370 .cloned()
2371 .collect::<BTreeSet<_>>();
2372 let (graph, query_snapshot, reusable_products, reused, rejected) =
2373 match build_graph_with_reuse(
2374 &request.workspace,
2375 &snapshot,
2376 &request.reusable_products,
2377 &invalidated,
2378 ) {
2379 Ok(result) => result,
2380 Err(error) => {
2381 return IncrementalCompilationOutcomeV1 {
2382 outcome: self.fail(error),
2383 reused_product_identities: Vec::new(),
2384 recomputed_work_units: Vec::new(),
2385 rejected_reuse_product_identities: Vec::new(),
2386 };
2387 }
2388 };
2389 if let Err(error) = snapshot.validate().and_then(|()| graph.validate()) {
2390 return IncrementalCompilationOutcomeV1 {
2391 outcome: self.fail(error),
2392 reused_product_identities: Vec::new(),
2393 recomputed_work_units: Vec::new(),
2394 rejected_reuse_product_identities: rejected,
2395 };
2396 }
2397 let committed = CommittedWorkspaceState {
2398 snapshot: Arc::new(snapshot),
2399 graph: Arc::new(graph),
2400 committed_product_keys: Vec::new(),
2401 committed_workspace_product_keys: Vec::new(),
2402 };
2403 self.workspace_id = committed.snapshot.workspace_id.clone();
2404 self.compiler_contract = committed.snapshot.compiler_contract.clone();
2405 self.committed = Some(committed.clone());
2406 self.active_attempt = None;
2407 self.status = CompilerSessionStatus::Ready;
2408 IncrementalCompilationOutcomeV1 {
2409 outcome: CompilationOutcome::Committed(CommittedCompilation {
2410 snapshot: committed.snapshot,
2411 graph: committed.graph,
2412 plan: legacy_plan,
2413 query_snapshot: Arc::new(query_snapshot),
2414 reusable_products,
2415 }),
2416 reused_product_identities: reused,
2417 recomputed_work_units: request.plan.recompute_work_units,
2418 rejected_reuse_product_identities: rejected,
2419 }
2420 }
2421 fn fail(&mut self, error: PlatformFailure) -> CompilationOutcome {
2422 self.active_attempt = None;
2423 self.status = CompilerSessionStatus::Failed;
2424 CompilationOutcome::PlatformFailure(error)
2425 }
2426 fn cancel(&mut self) -> CompilationOutcome {
2427 self.active_attempt = None;
2428 self.status = CompilerSessionStatus::Failed;
2429 CompilationOutcome::Cancelled(CancelledCompilation {
2430 message: "compilation cancelled".into(),
2431 })
2432 }
2433 #[must_use]
2434 pub fn inspect(&self) -> CompilerSessionInspection {
2435 CompilerSessionInspection {
2436 schema_version: COMPILER_SESSION_SCHEMA_VERSION,
2437 session_id: self.session_id.clone(),
2438 workspace_id: self.workspace_id.clone(),
2439 compiler_contract: self.compiler_contract.clone(),
2440 status: self.status,
2441 committed_snapshot_id: self
2442 .committed
2443 .as_ref()
2444 .map(|value| value.snapshot.snapshot_id.clone()),
2445 active_attempt: self
2446 .active_attempt
2447 .as_ref()
2448 .map(|value| value.attempt_id.clone()),
2449 committed_product_count: self
2450 .committed
2451 .as_ref()
2452 .map_or(0, |value| value.committed_product_keys.len()),
2453 cache: CacheSummary {
2454 entry_count: self.cache.entries.len(),
2455 total_weight: self.cache.total_weight,
2456 },
2457 }
2458 }
2459}
2460type GraphBuildResult = (
2461 WorkspaceGraph,
2462 ToolingQuerySnapshotV1,
2463 Vec<CanonicalReusableProductV1>,
2464 Vec<ProductKey>,
2465 Vec<ProductKey>,
2466);
2467
2468fn build_graph_with_reuse(
2469 input: &WorkspaceInput,
2470 snapshot: &WorkspaceSnapshot,
2471 offered: &[CanonicalReusableProductV1],
2472 invalidated: &BTreeSet<SourceUnitId>,
2473) -> Result<GraphBuildResult, PlatformFailure> {
2474 let (_, fingerprint, inputs) = normalize(input)?;
2475 let parser_contract = ContractVersion::new("parser:1");
2476 let offered_by_unit = offered
2477 .iter()
2478 .map(|product| (product.source_unit_id.clone(), product))
2479 .collect::<BTreeMap<_, _>>();
2480 let mut reused = Vec::new();
2481 let mut rejected = Vec::new();
2482 let mut parsed = Vec::new();
2483 let mut reusable_products = Vec::new();
2484 for source in &inputs {
2485 let expected_key = product_key(
2486 &source.source_unit_id,
2487 &source.source_revision_id,
2488 ProductKind::Parse,
2489 &parser_contract,
2490 &fingerprint,
2491 );
2492 let reusable = offered_by_unit
2493 .get(&source.source_unit_id)
2494 .and_then(|product| {
2495 let product = *product;
2496 let valid = !invalidated.contains(&source.source_unit_id)
2497 && product.source_revision_id == source.source_revision_id
2498 && product.product_key == expected_key
2499 && WorkspacePath::new(product.parsed().path.to_string_lossy())
2500 .is_ok_and(|path| path == source.path);
2501 if valid {
2502 Some(product)
2503 } else {
2504 rejected.push(product.product_key.clone());
2505 None
2506 }
2507 });
2508 let file = reusable.map_or_else(
2509 || presolve_parser::parse_file(source.path.as_str(), &source.source),
2510 |product| {
2511 reused.push(product.product_key.clone());
2512 product.parsed().clone()
2513 },
2514 );
2515 reusable_products.push(CanonicalReusableProductV1 {
2516 source_unit_id: source.source_unit_id.clone(),
2517 source_revision_id: source.source_revision_id.clone(),
2518 product_key: expected_key,
2519 parsed: Arc::new(file.clone()),
2520 });
2521 parsed.push(file);
2522 }
2523 let unit = CompilationUnit::from_parsed_files(parsed.clone());
2524 let model = build_application_semantic_model_for_unit(&unit);
2525 let query_snapshot = build_tooling_query_snapshot_v1(snapshot, &model).map_err(|_| {
2526 PlatformFailure::new(
2527 PlatformFailureCode::CommitInvariantFailed,
2528 "compiler query snapshot provenance is not bound to the validated workspace snapshot",
2529 )
2530 })?;
2531 let model_digest = Digest::sha256(format!(
2532 "components:{};diagnostics:{}",
2533 model.components.len(),
2534 model.diagnostics.len()
2535 ));
2536 let units = inputs
2537 .iter()
2538 .map(|source| {
2539 let key = product_key(
2540 &source.source_unit_id,
2541 &source.source_revision_id,
2542 ProductKind::Parse,
2543 &parser_contract,
2544 &fingerprint,
2545 );
2546 WorkspaceUnit {
2547 source_unit_id: source.source_unit_id.clone(),
2548 path: source.path.clone(),
2549 source_revision_id: source.source_revision_id.clone(),
2550 source_digest: source.source_digest.clone(),
2551 source_length: source.source.len() as u64,
2552 language: source.language,
2553 products: vec![UnitProductRef {
2554 product_kind: ProductKind::Parse,
2555 product_key: key,
2556 producer_contract: parser_contract.clone(),
2557 content_digest: source.source_digest.clone(),
2558 }],
2559 }
2560 })
2561 .collect::<Vec<_>>();
2562 let by_path = inputs
2563 .iter()
2564 .map(|source| (source.path.clone(), source.source_unit_id.clone()))
2565 .collect::<BTreeMap<_, _>>();
2566 let mut edges = Vec::new();
2567 for (source, file) in inputs.iter().zip(parsed.iter()) {
2568 for import in &file.imports {
2569 if let Some(target) = resolve_import(&source.path, &import.source, &by_path) {
2570 edges.push(WorkspaceDependencyEdge {
2571 source: source.source_unit_id.clone(),
2572 kind: WorkspaceDependencyKind::Compile,
2573 target,
2574 });
2575 }
2576 }
2577 }
2578 edges.sort_by_key(|edge| (edge.source.clone(), edge.kind, edge.target.clone()));
2579 edges.dedup();
2580 let asm_contract = ContractVersion::new("asm:1");
2581 let application_products = vec![ApplicationProductRef {
2582 product_kind: ApplicationProductKind::ApplicationSemanticModel,
2583 product_key: workspace_product_key(
2584 &snapshot.workspace_id,
2585 &snapshot.snapshot_id,
2586 ApplicationProductKind::ApplicationSemanticModel,
2587 &asm_contract,
2588 &fingerprint,
2589 ),
2590 producer_contract: asm_contract,
2591 content_digest: model_digest,
2592 }];
2593 let graph = WorkspaceGraph {
2594 schema_version: WORKSPACE_GRAPH_SCHEMA_VERSION,
2595 workspace_id: snapshot.workspace_id.clone(),
2596 snapshot_id: snapshot.snapshot_id.clone(),
2597 compiler_contract: snapshot.compiler_contract.clone(),
2598 units,
2599 dependency_edges: edges,
2600 application_products,
2601 };
2602 reused.sort();
2603 reused.dedup();
2604 rejected.sort();
2605 rejected.dedup();
2606 Ok((graph, query_snapshot, reusable_products, reused, rejected))
2607}
2608fn resolve_import(
2609 source: &WorkspacePath,
2610 raw: &str,
2611 known: &BTreeMap<WorkspacePath, SourceUnitId>,
2612) -> Option<SourceUnitId> {
2613 if !raw.starts_with('.') {
2614 return None;
2615 }
2616 let mut parts = source.0.split('/').collect::<Vec<_>>();
2617 parts.pop();
2618 let separated = raw.replace('\\', "/");
2619 for part in separated.split('/') {
2620 match part {
2621 "." => {}
2622 ".." => {
2623 parts.pop()?;
2624 }
2625 value => parts.push(value),
2626 }
2627 }
2628 let base = parts.join("/");
2629 for candidate in [
2630 base.clone(),
2631 format!("{base}.ts"),
2632 format!("{base}.tsx"),
2633 format!("{base}.js"),
2634 format!("{base}.jsx"),
2635 format!("{base}/index.ts"),
2636 format!("{base}/index.tsx"),
2637 ] {
2638 if let Ok(path) = WorkspacePath::new(candidate) {
2639 if let Some(id) = known.get(&path) {
2640 return Some(id.clone());
2641 }
2642 }
2643 }
2644 None
2645}
2646
2647#[derive(Debug, Clone, PartialEq, Eq)]
2648pub struct CacheSummary {
2649 pub entry_count: usize,
2650 pub total_weight: u64,
2651}
2652#[derive(Debug, Clone, PartialEq, Eq)]
2653pub struct CompilerSessionInspection {
2654 pub schema_version: u32,
2655 pub session_id: CompilerSessionId,
2656 pub workspace_id: WorkspaceId,
2657 pub compiler_contract: ContractVersion,
2658 pub status: CompilerSessionStatus,
2659 pub committed_snapshot_id: Option<WorkspaceSnapshotId>,
2660 pub active_attempt: Option<CompilationAttemptId>,
2661 pub committed_product_count: usize,
2662 pub cache: CacheSummary,
2663}
2664impl CompilerSessionInspection {
2665 #[must_use]
2666 pub fn to_canonical_json(&self) -> Vec<u8> {
2667 json_document(session_json(self))
2668 }
2669}
2670#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2671pub enum PlatformFailureCode {
2672 InvalidWorkspacePath,
2673 DuplicateWorkspacePath,
2674 UnsupportedSourceLanguage,
2675 InputReadFailed,
2676 InvalidWorkspaceGraph,
2677 InvalidWorkspaceSnapshot,
2678 InvalidIncrementalPlan,
2679 UnsupportedSchemaVersion,
2680 UnsupportedProducerContract,
2681 CacheCorruption,
2682 ProductDigestMismatch,
2683 DuplicateProductMismatch,
2684 MissingRequiredProduct,
2685 DependencyAuthorityUnavailable,
2686 ConcurrentAttempt,
2687 SessionClosed,
2688 CommitInvariantFailed,
2689 IncrementalEquivalenceFailed,
2690}
2691#[derive(Debug, Clone, PartialEq, Eq)]
2692pub struct PlatformFailure {
2693 pub code: PlatformFailureCode,
2694 pub message: String,
2695 pub workspace_path: Option<WorkspacePath>,
2696 pub source_unit_id: Option<SourceUnitId>,
2697 pub product_key: Option<ProductKey>,
2698}
2699impl PlatformFailure {
2700 fn new(code: PlatformFailureCode, message: impl Into<String>) -> Self {
2701 Self {
2702 code,
2703 message: message.into(),
2704 workspace_path: None,
2705 source_unit_id: None,
2706 product_key: None,
2707 }
2708 }
2709 fn path(message: impl Into<String>, path: &str) -> Self {
2710 Self {
2711 code: PlatformFailureCode::InvalidWorkspacePath,
2712 message: message.into(),
2713 workspace_path: Some(WorkspacePath(path.replace('\\', "/"))),
2714 source_unit_id: None,
2715 product_key: None,
2716 }
2717 }
2718}
2719#[derive(Debug, Clone)]
2720pub struct PlatformSerializationError {
2721 pub message: String,
2722}
2723impl PlatformSerializationError {
2724 fn validation(error: PlatformFailure) -> Self {
2725 Self {
2726 message: error.message,
2727 }
2728 }
2729}
2730
2731fn quote(value: &str) -> String {
2732 serde_json::to_string(value).expect("strings serialize")
2733}
2734fn json_document(value: String) -> Vec<u8> {
2735 let mut value = value.into_bytes();
2736 value.push(b'\n');
2737 value
2738}
2739fn snapshot_json(snapshot: &WorkspaceSnapshot) -> String {
2740 format!("{{\"schema_version\":{},\"workspace_id\":{},\"snapshot_id\":{},\"compiler_contract\":{},\"configuration_fingerprint\":{},\"units\":[{}]}}",snapshot.schema_version,quote(snapshot.workspace_id.as_str()),quote(snapshot.snapshot_id.as_str()),quote(snapshot.compiler_contract.as_str()),quote(snapshot.configuration_fingerprint.as_str()),snapshot.units.iter().map(|unit|format!("{{\"source_unit_id\":{},\"path\":{},\"source_revision_id\":{},\"source_digest\":{},\"source_length\":{},\"language\":{}}}",quote(unit.source_unit_id.as_str()),quote(unit.path.as_str()),quote(unit.source_revision_id.as_str()),quote(unit.source_digest.as_str()),unit.source_length,quote(unit.language.as_str()))).collect::<Vec<_>>().join(","))
2741}
2742fn graph_json(graph: &WorkspaceGraph) -> String {
2743 format!("{{\"schema_version\":{},\"workspace_id\":{},\"snapshot_id\":{},\"compiler_contract\":{},\"units\":[{}],\"dependency_edges\":[{}],\"application_products\":[{}]}}",graph.schema_version,quote(graph.workspace_id.as_str()),quote(graph.snapshot_id.as_str()),quote(graph.compiler_contract.as_str()),graph.units.iter().map(|unit|format!("{{\"source_unit_id\":{},\"path\":{},\"source_revision_id\":{},\"source_digest\":{},\"source_length\":{},\"language\":{},\"products\":[{}]}}",quote(unit.source_unit_id.as_str()),quote(unit.path.as_str()),quote(unit.source_revision_id.as_str()),quote(unit.source_digest.as_str()),unit.source_length,quote(unit.language.as_str()),unit.products.iter().map(|product|format!("{{\"product_kind\":{},\"product_key\":{},\"producer_contract\":{},\"content_digest\":{}}}",quote(product.product_kind.as_str()),quote(product.product_key.as_str()),quote(product.producer_contract.as_str()),quote(product.content_digest.as_str()))).collect::<Vec<_>>().join(","))).collect::<Vec<_>>().join(","),graph.dependency_edges.iter().map(|edge|format!("{{\"source\":{},\"kind\":{},\"target\":{}}}",quote(edge.source.as_str()),quote(edge.kind.as_str()),quote(edge.target.as_str()))).collect::<Vec<_>>().join(","),graph.application_products.iter().map(|product|format!("{{\"product_kind\":{},\"product_key\":{},\"producer_contract\":{},\"content_digest\":{}}}",quote(product.product_kind.as_str()),quote(product.product_key.as_str()),quote(product.producer_contract.as_str()),quote(product.content_digest.as_str()))).collect::<Vec<_>>().join(","))
2744}
2745
2746fn incremental_plan_json(plan: &IncrementalPlan) -> String {
2747 let ids = |values: &[SourceUnitId]| {
2748 values
2749 .iter()
2750 .map(|value| quote(value.as_str()))
2751 .collect::<Vec<_>>()
2752 .join(",")
2753 };
2754 let changes = format!("{{\"baseline_snapshot_id\":{},\"candidate_snapshot_id\":{},\"configuration_changed\":{},\"added\":[{}],\"removed\":[{}],\"modified\":[{}],\"unchanged\":[{}]}}", quote(plan.changes.baseline_snapshot_id.as_str()), quote(plan.changes.candidate_snapshot_id.as_str()), plan.changes.configuration_changed, ids(&plan.changes.added), ids(&plan.changes.removed), ids(&plan.changes.modified), ids(&plan.changes.unchanged));
2755 let invalidated = plan
2756 .invalidated_units
2757 .iter()
2758 .map(|item| {
2759 format!(
2760 "{{\"source_unit_id\":{},\"reason\":{},\"caused_by\":[{}]}}",
2761 quote(item.source_unit_id.as_str()),
2762 quote(item.reason.as_str()),
2763 ids(&item.caused_by)
2764 )
2765 })
2766 .collect::<Vec<_>>()
2767 .join(",");
2768 let stages = plan
2769 .stages
2770 .iter()
2771 .map(|stage| {
2772 format!(
2773 "{{\"stage_index\":{},\"kind\":{},\"units\":[{}]}}",
2774 stage.stage_index,
2775 quote(stage.kind.as_str()),
2776 ids(&stage.units)
2777 )
2778 })
2779 .collect::<Vec<_>>()
2780 .join(",");
2781 format!("{{\"schema_version\":{},\"plan_id\":{},\"baseline_snapshot_id\":{},\"candidate_snapshot_id\":{},\"mode\":{},\"changes\":{},\"invalidated_units\":[{}],\"stages\":[{}],\"retained_products\":[{}],\"discarded_products\":[{}],\"expected_commit\":{{\"snapshot_id\":{},\"workspace_graph_schema_version\":{}}}}}", plan.schema_version, quote(plan.plan_id.as_str()), quote(plan.baseline_snapshot_id.as_str()), quote(plan.candidate_snapshot_id.as_str()), quote(plan.mode.as_str()), changes, invalidated, stages, plan.retained_products.iter().map(|key|quote(key.as_str())).collect::<Vec<_>>().join(","), plan.discarded_products.iter().map(|key|quote(key.as_str())).collect::<Vec<_>>().join(","), quote(plan.expected_commit.snapshot_id.as_str()), plan.expected_commit.workspace_graph_schema_version)
2782}
2783fn l5_plan_bytes_without_fingerprint(plan: &IncrementalCompilationPlanV1) -> Vec<u8> {
2784 l5_plan_json_without_fingerprint(plan).into_bytes()
2785}
2786fn l5_plan_json_without_fingerprint(plan: &IncrementalCompilationPlanV1) -> String {
2787 let ids = |values: &[SourceUnitId]| {
2788 values
2789 .iter()
2790 .map(|value| quote(value.as_str()))
2791 .collect::<Vec<_>>()
2792 .join(",")
2793 };
2794 let products = plan
2795 .reusable_product_identities
2796 .iter()
2797 .map(|value| quote(value.as_str()))
2798 .collect::<Vec<_>>()
2799 .join(",");
2800 let changes = plan
2801 .input_changes
2802 .iter()
2803 .map(|change| {
2804 format!(
2805 "{{\"kind\":{},\"identity\":{}}}",
2806 quote(change.kind.as_str()),
2807 quote(&change.identity)
2808 )
2809 })
2810 .collect::<Vec<_>>()
2811 .join(",");
2812 let fallback = plan
2813 .fallback_reasons
2814 .iter()
2815 .map(|reason| quote(reason.code()))
2816 .collect::<Vec<_>>()
2817 .join(",");
2818 format!(
2819 "{{\"schema\":{},\"baseline_publication_identity\":{},\"candidate_configuration_fingerprint\":{},\"candidate_source_universe_fingerprint\":{},\"input_changes\":[{}],\"directly_invalidated\":[{}],\"invalidation_closure\":[{}],\"reusable_product_identities\":[{}],\"recompute_work_units\":[{}],\"fallback_reasons\":[{}],\"mode\":{}}}",
2820 quote(plan.schema),
2821 plan.baseline_publication_identity.as_ref().map_or_else(|| "null".into(), |value| quote(value)),
2822 quote(plan.candidate_configuration_fingerprint.as_str()),
2823 quote(plan.candidate_source_universe_fingerprint.as_str()),
2824 changes,
2825 ids(&plan.directly_invalidated),
2826 ids(&plan.invalidation_closure),
2827 products,
2828 ids(&plan.recompute_work_units),
2829 fallback,
2830 quote(plan.mode.as_str()),
2831 )
2832}
2833fn l5_plan_json(plan: &IncrementalCompilationPlanV1) -> String {
2834 let without = l5_plan_json_without_fingerprint(plan);
2835 without
2836 .strip_suffix('}')
2837 .expect("canonical L5 plan is an object")
2838 .to_owned()
2839 + &format!(
2840 ",\"plan_fingerprint\":{}}}",
2841 quote(plan.plan_fingerprint.as_str())
2842 )
2843}
2844fn cache_json(cache: &ProductCacheInspection) -> String {
2845 let entries = cache.entries.iter().map(|entry| format!("{{\"product_key\":{},\"product_kind\":{},\"content_digest\":{},\"state\":{},\"weight\":{}}}", quote(entry.product_key.as_str()), quote(entry.product_kind.as_str()), quote(entry.content_digest.as_str()), quote(match entry.state { CacheEntryState::AttemptLocal => "attempt_local", CacheEntryState::Committed => "committed" }), entry.weight)).collect::<Vec<_>>().join(",");
2846 format!("{{\"schema_version\":{},\"entry_count\":{},\"total_weight\":{},\"limits\":{{\"maximum_entries\":{},\"maximum_weight\":{}}},\"entries\":[{}]}}", cache.schema_version, cache.entry_count, cache.total_weight, cache.limits.maximum_entries, cache.limits.maximum_weight, entries)
2847}
2848fn session_json(session: &CompilerSessionInspection) -> String {
2849 format!("{{\"schema_version\":{},\"session_id\":{},\"workspace_id\":{},\"compiler_contract\":{},\"status\":{},\"committed_snapshot_id\":{},\"active_attempt\":{},\"committed_product_count\":{},\"cache\":{{\"entry_count\":{},\"total_weight\":{}}}}}", session.schema_version, quote(session.session_id.as_str()), quote(session.workspace_id.as_str()), quote(session.compiler_contract.as_str()), quote(match session.status { CompilerSessionStatus::Empty => "empty", CompilerSessionStatus::Ready => "ready", CompilerSessionStatus::Compiling => "compiling", CompilerSessionStatus::Failed => "failed", CompilerSessionStatus::Closed => "closed" }), session.committed_snapshot_id.as_ref().map_or_else(|| "null".into(), |id|quote(id.as_str())), session.active_attempt.as_ref().map_or_else(|| "null".into(), |id|quote(id.as_str())), session.committed_product_count, session.cache.entry_count, session.cache.total_weight)
2850}
2851
2852pub mod workspace {
2855 pub use super::{
2856 ApplicationProductKind, ApplicationProductRef, ContractVersion, Digest, ProductKey,
2857 ProductKind, SourceLanguage, SourceRevisionId, SourceUnitId, UnitProductRef,
2858 WorkspaceConfiguration, WorkspaceDependencyEdge, WorkspaceDependencyKind, WorkspaceGraph,
2859 WorkspaceId, WorkspaceInput, WorkspacePath, WorkspaceProductKey, WorkspaceSource,
2860 WorkspaceUnit,
2861 };
2862}
2863pub mod snapshot {
2864 pub use super::{
2865 compare_snapshots, SnapshotUnit, WorkspaceChangeSet, WorkspaceSnapshot, WorkspaceSnapshotId,
2866 };
2867}
2868pub mod incremental {
2869 pub use super::{
2870 force_clean_fallback_v1, plan_incremental, plan_incremental_compilation_v1,
2871 source_universe_fingerprint_v1, CanonicalReusableProductV1, ExpectedCommit,
2872 IncrementalCompilationModeV1, IncrementalCompilationOutcomeV1,
2873 IncrementalCompilationPlanV1, IncrementalCompileWorkspaceRequestV1,
2874 IncrementalFallbackReasonV1, IncrementalInputChangeKindV1, IncrementalInputChangeV1,
2875 IncrementalMode, IncrementalPlan, IncrementalPlanId, IncrementalStage,
2876 IncrementalStageKind, InvalidatedUnit, InvalidationReason,
2877 };
2878}
2879pub mod cache {
2880 pub use super::{
2881 CacheEntry, CacheEntryState, CacheInspectionEntry, CacheLimits, CachedProduct,
2882 ProductCache, ProductCacheInspection,
2883 };
2884}
2885pub mod session {
2886 pub use super::{
2887 CancellationToken, CommittedCompilation, CompilationAttemptId, CompilationOutcome,
2888 CompileWorkspaceRequest, CompilerSessionId, CompilerSessionInspection,
2889 CompilerSessionState, CompilerSessionStatus, IncrementalCompilationOutcomeV1,
2890 IncrementalCompileWorkspaceRequestV1, RequestedCompilationMode,
2891 };
2892}
2893pub mod inspection {
2894 pub use super::{CompilerSessionInspection, ProductCacheInspection};
2895}
2896
2897#[cfg(test)]
2898mod tests {
2899 use super::*;
2900 fn source(path: &str, body: &str) -> WorkspaceSource {
2901 WorkspaceSource {
2902 path: path.into(),
2903 source: body.into(),
2904 language: None,
2905 }
2906 }
2907 fn input(sources: Vec<WorkspaceSource>) -> WorkspaceInput {
2908 WorkspaceInput::new(sources)
2909 }
2910 #[test]
2911 fn paths_normalize_and_reject_escape() {
2912 assert_eq!(
2913 WorkspacePath::new("src\\./Counter.tsx").unwrap().as_str(),
2914 "src/Counter.tsx"
2915 );
2916 assert!(WorkspacePath::new("../Counter.tsx").is_err());
2917 assert!(WorkspacePath::new("/Counter.tsx").is_err());
2918 }
2919 #[test]
2920 fn identities_are_content_and_path_sensitive() {
2921 let a =
2922 WorkspaceSnapshot::from_input(&input(vec![source("src/A.tsx", "class A {}")])).unwrap();
2923 let b = WorkspaceSnapshot::from_input(&input(vec![source("src/A.tsx", "class A { x=1 }")]))
2924 .unwrap();
2925 let c =
2926 WorkspaceSnapshot::from_input(&input(vec![source("src/B.tsx", "class A {}")])).unwrap();
2927 assert_eq!(a.units[0].source_unit_id, b.units[0].source_unit_id);
2928 assert_ne!(a.units[0].source_revision_id, b.units[0].source_revision_id);
2929 assert_ne!(a.units[0].source_unit_id, c.units[0].source_unit_id);
2930 }
2931 #[test]
2932 fn snapshots_and_graphs_are_enumeration_independent() {
2933 let left = input(vec![
2934 source("src/B.tsx", "class B {}"),
2935 source("src/A.tsx", "class A {}"),
2936 ]);
2937 let right = input(vec![
2938 source("src/A.tsx", "class A {}"),
2939 source("src/B.tsx", "class B {}"),
2940 ]);
2941 let l = WorkspaceSnapshot::from_input(&left).unwrap();
2942 let r = WorkspaceSnapshot::from_input(&right).unwrap();
2943 assert_eq!(
2944 l.to_canonical_json().unwrap(),
2945 r.to_canonical_json().unwrap()
2946 );
2947 let mut session = CompilerSessionState::new(
2948 l.workspace_id.clone(),
2949 l.compiler_contract.clone(),
2950 CacheLimits::default(),
2951 );
2952 let outcome = session.compile_workspace(CompileWorkspaceRequest {
2953 workspace: left,
2954 mode: RequestedCompilationMode::Full,
2955 cancellation: CancellationToken::new(),
2956 });
2957 let CompilationOutcome::Committed(done) = outcome else {
2958 panic!("expected commit")
2959 };
2960 assert!(done.graph.to_canonical_json().unwrap().ends_with(b"\n"));
2961 }
2962 #[test]
2963 fn changes_classify_rename_as_removal_and_addition() {
2964 let first =
2965 WorkspaceSnapshot::from_input(&input(vec![source("src/A.tsx", "class A {}")])).unwrap();
2966 let second =
2967 WorkspaceSnapshot::from_input(&input(vec![source("src/B.tsx", "class A {}")])).unwrap();
2968 let changes = compare_snapshots(&first, &second);
2969 assert_eq!(changes.added.len(), 1);
2970 assert_eq!(changes.removed.len(), 1);
2971 }
2972 #[test]
2973 fn session_cancellation_keeps_baseline() {
2974 let initial = input(vec![source("src/A.tsx", "class A {}")]);
2975 let snapshot = WorkspaceSnapshot::from_input(&initial).unwrap();
2976 let mut session = CompilerSessionState::new(
2977 snapshot.workspace_id.clone(),
2978 snapshot.compiler_contract.clone(),
2979 CacheLimits::default(),
2980 );
2981 assert!(matches!(
2982 session.compile_workspace(CompileWorkspaceRequest {
2983 workspace: initial,
2984 mode: RequestedCompilationMode::Full,
2985 cancellation: CancellationToken::new()
2986 }),
2987 CompilationOutcome::Committed(_)
2988 ));
2989 let baseline = session.committed_snapshot().unwrap().snapshot_id.clone();
2990 let cancellation = CancellationToken::new();
2991 cancellation.cancel();
2992 assert!(matches!(
2993 session.compile_workspace(CompileWorkspaceRequest {
2994 workspace: input(vec![source("src/A.tsx", "class A { value=1 }")]),
2995 mode: RequestedCompilationMode::Automatic,
2996 cancellation
2997 }),
2998 CompilationOutcome::Cancelled(_)
2999 ));
3000 assert_eq!(session.committed_snapshot().unwrap().snapshot_id, baseline);
3001 }
3002
3003 #[test]
3004 fn clean_full_equivalence_for_incremental_workspace_changes() {
3005 let baseline = input(vec![
3006 source("src/Dependency.tsx", "export class Dependency {}"),
3007 source(
3008 "src/App.tsx",
3009 "import { Dependency } from './Dependency'; export class App {}",
3010 ),
3011 ]);
3012 let candidate = input(vec![
3013 source(
3014 "src/Dependency.tsx",
3015 "export class Dependency { value = 1; }",
3016 ),
3017 source(
3018 "src/App.tsx",
3019 "import { Dependency } from './Dependency'; export class App {}",
3020 ),
3021 source("src/Added.tsx", "export class Added {}"),
3022 ]);
3023 let initial = WorkspaceSnapshot::from_input(&baseline).unwrap();
3024 let mut incremental = CompilerSessionState::new(
3025 initial.workspace_id.clone(),
3026 initial.compiler_contract.clone(),
3027 CacheLimits::default(),
3028 );
3029 assert!(matches!(
3030 incremental.compile_workspace(CompileWorkspaceRequest {
3031 workspace: baseline,
3032 mode: RequestedCompilationMode::Full,
3033 cancellation: CancellationToken::new(),
3034 }),
3035 CompilationOutcome::Committed(_)
3036 ));
3037 let incremental_result = incremental.compile_workspace(CompileWorkspaceRequest {
3038 workspace: candidate.clone(),
3039 mode: RequestedCompilationMode::Automatic,
3040 cancellation: CancellationToken::new(),
3041 });
3042 let CompilationOutcome::Committed(incremental_result) = incremental_result else {
3043 panic!("incremental candidate should commit");
3044 };
3045 assert_eq!(incremental_result.plan.mode, IncrementalMode::Incremental);
3046
3047 let mut clean = CompilerSessionState::new(
3048 initial.workspace_id,
3049 initial.compiler_contract,
3050 CacheLimits::default(),
3051 );
3052 let clean_result = clean.compile_workspace(CompileWorkspaceRequest {
3053 workspace: candidate,
3054 mode: RequestedCompilationMode::Full,
3055 cancellation: CancellationToken::new(),
3056 });
3057 let CompilationOutcome::Committed(clean_result) = clean_result else {
3058 panic!("clean candidate should commit");
3059 };
3060 assert_eq!(
3061 incremental_result.snapshot.to_canonical_json().unwrap(),
3062 clean_result.snapshot.to_canonical_json().unwrap()
3063 );
3064 assert_eq!(
3065 incremental_result.graph.to_canonical_json().unwrap(),
3066 clean_result.graph.to_canonical_json().unwrap()
3067 );
3068 }
3069}