Something went wrong. Try again.
A human-friendly DSL for ATProto Lexicons
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649use 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<String, Module>,}
/// 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<String, Symbol>,}
#[derive(Debug, Clone, PartialEq, Default)]struct ImportTable { mappings: BTreeMap<String, ImportedSymbol>, used_imports: BTreeSet<String>, // Maps namespace alias to full namespace path // e.g., "example" -> "com.example" namespace_aliases: BTreeMap<String, String>,}
#[derive(Debug, Clone, PartialEq)]struct ImportedSymbol { original_path: Vec<String>, 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<Self, ValidationErrors> { 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<Self, ValidationErrors> { 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<String> = 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<ValidationError> = 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<Constraint> { 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<PrimitiveType> { 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<Type> { 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::<Vec<_>>() .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::<Vec<_>>() .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<String>)> { 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<String> { 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<String> { 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<ResolvedRef> { 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::<Vec<_>>() .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<String> { 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<String> = 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::<Vec<_>>() .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<SymbolTable, ValidationErrors> { 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<String, Vec<&Item>> = 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::<Vec<_>>() .join(".") } } else { path.segments[..path.segments.len() - 1] .iter() .map(|s| s.name.as_str()) .collect::<Vec<_>>() .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); }}