Skip to main content

rexlang_lsp/
lib.rs

1#![forbid(unsafe_code)]
2#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
3
4use std::collections::{BTreeSet, HashMap, HashSet};
5use std::fs;
6use std::hash::{Hash, Hasher};
7use std::path::PathBuf;
8use std::sync::{Mutex, OnceLock};
9
10use lsp_types::{
11    CodeAction, CodeActionKind, CodeActionOrCommand, CompletionItem, CompletionItemKind,
12    Diagnostic, DiagnosticSeverity, DocumentSymbol, GotoDefinitionResponse, Hover, HoverContents,
13    Location, MarkupContent, MarkupKind, Position, Range, SymbolKind, TextEdit, Url, WorkspaceEdit,
14};
15use rexlang_ast::expr::{
16    Decl, DeclareFnDecl, Expr, FnDecl, ImportDecl, ImportPath, InstanceDecl, Pattern, Program,
17    Symbol, TypeConstraint, TypeDecl, TypeExpr, Var, intern,
18};
19use rexlang_lexer::{
20    LexicalError, Token, Tokens,
21    span::{Position as RexPosition, Span, Spanned},
22};
23use rexlang_parser::{Parser, error::ParserErr};
24use rexlang_typesystem::{
25    BuiltinTypeId, Scheme, Type, TypeError as TsTypeError, TypeKind, TypeSystem, TypedExpr,
26    TypedExprKind, Types, instantiate, unify,
27};
28use rexlang_util::{GasMeter, sha256_hex};
29use serde_json::{Value, json, to_value};
30#[cfg(not(target_arch = "wasm32"))]
31use tokio::sync::RwLock;
32#[cfg(not(target_arch = "wasm32"))]
33use tower_lsp::jsonrpc::Result;
34#[cfg(not(target_arch = "wasm32"))]
35use tower_lsp::lsp_types::{
36    CodeActionOptions, CodeActionParams, CodeActionResponse, CompletionOptions, CompletionParams,
37    CompletionResponse, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
38    DidOpenTextDocumentParams, DocumentFormattingParams, DocumentSymbolParams,
39    DocumentSymbolResponse, ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams,
40    HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams,
41    MessageType, OneOf, ReferenceParams, RenameParams, ServerCapabilities, ServerInfo,
42    TextDocumentSyncCapability, TextDocumentSyncKind,
43};
44#[cfg(not(target_arch = "wasm32"))]
45use tower_lsp::{Client, LanguageServer, LspService, Server};
46
47const MAX_DIAGNOSTICS: usize = 50;
48const CMD_EXPECTED_TYPE_AT: &str = "rex.expectedTypeAt";
49const CMD_FUNCTIONS_PRODUCING_EXPECTED_TYPE_AT: &str = "rex.functionsProducingExpectedTypeAt";
50const CMD_FUNCTIONS_ACCEPTING_INFERRED_TYPE_AT: &str = "rex.functionsAcceptingInferredTypeAt";
51const CMD_ADAPTERS_FROM_INFERRED_TO_EXPECTED_AT: &str = "rex.adaptersFromInferredToExpectedAt";
52const CMD_FUNCTIONS_COMPATIBLE_WITH_IN_SCOPE_VALUES_AT: &str =
53    "rex.functionsCompatibleWithInScopeValuesAt";
54const CMD_HOLES_EXPECTED_TYPES: &str = "rex.holesExpectedTypes";
55const CMD_SEMANTIC_LOOP_STEP: &str = "rex.semanticLoopStep";
56const CMD_SEMANTIC_LOOP_APPLY_QUICK_FIX_AT: &str = "rex.semanticLoopApplyQuickFixAt";
57const CMD_SEMANTIC_LOOP_APPLY_BEST_QUICK_FIXES_AT: &str = "rex.semanticLoopApplyBestQuickFixesAt";
58const NO_IMPROVEMENT_STREAK_LIMIT: usize = 2;
59const MAX_SEMANTIC_ENV_SCHEMES_SCAN: usize = 1024;
60const MAX_SEMANTIC_IN_SCOPE_VALUES: usize = 128;
61const MAX_SEMANTIC_CANDIDATES: usize = 64;
62const MAX_SEMANTIC_HOLE_FILL_ARITY: usize = 8;
63const MAX_SEMANTIC_HOLES: usize = 128;
64const BUILTIN_TYPES: &[&str] = &[
65    "u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "f32", "f64", "bool", "string", "uuid",
66    "datetime", "Dict", "List", "Array", "Option", "Result",
67];
68const BUILTIN_VALUES: &[&str] = &["true", "false", "null", "Some", "None", "Ok", "Err"];
69
70#[derive(Debug)]
71enum TokenizeOrParseError {
72    Lex(LexicalError),
73    Parse(Vec<ParserErr>),
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77enum BulkQuickFixStrategy {
78    Conservative,
79    Aggressive,
80}
81
82impl BulkQuickFixStrategy {
83    fn parse(s: &str) -> Self {
84        if s.eq_ignore_ascii_case("aggressive") {
85            Self::Aggressive
86        } else {
87            Self::Conservative
88        }
89    }
90
91    fn as_str(self) -> &'static str {
92        match self {
93            Self::Conservative => "conservative",
94            Self::Aggressive => "aggressive",
95        }
96    }
97}
98
99#[derive(Clone)]
100struct CachedParse {
101    hash: u64,
102    tokens: Tokens,
103    program: Program,
104}
105
106fn text_hash(text: &str) -> u64 {
107    let mut hasher = std::collections::hash_map::DefaultHasher::new();
108    text.hash(&mut hasher);
109    hasher.finish()
110}
111
112fn parse_cache() -> &'static Mutex<HashMap<Url, CachedParse>> {
113    static CACHE: OnceLock<Mutex<HashMap<Url, CachedParse>>> = OnceLock::new();
114    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
115}
116
117fn semantic_candidate_values(ts: &TypeSystem) -> Vec<(Symbol, Vec<Scheme>)> {
118    let mut out = Vec::new();
119    let mut scanned = 0usize;
120    for (name, schemes) in &ts.env.values {
121        if scanned >= MAX_SEMANTIC_ENV_SCHEMES_SCAN {
122            break;
123        }
124        let remaining = MAX_SEMANTIC_ENV_SCHEMES_SCAN - scanned;
125        let kept = schemes.iter().take(remaining).cloned().collect::<Vec<_>>();
126        if kept.is_empty() {
127            continue;
128        }
129        scanned += kept.len();
130        out.push((name.clone(), kept));
131    }
132    out
133}
134
135fn clear_parse_cache(uri: &Url) {
136    let Ok(mut cache) = parse_cache().lock() else {
137        return;
138    };
139    cache.remove(uri);
140}
141
142#[cfg(not(target_arch = "wasm32"))]
143fn uri_to_file_path(uri: &Url) -> Option<PathBuf> {
144    uri.to_file_path().ok()
145}
146
147#[cfg(target_arch = "wasm32")]
148fn uri_to_file_path(_uri: &Url) -> Option<PathBuf> {
149    None
150}
151
152#[cfg(not(target_arch = "wasm32"))]
153fn url_from_file_path(path: &std::path::Path) -> Option<Url> {
154    Url::from_file_path(path).ok()
155}
156
157#[cfg(target_arch = "wasm32")]
158fn url_from_file_path(_path: &std::path::Path) -> Option<Url> {
159    None
160}
161
162fn tokenize_and_parse(text: &str) -> std::result::Result<(Tokens, Program), TokenizeOrParseError> {
163    let tokens = Token::tokenize(text).map_err(TokenizeOrParseError::Lex)?;
164    let mut parser = Parser::new(tokens.clone());
165    let program = parser
166        .parse_program(&mut GasMeter::default())
167        .map_err(TokenizeOrParseError::Parse)?;
168    Ok((tokens, program))
169}
170
171fn tokenize_and_parse_cached(
172    uri: &Url,
173    text: &str,
174) -> std::result::Result<(Tokens, Program), TokenizeOrParseError> {
175    let hash = text_hash(text);
176    if let Ok(cache) = parse_cache().lock()
177        && let Some(cached) = cache.get(uri)
178        && cached.hash == hash
179    {
180        return Ok((cached.tokens.clone(), cached.program.clone()));
181    }
182
183    let (tokens, program) = tokenize_and_parse(text)?;
184    if let Ok(mut cache) = parse_cache().lock() {
185        cache.insert(
186            uri.clone(),
187            CachedParse {
188                hash,
189                tokens: tokens.clone(),
190                program: program.clone(),
191            },
192        );
193    }
194    Ok((tokens, program))
195}
196
197#[derive(Clone)]
198struct ImportLibraryInfo {
199    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
200    path: Option<PathBuf>,
201    value_map: HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol>, // field -> internal name
202    type_map: HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol>,
203    class_map: HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol>,
204    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
205    export_defs: HashMap<String, Span>,
206}
207
208fn is_ident_like(name: &str) -> bool {
209    let mut chars = name.chars();
210    let Some(first) = chars.next() else {
211        return false;
212    };
213    if !(first.is_ascii_alphabetic() || first == '_') {
214        return false;
215    }
216    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
217}
218
219fn prelude_completion_values() -> &'static Vec<(String, CompletionItemKind)> {
220    static PRELUDE_VALUES: OnceLock<Vec<(String, CompletionItemKind)>> = OnceLock::new();
221    PRELUDE_VALUES.get_or_init(|| {
222        let ts = match TypeSystem::with_prelude() {
223            Ok(ts) => ts,
224            Err(e) => {
225                eprintln!("rexlang-lsp: failed to build prelude for completions: {e}");
226                return Vec::new();
227            }
228        };
229        let mut out = Vec::new();
230        for (name, schemes) in ts.env.values.iter() {
231            let name = name.as_ref().to_string();
232            if !is_ident_like(&name) {
233                continue;
234            }
235            let is_fun = schemes
236                .iter()
237                .any(|scheme| matches!(scheme.typ.as_ref(), TypeKind::Fun(..)));
238            let kind = if is_fun {
239                CompletionItemKind::FUNCTION
240            } else {
241                CompletionItemKind::VARIABLE
242            };
243            out.push((name, kind));
244        }
245        out.sort_by(|(a, _), (b, _)| a.cmp(b));
246        out
247    })
248}
249
250fn library_prefix(hash: &str) -> String {
251    let short = if hash.len() >= 16 { &hash[..16] } else { hash };
252    format!("@m{short}")
253}
254
255fn inject_program_decls(
256    ts: &mut TypeSystem,
257    program: &Program,
258    want_prepared_instance: Option<usize>,
259) -> std::result::Result<InjectedDecls, TsTypeError> {
260    let mut instances = Vec::new();
261    let mut prepared_target = None;
262    let mut pending_non_instances: Vec<Decl> = Vec::new();
263
264    let flush_non_instances =
265        |ts: &mut TypeSystem, pending: &mut Vec<Decl>| -> std::result::Result<(), TsTypeError> {
266            if pending.is_empty() {
267                return Ok(());
268            }
269            ts.inject_decls(pending)?;
270            pending.clear();
271            Ok(())
272        };
273
274    for (idx, decl) in program.decls.iter().enumerate() {
275        match decl {
276            Decl::Instance(inst_decl) => {
277                flush_non_instances(ts, &mut pending_non_instances)?;
278                let prepared = ts.inject_instance_decl(inst_decl)?;
279                if want_prepared_instance.is_some_and(|want| want == idx) {
280                    prepared_target = Some(prepared.clone());
281                }
282                instances.push((idx, prepared));
283            }
284            _ => pending_non_instances.push(decl.clone()),
285        }
286    }
287    flush_non_instances(ts, &mut pending_non_instances)?;
288
289    Ok((instances, prepared_target))
290}
291
292type PreparedInstanceDecl = rexlang_typesystem::PreparedInstanceDecl;
293type PreparedInstance = (usize, PreparedInstanceDecl);
294type InjectedDecls = (Vec<PreparedInstance>, Option<PreparedInstanceDecl>);
295
296fn rewrite_type_expr(
297    ty: &TypeExpr,
298    type_map: &HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol>,
299) -> TypeExpr {
300    match ty {
301        TypeExpr::Name(span, name) => {
302            if let Some(new) = type_map.get(&name.to_dotted_symbol()) {
303                TypeExpr::Name(*span, rexlang_ast::expr::NameRef::Unqualified(new.clone()))
304            } else {
305                TypeExpr::Name(*span, name.clone())
306            }
307        }
308        TypeExpr::App(span, f, x) => TypeExpr::App(
309            *span,
310            Box::new(rewrite_type_expr(f, type_map)),
311            Box::new(rewrite_type_expr(x, type_map)),
312        ),
313        TypeExpr::Fun(span, a, b) => TypeExpr::Fun(
314            *span,
315            Box::new(rewrite_type_expr(a, type_map)),
316            Box::new(rewrite_type_expr(b, type_map)),
317        ),
318        TypeExpr::Tuple(span, elems) => TypeExpr::Tuple(
319            *span,
320            elems
321                .iter()
322                .map(|e| rewrite_type_expr(e, type_map))
323                .collect(),
324        ),
325        TypeExpr::Record(span, fields) => TypeExpr::Record(
326            *span,
327            fields
328                .iter()
329                .map(|(name, ty)| (name.clone(), rewrite_type_expr(ty, type_map)))
330                .collect(),
331        ),
332    }
333}
334
335fn collect_pattern_bindings(pat: &Pattern, out: &mut Vec<rexlang_ast::expr::Symbol>) {
336    match pat {
337        Pattern::Wildcard(..) => {}
338        Pattern::Var(v) => out.push(v.name.clone()),
339        Pattern::Named(_, _, args) => {
340            for arg in args {
341                collect_pattern_bindings(arg, out);
342            }
343        }
344        Pattern::Tuple(_, elems) | Pattern::List(_, elems) => {
345            for elem in elems {
346                collect_pattern_bindings(elem, out);
347            }
348        }
349        Pattern::Cons(_, head, tail) => {
350            collect_pattern_bindings(head, out);
351            collect_pattern_bindings(tail, out);
352        }
353        Pattern::Dict(_, fields) => {
354            for (_, pat) in fields {
355                collect_pattern_bindings(pat, out);
356            }
357        }
358    }
359}
360
361fn rewrite_import_projections_expr(
362    expr: &Expr,
363    bound: &mut BTreeSet<rexlang_ast::expr::Symbol>,
364    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
365    diagnostics: &mut Vec<Diagnostic>,
366) -> Expr {
367    match expr {
368        Expr::Project(span, base, field) => {
369            if let Expr::Var(v) = base.as_ref()
370                && !bound.contains(&v.name)
371                && let Some(info) = imports.get(&v.name)
372            {
373                if let Some(internal) = info.value_map.get(field) {
374                    return Expr::Var(Var {
375                        span: *span,
376                        name: internal.clone(),
377                    });
378                }
379                diagnostics.push(diagnostic_for_span(
380                    *span,
381                    format!("library `{}` does not export `{}`", v.name, field),
382                ));
383            }
384            Expr::Project(
385                *span,
386                std::sync::Arc::new(rewrite_import_projections_expr(
387                    base,
388                    bound,
389                    imports,
390                    diagnostics,
391                )),
392                field.clone(),
393            )
394        }
395        Expr::Var(v) => Expr::Var(v.clone()),
396        Expr::Bool(span, v) => Expr::Bool(*span, *v),
397        Expr::Uint(span, v) => Expr::Uint(*span, *v),
398        Expr::Int(span, v) => Expr::Int(*span, *v),
399        Expr::Float(span, v) => Expr::Float(*span, *v),
400        Expr::String(span, v) => Expr::String(*span, v.clone()),
401        Expr::Uuid(span, v) => Expr::Uuid(*span, *v),
402        Expr::DateTime(span, v) => Expr::DateTime(*span, *v),
403        Expr::Hole(span) => Expr::Hole(*span),
404        Expr::Tuple(span, elems) => Expr::Tuple(
405            *span,
406            elems
407                .iter()
408                .map(|e| {
409                    std::sync::Arc::new(rewrite_import_projections_expr(
410                        e,
411                        bound,
412                        imports,
413                        diagnostics,
414                    ))
415                })
416                .collect(),
417        ),
418        Expr::List(span, elems) => Expr::List(
419            *span,
420            elems
421                .iter()
422                .map(|e| {
423                    std::sync::Arc::new(rewrite_import_projections_expr(
424                        e,
425                        bound,
426                        imports,
427                        diagnostics,
428                    ))
429                })
430                .collect(),
431        ),
432        Expr::Dict(span, kvs) => Expr::Dict(
433            *span,
434            kvs.iter()
435                .map(|(k, v)| {
436                    (
437                        k.clone(),
438                        std::sync::Arc::new(rewrite_import_projections_expr(
439                            v,
440                            bound,
441                            imports,
442                            diagnostics,
443                        )),
444                    )
445                })
446                .collect(),
447        ),
448        Expr::RecordUpdate(span, base, updates) => Expr::RecordUpdate(
449            *span,
450            std::sync::Arc::new(rewrite_import_projections_expr(
451                base,
452                bound,
453                imports,
454                diagnostics,
455            )),
456            updates
457                .iter()
458                .map(|(k, v)| {
459                    (
460                        k.clone(),
461                        std::sync::Arc::new(rewrite_import_projections_expr(
462                            v,
463                            bound,
464                            imports,
465                            diagnostics,
466                        )),
467                    )
468                })
469                .collect(),
470        ),
471        Expr::App(span, f, x) => Expr::App(
472            *span,
473            std::sync::Arc::new(rewrite_import_projections_expr(
474                f,
475                bound,
476                imports,
477                diagnostics,
478            )),
479            std::sync::Arc::new(rewrite_import_projections_expr(
480                x,
481                bound,
482                imports,
483                diagnostics,
484            )),
485        ),
486        Expr::Lam(span, scope, param, ann, constraints, body) => {
487            let ann = ann
488                .as_ref()
489                .map(|t| rewrite_import_projections_type_expr(t, bound, imports));
490            let constraints = constraints
491                .iter()
492                .map(|c| TypeConstraint {
493                    class: rewrite_import_projections_class_name(&c.class, bound, imports),
494                    typ: rewrite_import_projections_type_expr(&c.typ, bound, imports),
495                })
496                .collect();
497            bound.insert(param.name.clone());
498            let out = Expr::Lam(
499                *span,
500                scope.clone(),
501                param.clone(),
502                ann,
503                constraints,
504                std::sync::Arc::new(rewrite_import_projections_expr(
505                    body,
506                    bound,
507                    imports,
508                    diagnostics,
509                )),
510            );
511            bound.remove(&param.name);
512            out
513        }
514        Expr::Let(span, var, ann, val, body) => {
515            let val = std::sync::Arc::new(rewrite_import_projections_expr(
516                val,
517                bound,
518                imports,
519                diagnostics,
520            ));
521            bound.insert(var.name.clone());
522            let body = std::sync::Arc::new(rewrite_import_projections_expr(
523                body,
524                bound,
525                imports,
526                diagnostics,
527            ));
528            bound.remove(&var.name);
529            Expr::Let(
530                *span,
531                var.clone(),
532                ann.as_ref()
533                    .map(|t| rewrite_import_projections_type_expr(t, bound, imports)),
534                val,
535                body,
536            )
537        }
538        Expr::LetRec(span, bindings, body) => {
539            let anns: Vec<Option<TypeExpr>> = bindings
540                .iter()
541                .map(|(_, ann, _)| {
542                    ann.as_ref()
543                        .map(|t| rewrite_import_projections_type_expr(t, bound, imports))
544                })
545                .collect();
546            let names: Vec<Symbol> = bindings
547                .iter()
548                .map(|(var, _, _)| var.name.clone())
549                .collect();
550            for name in &names {
551                bound.insert(name.clone());
552            }
553            let bindings = bindings
554                .iter()
555                .zip(anns)
556                .map(|((var, _ann, def), ann)| {
557                    (
558                        var.clone(),
559                        ann,
560                        std::sync::Arc::new(rewrite_import_projections_expr(
561                            def,
562                            bound,
563                            imports,
564                            diagnostics,
565                        )),
566                    )
567                })
568                .collect();
569            let body = std::sync::Arc::new(rewrite_import_projections_expr(
570                body,
571                bound,
572                imports,
573                diagnostics,
574            ));
575            for name in &names {
576                bound.remove(name);
577            }
578            Expr::LetRec(*span, bindings, body)
579        }
580        Expr::Ite(span, c, t, e) => Expr::Ite(
581            *span,
582            std::sync::Arc::new(rewrite_import_projections_expr(
583                c,
584                bound,
585                imports,
586                diagnostics,
587            )),
588            std::sync::Arc::new(rewrite_import_projections_expr(
589                t,
590                bound,
591                imports,
592                diagnostics,
593            )),
594            std::sync::Arc::new(rewrite_import_projections_expr(
595                e,
596                bound,
597                imports,
598                diagnostics,
599            )),
600        ),
601        Expr::Match(span, scrutinee, arms) => {
602            let scrutinee = std::sync::Arc::new(rewrite_import_projections_expr(
603                scrutinee,
604                bound,
605                imports,
606                diagnostics,
607            ));
608            let mut out_arms = Vec::new();
609            for (pat, arm_expr) in arms {
610                let mut binds = Vec::new();
611                collect_pattern_bindings(pat, &mut binds);
612                for b in &binds {
613                    bound.insert(b.clone());
614                }
615                let arm_expr = std::sync::Arc::new(rewrite_import_projections_expr(
616                    arm_expr,
617                    bound,
618                    imports,
619                    diagnostics,
620                ));
621                for b in &binds {
622                    bound.remove(b);
623                }
624                out_arms.push((pat.clone(), arm_expr));
625            }
626            Expr::Match(*span, scrutinee, out_arms)
627        }
628        Expr::Ann(span, e, t) => Expr::Ann(
629            *span,
630            std::sync::Arc::new(rewrite_import_projections_expr(
631                e,
632                bound,
633                imports,
634                diagnostics,
635            )),
636            rewrite_import_projections_type_expr(t, bound, imports),
637        ),
638    }
639}
640
641fn qualified_alias_member(
642    name: &rexlang_ast::expr::NameRef,
643) -> Option<(&rexlang_ast::expr::Symbol, &rexlang_ast::expr::Symbol)> {
644    match name {
645        rexlang_ast::expr::NameRef::Qualified(_, segments) if segments.len() == 2 => {
646            Some((&segments[0], &segments[1]))
647        }
648        _ => None,
649    }
650}
651
652fn rewrite_import_projections_class_name(
653    class: &rexlang_ast::expr::NameRef,
654    bound: &BTreeSet<rexlang_ast::expr::Symbol>,
655    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
656) -> rexlang_ast::expr::NameRef {
657    let Some((alias, member)) = qualified_alias_member(class) else {
658        return class.clone();
659    };
660    if bound.contains(alias) {
661        return class.clone();
662    }
663    let Some(info) = imports.get(alias) else {
664        return class.clone();
665    };
666    info.class_map
667        .get(member)
668        .map(|s| rexlang_ast::expr::NameRef::Unqualified(s.clone()))
669        .unwrap_or_else(|| class.clone())
670}
671
672fn rewrite_import_projections_type_expr(
673    ty: &TypeExpr,
674    bound: &BTreeSet<rexlang_ast::expr::Symbol>,
675    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
676) -> TypeExpr {
677    match ty {
678        TypeExpr::Name(span, name) => {
679            let Some((alias, member)) = qualified_alias_member(name) else {
680                return TypeExpr::Name(*span, name.clone());
681            };
682            if bound.contains(alias) {
683                return TypeExpr::Name(*span, name.clone());
684            }
685            let Some(info) = imports.get(alias) else {
686                return TypeExpr::Name(*span, name.clone());
687            };
688            if let Some(new) = info.type_map.get(member) {
689                TypeExpr::Name(*span, rexlang_ast::expr::NameRef::Unqualified(new.clone()))
690            } else if let Some(new) = info.class_map.get(member) {
691                TypeExpr::Name(*span, rexlang_ast::expr::NameRef::Unqualified(new.clone()))
692            } else {
693                TypeExpr::Name(*span, name.clone())
694            }
695        }
696        TypeExpr::App(span, f, x) => TypeExpr::App(
697            *span,
698            Box::new(rewrite_import_projections_type_expr(f, bound, imports)),
699            Box::new(rewrite_import_projections_type_expr(x, bound, imports)),
700        ),
701        TypeExpr::Fun(span, a, b) => TypeExpr::Fun(
702            *span,
703            Box::new(rewrite_import_projections_type_expr(a, bound, imports)),
704            Box::new(rewrite_import_projections_type_expr(b, bound, imports)),
705        ),
706        TypeExpr::Tuple(span, elems) => TypeExpr::Tuple(
707            *span,
708            elems
709                .iter()
710                .map(|e| rewrite_import_projections_type_expr(e, bound, imports))
711                .collect(),
712        ),
713        TypeExpr::Record(span, fields) => TypeExpr::Record(
714            *span,
715            fields
716                .iter()
717                .map(|(name, t)| {
718                    (
719                        name.clone(),
720                        rewrite_import_projections_type_expr(t, bound, imports),
721                    )
722                })
723                .collect(),
724        ),
725    }
726}
727
728fn rewrite_program_import_projections(
729    program: &Program,
730    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
731    diagnostics: &mut Vec<Diagnostic>,
732) -> Program {
733    let decl_bound = BTreeSet::new();
734    let decls = program
735        .decls
736        .iter()
737        .map(|decl| match decl {
738            Decl::Fn(fd) => {
739                let mut bound: BTreeSet<rexlang_ast::expr::Symbol> =
740                    fd.params.iter().map(|(v, _)| v.name.clone()).collect();
741                let body = std::sync::Arc::new(rewrite_import_projections_expr(
742                    fd.body.as_ref(),
743                    &mut bound,
744                    imports,
745                    diagnostics,
746                ));
747                Decl::Fn(FnDecl {
748                    span: fd.span,
749                    is_pub: fd.is_pub,
750                    name: fd.name.clone(),
751                    params: fd
752                        .params
753                        .iter()
754                        .map(|(v, t)| {
755                            (
756                                v.clone(),
757                                rewrite_import_projections_type_expr(t, &decl_bound, imports),
758                            )
759                        })
760                        .collect(),
761                    ret: rewrite_import_projections_type_expr(&fd.ret, &decl_bound, imports),
762                    constraints: fd
763                        .constraints
764                        .iter()
765                        .map(|c| TypeConstraint {
766                            class: rewrite_import_projections_class_name(
767                                &c.class,
768                                &decl_bound,
769                                imports,
770                            ),
771                            typ: rewrite_import_projections_type_expr(&c.typ, &decl_bound, imports),
772                        })
773                        .collect(),
774                    body,
775                })
776            }
777            Decl::DeclareFn(df) => Decl::DeclareFn(DeclareFnDecl {
778                span: df.span,
779                is_pub: df.is_pub,
780                name: df.name.clone(),
781                params: df
782                    .params
783                    .iter()
784                    .map(|(v, t)| {
785                        (
786                            v.clone(),
787                            rewrite_import_projections_type_expr(t, &decl_bound, imports),
788                        )
789                    })
790                    .collect(),
791                ret: rewrite_import_projections_type_expr(&df.ret, &decl_bound, imports),
792                constraints: df
793                    .constraints
794                    .iter()
795                    .map(|c| TypeConstraint {
796                        class: rewrite_import_projections_class_name(
797                            &c.class,
798                            &decl_bound,
799                            imports,
800                        ),
801                        typ: rewrite_import_projections_type_expr(&c.typ, &decl_bound, imports),
802                    })
803                    .collect(),
804            }),
805            Decl::Type(td) => Decl::Type(TypeDecl {
806                span: td.span,
807                is_pub: td.is_pub,
808                name: td.name.clone(),
809                params: td.params.clone(),
810                variants: td
811                    .variants
812                    .iter()
813                    .map(|v| rexlang_ast::expr::TypeVariant {
814                        name: v.name.clone(),
815                        args: v
816                            .args
817                            .iter()
818                            .map(|t| rewrite_import_projections_type_expr(t, &decl_bound, imports))
819                            .collect(),
820                    })
821                    .collect(),
822            }),
823            Decl::Class(cd) => Decl::Class(rexlang_ast::expr::ClassDecl {
824                span: cd.span,
825                is_pub: cd.is_pub,
826                name: cd.name.clone(),
827                params: cd.params.clone(),
828                supers: cd
829                    .supers
830                    .iter()
831                    .map(|c| TypeConstraint {
832                        class: rewrite_import_projections_class_name(
833                            &c.class,
834                            &decl_bound,
835                            imports,
836                        ),
837                        typ: rewrite_import_projections_type_expr(&c.typ, &decl_bound, imports),
838                    })
839                    .collect(),
840                methods: cd
841                    .methods
842                    .iter()
843                    .map(|m| rexlang_ast::expr::ClassMethodSig {
844                        name: m.name.clone(),
845                        typ: rewrite_import_projections_type_expr(&m.typ, &decl_bound, imports),
846                    })
847                    .collect(),
848            }),
849            Decl::Instance(inst) => {
850                let methods = inst
851                    .methods
852                    .iter()
853                    .map(|m| {
854                        let mut bound = BTreeSet::new();
855                        let body = std::sync::Arc::new(rewrite_import_projections_expr(
856                            m.body.as_ref(),
857                            &mut bound,
858                            imports,
859                            diagnostics,
860                        ));
861                        rexlang_ast::expr::InstanceMethodImpl {
862                            name: m.name.clone(),
863                            body,
864                        }
865                    })
866                    .collect();
867                Decl::Instance(InstanceDecl {
868                    span: inst.span,
869                    is_pub: inst.is_pub,
870                    class: rewrite_import_projections_class_name(
871                        &rexlang_ast::expr::NameRef::from_dotted(inst.class.as_ref()),
872                        &decl_bound,
873                        imports,
874                    )
875                    .to_dotted_symbol(),
876                    head: rewrite_import_projections_type_expr(&inst.head, &decl_bound, imports),
877                    context: inst
878                        .context
879                        .iter()
880                        .map(|c| TypeConstraint {
881                            class: rewrite_import_projections_class_name(
882                                &c.class,
883                                &decl_bound,
884                                imports,
885                            ),
886                            typ: rewrite_import_projections_type_expr(&c.typ, &decl_bound, imports),
887                        })
888                        .collect(),
889                    methods,
890                })
891            }
892            other => other.clone(),
893        })
894        .collect();
895
896    let mut bound = BTreeSet::new();
897    let expr = std::sync::Arc::new(rewrite_import_projections_expr(
898        program.expr.as_ref(),
899        &mut bound,
900        imports,
901        diagnostics,
902    ));
903
904    Program { decls, expr }
905}
906
907fn validate_import_projection_class_name(
908    class: &rexlang_ast::expr::NameRef,
909    span: Span,
910    bound: &BTreeSet<rexlang_ast::expr::Symbol>,
911    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
912    diagnostics: &mut Vec<Diagnostic>,
913) {
914    let Some((alias, member)) = qualified_alias_member(class) else {
915        return;
916    };
917    if bound.contains(alias) {
918        return;
919    }
920    let Some(info) = imports.get(alias) else {
921        return;
922    };
923    if info.class_map.contains_key(member) {
924        return;
925    }
926    diagnostics.push(diagnostic_for_span(
927        span,
928        format!("library `{alias}` does not export `{member}`"),
929    ));
930}
931
932fn validate_import_projection_type_expr(
933    ty: &TypeExpr,
934    bound: &BTreeSet<rexlang_ast::expr::Symbol>,
935    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
936    diagnostics: &mut Vec<Diagnostic>,
937) {
938    match ty {
939        TypeExpr::Name(span, name) => {
940            let Some((alias, member)) = qualified_alias_member(name) else {
941                return;
942            };
943            if bound.contains(alias) {
944                return;
945            }
946            let Some(info) = imports.get(alias) else {
947                return;
948            };
949            if info.type_map.contains_key(member) || info.class_map.contains_key(member) {
950                return;
951            }
952            diagnostics.push(diagnostic_for_span(
953                *span,
954                format!("library `{alias}` does not export `{member}`"),
955            ));
956        }
957        TypeExpr::App(_, f, x) => {
958            validate_import_projection_type_expr(f, bound, imports, diagnostics);
959            validate_import_projection_type_expr(x, bound, imports, diagnostics);
960        }
961        TypeExpr::Fun(_, a, b) => {
962            validate_import_projection_type_expr(a, bound, imports, diagnostics);
963            validate_import_projection_type_expr(b, bound, imports, diagnostics);
964        }
965        TypeExpr::Tuple(_, elems) => {
966            for e in elems {
967                validate_import_projection_type_expr(e, bound, imports, diagnostics);
968            }
969        }
970        TypeExpr::Record(_, fields) => {
971            for (_, t) in fields {
972                validate_import_projection_type_expr(t, bound, imports, diagnostics);
973            }
974        }
975    }
976}
977
978fn validate_import_projection_expr(
979    expr: &Expr,
980    bound: &mut BTreeSet<rexlang_ast::expr::Symbol>,
981    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
982    diagnostics: &mut Vec<Diagnostic>,
983) {
984    match expr {
985        Expr::Lam(_, _, param, ann, constraints, body) => {
986            if let Some(ann) = ann {
987                validate_import_projection_type_expr(ann, bound, imports, diagnostics);
988            }
989            for c in constraints {
990                validate_import_projection_class_name(
991                    &c.class,
992                    *c.typ.span(),
993                    bound,
994                    imports,
995                    diagnostics,
996                );
997                validate_import_projection_type_expr(&c.typ, bound, imports, diagnostics);
998            }
999            bound.insert(param.name.clone());
1000            validate_import_projection_expr(body, bound, imports, diagnostics);
1001            bound.remove(&param.name);
1002        }
1003        Expr::Let(_, var, ann, val, body) => {
1004            if let Some(ann) = ann {
1005                validate_import_projection_type_expr(ann, bound, imports, diagnostics);
1006            }
1007            validate_import_projection_expr(val, bound, imports, diagnostics);
1008            bound.insert(var.name.clone());
1009            validate_import_projection_expr(body, bound, imports, diagnostics);
1010            bound.remove(&var.name);
1011        }
1012        Expr::LetRec(_, bindings, body) => {
1013            for (_, ann, _) in bindings {
1014                if let Some(ann) = ann {
1015                    validate_import_projection_type_expr(ann, bound, imports, diagnostics);
1016                }
1017            }
1018            let names: Vec<_> = bindings
1019                .iter()
1020                .map(|(var, _, _)| var.name.clone())
1021                .collect();
1022            for name in &names {
1023                bound.insert(name.clone());
1024            }
1025            for (_, _ann, def) in bindings {
1026                validate_import_projection_expr(def, bound, imports, diagnostics);
1027            }
1028            validate_import_projection_expr(body, bound, imports, diagnostics);
1029            for name in &names {
1030                bound.remove(name);
1031            }
1032        }
1033        Expr::Match(_, scrutinee, arms) => {
1034            validate_import_projection_expr(scrutinee, bound, imports, diagnostics);
1035            for (pat, arm_expr) in arms {
1036                let mut binds = Vec::new();
1037                collect_pattern_bindings(pat, &mut binds);
1038                for b in &binds {
1039                    bound.insert(b.clone());
1040                }
1041                validate_import_projection_expr(arm_expr, bound, imports, diagnostics);
1042                for b in &binds {
1043                    bound.remove(b);
1044                }
1045            }
1046        }
1047        Expr::Tuple(_, elems) | Expr::List(_, elems) => {
1048            for e in elems {
1049                validate_import_projection_expr(e, bound, imports, diagnostics);
1050            }
1051        }
1052        Expr::Dict(_, kvs) => {
1053            for v in kvs.values() {
1054                validate_import_projection_expr(v, bound, imports, diagnostics);
1055            }
1056        }
1057        Expr::RecordUpdate(_, base, updates) => {
1058            validate_import_projection_expr(base, bound, imports, diagnostics);
1059            for v in updates.values() {
1060                validate_import_projection_expr(v, bound, imports, diagnostics);
1061            }
1062        }
1063        Expr::App(_, f, x) => {
1064            validate_import_projection_expr(f, bound, imports, diagnostics);
1065            validate_import_projection_expr(x, bound, imports, diagnostics);
1066        }
1067        Expr::Ite(_, c, t, e) => {
1068            validate_import_projection_expr(c, bound, imports, diagnostics);
1069            validate_import_projection_expr(t, bound, imports, diagnostics);
1070            validate_import_projection_expr(e, bound, imports, diagnostics);
1071        }
1072        Expr::Ann(_, e, t) => {
1073            validate_import_projection_expr(e, bound, imports, diagnostics);
1074            validate_import_projection_type_expr(t, bound, imports, diagnostics);
1075        }
1076        Expr::Project(_, base, _) => {
1077            validate_import_projection_expr(base, bound, imports, diagnostics);
1078        }
1079        Expr::Var(..)
1080        | Expr::Bool(..)
1081        | Expr::Uint(..)
1082        | Expr::Int(..)
1083        | Expr::Float(..)
1084        | Expr::String(..)
1085        | Expr::Uuid(..)
1086        | Expr::DateTime(..)
1087        | Expr::Hole(..) => {}
1088    }
1089}
1090
1091fn validate_import_projection_uses(
1092    program: &Program,
1093    imports: &HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
1094    diagnostics: &mut Vec<Diagnostic>,
1095) {
1096    let decl_bound = BTreeSet::new();
1097    for decl in &program.decls {
1098        match decl {
1099            Decl::Fn(fd) => {
1100                for (_, t) in &fd.params {
1101                    validate_import_projection_type_expr(t, &decl_bound, imports, diagnostics);
1102                }
1103                validate_import_projection_type_expr(&fd.ret, &decl_bound, imports, diagnostics);
1104                for c in &fd.constraints {
1105                    validate_import_projection_class_name(
1106                        &c.class,
1107                        *c.typ.span(),
1108                        &decl_bound,
1109                        imports,
1110                        diagnostics,
1111                    );
1112                    validate_import_projection_type_expr(&c.typ, &decl_bound, imports, diagnostics);
1113                }
1114                let mut bound: BTreeSet<rexlang_ast::expr::Symbol> =
1115                    fd.params.iter().map(|(v, _)| v.name.clone()).collect();
1116                validate_import_projection_expr(fd.body.as_ref(), &mut bound, imports, diagnostics);
1117            }
1118            Decl::DeclareFn(df) => {
1119                for (_, t) in &df.params {
1120                    validate_import_projection_type_expr(t, &decl_bound, imports, diagnostics);
1121                }
1122                validate_import_projection_type_expr(&df.ret, &decl_bound, imports, diagnostics);
1123                for c in &df.constraints {
1124                    validate_import_projection_class_name(
1125                        &c.class,
1126                        *c.typ.span(),
1127                        &decl_bound,
1128                        imports,
1129                        diagnostics,
1130                    );
1131                    validate_import_projection_type_expr(&c.typ, &decl_bound, imports, diagnostics);
1132                }
1133            }
1134            Decl::Type(td) => {
1135                for v in &td.variants {
1136                    for t in &v.args {
1137                        validate_import_projection_type_expr(t, &decl_bound, imports, diagnostics);
1138                    }
1139                }
1140            }
1141            Decl::Class(cd) => {
1142                for c in &cd.supers {
1143                    validate_import_projection_class_name(
1144                        &c.class,
1145                        *c.typ.span(),
1146                        &decl_bound,
1147                        imports,
1148                        diagnostics,
1149                    );
1150                    validate_import_projection_type_expr(&c.typ, &decl_bound, imports, diagnostics);
1151                }
1152                for m in &cd.methods {
1153                    validate_import_projection_type_expr(&m.typ, &decl_bound, imports, diagnostics);
1154                }
1155            }
1156            Decl::Instance(inst) => {
1157                validate_import_projection_class_name(
1158                    &rexlang_ast::expr::NameRef::from_dotted(inst.class.as_ref()),
1159                    inst.span,
1160                    &decl_bound,
1161                    imports,
1162                    diagnostics,
1163                );
1164                validate_import_projection_type_expr(&inst.head, &decl_bound, imports, diagnostics);
1165                for c in &inst.context {
1166                    validate_import_projection_class_name(
1167                        &c.class,
1168                        *c.typ.span(),
1169                        &decl_bound,
1170                        imports,
1171                        diagnostics,
1172                    );
1173                    validate_import_projection_type_expr(&c.typ, &decl_bound, imports, diagnostics);
1174                }
1175                for m in &inst.methods {
1176                    let mut bound = BTreeSet::new();
1177                    validate_import_projection_expr(
1178                        m.body.as_ref(),
1179                        &mut bound,
1180                        imports,
1181                        diagnostics,
1182                    );
1183                }
1184            }
1185            Decl::Import(..) => {}
1186        }
1187    }
1188    let mut bound = BTreeSet::new();
1189    validate_import_projection_expr(program.expr.as_ref(), &mut bound, imports, diagnostics);
1190}
1191
1192type PreparedProgram = (
1193    Program,
1194    TypeSystem,
1195    HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo>,
1196    Vec<Diagnostic>,
1197);
1198
1199fn prepare_program_with_imports(
1200    uri: &Url,
1201    program: &Program,
1202) -> std::result::Result<PreparedProgram, String> {
1203    let mut ts = TypeSystem::with_prelude().map_err(|e| format!("failed to build prelude: {e}"))?;
1204    let mut diagnostics = Vec::new();
1205
1206    let importer = uri_to_file_path(uri);
1207
1208    let mut imports: HashMap<rexlang_ast::expr::Symbol, ImportLibraryInfo> = HashMap::new();
1209
1210    for decl in &program.decls {
1211        let Decl::Import(ImportDecl {
1212            span, path, alias, ..
1213        }) = decl
1214        else {
1215            continue;
1216        };
1217        let import_span = *span;
1218
1219        let (segments, expected_sha) = match path {
1220            ImportPath::Local { segments, sha } => (segments.as_slice(), sha.as_deref()),
1221            ImportPath::Remote { .. } => {
1222                // LSP does not attempt network fetches; leave it unresolved.
1223                continue;
1224            }
1225        };
1226
1227        let library_name = segments
1228            .iter()
1229            .map(|s| s.as_ref())
1230            .collect::<Vec<_>>()
1231            .join(".");
1232
1233        let (library_path, hash, source, library_label, keep_constraints) = if let Some(source) =
1234            rexlang_util::stdlib_source(&library_name)
1235        {
1236            let hash = sha256_hex(source.as_bytes());
1237            if let Some(expected) = expected_sha {
1238                let expected = expected.to_ascii_lowercase();
1239                if !hash.starts_with(&expected) {
1240                    diagnostics.push(diagnostic_for_span(
1241                        import_span,
1242                        format!(
1243                            "sha mismatch for `{library_name}`: expected #{expected}, got #{hash}",
1244                        ),
1245                    ));
1246                }
1247            }
1248            (None, hash, source.to_string(), library_name, true)
1249        } else {
1250            let Some(importer) = importer.as_ref() else {
1251                // Without a stable file location we cannot resolve local imports.
1252                // (Stdlib imports are handled above.)
1253                continue;
1254            };
1255            let Some(base_dir) = importer.parent() else {
1256                diagnostics.push(diagnostic_for_span(
1257                    import_span,
1258                    "cannot resolve local import without a base directory".to_string(),
1259                ));
1260                continue;
1261            };
1262            let library_path = match rexlang_util::resolve_local_import_path(base_dir, segments) {
1263                Ok(Some(p)) => p,
1264                Ok(None) => {
1265                    diagnostics.push(diagnostic_for_span(
1266                        import_span,
1267                        format!("library not found for import `{library_name}`"),
1268                    ));
1269                    continue;
1270                }
1271                Err(err) => {
1272                    diagnostics.push(diagnostic_for_span(import_span, err.to_string()));
1273                    continue;
1274                }
1275            };
1276            let Ok(library_path) = library_path.canonicalize() else {
1277                diagnostics.push(diagnostic_for_span(
1278                    import_span,
1279                    format!("library not found for import `{library_name}`"),
1280                ));
1281                continue;
1282            };
1283
1284            let bytes = match fs::read(&library_path) {
1285                Ok(b) => b,
1286                Err(e) => {
1287                    diagnostics.push(diagnostic_for_span(
1288                        import_span,
1289                        format!("failed to read library `{}`: {e}", library_path.display()),
1290                    ));
1291                    continue;
1292                }
1293            };
1294            let hash = sha256_hex(&bytes);
1295            if let Some(expected) = expected_sha {
1296                let expected = expected.to_ascii_lowercase();
1297                if !hash.starts_with(&expected) {
1298                    diagnostics.push(diagnostic_for_span(
1299                        import_span,
1300                        format!(
1301                            "sha mismatch for `{}`: expected #{expected}, got #{hash}",
1302                            library_path.display()
1303                        ),
1304                    ));
1305                }
1306            }
1307
1308            let source = match String::from_utf8(bytes) {
1309                Ok(s) => s,
1310                Err(e) => {
1311                    diagnostics.push(diagnostic_for_span(
1312                        import_span,
1313                        format!("library `{}` is not utf-8: {e}", library_path.display()),
1314                    ));
1315                    continue;
1316                }
1317            };
1318            (
1319                Some(library_path.clone()),
1320                hash,
1321                source,
1322                library_path.display().to_string(),
1323                false,
1324            )
1325        };
1326
1327        let (tokens, library_program) = match tokenize_and_parse(&source) {
1328            Ok(v) => v,
1329            Err(TokenizeOrParseError::Lex(err)) => {
1330                let msg = match err {
1331                    LexicalError::UnexpectedToken(span) => format!(
1332                        "lex error in library `{}` at {}:{}",
1333                        library_label, span.begin.line, span.begin.column
1334                    ),
1335                    LexicalError::InvalidLiteral {
1336                        kind,
1337                        text,
1338                        error,
1339                        span,
1340                    } => format!(
1341                        "lex error in library `{}` at {}:{}: invalid {kind} literal `{text}`: {error}",
1342                        library_label, span.begin.line, span.begin.column
1343                    ),
1344                    LexicalError::Internal(msg) => {
1345                        format!("internal lexer error in library `{library_label}`: {msg}")
1346                    }
1347                };
1348                diagnostics.push(diagnostic_for_span(import_span, msg));
1349                continue;
1350            }
1351            Err(TokenizeOrParseError::Parse(errs)) => {
1352                for err in errs {
1353                    diagnostics.push(diagnostic_for_span(
1354                        import_span,
1355                        format!(
1356                            "parse error in library `{}` at {}:{}: {}",
1357                            library_label, err.span.begin.line, err.span.begin.column, err.message
1358                        ),
1359                    ));
1360                    if diagnostics.len() >= MAX_DIAGNOSTICS {
1361                        break;
1362                    }
1363                }
1364                continue;
1365            }
1366        };
1367
1368        let index = index_decl_spans(&library_program, &tokens);
1369        let prefix = library_prefix(&hash);
1370
1371        let mut type_map: HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol> =
1372            HashMap::new();
1373        let mut class_map: HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol> =
1374            HashMap::new();
1375        for decl in &library_program.decls {
1376            match decl {
1377                Decl::Type(td) => {
1378                    type_map.insert(
1379                        td.name.clone(),
1380                        intern(&format!("{prefix}.{}", td.name.as_ref())),
1381                    );
1382                }
1383                Decl::Class(cd) => {
1384                    class_map.insert(
1385                        cd.name.clone(),
1386                        intern(&format!("{prefix}.{}", cd.name.as_ref())),
1387                    );
1388                }
1389                _ => {}
1390            }
1391        }
1392
1393        // Inject library type decls (renamed) so exported signatures can refer to them.
1394        for decl in &library_program.decls {
1395            let Decl::Type(td) = decl else { continue };
1396            let name = type_map
1397                .get(&td.name)
1398                .cloned()
1399                .unwrap_or_else(|| td.name.clone());
1400            let variants = td
1401                .variants
1402                .iter()
1403                .map(|v| rexlang_ast::expr::TypeVariant {
1404                    name: intern(&format!("{prefix}.{}", v.name.as_ref())),
1405                    args: v
1406                        .args
1407                        .iter()
1408                        .map(|t| rewrite_type_expr(t, &type_map))
1409                        .collect(),
1410                })
1411                .collect();
1412            let td2 = TypeDecl {
1413                span: td.span,
1414                is_pub: td.is_pub,
1415                name,
1416                params: td.params.clone(),
1417                variants,
1418            };
1419            let _ = ts.inject_type_decl(&td2);
1420        }
1421
1422        let mut value_map: HashMap<rexlang_ast::expr::Symbol, rexlang_ast::expr::Symbol> =
1423            HashMap::new();
1424        let mut export_names: BTreeSet<String> = BTreeSet::new();
1425
1426        // Exported functions (pub only)
1427        for decl in &library_program.decls {
1428            match decl {
1429                Decl::Fn(fd) if fd.is_pub => {
1430                    let internal = intern(&format!("{prefix}.{}", fd.name.name.as_ref()));
1431                    value_map.insert(intern(fd.name.name.as_ref()), internal.clone());
1432                    export_names.insert(fd.name.name.as_ref().to_string());
1433
1434                    let params = fd
1435                        .params
1436                        .iter()
1437                        .map(|(v, ty)| (v.clone(), rewrite_type_expr(ty, &type_map)))
1438                        .collect();
1439                    let ret = rewrite_type_expr(&fd.ret, &type_map);
1440                    let decl = DeclareFnDecl {
1441                        span: fd.span,
1442                        is_pub: true,
1443                        name: Var {
1444                            span: fd.name.span,
1445                            name: internal,
1446                        },
1447                        params,
1448                        ret,
1449                        constraints: if keep_constraints {
1450                            fd.constraints.clone()
1451                        } else {
1452                            Default::default()
1453                        },
1454                    };
1455                    let _ = ts.inject_declare_fn_decl(&decl);
1456                }
1457                Decl::DeclareFn(df) if df.is_pub => {
1458                    let internal = intern(&format!("{prefix}.{}", df.name.name.as_ref()));
1459                    value_map.insert(intern(df.name.name.as_ref()), internal.clone());
1460                    export_names.insert(df.name.name.as_ref().to_string());
1461
1462                    let params = df
1463                        .params
1464                        .iter()
1465                        .map(|(v, ty)| (v.clone(), rewrite_type_expr(ty, &type_map)))
1466                        .collect();
1467                    let ret = rewrite_type_expr(&df.ret, &type_map);
1468                    let decl = DeclareFnDecl {
1469                        span: df.span,
1470                        is_pub: true,
1471                        name: Var {
1472                            span: df.name.span,
1473                            name: internal,
1474                        },
1475                        params,
1476                        ret,
1477                        constraints: if keep_constraints {
1478                            df.constraints.clone()
1479                        } else {
1480                            Default::default()
1481                        },
1482                    };
1483                    let _ = ts.inject_declare_fn_decl(&decl);
1484                }
1485                Decl::Type(td) if td.is_pub => {
1486                    // Public constructors are accessible as values.
1487                    for variant in &td.variants {
1488                        let internal = intern(&format!("{prefix}.{}", variant.name.as_ref()));
1489                        value_map.insert(variant.name.clone(), internal);
1490                        export_names.insert(variant.name.as_ref().to_string());
1491                    }
1492                }
1493                _ => {}
1494            }
1495        }
1496
1497        let mut export_defs = HashMap::new();
1498        for name in &export_names {
1499            if let Some(span) = index
1500                .fn_defs
1501                .get(name)
1502                .copied()
1503                .or_else(|| index.ctor_defs.get(name).copied())
1504            {
1505                export_defs.insert(name.clone(), span);
1506            }
1507        }
1508
1509        imports.insert(
1510            alias.clone(),
1511            ImportLibraryInfo {
1512                path: library_path,
1513                value_map,
1514                type_map,
1515                class_map,
1516                export_defs,
1517            },
1518        );
1519    }
1520
1521    validate_import_projection_uses(program, &imports, &mut diagnostics);
1522    let rewritten = rewrite_program_import_projections(program, &imports, &mut diagnostics);
1523    Ok((rewritten, ts, imports, diagnostics))
1524}
1525
1526fn completion_exports_for_library_alias(
1527    uri: &Url,
1528    program: &Program,
1529    alias: &str,
1530) -> std::result::Result<Vec<String>, String> {
1531    let alias_sym = intern(alias);
1532    let Some(import_decl) = program.decls.iter().find_map(|d| {
1533        let Decl::Import(id) = d else { return None };
1534        if id.alias == alias_sym {
1535            Some(id)
1536        } else {
1537            None
1538        }
1539    }) else {
1540        return Ok(Vec::new());
1541    };
1542
1543    let ImportPath::Local { segments, sha: _ } = &import_decl.path else {
1544        return Ok(Vec::new());
1545    };
1546
1547    let library_name = segments
1548        .iter()
1549        .map(|s| s.as_ref())
1550        .collect::<Vec<_>>()
1551        .join(".");
1552
1553    let source = if let Some(source) = rexlang_util::stdlib_source(&library_name) {
1554        source.to_string()
1555    } else {
1556        let importer = uri_to_file_path(uri).ok_or_else(|| "not a file uri".to_string())?;
1557        let Some(base_dir) = importer.parent() else {
1558            return Ok(Vec::new());
1559        };
1560        let Some(library_path) = rexlang_util::resolve_local_import_path(base_dir, segments)
1561            .ok()
1562            .flatten()
1563            .and_then(|p| p.canonicalize().ok())
1564        else {
1565            return Ok(Vec::new());
1566        };
1567        fs::read_to_string(&library_path).map_err(|e| e.to_string())?
1568    };
1569    let (_tokens, library_program) =
1570        tokenize_and_parse(&source).map_err(|_| "parse error".to_string())?;
1571
1572    let mut exports = BTreeSet::new();
1573    for decl in &library_program.decls {
1574        match decl {
1575            Decl::Fn(fd) if fd.is_pub => {
1576                exports.insert(fd.name.name.as_ref().to_string());
1577            }
1578            Decl::DeclareFn(df) if df.is_pub => {
1579                exports.insert(df.name.name.as_ref().to_string());
1580            }
1581            Decl::Type(td) if td.is_pub => {
1582                for variant in &td.variants {
1583                    exports.insert(variant.name.as_ref().to_string());
1584                }
1585            }
1586            _ => {}
1587        }
1588    }
1589    Ok(exports.into_iter().collect())
1590}
1591
1592#[cfg(not(target_arch = "wasm32"))]
1593struct RexServer {
1594    client: Client,
1595    documents: RwLock<HashMap<Url, String>>,
1596}
1597
1598#[cfg(not(target_arch = "wasm32"))]
1599impl RexServer {
1600    fn new(client: Client) -> Self {
1601        Self {
1602            client,
1603            documents: RwLock::new(HashMap::new()),
1604        }
1605    }
1606
1607    async fn publish_diagnostics(&self, uri: Url, text: &str) {
1608        let uri_for_job = uri.clone();
1609        let text_for_job = text.to_string();
1610        let diagnostics = match tokio::task::spawn_blocking(move || {
1611            diagnostics_from_text(&uri_for_job, &text_for_job)
1612        })
1613        .await
1614        {
1615            Ok(diags) => diags,
1616            Err(err) => {
1617                self.client
1618                    .log_message(
1619                        MessageType::ERROR,
1620                        format!("failed to compute diagnostics: {err}"),
1621                    )
1622                    .await;
1623                Vec::new()
1624            }
1625        };
1626        self.client
1627            .publish_diagnostics(uri, diagnostics, None)
1628            .await;
1629    }
1630}
1631
1632#[cfg(not(target_arch = "wasm32"))]
1633#[tower_lsp::async_trait]
1634impl LanguageServer for RexServer {
1635    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
1636        Ok(InitializeResult {
1637            capabilities: ServerCapabilities {
1638                text_document_sync: Some(TextDocumentSyncCapability::Kind(
1639                    TextDocumentSyncKind::FULL,
1640                )),
1641                hover_provider: Some(HoverProviderCapability::Simple(true)),
1642                completion_provider: Some(CompletionOptions {
1643                    trigger_characters: Some(vec![".".to_string()]),
1644                    ..CompletionOptions::default()
1645                }),
1646                code_action_provider: Some(
1647                    tower_lsp::lsp_types::CodeActionProviderCapability::Options(
1648                        CodeActionOptions {
1649                            code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
1650                            ..CodeActionOptions::default()
1651                        },
1652                    ),
1653                ),
1654                execute_command_provider: Some(ExecuteCommandOptions {
1655                    commands: vec![
1656                        CMD_EXPECTED_TYPE_AT.to_string(),
1657                        CMD_FUNCTIONS_PRODUCING_EXPECTED_TYPE_AT.to_string(),
1658                        CMD_FUNCTIONS_ACCEPTING_INFERRED_TYPE_AT.to_string(),
1659                        CMD_ADAPTERS_FROM_INFERRED_TO_EXPECTED_AT.to_string(),
1660                        CMD_FUNCTIONS_COMPATIBLE_WITH_IN_SCOPE_VALUES_AT.to_string(),
1661                        CMD_HOLES_EXPECTED_TYPES.to_string(),
1662                        CMD_SEMANTIC_LOOP_STEP.to_string(),
1663                        CMD_SEMANTIC_LOOP_APPLY_QUICK_FIX_AT.to_string(),
1664                        CMD_SEMANTIC_LOOP_APPLY_BEST_QUICK_FIXES_AT.to_string(),
1665                    ],
1666                    ..ExecuteCommandOptions::default()
1667                }),
1668                definition_provider: Some(OneOf::Left(true)),
1669                references_provider: Some(OneOf::Left(true)),
1670                rename_provider: Some(OneOf::Left(true)),
1671                document_symbol_provider: Some(OneOf::Left(true)),
1672                document_formatting_provider: Some(OneOf::Left(true)),
1673                ..ServerCapabilities::default()
1674            },
1675            server_info: Some(ServerInfo {
1676                name: "rexlang-lsp".to_string(),
1677                version: Some("0.1.0".to_string()),
1678            }),
1679        })
1680    }
1681
1682    async fn initialized(&self, _: InitializedParams) {
1683        self.client
1684            .log_message(MessageType::INFO, "Rex LSP initialized")
1685            .await;
1686    }
1687
1688    async fn shutdown(&self) -> Result<()> {
1689        Ok(())
1690    }
1691
1692    async fn did_open(&self, params: DidOpenTextDocumentParams) {
1693        let uri = params.text_document.uri;
1694        let text = params.text_document.text;
1695
1696        self.documents
1697            .write()
1698            .await
1699            .insert(uri.clone(), text.clone());
1700        clear_parse_cache(&uri);
1701        self.publish_diagnostics(uri, &text).await;
1702    }
1703
1704    async fn did_change(&self, params: DidChangeTextDocumentParams) {
1705        let uri = params.text_document.uri;
1706        let text = params
1707            .content_changes
1708            .into_iter()
1709            .last()
1710            .map(|change| change.text);
1711
1712        if let Some(text) = text {
1713            self.documents
1714                .write()
1715                .await
1716                .insert(uri.clone(), text.clone());
1717            clear_parse_cache(&uri);
1718            self.publish_diagnostics(uri, &text).await;
1719        }
1720    }
1721
1722    async fn did_close(&self, params: DidCloseTextDocumentParams) {
1723        let uri = params.text_document.uri;
1724        self.documents.write().await.remove(&uri);
1725        clear_parse_cache(&uri);
1726        self.client.publish_diagnostics(uri, Vec::new(), None).await;
1727    }
1728
1729    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
1730        let uri = params.text_document_position_params.text_document.uri;
1731        let position = params.text_document_position_params.position;
1732        let text = { self.documents.read().await.get(&uri).cloned() };
1733
1734        let Some(text) = text else {
1735            return Ok(None);
1736        };
1737
1738        let uri_for_job = uri.clone();
1739        let text_for_job = text.clone();
1740        let type_contents = match tokio::task::spawn_blocking(move || {
1741            hover_type_contents(&uri_for_job, &text_for_job, position)
1742        })
1743        .await
1744        {
1745            Ok(contents) => contents,
1746            Err(err) => {
1747                self.client
1748                    .log_message(MessageType::ERROR, format!("hover failed: {err}"))
1749                    .await;
1750                None
1751            }
1752        };
1753
1754        let contents = type_contents.or_else(|| {
1755            let word = word_at_position(&text, position)?;
1756            hover_contents(&word)
1757        });
1758
1759        Ok(contents.map(|contents| Hover {
1760            contents,
1761            range: None,
1762        }))
1763    }
1764
1765    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
1766        let uri = params.text_document_position.text_document.uri;
1767        let position = params.text_document_position.position;
1768        let text = { self.documents.read().await.get(&uri).cloned() };
1769
1770        let Some(text) = text else {
1771            return Ok(None);
1772        };
1773
1774        let uri_for_job = uri.clone();
1775        let text_for_job = text;
1776        let items = match tokio::task::spawn_blocking(move || {
1777            completion_items(&uri_for_job, &text_for_job, position)
1778        })
1779        .await
1780        {
1781            Ok(items) => items,
1782            Err(err) => {
1783                self.client
1784                    .log_message(MessageType::ERROR, format!("completion failed: {err}"))
1785                    .await;
1786                Vec::new()
1787            }
1788        };
1789        Ok(Some(CompletionResponse::Array(items)))
1790    }
1791
1792    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1793        let uri = params.text_document.uri;
1794        let text = { self.documents.read().await.get(&uri).cloned() };
1795        let Some(text) = text else {
1796            return Ok(None);
1797        };
1798
1799        let range = params.range;
1800        let diagnostics = params.context.diagnostics;
1801        let uri_for_job = uri.clone();
1802        let text_for_job = text;
1803        let actions = match tokio::task::spawn_blocking(move || {
1804            code_actions_for_source(&uri_for_job, &text_for_job, range, &diagnostics)
1805        })
1806        .await
1807        {
1808            Ok(actions) => actions,
1809            Err(err) => {
1810                self.client
1811                    .log_message(MessageType::ERROR, format!("code action failed: {err}"))
1812                    .await;
1813                Vec::new()
1814            }
1815        };
1816
1817        Ok(Some(actions))
1818    }
1819
1820    async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
1821        let arguments = params.arguments;
1822        let command = params.command;
1823        if command == CMD_HOLES_EXPECTED_TYPES {
1824            let Some(uri) = command_uri(&arguments) else {
1825                return Ok(None);
1826            };
1827            let text = { self.documents.read().await.get(&uri).cloned() };
1828            let Some(text) = text else {
1829                return Ok(None);
1830            };
1831            return Ok(execute_query_command_for_document_without_position(
1832                &command, &uri, &text,
1833            ));
1834        }
1835        if command == CMD_SEMANTIC_LOOP_STEP {
1836            let Some((uri, position)) = command_uri_and_position(&arguments) else {
1837                return Ok(None);
1838            };
1839            let text = { self.documents.read().await.get(&uri).cloned() };
1840            let Some(text) = text else {
1841                return Ok(None);
1842            };
1843            return Ok(execute_semantic_loop_step(&uri, &text, position));
1844        }
1845        if command == CMD_SEMANTIC_LOOP_APPLY_QUICK_FIX_AT {
1846            let Some((uri, position, quick_fix_id)) = command_uri_position_and_id(&arguments)
1847            else {
1848                return Ok(None);
1849            };
1850            let text = { self.documents.read().await.get(&uri).cloned() };
1851            let Some(text) = text else {
1852                return Ok(None);
1853            };
1854            return Ok(execute_semantic_loop_apply_quick_fix(
1855                &uri,
1856                &text,
1857                position,
1858                &quick_fix_id,
1859            ));
1860        }
1861        if command == CMD_SEMANTIC_LOOP_APPLY_BEST_QUICK_FIXES_AT {
1862            let Some((uri, position, max_steps, strategy, dry_run)) =
1863                command_uri_position_max_steps_strategy_and_dry_run(&arguments)
1864            else {
1865                return Ok(None);
1866            };
1867            let text = { self.documents.read().await.get(&uri).cloned() };
1868            let Some(text) = text else {
1869                return Ok(None);
1870            };
1871            return Ok(execute_semantic_loop_apply_best_quick_fixes(
1872                &uri, &text, position, max_steps, strategy, dry_run,
1873            ));
1874        }
1875
1876        let Some((uri, position)) = command_uri_and_position(&arguments) else {
1877            return Ok(None);
1878        };
1879        let text = { self.documents.read().await.get(&uri).cloned() };
1880        let Some(text) = text else {
1881            return Ok(None);
1882        };
1883        Ok(execute_query_command_for_document(
1884            &command, &uri, &text, position,
1885        ))
1886    }
1887
1888    async fn goto_definition(
1889        &self,
1890        params: GotoDefinitionParams,
1891    ) -> Result<Option<GotoDefinitionResponse>> {
1892        let uri = params.text_document_position_params.text_document.uri;
1893        let position = params.text_document_position_params.position;
1894        let text = { self.documents.read().await.get(&uri).cloned() };
1895
1896        let Some(text) = text else {
1897            return Ok(None);
1898        };
1899
1900        let uri_for_job = uri.clone();
1901        let text_for_job = text;
1902        let response = match tokio::task::spawn_blocking(move || {
1903            goto_definition_response(&uri_for_job, &text_for_job, position)
1904        })
1905        .await
1906        {
1907            Ok(resp) => resp,
1908            Err(err) => {
1909                self.client
1910                    .log_message(MessageType::ERROR, format!("goto definition failed: {err}"))
1911                    .await;
1912                None
1913            }
1914        };
1915        Ok(response)
1916    }
1917
1918    async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
1919        let uri = params.text_document_position.text_document.uri;
1920        let position = params.text_document_position.position;
1921        let include_declaration = params.context.include_declaration;
1922        let text = { self.documents.read().await.get(&uri).cloned() };
1923
1924        let Some(text) = text else {
1925            return Ok(None);
1926        };
1927
1928        let uri_for_job = uri.clone();
1929        let text_for_job = text;
1930        let refs = match tokio::task::spawn_blocking(move || {
1931            references_for_source(&uri_for_job, &text_for_job, position, include_declaration)
1932        })
1933        .await
1934        {
1935            Ok(items) => items,
1936            Err(err) => {
1937                self.client
1938                    .log_message(MessageType::ERROR, format!("references failed: {err}"))
1939                    .await;
1940                Vec::new()
1941            }
1942        };
1943        Ok(Some(refs))
1944    }
1945
1946    async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
1947        let uri = params.text_document_position.text_document.uri;
1948        let position = params.text_document_position.position;
1949        let new_name = params.new_name;
1950        let text = { self.documents.read().await.get(&uri).cloned() };
1951
1952        let Some(text) = text else {
1953            return Ok(None);
1954        };
1955
1956        let uri_for_job = uri.clone();
1957        let text_for_job = text;
1958        let edit = match tokio::task::spawn_blocking(move || {
1959            rename_for_source(&uri_for_job, &text_for_job, position, &new_name)
1960        })
1961        .await
1962        {
1963            Ok(edit) => edit,
1964            Err(err) => {
1965                self.client
1966                    .log_message(MessageType::ERROR, format!("rename failed: {err}"))
1967                    .await;
1968                None
1969            }
1970        };
1971        Ok(edit)
1972    }
1973
1974    async fn document_symbol(
1975        &self,
1976        params: DocumentSymbolParams,
1977    ) -> Result<Option<DocumentSymbolResponse>> {
1978        let uri = params.text_document.uri;
1979        let text = { self.documents.read().await.get(&uri).cloned() };
1980        let Some(text) = text else {
1981            return Ok(None);
1982        };
1983        let symbols = document_symbols_for_source(&uri, &text);
1984        Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1985    }
1986
1987    async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1988        let uri = params.text_document.uri;
1989        let text = { self.documents.read().await.get(&uri).cloned() };
1990        let Some(text) = text else {
1991            return Ok(None);
1992        };
1993        Ok(format_edits_for_source(&text))
1994    }
1995}
1996
1997fn goto_definition_response(
1998    uri: &Url,
1999    text: &str,
2000    position: Position,
2001) -> Option<GotoDefinitionResponse> {
2002    // Parse on-demand. This keeps steady-state typing latency low; “go to
2003    // definition” is an explicit user action where a little work is fine.
2004    let Ok((tokens, program)) = tokenize_and_parse_cached(uri, text) else {
2005        return None;
2006    };
2007
2008    let imported_projection = imported_projection_at_position(&tokens, position);
2009
2010    let (ident, _token_span) = ident_token_at_position(&tokens, position)?;
2011
2012    // If the cursor is on `alias.field` and `alias` is a local import, jump
2013    // to the exported declaration in the imported library.
2014    if let Some((alias, field)) = imported_projection
2015        && let Ok((_rewritten, _ts, imports, _diags)) = prepare_program_with_imports(uri, &program)
2016    {
2017        let alias_sym = intern(&alias);
2018        if let Some(info) = imports.get(&alias_sym)
2019            && let Some(span) = info.export_defs.get(&field)
2020            && let Some(path) = info.path.as_ref()
2021            && let Some(module_uri) = url_from_file_path(path)
2022        {
2023            return Some(GotoDefinitionResponse::Scalar(Location {
2024                uri: module_uri,
2025                range: span_to_range(*span),
2026            }));
2027        }
2028    }
2029
2030    let index = index_decl_spans(&program, &tokens);
2031    let pos = lsp_to_rex_position(position);
2032
2033    // Pick the expression tree that actually contains the cursor. Top-level
2034    // instance method bodies are not part of `expr_with_fns()`, so we have
2035    // to handle them explicitly.
2036    let expr_with_fns = program.expr_with_fns();
2037    let mut root_expr: &Expr = expr_with_fns.as_ref();
2038    for decl in &program.decls {
2039        let Decl::Instance(inst) = decl else {
2040            continue;
2041        };
2042        for method in &inst.methods {
2043            if position_in_span(pos, *method.body.span()) {
2044                root_expr = method.body.as_ref();
2045                break;
2046            }
2047        }
2048    }
2049
2050    let value_def =
2051        definition_span_for_value_ident(root_expr, pos, &ident, &mut Vec::new(), &tokens);
2052
2053    let instance_method_def = index
2054        .instance_method_defs
2055        .iter()
2056        .find_map(|(span, methods)| {
2057            if position_in_span(pos, *span) {
2058                methods.get(&ident).copied()
2059            } else {
2060                None
2061            }
2062        });
2063
2064    let target_span = value_def
2065        .or(instance_method_def)
2066        .or(index.class_method_defs.get(&ident).copied())
2067        .or(index.fn_defs.get(&ident).copied())
2068        .or(index.ctor_defs.get(&ident).copied())
2069        .or(index.type_defs.get(&ident).copied())
2070        .or(index.class_defs.get(&ident).copied())?;
2071
2072    Some(GotoDefinitionResponse::Scalar(Location {
2073        uri: uri.clone(),
2074        range: span_to_range(target_span),
2075    }))
2076}
2077
2078fn range_to_span(range: Range) -> Span {
2079    Span::new(
2080        (range.start.line + 1) as usize,
2081        (range.start.character + 1) as usize,
2082        (range.end.line + 1) as usize,
2083        (range.end.character + 1) as usize,
2084    )
2085}
2086
2087fn pattern_bindings_with_spans(pat: &Pattern, out: &mut Vec<(String, Span)>) {
2088    match pat {
2089        Pattern::Var(var) => out.push((var.name.to_string(), var.span)),
2090        Pattern::Named(_, _, args) => {
2091            for arg in args {
2092                pattern_bindings_with_spans(arg, out);
2093            }
2094        }
2095        Pattern::Tuple(_, elems) | Pattern::List(_, elems) => {
2096            for elem in elems {
2097                pattern_bindings_with_spans(elem, out);
2098            }
2099        }
2100        Pattern::Cons(_, head, tail) => {
2101            pattern_bindings_with_spans(head, out);
2102            pattern_bindings_with_spans(tail, out);
2103        }
2104        Pattern::Dict(_, fields) => {
2105            for (_, pat) in fields {
2106                pattern_bindings_with_spans(pat, out);
2107            }
2108        }
2109        Pattern::Wildcard(..) => {}
2110    }
2111}
2112
2113fn collect_references_in_expr(
2114    expr: &Expr,
2115    ident: &str,
2116    target_span: Span,
2117    uri: &Url,
2118    top_level_defs: &HashMap<String, Span>,
2119    scope: &mut Vec<(String, Span)>,
2120    out: &mut Vec<Location>,
2121) {
2122    match expr {
2123        Expr::Var(var) => {
2124            if var.name.as_ref() != ident {
2125                return;
2126            }
2127            let resolved = scope
2128                .iter()
2129                .rev()
2130                .find_map(|(name, span)| (name == ident).then_some(*span))
2131                .or_else(|| top_level_defs.get(ident).copied());
2132            if resolved.is_some_and(|span| span == target_span) {
2133                out.push(Location {
2134                    uri: uri.clone(),
2135                    range: span_to_range(var.span),
2136                });
2137            }
2138        }
2139        Expr::Let(_, var, _ann, def, body) => {
2140            collect_references_in_expr(def, ident, target_span, uri, top_level_defs, scope, out);
2141            scope.push((var.name.to_string(), var.span));
2142            collect_references_in_expr(body, ident, target_span, uri, top_level_defs, scope, out);
2143            scope.pop();
2144        }
2145        Expr::LetRec(_, bindings, body) => {
2146            let base_len = scope.len();
2147            for (var, _ann, _def) in bindings {
2148                scope.push((var.name.to_string(), var.span));
2149            }
2150            for (_var, _ann, def) in bindings {
2151                collect_references_in_expr(
2152                    def,
2153                    ident,
2154                    target_span,
2155                    uri,
2156                    top_level_defs,
2157                    scope,
2158                    out,
2159                );
2160            }
2161            collect_references_in_expr(body, ident, target_span, uri, top_level_defs, scope, out);
2162            scope.truncate(base_len);
2163        }
2164        Expr::Lam(_, _scope, param, _ann, _constraints, body) => {
2165            scope.push((param.name.to_string(), param.span));
2166            collect_references_in_expr(body, ident, target_span, uri, top_level_defs, scope, out);
2167            scope.pop();
2168        }
2169        Expr::Match(_, scrutinee, arms) => {
2170            collect_references_in_expr(
2171                scrutinee,
2172                ident,
2173                target_span,
2174                uri,
2175                top_level_defs,
2176                scope,
2177                out,
2178            );
2179            for (pat, arm) in arms {
2180                let base_len = scope.len();
2181                let mut binds = Vec::new();
2182                pattern_bindings_with_spans(pat, &mut binds);
2183                scope.extend(binds);
2184                collect_references_in_expr(
2185                    arm,
2186                    ident,
2187                    target_span,
2188                    uri,
2189                    top_level_defs,
2190                    scope,
2191                    out,
2192                );
2193                scope.truncate(base_len);
2194            }
2195        }
2196        Expr::App(_, fun, arg) => {
2197            collect_references_in_expr(fun, ident, target_span, uri, top_level_defs, scope, out);
2198            collect_references_in_expr(arg, ident, target_span, uri, top_level_defs, scope, out);
2199        }
2200        Expr::Project(_, base, _) => {
2201            collect_references_in_expr(base, ident, target_span, uri, top_level_defs, scope, out);
2202        }
2203        Expr::Tuple(_, elems) | Expr::List(_, elems) => {
2204            for elem in elems {
2205                collect_references_in_expr(
2206                    elem,
2207                    ident,
2208                    target_span,
2209                    uri,
2210                    top_level_defs,
2211                    scope,
2212                    out,
2213                );
2214            }
2215        }
2216        Expr::Dict(_, entries) => {
2217            for value in entries.values() {
2218                collect_references_in_expr(
2219                    value,
2220                    ident,
2221                    target_span,
2222                    uri,
2223                    top_level_defs,
2224                    scope,
2225                    out,
2226                );
2227            }
2228        }
2229        Expr::RecordUpdate(_, base, updates) => {
2230            collect_references_in_expr(base, ident, target_span, uri, top_level_defs, scope, out);
2231            for value in updates.values() {
2232                collect_references_in_expr(
2233                    value,
2234                    ident,
2235                    target_span,
2236                    uri,
2237                    top_level_defs,
2238                    scope,
2239                    out,
2240                );
2241            }
2242        }
2243        Expr::Ite(_, cond, then_expr, else_expr) => {
2244            collect_references_in_expr(cond, ident, target_span, uri, top_level_defs, scope, out);
2245            collect_references_in_expr(
2246                then_expr,
2247                ident,
2248                target_span,
2249                uri,
2250                top_level_defs,
2251                scope,
2252                out,
2253            );
2254            collect_references_in_expr(
2255                else_expr,
2256                ident,
2257                target_span,
2258                uri,
2259                top_level_defs,
2260                scope,
2261                out,
2262            );
2263        }
2264        Expr::Ann(_, inner, _) => {
2265            collect_references_in_expr(inner, ident, target_span, uri, top_level_defs, scope, out);
2266        }
2267        Expr::Bool(..)
2268        | Expr::Uint(..)
2269        | Expr::Int(..)
2270        | Expr::Float(..)
2271        | Expr::String(..)
2272        | Expr::Uuid(..)
2273        | Expr::DateTime(..)
2274        | Expr::Hole(..) => {}
2275    }
2276}
2277
2278fn references_for_source(
2279    uri: &Url,
2280    text: &str,
2281    position: Position,
2282    include_declaration: bool,
2283) -> Vec<Location> {
2284    let Ok((tokens, program)) = tokenize_and_parse_cached(uri, text) else {
2285        return Vec::new();
2286    };
2287    let Some((ident, _token_span)) = ident_token_at_position(&tokens, position) else {
2288        return Vec::new();
2289    };
2290
2291    let Some(def_response) = goto_definition_response(uri, text, position) else {
2292        return Vec::new();
2293    };
2294    let GotoDefinitionResponse::Scalar(def_location) = def_response else {
2295        return Vec::new();
2296    };
2297    if def_location.uri != *uri {
2298        return Vec::new();
2299    }
2300    let target_span = range_to_span(def_location.range);
2301
2302    let index = index_decl_spans(&program, &tokens);
2303    let mut top_level_defs = index.fn_defs;
2304    top_level_defs.extend(index.ctor_defs);
2305
2306    let mut refs = Vec::new();
2307    if include_declaration {
2308        refs.push(def_location);
2309    }
2310    let expr = program.expr_with_fns();
2311    collect_references_in_expr(
2312        expr.as_ref(),
2313        &ident,
2314        target_span,
2315        uri,
2316        &top_level_defs,
2317        &mut Vec::new(),
2318        &mut refs,
2319    );
2320    refs.sort_by_key(|location| {
2321        (
2322            location.range.start.line,
2323            location.range.start.character,
2324            location.range.end.line,
2325            location.range.end.character,
2326        )
2327    });
2328    refs.dedup_by(|a, b| a.range == b.range && a.uri == b.uri);
2329    refs
2330}
2331
2332fn rename_for_source(
2333    uri: &Url,
2334    text: &str,
2335    position: Position,
2336    new_name: &str,
2337) -> Option<WorkspaceEdit> {
2338    if !is_ident_like(new_name) {
2339        return None;
2340    }
2341    let refs = references_for_source(uri, text, position, true);
2342    if refs.is_empty() {
2343        return None;
2344    }
2345    let edits: Vec<TextEdit> = refs
2346        .into_iter()
2347        .map(|location| TextEdit {
2348            range: location.range,
2349            new_text: new_name.to_string(),
2350        })
2351        .collect();
2352    let mut changes = HashMap::new();
2353    changes.insert(uri.clone(), edits);
2354    Some(WorkspaceEdit {
2355        changes: Some(changes),
2356        document_changes: None,
2357        change_annotations: None,
2358    })
2359}
2360
2361fn code_actions_for_source(
2362    uri: &Url,
2363    text: &str,
2364    request_range: Range,
2365    diagnostics: &[Diagnostic],
2366) -> Vec<CodeActionOrCommand> {
2367    let parsed = tokenize_and_parse_cached(uri, text)
2368        .ok()
2369        .map(|(_tokens, program)| program);
2370    let mut actions = Vec::new();
2371
2372    // Hole fill is position-driven and should be available even when other diagnostics exist.
2373    actions.extend(code_actions_for_hole_fill(
2374        uri,
2375        text,
2376        parsed.as_ref(),
2377        request_range,
2378    ));
2379
2380    for diag in diagnostics {
2381        let usable_diag_range = range_is_usable_for_text(text, diag.range);
2382        if usable_diag_range
2383            && !range_is_empty(diag.range)
2384            && !ranges_overlap(diag.range, request_range)
2385            && !range_contains_position(diag.range, request_range.start)
2386            && !range_contains_position(diag.range, request_range.end)
2387        {
2388            continue;
2389        }
2390        actions.extend(code_actions_for_diagnostic(
2391            uri,
2392            text,
2393            parsed.as_ref(),
2394            request_range,
2395            diag,
2396        ));
2397    }
2398
2399    actions
2400}
2401
2402fn code_actions_for_hole_fill(
2403    uri: &Url,
2404    text: &str,
2405    program: Option<&Program>,
2406    request_range: Range,
2407) -> Vec<CodeActionOrCommand> {
2408    let Some(program) = program else {
2409        return Vec::new();
2410    };
2411    let mut hole_spans = Vec::new();
2412    collect_hole_spans(program.expr_with_fns().as_ref(), &mut hole_spans);
2413    let Some(hole_span) = hole_spans
2414        .into_iter()
2415        .find(|span| ranges_overlap(span_to_range(*span), request_range))
2416    else {
2417        return Vec::new();
2418    };
2419    let hole_range = span_to_range(hole_span);
2420    let pos = hole_range.start;
2421    let candidates = hole_fill_candidates_at_position(uri, text, pos);
2422    let mut actions = Vec::new();
2423    for (name, replacement) in candidates.into_iter().take(8) {
2424        let diagnostic = Diagnostic {
2425            range: hole_range,
2426            severity: Some(DiagnosticSeverity::HINT),
2427            message: "hole".to_string(),
2428            source: Some("rexlang-lsp".to_string()),
2429            ..Diagnostic::default()
2430        };
2431        actions.push(code_action_replace(
2432            format!("Fill hole with `{name}`"),
2433            uri,
2434            hole_range,
2435            replacement,
2436            diagnostic,
2437        ));
2438    }
2439    actions
2440}
2441
2442fn code_actions_for_diagnostic(
2443    uri: &Url,
2444    text: &str,
2445    program: Option<&Program>,
2446    request_range: Range,
2447    diagnostic: &Diagnostic,
2448) -> Vec<CodeActionOrCommand> {
2449    let mut actions = Vec::new();
2450    let target_range = if range_is_usable_for_text(text, diagnostic.range) {
2451        diagnostic.range
2452    } else {
2453        request_range
2454    };
2455
2456    if diagnostic
2457        .message
2458        .contains("typed hole `?` must be filled before evaluation")
2459    {
2460        actions.extend(code_actions_for_hole_fill(uri, text, program, target_range));
2461    }
2462
2463    if let Some(name) = unknown_var_name_from_message(&diagnostic.message) {
2464        if let Some(program) = program {
2465            let mut candidates: Vec<String> =
2466                values_in_scope_at_position(program, target_range.start)
2467                    .into_keys()
2468                    .filter(|candidate| candidate != name)
2469                    .collect();
2470            candidates.sort_by_key(|candidate| levenshtein_distance(candidate, name));
2471            for candidate in candidates.into_iter().take(3) {
2472                actions.push(code_action_replace(
2473                    format!("Replace `{name}` with `{candidate}`"),
2474                    uri,
2475                    target_range,
2476                    candidate,
2477                    diagnostic.clone(),
2478                ));
2479            }
2480        }
2481
2482        actions.push(code_action_insert(
2483            format!("Introduce `let {name} = null`"),
2484            uri,
2485            Position {
2486                line: 0,
2487                character: 0,
2488            },
2489            format!("let {name} = null in\n"),
2490            diagnostic.clone(),
2491        ));
2492    }
2493
2494    if is_list_scalar_unification_error(&diagnostic.message)
2495        && let Some(selected) = text_for_range(text, target_range)
2496    {
2497        let trimmed = selected.trim();
2498        if !trimmed.is_empty() {
2499            actions.push(code_action_replace(
2500                "Wrap expression in list literal".to_string(),
2501                uri,
2502                target_range,
2503                format!("[{selected}]"),
2504                diagnostic.clone(),
2505            ));
2506            if trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.len() >= 2 {
2507                let unwrapped = trimmed[1..trimmed.len() - 1].to_string();
2508                actions.push(code_action_replace(
2509                    "Unwrap list literal".to_string(),
2510                    uri,
2511                    target_range,
2512                    unwrapped,
2513                    diagnostic.clone(),
2514                ));
2515            }
2516        }
2517    }
2518
2519    if is_array_list_unification_error(&diagnostic.message) {
2520        let selected_range =
2521            if !range_is_empty(request_range) && range_is_usable_for_text(text, request_range) {
2522                request_range
2523            } else {
2524                target_range
2525            };
2526        if let Some(selected) = text_for_range(text, selected_range) {
2527            let trimmed = selected.trim();
2528            if !trimmed.is_empty() && !trimmed.starts_with("to_list") {
2529                actions.push(code_action_replace(
2530                    "Convert expression to list with `to_list`".to_string(),
2531                    uri,
2532                    selected_range,
2533                    format!("to_list ({selected})"),
2534                    diagnostic.clone(),
2535                ));
2536            }
2537        }
2538    }
2539
2540    if is_function_value_unification_error(&diagnostic.message)
2541        && let Some(selected) = text_for_range(text, target_range)
2542    {
2543        let trimmed = selected.trim();
2544        if !trimmed.is_empty() {
2545            actions.push(code_action_replace(
2546                "Apply expression to missing argument".to_string(),
2547                uri,
2548                target_range,
2549                format!("({selected} null)"),
2550                diagnostic.clone(),
2551            ));
2552            actions.push(code_action_replace(
2553                "Wrap expression in lambda".to_string(),
2554                uri,
2555                target_range,
2556                format!("(\\_ -> {selected})"),
2557                diagnostic.clone(),
2558            ));
2559        }
2560    }
2561
2562    if diagnostic.message.starts_with("non-exhaustive match for ") {
2563        let newline = if diagnostic.range.start.line == diagnostic.range.end.line {
2564            " "
2565        } else {
2566            "\n"
2567        };
2568        actions.push(code_action_insert(
2569            "Add wildcard arm to match".to_string(),
2570            uri,
2571            diagnostic.range.end,
2572            format!("{newline}when _ -> null"),
2573            diagnostic.clone(),
2574        ));
2575    }
2576
2577    if let Some(field) = field_not_definitely_available_from_message(&diagnostic.message)
2578        && let Some(program) = program
2579        && let Some(selected) = text_for_range(text, target_range)
2580    {
2581        let candidates = default_record_candidates_for_field(program, field);
2582        for ty_name in &candidates {
2583            if let Some(new_text) = replace_first_default_with_is(&selected, ty_name) {
2584                actions.push(code_action_replace(
2585                    format!("Disambiguate `default` as `{ty_name}`"),
2586                    uri,
2587                    target_range,
2588                    new_text,
2589                    diagnostic.clone(),
2590                ));
2591            }
2592        }
2593
2594        if let Some((binding_name, insert_pos)) =
2595            find_let_binding_for_def_range(program, target_range)
2596        {
2597            for ty_name in &candidates {
2598                actions.push(code_action_insert(
2599                    format!("Annotate `{binding_name}` as `{ty_name}`"),
2600                    uri,
2601                    insert_pos,
2602                    format!(": {ty_name}"),
2603                    diagnostic.clone(),
2604                ));
2605            }
2606        }
2607    }
2608
2609    actions
2610}
2611
2612fn code_action_replace(
2613    title: String,
2614    uri: &Url,
2615    range: Range,
2616    new_text: String,
2617    diagnostic: Diagnostic,
2618) -> CodeActionOrCommand {
2619    code_action_with_edit(title, uri, TextEdit { range, new_text }, diagnostic)
2620}
2621
2622fn code_action_insert(
2623    title: String,
2624    uri: &Url,
2625    position: Position,
2626    new_text: String,
2627    diagnostic: Diagnostic,
2628) -> CodeActionOrCommand {
2629    code_action_with_edit(
2630        title,
2631        uri,
2632        TextEdit {
2633            range: Range {
2634                start: position,
2635                end: position,
2636            },
2637            new_text,
2638        },
2639        diagnostic,
2640    )
2641}
2642
2643fn code_action_with_edit(
2644    title: String,
2645    uri: &Url,
2646    edit: TextEdit,
2647    diagnostic: Diagnostic,
2648) -> CodeActionOrCommand {
2649    let mut changes = HashMap::new();
2650    changes.insert(uri.clone(), vec![edit]);
2651    CodeActionOrCommand::CodeAction(CodeAction {
2652        title,
2653        kind: Some(CodeActionKind::QUICKFIX),
2654        diagnostics: Some(vec![diagnostic]),
2655        edit: Some(WorkspaceEdit {
2656            changes: Some(changes),
2657            document_changes: None,
2658            change_annotations: None,
2659        }),
2660        command: None,
2661        is_preferred: Some(true),
2662        disabled: None,
2663        data: None,
2664    })
2665}
2666
2667fn text_for_range(text: &str, range: Range) -> Option<String> {
2668    let start = offset_at(text, range.start)?;
2669    let end = offset_at(text, range.end)?;
2670    (start <= end && end <= text.len()).then(|| text[start..end].to_string())
2671}
2672
2673fn range_is_usable_for_text(text: &str, range: Range) -> bool {
2674    let Some(start) = offset_at(text, range.start) else {
2675        return false;
2676    };
2677    let Some(end) = offset_at(text, range.end) else {
2678        return false;
2679    };
2680    start <= end && end <= text.len()
2681}
2682
2683fn ranges_overlap(a: Range, b: Range) -> bool {
2684    position_leq_lsp(a.start, b.end) && position_leq_lsp(b.start, a.end)
2685}
2686
2687fn position_leq_lsp(left: Position, right: Position) -> bool {
2688    left.line < right.line || (left.line == right.line && left.character <= right.character)
2689}
2690
2691fn range_is_empty(range: Range) -> bool {
2692    range.start.line == range.end.line && range.start.character == range.end.character
2693}
2694
2695fn unknown_var_name_from_message(message: &str) -> Option<&str> {
2696    message.strip_prefix("unbound variable ").map(str::trim)
2697}
2698
2699fn field_not_definitely_available_from_message(message: &str) -> Option<&str> {
2700    let rest = message.strip_prefix("field `")?;
2701    let (field, tail) = rest.split_once('`')?;
2702    tail.contains("is not definitely available on")
2703        .then_some(field)
2704}
2705
2706fn default_record_candidates_for_field(program: &Program, field: &str) -> Vec<String> {
2707    let mut out = Vec::new();
2708    let mut seen = HashSet::new();
2709    for decl in &program.decls {
2710        let Decl::Instance(inst) = decl else {
2711            continue;
2712        };
2713        if inst.class.as_ref() != "Default" {
2714            continue;
2715        }
2716        let TypeExpr::Name(_, ty_name) = &inst.head else {
2717            continue;
2718        };
2719        if !type_decl_has_record_field(program, ty_name.as_ref(), field) {
2720            continue;
2721        }
2722        let ty_name = ty_name.as_ref().to_string();
2723        if seen.insert(ty_name.clone()) {
2724            out.push(ty_name);
2725        }
2726    }
2727    out
2728}
2729
2730fn type_decl_has_record_field(program: &Program, type_name: &str, field: &str) -> bool {
2731    program.decls.iter().any(|decl| {
2732        let Decl::Type(td) = decl else {
2733            return false;
2734        };
2735        if td.name.as_ref() != type_name {
2736            return false;
2737        }
2738        td.variants.iter().any(|variant| {
2739            variant.args.iter().any(|arg| {
2740                let TypeExpr::Record(_, fields) = arg else {
2741                    return false;
2742                };
2743                fields.iter().any(|(name, _)| name.as_ref() == field)
2744            })
2745        })
2746    })
2747}
2748
2749fn replace_first_default_with_is(source: &str, ty_name: &str) -> Option<String> {
2750    for (idx, _) in source.match_indices("default") {
2751        let left_ok = if idx == 0 {
2752            true
2753        } else {
2754            !is_ident_char(source[..idx].chars().next_back().unwrap_or('_'))
2755        };
2756        let right_idx = idx + "default".len();
2757        let right_ok = if right_idx >= source.len() {
2758            true
2759        } else {
2760            !is_ident_char(source[right_idx..].chars().next().unwrap_or('_'))
2761        };
2762        if !(left_ok && right_ok) {
2763            continue;
2764        }
2765
2766        let mut replaced = String::with_capacity(source.len() + ty_name.len() + 8);
2767        replaced.push_str(&source[..idx]);
2768        replaced.push_str("(default is ");
2769        replaced.push_str(ty_name);
2770        replaced.push(')');
2771        replaced.push_str(&source[right_idx..]);
2772        return Some(replaced);
2773    }
2774    None
2775}
2776
2777fn is_ident_char(c: char) -> bool {
2778    c.is_ascii_alphanumeric() || c == '_'
2779}
2780
2781fn is_hole_name(name: &str) -> bool {
2782    name == "_" || name.starts_with('_')
2783}
2784
2785fn is_list_scalar_unification_error(message: &str) -> bool {
2786    let Some(rest) = message.strip_prefix("types do not unify: ") else {
2787        return false;
2788    };
2789    let Some((left, right)) = rest.split_once(" vs ") else {
2790        return false;
2791    };
2792    list_inner_type(left.trim()).is_some_and(|inner| inner == right.trim())
2793        || list_inner_type(right.trim()).is_some_and(|inner| inner == left.trim())
2794}
2795
2796fn list_inner_type(typ: &str) -> Option<&str> {
2797    if let Some(inner) = typ
2798        .strip_prefix("List<")
2799        .and_then(|rest| rest.strip_suffix('>'))
2800    {
2801        return Some(inner);
2802    }
2803    typ.strip_prefix("(List ")
2804        .and_then(|rest| rest.strip_suffix(')'))
2805}
2806
2807fn is_array_list_unification_error(message: &str) -> bool {
2808    let Some(rest) = message.strip_prefix("types do not unify: ") else {
2809        return false;
2810    };
2811    let Some((left, right)) = rest.split_once(" vs ") else {
2812        return false;
2813    };
2814    let left = left.trim();
2815    let right = right.trim();
2816    let left_has_array = left.contains("Array");
2817    let left_has_list = left.contains("List");
2818    let right_has_array = right.contains("Array");
2819    let right_has_list = right.contains("List");
2820    (left_has_array && right_has_list) || (left_has_list && right_has_array)
2821}
2822
2823fn is_function_value_unification_error(message: &str) -> bool {
2824    let Some(rest) = message.strip_prefix("types do not unify: ") else {
2825        return false;
2826    };
2827    let Some((left, right)) = rest.split_once(" vs ") else {
2828        return false;
2829    };
2830    let left_is_fun = looks_like_fun_type(left.trim());
2831    let right_is_fun = looks_like_fun_type(right.trim());
2832    left_is_fun ^ right_is_fun
2833}
2834
2835fn looks_like_fun_type(typ: &str) -> bool {
2836    let mut depth = 0usize;
2837    let bytes = typ.as_bytes();
2838    let mut i = 0usize;
2839    while i + 1 < bytes.len() {
2840        match bytes[i] as char {
2841            '(' | '{' | '[' => depth += 1,
2842            ')' | '}' | ']' => depth = depth.saturating_sub(1),
2843            '-' if bytes[i + 1] as char == '>' && depth == 0 => return true,
2844            _ => {}
2845        }
2846        i += 1;
2847    }
2848
2849    if typ.starts_with('(') && typ.ends_with(')') {
2850        return looks_like_fun_type(&typ[1..typ.len() - 1]);
2851    }
2852    false
2853}
2854
2855fn split_fun_type(typ: &Type) -> (Vec<Type>, Type) {
2856    let mut args = Vec::new();
2857    let mut cur = typ.clone();
2858    while let TypeKind::Fun(arg, ret) = cur.as_ref() {
2859        args.push(arg.clone());
2860        cur = ret.clone();
2861    }
2862    (args, cur)
2863}
2864
2865fn in_scope_value_types_at_position(
2866    uri: &Url,
2867    text: &str,
2868    position: Position,
2869) -> Vec<(String, Type)> {
2870    let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) else {
2871        return Vec::new();
2872    };
2873    let Ok((program, mut ts, _imports, _import_diags)) =
2874        prepare_program_with_imports(uri, &program)
2875    else {
2876        return Vec::new();
2877    };
2878    if inject_program_decls(&mut ts, &program, None).is_err() {
2879        return Vec::new();
2880    }
2881
2882    let expr = program.expr_with_fns();
2883    let Ok((typed, _preds, _ty)) = ts.infer_typed(expr.as_ref()) else {
2884        return Vec::new();
2885    };
2886    let pos = lsp_to_rex_position(position);
2887
2888    fn visit(
2889        expr: &Expr,
2890        typed: &TypedExpr,
2891        pos: RexPosition,
2892        scope: &mut Vec<(String, Type)>,
2893        best: &mut Option<Vec<(String, Type)>>,
2894    ) {
2895        if !position_in_span(pos, *expr.span()) {
2896            return;
2897        }
2898        *best = Some(scope.clone());
2899
2900        match (expr, &typed.kind) {
2901            (
2902                Expr::Let(_span, var, _ann, def, body),
2903                TypedExprKind::Let {
2904                    def: tdef,
2905                    body: tbody,
2906                    ..
2907                },
2908            ) => {
2909                if position_in_span(pos, *def.span()) {
2910                    visit(def.as_ref(), tdef.as_ref(), pos, scope, best);
2911                    return;
2912                }
2913                if position_in_span(pos, *body.span()) {
2914                    scope.push((var.name.to_string(), tdef.typ.clone()));
2915                    visit(body.as_ref(), tbody.as_ref(), pos, scope, best);
2916                    scope.pop();
2917                }
2918            }
2919            (
2920                Expr::LetRec(_span, bindings, body),
2921                TypedExprKind::LetRec {
2922                    bindings: typed_bindings,
2923                    body: typed_body,
2924                },
2925            ) => {
2926                let base = scope.len();
2927                for ((name, _ann, _def), (_typed_name, typed_def)) in
2928                    bindings.iter().zip(typed_bindings.iter())
2929                {
2930                    scope.push((name.name.to_string(), typed_def.typ.clone()));
2931                }
2932                for ((_, _, def), (_, typed_def)) in bindings.iter().zip(typed_bindings.iter()) {
2933                    if position_in_span(pos, *def.span()) {
2934                        visit(def.as_ref(), typed_def, pos, scope, best);
2935                        scope.truncate(base);
2936                        return;
2937                    }
2938                }
2939                if position_in_span(pos, *body.span()) {
2940                    visit(body.as_ref(), typed_body.as_ref(), pos, scope, best);
2941                }
2942                scope.truncate(base);
2943            }
2944            (
2945                Expr::Lam(_span, _scope, param, _ann, _constraints, body),
2946                TypedExprKind::Lam {
2947                    body: typed_body, ..
2948                },
2949            ) => {
2950                if let TypeKind::Fun(arg, _ret) = typed.typ.as_ref() {
2951                    scope.push((param.name.to_string(), arg.clone()));
2952                    visit(body.as_ref(), typed_body.as_ref(), pos, scope, best);
2953                    scope.pop();
2954                }
2955            }
2956            (Expr::App(_span, fun, arg), TypedExprKind::App(tfun, targ)) => {
2957                if position_in_span(pos, *fun.span()) {
2958                    visit(fun.as_ref(), tfun.as_ref(), pos, scope, best);
2959                } else if position_in_span(pos, *arg.span()) {
2960                    visit(arg.as_ref(), targ.as_ref(), pos, scope, best);
2961                }
2962            }
2963            (Expr::Project(_span, base, _field), TypedExprKind::Project { expr: tbase, .. }) => {
2964                visit(base.as_ref(), tbase.as_ref(), pos, scope, best);
2965            }
2966            (
2967                Expr::Ite(_span, cond, then_expr, else_expr),
2968                TypedExprKind::Ite {
2969                    cond: tcond,
2970                    then_expr: tthen,
2971                    else_expr: telse,
2972                },
2973            ) => {
2974                if position_in_span(pos, *cond.span()) {
2975                    visit(cond.as_ref(), tcond.as_ref(), pos, scope, best);
2976                } else if position_in_span(pos, *then_expr.span()) {
2977                    visit(then_expr.as_ref(), tthen.as_ref(), pos, scope, best);
2978                } else if position_in_span(pos, *else_expr.span()) {
2979                    visit(else_expr.as_ref(), telse.as_ref(), pos, scope, best);
2980                }
2981            }
2982            (Expr::Tuple(_span, elems), TypedExprKind::Tuple(typed_elems))
2983            | (Expr::List(_span, elems), TypedExprKind::List(typed_elems)) => {
2984                for (elem, typed_elem) in elems.iter().zip(typed_elems.iter()) {
2985                    if position_in_span(pos, *elem.span()) {
2986                        visit(elem.as_ref(), typed_elem, pos, scope, best);
2987                        break;
2988                    }
2989                }
2990            }
2991            (Expr::Dict(_span, kvs), TypedExprKind::Dict(typed_kvs)) => {
2992                for (key, value) in kvs {
2993                    if position_in_span(pos, *value.span())
2994                        && let Some(typed_v) = typed_kvs.get(key)
2995                    {
2996                        visit(value.as_ref(), typed_v, pos, scope, best);
2997                        break;
2998                    }
2999                }
3000            }
3001            (
3002                Expr::RecordUpdate(_span, base, updates),
3003                TypedExprKind::RecordUpdate {
3004                    base: tbase,
3005                    updates: typed_updates,
3006                },
3007            ) => {
3008                if position_in_span(pos, *base.span()) {
3009                    visit(base.as_ref(), tbase.as_ref(), pos, scope, best);
3010                } else {
3011                    for (key, value) in updates {
3012                        if position_in_span(pos, *value.span())
3013                            && let Some(typed_v) = typed_updates.get(key)
3014                        {
3015                            visit(value.as_ref(), typed_v, pos, scope, best);
3016                            break;
3017                        }
3018                    }
3019                }
3020            }
3021            (
3022                Expr::Match(_span, scrutinee, arms),
3023                TypedExprKind::Match {
3024                    scrutinee: tscrutinee,
3025                    arms: typed_arms,
3026                },
3027            ) => {
3028                if position_in_span(pos, *scrutinee.span()) {
3029                    visit(scrutinee.as_ref(), tscrutinee.as_ref(), pos, scope, best);
3030                } else {
3031                    for ((_pat, arm), (_typed_pat, typed_arm)) in arms.iter().zip(typed_arms.iter())
3032                    {
3033                        if position_in_span(pos, *arm.span()) {
3034                            visit(arm.as_ref(), typed_arm, pos, scope, best);
3035                            break;
3036                        }
3037                    }
3038                }
3039            }
3040            (Expr::Ann(_span, inner, _), _) => visit(inner.as_ref(), typed, pos, scope, best),
3041            _ => {}
3042        }
3043    }
3044
3045    let mut best = None;
3046    visit(expr.as_ref(), &typed, pos, &mut Vec::new(), &mut best);
3047    best.unwrap_or_default()
3048}
3049
3050fn hole_fill_candidates_at_position(
3051    uri: &Url,
3052    text: &str,
3053    position: Position,
3054) -> Vec<(String, String)> {
3055    let Some(target_type) = expected_type_at_position_type(uri, text, position) else {
3056        return Vec::new();
3057    };
3058    let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) else {
3059        return Vec::new();
3060    };
3061    let Ok((program, mut ts, _imports, _import_diags)) =
3062        prepare_program_with_imports(uri, &program)
3063    else {
3064        return Vec::new();
3065    };
3066    if inject_program_decls(&mut ts, &program, None).is_err() {
3067        return Vec::new();
3068    }
3069    let mut in_scope = in_scope_value_types_at_position(uri, text, position)
3070        .into_iter()
3071        .filter(|(name, _)| is_ident_like(name))
3072        .collect::<Vec<_>>();
3073    if in_scope.len() > MAX_SEMANTIC_IN_SCOPE_VALUES {
3074        in_scope = in_scope.split_off(in_scope.len().saturating_sub(MAX_SEMANTIC_IN_SCOPE_VALUES));
3075    }
3076
3077    let values = semantic_candidate_values(&ts);
3078
3079    let mut adapters: Vec<(String, Type, Type)> = Vec::new();
3080    for (name, schemes) in &values {
3081        let name = name.to_string();
3082        if !is_ident_like(&name) {
3083            continue;
3084        }
3085        for scheme in schemes {
3086            let (_preds, inst_ty) = instantiate(scheme, &mut ts.supply);
3087            let (args, ret) = split_fun_type(&inst_ty);
3088            if args.len() == 1 {
3089                adapters.push((name.clone(), args[0].clone(), ret));
3090            }
3091        }
3092    }
3093
3094    let mut out: Vec<(usize, usize, String, String)> = Vec::new();
3095    for (name, schemes) in values {
3096        let name = name.to_string();
3097        if !is_ident_like(&name) {
3098            continue;
3099        }
3100        for scheme in schemes {
3101            let (_preds, inst_ty) = instantiate(&scheme, &mut ts.supply);
3102            let (args, ret) = split_fun_type(&inst_ty);
3103            if args.is_empty()
3104                || args.len() > MAX_SEMANTIC_HOLE_FILL_ARITY
3105                || unify(&ret, &target_type).is_err()
3106            {
3107                continue;
3108            }
3109
3110            let mut unresolved = 0usize;
3111            let mut adapter_uses = 0usize;
3112            let mut rendered_args = Vec::new();
3113            for arg_ty in args {
3114                if let Some((value_name, _value_ty)) = in_scope
3115                    .iter()
3116                    .rev()
3117                    .find(|(_, value_ty)| unify(value_ty, &arg_ty).is_ok())
3118                {
3119                    rendered_args.push(value_name.clone());
3120                    continue;
3121                }
3122
3123                let mut adapted = None;
3124                for (adapter_name, adapter_arg, adapter_ret) in &adapters {
3125                    if unify(adapter_ret, &arg_ty).is_err() {
3126                        continue;
3127                    }
3128                    if let Some((value_name, _value_ty)) = in_scope
3129                        .iter()
3130                        .rev()
3131                        .find(|(_, value_ty)| unify(value_ty, adapter_arg).is_ok())
3132                    {
3133                        adapted = Some(format!("({adapter_name} {value_name})"));
3134                        break;
3135                    }
3136                }
3137                if let Some(expr) = adapted {
3138                    adapter_uses += 1;
3139                    rendered_args.push(expr);
3140                } else {
3141                    unresolved += 1;
3142                    rendered_args.push("?".to_string());
3143                }
3144            }
3145
3146            let replacement = format!("{name} {}", rendered_args.join(" "));
3147            out.push((unresolved, adapter_uses, name.clone(), replacement));
3148        }
3149    }
3150
3151    out.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
3152    out.dedup_by(|a, b| a.2 == b.2 && a.3 == b.3);
3153    if out.len() > MAX_SEMANTIC_CANDIDATES {
3154        out.truncate(MAX_SEMANTIC_CANDIDATES);
3155    }
3156    out.into_iter()
3157        .map(|(_u, _a, name, replacement)| (name, replacement))
3158        .collect()
3159}
3160
3161fn levenshtein_distance(left: &str, right: &str) -> usize {
3162    if left == right {
3163        return 0;
3164    }
3165    if left.is_empty() {
3166        return right.chars().count();
3167    }
3168    if right.is_empty() {
3169        return left.chars().count();
3170    }
3171
3172    let right_len = right.chars().count();
3173    let mut prev: Vec<usize> = (0..=right_len).collect();
3174    let mut cur = vec![0usize; right_len + 1];
3175
3176    for (i, lc) in left.chars().enumerate() {
3177        cur[0] = i + 1;
3178        for (j, rc) in right.chars().enumerate() {
3179            let insert_cost = cur[j] + 1;
3180            let delete_cost = prev[j + 1] + 1;
3181            let replace_cost = prev[j] + usize::from(lc != rc);
3182            cur[j + 1] = insert_cost.min(delete_cost).min(replace_cost);
3183        }
3184        std::mem::swap(&mut prev, &mut cur);
3185    }
3186
3187    prev[right_len]
3188}
3189
3190#[allow(deprecated)]
3191fn symbol_for_decl(decl: &Decl) -> Option<DocumentSymbol> {
3192    match decl {
3193        Decl::Type(td) => Some(DocumentSymbol {
3194            name: td.name.to_string(),
3195            detail: Some("type".to_string()),
3196            kind: SymbolKind::ENUM,
3197            tags: None,
3198            deprecated: None,
3199            range: span_to_range(td.span),
3200            selection_range: span_to_range(td.span),
3201            children: Some(
3202                td.variants
3203                    .iter()
3204                    .map(|variant| DocumentSymbol {
3205                        name: variant.name.to_string(),
3206                        detail: Some("variant".to_string()),
3207                        kind: SymbolKind::ENUM_MEMBER,
3208                        tags: None,
3209                        deprecated: None,
3210                        range: span_to_range(td.span),
3211                        selection_range: span_to_range(td.span),
3212                        children: None,
3213                    })
3214                    .collect(),
3215            ),
3216        }),
3217        Decl::Fn(fd) => Some(DocumentSymbol {
3218            name: fd.name.name.to_string(),
3219            detail: Some("fn".to_string()),
3220            kind: SymbolKind::FUNCTION,
3221            tags: None,
3222            deprecated: None,
3223            range: span_to_range(fd.span),
3224            selection_range: span_to_range(fd.name.span),
3225            children: None,
3226        }),
3227        Decl::DeclareFn(df) => Some(DocumentSymbol {
3228            name: df.name.name.to_string(),
3229            detail: Some("declare fn".to_string()),
3230            kind: SymbolKind::FUNCTION,
3231            tags: None,
3232            deprecated: None,
3233            range: span_to_range(df.span),
3234            selection_range: span_to_range(df.name.span),
3235            children: None,
3236        }),
3237        Decl::Import(id) => Some(DocumentSymbol {
3238            name: id.alias.to_string(),
3239            detail: Some("import".to_string()),
3240            kind: SymbolKind::MODULE,
3241            tags: None,
3242            deprecated: None,
3243            range: span_to_range(id.span),
3244            selection_range: span_to_range(id.span),
3245            children: None,
3246        }),
3247        Decl::Class(cd) => Some(DocumentSymbol {
3248            name: cd.name.to_string(),
3249            detail: Some("class".to_string()),
3250            kind: SymbolKind::INTERFACE,
3251            tags: None,
3252            deprecated: None,
3253            range: span_to_range(cd.span),
3254            selection_range: span_to_range(cd.span),
3255            children: Some(
3256                cd.methods
3257                    .iter()
3258                    .map(|method| DocumentSymbol {
3259                        name: method.name.to_string(),
3260                        detail: Some("method".to_string()),
3261                        kind: SymbolKind::METHOD,
3262                        tags: None,
3263                        deprecated: None,
3264                        range: span_to_range(cd.span),
3265                        selection_range: span_to_range(cd.span),
3266                        children: None,
3267                    })
3268                    .collect(),
3269            ),
3270        }),
3271        Decl::Instance(id) => Some(DocumentSymbol {
3272            name: format!("instance {}", id.class),
3273            detail: Some("instance".to_string()),
3274            kind: SymbolKind::OBJECT,
3275            tags: None,
3276            deprecated: None,
3277            range: span_to_range(id.span),
3278            selection_range: span_to_range(id.span),
3279            children: Some(
3280                id.methods
3281                    .iter()
3282                    .map(|method| DocumentSymbol {
3283                        name: method.name.to_string(),
3284                        detail: Some("method".to_string()),
3285                        kind: SymbolKind::METHOD,
3286                        tags: None,
3287                        deprecated: None,
3288                        range: span_to_range(*method.body.span()),
3289                        selection_range: span_to_range(*method.body.span()),
3290                        children: None,
3291                    })
3292                    .collect(),
3293            ),
3294        }),
3295    }
3296}
3297
3298fn document_symbols_for_source(uri: &Url, text: &str) -> Vec<DocumentSymbol> {
3299    let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) else {
3300        return Vec::new();
3301    };
3302    program.decls.iter().filter_map(symbol_for_decl).collect()
3303}
3304
3305fn full_document_range(text: &str) -> Range {
3306    let mut line = 0u32;
3307    let mut col = 0u32;
3308    for ch in text.chars() {
3309        if ch == '\n' {
3310            line += 1;
3311            col = 0;
3312        } else {
3313            col += 1;
3314        }
3315    }
3316    Range {
3317        start: Position {
3318            line: 0,
3319            character: 0,
3320        },
3321        end: Position {
3322            line,
3323            character: col,
3324        },
3325    }
3326}
3327
3328fn format_source(text: &str) -> String {
3329    let mut out = String::new();
3330    let mut first = true;
3331    for line in text.lines() {
3332        if !first {
3333            out.push('\n');
3334        }
3335        first = false;
3336        out.push_str(line.trim_end());
3337    }
3338    if text.ends_with('\n') || !out.is_empty() {
3339        out.push('\n');
3340    }
3341    out
3342}
3343
3344fn format_edits_for_source(text: &str) -> Option<Vec<TextEdit>> {
3345    let formatted = format_source(text);
3346    if formatted == text {
3347        return None;
3348    }
3349    Some(vec![TextEdit {
3350        range: full_document_range(text),
3351        new_text: formatted,
3352    }])
3353}
3354
3355fn diagnostics_from_text(uri: &Url, text: &str) -> Vec<Diagnostic> {
3356    let mut diagnostics = Vec::new();
3357
3358    match tokenize_and_parse_cached(uri, text) {
3359        Ok((tokens, program)) => {
3360            push_comment_diagnostics(&tokens, &mut diagnostics);
3361            if diagnostics.len() < MAX_DIAGNOSTICS {
3362                push_type_diagnostics(uri, text, &program, &mut diagnostics);
3363            }
3364        }
3365        Err(TokenizeOrParseError::Lex(err)) => {
3366            let (span, message) = match err {
3367                LexicalError::UnexpectedToken(span) => (span, "Unexpected token".to_string()),
3368                LexicalError::InvalidLiteral {
3369                    kind,
3370                    text,
3371                    error,
3372                    span,
3373                } => (span, format!("invalid {kind} literal `{text}`: {error}")),
3374                LexicalError::Internal(msg) => (
3375                    Span::new(1, 1, 1, 1),
3376                    format!("internal lexer error: {msg}"),
3377                ),
3378            };
3379            diagnostics.push(diagnostic_for_span(span, message));
3380        }
3381        Err(TokenizeOrParseError::Parse(errors)) => {
3382            for err in errors {
3383                diagnostics.push(diagnostic_for_span(err.span, err.message));
3384                if diagnostics.len() >= MAX_DIAGNOSTICS {
3385                    break;
3386                }
3387            }
3388        }
3389    }
3390
3391    diagnostics
3392}
3393
3394struct HoverType {
3395    span: Span,
3396    label: String,
3397    typ: String,
3398    overloads: Vec<String>,
3399}
3400
3401fn hover_type_contents(uri: &Url, text: &str, position: Position) -> Option<HoverContents> {
3402    let (tokens, program) = tokenize_and_parse_cached(uri, text).ok()?;
3403    let (name, name_span, name_is_ident) = name_token_at_position(&tokens, position)?;
3404    let (program, mut ts, _imports, _import_diags) =
3405        prepare_program_with_imports(uri, &program).ok()?;
3406
3407    let pos = lsp_to_rex_position(position);
3408
3409    // If the cursor is inside an instance method body, typecheck that method
3410    // body using the instance context rules (so hover works inside methods).
3411    let mut target_instance: Option<(usize, usize)> = None;
3412    for (decl_idx, decl) in program.decls.iter().enumerate() {
3413        let Decl::Instance(inst) = decl else {
3414            continue;
3415        };
3416        for (method_idx, method) in inst.methods.iter().enumerate() {
3417            if position_in_span(pos, *method.body.span()) {
3418                target_instance = Some((decl_idx, method_idx));
3419                break;
3420            }
3421        }
3422        if target_instance.is_some() {
3423            break;
3424        }
3425    }
3426
3427    let (_instances, prepared_target_instance) = inject_program_decls(
3428        &mut ts,
3429        &program,
3430        target_instance.map(|(decl_idx, _)| decl_idx),
3431    )
3432    .ok()?;
3433
3434    let expr_with_fns = program.expr_with_fns();
3435
3436    let root_expr: &Expr;
3437    let typed_root: TypedExpr;
3438
3439    if let Some((decl_idx, method_idx)) = target_instance {
3440        let Decl::Instance(inst) = &program.decls[decl_idx] else {
3441            return None;
3442        };
3443        let prepared = prepared_target_instance?;
3444        let method = inst.methods.get(method_idx)?;
3445        typed_root = ts.typecheck_instance_method(&prepared, method).ok()?;
3446        root_expr = method.body.as_ref();
3447    } else {
3448        let (typed, _preds, _) = ts.infer_typed(expr_with_fns.as_ref()).ok()?;
3449        typed_root = typed;
3450        root_expr = expr_with_fns.as_ref();
3451    }
3452
3453    let hover = hover_type_in_expr(
3454        &mut ts,
3455        root_expr,
3456        &typed_root,
3457        pos,
3458        &name,
3459        name_span,
3460        name_is_ident,
3461    )?;
3462
3463    let mut md = String::new();
3464    md.push_str("```rex\n");
3465    md.push_str(&hover.label);
3466    md.push_str(" : ");
3467    md.push_str(&hover.typ);
3468    md.push_str("\n```");
3469
3470    if !hover.overloads.is_empty() {
3471        md.push_str("\n\nOverloads:\n");
3472        for ov in &hover.overloads {
3473            md.push_str("- `");
3474            md.push_str(ov);
3475            md.push_str("`\n");
3476        }
3477    }
3478
3479    Some(HoverContents::Markup(MarkupContent {
3480        kind: MarkupKind::Markdown,
3481        value: md,
3482    }))
3483}
3484
3485fn expected_type_at_position(uri: &Url, text: &str, position: Position) -> Option<String> {
3486    expected_type_at_position_type(uri, text, position).map(|ty| ty.to_string())
3487}
3488
3489fn inferred_type_at_position(uri: &Url, text: &str, position: Position) -> Option<String> {
3490    inferred_type_at_position_type(uri, text, position).map(|ty| ty.to_string())
3491}
3492
3493fn expected_type_at_position_type(uri: &Url, text: &str, position: Position) -> Option<Type> {
3494    let (_tokens, program) = tokenize_and_parse_cached(uri, text).ok()?;
3495    let (program, mut ts, _imports, _import_diags) =
3496        prepare_program_with_imports(uri, &program).ok()?;
3497
3498    let pos = lsp_to_rex_position(position);
3499
3500    // Mirror hover behavior inside instance methods.
3501    let mut target_instance: Option<(usize, usize)> = None;
3502    for (decl_idx, decl) in program.decls.iter().enumerate() {
3503        let Decl::Instance(inst) = decl else {
3504            continue;
3505        };
3506        for (method_idx, method) in inst.methods.iter().enumerate() {
3507            if position_in_span(pos, *method.body.span()) {
3508                target_instance = Some((decl_idx, method_idx));
3509                break;
3510            }
3511        }
3512        if target_instance.is_some() {
3513            break;
3514        }
3515    }
3516
3517    let (_instances, prepared_target_instance) = inject_program_decls(
3518        &mut ts,
3519        &program,
3520        target_instance.map(|(decl_idx, _)| decl_idx),
3521    )
3522    .ok()?;
3523
3524    let expr_with_fns = program.expr_with_fns();
3525    let root_expr: &Expr;
3526    let typed_root: TypedExpr;
3527
3528    if let Some((decl_idx, method_idx)) = target_instance {
3529        let Decl::Instance(inst) = &program.decls[decl_idx] else {
3530            return None;
3531        };
3532        let prepared = prepared_target_instance?;
3533        let method = inst.methods.get(method_idx)?;
3534        typed_root = ts.typecheck_instance_method(&prepared, method).ok()?;
3535        root_expr = method.body.as_ref();
3536    } else {
3537        let (typed, _preds, _) = ts.infer_typed(expr_with_fns.as_ref()).ok()?;
3538        typed_root = typed;
3539        root_expr = expr_with_fns.as_ref();
3540    }
3541
3542    expected_type_in_expr(root_expr, &typed_root, pos)
3543}
3544
3545fn inferred_type_at_position_type(uri: &Url, text: &str, position: Position) -> Option<Type> {
3546    let (_tokens, program) = tokenize_and_parse_cached(uri, text).ok()?;
3547    let (program, mut ts, _imports, _import_diags) =
3548        prepare_program_with_imports(uri, &program).ok()?;
3549
3550    let pos = lsp_to_rex_position(position);
3551
3552    let mut target_instance: Option<(usize, usize)> = None;
3553    for (decl_idx, decl) in program.decls.iter().enumerate() {
3554        let Decl::Instance(inst) = decl else {
3555            continue;
3556        };
3557        for (method_idx, method) in inst.methods.iter().enumerate() {
3558            if position_in_span(pos, *method.body.span()) {
3559                target_instance = Some((decl_idx, method_idx));
3560                break;
3561            }
3562        }
3563        if target_instance.is_some() {
3564            break;
3565        }
3566    }
3567
3568    let (_instances, prepared_target_instance) = inject_program_decls(
3569        &mut ts,
3570        &program,
3571        target_instance.map(|(decl_idx, _)| decl_idx),
3572    )
3573    .ok()?;
3574
3575    let expr_with_fns = program.expr_with_fns();
3576    let root_expr: &Expr;
3577    let typed_root: TypedExpr;
3578
3579    if let Some((decl_idx, method_idx)) = target_instance {
3580        let Decl::Instance(inst) = &program.decls[decl_idx] else {
3581            return None;
3582        };
3583        let prepared = prepared_target_instance?;
3584        let method = inst.methods.get(method_idx)?;
3585        typed_root = ts.typecheck_instance_method(&prepared, method).ok()?;
3586        root_expr = method.body.as_ref();
3587    } else {
3588        let (typed, _preds, _) = ts.infer_typed(expr_with_fns.as_ref()).ok()?;
3589        typed_root = typed;
3590        root_expr = expr_with_fns.as_ref();
3591    }
3592
3593    inferred_type_in_expr(root_expr, &typed_root, pos)
3594}
3595
3596fn expected_type_in_expr(expr: &Expr, typed: &TypedExpr, pos: RexPosition) -> Option<Type> {
3597    #[derive(Clone)]
3598    struct Candidate {
3599        span: Span,
3600        typ: Type,
3601    }
3602
3603    fn span_size(span: Span) -> (usize, usize) {
3604        (
3605            span.end.line.saturating_sub(span.begin.line),
3606            span.end.column.saturating_sub(span.begin.column),
3607        )
3608    }
3609
3610    fn consider(best: &mut Option<Candidate>, span: Span, typ: &Type) {
3611        let replace = best
3612            .as_ref()
3613            .is_none_or(|cur| span_size(span) < span_size(cur.span));
3614        if replace {
3615            *best = Some(Candidate {
3616                span,
3617                typ: typ.clone(),
3618            });
3619        }
3620    }
3621
3622    fn visit(
3623        expr: &Expr,
3624        typed: &TypedExpr,
3625        pos: RexPosition,
3626        expected: Option<&Type>,
3627        best: &mut Option<Candidate>,
3628    ) {
3629        if !position_in_span(pos, *expr.span()) {
3630            return;
3631        }
3632
3633        if let Some(expected) = expected {
3634            consider(best, *expr.span(), expected);
3635        }
3636
3637        match (expr, &typed.kind) {
3638            (
3639                Expr::Let(_span, _name, _ann, def, body),
3640                TypedExprKind::Let {
3641                    def: tdef,
3642                    body: tbody,
3643                    ..
3644                },
3645            ) => {
3646                visit(def.as_ref(), tdef.as_ref(), pos, Some(&tdef.typ), best);
3647                visit(body.as_ref(), tbody.as_ref(), pos, Some(&typed.typ), best);
3648            }
3649            (
3650                Expr::LetRec(_span, bindings, body),
3651                TypedExprKind::LetRec {
3652                    bindings: typed_bindings,
3653                    body: typed_body,
3654                },
3655            ) => {
3656                for ((_name, _ann, def), (_typed_name, typed_def)) in
3657                    bindings.iter().zip(typed_bindings.iter())
3658                {
3659                    visit(def.as_ref(), typed_def, pos, Some(&typed_def.typ), best);
3660                }
3661                visit(
3662                    body.as_ref(),
3663                    typed_body.as_ref(),
3664                    pos,
3665                    Some(&typed.typ),
3666                    best,
3667                );
3668            }
3669            (
3670                Expr::Lam(_span, _scope, _param, _ann, _constraints, body),
3671                TypedExprKind::Lam {
3672                    body: typed_body, ..
3673                },
3674            ) => {
3675                let body_expected = match typed.typ.as_ref() {
3676                    TypeKind::Fun(_arg, ret) => Some(ret),
3677                    _ => None,
3678                };
3679                visit(body.as_ref(), typed_body.as_ref(), pos, body_expected, best);
3680            }
3681            (Expr::App(_span, f, x), TypedExprKind::App(tf, tx)) => {
3682                let expected_arg = match tf.typ.as_ref() {
3683                    TypeKind::Fun(arg, _ret) => Some(arg),
3684                    _ => None,
3685                };
3686                visit(x.as_ref(), tx.as_ref(), pos, expected_arg, best);
3687
3688                let expected_fun = Type::fun(tx.typ.clone(), typed.typ.clone());
3689                visit(f.as_ref(), tf.as_ref(), pos, Some(&expected_fun), best);
3690            }
3691            (Expr::Project(_span, base, _field), TypedExprKind::Project { expr: tbase, .. }) => {
3692                visit(base.as_ref(), tbase.as_ref(), pos, None, best);
3693            }
3694            (
3695                Expr::Ite(_span, cond, then_expr, else_expr),
3696                TypedExprKind::Ite {
3697                    cond: tcond,
3698                    then_expr: tthen,
3699                    else_expr: telse,
3700                },
3701            ) => {
3702                let bool_ty = Type::builtin(BuiltinTypeId::Bool);
3703                visit(cond.as_ref(), tcond.as_ref(), pos, Some(&bool_ty), best);
3704                visit(
3705                    then_expr.as_ref(),
3706                    tthen.as_ref(),
3707                    pos,
3708                    Some(&typed.typ),
3709                    best,
3710                );
3711                visit(
3712                    else_expr.as_ref(),
3713                    telse.as_ref(),
3714                    pos,
3715                    Some(&typed.typ),
3716                    best,
3717                );
3718            }
3719            (Expr::Tuple(_span, elems), TypedExprKind::Tuple(typed_elems)) => {
3720                for (elem, typed_elem) in elems.iter().zip(typed_elems.iter()) {
3721                    visit(elem.as_ref(), typed_elem, pos, Some(&typed_elem.typ), best);
3722                }
3723            }
3724            (Expr::List(_span, elems), TypedExprKind::List(typed_elems)) => {
3725                let list_elem_expected = match typed.typ.as_ref() {
3726                    TypeKind::App(head, elem) => match head.as_ref() {
3727                        TypeKind::Con(tc)
3728                            if tc.builtin_id == Some(BuiltinTypeId::List) && tc.arity == 1 =>
3729                        {
3730                            Some(elem)
3731                        }
3732                        _ => None,
3733                    },
3734                    _ => None,
3735                };
3736                for (elem, typed_elem) in elems.iter().zip(typed_elems.iter()) {
3737                    let expected = list_elem_expected.unwrap_or(&typed_elem.typ);
3738                    visit(elem.as_ref(), typed_elem, pos, Some(expected), best);
3739                }
3740            }
3741            (Expr::Dict(_span, kvs), TypedExprKind::Dict(typed_kvs)) => {
3742                for (key, value) in kvs {
3743                    if let Some(typed_value) = typed_kvs.get(key) {
3744                        visit(
3745                            value.as_ref(),
3746                            typed_value,
3747                            pos,
3748                            Some(&typed_value.typ),
3749                            best,
3750                        );
3751                    }
3752                }
3753            }
3754            (
3755                Expr::RecordUpdate(_span, base, updates),
3756                TypedExprKind::RecordUpdate {
3757                    base: typed_base,
3758                    updates: typed_updates,
3759                },
3760            ) => {
3761                visit(base.as_ref(), typed_base.as_ref(), pos, None, best);
3762                for (key, value) in updates {
3763                    if let Some(typed_value) = typed_updates.get(key) {
3764                        visit(
3765                            value.as_ref(),
3766                            typed_value,
3767                            pos,
3768                            Some(&typed_value.typ),
3769                            best,
3770                        );
3771                    }
3772                }
3773            }
3774            (
3775                Expr::Match(_span, scrutinee, arms),
3776                TypedExprKind::Match {
3777                    scrutinee: tscrutinee,
3778                    arms: typed_arms,
3779                },
3780            ) => {
3781                visit(
3782                    scrutinee.as_ref(),
3783                    tscrutinee.as_ref(),
3784                    pos,
3785                    Some(&tscrutinee.typ),
3786                    best,
3787                );
3788                for ((_pat, arm), (_typed_pat, typed_arm)) in arms.iter().zip(typed_arms.iter()) {
3789                    visit(arm.as_ref(), typed_arm, pos, Some(&typed.typ), best);
3790                }
3791            }
3792            (Expr::Ann(_span, inner, _ann), _) => {
3793                visit(inner.as_ref(), typed, pos, Some(&typed.typ), best);
3794            }
3795            _ => {}
3796        }
3797    }
3798
3799    let mut best: Option<Candidate> = None;
3800    visit(expr, typed, pos, None, &mut best);
3801    best.map(|candidate| candidate.typ)
3802}
3803
3804fn inferred_type_in_expr(expr: &Expr, typed: &TypedExpr, pos: RexPosition) -> Option<Type> {
3805    fn span_size(span: Span) -> (usize, usize) {
3806        (
3807            span.end.line.saturating_sub(span.begin.line),
3808            span.end.column.saturating_sub(span.begin.column),
3809        )
3810    }
3811
3812    fn visit(expr: &Expr, typed: &TypedExpr, pos: RexPosition, best: &mut Option<(Span, Type)>) {
3813        let span = *expr.span();
3814        if !position_in_span(pos, span) {
3815            return;
3816        }
3817        if best
3818            .as_ref()
3819            .is_none_or(|(best_span, _)| span_size(span) < span_size(*best_span))
3820        {
3821            *best = Some((span, typed.typ.clone()));
3822        }
3823
3824        match (expr, &typed.kind) {
3825            (
3826                Expr::Let(_, _, _, def, body),
3827                TypedExprKind::Let {
3828                    def: tdef,
3829                    body: tbody,
3830                    ..
3831                },
3832            ) => {
3833                visit(def.as_ref(), tdef.as_ref(), pos, best);
3834                visit(body.as_ref(), tbody.as_ref(), pos, best);
3835            }
3836            (
3837                Expr::LetRec(_, bindings, body),
3838                TypedExprKind::LetRec {
3839                    bindings: typed_bindings,
3840                    body: typed_body,
3841                },
3842            ) => {
3843                for ((_, _, def), (_, typed_def)) in bindings.iter().zip(typed_bindings.iter()) {
3844                    visit(def.as_ref(), typed_def, pos, best);
3845                }
3846                visit(body.as_ref(), typed_body.as_ref(), pos, best);
3847            }
3848            (
3849                Expr::Lam(_, _, _, _, _, body),
3850                TypedExprKind::Lam {
3851                    body: typed_body, ..
3852                },
3853            ) => {
3854                visit(body.as_ref(), typed_body.as_ref(), pos, best);
3855            }
3856            (Expr::App(_, f, x), TypedExprKind::App(tf, tx)) => {
3857                visit(f.as_ref(), tf.as_ref(), pos, best);
3858                visit(x.as_ref(), tx.as_ref(), pos, best);
3859            }
3860            (Expr::Project(_, base, _), TypedExprKind::Project { expr: tbase, .. }) => {
3861                visit(base.as_ref(), tbase.as_ref(), pos, best);
3862            }
3863            (
3864                Expr::Ite(_, cond, then_expr, else_expr),
3865                TypedExprKind::Ite {
3866                    cond: tcond,
3867                    then_expr: tthen,
3868                    else_expr: telse,
3869                },
3870            ) => {
3871                visit(cond.as_ref(), tcond.as_ref(), pos, best);
3872                visit(then_expr.as_ref(), tthen.as_ref(), pos, best);
3873                visit(else_expr.as_ref(), telse.as_ref(), pos, best);
3874            }
3875            (Expr::Tuple(_, elems), TypedExprKind::Tuple(typed_elems))
3876            | (Expr::List(_, elems), TypedExprKind::List(typed_elems)) => {
3877                for (elem, typed_elem) in elems.iter().zip(typed_elems.iter()) {
3878                    visit(elem.as_ref(), typed_elem, pos, best);
3879                }
3880            }
3881            (Expr::Dict(_, kvs), TypedExprKind::Dict(typed_kvs)) => {
3882                for (key, value) in kvs {
3883                    if let Some(typed_value) = typed_kvs.get(key) {
3884                        visit(value.as_ref(), typed_value, pos, best);
3885                    }
3886                }
3887            }
3888            (
3889                Expr::RecordUpdate(_, base, updates),
3890                TypedExprKind::RecordUpdate {
3891                    base: typed_base,
3892                    updates: typed_updates,
3893                },
3894            ) => {
3895                visit(base.as_ref(), typed_base.as_ref(), pos, best);
3896                for (key, value) in updates {
3897                    if let Some(typed_value) = typed_updates.get(key) {
3898                        visit(value.as_ref(), typed_value, pos, best);
3899                    }
3900                }
3901            }
3902            (
3903                Expr::Match(_, scrutinee, arms),
3904                TypedExprKind::Match {
3905                    scrutinee: tscrutinee,
3906                    arms: typed_arms,
3907                },
3908            ) => {
3909                visit(scrutinee.as_ref(), tscrutinee.as_ref(), pos, best);
3910                for ((_pat, arm), (_typed_pat, typed_arm)) in arms.iter().zip(typed_arms.iter()) {
3911                    visit(arm.as_ref(), typed_arm, pos, best);
3912                }
3913            }
3914            (Expr::Ann(_, inner, _), _) => visit(inner.as_ref(), typed, pos, best),
3915            _ => {}
3916        }
3917    }
3918
3919    let mut best: Option<(Span, Type)> = None;
3920    visit(expr, typed, pos, &mut best);
3921    best.map(|(_, ty)| ty)
3922}
3923
3924fn functions_producing_expected_type_at_position(
3925    uri: &Url,
3926    text: &str,
3927    position: Position,
3928) -> Vec<(String, String)> {
3929    let Some(target_type) = expected_type_at_position_type(uri, text, position) else {
3930        return Vec::new();
3931    };
3932
3933    let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) else {
3934        return Vec::new();
3935    };
3936    let Ok((program, mut ts, _imports, _import_diags)) =
3937        prepare_program_with_imports(uri, &program)
3938    else {
3939        return Vec::new();
3940    };
3941    if inject_program_decls(&mut ts, &program, None).is_err() {
3942        return Vec::new();
3943    }
3944
3945    let values = semantic_candidate_values(&ts);
3946
3947    let mut out = Vec::new();
3948    for (name, schemes) in values {
3949        for scheme in schemes {
3950            let (_preds, inst_ty) = instantiate(&scheme, &mut ts.supply);
3951            let mut cur = &inst_ty;
3952            let mut is_function = false;
3953            while let TypeKind::Fun(_, ret) = cur.as_ref() {
3954                is_function = true;
3955                cur = ret;
3956            }
3957            if !is_function {
3958                continue;
3959            }
3960            if unify(cur, &target_type).is_ok() {
3961                out.push((name.to_string(), scheme.typ.to_string()));
3962            }
3963        }
3964    }
3965
3966    out.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
3967    out.dedup();
3968    if out.len() > MAX_SEMANTIC_CANDIDATES {
3969        out.truncate(MAX_SEMANTIC_CANDIDATES);
3970    }
3971    out
3972}
3973
3974fn functions_accepting_inferred_type_at_position(
3975    uri: &Url,
3976    text: &str,
3977    position: Position,
3978) -> Vec<(String, String)> {
3979    let Some(source_type) = inferred_type_at_position_type(uri, text, position) else {
3980        return Vec::new();
3981    };
3982
3983    let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) else {
3984        return Vec::new();
3985    };
3986    let Ok((program, mut ts, _imports, _import_diags)) =
3987        prepare_program_with_imports(uri, &program)
3988    else {
3989        return Vec::new();
3990    };
3991    if inject_program_decls(&mut ts, &program, None).is_err() {
3992        return Vec::new();
3993    }
3994
3995    let values = semantic_candidate_values(&ts);
3996
3997    let mut out = Vec::new();
3998    for (name, schemes) in values {
3999        let name = name.to_string();
4000        if !is_ident_like(&name) {
4001            continue;
4002        }
4003        for scheme in schemes {
4004            let (_preds, inst_ty) = instantiate(&scheme, &mut ts.supply);
4005            let (args, _ret) = split_fun_type(&inst_ty);
4006            if let Some(first_arg) = args.first()
4007                && unify(first_arg, &source_type).is_ok()
4008            {
4009                out.push((name.clone(), scheme.typ.to_string()));
4010            }
4011        }
4012    }
4013
4014    out.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
4015    out.dedup();
4016    if out.len() > MAX_SEMANTIC_CANDIDATES {
4017        out.truncate(MAX_SEMANTIC_CANDIDATES);
4018    }
4019    out
4020}
4021
4022fn adapters_from_inferred_to_expected_at_position(
4023    uri: &Url,
4024    text: &str,
4025    position: Position,
4026) -> Vec<(String, String)> {
4027    let Some(source_type) = inferred_type_at_position_type(uri, text, position) else {
4028        return Vec::new();
4029    };
4030    let Some(target_type) = expected_type_at_position_type(uri, text, position) else {
4031        return Vec::new();
4032    };
4033
4034    let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) else {
4035        return Vec::new();
4036    };
4037    let Ok((program, mut ts, _imports, _import_diags)) =
4038        prepare_program_with_imports(uri, &program)
4039    else {
4040        return Vec::new();
4041    };
4042    if inject_program_decls(&mut ts, &program, None).is_err() {
4043        return Vec::new();
4044    }
4045
4046    let values = semantic_candidate_values(&ts);
4047
4048    let mut out = Vec::new();
4049    for (name, schemes) in values {
4050        let name = name.to_string();
4051        if !is_ident_like(&name) {
4052            continue;
4053        }
4054        for scheme in schemes {
4055            let (_preds, inst_ty) = instantiate(&scheme, &mut ts.supply);
4056            let (args, ret) = split_fun_type(&inst_ty);
4057            if args.len() == 1
4058                && unify(&args[0], &source_type).is_ok()
4059                && unify(&ret, &target_type).is_ok()
4060            {
4061                out.push((name.clone(), scheme.typ.to_string()));
4062            }
4063        }
4064    }
4065
4066    out.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
4067    out.dedup();
4068    if out.len() > MAX_SEMANTIC_CANDIDATES {
4069        out.truncate(MAX_SEMANTIC_CANDIDATES);
4070    }
4071    out
4072}
4073
4074fn functions_compatible_with_in_scope_values_at_position(
4075    uri: &Url,
4076    text: &str,
4077    position: Position,
4078) -> Vec<String> {
4079    let produced = functions_producing_expected_type_at_position(uri, text, position);
4080    let mut produced_by_name: HashMap<String, Vec<String>> = HashMap::new();
4081    for (name, typ) in produced {
4082        produced_by_name.entry(name).or_default().push(typ);
4083    }
4084
4085    let mut out = Vec::new();
4086    for (name, replacement) in hole_fill_candidates_at_position(uri, text, position) {
4087        if replacement.contains('?') {
4088            continue;
4089        }
4090        if let Some(types) = produced_by_name.get(&name) {
4091            for typ in types {
4092                out.push(format!("{name} : {typ} => {replacement}"));
4093            }
4094        } else {
4095            out.push(format!("{name} => {replacement}"));
4096        }
4097    }
4098    out.sort();
4099    out.dedup();
4100    if out.len() > MAX_SEMANTIC_CANDIDATES {
4101        out.truncate(MAX_SEMANTIC_CANDIDATES);
4102    }
4103    out
4104}
4105
4106fn execute_query_command_for_document(
4107    command: &str,
4108    uri: &Url,
4109    text: &str,
4110    position: Position,
4111) -> Option<Value> {
4112    match command {
4113        CMD_EXPECTED_TYPE_AT => Some(match expected_type_at_position(uri, text, position) {
4114            Some(typ) => json!({ "expectedType": typ }),
4115            None => Value::Null,
4116        }),
4117        CMD_FUNCTIONS_ACCEPTING_INFERRED_TYPE_AT => Some(json!({
4118            "inferredType": inferred_type_at_position(uri, text, position),
4119            "items": functions_accepting_inferred_type_at_position(uri, text, position)
4120                .into_iter()
4121                .map(|(name, typ)| format!("{name} : {typ}"))
4122                .collect::<Vec<_>>()
4123        })),
4124        CMD_ADAPTERS_FROM_INFERRED_TO_EXPECTED_AT => Some(json!({
4125            "inferredType": inferred_type_at_position(uri, text, position),
4126            "expectedType": expected_type_at_position(uri, text, position),
4127            "items": adapters_from_inferred_to_expected_at_position(uri, text, position)
4128                .into_iter()
4129                .map(|(name, typ)| format!("{name} : {typ}"))
4130                .collect::<Vec<_>>()
4131        })),
4132        CMD_FUNCTIONS_COMPATIBLE_WITH_IN_SCOPE_VALUES_AT => Some(json!({
4133            "items": functions_compatible_with_in_scope_values_at_position(uri, text, position)
4134        })),
4135        CMD_FUNCTIONS_PRODUCING_EXPECTED_TYPE_AT => {
4136            let items = functions_producing_expected_type_at_position(uri, text, position)
4137                .into_iter()
4138                .map(|(name, typ)| format!("{name} : {typ}"))
4139                .collect::<Vec<_>>();
4140            Some(json!({ "items": items }))
4141        }
4142        _ => None,
4143    }
4144}
4145
4146fn execute_query_command_for_document_without_position(
4147    command: &str,
4148    uri: &Url,
4149    text: &str,
4150) -> Option<Value> {
4151    match command {
4152        CMD_HOLES_EXPECTED_TYPES => Some(json!({
4153            "holes": hole_expected_types_for_document(uri, text)
4154        })),
4155        _ => None,
4156    }
4157}
4158
4159fn workspace_edit_fingerprint(edit: &WorkspaceEdit) -> String {
4160    let mut payload = String::new();
4161    if let Some(changes) = &edit.changes {
4162        let mut uris = changes.keys().cloned().collect::<Vec<_>>();
4163        uris.sort_by(|a, b| a.as_str().cmp(b.as_str()));
4164        for uri in uris {
4165            payload.push_str(uri.as_str());
4166            payload.push('\n');
4167            if let Some(edits) = changes.get(&uri) {
4168                for edit in edits {
4169                    payload.push_str(&format!(
4170                        "{}:{}-{}:{}\n",
4171                        edit.range.start.line,
4172                        edit.range.start.character,
4173                        edit.range.end.line,
4174                        edit.range.end.character
4175                    ));
4176                    payload.push_str(&edit.new_text);
4177                    payload.push('\n');
4178                }
4179            }
4180        }
4181    }
4182    if let Some(document_changes) = &edit.document_changes
4183        && let Ok(encoded) = serde_json::to_string(document_changes)
4184    {
4185        payload.push_str(&encoded);
4186    }
4187    if let Some(change_annotations) = &edit.change_annotations
4188        && let Ok(encoded) = serde_json::to_string(change_annotations)
4189    {
4190        payload.push_str(&encoded);
4191    }
4192    sha256_hex(payload.as_bytes())
4193}
4194
4195fn semantic_quick_fixes_for_range(
4196    uri: &Url,
4197    text: &str,
4198    cursor_range: Range,
4199    diagnostics: &[Diagnostic],
4200) -> Vec<Value> {
4201    let mut out = code_actions_for_source(uri, text, cursor_range, diagnostics)
4202        .into_iter()
4203        .filter_map(|action| match action {
4204            CodeActionOrCommand::CodeAction(action) => Some(action),
4205            CodeActionOrCommand::Command(_) => None,
4206        })
4207        .map(|action| {
4208            let kind = action
4209                .kind
4210                .and_then(|k| to_value(k).ok())
4211                .and_then(|v| v.as_str().map(str::to_string));
4212            let edit = action.edit.unwrap_or(WorkspaceEdit {
4213                changes: None,
4214                document_changes: None,
4215                change_annotations: None,
4216            });
4217            let fingerprint = workspace_edit_fingerprint(&edit);
4218            json!({
4219                "id": format!("qf-{}", &fingerprint[..16]),
4220                "title": action.title,
4221                "kind": kind,
4222                "edit": to_value(edit).unwrap_or(Value::Null),
4223            })
4224        })
4225        .collect::<Vec<_>>();
4226
4227    out.sort_by_key(|item| {
4228        (
4229            item.get("title")
4230                .and_then(Value::as_str)
4231                .unwrap_or("")
4232                .to_string(),
4233            item.get("id")
4234                .and_then(Value::as_str)
4235                .unwrap_or("")
4236                .to_string(),
4237        )
4238    });
4239    out.dedup_by(|a, b| a.get("id") == b.get("id"));
4240    out
4241}
4242
4243fn execute_semantic_loop_step(uri: &Url, text: &str, position: Position) -> Option<Value> {
4244    let expected_type = expected_type_at_position(uri, text, position)
4245        .or_else(|| expected_type_from_syntax_context(uri, text, position));
4246    let inferred_type = inferred_type_at_position(uri, text, position);
4247
4248    let mut in_scope_values = in_scope_value_types_at_position(uri, text, position)
4249        .into_iter()
4250        .filter(|(name, _)| is_ident_like(name))
4251        .map(|(name, typ)| format!("{name} : {typ}"))
4252        .collect::<Vec<_>>();
4253    in_scope_values.sort();
4254    in_scope_values.dedup();
4255    if in_scope_values.len() > MAX_SEMANTIC_IN_SCOPE_VALUES {
4256        in_scope_values.truncate(MAX_SEMANTIC_IN_SCOPE_VALUES);
4257    }
4258
4259    let function_candidates = functions_producing_expected_type_at_position(uri, text, position)
4260        .into_iter()
4261        .map(|(name, typ)| format!("{name} : {typ}"))
4262        .collect::<Vec<_>>();
4263
4264    let hole_fill_candidates = hole_fill_candidates_at_position(uri, text, position)
4265        .into_iter()
4266        .map(|(name, replacement)| json!({ "name": name, "replacement": replacement }))
4267        .collect::<Vec<_>>();
4268    let functions_accepting_inferred_type =
4269        functions_accepting_inferred_type_at_position(uri, text, position)
4270            .into_iter()
4271            .map(|(name, typ)| format!("{name} : {typ}"))
4272            .collect::<Vec<_>>();
4273    let adapters_from_inferred_to_expected =
4274        adapters_from_inferred_to_expected_at_position(uri, text, position)
4275            .into_iter()
4276            .map(|(name, typ)| format!("{name} : {typ}"))
4277            .collect::<Vec<_>>();
4278    let compatible_with_in_scope_values =
4279        functions_compatible_with_in_scope_values_at_position(uri, text, position);
4280
4281    let cursor_range = Range {
4282        start: position,
4283        end: position,
4284    };
4285    let mut local_diagnostics: Vec<Diagnostic> = diagnostics_from_text(uri, text)
4286        .into_iter()
4287        .filter(|diag| ranges_overlap(diag.range, cursor_range))
4288        .collect();
4289    local_diagnostics.sort_by_key(|diag| {
4290        (
4291            diag.range.start.line,
4292            diag.range.start.character,
4293            diag.range.end.line,
4294            diag.range.end.character,
4295            diag.message.clone(),
4296        )
4297    });
4298
4299    let quick_fixes = semantic_quick_fixes_for_range(uri, text, cursor_range, &local_diagnostics);
4300    let mut quick_fix_titles = quick_fixes
4301        .iter()
4302        .filter_map(|item| item.get("title").and_then(Value::as_str))
4303        .map(ToString::to_string)
4304        .collect::<Vec<_>>();
4305    quick_fix_titles.sort();
4306    quick_fix_titles.dedup();
4307
4308    Some(json!({
4309        "expectedType": expected_type,
4310        "inferredType": inferred_type,
4311        "inScopeValues": in_scope_values,
4312        "functionCandidates": function_candidates,
4313        "holeFillCandidates": hole_fill_candidates,
4314        "functionsAcceptingInferredType": functions_accepting_inferred_type,
4315        "adaptersFromInferredToExpectedType": adapters_from_inferred_to_expected,
4316        "functionsCompatibleWithInScopeValues": compatible_with_in_scope_values,
4317        "localDiagnostics": local_diagnostics.into_iter().map(|diag| {
4318            json!({
4319                "message": diag.message,
4320                "line": diag.range.start.line,
4321                "character": diag.range.start.character,
4322            })
4323        }).collect::<Vec<_>>(),
4324        "quickFixes": quick_fixes,
4325        "quickFixTitles": quick_fix_titles,
4326        "holes": hole_expected_types_for_document(uri, text),
4327    }))
4328}
4329
4330fn execute_semantic_loop_apply_quick_fix(
4331    uri: &Url,
4332    text: &str,
4333    position: Position,
4334    quick_fix_id: &str,
4335) -> Option<Value> {
4336    let cursor_range = Range {
4337        start: position,
4338        end: position,
4339    };
4340    let local_diagnostics: Vec<Diagnostic> = diagnostics_from_text(uri, text)
4341        .into_iter()
4342        .filter(|diag| ranges_overlap(diag.range, cursor_range))
4343        .collect();
4344    let quick_fixes = semantic_quick_fixes_for_range(uri, text, cursor_range, &local_diagnostics);
4345    let quick_fix = quick_fixes.into_iter().find(|item| {
4346        item.get("id")
4347            .and_then(Value::as_str)
4348            .is_some_and(|id| id == quick_fix_id)
4349    });
4350
4351    Some(match quick_fix {
4352        Some(quick_fix) => json!({ "quickFix": quick_fix }),
4353        None => Value::Null,
4354    })
4355}
4356
4357fn quick_fix_priority(strategy: BulkQuickFixStrategy, title: &str) -> usize {
4358    let aggressive_introduce =
4359        strategy == BulkQuickFixStrategy::Aggressive && title.starts_with("Introduce `let ");
4360    if title.starts_with("Fill hole with `") {
4361        0
4362    } else if title.starts_with("Replace `") || aggressive_introduce {
4363        1
4364    } else if title.starts_with("Add wildcard arm") {
4365        2
4366    } else if title.starts_with("Wrap expression in list literal") {
4367        3
4368    } else if title.starts_with("Unwrap single-item list literal") {
4369        4
4370    } else if title.starts_with("Apply expression to missing argument") {
4371        5
4372    } else if title.starts_with("Wrap expression in lambda") {
4373        6
4374    } else if title.starts_with("Introduce `let ") {
4375        7
4376    } else {
4377        10
4378    }
4379}
4380
4381fn best_quick_fix_from_candidates(
4382    candidates: &[Value],
4383    strategy: BulkQuickFixStrategy,
4384) -> Option<Value> {
4385    candidates
4386        .iter()
4387        .min_by_key(|item| {
4388            let title = item.get("title").and_then(Value::as_str).unwrap_or("");
4389            let id = item.get("id").and_then(Value::as_str).unwrap_or("");
4390            (
4391                quick_fix_priority(strategy, title),
4392                title.to_string(),
4393                id.to_string(),
4394            )
4395        })
4396        .cloned()
4397}
4398
4399fn apply_workspace_edit_to_text(uri: &Url, text: &str, edit: &WorkspaceEdit) -> Option<String> {
4400    let changes = edit.changes.as_ref()?;
4401    let edits = changes.get(uri)?.clone();
4402    if edits.is_empty() {
4403        return Some(text.to_string());
4404    }
4405    let mut with_offsets = Vec::new();
4406    for edit in edits {
4407        let start = offset_at(text, edit.range.start)?;
4408        let end = offset_at(text, edit.range.end)?;
4409        if start > end || end > text.len() {
4410            return None;
4411        }
4412        with_offsets.push((start, end, edit.new_text));
4413    }
4414    with_offsets.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.cmp(&a.1)));
4415
4416    let mut out = text.to_string();
4417    for (start, end, replacement) in with_offsets {
4418        out.replace_range(start..end, &replacement);
4419    }
4420    Some(out)
4421}
4422
4423fn text_state_hash(text: &str) -> String {
4424    sha256_hex(text.as_bytes())
4425}
4426
4427fn next_no_improvement_streak(streak: usize, diagnostics_delta: i64) -> usize {
4428    if diagnostics_delta > 0 { 0 } else { streak + 1 }
4429}
4430
4431fn execute_semantic_loop_apply_best_quick_fixes(
4432    uri: &Url,
4433    text: &str,
4434    position: Position,
4435    max_steps: usize,
4436    strategy: BulkQuickFixStrategy,
4437    dry_run: bool,
4438) -> Option<Value> {
4439    let cursor_range = Range {
4440        start: position,
4441        end: position,
4442    };
4443    let mut current_text = text.to_string();
4444    let mut applied = Vec::new();
4445    let mut steps = Vec::new();
4446    let mut stopped_reason = "noQuickFix".to_string();
4447    let mut stopped_reason_detail = "no quick-fixes available at cursor".to_string();
4448    let mut no_improvement_streak = 0usize;
4449    let mut last_diagnostics_delta = 0i64;
4450    let mut seen_states: HashSet<String> = HashSet::new();
4451    seen_states.insert(text_state_hash(&current_text));
4452
4453    for step_index in 0..max_steps {
4454        let local_diagnostics: Vec<Diagnostic> = diagnostics_from_text(uri, &current_text)
4455            .into_iter()
4456            .filter(|diag| ranges_overlap(diag.range, cursor_range))
4457            .collect();
4458        let diagnostics_before = local_diagnostics
4459            .iter()
4460            .map(|diag| {
4461                json!({
4462                    "message": diag.message,
4463                    "line": diag.range.start.line,
4464                    "character": diag.range.start.character,
4465                })
4466            })
4467            .collect::<Vec<_>>();
4468        let quick_fixes =
4469            semantic_quick_fixes_for_range(uri, &current_text, cursor_range, &local_diagnostics);
4470        let Some(best) = best_quick_fix_from_candidates(&quick_fixes, strategy) else {
4471            stopped_reason = "noQuickFix".to_string();
4472            stopped_reason_detail = "no candidate quick-fix was available".to_string();
4473            break;
4474        };
4475        let edit_value = best.get("edit").cloned().unwrap_or(Value::Null);
4476        let Ok(edit) = serde_json::from_value::<WorkspaceEdit>(edit_value) else {
4477            stopped_reason = "invalidEdit".to_string();
4478            stopped_reason_detail = "selected quick-fix edit was invalid".to_string();
4479            break;
4480        };
4481        let Some(next_text) = apply_workspace_edit_to_text(uri, &current_text, &edit) else {
4482            stopped_reason = "applyFailed".to_string();
4483            stopped_reason_detail = "failed to apply selected workspace edit".to_string();
4484            break;
4485        };
4486        if next_text == current_text {
4487            stopped_reason = "noTextChange".to_string();
4488            stopped_reason_detail = "selected quick-fix did not change text".to_string();
4489            break;
4490        }
4491        let next_hash = text_state_hash(&next_text);
4492        if seen_states.contains(&next_hash) {
4493            stopped_reason = "cycleDetected".to_string();
4494            stopped_reason_detail = "next text state already seen in this run".to_string();
4495            break;
4496        }
4497        let diagnostics_after_step: Vec<Value> = diagnostics_from_text(uri, &next_text)
4498            .into_iter()
4499            .filter(|diag| ranges_overlap(diag.range, cursor_range))
4500            .map(|diag| {
4501                json!({
4502                    "message": diag.message,
4503                    "line": diag.range.start.line,
4504                    "character": diag.range.start.character,
4505                })
4506            })
4507            .collect();
4508        let before_count = diagnostics_before.len();
4509        let after_count = diagnostics_after_step.len();
4510        let diagnostics_delta = (before_count as i64) - (after_count as i64);
4511        last_diagnostics_delta = diagnostics_delta;
4512        no_improvement_streak =
4513            next_no_improvement_streak(no_improvement_streak, diagnostics_delta);
4514        steps.push(json!({
4515            "index": step_index,
4516            "quickFix": best.clone(),
4517            "diagnosticsBefore": diagnostics_before,
4518            "diagnosticsAfter": diagnostics_after_step,
4519            "diagnosticsBeforeCount": before_count,
4520            "diagnosticsAfterCount": after_count,
4521            "diagnosticsDelta": diagnostics_delta,
4522            "noImprovementStreak": no_improvement_streak,
4523        }));
4524        applied.push(best);
4525        current_text = next_text;
4526        seen_states.insert(next_hash);
4527        if no_improvement_streak >= NO_IMPROVEMENT_STREAK_LIMIT {
4528            stopped_reason = "noImprovementStreak".to_string();
4529            stopped_reason_detail =
4530                format!("diagnostics did not improve for {NO_IMPROVEMENT_STREAK_LIMIT} step(s)");
4531            break;
4532        }
4533        stopped_reason = "maxStepsReached".to_string();
4534        stopped_reason_detail = format!("reached maxSteps={max_steps}");
4535    }
4536
4537    let diagnostics_after: Vec<Value> = diagnostics_from_text(uri, &current_text)
4538        .into_iter()
4539        .filter(|diag| ranges_overlap(diag.range, cursor_range))
4540        .map(|diag| {
4541            json!({
4542                "message": diag.message,
4543                "line": diag.range.start.line,
4544                "character": diag.range.start.character,
4545            })
4546        })
4547        .collect();
4548
4549    Some(json!({
4550        "strategy": strategy.as_str(),
4551        "dryRun": dry_run,
4552        "appliedQuickFixes": applied,
4553        "appliedCount": applied.len(),
4554        "steps": steps,
4555        "updatedText": current_text,
4556        "localDiagnosticsAfter": diagnostics_after,
4557        "stoppedReason": stopped_reason,
4558        "stoppedReasonDetail": stopped_reason_detail,
4559        "lastDiagnosticsDelta": last_diagnostics_delta,
4560        "noImprovementStreak": no_improvement_streak,
4561        "seenStatesCount": seen_states.len(),
4562    }))
4563}
4564
4565fn hole_expected_types_for_document(uri: &Url, text: &str) -> Vec<Value> {
4566    let mut holes = Vec::new();
4567
4568    // First-class holes: parse `?` nodes directly.
4569    if let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) {
4570        let mut spans = Vec::new();
4571        collect_hole_spans(program.expr_with_fns().as_ref(), &mut spans);
4572        for span in spans {
4573            let pos = span_to_range(span).start;
4574            if let Some(expected_type) = expected_type_at_position(uri, text, pos)
4575                .or_else(|| expected_type_from_syntax_context(uri, text, pos))
4576            {
4577                holes.push(json!({
4578                    "name": "?",
4579                    "line": pos.line,
4580                    "character": pos.character,
4581                    "expectedType": expected_type
4582                }));
4583            }
4584        }
4585    }
4586
4587    // Backward-compat fallback: `_foo` placeholder variables still treated as holes.
4588    let diagnostics = diagnostics_from_text(uri, text);
4589    for diag in diagnostics {
4590        let Some(name) = unknown_var_name_from_message(&diag.message) else {
4591            continue;
4592        };
4593        if !is_hole_name(name) {
4594            continue;
4595        }
4596        if !range_is_usable_for_text(text, diag.range) {
4597            continue;
4598        }
4599        let pos = diag.range.start;
4600        if let Some(expected_type) = expected_type_at_position(uri, text, pos)
4601            .or_else(|| expected_type_from_syntax_context(uri, text, pos))
4602        {
4603            holes.push(json!({
4604                "name": name,
4605                "line": pos.line,
4606                "character": pos.character,
4607                "expectedType": expected_type
4608            }));
4609        }
4610    }
4611    holes.sort_by_key(|item| {
4612        let line = item.get("line").and_then(Value::as_u64).unwrap_or(0);
4613        let ch = item.get("character").and_then(Value::as_u64).unwrap_or(0);
4614        let name = item
4615            .get("name")
4616            .and_then(Value::as_str)
4617            .unwrap_or("")
4618            .to_string();
4619        (line, ch, name)
4620    });
4621    holes.dedup_by(|a, b| {
4622        a.get("name") == b.get("name")
4623            && a.get("line") == b.get("line")
4624            && a.get("character") == b.get("character")
4625    });
4626    if holes.len() > MAX_SEMANTIC_HOLES {
4627        holes.truncate(MAX_SEMANTIC_HOLES);
4628    }
4629    holes
4630}
4631
4632fn collect_hole_spans(expr: &Expr, out: &mut Vec<Span>) {
4633    match expr {
4634        Expr::Hole(span) => out.push(*span),
4635        Expr::App(_, f, x) => {
4636            collect_hole_spans(f, out);
4637            collect_hole_spans(x, out);
4638        }
4639        Expr::Project(_, base, _) => collect_hole_spans(base, out),
4640        Expr::Lam(_, _scope, _param, _ann, _constraints, body) => collect_hole_spans(body, out),
4641        Expr::Let(_, _var, _ann, def, body) => {
4642            collect_hole_spans(def, out);
4643            collect_hole_spans(body, out);
4644        }
4645        Expr::LetRec(_, bindings, body) => {
4646            for (_var, _ann, def) in bindings {
4647                collect_hole_spans(def, out);
4648            }
4649            collect_hole_spans(body, out);
4650        }
4651        Expr::Ite(_, cond, then_expr, else_expr) => {
4652            collect_hole_spans(cond, out);
4653            collect_hole_spans(then_expr, out);
4654            collect_hole_spans(else_expr, out);
4655        }
4656        Expr::Match(_, scrutinee, arms) => {
4657            collect_hole_spans(scrutinee, out);
4658            for (_pat, arm) in arms {
4659                collect_hole_spans(arm, out);
4660            }
4661        }
4662        Expr::Ann(_, inner, _) => collect_hole_spans(inner, out),
4663        Expr::Tuple(_, elems) | Expr::List(_, elems) => {
4664            for elem in elems {
4665                collect_hole_spans(elem, out);
4666            }
4667        }
4668        Expr::Dict(_, kvs) => {
4669            for value in kvs.values() {
4670                collect_hole_spans(value, out);
4671            }
4672        }
4673        Expr::RecordUpdate(_, base, updates) => {
4674            collect_hole_spans(base, out);
4675            for value in updates.values() {
4676                collect_hole_spans(value, out);
4677            }
4678        }
4679        Expr::Var(_)
4680        | Expr::Bool(..)
4681        | Expr::Uint(..)
4682        | Expr::Int(..)
4683        | Expr::Float(..)
4684        | Expr::String(..)
4685        | Expr::Uuid(..)
4686        | Expr::DateTime(..) => {}
4687    }
4688}
4689
4690fn expected_type_from_syntax_context(uri: &Url, text: &str, position: Position) -> Option<String> {
4691    let (_tokens, program) = tokenize_and_parse_cached(uri, text).ok()?;
4692    let pos = lsp_to_rex_position(position);
4693
4694    fn visit(expr: &Expr, pos: RexPosition) -> Option<String> {
4695        if !position_in_span(pos, *expr.span()) {
4696            return None;
4697        }
4698        match expr {
4699            Expr::Let(_span, _name, ann, def, body) => {
4700                if position_in_span(pos, *def.span())
4701                    && let Some(ann) = ann
4702                {
4703                    return Some(ann.to_string());
4704                }
4705                visit(def.as_ref(), pos).or_else(|| visit(body.as_ref(), pos))
4706            }
4707            Expr::Ann(_span, inner, ann) => {
4708                if position_in_span(pos, *inner.span()) {
4709                    return Some(ann.to_string());
4710                }
4711                visit(inner.as_ref(), pos)
4712            }
4713            Expr::Ite(_span, cond, then_expr, else_expr) => {
4714                if position_in_span(pos, *cond.span()) {
4715                    return Some("bool".to_string());
4716                }
4717                visit(cond.as_ref(), pos)
4718                    .or_else(|| visit(then_expr.as_ref(), pos))
4719                    .or_else(|| visit(else_expr.as_ref(), pos))
4720            }
4721            Expr::App(_span, f, x) => visit(f.as_ref(), pos).or_else(|| visit(x.as_ref(), pos)),
4722            Expr::Project(_span, base, _field) => visit(base.as_ref(), pos),
4723            Expr::Lam(_span, _scope, _param, _ann, _constraints, body) => visit(body.as_ref(), pos),
4724            Expr::LetRec(_span, bindings, body) => {
4725                for (_name, _ann, def) in bindings {
4726                    if let Some(found) = visit(def.as_ref(), pos) {
4727                        return Some(found);
4728                    }
4729                }
4730                visit(body.as_ref(), pos)
4731            }
4732            Expr::Match(_span, scrutinee, arms) => {
4733                if let Some(found) = visit(scrutinee.as_ref(), pos) {
4734                    return Some(found);
4735                }
4736                for (_pat, arm) in arms {
4737                    if let Some(found) = visit(arm.as_ref(), pos) {
4738                        return Some(found);
4739                    }
4740                }
4741                None
4742            }
4743            Expr::Tuple(_span, elems) | Expr::List(_span, elems) => {
4744                for elem in elems {
4745                    if let Some(found) = visit(elem.as_ref(), pos) {
4746                        return Some(found);
4747                    }
4748                }
4749                None
4750            }
4751            Expr::Dict(_span, kvs) => {
4752                for value in kvs.values() {
4753                    if let Some(found) = visit(value.as_ref(), pos) {
4754                        return Some(found);
4755                    }
4756                }
4757                None
4758            }
4759            Expr::RecordUpdate(_span, base, updates) => {
4760                if let Some(found) = visit(base.as_ref(), pos) {
4761                    return Some(found);
4762                }
4763                for value in updates.values() {
4764                    if let Some(found) = visit(value.as_ref(), pos) {
4765                        return Some(found);
4766                    }
4767                }
4768                None
4769            }
4770            Expr::Var(_)
4771            | Expr::Bool(..)
4772            | Expr::Uint(..)
4773            | Expr::Int(..)
4774            | Expr::Float(..)
4775            | Expr::String(..)
4776            | Expr::Uuid(..)
4777            | Expr::DateTime(..)
4778            | Expr::Hole(..) => None,
4779        }
4780    }
4781
4782    visit(program.expr_with_fns().as_ref(), pos)
4783}
4784
4785fn command_uri_and_position(arguments: &[Value]) -> Option<(Url, Position)> {
4786    if arguments.len() >= 3 {
4787        let uri = arguments.first()?.as_str()?;
4788        let line = arguments.get(1)?.as_u64()? as u32;
4789        let character = arguments.get(2)?.as_u64()? as u32;
4790        let uri = Url::parse(uri).ok()?;
4791        return Some((uri, Position { line, character }));
4792    }
4793
4794    let obj = arguments.first()?.as_object()?;
4795    let uri = obj.get("uri")?.as_str()?;
4796    let line = obj.get("line")?.as_u64()? as u32;
4797    let character = obj.get("character")?.as_u64()? as u32;
4798    let uri = Url::parse(uri).ok()?;
4799    Some((uri, Position { line, character }))
4800}
4801
4802fn command_uri(arguments: &[Value]) -> Option<Url> {
4803    if arguments.is_empty() {
4804        return None;
4805    }
4806    if let Some(uri) = arguments.first().and_then(Value::as_str) {
4807        return Url::parse(uri).ok();
4808    }
4809    let obj = arguments.first()?.as_object()?;
4810    let uri = obj.get("uri")?.as_str()?;
4811    Url::parse(uri).ok()
4812}
4813
4814fn command_uri_position_and_id(arguments: &[Value]) -> Option<(Url, Position, String)> {
4815    if arguments.len() >= 4 {
4816        let uri = arguments.first()?.as_str()?;
4817        let line = arguments.get(1)?.as_u64()? as u32;
4818        let character = arguments.get(2)?.as_u64()? as u32;
4819        let id = arguments.get(3)?.as_str()?.to_string();
4820        let uri = Url::parse(uri).ok()?;
4821        return Some((uri, Position { line, character }, id));
4822    }
4823
4824    let obj = arguments.first()?.as_object()?;
4825    let uri = obj.get("uri")?.as_str()?;
4826    let line = obj.get("line")?.as_u64()? as u32;
4827    let character = obj.get("character")?.as_u64()? as u32;
4828    let id = obj.get("id")?.as_str()?.to_string();
4829    let uri = Url::parse(uri).ok()?;
4830    Some((uri, Position { line, character }, id))
4831}
4832
4833fn command_uri_position_max_steps_strategy_and_dry_run(
4834    arguments: &[Value],
4835) -> Option<(Url, Position, usize, BulkQuickFixStrategy, bool)> {
4836    if arguments.len() >= 3 {
4837        let uri = arguments.first()?.as_str()?;
4838        let line = arguments.get(1)?.as_u64()? as u32;
4839        let character = arguments.get(2)?.as_u64()? as u32;
4840        let max_steps = arguments
4841            .get(3)
4842            .and_then(Value::as_u64)
4843            .map(|n| n as usize)
4844            .unwrap_or(3);
4845        let strategy = arguments
4846            .get(4)
4847            .and_then(Value::as_str)
4848            .map(BulkQuickFixStrategy::parse)
4849            .unwrap_or(BulkQuickFixStrategy::Conservative);
4850        let dry_run = arguments.get(5).and_then(Value::as_bool).unwrap_or(false);
4851        let uri = Url::parse(uri).ok()?;
4852        return Some((
4853            uri,
4854            Position { line, character },
4855            max_steps.clamp(1, 20),
4856            strategy,
4857            dry_run,
4858        ));
4859    }
4860
4861    let obj = arguments.first()?.as_object()?;
4862    let uri = obj.get("uri")?.as_str()?;
4863    let line = obj.get("line")?.as_u64()? as u32;
4864    let character = obj.get("character")?.as_u64()? as u32;
4865    let max_steps = obj
4866        .get("maxSteps")
4867        .and_then(Value::as_u64)
4868        .map(|n| n as usize)
4869        .unwrap_or(3)
4870        .clamp(1, 20);
4871    let strategy = obj
4872        .get("strategy")
4873        .and_then(Value::as_str)
4874        .map(BulkQuickFixStrategy::parse)
4875        .unwrap_or(BulkQuickFixStrategy::Conservative);
4876    let dry_run = obj.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
4877    let uri = Url::parse(uri).ok()?;
4878    Some((
4879        uri,
4880        Position { line, character },
4881        max_steps,
4882        strategy,
4883        dry_run,
4884    ))
4885}
4886
4887fn hover_type_in_expr(
4888    ts: &mut TypeSystem,
4889    expr: &Expr,
4890    typed: &TypedExpr,
4891    pos: RexPosition,
4892    name: &str,
4893    name_span: Span,
4894    name_is_ident: bool,
4895) -> Option<HoverType> {
4896    fn span_contains_pos(span: Span, pos: RexPosition) -> bool {
4897        position_in_span(pos, span)
4898    }
4899
4900    fn span_contains_span(outer: Span, inner: Span) -> bool {
4901        position_leq(outer.begin, inner.begin) && position_leq(inner.end, outer.end)
4902    }
4903
4904    fn span_size(span: Span) -> (usize, usize) {
4905        (
4906            span.end.line.saturating_sub(span.begin.line),
4907            span.end.column.saturating_sub(span.begin.column),
4908        )
4909    }
4910
4911    fn peel_fun(ty: &Type) -> (Vec<Type>, Type) {
4912        let mut args = Vec::new();
4913        let mut cur = ty.clone();
4914        while let TypeKind::Fun(a, b) = cur.as_ref() {
4915            args.push(a.clone());
4916            cur = b.clone();
4917        }
4918        (args, cur)
4919    }
4920
4921    fn add_bindings_from_pattern(
4922        ts: &mut TypeSystem,
4923        scrutinee_ty: &Type,
4924        pat: &Pattern,
4925        out: &mut HashMap<String, Type>,
4926    ) {
4927        match pat {
4928            Pattern::Wildcard(..) => {}
4929            Pattern::Var(v) => {
4930                out.insert(v.name.as_ref().to_string(), scrutinee_ty.clone());
4931            }
4932            Pattern::Named(_span, ctor, args) => {
4933                let ctor_name = ctor.to_dotted_symbol();
4934                let Some(schemes) = ts.env.lookup(&ctor_name) else {
4935                    return;
4936                };
4937                let Some(scheme) = schemes.first() else {
4938                    return;
4939                };
4940
4941                let (_preds, ctor_ty) = instantiate(scheme, &mut ts.supply);
4942                let (arg_tys, result_ty) = peel_fun(&ctor_ty);
4943                let Ok(s) = unify(&result_ty, scrutinee_ty) else {
4944                    return;
4945                };
4946
4947                for (subpat, arg_ty) in args.iter().zip(arg_tys.iter()) {
4948                    add_bindings_from_pattern(ts, &arg_ty.apply(&s), subpat, out);
4949                }
4950            }
4951            Pattern::Tuple(_span, elems) => {
4952                let elem_tys: Vec<Type> = (0..elems.len())
4953                    .map(|_| Type::var(ts.supply.fresh(None)))
4954                    .collect();
4955                let expected = Type::tuple(elem_tys.clone());
4956                let Ok(s) = unify(scrutinee_ty, &expected) else {
4957                    return;
4958                };
4959                for (p, ty) in elems.iter().zip(elem_tys.iter()) {
4960                    add_bindings_from_pattern(ts, &ty.apply(&s), p, out);
4961                }
4962            }
4963            Pattern::List(_span, elems) => {
4964                let tv = ts.supply.fresh(None);
4965                let elem = Type::var(tv.clone());
4966                let list_ty = Type::app(Type::builtin(BuiltinTypeId::List), elem.clone());
4967                let Ok(s) = unify(scrutinee_ty, &list_ty) else {
4968                    return;
4969                };
4970                let elem_ty = elem.apply(&s);
4971                for p in elems {
4972                    add_bindings_from_pattern(ts, &elem_ty, p, out);
4973                }
4974            }
4975            Pattern::Cons(_span, head, tail) => {
4976                let tv = ts.supply.fresh(None);
4977                let elem = Type::var(tv.clone());
4978                let list_ty = Type::app(Type::builtin(BuiltinTypeId::List), elem.clone());
4979                let Ok(s) = unify(scrutinee_ty, &list_ty) else {
4980                    return;
4981                };
4982                let elem_ty = elem.apply(&s);
4983                let list_ty = list_ty.apply(&s);
4984                add_bindings_from_pattern(ts, &elem_ty, head.as_ref(), out);
4985                add_bindings_from_pattern(ts, &list_ty, tail.as_ref(), out);
4986            }
4987            Pattern::Dict(_span, keys) => match scrutinee_ty.as_ref() {
4988                TypeKind::Record(fields) => {
4989                    for (key, pat) in keys {
4990                        if let Some((_, ty)) = fields.iter().find(|(n, _)| n == key) {
4991                            add_bindings_from_pattern(ts, ty, pat, out);
4992                        }
4993                    }
4994                }
4995                _ => {
4996                    let tv = ts.supply.fresh(None);
4997                    let elem = Type::var(tv.clone());
4998                    let dict_ty = Type::app(Type::builtin(BuiltinTypeId::Dict), elem.clone());
4999                    let Ok(s) = unify(scrutinee_ty, &dict_ty) else {
5000                        return;
5001                    };
5002                    let elem_ty = elem.apply(&s);
5003                    for (_key, pat) in keys {
5004                        add_bindings_from_pattern(ts, &elem_ty, pat, out);
5005                    }
5006                }
5007            },
5008        }
5009    }
5010
5011    struct VisitCtx<'a> {
5012        pos: RexPosition,
5013        name: &'a str,
5014        name_span: Span,
5015        name_is_ident: bool,
5016        best: &'a mut Option<HoverType>,
5017    }
5018
5019    fn visit(ts: &mut TypeSystem, expr: &Expr, typed: &TypedExpr, ctx: &mut VisitCtx<'_>) {
5020        if !span_contains_pos(*expr.span(), ctx.pos) {
5021            return;
5022        }
5023
5024        let consider = |best: &mut Option<HoverType>, candidate: HoverType| {
5025            let take = best
5026                .as_ref()
5027                .is_none_or(|b| span_size(candidate.span) < span_size(b.span));
5028            if take {
5029                *best = Some(candidate);
5030            }
5031        };
5032
5033        // 1) Pattern-bound variables (match arms).
5034        if ctx.name_is_ident
5035            && let (
5036                Expr::Match(_span, _scrutinee, arms),
5037                TypedExprKind::Match {
5038                    scrutinee,
5039                    arms: typed_arms,
5040                },
5041            ) = (&expr, &typed.kind)
5042            && span_contains_span(*expr.span(), ctx.name_span)
5043        {
5044            for ((_pat, _arm_body), (typed_pat, _typed_arm_body)) in
5045                arms.iter().zip(typed_arms.iter())
5046            {
5047                // The `Pattern` is cloned into the typed tree; use either.
5048                let pat_span = *typed_pat.span();
5049                if span_contains_span(pat_span, ctx.name_span) {
5050                    let mut bindings: HashMap<String, Type> = HashMap::new();
5051                    add_bindings_from_pattern(ts, &scrutinee.typ, typed_pat, &mut bindings);
5052                    if let Some(ty) = bindings.get(ctx.name) {
5053                        consider(
5054                            ctx.best,
5055                            HoverType {
5056                                span: ctx.name_span,
5057                                label: ctx.name.to_string(),
5058                                typ: ty.to_string(),
5059                                overloads: Vec::new(),
5060                            },
5061                        );
5062                    }
5063                    break;
5064                }
5065            }
5066        }
5067
5068        // 2) Binding sites: `let x = ...` and lambda params.
5069        match (expr, &typed.kind) {
5070            (
5071                Expr::Let(_span, binding, _ann, def, body),
5072                TypedExprKind::Let {
5073                    def: tdef,
5074                    body: tbody,
5075                    ..
5076                },
5077            ) => {
5078                if span_contains_pos(binding.span, ctx.pos) {
5079                    consider(
5080                        ctx.best,
5081                        HoverType {
5082                            span: binding.span,
5083                            label: binding.name.as_ref().to_string(),
5084                            typ: tdef.typ.to_string(),
5085                            overloads: Vec::new(),
5086                        },
5087                    );
5088                }
5089                visit(ts, def.as_ref(), tdef.as_ref(), ctx);
5090                visit(ts, body.as_ref(), tbody.as_ref(), ctx);
5091            }
5092            (
5093                Expr::LetRec(_span, bindings, body),
5094                TypedExprKind::LetRec {
5095                    bindings: typed_bindings,
5096                    body: typed_body,
5097                },
5098            ) => {
5099                for ((binding, _ann, def), (_name, typed_def)) in
5100                    bindings.iter().zip(typed_bindings.iter())
5101                {
5102                    if span_contains_pos(binding.span, ctx.pos) {
5103                        consider(
5104                            ctx.best,
5105                            HoverType {
5106                                span: binding.span,
5107                                label: binding.name.as_ref().to_string(),
5108                                typ: typed_def.typ.to_string(),
5109                                overloads: Vec::new(),
5110                            },
5111                        );
5112                    }
5113                    visit(ts, def.as_ref(), typed_def, ctx);
5114                }
5115                visit(ts, body.as_ref(), typed_body.as_ref(), ctx);
5116            }
5117            (
5118                Expr::Lam(_span, _scope, param, _ann, _constraints, body),
5119                TypedExprKind::Lam { body: tbody, .. },
5120            ) => {
5121                if span_contains_pos(param.span, ctx.pos) {
5122                    let param_ty = match typed.typ.as_ref() {
5123                        TypeKind::Fun(a, _b) => a.to_string(),
5124                        _ => "<unknown>".to_string(),
5125                    };
5126                    consider(
5127                        ctx.best,
5128                        HoverType {
5129                            span: param.span,
5130                            label: param.name.as_ref().to_string(),
5131                            typ: param_ty,
5132                            overloads: Vec::new(),
5133                        },
5134                    );
5135                }
5136                visit(ts, body.as_ref(), tbody.as_ref(), ctx);
5137            }
5138            (Expr::Var(v), TypedExprKind::Var { overloads, .. }) => {
5139                if span_contains_pos(v.span, ctx.pos) {
5140                    consider(
5141                        ctx.best,
5142                        HoverType {
5143                            span: v.span,
5144                            label: v.name.as_ref().to_string(),
5145                            typ: typed.typ.to_string(),
5146                            overloads: overloads.iter().map(|t| t.to_string()).collect(),
5147                        },
5148                    );
5149                }
5150            }
5151            (Expr::Ann(_span, inner, _ann), _) => {
5152                visit(ts, inner.as_ref(), typed, ctx);
5153            }
5154            (Expr::Tuple(_span, elems), TypedExprKind::Tuple(telems)) => {
5155                for (e, t) in elems.iter().zip(telems.iter()) {
5156                    visit(ts, e.as_ref(), t, ctx);
5157                }
5158            }
5159            (Expr::List(_span, elems), TypedExprKind::List(telems)) => {
5160                for (e, t) in elems.iter().zip(telems.iter()) {
5161                    visit(ts, e.as_ref(), t, ctx);
5162                }
5163            }
5164            (Expr::Dict(_span, kvs), TypedExprKind::Dict(tkvs)) => {
5165                for (k, v) in kvs {
5166                    if let Some(tv) = tkvs.get(k) {
5167                        visit(ts, v.as_ref(), tv, ctx);
5168                    }
5169                }
5170            }
5171            (
5172                Expr::RecordUpdate(_span, base, updates),
5173                TypedExprKind::RecordUpdate {
5174                    base: tbase,
5175                    updates: tupdates,
5176                },
5177            ) => {
5178                visit(ts, base.as_ref(), tbase.as_ref(), ctx);
5179                for (k, v) in updates {
5180                    if let Some(tv) = tupdates.get(k) {
5181                        visit(ts, v.as_ref(), tv, ctx);
5182                    }
5183                }
5184            }
5185            (Expr::App(_span, f, x), TypedExprKind::App(tf, tx)) => {
5186                visit(ts, f.as_ref(), tf.as_ref(), ctx);
5187                visit(ts, x.as_ref(), tx.as_ref(), ctx);
5188            }
5189            (Expr::Project(_span, e, _field), TypedExprKind::Project { expr: te, .. }) => {
5190                visit(ts, e.as_ref(), te.as_ref(), ctx);
5191            }
5192            (
5193                Expr::Ite(_span, c, t, e),
5194                TypedExprKind::Ite {
5195                    cond,
5196                    then_expr,
5197                    else_expr,
5198                },
5199            ) => {
5200                visit(ts, c.as_ref(), cond.as_ref(), ctx);
5201                visit(ts, t.as_ref(), then_expr.as_ref(), ctx);
5202                visit(ts, e.as_ref(), else_expr.as_ref(), ctx);
5203            }
5204            (
5205                Expr::Match(_span, scrutinee, arms),
5206                TypedExprKind::Match {
5207                    scrutinee: tscrut,
5208                    arms: tarms,
5209                },
5210            ) => {
5211                visit(ts, scrutinee.as_ref(), tscrut.as_ref(), ctx);
5212                for ((_pat, arm_body), (_tpat, tarm_body)) in arms.iter().zip(tarms.iter()) {
5213                    visit(ts, arm_body.as_ref(), tarm_body, ctx);
5214                }
5215            }
5216            _ => {}
5217        }
5218    }
5219
5220    let mut best = None;
5221    let mut ctx = VisitCtx {
5222        pos,
5223        name,
5224        name_span,
5225        name_is_ident,
5226        best: &mut best,
5227    };
5228    visit(ts, expr, typed, &mut ctx);
5229    best
5230}
5231
5232fn name_token_at_position(tokens: &Tokens, position: Position) -> Option<(String, Span, bool)> {
5233    for token in &tokens.items {
5234        let (name, span, is_ident) = match token {
5235            Token::Ident(name, span, ..) => (name.clone(), *span, true),
5236            Token::Add(span) => ("+".to_string(), *span, false),
5237            Token::Sub(span) => ("-".to_string(), *span, false),
5238            Token::Mul(span) => ("*".to_string(), *span, false),
5239            Token::Div(span) => ("/".to_string(), *span, false),
5240            Token::Mod(span) => ("%".to_string(), *span, false),
5241            Token::Concat(span) => ("++".to_string(), *span, false),
5242            Token::Eq(span) => ("==".to_string(), *span, false),
5243            Token::Ne(span) => ("!=".to_string(), *span, false),
5244            Token::Lt(span) => ("<".to_string(), *span, false),
5245            Token::Le(span) => ("<=".to_string(), *span, false),
5246            Token::Gt(span) => (">".to_string(), *span, false),
5247            Token::Ge(span) => (">=".to_string(), *span, false),
5248            Token::And(span) => ("&&".to_string(), *span, false),
5249            Token::Or(span) => ("||".to_string(), *span, false),
5250            _ => continue,
5251        };
5252        if range_touches_position(span_to_range(span), position) {
5253            return Some((name, span, is_ident));
5254        }
5255    }
5256    None
5257}
5258
5259fn push_comment_diagnostics(tokens: &Tokens, diagnostics: &mut Vec<Diagnostic>) {
5260    let mut index = 0;
5261
5262    while index < tokens.items.len() && diagnostics.len() < MAX_DIAGNOSTICS {
5263        match tokens.items[index] {
5264            Token::CommentL(span) => {
5265                let mut cursor = index + 1;
5266                while cursor < tokens.items.len() {
5267                    if matches!(tokens.items[cursor], Token::CommentR(_)) {
5268                        break;
5269                    }
5270                    cursor += 1;
5271                }
5272
5273                if cursor >= tokens.items.len() {
5274                    diagnostics.push(diagnostic_for_span(
5275                        span,
5276                        "Unclosed block comment opener ({-).",
5277                    ));
5278                    break;
5279                }
5280
5281                index = cursor + 1;
5282            }
5283            Token::CommentR(span) => {
5284                diagnostics.push(diagnostic_for_span(
5285                    span,
5286                    "Unmatched block comment closer (-}).",
5287                ));
5288                index += 1;
5289            }
5290            _ => index += 1,
5291        }
5292    }
5293}
5294
5295fn diagnostic_for_span(span: Span, message: impl Into<String>) -> Diagnostic {
5296    Diagnostic {
5297        range: span_to_range(span),
5298        severity: Some(DiagnosticSeverity::ERROR),
5299        message: message.into(),
5300        source: Some("rexlang-lsp".to_string()),
5301        ..Diagnostic::default()
5302    }
5303}
5304
5305fn push_type_diagnostics(
5306    uri: &Url,
5307    text: &str,
5308    program: &Program,
5309    diagnostics: &mut Vec<Diagnostic>,
5310) {
5311    // Type inference is meaningfully more expensive than lex/parse, and we run
5312    // diagnostics on every full-text change. Keep the cost model explicit.
5313    const MAX_TYPECHECK_BYTES: usize = 256 * 1024;
5314    if text.len() > MAX_TYPECHECK_BYTES {
5315        return;
5316    }
5317
5318    let (program, mut ts, _imports, import_diags) = match prepare_program_with_imports(uri, program)
5319    {
5320        Ok(v) => v,
5321        Err(err) => {
5322            diagnostics.push(diagnostic_for_span(primary_program_span(program), err));
5323            return;
5324        }
5325    };
5326    diagnostics.extend(import_diags);
5327    if diagnostics.len() >= MAX_DIAGNOSTICS {
5328        diagnostics.truncate(MAX_DIAGNOSTICS);
5329        return;
5330    }
5331
5332    let (instances, _prepared_target) = match inject_program_decls(&mut ts, &program, None) {
5333        Ok(v) => v,
5334        Err(err) => {
5335            push_ts_error(
5336                err,
5337                diagnostics,
5338                None,
5339                Some(&ts),
5340                Some(primary_program_span(&program)),
5341            );
5342            return;
5343        }
5344    };
5345
5346    // Typecheck instance method bodies too, so errors inside the instance show
5347    // up as diagnostics.
5348    for (decl_idx, prepared) in instances {
5349        if diagnostics.len() >= MAX_DIAGNOSTICS {
5350            break;
5351        }
5352        let Decl::Instance(inst_decl) = &program.decls[decl_idx] else {
5353            continue;
5354        };
5355        for method in &inst_decl.methods {
5356            if let Err(err) = ts.typecheck_instance_method(&prepared, method) {
5357                push_ts_error(
5358                    err,
5359                    diagnostics,
5360                    Some(method.body.as_ref()),
5361                    Some(&ts),
5362                    None,
5363                );
5364                if diagnostics.len() >= MAX_DIAGNOSTICS {
5365                    break;
5366                }
5367            }
5368        }
5369    }
5370
5371    if let Err(err) = ts.infer(program.expr.as_ref()) {
5372        let before = diagnostics.len();
5373        push_ts_error(
5374            err,
5375            diagnostics,
5376            Some(program.expr.as_ref()),
5377            Some(&ts),
5378            None,
5379        );
5380        if let Some(primary) = diagnostics.get(before).cloned() {
5381            push_additional_default_record_update_ambiguity_diagnostics(
5382                program.expr.as_ref(),
5383                &primary.message,
5384                diagnostics,
5385            );
5386        }
5387        return;
5388    }
5389
5390    push_hole_diagnostics(&program, diagnostics);
5391}
5392
5393fn primary_program_span(program: &Program) -> Span {
5394    match program.decls.first() {
5395        Some(Decl::Type(d)) => d.span,
5396        Some(Decl::Fn(d)) => d.span,
5397        Some(Decl::DeclareFn(d)) => d.span,
5398        Some(Decl::Import(d)) => d.span,
5399        Some(Decl::Class(d)) => d.span,
5400        Some(Decl::Instance(d)) => d.span,
5401        None => *program.expr.span(),
5402    }
5403}
5404
5405fn push_hole_diagnostics(program: &Program, diagnostics: &mut Vec<Diagnostic>) {
5406    let mut spans = Vec::new();
5407    collect_hole_spans(program.expr_with_fns().as_ref(), &mut spans);
5408    spans.sort_unstable_by_key(|s| (s.begin.line, s.begin.column, s.end.line, s.end.column));
5409    spans.dedup();
5410
5411    for span in spans {
5412        if diagnostics.len() >= MAX_DIAGNOSTICS {
5413            break;
5414        }
5415        diagnostics.push(Diagnostic {
5416            range: span_to_range(span),
5417            severity: Some(DiagnosticSeverity::ERROR),
5418            message: "typed hole `?` must be filled before evaluation".to_string(),
5419            source: Some("rexlang-typesystem".to_string()),
5420            ..Diagnostic::default()
5421        });
5422    }
5423}
5424
5425fn unknown_var_name(err: &TsTypeError) -> Option<Symbol> {
5426    match err {
5427        TsTypeError::UnknownVar(name) => Some(name.clone()),
5428        TsTypeError::Spanned { error, .. } => unknown_var_name(error),
5429        _ => None,
5430    }
5431}
5432
5433fn field_not_definitely_available_tail(message: &str) -> Option<(&str, &str)> {
5434    let rest = message.strip_prefix("field `")?;
5435    let (field, tail) = rest.split_once('`')?;
5436    tail.contains("is not definitely available on")
5437        .then_some((field, tail))
5438}
5439
5440fn push_additional_default_record_update_ambiguity_diagnostics(
5441    expr: &Expr,
5442    primary_message: &str,
5443    diagnostics: &mut Vec<Diagnostic>,
5444) {
5445    let Some((_field, tail)) = field_not_definitely_available_tail(primary_message) else {
5446        return;
5447    };
5448    let mut updates = Vec::new();
5449    collect_default_record_updates(expr, &mut updates);
5450    for (span, fields) in updates {
5451        if diagnostics.len() >= MAX_DIAGNOSTICS {
5452            break;
5453        }
5454        let Some(field) = fields.first() else {
5455            continue;
5456        };
5457        let message = format!("field `{field}`{tail}");
5458        let range = span_to_range(span);
5459        if diagnostics
5460            .iter()
5461            .any(|d| d.range == range && d.message == message)
5462        {
5463            continue;
5464        }
5465        diagnostics.push(Diagnostic {
5466            range,
5467            severity: Some(DiagnosticSeverity::ERROR),
5468            message,
5469            source: Some("rexlang-typesystem".to_string()),
5470            ..Diagnostic::default()
5471        });
5472    }
5473}
5474
5475fn collect_default_record_updates(expr: &Expr, out: &mut Vec<(Span, Vec<String>)>) {
5476    match expr {
5477        Expr::RecordUpdate(span, base, updates) => {
5478            if matches!(base.as_ref(), Expr::Var(v) if v.name.as_ref() == "default") {
5479                let fields = updates
5480                    .keys()
5481                    .map(|name| name.as_ref().to_string())
5482                    .collect::<Vec<_>>();
5483                if !fields.is_empty() {
5484                    out.push((*span, fields));
5485                }
5486            }
5487            collect_default_record_updates(base, out);
5488            for value in updates.values() {
5489                collect_default_record_updates(value, out);
5490            }
5491        }
5492        Expr::App(_, fun, arg) => {
5493            collect_default_record_updates(fun, out);
5494            collect_default_record_updates(arg, out);
5495        }
5496        Expr::Project(_, base, _) => collect_default_record_updates(base, out),
5497        Expr::Lam(_, _, _, _, _, body) => collect_default_record_updates(body, out),
5498        Expr::Let(_, _, _, def, body) => {
5499            collect_default_record_updates(def, out);
5500            collect_default_record_updates(body, out);
5501        }
5502        Expr::LetRec(_, bindings, body) => {
5503            for (_var, _ann, def) in bindings {
5504                collect_default_record_updates(def, out);
5505            }
5506            collect_default_record_updates(body, out);
5507        }
5508        Expr::Ite(_, cond, then_expr, else_expr) => {
5509            collect_default_record_updates(cond, out);
5510            collect_default_record_updates(then_expr, out);
5511            collect_default_record_updates(else_expr, out);
5512        }
5513        Expr::Match(_, scrutinee, arms) => {
5514            collect_default_record_updates(scrutinee, out);
5515            for (_pat, arm) in arms {
5516                collect_default_record_updates(arm, out);
5517            }
5518        }
5519        Expr::Ann(_, inner, _) => collect_default_record_updates(inner, out),
5520        Expr::Tuple(_, items) | Expr::List(_, items) => {
5521            for item in items {
5522                collect_default_record_updates(item, out);
5523            }
5524        }
5525        Expr::Dict(_, entries) => {
5526            for value in entries.values() {
5527                collect_default_record_updates(value, out);
5528            }
5529        }
5530        Expr::Var(..)
5531        | Expr::Bool(..)
5532        | Expr::Uint(..)
5533        | Expr::Int(..)
5534        | Expr::Float(..)
5535        | Expr::String(..)
5536        | Expr::Uuid(..)
5537        | Expr::DateTime(..)
5538        | Expr::Hole(..) => {}
5539    }
5540}
5541
5542fn find_let_binding_for_def_range(program: &Program, target: Range) -> Option<(String, Position)> {
5543    find_let_binding_for_def_range_in_expr(program.expr_with_fns().as_ref(), target)
5544}
5545
5546fn find_let_binding_for_def_range_in_expr(
5547    expr: &Expr,
5548    target: Range,
5549) -> Option<(String, Position)> {
5550    match expr {
5551        Expr::Let(_, var, ann, def, body) => {
5552            let def_range = span_to_range(*def.span());
5553            if ranges_overlap(def_range, target) && ann.is_none() {
5554                return Some((var.name.as_ref().to_string(), span_to_range(var.span).end));
5555            }
5556            find_let_binding_for_def_range_in_expr(def.as_ref(), target)
5557                .or_else(|| find_let_binding_for_def_range_in_expr(body.as_ref(), target))
5558        }
5559        Expr::LetRec(_, bindings, body) => {
5560            for (var, ann, def) in bindings {
5561                let def_range = span_to_range(*def.span());
5562                if ranges_overlap(def_range, target) && ann.is_none() {
5563                    return Some((var.name.as_ref().to_string(), span_to_range(var.span).end));
5564                }
5565                if let Some(found) = find_let_binding_for_def_range_in_expr(def.as_ref(), target) {
5566                    return Some(found);
5567                }
5568            }
5569            find_let_binding_for_def_range_in_expr(body.as_ref(), target)
5570        }
5571        Expr::App(_, fun, arg) => find_let_binding_for_def_range_in_expr(fun.as_ref(), target)
5572            .or_else(|| find_let_binding_for_def_range_in_expr(arg.as_ref(), target)),
5573        Expr::Project(_, base, _) => find_let_binding_for_def_range_in_expr(base.as_ref(), target),
5574        Expr::RecordUpdate(_, base, updates) => {
5575            find_let_binding_for_def_range_in_expr(base.as_ref(), target).or_else(|| {
5576                updates
5577                    .values()
5578                    .find_map(|expr| find_let_binding_for_def_range_in_expr(expr.as_ref(), target))
5579            })
5580        }
5581        Expr::Lam(_, _, _, _, _, body) => {
5582            find_let_binding_for_def_range_in_expr(body.as_ref(), target)
5583        }
5584        Expr::Ite(_, cond, then_expr, else_expr) => {
5585            find_let_binding_for_def_range_in_expr(cond.as_ref(), target)
5586                .or_else(|| find_let_binding_for_def_range_in_expr(then_expr.as_ref(), target))
5587                .or_else(|| find_let_binding_for_def_range_in_expr(else_expr.as_ref(), target))
5588        }
5589        Expr::Match(_, scrutinee, arms) => {
5590            find_let_binding_for_def_range_in_expr(scrutinee.as_ref(), target).or_else(|| {
5591                arms.iter().find_map(|(_, arm)| {
5592                    find_let_binding_for_def_range_in_expr(arm.as_ref(), target)
5593                })
5594            })
5595        }
5596        Expr::Ann(_, inner, _) => find_let_binding_for_def_range_in_expr(inner.as_ref(), target),
5597        Expr::Tuple(_, items) | Expr::List(_, items) => items
5598            .iter()
5599            .find_map(|item| find_let_binding_for_def_range_in_expr(item.as_ref(), target)),
5600        Expr::Dict(_, entries) => entries
5601            .values()
5602            .find_map(|value| find_let_binding_for_def_range_in_expr(value.as_ref(), target)),
5603        Expr::Var(..)
5604        | Expr::Bool(..)
5605        | Expr::Uint(..)
5606        | Expr::Int(..)
5607        | Expr::Float(..)
5608        | Expr::String(..)
5609        | Expr::Uuid(..)
5610        | Expr::DateTime(..)
5611        | Expr::Hole(..) => None,
5612    }
5613}
5614
5615fn collect_unbound_var_spans(
5616    expr: &Expr,
5617    target: &Symbol,
5618    bound: &mut Vec<Symbol>,
5619    out: &mut Vec<Span>,
5620) {
5621    match expr {
5622        Expr::Var(var) => {
5623            if var.name == *target && !bound.iter().any(|name| name == &var.name) {
5624                out.push(var.span);
5625            }
5626        }
5627        Expr::App(_, fun, arg) => {
5628            collect_unbound_var_spans(fun, target, bound, out);
5629            collect_unbound_var_spans(arg, target, bound, out);
5630        }
5631        Expr::Project(_, base, _) => {
5632            collect_unbound_var_spans(base, target, bound, out);
5633        }
5634        Expr::Lam(_, _scope, param, _ann, _constraints, body) => {
5635            bound.push(param.name.clone());
5636            collect_unbound_var_spans(body, target, bound, out);
5637            bound.pop();
5638        }
5639        Expr::Let(_, var, _ann, def, body) => {
5640            collect_unbound_var_spans(def, target, bound, out);
5641            bound.push(var.name.clone());
5642            collect_unbound_var_spans(body, target, bound, out);
5643            bound.pop();
5644        }
5645        Expr::LetRec(_, bindings, body) => {
5646            let base_len = bound.len();
5647            for (var, _ann, _def) in bindings {
5648                bound.push(var.name.clone());
5649            }
5650            for (_var, _ann, def) in bindings {
5651                collect_unbound_var_spans(def, target, bound, out);
5652            }
5653            collect_unbound_var_spans(body, target, bound, out);
5654            bound.truncate(base_len);
5655        }
5656        Expr::Ite(_, cond, then_expr, else_expr) => {
5657            collect_unbound_var_spans(cond, target, bound, out);
5658            collect_unbound_var_spans(then_expr, target, bound, out);
5659            collect_unbound_var_spans(else_expr, target, bound, out);
5660        }
5661        Expr::Match(_, scrutinee, arms) => {
5662            collect_unbound_var_spans(scrutinee, target, bound, out);
5663            for (pat, arm) in arms {
5664                let base_len = bound.len();
5665                let mut pat_bindings = Vec::new();
5666                collect_pattern_bindings(pat, &mut pat_bindings);
5667                bound.extend(pat_bindings);
5668                collect_unbound_var_spans(arm, target, bound, out);
5669                bound.truncate(base_len);
5670            }
5671        }
5672        Expr::Ann(_, inner, _) => {
5673            collect_unbound_var_spans(inner, target, bound, out);
5674        }
5675        Expr::Tuple(_, items) | Expr::List(_, items) => {
5676            for item in items {
5677                collect_unbound_var_spans(item, target, bound, out);
5678            }
5679        }
5680        Expr::Dict(_, kvs) | Expr::RecordUpdate(_, _, kvs) => {
5681            for expr in kvs.values() {
5682                collect_unbound_var_spans(expr, target, bound, out);
5683            }
5684            if let Expr::RecordUpdate(_, base, _) = expr {
5685                collect_unbound_var_spans(base, target, bound, out);
5686            }
5687        }
5688        Expr::Bool(..)
5689        | Expr::Uint(..)
5690        | Expr::Int(..)
5691        | Expr::Float(..)
5692        | Expr::String(..)
5693        | Expr::Uuid(..)
5694        | Expr::DateTime(..)
5695        | Expr::Hole(..) => {}
5696    }
5697}
5698
5699fn push_ts_error(
5700    err: TsTypeError,
5701    diagnostics: &mut Vec<Diagnostic>,
5702    expr: Option<&Expr>,
5703    ts: Option<&TypeSystem>,
5704    fallback_span: Option<Span>,
5705) {
5706    let unknown_target = unknown_var_name(&err);
5707    let (span, message) = match &err {
5708        TsTypeError::Spanned { span, error } => (*span, error.to_string()),
5709        other => (
5710            fallback_span
5711                .or_else(|| expr.map(|e| *e.span()))
5712                .unwrap_or_default(),
5713            other.to_string(),
5714        ),
5715    };
5716
5717    if let (Some(target), Some(expr), Some(ts)) = (unknown_target, expr, ts)
5718        && ts.env.lookup(&target).is_none()
5719    {
5720        let mut spans = Vec::new();
5721        collect_unbound_var_spans(expr, &target, &mut Vec::new(), &mut spans);
5722        spans.sort_unstable_by_key(|s| (s.begin.line, s.begin.column, s.end.line, s.end.column));
5723        spans.dedup();
5724        if !spans.is_empty() {
5725            for unbound_span in spans {
5726                if diagnostics.len() >= MAX_DIAGNOSTICS {
5727                    break;
5728                }
5729                diagnostics.push(Diagnostic {
5730                    range: span_to_range(unbound_span),
5731                    severity: Some(DiagnosticSeverity::ERROR),
5732                    message: message.clone(),
5733                    source: Some("rexlang-typesystem".to_string()),
5734                    ..Diagnostic::default()
5735                });
5736            }
5737            return;
5738        }
5739    }
5740
5741    diagnostics.push(Diagnostic {
5742        range: span_to_range(span),
5743        severity: Some(DiagnosticSeverity::ERROR),
5744        message,
5745        source: Some("rexlang-typesystem".to_string()),
5746        ..Diagnostic::default()
5747    });
5748}
5749
5750fn range_contains_position(range: Range, position: Position) -> bool {
5751    let after_start = position.line > range.start.line
5752        || (position.line == range.start.line && position.character >= range.start.character);
5753    let before_end = position.line < range.end.line
5754        || (position.line == range.end.line && position.character < range.end.character);
5755    after_start && before_end
5756}
5757
5758fn range_touches_position(range: Range, position: Position) -> bool {
5759    // LSP ranges are end-exclusive, but VS Code often sends positions at the
5760    // *end* of a word (especially for single-character identifiers). For user
5761    // interactions like go-to-definition, treating `position == end` as “still
5762    // on that token” is a better UX trade.
5763    if range_contains_position(range, position) {
5764        return true;
5765    }
5766    if position.line != range.end.line || position.character != range.end.character {
5767        return false;
5768    }
5769    // Exclude the degenerate case (empty range), just in case.
5770    position.line != range.start.line || position.character != range.start.character
5771}
5772
5773fn span_to_range(span: Span) -> Range {
5774    Range {
5775        start: position_from_span(span.begin.line, span.begin.column),
5776        end: position_from_span(span.end.line, span.end.column),
5777    }
5778}
5779
5780fn position_from_span(line: usize, column: usize) -> Position {
5781    Position {
5782        line: line.saturating_sub(1) as u32,
5783        character: column.saturating_sub(1) as u32,
5784    }
5785}
5786
5787fn hover_contents(word: &str) -> Option<HoverContents> {
5788    if let Some(doc) = keyword_doc(word) {
5789        return Some(markdown_hover(word, "keyword", doc));
5790    }
5791
5792    if let Some(doc) = type_doc(word) {
5793        return Some(markdown_hover(word, "type", doc));
5794    }
5795
5796    if let Some(doc) = value_doc(word) {
5797        return Some(markdown_hover(word, "value", doc));
5798    }
5799
5800    None
5801}
5802
5803fn markdown_hover(word: &str, kind: &str, doc: &str) -> HoverContents {
5804    HoverContents::Markup(MarkupContent {
5805        kind: MarkupKind::Markdown,
5806        value: format!("**{}** {}\n\n{}", word, kind, doc),
5807    })
5808}
5809
5810fn keyword_doc(word: &str) -> Option<&'static str> {
5811    match word {
5812        "let" => Some("Introduces local bindings."),
5813        "in" => Some("Begins the expression body for a let binding."),
5814        "type" => Some("Declares a type or ADT."),
5815        "match" => Some("Starts a pattern match expression."),
5816        "when" => Some("Introduces a match arm."),
5817        "if" => Some("Conditional expression keyword."),
5818        "then" => Some("Conditional expression branch."),
5819        "else" => Some("Fallback branch of a conditional expression."),
5820        "as" => Some("Type ascription or aliasing keyword."),
5821        "for" => Some("List/dict comprehension keyword (when supported)."),
5822        "is" => Some("Type assertion keyword."),
5823        _ => None,
5824    }
5825}
5826
5827fn type_doc(word: &str) -> Option<&'static str> {
5828    match word {
5829        "bool" => Some("Boolean type."),
5830        "string" => Some("UTF-8 string type."),
5831        "uuid" => Some("UUID type."),
5832        "datetime" => Some("Datetime type."),
5833        "u8" => Some("Unsigned 8-bit integer."),
5834        "u16" => Some("Unsigned 16-bit integer."),
5835        "u32" => Some("Unsigned 32-bit integer."),
5836        "u64" => Some("Unsigned 64-bit integer."),
5837        "i8" => Some("Signed 8-bit integer."),
5838        "i16" => Some("Signed 16-bit integer."),
5839        "i32" => Some("Signed 32-bit integer."),
5840        "i64" => Some("Signed 64-bit integer."),
5841        "f32" => Some("32-bit float."),
5842        "f64" => Some("64-bit float."),
5843        "List" => Some("List type constructor."),
5844        "Dict" => Some("Dictionary type constructor."),
5845        "Array" => Some("Array type constructor."),
5846        "Option" => Some("Optional type constructor."),
5847        "Result" => Some("Result type constructor."),
5848        _ => None,
5849    }
5850}
5851
5852fn value_doc(word: &str) -> Option<&'static str> {
5853    match word {
5854        "true" => Some("Boolean literal."),
5855        "false" => Some("Boolean literal."),
5856        "null" => Some("Null literal."),
5857        "Some" => Some("Option constructor."),
5858        "None" => Some("Option empty constructor."),
5859        "Ok" => Some("Result success constructor."),
5860        "Err" => Some("Result error constructor."),
5861        _ => None,
5862    }
5863}
5864
5865fn completion_items(uri: &Url, text: &str, position: Position) -> Vec<CompletionItem> {
5866    let field_mode = is_field_completion(text, position);
5867    let base_ident = if field_mode {
5868        field_base_ident(text, position)
5869    } else {
5870        None
5871    };
5872    if let Ok((_tokens, program)) = tokenize_and_parse_cached(uri, text) {
5873        return completion_items_from_program(
5874            &program,
5875            position,
5876            field_mode,
5877            base_ident.as_deref(),
5878            uri,
5879        );
5880    }
5881
5882    completion_items_fallback(text, base_ident.as_deref(), field_mode)
5883}
5884
5885fn completion_items_from_program(
5886    program: &Program,
5887    position: Position,
5888    field_mode: bool,
5889    base_ident: Option<&str>,
5890    uri: &Url,
5891) -> Vec<CompletionItem> {
5892    if field_mode {
5893        if let Some(base_ident) = base_ident
5894            && let Ok(exports) = completion_exports_for_library_alias(uri, program, base_ident)
5895            && !exports.is_empty()
5896        {
5897            return exports
5898                .into_iter()
5899                .map(|label| completion_item(label, CompletionItemKind::FIELD))
5900                .collect();
5901        }
5902        if let Some(fields) = field_completion_for_position(program, position, base_ident) {
5903            return fields
5904                .into_iter()
5905                .map(|label| completion_item(label, CompletionItemKind::FIELD))
5906                .collect();
5907        }
5908        return Vec::new();
5909    }
5910
5911    let mut value_kinds = values_in_scope_at_position(program, position);
5912    let pos = lsp_to_rex_position(position);
5913    for decl in &program.decls {
5914        let Decl::Import(id) = decl else { continue };
5915        if position_in_span(pos, id.span) || position_leq(id.span.end, pos) {
5916            value_kinds
5917                .entry(id.alias.as_ref().to_string())
5918                .or_insert(CompletionItemKind::MODULE);
5919        }
5920    }
5921    for value in BUILTIN_VALUES {
5922        value_kinds
5923            .entry((*value).to_string())
5924            .or_insert(CompletionItemKind::VARIABLE);
5925    }
5926    for (value, kind) in prelude_completion_values() {
5927        value_kinds.entry(value.clone()).or_insert(*kind);
5928    }
5929    for ctor in collect_constructors(program) {
5930        value_kinds
5931            .entry(ctor)
5932            .or_insert(CompletionItemKind::CONSTRUCTOR);
5933    }
5934
5935    let mut type_names = collect_type_names(program);
5936    type_names.extend(BUILTIN_TYPES.iter().map(|value| value.to_string()));
5937
5938    let mut items = Vec::new();
5939    items.extend(
5940        value_kinds
5941            .into_iter()
5942            .map(|(label, kind)| completion_item(label, kind)),
5943    );
5944    items.extend(
5945        type_names
5946            .into_iter()
5947            .map(|label| completion_item(label, CompletionItemKind::CLASS)),
5948    );
5949
5950    items
5951}
5952
5953fn completion_items_fallback(
5954    text: &str,
5955    base_ident: Option<&str>,
5956    field_mode: bool,
5957) -> Vec<CompletionItem> {
5958    let mut identifiers: HashMap<String, CompletionItemKind> = HashMap::new();
5959
5960    if let Ok(tokens) = Token::tokenize(text) {
5961        identifiers.extend(function_defs_from_tokens(&tokens));
5962
5963        let mut index = 0usize;
5964        while index < tokens.items.len() {
5965            if let Token::Ident(name, ..) = &tokens.items[index] {
5966                identifiers
5967                    .entry(name.clone())
5968                    .or_insert(CompletionItemKind::VARIABLE);
5969            }
5970            index += 1;
5971        }
5972
5973        if field_mode {
5974            if let Some(base_ident) = base_ident
5975                && let Some(fields) = fallback_field_map(&tokens).get(base_ident)
5976            {
5977                return fields
5978                    .iter()
5979                    .cloned()
5980                    .map(|label| completion_item(label, CompletionItemKind::FIELD))
5981                    .collect();
5982            }
5983            return Vec::new();
5984        }
5985    }
5986
5987    let mut items: Vec<CompletionItem> = identifiers
5988        .into_iter()
5989        .map(|(label, kind)| completion_item(label, kind))
5990        .collect();
5991    items.extend(
5992        BUILTIN_TYPES
5993            .iter()
5994            .map(|label| completion_item((*label).to_string(), CompletionItemKind::CLASS)),
5995    );
5996    items
5997}
5998
5999fn completion_item(label: String, kind: CompletionItemKind) -> CompletionItem {
6000    CompletionItem {
6001        label,
6002        kind: Some(kind),
6003        ..CompletionItem::default()
6004    }
6005}
6006
6007fn values_in_scope_at_position(
6008    program: &Program,
6009    position: Position,
6010) -> HashMap<String, CompletionItemKind> {
6011    let pos = lsp_to_rex_position(position);
6012    let expr = program.expr_with_fns();
6013    values_in_scope_at_expr(&expr, pos, &mut Vec::new()).unwrap_or_default()
6014}
6015
6016fn values_in_scope_at_expr(
6017    expr: &Expr,
6018    position: RexPosition,
6019    scope: &mut Vec<(String, CompletionItemKind)>,
6020) -> Option<HashMap<String, CompletionItemKind>> {
6021    if !position_in_span(position, *expr.span()) {
6022        return None;
6023    }
6024
6025    fn scope_to_map(scope: &[(String, CompletionItemKind)]) -> HashMap<String, CompletionItemKind> {
6026        // If a name appears multiple times, prefer the most “specific” kind.
6027        // (A function is still a value, but it’s nicer to present it as a function.)
6028        let mut map = HashMap::new();
6029        for (name, kind) in scope {
6030            let slot = map.entry(name.clone()).or_insert(*kind);
6031            if *slot != CompletionItemKind::FUNCTION && *kind == CompletionItemKind::FUNCTION {
6032                *slot = *kind;
6033            }
6034        }
6035        map
6036    }
6037
6038    match expr {
6039        Expr::Let(_span, var, _ann, def, body) => {
6040            if position_in_span(position, *def.span()) {
6041                return values_in_scope_at_expr(def, position, scope)
6042                    .or_else(|| Some(scope_to_map(scope)));
6043            }
6044
6045            if position_in_span(position, *body.span()) {
6046                let kind = matches!(def.as_ref(), Expr::Lam(..))
6047                    .then_some(CompletionItemKind::FUNCTION)
6048                    .unwrap_or(CompletionItemKind::VARIABLE);
6049                scope.push((var.name.to_string(), kind));
6050                let out = values_in_scope_at_expr(body, position, scope)
6051                    .or_else(|| Some(scope_to_map(scope)));
6052                scope.pop();
6053                return out;
6054            }
6055
6056            Some(scope_to_map(scope))
6057        }
6058        Expr::LetRec(_span, bindings, body) => {
6059            let base_len = scope.len();
6060            scope.extend(bindings.iter().map(|(var, _ann, def)| {
6061                let kind = matches!(def.as_ref(), Expr::Lam(..))
6062                    .then_some(CompletionItemKind::FUNCTION)
6063                    .unwrap_or(CompletionItemKind::VARIABLE);
6064                (var.name.to_string(), kind)
6065            }));
6066
6067            for (_, _, def) in bindings {
6068                if position_in_span(position, *def.span()) {
6069                    let out = values_in_scope_at_expr(def, position, scope)
6070                        .or_else(|| Some(scope_to_map(scope)));
6071                    scope.truncate(base_len);
6072                    return out;
6073                }
6074            }
6075
6076            if position_in_span(position, *body.span()) {
6077                let out = values_in_scope_at_expr(body, position, scope)
6078                    .or_else(|| Some(scope_to_map(scope)));
6079                scope.truncate(base_len);
6080                return out;
6081            }
6082
6083            scope.truncate(base_len);
6084            Some(scope_to_map(scope))
6085        }
6086        Expr::Lam(_span, _scope, param, _ann, _constraints, body) => {
6087            if position_in_span(position, *body.span()) {
6088                scope.push((param.name.to_string(), CompletionItemKind::VARIABLE));
6089                let out = values_in_scope_at_expr(body, position, scope)
6090                    .or_else(|| Some(scope_to_map(scope)));
6091                scope.pop();
6092                return out;
6093            }
6094            Some(scope_to_map(scope))
6095        }
6096        Expr::Match(_span, scrutinee, arms) => {
6097            if position_in_span(position, *scrutinee.span()) {
6098                return values_in_scope_at_expr(scrutinee, position, scope)
6099                    .or_else(|| Some(scope_to_map(scope)));
6100            }
6101            for (pattern, arm) in arms {
6102                if position_in_span(position, *pattern.span()) {
6103                    return Some(scope_to_map(scope));
6104                }
6105                if position_in_span(position, *arm.span()) {
6106                    let base_len = scope.len();
6107                    scope.extend(
6108                        pattern_vars(pattern)
6109                            .into_iter()
6110                            .map(|name| (name, CompletionItemKind::VARIABLE)),
6111                    );
6112                    let out = values_in_scope_at_expr(arm, position, scope)
6113                        .or_else(|| Some(scope_to_map(scope)));
6114                    scope.truncate(base_len);
6115                    return out;
6116                }
6117            }
6118            Some(scope_to_map(scope))
6119        }
6120        Expr::App(_span, fun, arg) => {
6121            if position_in_span(position, *fun.span()) {
6122                return values_in_scope_at_expr(fun, position, scope)
6123                    .or_else(|| Some(scope_to_map(scope)));
6124            }
6125            if position_in_span(position, *arg.span()) {
6126                return values_in_scope_at_expr(arg, position, scope)
6127                    .or_else(|| Some(scope_to_map(scope)));
6128            }
6129            Some(scope_to_map(scope))
6130        }
6131        Expr::Project(_span, base, _field) => {
6132            if position_in_span(position, *base.span()) {
6133                return values_in_scope_at_expr(base, position, scope)
6134                    .or_else(|| Some(scope_to_map(scope)));
6135            }
6136            Some(scope_to_map(scope))
6137        }
6138        Expr::Tuple(_span, elems) | Expr::List(_span, elems) => {
6139            for elem in elems {
6140                if position_in_span(position, *elem.span()) {
6141                    return values_in_scope_at_expr(elem, position, scope)
6142                        .or_else(|| Some(scope_to_map(scope)));
6143                }
6144            }
6145            Some(scope_to_map(scope))
6146        }
6147        Expr::Dict(_span, entries) => {
6148            for value in entries.values() {
6149                if position_in_span(position, *value.span()) {
6150                    return values_in_scope_at_expr(value, position, scope)
6151                        .or_else(|| Some(scope_to_map(scope)));
6152                }
6153            }
6154            Some(scope_to_map(scope))
6155        }
6156        Expr::Ite(_span, cond, then_expr, else_expr) => {
6157            if position_in_span(position, *cond.span()) {
6158                return values_in_scope_at_expr(cond, position, scope)
6159                    .or_else(|| Some(scope_to_map(scope)));
6160            }
6161            if position_in_span(position, *then_expr.span()) {
6162                return values_in_scope_at_expr(then_expr, position, scope)
6163                    .or_else(|| Some(scope_to_map(scope)));
6164            }
6165            if position_in_span(position, *else_expr.span()) {
6166                return values_in_scope_at_expr(else_expr, position, scope)
6167                    .or_else(|| Some(scope_to_map(scope)));
6168            }
6169            Some(scope_to_map(scope))
6170        }
6171        Expr::Ann(_span, inner, _ann) => {
6172            if position_in_span(position, *inner.span()) {
6173                return values_in_scope_at_expr(inner, position, scope)
6174                    .or_else(|| Some(scope_to_map(scope)));
6175            }
6176            Some(scope_to_map(scope))
6177        }
6178        _ => Some(scope_to_map(scope)),
6179    }
6180}
6181
6182fn function_defs_from_tokens(tokens: &Tokens) -> HashMap<String, CompletionItemKind> {
6183    // Heuristic fallback when parsing fails: detect `let name = \...` and mark
6184    // `name` as a function completion.
6185    //
6186    // Also detects `fn name ...` declarations.
6187    //
6188    // This keeps completion useful while the user is mid-edit and the AST is
6189    // temporarily invalid.
6190    let mut out = HashMap::new();
6191    let items = &tokens.items;
6192    let mut index = 0usize;
6193
6194    let next_non_ws = |mut i: usize| -> Option<usize> {
6195        while i < items.len() && items[i].is_whitespace() {
6196            i += 1;
6197        }
6198        (i < items.len()).then_some(i)
6199    };
6200
6201    while index < items.len() {
6202        if matches!(items[index], Token::Fn(..)) {
6203            let Some(i) = next_non_ws(index + 1) else {
6204                break;
6205            };
6206            if let Token::Ident(name, ..) = &items[i] {
6207                out.insert(name.clone(), CompletionItemKind::FUNCTION);
6208            }
6209            index += 1;
6210            continue;
6211        }
6212
6213        if !matches!(items[index], Token::Let(..)) {
6214            index += 1;
6215            continue;
6216        }
6217
6218        let Some(mut i) = next_non_ws(index + 1) else {
6219            break;
6220        };
6221
6222        let name = match &items[i] {
6223            Token::Ident(name, ..) => name.clone(),
6224            _ => {
6225                index += 1;
6226                continue;
6227            }
6228        };
6229
6230        // Walk to `=` (skipping whitespace) and then check if the next token is `\` / `λ`.
6231        i += 1;
6232        loop {
6233            let Some(j) = next_non_ws(i) else {
6234                break;
6235            };
6236            match &items[j] {
6237                Token::Assign(..) => {
6238                    let Some(k) = next_non_ws(j + 1) else {
6239                        break;
6240                    };
6241                    if matches!(items[k], Token::BackSlash(..)) {
6242                        out.insert(name, CompletionItemKind::FUNCTION);
6243                    }
6244                    break;
6245                }
6246                Token::SemiColon(..) => break,
6247                _ => i = j + 1,
6248            }
6249        }
6250
6251        index += 1;
6252    }
6253
6254    out
6255}
6256
6257fn collect_type_names(program: &Program) -> BTreeSet<String> {
6258    let mut names = BTreeSet::new();
6259    for decl in &program.decls {
6260        if let Decl::Type(TypeDecl { name, .. }) = decl {
6261            names.insert(name.to_string());
6262        }
6263    }
6264    names
6265}
6266
6267fn collect_constructors(program: &Program) -> BTreeSet<String> {
6268    let mut names = BTreeSet::new();
6269    for decl in &program.decls {
6270        if let Decl::Type(TypeDecl { variants, .. }) = decl {
6271            for variant in variants {
6272                names.insert(variant.name.to_string());
6273            }
6274        }
6275    }
6276    names
6277}
6278
6279fn collect_fields_type_expr(typ: &TypeExpr, fields: &mut BTreeSet<String>) {
6280    match typ {
6281        TypeExpr::Record(_, entries) => {
6282            for (name, _ty) in entries {
6283                fields.insert(name.to_string());
6284            }
6285        }
6286        TypeExpr::App(_, fun, arg) => {
6287            collect_fields_type_expr(fun, fields);
6288            collect_fields_type_expr(arg, fields);
6289        }
6290        TypeExpr::Fun(_, arg, ret) => {
6291            collect_fields_type_expr(arg, fields);
6292            collect_fields_type_expr(ret, fields);
6293        }
6294        TypeExpr::Tuple(_, elems) => {
6295            for elem in elems {
6296                collect_fields_type_expr(elem, fields);
6297            }
6298        }
6299        TypeExpr::Name(..) => {}
6300    }
6301}
6302
6303fn field_completion_for_position(
6304    program: &Program,
6305    position: Position,
6306    base_ident: Option<&str>,
6307) -> Option<BTreeSet<String>> {
6308    let type_fields = type_fields_map(program);
6309    let env = field_env_at_position(program, position, &type_fields);
6310    let pos = lsp_to_rex_position(position);
6311
6312    let expr = program.expr_with_fns();
6313    if let Some(base) = project_base_at_position(&expr, pos)
6314        && let Some(fields) = fields_for_expr(base, &env, &type_fields)
6315    {
6316        return Some(fields);
6317    }
6318
6319    if let Some(base_ident) = base_ident {
6320        if let Some(fields) = env.get(base_ident) {
6321            return Some(fields.clone());
6322        }
6323        if let Some(fields) = type_fields.get(base_ident) {
6324            return Some(fields.clone());
6325        }
6326    }
6327
6328    None
6329}
6330
6331fn type_fields_map(program: &Program) -> HashMap<String, BTreeSet<String>> {
6332    let mut map = HashMap::new();
6333    for decl in &program.decls {
6334        if let Decl::Type(TypeDecl { name, variants, .. }) = decl {
6335            let mut fields = BTreeSet::new();
6336            for variant in variants {
6337                for arg in &variant.args {
6338                    collect_fields_type_expr(arg, &mut fields);
6339                }
6340            }
6341            if !fields.is_empty() {
6342                map.insert(name.to_string(), fields);
6343            }
6344        }
6345    }
6346    map
6347}
6348
6349fn field_env_at_position(
6350    program: &Program,
6351    position: Position,
6352    type_fields: &HashMap<String, BTreeSet<String>>,
6353) -> HashMap<String, BTreeSet<String>> {
6354    let pos = lsp_to_rex_position(position);
6355    let expr = program.expr_with_fns();
6356    field_env_at_expr(&expr, pos, &HashMap::new(), type_fields).unwrap_or_default()
6357}
6358
6359fn field_env_at_expr(
6360    expr: &Expr,
6361    position: RexPosition,
6362    env: &HashMap<String, BTreeSet<String>>,
6363    type_fields: &HashMap<String, BTreeSet<String>>,
6364) -> Option<HashMap<String, BTreeSet<String>>> {
6365    if !position_in_span(position, *expr.span()) {
6366        return None;
6367    }
6368
6369    match expr {
6370        Expr::Let(_, var, ann, def, body) => {
6371            if position_in_span(position, *def.span()) {
6372                return field_env_at_expr(def, position, env, type_fields)
6373                    .or_else(|| Some(env.clone()));
6374            }
6375            if position_in_span(position, *body.span()) {
6376                let mut env_with = env.clone();
6377                let fields = binding_fields(ann.as_ref(), def, type_fields).unwrap_or_default();
6378                env_with.insert(var.name.to_string(), fields);
6379                if let Some(inner) = field_env_at_expr(body, position, &env_with, type_fields) {
6380                    return Some(inner);
6381                }
6382                return Some(env_with);
6383            }
6384            Some(env.clone())
6385        }
6386        Expr::LetRec(_, bindings, body) => {
6387            let mut env_with = env.clone();
6388            for (var, ann, def) in bindings {
6389                let fields = binding_fields(ann.as_ref(), def, type_fields).unwrap_or_default();
6390                env_with.insert(var.name.to_string(), fields);
6391            }
6392            for (_, _, def) in bindings {
6393                if position_in_span(position, *def.span()) {
6394                    return field_env_at_expr(def, position, &env_with, type_fields)
6395                        .or_else(|| Some(env_with.clone()));
6396                }
6397            }
6398            if position_in_span(position, *body.span()) {
6399                if let Some(inner) = field_env_at_expr(body, position, &env_with, type_fields) {
6400                    return Some(inner);
6401                }
6402                return Some(env_with);
6403            }
6404            Some(env.clone())
6405        }
6406        Expr::Lam(_, _scope, param, ann, _constraints, body) => {
6407            if position_in_span(position, *body.span()) {
6408                let mut env_with = env.clone();
6409                let fields = ann
6410                    .as_ref()
6411                    .and_then(|ann| fields_from_type_expr(ann, type_fields))
6412                    .unwrap_or_default();
6413                env_with.insert(param.name.to_string(), fields);
6414                if let Some(inner) = field_env_at_expr(body, position, &env_with, type_fields) {
6415                    return Some(inner);
6416                }
6417                return Some(env_with);
6418            }
6419            Some(env.clone())
6420        }
6421        Expr::Match(_, scrutinee, arms) => {
6422            if position_in_span(position, *scrutinee.span()) {
6423                return field_env_at_expr(scrutinee, position, env, type_fields)
6424                    .or_else(|| Some(env.clone()));
6425            }
6426            for (pattern, arm) in arms {
6427                if position_in_span(position, *pattern.span()) {
6428                    return Some(env.clone());
6429                }
6430                if position_in_span(position, *arm.span()) {
6431                    let mut env_with = env.clone();
6432                    env_with.extend(
6433                        pattern_vars(pattern)
6434                            .into_iter()
6435                            .map(|name| (name, BTreeSet::new())),
6436                    );
6437                    if let Some(inner) = field_env_at_expr(arm, position, &env_with, type_fields) {
6438                        return Some(inner);
6439                    }
6440                    return Some(env_with);
6441                }
6442            }
6443            Some(env.clone())
6444        }
6445        Expr::App(_, fun, arg) => {
6446            if position_in_span(position, *fun.span()) {
6447                return field_env_at_expr(fun, position, env, type_fields)
6448                    .or_else(|| Some(env.clone()));
6449            }
6450            if position_in_span(position, *arg.span()) {
6451                return field_env_at_expr(arg, position, env, type_fields)
6452                    .or_else(|| Some(env.clone()));
6453            }
6454            Some(env.clone())
6455        }
6456        Expr::Project(_, base, _field) => {
6457            if position_in_span(position, *base.span()) {
6458                return field_env_at_expr(base, position, env, type_fields)
6459                    .or_else(|| Some(env.clone()));
6460            }
6461            Some(env.clone())
6462        }
6463        Expr::Tuple(_, elems) | Expr::List(_, elems) => {
6464            for elem in elems {
6465                if position_in_span(position, *elem.span()) {
6466                    return field_env_at_expr(elem, position, env, type_fields)
6467                        .or_else(|| Some(env.clone()));
6468                }
6469            }
6470            Some(env.clone())
6471        }
6472        Expr::Dict(_, entries) => {
6473            for value in entries.values() {
6474                if position_in_span(position, *value.span()) {
6475                    return field_env_at_expr(value, position, env, type_fields)
6476                        .or_else(|| Some(env.clone()));
6477                }
6478            }
6479            Some(env.clone())
6480        }
6481        Expr::Ite(_, cond, then_expr, else_expr) => {
6482            if position_in_span(position, *cond.span()) {
6483                return field_env_at_expr(cond, position, env, type_fields)
6484                    .or_else(|| Some(env.clone()));
6485            }
6486            if position_in_span(position, *then_expr.span()) {
6487                return field_env_at_expr(then_expr, position, env, type_fields)
6488                    .or_else(|| Some(env.clone()));
6489            }
6490            if position_in_span(position, *else_expr.span()) {
6491                return field_env_at_expr(else_expr, position, env, type_fields)
6492                    .or_else(|| Some(env.clone()));
6493            }
6494            Some(env.clone())
6495        }
6496        Expr::Ann(_, inner, _ann) => {
6497            if position_in_span(position, *inner.span()) {
6498                return field_env_at_expr(inner, position, env, type_fields)
6499                    .or_else(|| Some(env.clone()));
6500            }
6501            Some(env.clone())
6502        }
6503        _ => Some(env.clone()),
6504    }
6505}
6506
6507fn binding_fields(
6508    ann: Option<&TypeExpr>,
6509    def: &Expr,
6510    type_fields: &HashMap<String, BTreeSet<String>>,
6511) -> Option<BTreeSet<String>> {
6512    if let Some(ann) = ann
6513        && let Some(fields) = fields_from_type_expr(ann, type_fields)
6514    {
6515        return Some(fields);
6516    }
6517
6518    if let Expr::Ann(_, _inner, ann) = def
6519        && let Some(fields) = fields_from_type_expr(ann, type_fields)
6520    {
6521        return Some(fields);
6522    }
6523
6524    if let Expr::Dict(_, entries) = def {
6525        let fields: BTreeSet<String> = entries.keys().map(|name| name.to_string()).collect();
6526        if !fields.is_empty() {
6527            return Some(fields);
6528        }
6529    }
6530
6531    None
6532}
6533
6534fn fields_from_type_expr(
6535    typ: &TypeExpr,
6536    type_fields: &HashMap<String, BTreeSet<String>>,
6537) -> Option<BTreeSet<String>> {
6538    match typ {
6539        TypeExpr::Record(_, entries) => {
6540            let fields: BTreeSet<String> =
6541                entries.iter().map(|(name, _)| name.to_string()).collect();
6542            if fields.is_empty() {
6543                None
6544            } else {
6545                Some(fields)
6546            }
6547        }
6548        _ => {
6549            if let Some(type_name) = type_name_from_type_expr(typ) {
6550                return type_fields.get(&type_name).cloned();
6551            }
6552            None
6553        }
6554    }
6555}
6556
6557fn type_name_from_type_expr(typ: &TypeExpr) -> Option<String> {
6558    match typ {
6559        TypeExpr::Name(_, name) => Some(name.to_string()),
6560        TypeExpr::App(_, fun, _) => type_name_from_type_expr(fun),
6561        _ => None,
6562    }
6563}
6564
6565fn fields_for_expr(
6566    expr: &Expr,
6567    env: &HashMap<String, BTreeSet<String>>,
6568    type_fields: &HashMap<String, BTreeSet<String>>,
6569) -> Option<BTreeSet<String>> {
6570    match expr {
6571        Expr::Dict(_, entries) => {
6572            let fields: BTreeSet<String> = entries.keys().map(|name| name.to_string()).collect();
6573            if fields.is_empty() {
6574                None
6575            } else {
6576                Some(fields)
6577            }
6578        }
6579        Expr::Var(var) => {
6580            if let Some(fields) = env.get(var.name.as_ref()) {
6581                return Some(fields.clone());
6582            }
6583            if let Some(fields) = type_fields.get(var.name.as_ref()) {
6584                return Some(fields.clone());
6585            }
6586            None
6587        }
6588        Expr::Ann(_, inner, ann) => fields_from_type_expr(ann, type_fields)
6589            .or_else(|| fields_for_expr(inner, env, type_fields)),
6590        Expr::Project(_, base, _) => fields_for_expr(base, env, type_fields),
6591        _ => None,
6592    }
6593}
6594
6595fn project_base_at_position(expr: &Expr, position: RexPosition) -> Option<&Expr> {
6596    if !position_in_span(position, *expr.span()) {
6597        return None;
6598    }
6599
6600    match expr {
6601        Expr::Project(_, base, _) => {
6602            if position_in_span(position, *base.span()) {
6603                return project_base_at_position(base, position);
6604            }
6605            Some(base.as_ref())
6606        }
6607        Expr::Let(_, _var, _ann, def, body) => {
6608            if let Some(found) = project_base_at_position(def, position) {
6609                return Some(found);
6610            }
6611            project_base_at_position(body, position)
6612        }
6613        Expr::LetRec(_, bindings, body) => {
6614            for (_, _, def) in bindings {
6615                if let Some(found) = project_base_at_position(def, position) {
6616                    return Some(found);
6617                }
6618            }
6619            project_base_at_position(body, position)
6620        }
6621        Expr::Lam(_, _scope, _param, _ann, _constraints, body) => {
6622            project_base_at_position(body, position)
6623        }
6624        Expr::Match(_, scrutinee, arms) => {
6625            if let Some(found) = project_base_at_position(scrutinee, position) {
6626                return Some(found);
6627            }
6628            for (_pattern, arm) in arms {
6629                if let Some(found) = project_base_at_position(arm, position) {
6630                    return Some(found);
6631                }
6632            }
6633            None
6634        }
6635        Expr::App(_, fun, arg) => {
6636            if let Some(found) = project_base_at_position(fun, position) {
6637                return Some(found);
6638            }
6639            project_base_at_position(arg, position)
6640        }
6641        Expr::Tuple(_, elems) | Expr::List(_, elems) => {
6642            for elem in elems {
6643                if let Some(found) = project_base_at_position(elem, position) {
6644                    return Some(found);
6645                }
6646            }
6647            None
6648        }
6649        Expr::Dict(_, entries) => {
6650            for value in entries.values() {
6651                if let Some(found) = project_base_at_position(value, position) {
6652                    return Some(found);
6653                }
6654            }
6655            None
6656        }
6657        Expr::Ite(_, cond, then_expr, else_expr) => {
6658            if let Some(found) = project_base_at_position(cond, position) {
6659                return Some(found);
6660            }
6661            if let Some(found) = project_base_at_position(then_expr, position) {
6662                return Some(found);
6663            }
6664            project_base_at_position(else_expr, position)
6665        }
6666        Expr::Ann(_, inner, _ann) => project_base_at_position(inner, position),
6667        _ => None,
6668    }
6669}
6670
6671fn fallback_field_map(tokens: &Tokens) -> HashMap<String, BTreeSet<String>> {
6672    let mut map = HashMap::new();
6673    let items = &tokens.items;
6674    let mut index = 0usize;
6675    while index + 2 < items.len() {
6676        if let Token::Ident(name, ..) = &items[index]
6677            && matches!(items[index + 1], Token::Assign(..) | Token::Colon(..))
6678            && matches!(items[index + 2], Token::BraceL(..))
6679            && let Some((fields, end_index)) = parse_record_fields(items, index + 2)
6680        {
6681            if !fields.is_empty() {
6682                map.insert(name.clone(), fields);
6683            }
6684            index = end_index + 1;
6685            continue;
6686        }
6687        index += 1;
6688    }
6689    map
6690}
6691
6692fn parse_record_fields(tokens: &[Token], start_index: usize) -> Option<(BTreeSet<String>, usize)> {
6693    if !matches!(tokens.get(start_index), Some(Token::BraceL(..))) {
6694        return None;
6695    }
6696
6697    let mut depth = 0usize;
6698    let mut fields = BTreeSet::new();
6699    let mut index = start_index;
6700    while index < tokens.len() {
6701        match &tokens[index] {
6702            Token::BraceL(..) => depth += 1,
6703            Token::BraceR(..) => {
6704                depth = depth.saturating_sub(1);
6705                if depth == 0 {
6706                    return Some((fields, index));
6707                }
6708            }
6709            Token::Ident(name, ..) if depth == 1 => {
6710                if let Some(next) = tokens.get(index + 1)
6711                    && matches!(next, Token::Assign(..) | Token::Colon(..))
6712                {
6713                    fields.insert(name.clone());
6714                }
6715            }
6716            _ => {}
6717        }
6718        index += 1;
6719    }
6720
6721    None
6722}
6723
6724fn field_base_ident(text: &str, position: Position) -> Option<String> {
6725    let offset = offset_at(text, position)?;
6726    if offset == 0 {
6727        return None;
6728    }
6729
6730    let bytes = text.as_bytes();
6731    let mut index = offset.min(bytes.len());
6732
6733    while index > 0 && bytes[index - 1].is_ascii_whitespace() {
6734        index -= 1;
6735    }
6736    while index > 0 && is_word_byte(bytes[index - 1]) {
6737        index -= 1;
6738    }
6739    while index > 0 && bytes[index - 1].is_ascii_whitespace() {
6740        index -= 1;
6741    }
6742
6743    if index == 0 || bytes[index - 1] != b'.' {
6744        return None;
6745    }
6746
6747    index -= 1;
6748    while index > 0 && bytes[index - 1].is_ascii_whitespace() {
6749        index -= 1;
6750    }
6751
6752    let end = index;
6753    while index > 0 && is_word_byte(bytes[index - 1]) {
6754        index -= 1;
6755    }
6756
6757    if index == end {
6758        return None;
6759    }
6760
6761    Some(text[index..end].to_string())
6762}
6763
6764fn is_word_byte(byte: u8) -> bool {
6765    let ch = byte as char;
6766    ch.is_ascii_alphanumeric() || ch == '_'
6767}
6768
6769fn ident_token_at_position(tokens: &Tokens, position: Position) -> Option<(String, Span)> {
6770    for token in &tokens.items {
6771        let Token::Ident(name, span, ..) = token else {
6772            continue;
6773        };
6774        if range_touches_position(span_to_range(*span), position) {
6775            return Some((name.clone(), *span));
6776        }
6777    }
6778    None
6779}
6780
6781fn imported_projection_at_position(
6782    tokens: &Tokens,
6783    position: Position,
6784) -> Option<(String, String)> {
6785    fn is_trivia(token: &Token) -> bool {
6786        matches!(
6787            token,
6788            Token::Whitespace(..) | Token::CommentL(..) | Token::CommentR(..)
6789        )
6790    }
6791
6792    fn prev_non_trivia(tokens: &Tokens, start: usize) -> Option<usize> {
6793        let mut idx = start;
6794        while idx > 0 {
6795            idx -= 1;
6796            if !is_trivia(&tokens.items[idx]) {
6797                return Some(idx);
6798            }
6799        }
6800        None
6801    }
6802
6803    let mut ident_index = None;
6804    let mut field = None;
6805    for (idx, token) in tokens.items.iter().enumerate() {
6806        let Token::Ident(name, span, ..) = token else {
6807            continue;
6808        };
6809        if range_touches_position(span_to_range(*span), position) {
6810            ident_index = Some(idx);
6811            field = Some(name.clone());
6812            break;
6813        }
6814    }
6815    let ident_index = ident_index?;
6816    let field = field?;
6817
6818    let dot_idx = prev_non_trivia(tokens, ident_index)?;
6819    if !matches!(tokens.items[dot_idx], Token::Dot(..)) {
6820        return None;
6821    }
6822    let base_idx = prev_non_trivia(tokens, dot_idx)?;
6823    let Token::Ident(base, ..) = &tokens.items[base_idx] else {
6824        return None;
6825    };
6826
6827    Some((base.clone(), field))
6828}
6829
6830struct DeclSpanIndex {
6831    type_defs: HashMap<String, Span>,
6832    ctor_defs: HashMap<String, Span>,
6833    class_defs: HashMap<String, Span>,
6834    fn_defs: HashMap<String, Span>,
6835    class_method_defs: HashMap<String, Span>,
6836    instance_method_defs: Vec<(Span, HashMap<String, Span>)>,
6837}
6838
6839fn index_decl_spans(program: &Program, tokens: &Tokens) -> DeclSpanIndex {
6840    fn span_contains_span(outer: Span, inner: Span) -> bool {
6841        position_leq(outer.begin, inner.begin) && position_leq(inner.end, outer.end)
6842    }
6843
6844    let mut type_defs = HashMap::new();
6845    let mut ctor_defs = HashMap::new();
6846    let mut class_defs = HashMap::new();
6847    let mut fn_defs = HashMap::new();
6848    let mut class_method_defs = HashMap::new();
6849    let mut instance_method_defs = Vec::new();
6850
6851    for decl in &program.decls {
6852        match decl {
6853            Decl::Type(td) => {
6854                let decl_span = td.span;
6855                let mut expect_type_name = false;
6856                let mut expect_ctor_name = false;
6857
6858                for token in &tokens.items {
6859                    let token_span = *token.span();
6860                    if !span_contains_span(decl_span, token_span) {
6861                        continue;
6862                    }
6863
6864                    match token {
6865                        Token::Type(..) => {
6866                            expect_type_name = true;
6867                            expect_ctor_name = false;
6868                        }
6869                        Token::Ident(name, span, ..) if expect_type_name => {
6870                            type_defs.insert(name.clone(), *span);
6871                            expect_type_name = false;
6872                        }
6873                        Token::Assign(..) | Token::Pipe(..) => {
6874                            expect_ctor_name = true;
6875                        }
6876                        Token::Ident(name, span, ..) if expect_ctor_name => {
6877                            ctor_defs.insert(name.clone(), *span);
6878                            expect_ctor_name = false;
6879                        }
6880                        _ => {}
6881                    }
6882                }
6883            }
6884            Decl::Class(cd) => {
6885                let decl_span = cd.span;
6886                let mut expect_class_name = false;
6887                for i in 0..tokens.items.len() {
6888                    let token = &tokens.items[i];
6889                    let token_span = *token.span();
6890                    if !span_contains_span(decl_span, token_span) {
6891                        continue;
6892                    }
6893                    match token {
6894                        Token::Class(..) => expect_class_name = true,
6895                        Token::Ident(name, span, ..) if expect_class_name => {
6896                            class_defs.insert(name.clone(), *span);
6897                            expect_class_name = false;
6898                        }
6899                        Token::Ident(name, span, ..) => {
6900                            if let Some(next) = tokens.items.get(i + 1)
6901                                && matches!(next, Token::Colon(..))
6902                            {
6903                                class_method_defs.insert(name.clone(), *span);
6904                            }
6905                        }
6906                        _ => {}
6907                    }
6908                }
6909            }
6910            Decl::Instance(id) => {
6911                let decl_span = id.span;
6912                let mut methods = HashMap::new();
6913                for i in 0..tokens.items.len() {
6914                    let token = &tokens.items[i];
6915                    let token_span = *token.span();
6916                    if !span_contains_span(decl_span, token_span) {
6917                        continue;
6918                    }
6919                    if let Token::Ident(name, span, ..) = token
6920                        && let Some(next) = tokens.items.get(i + 1)
6921                        && matches!(next, Token::Assign(..))
6922                    {
6923                        methods.insert(name.clone(), *span);
6924                    }
6925                }
6926                instance_method_defs.push((decl_span, methods));
6927            }
6928            Decl::Fn(fd) => {
6929                fn_defs.insert(fd.name.name.as_ref().to_string(), fd.name.span);
6930            }
6931            Decl::DeclareFn(fd) => {
6932                fn_defs.insert(fd.name.name.as_ref().to_string(), fd.name.span);
6933            }
6934            Decl::Import(..) => {}
6935        }
6936    }
6937
6938    DeclSpanIndex {
6939        type_defs,
6940        ctor_defs,
6941        class_defs,
6942        fn_defs,
6943        class_method_defs,
6944        instance_method_defs,
6945    }
6946}
6947
6948fn definition_span_for_value_ident(
6949    expr: &Expr,
6950    position: RexPosition,
6951    ident: &str,
6952    bindings: &mut Vec<(String, Span)>,
6953    tokens: &Tokens,
6954) -> Option<Span> {
6955    if !position_in_span(position, *expr.span()) {
6956        return None;
6957    }
6958
6959    fn lookup_binding(bindings: &[(String, Span)], ident: &str) -> Option<Span> {
6960        bindings
6961            .iter()
6962            .rev()
6963            .find_map(|(name, span)| (name == ident).then_some(*span))
6964    }
6965
6966    fn definition_in_pattern(
6967        pat: &Pattern,
6968        position: RexPosition,
6969        ident: &str,
6970        _tokens: &Tokens,
6971    ) -> Option<Span> {
6972        if !position_in_span(position, *pat.span()) {
6973            return None;
6974        }
6975
6976        match pat {
6977            Pattern::Var(var) => (var.name.as_ref() == ident).then_some(var.span),
6978            Pattern::Named(_span, _name, args) => args
6979                .iter()
6980                .find_map(|arg| definition_in_pattern(arg, position, ident, _tokens)),
6981            Pattern::Tuple(_span, elems) => elems
6982                .iter()
6983                .find_map(|elem| definition_in_pattern(elem, position, ident, _tokens)),
6984            Pattern::List(_span, elems) => elems
6985                .iter()
6986                .find_map(|elem| definition_in_pattern(elem, position, ident, _tokens)),
6987            Pattern::Cons(_span, head, tail) => {
6988                definition_in_pattern(head, position, ident, _tokens)
6989                    .or_else(|| definition_in_pattern(tail, position, ident, _tokens))
6990            }
6991            Pattern::Dict(_span, fields) => fields
6992                .iter()
6993                .find_map(|(_, p)| definition_in_pattern(p, position, ident, _tokens)),
6994            Pattern::Wildcard(..) => None,
6995        }
6996    }
6997
6998    fn push_pattern_bindings(pat: &Pattern, bindings: &mut Vec<(String, Span)>, _tokens: &Tokens) {
6999        match pat {
7000            Pattern::Var(var) => bindings.push((var.name.to_string(), var.span)),
7001            Pattern::Named(_span, _name, args) => {
7002                for arg in args {
7003                    push_pattern_bindings(arg, bindings, _tokens);
7004                }
7005            }
7006            Pattern::Tuple(_span, elems) => {
7007                for elem in elems {
7008                    push_pattern_bindings(elem, bindings, _tokens);
7009                }
7010            }
7011            Pattern::List(_span, elems) => {
7012                for elem in elems {
7013                    push_pattern_bindings(elem, bindings, _tokens);
7014                }
7015            }
7016            Pattern::Cons(_span, head, tail) => {
7017                push_pattern_bindings(head, bindings, _tokens);
7018                push_pattern_bindings(tail, bindings, _tokens);
7019            }
7020            Pattern::Dict(_span, fields) => {
7021                for (_key, pat) in fields {
7022                    push_pattern_bindings(pat, bindings, _tokens);
7023                }
7024            }
7025            Pattern::Wildcard(..) => {}
7026        }
7027    }
7028
7029    match expr {
7030        Expr::Var(var) => {
7031            if position_in_span(position, var.span) && var.name.as_ref() == ident {
7032                return lookup_binding(bindings, ident);
7033            }
7034            None
7035        }
7036        Expr::Let(_span, var, _ann, def, body) => {
7037            if position_in_span(position, var.span) && var.name.as_ref() == ident {
7038                return Some(var.span);
7039            }
7040
7041            if position_in_span(position, *def.span()) {
7042                return definition_span_for_value_ident(def, position, ident, bindings, tokens);
7043            }
7044            if position_in_span(position, *body.span()) {
7045                bindings.push((var.name.to_string(), var.span));
7046                let out = definition_span_for_value_ident(body, position, ident, bindings, tokens);
7047                bindings.pop();
7048                return out;
7049            }
7050            None
7051        }
7052        Expr::LetRec(_span, rec_bindings, body) => {
7053            for (var, _ann, _def) in rec_bindings {
7054                if position_in_span(position, var.span) && var.name.as_ref() == ident {
7055                    return Some(var.span);
7056                }
7057            }
7058
7059            let base_len = bindings.len();
7060            for (var, _ann, _def) in rec_bindings {
7061                bindings.push((var.name.to_string(), var.span));
7062            }
7063
7064            for (_var, _ann, def) in rec_bindings {
7065                if position_in_span(position, *def.span()) {
7066                    let out =
7067                        definition_span_for_value_ident(def, position, ident, bindings, tokens);
7068                    bindings.truncate(base_len);
7069                    return out;
7070                }
7071            }
7072            if position_in_span(position, *body.span()) {
7073                let out = definition_span_for_value_ident(body, position, ident, bindings, tokens);
7074                bindings.truncate(base_len);
7075                return out;
7076            }
7077
7078            bindings.truncate(base_len);
7079            None
7080        }
7081        Expr::Lam(_span, _scope, param, _ann, _constraints, body) => {
7082            if position_in_span(position, param.span) && param.name.as_ref() == ident {
7083                return Some(param.span);
7084            }
7085
7086            if position_in_span(position, *body.span()) {
7087                bindings.push((param.name.to_string(), param.span));
7088                let out = definition_span_for_value_ident(body, position, ident, bindings, tokens);
7089                bindings.pop();
7090                return out;
7091            }
7092            None
7093        }
7094        Expr::Match(_span, scrutinee, arms) => {
7095            if position_in_span(position, *scrutinee.span()) {
7096                return definition_span_for_value_ident(
7097                    scrutinee, position, ident, bindings, tokens,
7098                );
7099            }
7100
7101            for (pat, arm) in arms {
7102                if position_in_span(position, *pat.span()) {
7103                    return definition_in_pattern(pat, position, ident, tokens);
7104                }
7105
7106                if position_in_span(position, *arm.span()) {
7107                    let base_len = bindings.len();
7108                    push_pattern_bindings(pat, bindings, tokens);
7109                    let out =
7110                        definition_span_for_value_ident(arm, position, ident, bindings, tokens);
7111                    bindings.truncate(base_len);
7112                    return out;
7113                }
7114            }
7115            None
7116        }
7117        Expr::App(_span, fun, arg) => {
7118            if position_in_span(position, *fun.span()) {
7119                return definition_span_for_value_ident(fun, position, ident, bindings, tokens);
7120            }
7121            if position_in_span(position, *arg.span()) {
7122                return definition_span_for_value_ident(arg, position, ident, bindings, tokens);
7123            }
7124            None
7125        }
7126        Expr::Project(_span, base, _field) => {
7127            if position_in_span(position, *base.span()) {
7128                return definition_span_for_value_ident(base, position, ident, bindings, tokens);
7129            }
7130            None
7131        }
7132        Expr::Tuple(_span, elems) | Expr::List(_span, elems) => elems.iter().find_map(|elem| {
7133            position_in_span(position, *elem.span())
7134                .then(|| definition_span_for_value_ident(elem, position, ident, bindings, tokens))
7135                .flatten()
7136        }),
7137        Expr::Dict(_span, entries) => entries.values().find_map(|value| {
7138            position_in_span(position, *value.span())
7139                .then(|| definition_span_for_value_ident(value, position, ident, bindings, tokens))
7140                .flatten()
7141        }),
7142        Expr::Ite(_span, cond, then_expr, else_expr) => {
7143            if position_in_span(position, *cond.span()) {
7144                return definition_span_for_value_ident(cond, position, ident, bindings, tokens);
7145            }
7146            if position_in_span(position, *then_expr.span()) {
7147                return definition_span_for_value_ident(
7148                    then_expr, position, ident, bindings, tokens,
7149                );
7150            }
7151            if position_in_span(position, *else_expr.span()) {
7152                return definition_span_for_value_ident(
7153                    else_expr, position, ident, bindings, tokens,
7154                );
7155            }
7156            None
7157        }
7158        Expr::Ann(_span, inner, _ann) => {
7159            if position_in_span(position, *inner.span()) {
7160                return definition_span_for_value_ident(inner, position, ident, bindings, tokens);
7161            }
7162            None
7163        }
7164        _ => None,
7165    }
7166}
7167
7168// Note: completion uses `values_in_scope_at_position` instead of a plain list of
7169// names so it can classify `fn`/`let name = \...` as `CompletionItemKind::FUNCTION`.
7170
7171fn pattern_vars(pattern: &Pattern) -> Vec<String> {
7172    let mut vars = Vec::new();
7173    collect_pattern_vars(pattern, &mut vars);
7174    vars
7175}
7176
7177fn collect_pattern_vars(pattern: &Pattern, vars: &mut Vec<String>) {
7178    match pattern {
7179        Pattern::Var(var) => vars.push(var.name.to_string()),
7180        Pattern::Named(_, _name, args) => {
7181            for arg in args {
7182                collect_pattern_vars(arg, vars);
7183            }
7184        }
7185        Pattern::Tuple(_, elems) => {
7186            for elem in elems {
7187                collect_pattern_vars(elem, vars);
7188            }
7189        }
7190        Pattern::List(_, elems) => {
7191            for elem in elems {
7192                collect_pattern_vars(elem, vars);
7193            }
7194        }
7195        Pattern::Cons(_, head, tail) => {
7196            collect_pattern_vars(head, vars);
7197            collect_pattern_vars(tail, vars);
7198        }
7199        Pattern::Dict(_, fields) => {
7200            for (_key, pat) in fields {
7201                collect_pattern_vars(pat, vars);
7202            }
7203        }
7204        Pattern::Wildcard(_) => {}
7205    }
7206}
7207
7208fn is_field_completion(text: &str, position: Position) -> bool {
7209    let offset = match offset_at(text, position) {
7210        Some(offset) => offset,
7211        None => return false,
7212    };
7213
7214    if offset == 0 {
7215        return false;
7216    }
7217
7218    let mut start = offset;
7219    while start > 0 {
7220        let prev = text.as_bytes()[start - 1] as char;
7221        if is_word_char(prev) {
7222            start -= 1;
7223            continue;
7224        }
7225        break;
7226    }
7227
7228    if start > 0 && text.as_bytes()[start - 1] as char == '.' {
7229        return true;
7230    }
7231
7232    text.as_bytes()[offset.saturating_sub(1)] as char == '.'
7233}
7234
7235fn lsp_to_rex_position(position: Position) -> RexPosition {
7236    RexPosition::new(position.line as usize + 1, position.character as usize + 1)
7237}
7238
7239fn position_in_span(position: RexPosition, span: Span) -> bool {
7240    position_leq(span.begin, position) && position_leq(position, span.end)
7241}
7242
7243fn position_leq(left: RexPosition, right: RexPosition) -> bool {
7244    left.line < right.line || (left.line == right.line && left.column <= right.column)
7245}
7246
7247fn word_at_position(text: &str, position: Position) -> Option<String> {
7248    let offset = offset_at(text, position)?;
7249    if offset >= text.len() {
7250        return None;
7251    }
7252
7253    let chars: Vec<(usize, char)> = text.char_indices().collect();
7254    let mut idx = None;
7255    for (i, (byte_index, _)) in chars.iter().enumerate() {
7256        if *byte_index == offset {
7257            idx = Some(i);
7258            break;
7259        }
7260    }
7261
7262    let idx = idx?;
7263    if !is_word_char(chars[idx].1) {
7264        return None;
7265    }
7266
7267    let mut start = idx;
7268    while start > 0 && is_word_char(chars[start - 1].1) {
7269        start -= 1;
7270    }
7271
7272    let mut end = idx + 1;
7273    while end < chars.len() && is_word_char(chars[end].1) {
7274        end += 1;
7275    }
7276
7277    let start_byte = chars[start].0;
7278    let end_byte = if end < chars.len() {
7279        chars[end].0
7280    } else {
7281        text.len()
7282    };
7283
7284    Some(text[start_byte..end_byte].to_string())
7285}
7286
7287fn offset_at(text: &str, position: Position) -> Option<usize> {
7288    let mut offset = 0usize;
7289    let mut current_line = 0u32;
7290
7291    for mut line in text.split('\n') {
7292        if line.ends_with('\r') {
7293            line = &line[..line.len().saturating_sub(1)];
7294        }
7295
7296        if current_line == position.line {
7297            let mut remaining = position.character as usize;
7298            for (byte_index, _) in line.char_indices() {
7299                if remaining == 0 {
7300                    return Some(offset + byte_index);
7301                }
7302                remaining -= 1;
7303            }
7304            return Some(offset + line.len());
7305        }
7306
7307        offset += line.len() + 1;
7308        current_line += 1;
7309    }
7310
7311    if current_line == position.line {
7312        Some(offset)
7313    } else {
7314        None
7315    }
7316}
7317
7318fn is_word_char(ch: char) -> bool {
7319    ch.is_ascii_alphanumeric() || ch == '_'
7320}
7321
7322fn in_memory_doc_uri() -> Url {
7323    match Url::parse("inmemory:///docs.rex") {
7324        Ok(url) => url,
7325        Err(_) => panic!("static in-memory URI must parse"),
7326    }
7327}
7328
7329pub fn diagnostics_for_source(source: &str) -> Vec<Diagnostic> {
7330    let uri = in_memory_doc_uri();
7331    clear_parse_cache(&uri);
7332    diagnostics_from_text(&uri, source)
7333}
7334
7335pub fn completion_for_source(source: &str, line: u32, character: u32) -> Vec<CompletionItem> {
7336    let uri = in_memory_doc_uri();
7337    clear_parse_cache(&uri);
7338    completion_items(&uri, source, Position { line, character })
7339}
7340
7341pub fn hover_for_source(source: &str, line: u32, character: u32) -> Option<Hover> {
7342    let uri = in_memory_doc_uri();
7343    clear_parse_cache(&uri);
7344    let position = Position { line, character };
7345    let contents = hover_type_contents(&uri, source, position).or_else(|| {
7346        let word = word_at_position(source, position)?;
7347        hover_contents(&word)
7348    })?;
7349    Some(Hover {
7350        contents,
7351        range: None,
7352    })
7353}
7354
7355pub fn expected_type_for_source_public(source: &str, line: u32, character: u32) -> Option<String> {
7356    let uri = in_memory_doc_uri();
7357    clear_parse_cache(&uri);
7358    expected_type_at_position(&uri, source, Position { line, character })
7359}
7360
7361pub fn functions_producing_expected_type_for_source_public(
7362    source: &str,
7363    line: u32,
7364    character: u32,
7365) -> Vec<String> {
7366    let uri = in_memory_doc_uri();
7367    clear_parse_cache(&uri);
7368    functions_producing_expected_type_at_position(&uri, source, Position { line, character })
7369        .into_iter()
7370        .map(|(name, typ)| format!("{name} : {typ}"))
7371        .collect()
7372}
7373
7374pub fn references_for_source_public(
7375    source: &str,
7376    line: u32,
7377    character: u32,
7378    include_declaration: bool,
7379) -> Vec<Location> {
7380    let uri = in_memory_doc_uri();
7381    clear_parse_cache(&uri);
7382    references_for_source(
7383        &uri,
7384        source,
7385        Position { line, character },
7386        include_declaration,
7387    )
7388}
7389
7390pub fn rename_for_source_public(
7391    source: &str,
7392    line: u32,
7393    character: u32,
7394    new_name: &str,
7395) -> Option<WorkspaceEdit> {
7396    let uri = in_memory_doc_uri();
7397    clear_parse_cache(&uri);
7398    rename_for_source(&uri, source, Position { line, character }, new_name)
7399}
7400
7401pub fn document_symbols_for_source_public(source: &str) -> Vec<DocumentSymbol> {
7402    let uri = in_memory_doc_uri();
7403    clear_parse_cache(&uri);
7404    document_symbols_for_source(&uri, source)
7405}
7406
7407pub fn format_for_source_public(source: &str) -> Option<Vec<TextEdit>> {
7408    format_edits_for_source(source)
7409}
7410
7411pub fn code_actions_for_source_public(
7412    source: &str,
7413    line: u32,
7414    character: u32,
7415) -> Vec<CodeActionOrCommand> {
7416    let uri = in_memory_doc_uri();
7417    clear_parse_cache(&uri);
7418    let position = Position { line, character };
7419    let range = Range {
7420        start: position,
7421        end: position,
7422    };
7423    let diagnostics: Vec<Diagnostic> = diagnostics_from_text(&uri, source)
7424        .into_iter()
7425        .filter(|diag| {
7426            range_contains_position(diag.range, position)
7427                || range_touches_position(diag.range, position)
7428        })
7429        .collect();
7430    code_actions_for_source(&uri, source, range, &diagnostics)
7431}
7432
7433pub fn goto_definition_for_source(source: &str, line: u32, character: u32) -> Option<Location> {
7434    let uri = in_memory_doc_uri();
7435    clear_parse_cache(&uri);
7436    let pos = Position { line, character };
7437    let response = goto_definition_response(&uri, source, pos)?;
7438    match response {
7439        GotoDefinitionResponse::Scalar(location) => Some(location),
7440        GotoDefinitionResponse::Array(locations) => locations.into_iter().next(),
7441        GotoDefinitionResponse::Link(links) => links.into_iter().next().map(|link| Location {
7442            uri: link.target_uri,
7443            range: link.target_range,
7444        }),
7445    }
7446}
7447
7448#[cfg(not(target_arch = "wasm32"))]
7449pub async fn run_stdio() {
7450    let stdin = tokio::io::stdin();
7451    let stdout = tokio::io::stdout();
7452
7453    let (service, socket) = LspService::new(RexServer::new);
7454    Server::new(stdin, stdout, socket).serve(service).await;
7455}
7456
7457#[cfg(test)]
7458mod tests {
7459    use super::*;
7460    use rexlang_core::{Engine, GasMeter, Parser, Token};
7461    use rexlang_engine::{ValueDisplayOptions, pointer_display_with};
7462    use serde_json::Map;
7463    use std::fs;
7464    use std::path::PathBuf;
7465
7466    fn expect_object(value: &Value) -> &Map<String, Value> {
7467        value.as_object().expect("object")
7468    }
7469
7470    fn expect_array_field<'a>(obj: &'a Map<String, Value>, key: &str) -> &'a Vec<Value> {
7471        obj.get(key)
7472            .unwrap_or_else(|| panic!("missing `{key}`"))
7473            .as_array()
7474            .unwrap_or_else(|| panic!("`{key}` should be array"))
7475    }
7476
7477    fn expect_string_field<'a>(obj: &'a Map<String, Value>, key: &str) -> &'a str {
7478        obj.get(key)
7479            .unwrap_or_else(|| panic!("missing `{key}`"))
7480            .as_str()
7481            .unwrap_or_else(|| panic!("`{key}` should be string"))
7482    }
7483
7484    fn temp_dir(name: &str) -> PathBuf {
7485        let mut dir = std::env::temp_dir();
7486        let nonce = std::time::SystemTime::now()
7487            .duration_since(std::time::UNIX_EPOCH)
7488            .expect("clock before epoch")
7489            .as_nanos();
7490        dir.push(format!("rexlang-lsp-test-{name}-{nonce}"));
7491        fs::create_dir_all(&dir).expect("create temp dir");
7492        dir
7493    }
7494
7495    fn assert_internal_name_ref(name: &rexlang_ast::expr::NameRef) {
7496        match name {
7497            rexlang_ast::expr::NameRef::Unqualified(sym) => {
7498                assert!(
7499                    sym.as_ref().starts_with("@m"),
7500                    "expected internal rewritten symbol, got `{sym}`"
7501                );
7502            }
7503            other => panic!("expected unqualified rewritten name, got {other:?}"),
7504        }
7505    }
7506
7507    async fn eval_source_to_display(code: &str) -> (String, String) {
7508        let tokens = Token::tokenize(code).expect("tokenize source");
7509        let mut parser = Parser::new(tokens);
7510        let program = parser
7511            .parse_program(&mut GasMeter::default())
7512            .expect("parse source");
7513        let mut engine = Engine::with_prelude(()).expect("build engine");
7514        engine.inject_decls(&program.decls).expect("inject decls");
7515        let (ptr, ty) = rexlang_engine::Evaluator::new_with_compiler(
7516            rexlang_engine::RuntimeEnv::new(engine.clone()),
7517            rexlang_engine::Compiler::new(engine.clone()),
7518        )
7519        .eval(program.expr.as_ref(), &mut GasMeter::default())
7520        .await
7521        .expect("evaluate source");
7522        let display = pointer_display_with(
7523            &engine.heap,
7524            &ptr,
7525            ValueDisplayOptions {
7526                include_numeric_suffixes: true,
7527                ..ValueDisplayOptions::default()
7528            },
7529        )
7530        .expect("display value");
7531        (display, ty.to_string())
7532    }
7533
7534    #[test]
7535    fn stdlib_imports_typecheck_for_non_file_uri() {
7536        let uri = Url::parse("untitled:Test.rex").expect("uri");
7537        let text = r#"
7538import std.io
7539import std.process
7540
7541let _ = io.debug "hi" in
7542let p = process.spawn { cmd = "sh", args = ["-c"] } in
7543process.wait p
7544"#;
7545
7546        let diags = diagnostics_from_text(&uri, text);
7547        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
7548    }
7549
7550    #[test]
7551    fn prepare_program_rewrites_imported_type_refs_in_annotations() {
7552        let dir = temp_dir("prepare_program_rewrites_imported_type_refs_in_annotations");
7553        let main = dir.join("main.rex");
7554        let dep = dir.join("dep.rex");
7555        fs::write(
7556            &dep,
7557            r#"
7558pub type Boxed = Boxed i32
7559"#,
7560        )
7561        .expect("write dep");
7562        fs::write(&main, "()").expect("write main");
7563
7564        let uri = Url::from_file_path(&main).expect("main file uri");
7565        let source = r#"
7566import dep as D
7567
7568let x : D.Boxed = D.Boxed 1 in
7569x is D.Boxed
7570"#;
7571        let tokens = Token::tokenize(source).expect("tokenize");
7572        let mut parser = Parser::new(tokens);
7573        let program = parser
7574            .parse_program(&mut GasMeter::default())
7575            .expect("parse");
7576        let (rewritten, _ts, _imports, diags) =
7577            prepare_program_with_imports(&uri, &program).expect("prepare");
7578        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
7579
7580        let Expr::Let(_, _, Some(let_ann), _, body) = rewritten.expr.as_ref() else {
7581            panic!("expected rewritten let expression");
7582        };
7583        if let TypeExpr::Name(_, name) = let_ann {
7584            assert_internal_name_ref(name);
7585        } else {
7586            panic!("expected rewritten let annotation");
7587        }
7588
7589        let Expr::Ann(_, _, ann_ty) = body.as_ref() else {
7590            panic!("expected rewritten trailing annotation");
7591        };
7592        if let TypeExpr::Name(_, name) = ann_ty {
7593            assert_internal_name_ref(name);
7594        } else {
7595            panic!("expected rewritten annotation type");
7596        }
7597
7598        if let Expr::Let(_, _, _, def, _) = rewritten.expr.as_ref()
7599            && let Expr::App(_, ctor, _) = def.as_ref()
7600            && let Expr::Var(v) = ctor.as_ref()
7601        {
7602            assert!(
7603                v.name.as_ref().starts_with("@m"),
7604                "expected constructor projection rewrite to internal symbol"
7605            );
7606        } else {
7607            panic!("expected rewritten constructor application");
7608        }
7609    }
7610
7611    #[test]
7612    fn prepare_program_rewrites_imported_class_refs_in_instance_headers() {
7613        let dir = temp_dir("prepare_program_rewrites_imported_class_refs_in_instance_headers");
7614        let main = dir.join("main.rex");
7615        let dep = dir.join("dep.rex");
7616        fs::write(
7617            &dep,
7618            r#"
7619pub class Pick a where
7620    pick : a
7621()
7622"#,
7623        )
7624        .expect("write dep");
7625        fs::write(&main, "()").expect("write main");
7626
7627        let uri = Url::from_file_path(&main).expect("main file uri");
7628        let source = r#"
7629import dep as D
7630
7631instance D.Pick i32 where
7632    pick = 7
7633
7634pick is i32
7635"#;
7636        let tokens = Token::tokenize(source).expect("tokenize");
7637        let mut parser = Parser::new(tokens);
7638        let program = parser
7639            .parse_program(&mut GasMeter::default())
7640            .expect("parse");
7641        let (rewritten, _ts, _imports, diags) =
7642            prepare_program_with_imports(&uri, &program).expect("prepare");
7643        assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
7644
7645        let Some(inst) = rewritten.decls.iter().find_map(|decl| match decl {
7646            Decl::Instance(inst) => Some(inst),
7647            _ => None,
7648        }) else {
7649            panic!("expected instance declaration");
7650        };
7651        assert!(
7652            inst.class.as_ref().starts_with("@m"),
7653            "expected rewritten internal class symbol, got `{}`",
7654            inst.class
7655        );
7656    }
7657
7658    #[test]
7659    fn diagnostics_report_missing_class_export_in_instance_header() {
7660        let dir = temp_dir("diagnostics_report_missing_class_export_in_instance_header");
7661        let main = dir.join("main.rex");
7662        let dep = dir.join("dep.rex");
7663        fs::write(
7664            &dep,
7665            r#"
7666pub class Present a where
7667    present : a
7668()
7669"#,
7670        )
7671        .expect("write dep");
7672        fs::write(&main, "()").expect("write main");
7673
7674        let uri = Url::from_file_path(&main).expect("main file uri");
7675        let source = r#"
7676import dep as D
7677
7678instance D.Missing i32 where
7679    missing = 1
7680
76810
7682"#;
7683        let diags = diagnostics_from_text(&uri, source);
7684        assert!(
7685            diags
7686                .iter()
7687                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7688            "diagnostics: {diags:#?}"
7689        );
7690    }
7691
7692    #[test]
7693    fn diagnostics_report_missing_type_export_in_annotation() {
7694        let dir = temp_dir("diagnostics_report_missing_type_export_in_annotation");
7695        let main = dir.join("main.rex");
7696        let dep = dir.join("dep.rex");
7697        fs::write(
7698            &dep,
7699            r#"
7700pub type Present = Present i32
7701()
7702"#,
7703        )
7704        .expect("write dep");
7705        fs::write(&main, "()").expect("write main");
7706
7707        let uri = Url::from_file_path(&main).expect("main file uri");
7708        let source = r#"
7709import dep as D
7710
7711fn id x: D.Missing -> D.Missing = x
7712
77130
7714"#;
7715        let diags = diagnostics_from_text(&uri, source);
7716        assert!(
7717            diags
7718                .iter()
7719                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7720            "diagnostics: {diags:#?}"
7721        );
7722    }
7723
7724    #[test]
7725    fn diagnostics_report_missing_type_export_in_instance_head() {
7726        let dir = temp_dir("diagnostics_report_missing_type_export_in_instance_head");
7727        let main = dir.join("main.rex");
7728        let dep = dir.join("dep.rex");
7729        fs::write(
7730            &dep,
7731            r#"
7732pub class Marker a where
7733    marker : i32
7734()
7735"#,
7736        )
7737        .expect("write dep");
7738        fs::write(&main, "()").expect("write main");
7739
7740        let uri = Url::from_file_path(&main).expect("main file uri");
7741        let source = r#"
7742import dep as D
7743
7744instance D.Marker D.Missing where
7745    marker = 1
7746
77470
7748"#;
7749        let diags = diagnostics_from_text(&uri, source);
7750        assert!(
7751            diags
7752                .iter()
7753                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7754            "diagnostics: {diags:#?}"
7755        );
7756    }
7757
7758    #[test]
7759    fn diagnostics_report_missing_class_export_in_fn_where_constraint() {
7760        let dir = temp_dir("diagnostics_report_missing_class_export_in_fn_where_constraint");
7761        let main = dir.join("main.rex");
7762        let dep = dir.join("dep.rex");
7763        fs::write(
7764            &dep,
7765            r#"
7766pub class Present a where
7767    present : a
7768()
7769"#,
7770        )
7771        .expect("write dep");
7772        fs::write(&main, "()").expect("write main");
7773
7774        let uri = Url::from_file_path(&main).expect("main file uri");
7775        let source = r#"
7776import dep as D
7777
7778fn id x: i32 -> i32 where D.Missing i32 = x
7779
77800
7781"#;
7782        let diags = diagnostics_from_text(&uri, source);
7783        assert!(
7784            diags
7785                .iter()
7786                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7787            "diagnostics: {diags:#?}"
7788        );
7789    }
7790
7791    #[test]
7792    fn diagnostics_report_missing_class_export_in_declare_fn_where_constraint() {
7793        let dir =
7794            temp_dir("diagnostics_report_missing_class_export_in_declare_fn_where_constraint");
7795        let main = dir.join("main.rex");
7796        let dep = dir.join("dep.rex");
7797        fs::write(
7798            &dep,
7799            r#"
7800pub class Present a where
7801    present : a
7802()
7803"#,
7804        )
7805        .expect("write dep");
7806        fs::write(&main, "()").expect("write main");
7807
7808        let uri = Url::from_file_path(&main).expect("main file uri");
7809        let source = r#"
7810import dep as D
7811
7812declare fn id x: i32 -> i32 where D.Missing i32
7813
78140
7815"#;
7816        let diags = diagnostics_from_text(&uri, source);
7817        assert!(
7818            diags
7819                .iter()
7820                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7821            "diagnostics: {diags:#?}"
7822        );
7823    }
7824
7825    #[test]
7826    fn diagnostics_report_missing_class_export_in_class_super_constraint() {
7827        let dir = temp_dir("diagnostics_report_missing_class_export_in_class_super_constraint");
7828        let main = dir.join("main.rex");
7829        let dep = dir.join("dep.rex");
7830        fs::write(
7831            &dep,
7832            r#"
7833pub class Present a where
7834    present : a
7835()
7836"#,
7837        )
7838        .expect("write dep");
7839        fs::write(&main, "()").expect("write main");
7840
7841        let uri = Url::from_file_path(&main).expect("main file uri");
7842        let source = r#"
7843import dep as D
7844
7845class Local a <= D.Missing a where
7846    local : a
7847
78480
7849"#;
7850        let diags = diagnostics_from_text(&uri, source);
7851        assert!(
7852            diags
7853                .iter()
7854                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7855            "diagnostics: {diags:#?}"
7856        );
7857    }
7858
7859    #[test]
7860    fn diagnostics_allow_lambda_param_named_like_import_alias_in_annotation() {
7861        let dir = temp_dir("diagnostics_allow_lambda_param_named_like_import_alias_in_annotation");
7862        let main = dir.join("main.rex");
7863        let dep = dir.join("dep.rex");
7864        fs::write(
7865            &dep,
7866            r#"
7867pub type Boxed = Boxed i32
7868()
7869"#,
7870        )
7871        .expect("write dep");
7872        fs::write(&main, "()").expect("write main");
7873
7874        let uri = Url::from_file_path(&main).expect("main file uri");
7875        let source = r#"
7876import dep as D
7877
7878let f = \ (D : D.Boxed) -> 0 in
78790
7880"#;
7881        let diags = diagnostics_from_text(&uri, source);
7882        assert!(diags.is_empty(), "diagnostics: {diags:#?}");
7883    }
7884
7885    #[test]
7886    fn diagnostics_report_missing_type_export_in_letrec_annotation_with_alias_named_binding() {
7887        let dir = temp_dir(
7888            "diagnostics_report_missing_type_export_in_letrec_annotation_with_alias_named_binding",
7889        );
7890        let main = dir.join("main.rex");
7891        let dep = dir.join("dep.rex");
7892        fs::write(
7893            &dep,
7894            r#"
7895pub type Present = Present i32
7896()
7897"#,
7898        )
7899        .expect("write dep");
7900        fs::write(&main, "()").expect("write main");
7901
7902        let uri = Url::from_file_path(&main).expect("main file uri");
7903        let source = r#"
7904import dep as D
7905
7906let rec D: D.Missing = 1 in
79070
7908"#;
7909        let diags = diagnostics_from_text(&uri, source);
7910        assert!(
7911            diags
7912                .iter()
7913                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
7914            "diagnostics: {diags:#?}"
7915        );
7916    }
7917
7918    #[test]
7919    fn diagnostics_allow_letrec_annotation_with_alias_named_binding_for_valid_type() {
7920        let dir =
7921            temp_dir("diagnostics_allow_letrec_annotation_with_alias_named_binding_for_valid_type");
7922        let main = dir.join("main.rex");
7923        let dep = dir.join("dep.rex");
7924        fs::write(
7925            &dep,
7926            r#"
7927pub type Num = Num i32
7928()
7929"#,
7930        )
7931        .expect("write dep");
7932        fs::write(&main, "()").expect("write main");
7933
7934        let uri = Url::from_file_path(&main).expect("main file uri");
7935        let source = r#"
7936import dep as D
7937import dep (Num)
7938
7939let rec D: D.Num -> i32 = \_ -> 0 in
79400
7941"#;
7942        let diags = diagnostics_from_text(&uri, source);
7943        assert!(diags.is_empty(), "diagnostics: {diags:#?}");
7944    }
7945
7946    #[test]
7947    fn diagnostics_allow_let_annotation_with_alias_named_binding_for_valid_type() {
7948        let dir =
7949            temp_dir("diagnostics_allow_let_annotation_with_alias_named_binding_for_valid_type");
7950        let main = dir.join("main.rex");
7951        let dep = dir.join("dep.rex");
7952        fs::write(
7953            &dep,
7954            r#"
7955pub type Num = Num i32
7956()
7957"#,
7958        )
7959        .expect("write dep");
7960        fs::write(&main, "()").expect("write main");
7961
7962        let uri = Url::from_file_path(&main).expect("main file uri");
7963        let source = r#"
7964import dep as D
7965
7966let D: D.Num -> i32 = \_ -> 0 in
79670
7968"#;
7969        let diags = diagnostics_from_text(&uri, source);
7970        assert!(diags.is_empty(), "diagnostics: {diags:#?}");
7971    }
7972
7973    #[test]
7974    fn diagnostics_report_missing_type_export_in_let_annotation_with_alias_named_binding() {
7975        let dir = temp_dir(
7976            "diagnostics_report_missing_type_export_in_let_annotation_with_alias_named_binding",
7977        );
7978        let main = dir.join("main.rex");
7979        let dep = dir.join("dep.rex");
7980        fs::write(
7981            &dep,
7982            r#"
7983pub type Present = Present i32
7984()
7985"#,
7986        )
7987        .expect("write dep");
7988        fs::write(&main, "()").expect("write main");
7989
7990        let uri = Url::from_file_path(&main).expect("main file uri");
7991        let source = r#"
7992import dep as D
7993
7994let D: D.Missing = 1 in
79950
7996"#;
7997        let diags = diagnostics_from_text(&uri, source);
7998        assert!(
7999            diags
8000                .iter()
8001                .any(|d| d.message.contains("does not export") && d.message.contains("Missing")),
8002            "diagnostics: {diags:#?}"
8003        );
8004    }
8005
8006    #[test]
8007    fn reports_all_unknown_var_usages() {
8008        let text = r#"
8009let
8010  f = \x -> missing + x
8011in
8012  missing + (f missing)
8013"#;
8014
8015        let diags = diagnostics_for_source(text);
8016        let missing_diags = diags
8017            .iter()
8018            .filter(|d| d.message.contains("unbound variable") && d.message.contains("missing"))
8019            .count();
8020        assert_eq!(missing_diags, 3, "diagnostics: {diags:#?}");
8021    }
8022
8023    #[test]
8024    fn diagnostics_report_typed_hole_error() {
8025        let text = "let y : i32 = ? in y";
8026        let diags = diagnostics_for_source(text);
8027        assert!(
8028            diags.iter().any(|d| d
8029                .message
8030                .contains("typed hole `?` must be filled before evaluation")),
8031            "diagnostics: {diags:#?}"
8032        );
8033    }
8034
8035    #[test]
8036    fn diagnostics_report_both_default_record_update_ambiguities() {
8037        let text = r#"
8038type A = A { x: i32, y: i32 }
8039type B = B { x: i32, y: i32 }
8040
8041instance Default A
8042    default = A { x = 1, y = 2 }
8043
8044instance Default B
8045    default = B { x = 10, y = 20 }
8046
8047let
8048    a = { default with { x = 9 } },
8049    b = { default with { y = 8 } }
8050in
8051    (a, b)
8052"#;
8053        let diags = diagnostics_for_source(text);
8054        let field_diags: Vec<&Diagnostic> = diags
8055            .iter()
8056            .filter(|d| d.message.contains("is not definitely available on"))
8057            .collect();
8058        assert_eq!(field_diags.len(), 2, "diagnostics: {diags:#?}");
8059        assert!(
8060            field_diags.iter().any(|d| d.message.contains("field `x`")),
8061            "diagnostics: {diags:#?}"
8062        );
8063        assert!(
8064            field_diags.iter().any(|d| d.message.contains("field `y`")),
8065            "diagnostics: {diags:#?}"
8066        );
8067    }
8068
8069    #[tokio::test]
8070    async fn e2e_ambiguous_default_record_updates_two_quick_fix_styles_then_eval() {
8071        let text = r#"type A = A { x: i32, y: i32 }
8072type B = B { x: i32, y: i32 }
8073
8074instance Default A
8075    default = A { x = 1, y = 2 }
8076
8077instance Default B
8078    default = B { x = 10, y = 20 }
8079
8080let
8081    a = { default with { x = 9 } },
8082    b = { default with { y = 8 } }
8083in
8084    (a, b)
8085"#;
8086        let uri = in_memory_doc_uri();
8087        clear_parse_cache(&uri);
8088
8089        let mut field_diags: Vec<Diagnostic> = diagnostics_from_text(&uri, text)
8090            .into_iter()
8091            .filter(|diag| diag.message.contains("is not definitely available on"))
8092            .collect();
8093        field_diags.sort_by_key(|diag| {
8094            (
8095                diag.range.start.line,
8096                diag.range.start.character,
8097                diag.range.end.line,
8098                diag.range.end.character,
8099                diag.message.clone(),
8100            )
8101        });
8102        assert_eq!(field_diags.len(), 2, "diagnostics: {field_diags:#?}");
8103        assert_eq!(
8104            field_diags[0].message,
8105            "field `x` is not definitely available on 'a"
8106        );
8107        assert_eq!(
8108            field_diags[0].range,
8109            Range {
8110                start: Position {
8111                    line: 10,
8112                    character: 8,
8113                },
8114                end: Position {
8115                    line: 10,
8116                    character: 34,
8117                },
8118            }
8119        );
8120        assert_eq!(
8121            field_diags[1].message,
8122            "field `y` is not definitely available on 'a"
8123        );
8124        assert_eq!(
8125            field_diags[1].range,
8126            Range {
8127                start: Position {
8128                    line: 11,
8129                    character: 8,
8130                },
8131                end: Position {
8132                    line: 11,
8133                    character: 34,
8134                },
8135            }
8136        );
8137
8138        let step_a = execute_semantic_loop_step(
8139            &uri,
8140            text,
8141            Position {
8142                line: 10,
8143                character: 25,
8144            },
8145        )
8146        .expect("step for a");
8147        let quick_fix_a = step_a
8148            .get("quickFixes")
8149            .and_then(Value::as_array)
8150            .and_then(|items| {
8151                items.iter().find(|item| {
8152                    item.get("title").and_then(Value::as_str)
8153                        == Some("Disambiguate `default` as `A`")
8154                })
8155            })
8156            .cloned()
8157            .expect("quick fix for a using `is`");
8158        let edit_a: WorkspaceEdit =
8159            serde_json::from_value(quick_fix_a.get("edit").cloned().expect("edit for a"))
8160                .expect("workspace edit for a");
8161        let after_a = apply_workspace_edit_to_text(&uri, text, &edit_a).expect("apply edit for a");
8162
8163        let step_b = execute_semantic_loop_step(
8164            &uri,
8165            &after_a,
8166            Position {
8167                line: 11,
8168                character: 25,
8169            },
8170        )
8171        .expect("step for b");
8172        let quick_fix_b = step_b
8173            .get("quickFixes")
8174            .and_then(Value::as_array)
8175            .and_then(|items| {
8176                items.iter().find(|item| {
8177                    item.get("title").and_then(Value::as_str) == Some("Annotate `b` as `B`")
8178                })
8179            })
8180            .cloned()
8181            .expect("quick fix for b using let annotation");
8182        let edit_b: WorkspaceEdit =
8183            serde_json::from_value(quick_fix_b.get("edit").cloned().expect("edit for b"))
8184                .expect("workspace edit for b");
8185        let after_b =
8186            apply_workspace_edit_to_text(&uri, &after_a, &edit_b).expect("apply edit for b");
8187
8188        let diagnostics_after = diagnostics_from_text(&uri, &after_b);
8189        assert!(
8190            diagnostics_after.is_empty(),
8191            "unexpected diagnostics after fixes: {diagnostics_after:#?}\nupdated=\n{after_b}"
8192        );
8193
8194        let (value, ty) = eval_source_to_display(&after_b).await;
8195        assert_eq!(value, "(A {x = 9i32, y = 2i32}, B {x = 10i32, y = 8i32})");
8196        assert_eq!(ty, "(A, B)");
8197    }
8198
8199    #[test]
8200    fn diagnostics_for_decl_type_errors_are_not_whole_document() {
8201        let text = r#"
8202fn parse_ph : string -> Result string f64 = \raw ->
8203  if raw == "7.3" then Ok 7.3 else Err "bad reading"
8204"#;
8205        let uri = in_memory_doc_uri();
8206        clear_parse_cache(&uri);
8207        let diags = diagnostics_from_text(&uri, text);
8208        let full = full_document_range(text);
8209        let unification = diags
8210            .iter()
8211            .find(|d| d.message.contains("types do not unify"))
8212            .expect("unification diagnostic");
8213        assert_ne!(
8214            unification.range, full,
8215            "diagnostic unexpectedly spans whole document: {unification:#?}"
8216        );
8217    }
8218
8219    #[test]
8220    fn references_find_all_usages() {
8221        let text = r#"
8222let
8223  x = 1
8224in
8225  x + x
8226"#;
8227        let refs = references_for_source_public(text, 4, 2, true);
8228        assert_eq!(refs.len(), 3, "refs: {refs:#?}");
8229    }
8230
8231    #[test]
8232    fn rename_returns_workspace_edit() {
8233        let text = r#"
8234let
8235  x = 1
8236in
8237  x + x
8238"#;
8239        let edit = rename_for_source_public(text, 4, 2, "value").expect("rename edit");
8240        let changes = edit.changes.expect("changes map");
8241        let uri = Url::parse("inmemory:///docs.rex").expect("uri");
8242        let edits = changes.get(&uri).expect("uri edits");
8243        assert_eq!(edits.len(), 3, "edits: {edits:#?}");
8244    }
8245
8246    #[test]
8247    fn document_symbols_returns_top_level_items() {
8248        let text = r#"
8249type T = A | B
8250fn f : i32 -> i32 = \x -> x + 1
8251let x = 0 in f x
8252"#;
8253        let symbols = document_symbols_for_source_public(text);
8254        assert!(
8255            symbols.iter().any(|s| s.name == "T"),
8256            "symbols: {symbols:#?}"
8257        );
8258        assert!(
8259            symbols.iter().any(|s| s.name == "f"),
8260            "symbols: {symbols:#?}"
8261        );
8262    }
8263
8264    #[test]
8265    fn formatter_returns_text_edit() {
8266        let text = "let x = 1   \n";
8267        let edits = format_for_source_public(text).expect("format edits");
8268        assert_eq!(edits.len(), 1);
8269        assert_eq!(edits[0].new_text, "let x = 1\n");
8270    }
8271
8272    #[test]
8273    fn code_actions_offer_unknown_var_replacement() {
8274        let text = r#"
8275let
8276  x = 1
8277in
8278  y + x
8279"#;
8280        let actions = code_actions_for_source_public(text, 4, 2);
8281        let titles: Vec<String> = actions
8282            .into_iter()
8283            .filter_map(|action| match action {
8284                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8285                CodeActionOrCommand::Command(_) => None,
8286            })
8287            .collect();
8288        assert!(
8289            titles
8290                .iter()
8291                .any(|title| title.contains("Replace `y` with `x`")),
8292            "titles: {titles:#?}"
8293        );
8294    }
8295
8296    #[test]
8297    fn code_actions_offer_list_wrap_fix() {
8298        let text = r#"
8299let
8300  xs : List i32 = 1
8301in
8302  xs
8303"#;
8304        let uri = in_memory_doc_uri();
8305        clear_parse_cache(&uri);
8306        let diagnostics = diagnostics_from_text(&uri, text);
8307        let list_diag = diagnostics
8308            .into_iter()
8309            .find(|diag| diag.message.contains("types do not unify"))
8310            .expect("expected unification diagnostic");
8311        let list_diag_message = list_diag.message.clone();
8312        assert!(
8313            is_list_scalar_unification_error(&list_diag_message),
8314            "expected list/scalar mismatch, got: {list_diag_message}"
8315        );
8316        let request_range = full_document_range(text);
8317        let actions = code_actions_for_source(&uri, text, request_range, &[list_diag]);
8318        let titles: Vec<String> = actions
8319            .into_iter()
8320            .filter_map(|action| match action {
8321                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8322                CodeActionOrCommand::Command(_) => None,
8323            })
8324            .collect();
8325        assert!(
8326            titles
8327                .iter()
8328                .any(|title| title == "Wrap expression in list literal"),
8329            "diag: {:?}; titles: {titles:#?}",
8330            list_diag_message
8331        );
8332    }
8333
8334    #[test]
8335    fn code_actions_offer_non_exhaustive_match_fix() {
8336        let text = "match (Some 1) when Some x -> x";
8337        let actions = code_actions_for_source_public(text, 0, 2);
8338        let titles: Vec<String> = actions
8339            .into_iter()
8340            .filter_map(|action| match action {
8341                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8342                CodeActionOrCommand::Command(_) => None,
8343            })
8344            .collect();
8345        assert!(
8346            titles
8347                .iter()
8348                .any(|title| title == "Add wildcard arm to match"),
8349            "titles: {titles:#?}"
8350        );
8351    }
8352
8353    #[test]
8354    fn code_actions_offer_function_value_mismatch_fixes() {
8355        let text = "let f = \\x -> x in f + 1";
8356        let uri = in_memory_doc_uri();
8357        clear_parse_cache(&uri);
8358        let diagnostics = diagnostics_from_text(&uri, text);
8359        let fun_mismatch = diagnostics
8360            .into_iter()
8361            .find(|diag| is_function_value_unification_error(&diag.message))
8362            .expect("expected function/value mismatch diagnostic");
8363        let actions =
8364            code_actions_for_source(&uri, text, full_document_range(text), &[fun_mismatch]);
8365        let titles: Vec<String> = actions
8366            .into_iter()
8367            .filter_map(|action| match action {
8368                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8369                CodeActionOrCommand::Command(_) => None,
8370            })
8371            .collect();
8372        assert!(
8373            titles
8374                .iter()
8375                .any(|title| title == "Apply expression to missing argument"),
8376            "titles: {titles:#?}"
8377        );
8378        assert!(
8379            titles
8380                .iter()
8381                .any(|title| title == "Wrap expression in lambda"),
8382            "titles: {titles:#?}"
8383        );
8384    }
8385
8386    #[test]
8387    fn code_actions_offer_to_list_fix_for_array_list_mismatch() {
8388        let text = r#"
8389let
8390  arr = prim_array_from_list [1, 2, 3],
8391  xs : List i32 = arr
8392in
8393  xs
8394"#;
8395        let uri = in_memory_doc_uri();
8396        clear_parse_cache(&uri);
8397        let diagnostics = diagnostics_from_text(&uri, text);
8398        let mismatch = diagnostics
8399            .into_iter()
8400            .find(|diag| is_array_list_unification_error(&diag.message))
8401            .expect("expected array/list mismatch diagnostic");
8402        let arr_range = Range {
8403            start: Position {
8404                line: 3,
8405                character: 18,
8406            },
8407            end: Position {
8408                line: 3,
8409                character: 21,
8410            },
8411        };
8412        let actions =
8413            code_actions_for_source(&uri, text, arr_range, std::slice::from_ref(&mismatch));
8414        let code_actions: Vec<CodeAction> = actions
8415            .into_iter()
8416            .filter_map(|action| match action {
8417                CodeActionOrCommand::CodeAction(action) => Some(action),
8418                CodeActionOrCommand::Command(_) => None,
8419            })
8420            .collect();
8421        let fix = code_actions
8422            .iter()
8423            .find(|action| action.title == "Convert expression to list with `to_list`")
8424            .expect("expected to_list quick fix");
8425
8426        let edit = fix
8427            .edit
8428            .as_ref()
8429            .expect("to_list quick fix must include edit");
8430        let changes = edit
8431            .changes
8432            .as_ref()
8433            .expect("to_list quick fix must include changes");
8434        let edits = changes
8435            .get(&uri)
8436            .expect("to_list quick fix must target current document");
8437        assert!(
8438            edits.iter().any(|e| e.new_text.contains("to_list (arr)")),
8439            "edits: {edits:#?}"
8440        );
8441    }
8442
8443    #[test]
8444    fn code_actions_offer_default_disambiguation_with_is_for_record_update() {
8445        let text = r#"
8446type A = A { x: i32, y: i32 }
8447type B = B { x: i32, y: i32 }
8448
8449instance Default A
8450    default = A { x = 1, y = 2 }
8451
8452instance Default B
8453    default = B { x = 10, y = 20 }
8454
8455let
8456    a = { default with { x = 9 } },
8457    b = { default with { y = 8 } }
8458in
8459    (a, b)
8460"#;
8461        let uri = in_memory_doc_uri();
8462        clear_parse_cache(&uri);
8463        let diagnostics = diagnostics_from_text(&uri, text);
8464        let diag = diagnostics
8465            .into_iter()
8466            .find(|d| d.message.contains("field `x` is not definitely available"))
8467            .expect("expected field availability diagnostic");
8468        let actions = code_actions_for_source(&uri, text, diag.range, std::slice::from_ref(&diag));
8469        let code_actions: Vec<CodeAction> = actions
8470            .into_iter()
8471            .filter_map(|action| match action {
8472                CodeActionOrCommand::CodeAction(action) => Some(action),
8473                CodeActionOrCommand::Command(_) => None,
8474            })
8475            .collect();
8476
8477        let titles: Vec<String> = code_actions.iter().map(|a| a.title.clone()).collect();
8478        assert!(
8479            titles
8480                .iter()
8481                .any(|title| title == "Disambiguate `default` as `A`"),
8482            "titles: {titles:#?}"
8483        );
8484        assert!(
8485            titles
8486                .iter()
8487                .any(|title| title == "Disambiguate `default` as `B`"),
8488            "titles: {titles:#?}"
8489        );
8490        assert!(
8491            titles.iter().any(|title| title == "Annotate `a` as `A`"),
8492            "titles: {titles:#?}"
8493        );
8494        assert!(
8495            titles.iter().any(|title| title == "Annotate `a` as `B`"),
8496            "titles: {titles:#?}"
8497        );
8498
8499        let contains_fix_for = |needle: &str| {
8500            code_actions.iter().any(|action| {
8501                action
8502                    .edit
8503                    .as_ref()
8504                    .and_then(|edit| edit.changes.as_ref())
8505                    .and_then(|changes| changes.get(&uri))
8506                    .is_some_and(|edits| edits.iter().any(|e| e.new_text.contains(needle)))
8507            })
8508        };
8509        assert!(
8510            contains_fix_for("(default is A)"),
8511            "expected `(default is A)` edit"
8512        );
8513        assert!(
8514            contains_fix_for("(default is B)"),
8515            "expected `(default is B)` edit"
8516        );
8517        assert!(contains_fix_for(": A"), "expected `: A` annotation edit");
8518        assert!(contains_fix_for(": B"), "expected `: B` annotation edit");
8519    }
8520
8521    #[test]
8522    fn code_actions_offer_hole_fill_candidates() {
8523        let text = r#"
8524fn aa_mk : i32 -> i32 = \x -> x
8525let y : i32 = ? in y
8526"#;
8527        let actions = code_actions_for_source_public(text, 2, 14);
8528        let titles: Vec<String> = actions
8529            .into_iter()
8530            .filter_map(|action| match action {
8531                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8532                CodeActionOrCommand::Command(_) => None,
8533            })
8534            .collect();
8535        assert!(
8536            titles
8537                .iter()
8538                .any(|title| title.contains("Fill hole with `aa_mk`")),
8539            "titles: {titles:#?}"
8540        );
8541    }
8542
8543    #[test]
8544    fn code_actions_offer_hole_fill_even_with_diagnostics_present() {
8545        let text = r#"
8546fn aa_mk : i32 -> i32 = \x -> x
8547let y : i32 = ? in y
8548"#;
8549        let uri = in_memory_doc_uri();
8550        clear_parse_cache(&uri);
8551        let request_range = Range {
8552            start: Position {
8553                line: 2,
8554                character: 14,
8555            },
8556            end: Position {
8557                line: 2,
8558                character: 14,
8559            },
8560        };
8561        let diagnostics = vec![Diagnostic {
8562            range: request_range,
8563            severity: Some(DiagnosticSeverity::ERROR),
8564            message: "synthetic diagnostic".to_string(),
8565            source: Some("test".to_string()),
8566            ..Diagnostic::default()
8567        }];
8568        let actions = code_actions_for_source(&uri, text, request_range, &diagnostics);
8569        let titles: Vec<String> = actions
8570            .into_iter()
8571            .filter_map(|action| match action {
8572                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8573                CodeActionOrCommand::Command(_) => None,
8574            })
8575            .collect();
8576        assert!(
8577            titles
8578                .iter()
8579                .any(|title| title.contains("Fill hole with `aa_mk`")),
8580            "titles: {titles:#?}"
8581        );
8582    }
8583
8584    #[test]
8585    fn code_actions_offer_hole_fill_for_real_typed_hole_diagnostic() {
8586        let text = r#"
8587fn aa_mk : i32 -> i32 = \x -> x
8588let y : i32 = ? in y
8589"#;
8590        let uri = in_memory_doc_uri();
8591        clear_parse_cache(&uri);
8592        let diagnostics = diagnostics_from_text(&uri, text);
8593        let hole_diag = diagnostics
8594            .iter()
8595            .find(|d| {
8596                d.message
8597                    .contains("typed hole `?` must be filled before evaluation")
8598            })
8599            .expect("typed hole diagnostic")
8600            .clone();
8601        let actions = code_actions_for_source(&uri, text, hole_diag.range, &[hole_diag]);
8602        let titles: Vec<String> = actions
8603            .into_iter()
8604            .filter_map(|action| match action {
8605                CodeActionOrCommand::CodeAction(action) => Some(action.title),
8606                CodeActionOrCommand::Command(_) => None,
8607            })
8608            .collect();
8609        assert!(
8610            titles
8611                .iter()
8612                .any(|title| title.contains("Fill hole with `aa_mk`")),
8613            "titles: {titles:#?}"
8614        );
8615    }
8616
8617    #[test]
8618    fn expected_type_reports_if_condition_bool() {
8619        let text = "if true then 1 else 2";
8620        let ty = expected_type_for_source_public(text, 0, 3).expect("expected type at condition");
8621        assert_eq!(ty, "bool");
8622    }
8623
8624    #[test]
8625    fn expected_type_reports_if_branch_type() {
8626        let text = "let x : i32 = if true then 1 else 2 in x";
8627        let ty = expected_type_for_source_public(text, 0, 27).expect("expected type at branch");
8628        assert_eq!(ty, "i32");
8629    }
8630
8631    #[test]
8632    fn expected_type_reports_function_argument_type() {
8633        let text = "let f = \\x -> x + 1 in f 2";
8634        let ty = expected_type_for_source_public(text, 0, 26).expect("expected type at argument");
8635        assert_eq!(ty, "'r");
8636    }
8637
8638    #[test]
8639    fn functions_producing_expected_type_include_user_fn() {
8640        let text = r#"
8641fn mk : i32 -> i32 = \x -> x
8642if true then 0 else 1
8643"#;
8644        let items = functions_producing_expected_type_for_source_public(text, 2, 13);
8645        assert!(
8646            items.iter().any(|item| item.starts_with("mk : ")),
8647            "items: {items:#?}"
8648        );
8649    }
8650
8651    #[test]
8652    fn command_parses_uri_position_tuple_args() {
8653        let args = vec![
8654            Value::String("inmemory:///docs.rex".to_string()),
8655            Value::from(2u64),
8656            Value::from(3u64),
8657        ];
8658        let (uri, pos) = command_uri_and_position(&args).expect("parsed command args");
8659        assert_eq!(uri.as_str(), "inmemory:///docs.rex");
8660        assert_eq!(pos.line, 2);
8661        assert_eq!(pos.character, 3);
8662    }
8663
8664    #[test]
8665    fn command_parses_uri_position_and_id_tuple_args() {
8666        let args = vec![
8667            Value::String("inmemory:///docs.rex".to_string()),
8668            Value::from(2u64),
8669            Value::from(3u64),
8670            Value::String("qf-abc".to_string()),
8671        ];
8672        let (uri, pos, id) = command_uri_position_and_id(&args).expect("parsed command args");
8673        assert_eq!(uri.as_str(), "inmemory:///docs.rex");
8674        assert_eq!(pos.line, 2);
8675        assert_eq!(pos.character, 3);
8676        assert_eq!(id, "qf-abc");
8677    }
8678
8679    #[test]
8680    fn command_parses_uri_position_max_steps_strategy_and_dry_run_tuple_args() {
8681        let args = vec![
8682            Value::String("inmemory:///docs.rex".to_string()),
8683            Value::from(2u64),
8684            Value::from(3u64),
8685            Value::from(5u64),
8686            Value::String("aggressive".to_string()),
8687            Value::Bool(true),
8688        ];
8689        let (uri, pos, max_steps, strategy, dry_run) =
8690            command_uri_position_max_steps_strategy_and_dry_run(&args)
8691                .expect("parsed command args");
8692        assert_eq!(uri.as_str(), "inmemory:///docs.rex");
8693        assert_eq!(pos.line, 2);
8694        assert_eq!(pos.character, 3);
8695        assert_eq!(max_steps, 5);
8696        assert_eq!(strategy, BulkQuickFixStrategy::Aggressive);
8697        assert!(dry_run);
8698    }
8699
8700    #[test]
8701    fn command_parses_uri_position_max_steps_strategy_and_dry_run_object_args() {
8702        let args = vec![json!({
8703            "uri": "inmemory:///docs.rex",
8704            "line": 4,
8705            "character": 9,
8706            "maxSteps": 99,
8707            "strategy": "conservative",
8708            "dryRun": false
8709        })];
8710        let (uri, pos, max_steps, strategy, dry_run) =
8711            command_uri_position_max_steps_strategy_and_dry_run(&args)
8712                .expect("parsed command args");
8713        assert_eq!(uri.as_str(), "inmemory:///docs.rex");
8714        assert_eq!(pos.line, 4);
8715        assert_eq!(pos.character, 9);
8716        assert_eq!(max_steps, 20, "maxSteps should clamp to upper bound");
8717        assert_eq!(strategy, BulkQuickFixStrategy::Conservative);
8718        assert!(!dry_run);
8719    }
8720
8721    #[test]
8722    fn execute_expected_type_command_returns_object() {
8723        let uri = in_memory_doc_uri();
8724        let text = "if true then 1 else 2";
8725        let out = execute_query_command_for_document(
8726            CMD_EXPECTED_TYPE_AT,
8727            &uri,
8728            text,
8729            Position {
8730                line: 0,
8731                character: 3,
8732            },
8733        )
8734        .expect("command output");
8735        let expected = out
8736            .as_object()
8737            .and_then(|o| o.get("expectedType"))
8738            .and_then(Value::as_str)
8739            .expect("expectedType");
8740        assert_eq!(expected, "bool");
8741    }
8742
8743    #[test]
8744    fn execute_functions_command_returns_items() {
8745        let uri = in_memory_doc_uri();
8746        let text = r#"
8747fn mk : i32 -> i32 = \x -> x
8748if true then 0 else 1
8749"#;
8750        let out = execute_query_command_for_document(
8751            CMD_FUNCTIONS_PRODUCING_EXPECTED_TYPE_AT,
8752            &uri,
8753            text,
8754            Position {
8755                line: 2,
8756                character: 13,
8757            },
8758        )
8759        .expect("command output");
8760        let items = out
8761            .as_object()
8762            .and_then(|o| o.get("items"))
8763            .and_then(Value::as_array)
8764            .expect("items array");
8765        assert!(
8766            items
8767                .iter()
8768                .filter_map(Value::as_str)
8769                .any(|item| item.starts_with("mk : ")),
8770            "items: {items:#?}"
8771        );
8772    }
8773
8774    #[test]
8775    fn semantic_functions_command_caps_items_count() {
8776        let uri = in_memory_doc_uri();
8777        let mut lines = Vec::new();
8778        for i in 0..200usize {
8779            lines.push(format!("fn mk_{i} : i32 -> i32 = \\x -> x"));
8780        }
8781        lines.push("let y : i32 = ? in y".to_string());
8782        let line = (lines.len() - 1) as u32;
8783        let text = lines.join("\n");
8784        let out = execute_query_command_for_document(
8785            CMD_FUNCTIONS_PRODUCING_EXPECTED_TYPE_AT,
8786            &uri,
8787            &text,
8788            Position {
8789                line,
8790                character: 14,
8791            },
8792        )
8793        .expect("command output");
8794        let items = out
8795            .as_object()
8796            .and_then(|o| o.get("items"))
8797            .and_then(Value::as_array)
8798            .expect("items array");
8799        assert!(
8800            items.len() <= MAX_SEMANTIC_CANDIDATES,
8801            "items_len={}; max={MAX_SEMANTIC_CANDIDATES}",
8802            items.len()
8803        );
8804    }
8805
8806    #[test]
8807    fn execute_functions_accepting_inferred_type_command_returns_items() {
8808        let uri = in_memory_doc_uri();
8809        let text = r#"
8810fn use_bool : bool -> i32 = \x -> 0
8811let x = true in x
8812"#;
8813        let out = execute_query_command_for_document(
8814            CMD_FUNCTIONS_ACCEPTING_INFERRED_TYPE_AT,
8815            &uri,
8816            text,
8817            Position {
8818                line: 2,
8819                character: 8,
8820            },
8821        )
8822        .expect("command output");
8823        let obj = out.as_object().expect("object");
8824        let inferred = obj
8825            .get("inferredType")
8826            .and_then(Value::as_str)
8827            .expect("inferredType");
8828        assert_eq!(inferred, "bool");
8829        let items = obj
8830            .get("items")
8831            .and_then(Value::as_array)
8832            .expect("items array");
8833        assert!(
8834            items
8835                .iter()
8836                .filter_map(Value::as_str)
8837                .any(|item| item.starts_with("use_bool : ")),
8838            "items: {items:#?}"
8839        );
8840    }
8841
8842    #[test]
8843    fn execute_adapters_from_inferred_to_expected_command_returns_items() {
8844        let uri = in_memory_doc_uri();
8845        let text = r#"
8846fn id_i32 : i32 -> i32 = \x -> x
8847let x = 1 in let y : i32 = x in y
8848"#;
8849        let out = execute_query_command_for_document(
8850            CMD_ADAPTERS_FROM_INFERRED_TO_EXPECTED_AT,
8851            &uri,
8852            text,
8853            Position {
8854                line: 2,
8855                character: 30,
8856            },
8857        )
8858        .expect("command output");
8859        let obj = out.as_object().expect("object");
8860        let inferred = obj
8861            .get("inferredType")
8862            .and_then(Value::as_str)
8863            .expect("inferredType");
8864        assert_eq!(inferred, "i32");
8865        let expected = obj
8866            .get("expectedType")
8867            .and_then(Value::as_str)
8868            .expect("expectedType");
8869        assert_eq!(expected, "i32");
8870        let items = obj
8871            .get("items")
8872            .and_then(Value::as_array)
8873            .expect("items array");
8874        assert!(
8875            items
8876                .iter()
8877                .filter_map(Value::as_str)
8878                .any(|item| item.starts_with("id_i32 : ")),
8879            "items: {items:#?}"
8880        );
8881    }
8882
8883    #[test]
8884    fn semantic_holes_command_caps_hole_count() {
8885        let uri = in_memory_doc_uri();
8886        let mut text = String::from("let ys : List i32 = [");
8887        for i in 0..160usize {
8888            if i > 0 {
8889                text.push_str(", ");
8890            }
8891            text.push('?');
8892        }
8893        text.push_str("] in ys");
8894        let out = execute_query_command_for_document_without_position(
8895            CMD_HOLES_EXPECTED_TYPES,
8896            &uri,
8897            &text,
8898        )
8899        .expect("command output");
8900        let holes = out
8901            .as_object()
8902            .and_then(|o| o.get("holes"))
8903            .and_then(Value::as_array)
8904            .expect("holes array");
8905        assert!(
8906            holes.len() <= MAX_SEMANTIC_HOLES,
8907            "holes_len={}; max={MAX_SEMANTIC_HOLES}",
8908            holes.len()
8909        );
8910    }
8911
8912    #[test]
8913    fn execute_functions_compatible_with_in_scope_values_command_returns_items() {
8914        let uri = in_memory_doc_uri();
8915        let text = r#"
8916fn to_string_i32 : i32 -> string = \x -> "ok"
8917let x = 1 in let y : string = ? in y
8918"#;
8919        let out = execute_query_command_for_document(
8920            CMD_FUNCTIONS_COMPATIBLE_WITH_IN_SCOPE_VALUES_AT,
8921            &uri,
8922            text,
8923            Position {
8924                line: 2,
8925                character: 30,
8926            },
8927        )
8928        .expect("command output");
8929        let items = out
8930            .as_object()
8931            .and_then(|o| o.get("items"))
8932            .and_then(Value::as_array)
8933            .expect("items array");
8934        assert!(
8935            items
8936                .iter()
8937                .filter_map(Value::as_str)
8938                .any(|item| item.contains("to_string_i32") && item.contains("to_string_i32 x")),
8939            "items: {items:#?}"
8940        );
8941    }
8942
8943    #[test]
8944    fn semantic_loop_step_caps_in_scope_values() {
8945        let uri = in_memory_doc_uri();
8946        let mut lines = Vec::new();
8947        for i in 0..160usize {
8948            lines.push(format!("fn x{i} : i32 -> i32 = \\v -> v"));
8949        }
8950        lines.push("let y : i32 = ? in y".to_string());
8951        let line = (lines.len() - 1) as u32;
8952        let text = lines.join("\n");
8953        let out = execute_semantic_loop_step(
8954            &uri,
8955            &text,
8956            Position {
8957                line,
8958                character: 14,
8959            },
8960        )
8961        .expect("step output");
8962        let in_scope_values = out
8963            .as_object()
8964            .and_then(|o| o.get("inScopeValues"))
8965            .and_then(Value::as_array)
8966            .expect("inScopeValues");
8967        assert!(
8968            in_scope_values.len() <= MAX_SEMANTIC_IN_SCOPE_VALUES,
8969            "in_scope_values_len={}; max={MAX_SEMANTIC_IN_SCOPE_VALUES}",
8970            in_scope_values.len()
8971        );
8972    }
8973
8974    #[test]
8975    fn hole_expected_types_detects_placeholder_vars() {
8976        let uri = in_memory_doc_uri();
8977        let text = "let y : i32 = _ in y";
8978        let holes = hole_expected_types_for_document(&uri, text);
8979        assert!(!holes.is_empty(), "holes: {holes:#?}");
8980        let expected = holes
8981            .iter()
8982            .find_map(|hole| hole.get("expectedType").and_then(Value::as_str));
8983        assert_eq!(expected, Some("i32"));
8984    }
8985
8986    #[test]
8987    fn hole_expected_types_detects_question_holes() {
8988        let uri = in_memory_doc_uri();
8989        let text = "let y : i32 = ? in y";
8990        let holes = hole_expected_types_for_document(&uri, text);
8991        assert!(!holes.is_empty(), "holes: {holes:#?}");
8992        let hole_name = holes
8993            .iter()
8994            .find_map(|hole| hole.get("name").and_then(Value::as_str));
8995        assert_eq!(hole_name, Some("?"));
8996        let expected = holes
8997            .iter()
8998            .find_map(|hole| hole.get("expectedType").and_then(Value::as_str));
8999        assert_eq!(expected, Some("i32"));
9000    }
9001
9002    #[test]
9003    fn execute_holes_command_returns_holes_array() {
9004        let uri = in_memory_doc_uri();
9005        let text = "let y : i32 = _ in y";
9006        let out = execute_query_command_for_document_without_position(
9007            CMD_HOLES_EXPECTED_TYPES,
9008            &uri,
9009            text,
9010        )
9011        .expect("command output");
9012        let holes = out
9013            .as_object()
9014            .and_then(|o| o.get("holes"))
9015            .and_then(Value::as_array)
9016            .expect("holes array");
9017        assert!(!holes.is_empty(), "holes: {holes:#?}");
9018    }
9019
9020    #[test]
9021    fn semantic_loop_step_reports_expected_type_and_candidates() {
9022        let uri = in_memory_doc_uri();
9023        let text = r#"
9024fn mk : i32 -> i32 = \x -> x
9025let y : i32 = ? in y
9026"#;
9027        let out = execute_semantic_loop_step(
9028            &uri,
9029            text,
9030            Position {
9031                line: 2,
9032                character: 14,
9033            },
9034        )
9035        .expect("command output");
9036        let obj = out.as_object().expect("object");
9037
9038        let expected = obj
9039            .get("expectedType")
9040            .and_then(Value::as_str)
9041            .expect("expectedType");
9042        assert_eq!(expected, "i32");
9043
9044        let function_candidates = obj
9045            .get("functionCandidates")
9046            .and_then(Value::as_array)
9047            .expect("functionCandidates");
9048        assert!(
9049            function_candidates
9050                .iter()
9051                .filter_map(Value::as_str)
9052                .any(|item| item.starts_with("mk : ")),
9053            "function candidates: {function_candidates:#?}"
9054        );
9055
9056        let quick_fix_titles = obj
9057            .get("quickFixTitles")
9058            .and_then(Value::as_array)
9059            .expect("quickFixTitles");
9060        assert!(
9061            quick_fix_titles
9062                .iter()
9063                .filter_map(Value::as_str)
9064                .any(|title| title == "Fill hole with `mk`"),
9065            "quick fixes: {quick_fix_titles:#?}"
9066        );
9067        let quick_fixes = obj
9068            .get("quickFixes")
9069            .and_then(Value::as_array)
9070            .expect("quickFixes");
9071        assert!(
9072            quick_fixes.iter().any(|qf| {
9073                qf.get("id").and_then(Value::as_str).is_some()
9074                    && qf.get("title").and_then(Value::as_str) == Some("Fill hole with `mk`")
9075                    && qf.get("kind").and_then(Value::as_str) == Some("quickfix")
9076                    && qf.get("edit").is_some()
9077            }),
9078            "quickFixes: {quick_fixes:#?}"
9079        );
9080
9081        let accepting = obj
9082            .get("functionsAcceptingInferredType")
9083            .and_then(Value::as_array)
9084            .expect("functionsAcceptingInferredType");
9085        assert!(
9086            accepting
9087                .iter()
9088                .filter_map(Value::as_str)
9089                .any(|item| item.starts_with("mk : ")),
9090            "accepting: {accepting:#?}"
9091        );
9092    }
9093
9094    #[test]
9095    fn semantic_loop_step_reports_local_diagnostics_and_fixes() {
9096        let uri = in_memory_doc_uri();
9097        let text = "let y = z in y";
9098        let out = execute_semantic_loop_step(
9099            &uri,
9100            text,
9101            Position {
9102                line: 0,
9103                character: 8,
9104            },
9105        )
9106        .expect("command output");
9107        let obj = out.as_object().expect("object");
9108
9109        let local_diagnostics = obj
9110            .get("localDiagnostics")
9111            .and_then(Value::as_array)
9112            .expect("localDiagnostics");
9113        assert!(
9114            local_diagnostics.iter().any(|diag| {
9115                diag.get("message")
9116                    .and_then(Value::as_str)
9117                    .is_some_and(|message| message.contains("unbound variable z"))
9118            }),
9119            "diagnostics: {local_diagnostics:#?}"
9120        );
9121
9122        let quick_fix_titles = obj
9123            .get("quickFixTitles")
9124            .and_then(Value::as_array)
9125            .expect("quickFixTitles");
9126        assert!(
9127            quick_fix_titles
9128                .iter()
9129                .filter_map(Value::as_str)
9130                .any(|title| title.contains("Introduce `let z = null`")),
9131            "quick fixes: {quick_fix_titles:#?}"
9132        );
9133        let quick_fixes = obj
9134            .get("quickFixes")
9135            .and_then(Value::as_array)
9136            .expect("quickFixes");
9137        assert!(
9138            quick_fixes.iter().any(|qf| {
9139                qf.get("id").and_then(Value::as_str).is_some()
9140                    && qf
9141                        .get("title")
9142                        .and_then(Value::as_str)
9143                        .is_some_and(|title| title.contains("Introduce `let z = null`"))
9144                    && qf.get("kind").and_then(Value::as_str) == Some("quickfix")
9145            }),
9146            "quickFixes: {quick_fixes:#?}"
9147        );
9148    }
9149
9150    #[test]
9151    fn semantic_loop_step_json_contract_is_stable() {
9152        let uri = in_memory_doc_uri();
9153        let text = r#"
9154fn mk : i32 -> i32 = \x -> x
9155let y : i32 = ? in y
9156"#;
9157        let out = execute_semantic_loop_step(
9158            &uri,
9159            text,
9160            Position {
9161                line: 2,
9162                character: 14,
9163            },
9164        )
9165        .expect("step output");
9166        let obj = expect_object(&out);
9167
9168        // Required top-level fields and types.
9169        assert!(obj.contains_key("expectedType"));
9170        assert!(obj.contains_key("inferredType"));
9171        expect_array_field(obj, "inScopeValues");
9172        expect_array_field(obj, "functionCandidates");
9173        expect_array_field(obj, "holeFillCandidates");
9174        expect_array_field(obj, "functionsAcceptingInferredType");
9175        expect_array_field(obj, "adaptersFromInferredToExpectedType");
9176        expect_array_field(obj, "functionsCompatibleWithInScopeValues");
9177        expect_array_field(obj, "localDiagnostics");
9178        expect_array_field(obj, "quickFixes");
9179        expect_array_field(obj, "quickFixTitles");
9180        expect_array_field(obj, "holes");
9181
9182        let quick_fixes = expect_array_field(obj, "quickFixes");
9183        assert!(
9184            !quick_fixes.is_empty(),
9185            "quickFixes should not be empty: {obj:#?}"
9186        );
9187        let first_quick_fix = expect_object(quick_fixes.first().expect("first quick fix"));
9188        expect_string_field(first_quick_fix, "id");
9189        expect_string_field(first_quick_fix, "title");
9190        // `kind` may be null for some future quick-fix types, but key should exist.
9191        assert!(
9192            first_quick_fix.contains_key("kind"),
9193            "quickFix should include `kind`: {first_quick_fix:#?}"
9194        );
9195        assert!(
9196            first_quick_fix.contains_key("edit"),
9197            "quickFix should include `edit`: {first_quick_fix:#?}"
9198        );
9199    }
9200
9201    #[test]
9202    fn semantic_loop_apply_quick_fix_resolves_by_id() {
9203        let uri = in_memory_doc_uri();
9204        let text = "let y = z in y";
9205        let step = execute_semantic_loop_step(
9206            &uri,
9207            text,
9208            Position {
9209                line: 0,
9210                character: 8,
9211            },
9212        )
9213        .expect("step output");
9214        let quick_fix_id = step
9215            .get("quickFixes")
9216            .and_then(Value::as_array)
9217            .and_then(|arr| arr.first())
9218            .and_then(|item| item.get("id"))
9219            .and_then(Value::as_str)
9220            .expect("quick fix id")
9221            .to_string();
9222
9223        let out = execute_semantic_loop_apply_quick_fix(
9224            &uri,
9225            text,
9226            Position {
9227                line: 0,
9228                character: 8,
9229            },
9230            &quick_fix_id,
9231        )
9232        .expect("apply output");
9233        let quick_fix = out
9234            .get("quickFix")
9235            .and_then(Value::as_object)
9236            .expect("quickFix object");
9237        assert_eq!(
9238            quick_fix
9239                .get("id")
9240                .and_then(Value::as_str)
9241                .expect("quickFix.id"),
9242            quick_fix_id
9243        );
9244        assert!(quick_fix.get("edit").is_some(), "quickFix: {quick_fix:#?}");
9245    }
9246
9247    #[test]
9248    fn semantic_loop_apply_quick_fix_json_contract_is_stable() {
9249        let uri = in_memory_doc_uri();
9250        let text = "let y = z in y";
9251        let step = execute_semantic_loop_step(
9252            &uri,
9253            text,
9254            Position {
9255                line: 0,
9256                character: 8,
9257            },
9258        )
9259        .expect("step output");
9260        let quick_fix_id = step
9261            .get("quickFixes")
9262            .and_then(Value::as_array)
9263            .and_then(|arr| arr.first())
9264            .and_then(|item| item.get("id"))
9265            .and_then(Value::as_str)
9266            .expect("quick fix id")
9267            .to_string();
9268
9269        let out = execute_semantic_loop_apply_quick_fix(
9270            &uri,
9271            text,
9272            Position {
9273                line: 0,
9274                character: 8,
9275            },
9276            &quick_fix_id,
9277        )
9278        .expect("apply output");
9279        let obj = expect_object(&out);
9280        let quick_fix = expect_object(obj.get("quickFix").expect("quickFix"));
9281        expect_string_field(quick_fix, "id");
9282        expect_string_field(quick_fix, "title");
9283        assert!(quick_fix.contains_key("kind"));
9284        assert!(quick_fix.contains_key("edit"));
9285    }
9286
9287    #[test]
9288    fn semantic_loop_apply_quick_fix_unknown_id_returns_null() {
9289        let uri = in_memory_doc_uri();
9290        let text = "let y = z in y";
9291        let out = execute_semantic_loop_apply_quick_fix(
9292            &uri,
9293            text,
9294            Position {
9295                line: 0,
9296                character: 8,
9297            },
9298            "qf-does-not-exist",
9299        )
9300        .expect("apply output");
9301        assert_eq!(out, Value::Null);
9302    }
9303
9304    #[test]
9305    fn semantic_loop_apply_best_quick_fixes_updates_text_and_reduces_errors() {
9306        let uri = in_memory_doc_uri();
9307        let text = "let y = z in y";
9308        let out = execute_semantic_loop_apply_best_quick_fixes(
9309            &uri,
9310            text,
9311            Position {
9312                line: 0,
9313                character: 8,
9314            },
9315            3,
9316            BulkQuickFixStrategy::Conservative,
9317            false,
9318        )
9319        .expect("bulk output");
9320
9321        let applied_count = out
9322            .get("appliedCount")
9323            .and_then(Value::as_u64)
9324            .expect("appliedCount");
9325        assert!(applied_count >= 1, "out: {out:#?}");
9326
9327        let updated_text = out
9328            .get("updatedText")
9329            .and_then(Value::as_str)
9330            .expect("updatedText");
9331        assert_ne!(updated_text, text, "out: {out:#?}");
9332
9333        let diagnostics_after = out
9334            .get("localDiagnosticsAfter")
9335            .and_then(Value::as_array)
9336            .expect("localDiagnosticsAfter");
9337        assert!(
9338            diagnostics_after.iter().all(|diag| {
9339                !diag
9340                    .get("message")
9341                    .and_then(Value::as_str)
9342                    .is_some_and(|m| m.contains("unbound variable z"))
9343            }),
9344            "diagnostics_after: {diagnostics_after:#?}"
9345        );
9346        let steps = out.get("steps").and_then(Value::as_array).expect("steps");
9347        assert!(!steps.is_empty(), "out: {out:#?}");
9348        let strategy = out
9349            .get("strategy")
9350            .and_then(Value::as_str)
9351            .expect("strategy");
9352        assert_eq!(strategy, "conservative");
9353        let first_step = steps
9354            .first()
9355            .and_then(Value::as_object)
9356            .expect("first step");
9357        assert!(
9358            first_step.get("quickFix").is_some(),
9359            "step: {first_step:#?}"
9360        );
9361        let before_count = first_step
9362            .get("diagnosticsBeforeCount")
9363            .and_then(Value::as_u64)
9364            .expect("diagnosticsBeforeCount");
9365        let after_count = first_step
9366            .get("diagnosticsAfterCount")
9367            .and_then(Value::as_u64)
9368            .expect("diagnosticsAfterCount");
9369        assert!(after_count <= before_count, "step: {first_step:#?}");
9370    }
9371
9372    #[test]
9373    fn semantic_loop_apply_best_quick_fixes_json_contract_is_stable() {
9374        let uri = in_memory_doc_uri();
9375        let text = "let y = z in y";
9376        let out = execute_semantic_loop_apply_best_quick_fixes(
9377            &uri,
9378            text,
9379            Position {
9380                line: 0,
9381                character: 8,
9382            },
9383            2,
9384            BulkQuickFixStrategy::Conservative,
9385            true,
9386        )
9387        .expect("bulk output");
9388        let obj = expect_object(&out);
9389
9390        assert_eq!(expect_string_field(obj, "strategy"), "conservative");
9391        obj.get("dryRun")
9392            .and_then(Value::as_bool)
9393            .expect("dryRun bool");
9394        obj.get("appliedCount")
9395            .and_then(Value::as_u64)
9396            .expect("appliedCount u64");
9397        obj.get("updatedText")
9398            .and_then(Value::as_str)
9399            .expect("updatedText string");
9400        expect_array_field(obj, "appliedQuickFixes");
9401        expect_array_field(obj, "steps");
9402        expect_array_field(obj, "localDiagnosticsAfter");
9403        expect_string_field(obj, "stoppedReason");
9404        expect_string_field(obj, "stoppedReasonDetail");
9405        obj.get("lastDiagnosticsDelta")
9406            .and_then(Value::as_i64)
9407            .expect("lastDiagnosticsDelta i64");
9408        obj.get("noImprovementStreak")
9409            .and_then(Value::as_u64)
9410            .expect("noImprovementStreak u64");
9411        obj.get("seenStatesCount")
9412            .and_then(Value::as_u64)
9413            .expect("seenStatesCount u64");
9414
9415        if let Some(first_step) = expect_array_field(obj, "steps").first() {
9416            let step_obj = expect_object(first_step);
9417            step_obj
9418                .get("index")
9419                .and_then(Value::as_u64)
9420                .expect("step.index");
9421            assert!(step_obj.contains_key("quickFix"));
9422            expect_array_field(step_obj, "diagnosticsBefore");
9423            expect_array_field(step_obj, "diagnosticsAfter");
9424            step_obj
9425                .get("diagnosticsBeforeCount")
9426                .and_then(Value::as_u64)
9427                .expect("step diagnosticsBeforeCount");
9428            step_obj
9429                .get("diagnosticsAfterCount")
9430                .and_then(Value::as_u64)
9431                .expect("step diagnosticsAfterCount");
9432            step_obj
9433                .get("diagnosticsDelta")
9434                .and_then(Value::as_i64)
9435                .expect("step diagnosticsDelta");
9436            step_obj
9437                .get("noImprovementStreak")
9438                .and_then(Value::as_u64)
9439                .expect("step noImprovementStreak");
9440        }
9441    }
9442
9443    #[test]
9444    fn semantic_loop_apply_best_quick_fixes_no_context_returns_no_quickfix() {
9445        let uri = in_memory_doc_uri();
9446        let text = "let x = 1 in x";
9447        let out = execute_semantic_loop_apply_best_quick_fixes(
9448            &uri,
9449            text,
9450            Position {
9451                line: 0,
9452                character: 4,
9453            },
9454            3,
9455            BulkQuickFixStrategy::Conservative,
9456            false,
9457        )
9458        .expect("bulk output");
9459        let obj = expect_object(&out);
9460        assert_eq!(expect_string_field(obj, "stoppedReason"), "noQuickFix");
9461        assert_eq!(
9462            obj.get("appliedCount")
9463                .and_then(Value::as_u64)
9464                .expect("appliedCount"),
9465            0
9466        );
9467        assert!(expect_array_field(obj, "steps").is_empty(), "out={out:#?}");
9468    }
9469
9470    #[test]
9471    fn semantic_loop_step_parse_error_still_returns_contract_shape() {
9472        let uri = in_memory_doc_uri();
9473        let text = "let x =";
9474        let out = execute_semantic_loop_step(
9475            &uri,
9476            text,
9477            Position {
9478                line: 0,
9479                character: 6,
9480            },
9481        )
9482        .expect("step output");
9483        let obj = expect_object(&out);
9484        // On parse failure we still return the same top-level contract shape.
9485        expect_array_field(obj, "quickFixes");
9486        expect_array_field(obj, "quickFixTitles");
9487        expect_array_field(obj, "localDiagnostics");
9488        expect_array_field(obj, "holes");
9489    }
9490
9491    #[test]
9492    fn golden_flow_hole_to_apply_by_id_reduces_hole_count() {
9493        let uri = in_memory_doc_uri();
9494        let text = r#"
9495fn mk : i32 -> i32 = \x -> x
9496let y : i32 = ? in y
9497"#;
9498        let step = execute_semantic_loop_step(
9499            &uri,
9500            text,
9501            Position {
9502                line: 2,
9503                character: 14,
9504            },
9505        )
9506        .expect("step output");
9507        let quick_fix_id = step
9508            .get("quickFixes")
9509            .and_then(Value::as_array)
9510            .and_then(|arr| {
9511                arr.iter().find(|item| {
9512                    item.get("title")
9513                        .and_then(Value::as_str)
9514                        .is_some_and(|title| title == "Fill hole with `mk`")
9515                })
9516            })
9517            .and_then(|item| item.get("id"))
9518            .and_then(Value::as_str)
9519            .expect("hole fill quick-fix id")
9520            .to_string();
9521        let apply = execute_semantic_loop_apply_quick_fix(
9522            &uri,
9523            text,
9524            Position {
9525                line: 2,
9526                character: 14,
9527            },
9528            &quick_fix_id,
9529        )
9530        .expect("apply output");
9531        let quick_fix = apply.get("quickFix").expect("quickFix returned").clone();
9532        let edit: WorkspaceEdit =
9533            serde_json::from_value(quick_fix.get("edit").cloned().expect("quickFix.edit"))
9534                .expect("workspace edit");
9535        let updated = apply_workspace_edit_to_text(&uri, text, &edit).expect("apply edit");
9536
9537        let holes_before = hole_expected_types_for_document(&uri, text);
9538        let holes_after = hole_expected_types_for_document(&uri, &updated);
9539        assert!(
9540            holes_after.len() < holes_before.len(),
9541            "before={holes_before:#?}; after={holes_after:#?}; updated=\n{updated}"
9542        );
9543    }
9544
9545    #[test]
9546    fn golden_flow_unknown_var_bulk_repairs_local_error() {
9547        let uri = in_memory_doc_uri();
9548        let text = "let y = z in y";
9549        let out = execute_semantic_loop_apply_best_quick_fixes(
9550            &uri,
9551            text,
9552            Position {
9553                line: 0,
9554                character: 8,
9555            },
9556            3,
9557            BulkQuickFixStrategy::Conservative,
9558            false,
9559        )
9560        .expect("bulk output");
9561        let diagnostics_after = out
9562            .get("localDiagnosticsAfter")
9563            .and_then(Value::as_array)
9564            .expect("localDiagnosticsAfter");
9565        assert!(
9566            diagnostics_after.iter().all(|diag| {
9567                !diag
9568                    .get("message")
9569                    .and_then(Value::as_str)
9570                    .is_some_and(|msg| msg.contains("unbound variable z"))
9571            }),
9572            "out={out:#?}"
9573        );
9574    }
9575
9576    #[test]
9577    fn golden_flow_complex_bulk_preview_then_apply_same_projection() {
9578        let uri = in_memory_doc_uri();
9579        let text = r#"
9580let
9581  y = z
9582in
9583  match y when Some x -> x
9584"#;
9585        let preview = execute_semantic_loop_apply_best_quick_fixes(
9586            &uri,
9587            text,
9588            Position {
9589                line: 2,
9590                character: 6,
9591            },
9592            3,
9593            BulkQuickFixStrategy::Aggressive,
9594            true,
9595        )
9596        .expect("preview output");
9597        let apply = execute_semantic_loop_apply_best_quick_fixes(
9598            &uri,
9599            text,
9600            Position {
9601                line: 2,
9602                character: 6,
9603            },
9604            3,
9605            BulkQuickFixStrategy::Aggressive,
9606            false,
9607        )
9608        .expect("apply output");
9609
9610        let preview_text = preview
9611            .get("updatedText")
9612            .and_then(Value::as_str)
9613            .expect("preview updatedText");
9614        let apply_text = apply
9615            .get("updatedText")
9616            .and_then(Value::as_str)
9617            .expect("apply updatedText");
9618        assert_eq!(
9619            preview_text, apply_text,
9620            "preview={preview:#?}\napply={apply:#?}"
9621        );
9622
9623        let preview_steps = preview
9624            .get("steps")
9625            .and_then(Value::as_array)
9626            .expect("preview steps");
9627        let apply_steps = apply
9628            .get("steps")
9629            .and_then(Value::as_array)
9630            .expect("apply steps");
9631        assert_eq!(preview_steps.len(), apply_steps.len());
9632    }
9633
9634    #[test]
9635    fn bulk_strategy_simple_unknown_var_applies_introduce_fix() {
9636        let uri = in_memory_doc_uri();
9637        let text = "let y = z in y";
9638        let out = execute_semantic_loop_apply_best_quick_fixes(
9639            &uri,
9640            text,
9641            Position {
9642                line: 0,
9643                character: 8,
9644            },
9645            1,
9646            BulkQuickFixStrategy::Conservative,
9647            false,
9648        )
9649        .expect("bulk output");
9650        let first_title = out
9651            .get("steps")
9652            .and_then(Value::as_array)
9653            .and_then(|steps| steps.first())
9654            .and_then(|step| step.get("quickFix"))
9655            .and_then(|qf| qf.get("title"))
9656            .and_then(Value::as_str)
9657            .expect("first quick-fix title");
9658        assert!(
9659            first_title.contains("Introduce `let z = null`"),
9660            "first_title={first_title}; out={out:#?}"
9661        );
9662    }
9663
9664    #[test]
9665    fn bulk_strategy_medium_distinguishes_conservative_vs_aggressive_ranking() {
9666        let candidates = vec![
9667            json!({
9668                "id": "qf-add",
9669                "title": "Add wildcard arm to match",
9670                "kind": "quickfix",
9671                "edit": Value::Null,
9672            }),
9673            json!({
9674                "id": "qf-intro",
9675                "title": "Introduce `let z = null`",
9676                "kind": "quickfix",
9677                "edit": Value::Null,
9678            }),
9679        ];
9680        let conservative =
9681            best_quick_fix_from_candidates(&candidates, BulkQuickFixStrategy::Conservative)
9682                .expect("conservative choice");
9683        let aggressive =
9684            best_quick_fix_from_candidates(&candidates, BulkQuickFixStrategy::Aggressive)
9685                .expect("aggressive choice");
9686        assert_eq!(
9687            conservative
9688                .get("title")
9689                .and_then(Value::as_str)
9690                .expect("conservative title"),
9691            "Add wildcard arm to match"
9692        );
9693        assert_eq!(
9694            aggressive
9695                .get("title")
9696                .and_then(Value::as_str)
9697                .expect("aggressive title"),
9698            "Introduce `let z = null`"
9699        );
9700    }
9701
9702    #[test]
9703    fn bulk_strategy_complex_returns_step_telemetry_for_each_step() {
9704        let uri = in_memory_doc_uri();
9705        let text = r#"
9706let
9707  y = z
9708in
9709  match y when Some x -> x
9710"#;
9711        let out = execute_semantic_loop_apply_best_quick_fixes(
9712            &uri,
9713            text,
9714            Position {
9715                line: 2,
9716                character: 6,
9717            },
9718            3,
9719            BulkQuickFixStrategy::Aggressive,
9720            false,
9721        )
9722        .expect("bulk output");
9723
9724        let steps = out
9725            .get("steps")
9726            .and_then(Value::as_array)
9727            .expect("steps array");
9728        assert!(!steps.is_empty(), "out: {out:#?}");
9729        for (i, step) in steps.iter().enumerate() {
9730            let diagnostics_before = step
9731                .get("diagnosticsBefore")
9732                .and_then(Value::as_array)
9733                .expect("diagnosticsBefore");
9734            let diagnostics_after = step
9735                .get("diagnosticsAfter")
9736                .and_then(Value::as_array)
9737                .expect("diagnosticsAfter");
9738            let before_count = step
9739                .get("diagnosticsBeforeCount")
9740                .and_then(Value::as_u64)
9741                .expect("diagnosticsBeforeCount");
9742            let after_count = step
9743                .get("diagnosticsAfterCount")
9744                .and_then(Value::as_u64)
9745                .expect("diagnosticsAfterCount");
9746            let delta = step
9747                .get("diagnosticsDelta")
9748                .and_then(Value::as_i64)
9749                .expect("diagnosticsDelta");
9750            assert_eq!(
9751                diagnostics_before.len() as u64,
9752                before_count,
9753                "step[{i}]={step:#?}"
9754            );
9755            assert_eq!(
9756                diagnostics_after.len() as u64,
9757                after_count,
9758                "step[{i}]={step:#?}"
9759            );
9760            assert_eq!(
9761                before_count as i64 - after_count as i64,
9762                delta,
9763                "step[{i}]={step:#?}"
9764            );
9765        }
9766    }
9767
9768    #[test]
9769    fn bulk_strategy_stops_after_requested_step_limit() {
9770        let uri = in_memory_doc_uri();
9771        let text = r#"
9772let
9773  y = z
9774in
9775  match y when Some x -> x
9776"#;
9777        let out = execute_semantic_loop_apply_best_quick_fixes(
9778            &uri,
9779            text,
9780            Position {
9781                line: 2,
9782                character: 6,
9783            },
9784            1,
9785            BulkQuickFixStrategy::Aggressive,
9786            false,
9787        )
9788        .expect("bulk output");
9789        let applied_count = out
9790            .get("appliedCount")
9791            .and_then(Value::as_u64)
9792            .expect("appliedCount");
9793        let steps = out.get("steps").and_then(Value::as_array).expect("steps");
9794        let stopped_reason = out
9795            .get("stoppedReason")
9796            .and_then(Value::as_str)
9797            .expect("stoppedReason");
9798        let stopped_detail = out
9799            .get("stoppedReasonDetail")
9800            .and_then(Value::as_str)
9801            .expect("stoppedReasonDetail");
9802        let seen_states = out
9803            .get("seenStatesCount")
9804            .and_then(Value::as_u64)
9805            .expect("seenStatesCount");
9806        assert_eq!(applied_count, 1, "out: {out:#?}");
9807        assert_eq!(steps.len(), 1, "out: {out:#?}");
9808        assert_eq!(stopped_reason, "maxStepsReached", "out: {out:#?}");
9809        assert!(
9810            stopped_detail.contains("maxSteps"),
9811            "stopped_detail={stopped_detail}; out={out:#?}"
9812        );
9813        assert!(seen_states >= 2, "out: {out:#?}");
9814    }
9815
9816    #[test]
9817    fn bulk_mode_dry_run_reports_flag_and_predicted_text() {
9818        let uri = in_memory_doc_uri();
9819        let text = "let y = z in y";
9820        let out = execute_semantic_loop_apply_best_quick_fixes(
9821            &uri,
9822            text,
9823            Position {
9824                line: 0,
9825                character: 8,
9826            },
9827            2,
9828            BulkQuickFixStrategy::Conservative,
9829            true,
9830        )
9831        .expect("bulk output");
9832        let dry_run = out.get("dryRun").and_then(Value::as_bool).expect("dryRun");
9833        assert!(dry_run, "out: {out:#?}");
9834        let updated_text = out
9835            .get("updatedText")
9836            .and_then(Value::as_str)
9837            .expect("updatedText");
9838        assert_ne!(updated_text, text, "out: {out:#?}");
9839    }
9840
9841    #[test]
9842    fn bulk_progress_guard_no_improvement_streak_logic() {
9843        assert_eq!(next_no_improvement_streak(0, 1), 0);
9844        assert_eq!(next_no_improvement_streak(0, 0), 1);
9845        assert_eq!(next_no_improvement_streak(1, -1), 2);
9846    }
9847
9848    #[test]
9849    fn bulk_progress_guard_cycle_detection_hashes_equal_texts() {
9850        let a = text_state_hash("let y = z in y");
9851        let b = text_state_hash("let y = z in y");
9852        let c = text_state_hash("let y = null in y");
9853        assert_eq!(a, b);
9854        assert_ne!(a, c);
9855    }
9856}