use crate::{ ast::*, error::{ValidationError, ValidationErrors}, span::Span, }; use alloc::collections::{BTreeMap, BTreeSet}; use alloc::string::String; use alloc::vec::Vec; #[derive(Debug, Clone, PartialEq)] pub struct Workspace { modules: BTreeMap, } /// How a reference path resolves in the workspace. /// /// ATProto names a def with three syntactically distinct forms — a local /// fragment (`#foo`), an explicit external reference (`ns.path#foo`), and /// a bare NSID (`ns.path`, shorthand for `#main`). MLF source collapses /// these into path expressions, and downstream code (codegen, LSP, /// diagnostics) needs to know which form to emit or display for the /// canonical output. `ResolvedRef` carries that discrimination so callers /// don't have to re-derive it from implementation details of the resolver. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolvedRef { /// The reference names a def in the same lexicon as the reference /// site. Codegen emits this as `#def_name`. Local { def_name: String }, /// The reference names a def in a different lexicon. Codegen emits /// this as `namespace#def_name`. External { namespace: String, def_name: String }, /// The reference resolves via MLF's implicit-main convention: the /// whole path names a lexicon whose "main" def is conventionally the /// def matching the last segment. In ATProto this corresponds to the /// bare NSID form (`namespace`, equivalent to `namespace#main`). ImplicitMain { namespace: String, def_name: String }, } #[derive(Debug, Clone, PartialEq)] struct Module { namespace: String, lexicon: Lexicon, symbols: SymbolTable, imports: ImportTable, } #[derive(Debug, Clone, PartialEq)] struct SymbolTable { types: BTreeMap, } #[derive(Debug, Clone, PartialEq, Default)] struct ImportTable { mappings: BTreeMap, used_imports: BTreeSet, // Maps namespace alias to full namespace path // e.g., "example" -> "com.example" namespace_aliases: BTreeMap, } #[derive(Debug, Clone, PartialEq)] struct ImportedSymbol { original_path: Vec, local_name: String, span: Span, } #[derive(Debug, Clone, PartialEq)] enum Symbol { Record { name: String, span: Span }, Alias { name: String, span: Span }, Token { name: String, span: Span }, } impl Workspace { pub fn new() -> Self { Self { modules: BTreeMap::new(), } } pub fn with_prelude() -> Result { let mut ws = Self::new(); let prelude_lexicon = crate::parser::parse_lexicon(crate::PRELUDE).map_err(|e| { let mut errors = ValidationErrors::new(); errors.push(ValidationError::InvalidConstraint { message: alloc::format!("Failed to parse prelude: {:?}", e), span: crate::span::Span::new(0, 0), module_namespace: "prelude".to_string(), }); errors })?; ws.add_module("prelude".into(), prelude_lexicon)?; Ok(ws) } pub fn with_std() -> Result { let mut ws = Self::new(); // Add prelude let prelude_lexicon = crate::parser::parse_lexicon(crate::PRELUDE).map_err(|e| { let mut errors = ValidationErrors::new(); errors.push(ValidationError::InvalidConstraint { message: alloc::format!("Failed to parse prelude: {:?}", e), span: crate::span::Span::new(0, 0), module_namespace: "prelude".to_string(), }); errors })?; ws.add_module("prelude".into(), prelude_lexicon)?; // Recursively load all .mlf files from the std directory fn load_std_modules( ws: &mut Workspace, dir: &include_dir::Dir, ) -> Result<(), ValidationErrors> { for file in dir.files() { if let Some(path_str) = file.path().to_str() { if path_str.ends_with(".mlf") && !path_str.ends_with("prelude.mlf") { // Convert file path to namespace (including filename) // e.g., "com/atproto/repo/defs.mlf" -> "com.atproto.repo.defs" // e.g., "com/atproto/lexicon/schema.mlf" -> "com.atproto.lexicon.schema" let namespace = path_str .strip_suffix(".mlf") .unwrap_or(path_str) .replace('/', "."); if let Some(contents) = file.contents_utf8() { let lexicon = crate::parser::parse_lexicon(contents).map_err(|e| { let mut errors = ValidationErrors::new(); errors.push(ValidationError::InvalidConstraint { message: alloc::format!( "Failed to parse {} (file: {}): {:?}", namespace, path_str, e ), span: crate::span::Span::new(0, 0), module_namespace: namespace.clone(), }); errors })?; // If the module already exists, merge the items if ws.modules.contains_key(&namespace) { let module = ws.modules.get_mut(&namespace).unwrap(); module.lexicon.items.extend(lexicon.items); // Rebuild symbol table module.symbols = Workspace::build_symbol_table(&namespace, &module.lexicon)?; } else { ws.add_module(namespace, lexicon)?; } } } } } for subdir in dir.dirs() { load_std_modules(ws, subdir)?; } Ok(()) } load_std_modules(&mut ws, &crate::STD_DIR)?; Ok(ws) } pub fn add_module( &mut self, namespace: String, lexicon: Lexicon, ) -> Result<(), ValidationErrors> { let symbols = Self::build_symbol_table(&namespace, &lexicon)?; let module = Module { namespace: namespace.clone(), lexicon, symbols, imports: ImportTable::default(), }; self.modules.insert(namespace, module); Ok(()) } pub fn resolve(&mut self) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); if let Err(mut import_errors) = self.resolve_imports() { errors.append(&mut import_errors); } let namespaces: Vec = self.modules.keys().cloned().collect(); for namespace in namespaces { let module = self.modules[&namespace].clone(); if let Err(mut module_errors) = self.resolve_module(&namespace, &module) { errors.append(&mut module_errors); } } if !errors.is_empty() { return Err(errors); } // Check for unused imports (warnings only, don't fail) if let Err(mut unused_import_errors) = self.check_unused_imports() { errors.append(&mut unused_import_errors); } if let Err(mut typecheck_errors) = self.typecheck() { errors.append(&mut typecheck_errors); } // Filter out warnings (UnusedImport) from blocking errors let blocking_errors: Vec = errors .errors .into_iter() .filter(|e| !matches!(e, ValidationError::UnusedImport { .. })) .collect(); if blocking_errors.is_empty() { Ok(()) } else { Err(ValidationErrors { errors: blocking_errors, }) } } fn check_unused_imports(&self) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for (namespace, module) in &self.modules { // Skip prelude module if namespace == "prelude" { continue; } for (local_name, imported) in &module.imports.mappings { if !module.imports.used_imports.contains(local_name) { errors.push(ValidationError::UnusedImport { name: local_name.clone(), span: imported.span, module_namespace: namespace.clone(), }); } } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn typecheck(&self) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for (namespace, module) in &self.modules { if let Err(mut module_errors) = self.typecheck_module(namespace, module) { errors.append(&mut module_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn typecheck_module(&self, namespace: &str, module: &Module) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for item in &module.lexicon.items { if let Err(mut item_errors) = self.typecheck_item(namespace, item) { errors.append(&mut item_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn typecheck_item(&self, namespace: &str, item: &Item) -> Result<(), ValidationErrors> { match item { Item::InlineType(i) => self.typecheck_inline_type(namespace, i), Item::DefType(d) => self.typecheck_def_type(namespace, d), Item::Record(r) => self.typecheck_record(namespace, r), _ => Ok(()), } } fn typecheck_inline_type( &self, namespace: &str, inline_type: &InlineType, ) -> Result<(), ValidationErrors> { self.typecheck_type(namespace, &inline_type.ty) } fn typecheck_def_type( &self, namespace: &str, def_type: &DefType, ) -> Result<(), ValidationErrors> { self.typecheck_type(namespace, &def_type.ty) } fn typecheck_record(&self, namespace: &str, record: &Record) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for field in &record.fields { if let Err(mut field_errors) = self.typecheck_type(namespace, &field.ty) { errors.append(&mut field_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn typecheck_type(&self, namespace: &str, ty: &Type) -> Result<(), ValidationErrors> { match ty { Type::Primitive { .. } | Type::Unknown { .. } => Ok(()), Type::Reference { .. } => Ok(()), Type::Array { inner, .. } => self.typecheck_type(namespace, inner), Type::Union { types, span, .. } => { let mut errors = ValidationErrors::new(); for ty in types { if let Err(mut ty_errors) = self.typecheck_type(namespace, ty) { errors.append(&mut ty_errors); } } if types.is_empty() { errors.push(ValidationError::InvalidConstraint { message: "Union type must have at least one member".into(), span: *span, module_namespace: namespace.to_string(), }); } if errors.is_empty() { Ok(()) } else { Err(errors) } } Type::Object { fields, .. } => { let mut errors = ValidationErrors::new(); for field in fields { if let Err(mut field_errors) = self.typecheck_type(namespace, &field.ty) { errors.append(&mut field_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } Type::Parenthesized { inner, .. } => self.typecheck_type(namespace, inner), Type::Constrained { base, constraints, span, } => { let mut errors = ValidationErrors::new(); if let Err(mut base_errors) = self.typecheck_type(namespace, base) { errors.append(&mut base_errors); } if let Err(mut constraint_errors) = self.typecheck_constraints(namespace, base, constraints, *span) { errors.append(&mut constraint_errors); } if let Err(mut refinement_errors) = self.check_constraint_refinement(namespace, base, constraints) { errors.append(&mut refinement_errors); } if errors.is_empty() { Ok(()) } else { Err(errors) } } } } fn typecheck_constraints( &self, namespace: &str, base: &Type, constraints: &[Constraint], _span: Span, ) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); let base_kind = self.get_base_primitive(base); for constraint in constraints { match constraint { Constraint::MinLength { span, .. } | Constraint::MaxLength { span, .. } => { // MinLength/MaxLength can be applied to strings or arrays let is_string = matches!(base_kind, Some(PrimitiveType::String)); let is_array = matches!(base, Type::Array { .. }); if !is_string && !is_array { errors.push(ValidationError::InvalidConstraint { message: alloc::format!( "Length constraint can only be applied to string or array types" ), span: *span, module_namespace: namespace.to_string(), }); } } Constraint::MinGraphemes { span, .. } | Constraint::MaxGraphemes { span, .. } | Constraint::Format { span, .. } | Constraint::Enum { span, .. } => { if !matches!(base_kind, Some(PrimitiveType::String)) { errors.push(ValidationError::InvalidConstraint { message: alloc::format!("String constraint on non-string type"), span: *span, module_namespace: namespace.to_string(), }); } } Constraint::Minimum { span, .. } | Constraint::Maximum { span, .. } => { if !matches!(base_kind, Some(PrimitiveType::Integer)) { errors.push(ValidationError::InvalidConstraint { message: alloc::format!("Numeric constraint on non-numeric type"), span: *span, module_namespace: namespace.to_string(), }); } } Constraint::Accept { span, .. } | Constraint::MaxSize { span, .. } => { if !matches!(base_kind, Some(PrimitiveType::Blob)) { errors.push(ValidationError::InvalidConstraint { message: alloc::format!("Blob constraint on non-blob type"), span: *span, module_namespace: namespace.to_string(), }); } } Constraint::KnownValues { .. } => {} Constraint::Default { .. } => {} Constraint::Const { .. } => {} } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn check_constraint_refinement( &self, namespace: &str, base: &Type, new_constraints: &[Constraint], ) -> Result<(), ValidationErrors> { let base_constraints = self.get_base_constraints(base); if base_constraints.is_empty() { return Ok(()); } let mut errors = ValidationErrors::new(); for new_constraint in new_constraints { match new_constraint { Constraint::MaxLength { value: new_max, span, } => { for base_constraint in &base_constraints { if let Constraint::MaxLength { value: base_max, .. } = base_constraint { if new_max > base_max { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "maxLength {} is greater than base maxLength {}", new_max, base_max ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::MinLength { value: new_min, span, } => { for base_constraint in &base_constraints { if let Constraint::MinLength { value: base_min, .. } = base_constraint { if new_min < base_min { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "minLength {} is less than base minLength {}", new_min, base_min ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::Maximum { value: new_max, span, } => { for base_constraint in &base_constraints { if let Constraint::Maximum { value: base_max, .. } = base_constraint { if new_max > base_max { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "maximum {} is greater than base maximum {}", new_max, base_max ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::Minimum { value: new_min, span, } => { for base_constraint in &base_constraints { if let Constraint::Minimum { value: base_min, .. } = base_constraint { if new_min < base_min { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "minimum {} is less than base minimum {}", new_min, base_min ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::MaxGraphemes { value: new_max, span, } => { for base_constraint in &base_constraints { if let Constraint::MaxGraphemes { value: base_max, .. } = base_constraint { if new_max > base_max { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "maxGraphemes {} is greater than base maxGraphemes {}", new_max, base_max ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::MinGraphemes { value: new_min, span, } => { for base_constraint in &base_constraints { if let Constraint::MinGraphemes { value: base_min, .. } = base_constraint { if new_min < base_min { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "minGraphemes {} is less than base minGraphemes {}", new_min, base_min ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::MaxSize { value: new_max, span, } => { for base_constraint in &base_constraints { if let Constraint::MaxSize { value: base_max, .. } = base_constraint { if new_max > base_max { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "maxSize {} is greater than base maxSize {}", new_max, base_max ), span: *span, module_namespace: namespace.to_string(), }); } } } } Constraint::Enum { values: new_values, span, } => { for base_constraint in &base_constraints { if let Constraint::Enum { values: base_values, .. } = base_constraint { for new_val in new_values { if !base_values.contains(new_val) { errors.push(ValidationError::ConstraintTooPermissive { message: alloc::format!( "enum value '{}' not in base enum", new_val ), span: *span, module_namespace: namespace.to_string(), }); } } } } } _ => {} } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn get_base_constraints(&self, ty: &Type) -> Vec { match ty { Type::Constrained { base, constraints, .. } => { let mut all_constraints = constraints.clone(); all_constraints.extend(self.get_base_constraints(base)); all_constraints } Type::Parenthesized { inner, .. } => self.get_base_constraints(inner), Type::Reference { path, .. } => { if let Some(resolved_ty) = self.resolve_type_reference(path) { self.get_base_constraints(&resolved_ty) } else { Vec::new() } } _ => Vec::new(), } } fn get_base_primitive(&self, ty: &Type) -> Option { match ty { Type::Primitive { kind, .. } => Some(*kind), Type::Constrained { base, .. } => self.get_base_primitive(base), Type::Parenthesized { inner, .. } => self.get_base_primitive(inner), Type::Reference { path, .. } => { if let Some(resolved_ty) = self.resolve_type_reference(path) { self.get_base_primitive(&resolved_ty) } else { None } } _ => None, } } pub fn resolve_type_reference(&self, path: &Path) -> Option { if path.segments.len() == 1 { let name = &path.segments[0].name; for (_, module) in &self.modules { if let Some(Symbol::Alias { .. }) = module.symbols.types.get(name) { for item in &module.lexicon.items { match item { Item::InlineType(i) if i.name.name == *name => { return Some(i.ty.clone()); } Item::DefType(d) if d.name.name == *name => { return Some(d.ty.clone()); } _ => {} } } } } } else { let target_namespace = path.segments[..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::>() .join("."); let type_name = &path.segments[path.segments.len() - 1].name; // First try: normal resolution (namespace + type) if let Some(module) = self.modules.get(&target_namespace) { for item in &module.lexicon.items { match item { Item::InlineType(i) if i.name.name == *type_name => { return Some(i.ty.clone()); } Item::DefType(d) if d.name.name == *type_name => { return Some(d.ty.clone()); } _ => {} } } } // Second try: implicit main resolution let full_namespace = path.to_string(); let namespace_suffix = full_namespace.split('.').last().unwrap_or(&full_namespace); if namespace_suffix == type_name { if let Some(module) = self.modules.get(&full_namespace) { for item in &module.lexicon.items { match item { Item::InlineType(i) if i.name.name == *type_name => { return Some(i.ty.clone()); } Item::DefType(d) if d.name.name == *type_name => { return Some(d.ty.clone()); } _ => {} } } } } } None } pub fn is_inline_type(&self, path: &Path) -> bool { if path.segments.len() == 1 { let name = &path.segments[0].name; for (_, module) in &self.modules { if let Some(Symbol::Alias { .. }) = module.symbols.types.get(name) { for item in &module.lexicon.items { if let Item::InlineType(i) = item { if i.name.name == *name { return true; } } } } } } else { let target_namespace = path.segments[..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::>() .join("."); let type_name = &path.segments[path.segments.len() - 1].name; if let Some(module) = self.modules.get(&target_namespace) { for item in &module.lexicon.items { if let Item::InlineType(i) = item { if i.name.name == *type_name { return true; } } } } } false } /// Get all imported types for a given namespace /// Returns a vector of (local_name, original_path) tuples pub fn get_imports(&self, namespace: &str) -> Vec<(String, Vec)> { if let Some(module) = self.modules.get(namespace) { module .imports .mappings .iter() .map(|(local_name, imported)| (local_name.clone(), imported.original_path.clone())) .collect() } else { Vec::new() } } /// Get all type names defined in a given namespace /// Returns a vector of type names pub fn get_namespace_types(&self, namespace: &str) -> Vec { if let Some(module) = self.modules.get(namespace) { module.symbols.types.keys().cloned().collect() } else { Vec::new() } } /// Get all namespaces in the workspace /// Returns a vector of namespace strings pub fn get_all_namespaces(&self) -> Vec { self.modules.keys().cloned().collect() } /// Get all fully-qualified type references (NSIDs) in the workspace /// Returns a vector of (nsid, namespace, typename) tuples /// Example: ("com.atproto.repo.strongRef", "com.atproto.repo", "strongRef") pub fn get_all_nsids(&self) -> Vec<(String, String, String)> { let mut nsids = Vec::new(); for (namespace, module) in &self.modules { // Skip prelude since it's auto-imported if namespace == "prelude" { continue; } for type_name in module.symbols.types.keys() { let nsid = if namespace.is_empty() { type_name.clone() } else { alloc::format!("{}.{}", namespace, type_name) }; nsids.push((nsid, namespace.clone(), type_name.clone())); } } nsids } /// Get the lexicon for a given namespace pub fn get_lexicon(&self, namespace: &str) -> Option<&Lexicon> { self.modules.get(namespace).map(|m| &m.lexicon) } /// Resolve a type reference to its actual namespace /// Resolve a reference path (evaluated from inside `current_namespace`) /// to the lexicon and def it names. Returns a structured /// [`ResolvedRef`] so callers can distinguish local, external, and /// implicit-main references without re-deriving the resolution /// decision from a bare namespace string. pub fn resolve_ref(&self, path: &Path, current_namespace: &str) -> Option { if path.segments.len() == 1 { let name = &path.segments[0].name; if let Some(module) = self.modules.get(current_namespace) { if module.symbols.types.contains_key(name) { return Some(ResolvedRef::Local { def_name: name.clone(), }); } if let Some(imported) = module.imports.mappings.get(name) { // `original_path` is the fully-qualified import path // (e.g. `["com", "atproto", "label", "defs", "label"]`). // All but the last segment form the namespace. if imported.original_path.len() > 1 { let namespace = imported.original_path[..imported.original_path.len() - 1].join("."); let def_name = imported.original_path.last().unwrap().clone(); return Some(ResolvedRef::External { namespace, def_name, }); } else { // Pathological single-segment import: treat as local. return Some(ResolvedRef::Local { def_name: name.clone(), }); } } } if let Some(module) = self.modules.get("prelude") { if module.symbols.types.contains_key(name) { return Some(ResolvedRef::External { namespace: "prelude".to_string(), def_name: name.clone(), }); } } return None; } // Multi-segment path: try the explicit `namespace.def_name` form first. let target_namespace = path.segments[..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::>() .join("."); let type_name = &path.segments[path.segments.len() - 1].name; if let Some(module) = self.modules.get(&target_namespace) { if module.symbols.types.contains_key(type_name) { let namespace = target_namespace; if namespace == current_namespace { return Some(ResolvedRef::Local { def_name: type_name.clone(), }); } return Some(ResolvedRef::External { namespace, def_name: type_name.clone(), }); } } // Fall back to the implicit-main convention: the whole path names a // lexicon whose "main" def is conventionally the def matching the // last segment. In ATProto this corresponds to a bare NSID. let full_namespace = path.to_string(); if let Some(module) = self.modules.get(&full_namespace) { let namespace_suffix = full_namespace.split('.').last().unwrap_or(&full_namespace); if namespace_suffix == type_name && module.symbols.types.contains_key(type_name) { return Some(ResolvedRef::ImplicitMain { namespace: full_namespace, def_name: type_name.clone(), }); } } None } /// Return only the resolved lexicon namespace for a reference path. /// Thin shim over [`Self::resolve_ref`]; prefer that for new call /// sites where the variant (local vs external vs implicit-main) /// matters. pub fn resolve_reference_namespace( &self, path: &Path, current_namespace: &str, ) -> Option { match self.resolve_ref(path, current_namespace)? { ResolvedRef::Local { .. } => Some(current_namespace.to_string()), ResolvedRef::External { namespace, .. } | ResolvedRef::ImplicitMain { namespace, .. } => Some(namespace), } } fn resolve_imports(&mut self) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); let namespaces: Vec = self.modules.keys().cloned().collect(); for namespace in namespaces { let lexicon = self.modules[&namespace].lexicon.clone(); for item in &lexicon.items { if let Item::Use(use_stmt) = item { if let Err(mut use_errors) = self.resolve_use_statement(&namespace, use_stmt) { errors.append(&mut use_errors); } } } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn resolve_use_statement( &mut self, current_namespace: &str, use_stmt: &Use, ) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); // Determine if this is: // 1. use namespace; (UseImports::All) // 2. use namespace { items }; (UseImports::Items with path=namespace) // 3. use namespace.typename as alias; (UseImports::Items with path=namespace.typename, single item) let (target_namespace, is_single_type_import) = match &use_stmt.imports { UseImports::All => { // use namespace; (use_stmt.path.to_string(), false) } UseImports::Items(items) => { // Check if this is the old syntax: use namespace.typename as alias // In this case, path has >=2 segments and items has 1 item whose name matches the last segment if items.len() == 1 && use_stmt.path.segments.len() >= 2 && items[0].name.name == use_stmt.path.segments.last().unwrap().name { // Old syntax: use namespace.typename as alias let namespace = use_stmt.path.segments[..use_stmt.path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::>() .join("."); (namespace, true) } else { // New syntax: use namespace { items }; (use_stmt.path.to_string(), false) } } }; // Check if the target namespace exists or if there are modules with that prefix let namespace_exists = self.modules.contains_key(&target_namespace); let has_children = self .modules .keys() .any(|ns| ns.starts_with(&alloc::format!("{}.", target_namespace))); if !namespace_exists && !has_children { errors.push(ValidationError::UndefinedReference { name: target_namespace.clone(), span: use_stmt.path.span, module_namespace: current_namespace.to_string(), }); return Err(errors); } let namespace_alias_to_add: Option<(String, String)> = match &use_stmt.imports { UseImports::All => { // Check for implicit main resolution // If namespace suffix matches a type name, import only that type // Otherwise, this is a namespace alias for path shortening let namespace_suffix = target_namespace .split('.') .last() .unwrap_or(&target_namespace); if let Some(target_module) = self.modules.get(&target_namespace) { // Module exists - check if there's a type matching the namespace suffix if target_module.symbols.types.contains_key(namespace_suffix) { // Implicit main resolution: no namespace alias, just type import None } else { // No type matching namespace suffix - this is a namespace alias // Create alias: namespace_suffix -> target_namespace // e.g., "use com.example;" creates alias "example" -> "com.example" Some((namespace_suffix.to_string(), target_namespace.clone())) } } else { // Module doesn't exist but has children - create namespace alias // e.g., "use com.example;" with only "com.example.defs" module Some((namespace_suffix.to_string(), target_namespace.clone())) } } UseImports::Items(_) => { // Items imports don't create namespace aliases None } }; let imports_to_add: Vec<(String, ImportedSymbol)> = match &use_stmt.imports { UseImports::All => { // Check for implicit main resolution // If namespace suffix matches a type name, import only that type let namespace_suffix = target_namespace .split('.') .last() .unwrap_or(&target_namespace); if let Some(target_module) = self.modules.get(&target_namespace) { // Check if there's a type matching the namespace suffix if target_module.symbols.types.contains_key(namespace_suffix) { // Implicit main resolution: import only the type matching the namespace suffix let imported = ImportedSymbol { original_path: use_stmt .path .segments .iter() .map(|s| s.name.clone()) .collect(), local_name: namespace_suffix.to_string(), span: use_stmt.path.span, }; vec![(namespace_suffix.to_string(), imported)] } else { // Namespace alias only, no type imports vec![] } } else { // Module doesn't exist - namespace alias only vec![] } } UseImports::Items(items) => { // Import specific items from the namespace let target_module = self.modules.get(&target_namespace).unwrap(); let mut imports = Vec::new(); // Get the namespace suffix for main resolution let namespace_suffix = target_namespace .split('.') .last() .unwrap_or(&target_namespace); for item in items { // Determine the actual type name to look up let type_name = if item.name.name == "main" { // Special case: "main" keyword resolves to the namespace suffix namespace_suffix.to_string() } else { item.name.name.clone() }; // Check if the type exists in the target module if !target_module.symbols.types.contains_key(&type_name) { errors.push(ValidationError::UndefinedReference { name: alloc::format!("{}.{}", target_namespace, item.name.name), span: item.name.span, module_namespace: current_namespace.to_string(), }); continue; } // Determine the local name (alias or original name) let local_name = if let Some(alias) = &item.alias { alias.name.clone() } else if item.name.name == "main" { // If importing main without alias, bind to namespace suffix namespace_suffix.to_string() } else { type_name.clone() }; // Build the original_path correctly based on import type let original_path = if is_single_type_import { // Old syntax: use namespace.typename as alias // Path segments already include the type name use_stmt .path .segments .iter() .map(|s| s.name.clone()) .collect() } else { // New syntax: use namespace { items }; // Need to append the type name to the namespace use_stmt .path .segments .iter() .map(|s| s.name.clone()) .chain(core::iter::once(type_name.clone())) .collect() }; imports.push(( local_name.clone(), ImportedSymbol { original_path, local_name, span: item.name.span, }, )); } imports } }; let module = self.modules.get_mut(current_namespace).unwrap(); for (local_name, imported) in imports_to_add { module.imports.mappings.insert(local_name, imported); } // Add namespace alias if one was created if let Some((alias, full_namespace)) = namespace_alias_to_add { module .imports .namespace_aliases .insert(alias, full_namespace); } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn has_main_annotation(annotations: &[Annotation]) -> bool { annotations.iter().any(|ann| ann.name.name == "main") } fn build_symbol_table( namespace: &str, lexicon: &Lexicon, ) -> Result { let mut symbols = SymbolTable { types: BTreeMap::new(), }; let mut errors = ValidationErrors::new(); // Extract namespace suffix for conflict checking let namespace_suffix = namespace.split('.').last().unwrap_or(namespace); // First pass: group items by name to detect duplicates let mut items_by_name: BTreeMap> = BTreeMap::new(); for item in &lexicon.items { let name = match item { Item::Record(r) => Some(r.name.name.as_str()), Item::InlineType(i) => Some(i.name.name.as_str()), Item::DefType(d) => Some(d.name.name.as_str()), Item::Token(t) => Some(t.name.name.as_str()), Item::Query(q) => Some(q.name.name.as_str()), Item::Procedure(p) => Some(p.name.name.as_str()), Item::Subscription(s) => Some(s.name.name.as_str()), Item::Use(_) | Item::SelfItem(_) => None, }; if let Some(name) = name { items_by_name .entry(name.to_string()) .or_insert_with(Vec::new) .push(item); } } // Second pass: validate each name group for (name, items) in items_by_name { // Check for reserved names if name == "main" || name == "defs" { for item in &items { let span = match item { Item::Record(r) => r.name.span, Item::InlineType(i) => i.name.span, Item::DefType(d) => d.name.span, Item::Token(t) => t.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, Item::Use(_) | Item::SelfItem(_) => continue, }; errors.push(crate::error::ValidationError::ReservedName { name: name.clone(), span, module_namespace: namespace.to_string(), }); } continue; } if items.len() == 1 { // No duplicates, just add to symbol table let item = items[0]; match item { Item::Record(r) => { symbols.types.insert( name.clone(), Symbol::Record { name: name.clone(), span: r.name.span, }, ); } Item::InlineType(i) => { symbols.types.insert( name.clone(), Symbol::Alias { name: name.clone(), span: i.name.span, }, ); } Item::DefType(d) => { symbols.types.insert( name.clone(), Symbol::Alias { name: name.clone(), span: d.name.span, }, ); } Item::Token(t) => { symbols.types.insert( name.clone(), Symbol::Token { name: name.clone(), span: t.name.span, }, ); } _ => {} // Queries, procedures, subscriptions not added to symbol table } } else { // Duplicates found - apply @main rules // Check if name matches namespace suffix if name != namespace_suffix { // Conflict not allowed - name doesn't match suffix for (idx, item) in items.iter().enumerate() { if idx > 0 { let span = match item { Item::Record(r) => r.name.span, Item::InlineType(i) => i.name.span, Item::DefType(d) => d.name.span, Item::Token(t) => t.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, Item::Use(_) | Item::SelfItem(_) => continue, }; errors.push(crate::error::ValidationError::ConflictNotAllowed { name: name.clone(), namespace_suffix: namespace_suffix.to_string(), span, module_namespace: namespace.to_string(), }); } } continue; } // Check if any inline types are involved (not allowed to conflict) let has_inline = items.iter().any(|item| matches!(item, Item::InlineType(_))); if has_inline { for (idx, item) in items.iter().enumerate() { if idx > 0 { let span = match item { Item::Record(r) => r.name.span, Item::InlineType(i) => i.name.span, Item::DefType(d) => d.name.span, Item::Token(t) => t.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, Item::Use(_) | Item::SelfItem(_) => continue, }; errors.push(crate::error::ValidationError::DuplicateDefinition { name: name.clone(), first_span: match items[0] { Item::Record(r) => r.name.span, Item::InlineType(i) => i.name.span, Item::DefType(d) => d.name.span, Item::Token(t) => t.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, Item::Use(_) | Item::SelfItem(_) => continue, }, second_span: span, module_namespace: namespace.to_string(), }); } } continue; } // Check @main annotations let items_with_main: Vec<&Item> = items .iter() .filter(|item| { let annotations = match item { Item::Record(r) => &r.annotations, Item::DefType(d) => &d.annotations, Item::Query(q) => &q.annotations, Item::Procedure(p) => &p.annotations, Item::Subscription(s) => &s.annotations, _ => return false, }; Self::has_main_annotation(annotations) }) .copied() .collect(); if items_with_main.is_empty() { // No @main annotation - ambiguous errors.push(crate::error::ValidationError::AmbiguousMain { name: name.clone(), namespace_suffix: namespace_suffix.to_string(), first_span: match items[0] { Item::Record(r) => r.name.span, Item::DefType(d) => d.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, _ => continue, }, second_span: match items[1] { Item::Record(r) => r.name.span, Item::DefType(d) => d.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, _ => continue, }, module_namespace: namespace.to_string(), }); } else if items_with_main.len() > 1 { // Multiple @main annotations errors.push(crate::error::ValidationError::MultipleMain { name: name.clone(), first_span: match items_with_main[0] { Item::Record(r) => r.name.span, Item::DefType(d) => d.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, _ => continue, }, second_span: match items_with_main[1] { Item::Record(r) => r.name.span, Item::DefType(d) => d.name.span, Item::Query(q) => q.name.span, Item::Procedure(p) => p.name.span, Item::Subscription(s) => s.name.span, _ => continue, }, module_namespace: namespace.to_string(), }); } else { // Exactly one @main - valid conflict // Add the non-main item to symbol table (only defs/aliases) for item in &items { let has_main = match item { Item::Record(r) => Self::has_main_annotation(&r.annotations), Item::DefType(d) => Self::has_main_annotation(&d.annotations), Item::Query(q) => Self::has_main_annotation(&q.annotations), Item::Procedure(p) => Self::has_main_annotation(&p.annotations), Item::Subscription(s) => Self::has_main_annotation(&s.annotations), _ => false, }; if !has_main { // Add non-main item to symbol table match item { Item::DefType(d) => { symbols.types.insert( name.clone(), Symbol::Alias { name: name.clone(), span: d.name.span, }, ); } Item::Record(r) => { symbols.types.insert( name.clone(), Symbol::Record { name: name.clone(), span: r.name.span, }, ); } Item::Token(t) => { symbols.types.insert( name.clone(), Symbol::Token { name: name.clone(), span: t.name.span, }, ); } _ => {} } } } } } } if errors.is_empty() { Ok(symbols) } else { Err(errors) } } fn resolve_module(&mut self, namespace: &str, module: &Module) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for item in &module.lexicon.items { if let Err(mut item_errors) = self.resolve_item(namespace, item) { errors.append(&mut item_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn resolve_item(&mut self, namespace: &str, item: &Item) -> Result<(), ValidationErrors> { match item { Item::Record(r) => self.resolve_record(namespace, r), Item::InlineType(i) => self.resolve_inline_type(namespace, i), Item::DefType(d) => self.resolve_def_type(namespace, d), Item::Query(q) => self.resolve_query(namespace, q), Item::Procedure(p) => self.resolve_procedure(namespace, p), Item::Subscription(s) => self.resolve_subscription(namespace, s), Item::Token(_) | Item::Use(_) | Item::SelfItem(_) => Ok(()), } } fn resolve_record(&mut self, namespace: &str, record: &Record) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for field in &record.fields { if let Err(mut field_errors) = self.resolve_type(namespace, &field.ty) { errors.append(&mut field_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn resolve_inline_type( &mut self, namespace: &str, inline_type: &InlineType, ) -> Result<(), ValidationErrors> { self.resolve_type(namespace, &inline_type.ty) } fn resolve_def_type( &mut self, namespace: &str, def_type: &DefType, ) -> Result<(), ValidationErrors> { self.resolve_type(namespace, &def_type.ty) } fn resolve_query(&mut self, namespace: &str, query: &Query) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for param in &query.params { if let Err(mut param_errors) = self.resolve_type(namespace, ¶m.ty) { errors.append(&mut param_errors); } } match &query.returns { ReturnType::None { .. } => { // No return type to resolve } ReturnType::Type(ty) => { if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { errors.append(&mut ty_errors); } } ReturnType::TypeWithErrors { success, .. } => { if let Err(mut ty_errors) = self.resolve_type(namespace, success) { errors.append(&mut ty_errors); } } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn resolve_procedure( &mut self, namespace: &str, procedure: &Procedure, ) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for param in &procedure.params { if let Err(mut param_errors) = self.resolve_type(namespace, ¶m.ty) { errors.append(&mut param_errors); } } match &procedure.returns { ReturnType::None { .. } => { // No return type to resolve } ReturnType::Type(ty) => { if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { errors.append(&mut ty_errors); } } ReturnType::TypeWithErrors { success, .. } => { if let Err(mut ty_errors) = self.resolve_type(namespace, success) { errors.append(&mut ty_errors); } } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn resolve_subscription( &mut self, namespace: &str, subscription: &Subscription, ) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new(); for param in &subscription.params { if let Err(mut param_errors) = self.resolve_type(namespace, ¶m.ty) { errors.append(&mut param_errors); } } if let Some(messages) = &subscription.messages { if let Err(mut msg_errors) = self.resolve_type(namespace, messages) { errors.append(&mut msg_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } fn resolve_type(&mut self, namespace: &str, ty: &Type) -> Result<(), ValidationErrors> { match ty { Type::Primitive { .. } | Type::Unknown { .. } => Ok(()), Type::Reference { path, span } => self.resolve_reference(namespace, path, *span), Type::Array { inner, .. } => self.resolve_type(namespace, inner), Type::Union { types, .. } => { let mut errors = ValidationErrors::new(); for ty in types { if let Err(mut ty_errors) = self.resolve_type(namespace, ty) { errors.append(&mut ty_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } Type::Object { fields, .. } => { let mut errors = ValidationErrors::new(); for field in fields { if let Err(mut field_errors) = self.resolve_type(namespace, &field.ty) { errors.append(&mut field_errors); } } if errors.is_empty() { Ok(()) } else { Err(errors) } } Type::Parenthesized { inner, .. } => self.resolve_type(namespace, inner), Type::Constrained { base, .. } => self.resolve_type(namespace, base), } } fn resolve_reference( &mut self, current_namespace: &str, path: &Path, span: Span, ) -> Result<(), ValidationErrors> { let full_path = path.to_string(); if path.segments.len() == 1 { let name = &path.segments[0].name; if let Some(module) = self.modules.get(current_namespace) { if module.symbols.types.contains_key(name) { return Ok(()); } if module.imports.mappings.contains_key(name) { // Mark this import as used if let Some(module_mut) = self.modules.get_mut(current_namespace) { module_mut.imports.used_imports.insert(name.clone()); } return Ok(()); } } if let Some(module) = self.modules.get("prelude") { if module.symbols.types.contains_key(name) { return Ok(()); } } let mut errors = ValidationErrors::new(); errors.push(crate::error::ValidationError::UndefinedReference { name: name.clone(), span, module_namespace: current_namespace.to_string(), }); return Err(errors); } // Multi-segment path: check for namespace alias // If the first segment is a namespace alias, expand it let first_segment = &path.segments[0].name; let type_name = &path.segments[path.segments.len() - 1].name; let target_namespace = if let Some(module) = self.modules.get(current_namespace) { if let Some(full_ns) = module.imports.namespace_aliases.get(first_segment) { // Found a namespace alias! Expand it // e.g., "example.defs.foo" with alias "example" -> "com.example" // The namespace part is "example.defs" which expands to "com.example.defs" // (excluding the last segment "foo" which is the type name) let middle_segments: Vec<&str> = path.segments[1..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect(); if middle_segments.is_empty() { // e.g., "example.foo" -> "com.example" full_ns.clone() } else { // e.g., "example.defs.foo" -> "com.example.defs" alloc::format!("{}.{}", full_ns, middle_segments.join(".")) } } else { // No alias, use the path as-is (excluding type name) path.segments[..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::>() .join(".") } } else { path.segments[..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::>() .join(".") }; // First try: normal resolution (namespace + type) if let Some(module) = self.modules.get(&target_namespace) { if module.symbols.types.contains_key(type_name) { return Ok(()); } } // Second try: implicit main resolution // If com.atproto.repo.strongRef fails, try treating the full path as a namespace // and look for a type named "strongRef" (matching the namespace suffix) // Need to use the expanded path if an alias was used let expanded_full_path = if target_namespace.is_empty() { type_name.to_string() } else { alloc::format!("{}.{}", target_namespace, type_name) }; if let Some(module) = self.modules.get(&expanded_full_path) { let namespace_suffix = expanded_full_path .split('.') .last() .unwrap_or(&expanded_full_path); if namespace_suffix == type_name && module.symbols.types.contains_key(type_name) { return Ok(()); } } let mut errors = ValidationErrors::new(); errors.push(crate::error::ValidationError::UndefinedReference { name: full_path, span, module_namespace: current_namespace.to_string(), }); Err(errors) } } impl Default for Workspace { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::parser::parse_lexicon; #[test] fn test_workspace_basic() { let mut ws = Workspace::new(); let input = "record foo { bar: string, }"; let lexicon = parse_lexicon(input).unwrap(); assert!(ws.add_module("test".into(), lexicon).is_ok()); } #[test] fn test_duplicate_definition() { let mut ws = Workspace::new(); let input = "record foo {} inline type foo = string;"; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("test".into(), lexicon); assert!(result.is_err()); } #[test] fn test_undefined_reference() { let mut ws = Workspace::new(); let input = "record foo { bar: unknown_type, }"; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_self_reference() { let mut ws = Workspace::new(); let input = "record foo { bar: foo, }"; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_cross_module_reference() { let mut ws = Workspace::new(); let a = parse_lexicon("record foo {}").unwrap(); ws.add_module("a".into(), a).unwrap(); let b = parse_lexicon("record bar { baz: a.foo, }").unwrap(); ws.add_module("b".into(), b).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_prelude_reference() { let ws = Workspace::with_prelude().unwrap(); // Prelude should be accessible assert!(ws.modules.contains_key("prelude")); } #[test] fn test_use_import_with_alias() { let mut ws = Workspace::new(); let a = parse_lexicon("record post {}").unwrap(); ws.add_module("app.bsky.feed".into(), a).unwrap(); let b = parse_lexicon("use app.bsky.feed.post as FeedPost; record like { subject: FeedPost, }") .unwrap(); ws.add_module("app.bsky.feed.like".into(), b).unwrap(); let result = ws.resolve(); if let Err(ref e) = result { eprintln!("Errors: {:?}", e); } assert!(result.is_ok()); } #[test] fn test_use_undefined_module() { let mut ws = Workspace::new(); let a = parse_lexicon("use nonexistent; record foo {}").unwrap(); ws.add_module("test".into(), a).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_use_undefined_type() { let mut ws = Workspace::new(); let a = parse_lexicon("record foo {}").unwrap(); ws.add_module("a".into(), a).unwrap(); let b = parse_lexicon("use a.bar; record baz {}").unwrap(); ws.add_module("b".into(), b).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_typecheck_string_constraint_on_string() { let mut ws = Workspace::new(); let input = r#"inline type shortString = string constrained { maxLength: 100, };"#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_typecheck_string_constraint_on_integer() { let mut ws = Workspace::new(); let input = r#"inline type constrainedInt = integer constrained { maxLength: 100, };"#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_typecheck_numeric_constraint_on_integer() { let mut ws = Workspace::new(); let input = r#"inline type positiveInt = integer constrained { minimum: 0, };"#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_typecheck_numeric_constraint_on_string() { let mut ws = Workspace::new(); let input = r#"inline type constrainedString = string constrained { minimum: 0, };"#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_typecheck_constrained_alias() { let mut ws = Workspace::new(); let input = r#" inline type shortString = string constrained { maxLength: 100, }; inline type tinyString = shortString constrained { maxLength: 50, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_maxlength_valid() { let mut ws = Workspace::new(); let input = r#" inline type shortString = string constrained { maxLength: 100, }; inline type tinyString = shortString constrained { maxLength: 50, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_maxlength_invalid() { let mut ws = Workspace::new(); let input = r#" inline type shortString = string constrained { maxLength: 50, }; inline type longString = shortString constrained { maxLength: 100, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); let errors = result.unwrap_err(); assert!( errors .errors .iter() .any(|e| matches!(e, ValidationError::ConstraintTooPermissive { .. })) ); } #[test] fn test_constraint_refinement_minlength_valid() { let mut ws = Workspace::new(); let input = r#" inline type minString = string constrained { minLength: 10, }; inline type longerMinString = minString constrained { minLength: 20, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_minlength_invalid() { let mut ws = Workspace::new(); let input = r#" inline type minString = string constrained { minLength: 20, }; inline type shorterMinString = minString constrained { minLength: 10, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_constraint_refinement_maximum_valid() { let mut ws = Workspace::new(); let input = r#" inline type largeInt = integer constrained { maximum: 1000, }; inline type smallInt = largeInt constrained { maximum: 100, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_maximum_invalid() { let mut ws = Workspace::new(); let input = r#" inline type smallInt = integer constrained { maximum: 100, }; inline type largeInt = smallInt constrained { maximum: 1000, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_constraint_refinement_minimum_valid() { let mut ws = Workspace::new(); let input = r#" inline type positiveInt = integer constrained { minimum: 0, }; inline type strictPositiveInt = positiveInt constrained { minimum: 1, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_minimum_invalid() { let mut ws = Workspace::new(); let input = r#" inline type positiveInt = integer constrained { minimum: 10, }; inline type lowerInt = positiveInt constrained { minimum: 5, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_constraint_refinement_enum_valid() { let mut ws = Workspace::new(); let input = r#" inline type colorEnum = string constrained { enum: ["red", "green", "blue"], }; inline type primaryColors = colorEnum constrained { enum: ["red", "blue"], }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_enum_invalid() { let mut ws = Workspace::new(); let input = r#" inline type primaryColors = string constrained { enum: ["red", "blue"], }; inline type moreColors = primaryColors constrained { enum: ["red", "blue", "green"], }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_constraint_refinement_chain() { let mut ws = Workspace::new(); let input = r#" inline type baseString = string constrained { maxLength: 1000, minLength: 10, }; inline type mediumString = baseString constrained { maxLength: 500, minLength: 20, }; inline type shortString = mediumString constrained { maxLength: 100, minLength: 30, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); assert!(ws.resolve().is_ok()); } #[test] fn test_constraint_refinement_chain_invalid() { let mut ws = Workspace::new(); let input = r#" inline type baseString = string constrained { maxLength: 1000, }; inline type mediumString = baseString constrained { maxLength: 500, }; inline type breakChain = mediumString constrained { maxLength: 600, }; "#; let lexicon = parse_lexicon(input).unwrap(); ws.add_module("test".into(), lexicon).unwrap(); let result = ws.resolve(); assert!(result.is_err()); } #[test] fn test_with_std() { let ws = Workspace::with_std().unwrap(); // Check that prelude is loaded assert!(ws.modules.contains_key("prelude")); // Print all loaded modules for debugging eprintln!("Loaded modules:"); for (name, _) in &ws.modules { eprintln!(" - {}", name); } // Check that std modules are loaded assert!(ws.modules.len() > 1, "Should have more than just prelude"); } #[test] fn test_main_annotation_valid_conflict() { let mut ws = Workspace::new(); let input = r#" @main record thread { title!: string, } def type thread = { id!: string, }; "#; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("com.example.thread".into(), lexicon); assert!(result.is_ok()); } #[test] fn test_main_annotation_ambiguous() { let mut ws = Workspace::new(); let input = r#" record thread { title!: string, } def type thread = { id!: string, }; "#; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("com.example.thread".into(), lexicon); assert!(result.is_err()); let errors = result.unwrap_err(); assert!( errors .errors .iter() .any(|e| matches!(e, ValidationError::AmbiguousMain { .. })) ); } #[test] fn test_main_annotation_multiple() { let mut ws = Workspace::new(); let input = r#" @main record thread { title!: string, } @main def type thread = { id!: string, }; "#; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("com.example.thread".into(), lexicon); assert!(result.is_err()); let errors = result.unwrap_err(); assert!( errors .errors .iter() .any(|e| matches!(e, ValidationError::MultipleMain { .. })) ); } #[test] fn test_main_annotation_conflict_not_allowed() { let mut ws = Workspace::new(); let input = r#" @main record post { text!: string, } def type post = { id!: string, }; "#; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("com.example.thread".into(), lexicon); assert!(result.is_err()); let errors = result.unwrap_err(); assert!( errors .errors .iter() .any(|e| matches!(e, ValidationError::ConflictNotAllowed { .. })) ); } #[test] fn test_main_annotation_inline_type_conflict() { let mut ws = Workspace::new(); let input = r#" inline type thread = string; def type thread = { id!: string, }; "#; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("com.example.thread".into(), lexicon); assert!(result.is_err()); let errors = result.unwrap_err(); assert!( errors .errors .iter() .any(|e| matches!(e, ValidationError::DuplicateDefinition { .. })) ); } #[test] fn test_main_annotation_query_valid() { let mut ws = Workspace::new(); let input = r#" @main query getThread(id!: string): threadData; def type thread = { id!: string, title!: string, }; def type threadData = { thread!: thread, }; "#; let lexicon = parse_lexicon(input).unwrap(); let result = ws.add_module("com.example.thread".into(), lexicon); assert!(result.is_ok()); } #[test] fn test_unused_import() { let mut ws = Workspace::new(); let a = parse_lexicon("record foo {}").unwrap(); ws.add_module("a".into(), a).unwrap(); let b = parse_lexicon("use a.foo as Foo; record bar {}").unwrap(); ws.add_module("b".into(), b).unwrap(); // UnusedImport is now a warning, not a blocking error let result = ws.resolve(); assert!(result.is_ok()); } #[test] fn test_used_import() { let mut ws = Workspace::new(); let a = parse_lexicon("record foo {}").unwrap(); ws.add_module("a".into(), a).unwrap(); let b = parse_lexicon("use a.foo as Foo; record bar { baz: Foo, }").unwrap(); ws.add_module("b".into(), b).unwrap(); let result = ws.resolve(); assert!(result.is_ok()); } #[test] fn test_unused_import_all() { let mut ws = Workspace::new(); let a = parse_lexicon("record foo {} record bar {}").unwrap(); ws.add_module("a".into(), a).unwrap(); let b = parse_lexicon("use a; record baz {}").unwrap(); ws.add_module("b".into(), b).unwrap(); // UnusedImport is now a warning, not a blocking error let result = ws.resolve(); assert!(result.is_ok()); } #[test] fn test_use_implicit_main_resolution() { let mut ws = Workspace::new(); // Create a namespace where the namespace suffix matches a type name // No @main annotation needed for implicit main resolution let profile_ns = parse_lexicon( r#" def type color = { red!: integer, green!: integer, blue!: integer, }; record profile { color: color, } "#, ) .unwrap(); ws.add_module("place.stream.chat.profile".into(), profile_ns) .unwrap(); // Import using implicit main resolution - should only import profile, not color let bookmark = parse_lexicon( r#" use place.stream.chat.profile; record bookmark { owner!: profile, } "#, ) .unwrap(); ws.add_module("com.example.bookmark".into(), bookmark) .unwrap(); // Should resolve without unused import warning for color let result = ws.resolve(); assert!(result.is_ok()); // Verify that only profile was imported (implicit main resolution) let imports = ws.get_imports("com.example.bookmark"); assert_eq!(imports.len(), 1); assert_eq!(imports[0].0, "profile"); } #[test] fn test_use_namespace_alias() { let mut ws = Workspace::new(); // Create a namespace where the suffix doesn't match a type name let defs = parse_lexicon( r#" def type foo = string; def type bar = integer; "#, ) .unwrap(); ws.add_module("com.example.defs".into(), defs).unwrap(); // Using "use com.example;" creates a namespace alias "example" -> "com.example" // This allows referencing types via the shortened path let app = parse_lexicon( r#" use com.example; record thing { x!: example.defs.foo, y!: example.defs.bar, } "#, ) .unwrap(); ws.add_module("com.example.app".into(), app).unwrap(); // Should resolve successfully using the namespace alias let result = ws.resolve(); if let Err(ref e) = result { eprintln!("Errors: {:?}", e); } assert!(result.is_ok()); // Verify that no types were imported (it's just a namespace alias) let imports = ws.get_imports("com.example.app"); assert_eq!(imports.len(), 0); } #[test] fn test_use_namespace_alias_nested() { let mut ws = Workspace::new(); // Create nested namespaces let actor_defs = parse_lexicon( r#" def type profileView = { did!: string, handle!: string, }; "#, ) .unwrap(); ws.add_module("app.bsky.actor.defs".into(), actor_defs) .unwrap(); let feed_post = parse_lexicon( r#" def type post = { text!: string, }; "#, ) .unwrap(); ws.add_module("app.bsky.feed.post".into(), feed_post) .unwrap(); // Use namespace alias to shorten references let like = parse_lexicon( r#" use app.bsky; record like { subject!: bsky.feed.post, actor!: bsky.actor.defs.profileView, } "#, ) .unwrap(); ws.add_module("app.bsky.feed.like".into(), like).unwrap(); let result = ws.resolve(); if let Err(ref e) = result { eprintln!("Errors: {:?}", e); } assert!(result.is_ok()); } #[test] fn test_use_namespace_reference_full_path() { let mut ws = Workspace::new(); // Create a namespace where the suffix doesn't match a type name let defs = parse_lexicon( r#" def type foo = string; def type bar = integer; "#, ) .unwrap(); ws.add_module("com.example.defs".into(), defs).unwrap(); // Using "use com.example.defs;" where "defs" is not a type name // creates a namespace alias "defs" -> "com.example.defs" let app = parse_lexicon( r#" use com.example.defs; record thing { x!: defs.foo, y!: defs.bar, } "#, ) .unwrap(); ws.add_module("com.example.app".into(), app).unwrap(); // Should resolve successfully using the namespace alias let result = ws.resolve(); if let Err(ref e) = result { eprintln!("Errors: {:?}", e); } assert!(result.is_ok()); // Verify that no types were imported (it's just a namespace alias) let imports = ws.get_imports("com.example.app"); assert_eq!(imports.len(), 0); } }