1use std::collections::{BTreeMap, HashSet};
11use std::path::PathBuf;
12use std::sync::{Arc, Mutex};
13use std::time::Instant;
14
15use harn_vm::VmValue;
16
17use super::agents::AgentId;
18use super::file_table::{fnv1a64, FileId};
19use super::imports;
20use super::state::{now_unix_ms, IndexState};
21use super::trigram;
22use super::versions::EditOp;
23use crate::error::HostlibError;
24use crate::tools::args::{
25 build_dict, dict_arg, optional_bool, optional_int_list, optional_string, optional_string_list,
26 require_string, str_value,
27};
28
29pub type SharedIndex = Arc<Mutex<Option<IndexState>>>;
36
37pub(super) const BUILTIN_QUERY: &str = "hostlib_code_index_query";
44pub(super) const BUILTIN_REBUILD: &str = "hostlib_code_index_rebuild";
45pub(super) const BUILTIN_STATS: &str = "hostlib_code_index_stats";
46pub(super) const BUILTIN_IMPORTS_FOR: &str = "hostlib_code_index_imports_for";
47pub(super) const BUILTIN_IMPORTERS_OF: &str = "hostlib_code_index_importers_of";
48
49pub(super) const BUILTIN_PATH_TO_ID: &str = "hostlib_code_index_path_to_id";
50pub(super) const BUILTIN_ID_TO_PATH: &str = "hostlib_code_index_id_to_path";
51pub(super) const BUILTIN_FILE_IDS: &str = "hostlib_code_index_file_ids";
52pub(super) const BUILTIN_FILE_META: &str = "hostlib_code_index_file_meta";
53pub(super) const BUILTIN_FILE_HASH: &str = "hostlib_code_index_file_hash";
54
55pub(super) const BUILTIN_READ_RANGE: &str = "hostlib_code_index_read_range";
56pub(super) const BUILTIN_REINDEX_FILE: &str = "hostlib_code_index_reindex_file";
57pub(super) const BUILTIN_TRIGRAM_QUERY: &str = "hostlib_code_index_trigram_query";
58pub(super) const BUILTIN_EXTRACT_TRIGRAMS: &str = "hostlib_code_index_extract_trigrams";
59pub(super) const BUILTIN_WORD_GET: &str = "hostlib_code_index_word_get";
60pub(super) const BUILTIN_DEPS_GET: &str = "hostlib_code_index_deps_get";
61pub(super) const BUILTIN_OUTLINE_GET: &str = "hostlib_code_index_outline_get";
62
63pub(super) const BUILTIN_CURRENT_SEQ: &str = "hostlib_code_index_current_seq";
64pub(super) const BUILTIN_CHANGES_SINCE: &str = "hostlib_code_index_changes_since";
65pub(super) const BUILTIN_VERSION_RECORD: &str = "hostlib_code_index_version_record";
66
67pub(super) const BUILTIN_AGENT_REGISTER: &str = "hostlib_code_index_agent_register";
68pub(super) const BUILTIN_AGENT_HEARTBEAT: &str = "hostlib_code_index_agent_heartbeat";
69pub(super) const BUILTIN_AGENT_UNREGISTER: &str = "hostlib_code_index_agent_unregister";
70pub(super) const BUILTIN_LOCK_TRY: &str = "hostlib_code_index_lock_try";
71pub(super) const BUILTIN_LOCK_RELEASE: &str = "hostlib_code_index_lock_release";
72pub(super) const BUILTIN_STATUS: &str = "hostlib_code_index_status";
73pub(super) const BUILTIN_CURRENT_AGENT_ID: &str = "hostlib_code_index_current_agent_id";
74
75pub(super) const BUILTIN_CYPHER: &str = "hostlib_code_index_cypher";
76pub(super) const BUILTIN_BRANCH_OVERLAY: &str = "hostlib_code_index_branch_overlay";
77pub(super) const BUILTIN_FRESHNESS: &str = "hostlib_code_index_freshness";
78
79pub(super) fn run_query(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
82 let raw = dict_arg(BUILTIN_QUERY, args)?;
83 let dict = raw.as_ref();
84 let needle = require_string(BUILTIN_QUERY, dict, "needle")?;
85 if needle.is_empty() {
86 return Err(HostlibError::InvalidParameter {
87 builtin: BUILTIN_QUERY,
88 param: "needle",
89 message: "must not be empty".to_string(),
90 });
91 }
92 let case_sensitive = optional_bool(BUILTIN_QUERY, dict, "case_sensitive", false)?;
93 let max_results = optional_positive_usize(BUILTIN_QUERY, dict, "max_results")?.unwrap_or(100);
94 let scope = optional_string_list(BUILTIN_QUERY, dict, "scope")?;
95
96 let guard = index.lock().expect("code_index mutex poisoned");
97 let Some(state) = guard.as_ref() else {
98 return Ok(empty_query_response());
99 };
100
101 let candidate_ids = candidates_for(state, &needle);
102 let mut hits: Vec<Hit> = Vec::new();
103 for id in candidate_ids {
104 let Some(file) = state.files.get(&id) else {
105 continue;
106 };
107 if !scope_allows(&scope, &file.relative_path) {
108 continue;
109 }
110 let Some(text) = read_file_text(&state.root, &file.relative_path) else {
111 continue;
112 };
113 let count = count_matches(&text, &needle, case_sensitive);
114 if count == 0 {
115 continue;
116 }
117 hits.push(Hit {
118 path: file.relative_path.clone(),
119 score: count as f64,
120 match_count: count,
121 });
122 }
123 hits.sort_by(|a, b| {
124 b.match_count
125 .cmp(&a.match_count)
126 .then_with(|| a.path.cmp(&b.path))
127 });
128 let truncated = hits.len() > max_results;
129 if truncated {
130 hits.truncate(max_results);
131 }
132 Ok(build_dict([
133 (
134 "results",
135 VmValue::List(Arc::new(hits.into_iter().map(hit_to_value).collect())),
136 ),
137 ("truncated", VmValue::Bool(truncated)),
138 ]))
139}
140
141pub(super) fn run_rebuild(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
142 let raw = dict_arg(BUILTIN_REBUILD, args)?;
143 let dict = raw.as_ref();
144 let _force = optional_bool(BUILTIN_REBUILD, dict, "force", false)?;
145 let root = optional_string(BUILTIN_REBUILD, dict, "root")?
146 .map(PathBuf::from)
147 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
148 if !root.exists() {
149 return Err(HostlibError::InvalidParameter {
150 builtin: BUILTIN_REBUILD,
151 param: "root",
152 message: format!("path `{}` does not exist", root.display()),
153 });
154 }
155 if !root.is_dir() {
156 return Err(HostlibError::InvalidParameter {
157 builtin: BUILTIN_REBUILD,
158 param: "root",
159 message: format!("path `{}` is not a directory", root.display()),
160 });
161 }
162 let started = Instant::now();
163 let (state, outcome) = IndexState::build_from_root(&root);
164 let elapsed_ms = started.elapsed().as_millis() as i64;
165 {
166 let mut guard = index.lock().expect("code_index mutex poisoned");
167 *guard = Some(state);
168 }
169 Ok(build_dict([
170 ("files_indexed", VmValue::Int(outcome.files_indexed as i64)),
171 ("files_skipped", VmValue::Int(outcome.files_skipped as i64)),
172 ("elapsed_ms", VmValue::Int(elapsed_ms)),
173 ]))
174}
175
176pub(super) fn run_stats(index: &SharedIndex, _args: &[VmValue]) -> Result<VmValue, HostlibError> {
177 let guard = index.lock().expect("code_index mutex poisoned");
178 let Some(state) = guard.as_ref() else {
179 return Ok(empty_stats_response());
180 };
181 Ok(build_dict([
182 ("indexed_files", VmValue::Int(state.files.len() as i64)),
183 (
184 "trigrams",
185 VmValue::Int(state.trigrams.distinct_trigrams() as i64),
186 ),
187 ("words", VmValue::Int(state.words.distinct_words() as i64)),
188 ("memory_bytes", VmValue::Int(state.estimated_bytes() as i64)),
189 (
190 "last_rebuild_unix_ms",
191 VmValue::Int(state.last_built_unix_ms),
192 ),
193 ]))
194}
195
196pub(super) fn run_imports_for(
197 index: &SharedIndex,
198 args: &[VmValue],
199) -> Result<VmValue, HostlibError> {
200 let raw = dict_arg(BUILTIN_IMPORTS_FOR, args)?;
201 let dict = raw.as_ref();
202 let path = require_string(BUILTIN_IMPORTS_FOR, dict, "path")?;
203 let guard = index.lock().expect("code_index mutex poisoned");
204 let Some(state) = guard.as_ref() else {
205 return Ok(empty_imports_response(&path));
206 };
207 let Some(file_id) = state.lookup_path(&path) else {
208 return Ok(empty_imports_response(&path));
209 };
210 let Some(file) = state.files.get(&file_id) else {
211 return Ok(empty_imports_response(&path));
212 };
213 let kind = imports::import_kind(&file.language).to_string();
214 let base_dir = imports::parent_dir(&file.relative_path);
215 let resolved_ids: HashSet<FileId> = state.deps.imports_of(file_id).into_iter().collect();
216 let mut entries: Vec<VmValue> = Vec::with_capacity(file.imports.len());
217 for raw_import in &file.imports {
218 let resolved_path =
219 imports::resolve_module(raw_import, &file.language, &base_dir, &state.path_to_id)
220 .filter(|id| resolved_ids.contains(id))
221 .and_then(|id| state.files.get(&id).map(|f| f.relative_path.clone()));
222 entries.push(import_entry(raw_import, resolved_path.as_deref(), &kind));
223 }
224 Ok(build_dict([
225 ("path", str_value(&file.relative_path)),
226 ("imports", VmValue::List(Arc::new(entries))),
227 ]))
228}
229
230pub(super) fn run_importers_of(
231 index: &SharedIndex,
232 args: &[VmValue],
233) -> Result<VmValue, HostlibError> {
234 let raw = dict_arg(BUILTIN_IMPORTERS_OF, args)?;
235 let dict = raw.as_ref();
236 let module = require_string(BUILTIN_IMPORTERS_OF, dict, "module")?;
237 let guard = index.lock().expect("code_index mutex poisoned");
238 let Some(state) = guard.as_ref() else {
239 return Ok(empty_importers_response(&module));
240 };
241
242 let target_id = state.lookup_path(&module).or_else(|| {
243 let needle = format!("/{module}");
247 state
248 .path_to_id
249 .iter()
250 .find(|(p, _)| p.ends_with(&needle) || *p == &module)
251 .map(|(_, id)| *id)
252 });
253
254 let mut importers: Vec<String> = match target_id {
255 Some(id) => state
256 .deps
257 .importers_of(id)
258 .into_iter()
259 .filter_map(|importer_id| {
260 state
261 .files
262 .get(&importer_id)
263 .map(|f| f.relative_path.clone())
264 })
265 .collect(),
266 None => Vec::new(),
267 };
268 importers.sort();
269 Ok(build_dict([
270 ("module", str_value(&module)),
271 (
272 "importers",
273 VmValue::List(Arc::new(importers.into_iter().map(str_value).collect())),
274 ),
275 ]))
276}
277
278pub(super) fn run_path_to_id(
281 index: &SharedIndex,
282 args: &[VmValue],
283) -> Result<VmValue, HostlibError> {
284 let raw = dict_arg(BUILTIN_PATH_TO_ID, args)?;
285 let path = require_string(BUILTIN_PATH_TO_ID, raw.as_ref(), "path")?;
286 let guard = index.lock().expect("code_index mutex poisoned");
287 let id = guard.as_ref().and_then(|s| s.lookup_path(&path));
288 Ok(match id {
289 Some(id) => VmValue::Int(id as i64),
290 None => VmValue::Nil,
291 })
292}
293
294pub(super) fn run_id_to_path(
295 index: &SharedIndex,
296 args: &[VmValue],
297) -> Result<VmValue, HostlibError> {
298 let raw = dict_arg(BUILTIN_ID_TO_PATH, args)?;
299 let id = require_positive_file_id(BUILTIN_ID_TO_PATH, raw.as_ref(), "file_id")?;
300 let guard = index.lock().expect("code_index mutex poisoned");
301 let path = guard
302 .as_ref()
303 .and_then(|s| s.files.get(&id))
304 .map(|f| f.relative_path.clone());
305 Ok(match path {
306 Some(p) => str_value(&p),
307 None => VmValue::Nil,
308 })
309}
310
311pub(super) fn run_file_ids(
312 index: &SharedIndex,
313 _args: &[VmValue],
314) -> Result<VmValue, HostlibError> {
315 let guard = index.lock().expect("code_index mutex poisoned");
316 let mut ids: Vec<FileId> = guard
317 .as_ref()
318 .map(|s| s.files.keys().copied().collect())
319 .unwrap_or_default();
320 ids.sort_unstable();
321 Ok(VmValue::List(Arc::new(
322 ids.into_iter().map(|id| VmValue::Int(id as i64)).collect(),
323 )))
324}
325
326pub(super) fn run_file_meta(
327 index: &SharedIndex,
328 args: &[VmValue],
329) -> Result<VmValue, HostlibError> {
330 let raw = dict_arg(BUILTIN_FILE_META, args)?;
331 let dict = raw.as_ref();
332 let guard = index.lock().expect("code_index mutex poisoned");
333 let Some(state) = guard.as_ref() else {
334 return Ok(VmValue::Nil);
335 };
336 let id_opt: Option<FileId> = if dict.contains_key("file_id") {
337 Some(require_positive_file_id(
338 BUILTIN_FILE_META,
339 dict,
340 "file_id",
341 )?)
342 } else if let Some(VmValue::String(p)) = dict.get("path") {
343 state.lookup_path(p)
344 } else {
345 return Err(HostlibError::MissingParameter {
346 builtin: BUILTIN_FILE_META,
347 param: "file_id|path",
348 });
349 };
350 let Some(id) = id_opt else {
351 return Ok(VmValue::Nil);
352 };
353 let Some(file) = state.files.get(&id) else {
354 return Ok(VmValue::Nil);
355 };
356 let last_edit_seq = state
357 .versions
358 .last_entry(&file.relative_path)
359 .map(|e| e.seq)
360 .unwrap_or(0);
361 Ok(build_dict([
362 ("id", VmValue::Int(file.id as i64)),
363 ("path", str_value(&file.relative_path)),
364 ("language", str_value(&file.language)),
365 ("size", VmValue::Int(file.size_bytes as i64)),
366 ("line_count", VmValue::Int(file.line_count as i64)),
367 ("hash", str_value(file.content_hash.to_string())),
368 ("mtime_ms", VmValue::Int(file.mtime_ms)),
369 ("last_edit_seq", VmValue::Int(last_edit_seq as i64)),
370 ]))
371}
372
373pub(super) fn run_file_hash(
374 index: &SharedIndex,
375 args: &[VmValue],
376) -> Result<VmValue, HostlibError> {
377 let raw = dict_arg(BUILTIN_FILE_HASH, args)?;
378 let path = require_string(BUILTIN_FILE_HASH, raw.as_ref(), "path")?;
379 let guard = index.lock().expect("code_index mutex poisoned");
380 let Some(state) = guard.as_ref() else {
381 return Ok(VmValue::Nil);
382 };
383 let Some(abs) = state.absolute_path(&path) else {
384 return Ok(VmValue::Nil);
385 };
386 let bytes = match crate::fs::read(&abs, None) {
387 Some(result) => result,
388 None => std::fs::read(&abs),
389 };
390 match bytes {
391 Ok(bytes) => Ok(str_value(fnv1a64(&bytes).to_string())),
392 Err(_) => Ok(VmValue::Nil),
393 }
394}
395
396pub(super) fn run_read_range(
399 index: &SharedIndex,
400 args: &[VmValue],
401) -> Result<VmValue, HostlibError> {
402 let raw = dict_arg(BUILTIN_READ_RANGE, args)?;
403 let dict = raw.as_ref();
404 let path = require_string(BUILTIN_READ_RANGE, dict, "path")?;
405 let start = optional_positive_i64(BUILTIN_READ_RANGE, dict, "start")?;
406 let end = optional_positive_i64(BUILTIN_READ_RANGE, dict, "end")?;
407 let guard = index.lock().expect("code_index mutex poisoned");
408 let abs = match guard.as_ref() {
409 Some(state) => {
410 state
411 .absolute_path(&path)
412 .ok_or_else(|| HostlibError::InvalidParameter {
413 builtin: BUILTIN_READ_RANGE,
414 param: "path",
415 message: "path must stay within the indexed workspace root".to_string(),
416 })?
417 }
418 None => PathBuf::from(&path),
419 };
420 drop(guard);
421
422 let content_result = match crate::fs::read_to_string(&abs, None) {
423 Some(result) => result,
424 None => std::fs::read_to_string(&abs),
425 };
426 let content = match content_result {
427 Ok(s) => s,
428 Err(_) => {
429 return Err(HostlibError::Backend {
430 builtin: BUILTIN_READ_RANGE,
431 message: format!("file not found: {path}"),
432 })
433 }
434 };
435
436 if start.is_none() && end.is_none() {
437 return Ok(build_dict([("content", str_value(&content))]));
438 }
439 let lines: Vec<&str> = content.split('\n').collect();
440 let total = lines.len() as i64;
441 let lo = (start.unwrap_or(1) - 1).max(0) as usize;
442 let hi = end.unwrap_or(total).min(total).max(0) as usize;
443 if lo >= hi {
444 return Ok(build_dict([
445 ("content", str_value("")),
446 ("start", VmValue::Int((lo as i64) + 1)),
447 ("end", VmValue::Int(hi as i64)),
448 ]));
449 }
450 let slice = lines[lo..hi].join("\n");
451 Ok(build_dict([
452 ("content", str_value(&slice)),
453 ("start", VmValue::Int((lo as i64) + 1)),
454 ("end", VmValue::Int(hi as i64)),
455 ]))
456}
457
458pub(super) fn run_reindex_file(
459 index: &SharedIndex,
460 args: &[VmValue],
461) -> Result<VmValue, HostlibError> {
462 let raw = dict_arg(BUILTIN_REINDEX_FILE, args)?;
463 let path = require_string(BUILTIN_REINDEX_FILE, raw.as_ref(), "path")?;
464 let mut guard = index.lock().expect("code_index mutex poisoned");
465 let Some(state) = guard.as_mut() else {
466 return Ok(build_dict([
467 ("indexed", VmValue::Bool(false)),
468 ("file_id", VmValue::Nil),
469 ]));
470 };
471 let Some(abs) = state.absolute_path(&path) else {
472 return Err(HostlibError::InvalidParameter {
473 builtin: BUILTIN_REINDEX_FILE,
474 param: "path",
475 message: "path must stay within the indexed workspace root".to_string(),
476 });
477 };
478 let id = state.reindex_file(&abs);
479 Ok(build_dict([
480 ("indexed", VmValue::Bool(id.is_some())),
481 (
482 "file_id",
483 id.map(|i| VmValue::Int(i as i64)).unwrap_or(VmValue::Nil),
484 ),
485 ]))
486}
487
488pub(super) fn run_trigram_query(
489 index: &SharedIndex,
490 args: &[VmValue],
491) -> Result<VmValue, HostlibError> {
492 let raw = dict_arg(BUILTIN_TRIGRAM_QUERY, args)?;
493 let dict = raw.as_ref();
494 let trigrams_raw = optional_int_list(BUILTIN_TRIGRAM_QUERY, dict, "trigrams")?;
495 let max_files = optional_positive_usize(BUILTIN_TRIGRAM_QUERY, dict, "max_files")?;
496 let mut trigrams = Vec::with_capacity(trigrams_raw.len());
497 for n in trigrams_raw {
498 if n < 0 {
499 return Err(HostlibError::InvalidParameter {
500 builtin: BUILTIN_TRIGRAM_QUERY,
501 param: "trigrams",
502 message: "entries must be >= 0".to_string(),
503 });
504 }
505 trigrams.push(n as u32);
506 }
507 let guard = index.lock().expect("code_index mutex poisoned");
508 let mut ids: Vec<FileId> = match guard.as_ref() {
509 Some(state) => state.trigrams.query(&trigrams).into_iter().collect(),
510 None => Vec::new(),
511 };
512 ids.sort_unstable();
513 if let Some(limit) = max_files {
514 ids.truncate(limit);
515 }
516 Ok(VmValue::List(Arc::new(
517 ids.into_iter().map(|id| VmValue::Int(id as i64)).collect(),
518 )))
519}
520
521pub(super) fn run_extract_trigrams(
522 _index: &SharedIndex,
523 args: &[VmValue],
524) -> Result<VmValue, HostlibError> {
525 let raw = dict_arg(BUILTIN_EXTRACT_TRIGRAMS, args)?;
526 let query = require_string(BUILTIN_EXTRACT_TRIGRAMS, raw.as_ref(), "query")?;
527 let mut tgs = trigram::query_trigrams(&query);
528 tgs.sort_unstable();
529 Ok(VmValue::List(Arc::new(
530 tgs.into_iter().map(|n| VmValue::Int(n as i64)).collect(),
531 )))
532}
533
534pub(super) fn run_word_get(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
535 let raw = dict_arg(BUILTIN_WORD_GET, args)?;
536 let word = require_string(BUILTIN_WORD_GET, raw.as_ref(), "word")?;
537 let guard = index.lock().expect("code_index mutex poisoned");
538 let hits: Vec<VmValue> = match guard.as_ref() {
539 Some(state) => state
540 .words
541 .get(&word)
542 .iter()
543 .map(|h| {
544 build_dict([
545 ("file_id", VmValue::Int(h.file as i64)),
546 ("line", VmValue::Int(h.line as i64)),
547 ])
548 })
549 .collect(),
550 None => Vec::new(),
551 };
552 Ok(VmValue::List(Arc::new(hits)))
553}
554
555pub(super) fn run_deps_get(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
556 let raw = dict_arg(BUILTIN_DEPS_GET, args)?;
557 let dict = raw.as_ref();
558 let id = require_positive_file_id(BUILTIN_DEPS_GET, dict, "file_id")?;
559 let direction = optional_string(BUILTIN_DEPS_GET, dict, "direction")?
560 .unwrap_or_else(|| "importers".to_string());
561 let guard = index.lock().expect("code_index mutex poisoned");
562 let mut neighbors: Vec<FileId> = match guard.as_ref() {
563 Some(state) => match direction.as_str() {
564 "importers" => state.deps.importers_of(id),
565 "imports" => state.deps.imports_of(id),
566 _ => {
567 return Err(HostlibError::InvalidParameter {
568 builtin: BUILTIN_DEPS_GET,
569 param: "direction",
570 message: format!("expected \"importers\" or \"imports\", got {direction:?}"),
571 })
572 }
573 },
574 None => Vec::new(),
575 };
576 neighbors.sort_unstable();
577 Ok(VmValue::List(Arc::new(
578 neighbors
579 .into_iter()
580 .map(|id| VmValue::Int(id as i64))
581 .collect(),
582 )))
583}
584
585pub(super) fn run_outline_get(
586 index: &SharedIndex,
587 args: &[VmValue],
588) -> Result<VmValue, HostlibError> {
589 let raw = dict_arg(BUILTIN_OUTLINE_GET, args)?;
590 let id = require_positive_file_id(BUILTIN_OUTLINE_GET, raw.as_ref(), "file_id")?;
591 let guard = index.lock().expect("code_index mutex poisoned");
592 let symbols: Vec<VmValue> = match guard.as_ref().and_then(|s| s.files.get(&id)) {
593 Some(file) => file
594 .symbols
595 .iter()
596 .map(|sym| {
597 build_dict([
598 ("name", str_value(&sym.name)),
599 ("kind", str_value(&sym.kind)),
600 ("start_line", VmValue::Int(sym.start_line as i64)),
601 ("end_line", VmValue::Int(sym.end_line as i64)),
602 ("signature", str_value(&sym.signature)),
603 ])
604 })
605 .collect(),
606 None => Vec::new(),
607 };
608 Ok(VmValue::List(Arc::new(symbols)))
609}
610
611pub(super) fn run_current_seq(
614 index: &SharedIndex,
615 _args: &[VmValue],
616) -> Result<VmValue, HostlibError> {
617 let guard = index.lock().expect("code_index mutex poisoned");
618 let seq = guard.as_ref().map(|s| s.versions.current_seq).unwrap_or(0);
619 Ok(VmValue::Int(seq as i64))
620}
621
622pub(super) fn run_changes_since(
623 index: &SharedIndex,
624 args: &[VmValue],
625) -> Result<VmValue, HostlibError> {
626 let raw = dict_arg(BUILTIN_CHANGES_SINCE, args)?;
627 let dict = raw.as_ref();
628 let seq = optional_non_negative_u64(BUILTIN_CHANGES_SINCE, dict, "seq", 0)?;
629 let limit = optional_positive_usize(BUILTIN_CHANGES_SINCE, dict, "limit")?;
630 let guard = index.lock().expect("code_index mutex poisoned");
631 let records = match guard.as_ref() {
632 Some(state) => state.versions.changes_since(seq, limit),
633 None => Vec::new(),
634 };
635 Ok(VmValue::List(Arc::new(
636 records
637 .into_iter()
638 .map(|r| {
639 build_dict([
640 ("path", str_value(&r.path)),
641 ("seq", VmValue::Int(r.seq as i64)),
642 ("agent_id", VmValue::Int(r.agent_id as i64)),
643 ("op", str_value(r.op.as_str())),
644 ("hash", str_value(r.hash.to_string())),
645 ("size", VmValue::Int(r.size as i64)),
646 ("timestamp_ms", VmValue::Int(r.timestamp_ms)),
647 ])
648 })
649 .collect(),
650 )))
651}
652
653pub(super) fn run_version_record(
654 index: &SharedIndex,
655 args: &[VmValue],
656) -> Result<VmValue, HostlibError> {
657 let raw = dict_arg(BUILTIN_VERSION_RECORD, args)?;
658 let dict = raw.as_ref();
659 let agent_id = require_non_negative_u64(BUILTIN_VERSION_RECORD, dict, "agent_id")?;
660 let path = require_string(BUILTIN_VERSION_RECORD, dict, "path")?;
661 let op_str =
662 optional_string(BUILTIN_VERSION_RECORD, dict, "op")?.unwrap_or_else(|| "write".to_string());
663 let op = EditOp::parse(&op_str).unwrap_or(EditOp::Write);
664 let hash = parse_hash(BUILTIN_VERSION_RECORD, dict, "hash")?;
665 let size = optional_non_negative_u64(BUILTIN_VERSION_RECORD, dict, "size", 0)?;
666 let now = now_unix_ms();
667 let mut guard = index.lock().expect("code_index mutex poisoned");
668 let state = ensure_state(BUILTIN_VERSION_RECORD, &mut guard)?;
669 let normalized = normalize_relative_path(state, &path);
670 let seq = state
671 .versions
672 .record(normalized, agent_id, op, hash, size, now);
673 state.agents.note_edit(agent_id, now);
674 Ok(VmValue::Int(seq as i64))
675}
676
677pub(super) fn run_agent_register(
680 index: &SharedIndex,
681 args: &[VmValue],
682) -> Result<VmValue, HostlibError> {
683 let raw = dict_arg(BUILTIN_AGENT_REGISTER, args)?;
684 let dict = raw.as_ref();
685 let name = optional_string(BUILTIN_AGENT_REGISTER, dict, "name")?
686 .unwrap_or_else(|| "agent".to_string());
687 let requested_id = optional_positive_u64(BUILTIN_AGENT_REGISTER, dict, "agent_id")?;
688 let now = now_unix_ms();
689 let mut guard = index.lock().expect("code_index mutex poisoned");
690 let state = ensure_state(BUILTIN_AGENT_REGISTER, &mut guard)?;
691 let id = match requested_id {
692 Some(id) => state.agents.register_with_id(id, name, now),
693 None => state.agents.register(name, now),
694 };
695 Ok(VmValue::Int(id as i64))
696}
697
698pub(super) fn run_agent_heartbeat(
699 index: &SharedIndex,
700 args: &[VmValue],
701) -> Result<VmValue, HostlibError> {
702 let raw = dict_arg(BUILTIN_AGENT_HEARTBEAT, args)?;
703 let id = require_positive_u64(BUILTIN_AGENT_HEARTBEAT, raw.as_ref(), "agent_id")?;
704 let now = now_unix_ms();
705 let mut guard = index.lock().expect("code_index mutex poisoned");
706 let state = ensure_state(BUILTIN_AGENT_HEARTBEAT, &mut guard)?;
707 state.agents.heartbeat(id, now);
708 Ok(VmValue::Bool(true))
709}
710
711pub(super) fn run_agent_unregister(
712 index: &SharedIndex,
713 args: &[VmValue],
714) -> Result<VmValue, HostlibError> {
715 let raw = dict_arg(BUILTIN_AGENT_UNREGISTER, args)?;
716 let id = require_positive_u64(BUILTIN_AGENT_UNREGISTER, raw.as_ref(), "agent_id")?;
717 let mut guard = index.lock().expect("code_index mutex poisoned");
718 let state = ensure_state(BUILTIN_AGENT_UNREGISTER, &mut guard)?;
719 state.agents.unregister(id);
720 Ok(VmValue::Bool(true))
721}
722
723pub(super) fn run_lock_try(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
724 let raw = dict_arg(BUILTIN_LOCK_TRY, args)?;
725 let dict = raw.as_ref();
726 let agent_id = require_positive_u64(BUILTIN_LOCK_TRY, dict, "agent_id")?;
727 let path = require_string(BUILTIN_LOCK_TRY, dict, "path")?;
728 let ttl = optional_positive_i64(BUILTIN_LOCK_TRY, dict, "ttl_ms")?;
729 let now = now_unix_ms();
730 let mut guard = index.lock().expect("code_index mutex poisoned");
731 let state = ensure_state(BUILTIN_LOCK_TRY, &mut guard)?;
732 let granted = state.agents.try_lock(agent_id, &path, ttl, now);
733 if granted {
734 return Ok(build_dict([
735 ("locked", VmValue::Bool(true)),
736 ("holder", VmValue::Int(agent_id as i64)),
737 ]));
738 }
739 let holder = state.agents.lock_holder(&path, now);
740 Ok(build_dict([
741 ("locked", VmValue::Bool(false)),
742 (
743 "holder",
744 holder
745 .map(|id| VmValue::Int(id as i64))
746 .unwrap_or(VmValue::Nil),
747 ),
748 ]))
749}
750
751pub(super) fn run_lock_release(
752 index: &SharedIndex,
753 args: &[VmValue],
754) -> Result<VmValue, HostlibError> {
755 let raw = dict_arg(BUILTIN_LOCK_RELEASE, args)?;
756 let dict = raw.as_ref();
757 let agent_id = require_positive_u64(BUILTIN_LOCK_RELEASE, dict, "agent_id")?;
758 let path = require_string(BUILTIN_LOCK_RELEASE, dict, "path")?;
759 let mut guard = index.lock().expect("code_index mutex poisoned");
760 let state = ensure_state(BUILTIN_LOCK_RELEASE, &mut guard)?;
761 state.agents.release_lock(agent_id, &path);
762 Ok(VmValue::Bool(true))
763}
764
765pub(super) fn run_status(index: &SharedIndex, _args: &[VmValue]) -> Result<VmValue, HostlibError> {
766 let guard = index.lock().expect("code_index mutex poisoned");
767 match guard.as_ref() {
768 Some(state) => Ok(build_dict([
769 ("file_count", VmValue::Int(state.files.len() as i64)),
770 (
771 "current_seq",
772 VmValue::Int(state.versions.current_seq as i64),
773 ),
774 ("last_indexed_at_ms", VmValue::Int(state.last_built_unix_ms)),
775 (
776 "git_head",
777 state
778 .git_head
779 .as_deref()
780 .map(str_value)
781 .unwrap_or(VmValue::Nil),
782 ),
783 (
784 "agents",
785 VmValue::List(Arc::new(
786 state
787 .agents
788 .agents()
789 .map(|info| {
790 build_dict([
791 ("id", VmValue::Int(info.id as i64)),
792 ("name", str_value(&info.name)),
793 (
794 "state",
795 str_value(match info.state {
796 super::agents::AgentState::Active => "active",
797 super::agents::AgentState::Crashed => "crashed",
798 super::agents::AgentState::Gone => "gone",
799 }),
800 ),
801 ("last_seen_ms", VmValue::Int(info.last_seen_ms)),
802 ("edit_count", VmValue::Int(info.edit_count as i64)),
803 ("lock_count", VmValue::Int(info.locked_paths.len() as i64)),
804 ])
805 })
806 .collect(),
807 )),
808 ),
809 ])),
810 None => Ok(build_dict([
811 ("file_count", VmValue::Int(0)),
812 ("current_seq", VmValue::Int(0)),
813 ("last_indexed_at_ms", VmValue::Int(0)),
814 ("git_head", VmValue::Nil),
815 ("agents", VmValue::List(Arc::new(Vec::new()))),
816 ])),
817 }
818}
819
820pub(super) fn run_current_agent_id(
821 slot: &Arc<Mutex<Option<AgentId>>>,
822 _args: &[VmValue],
823) -> Result<VmValue, HostlibError> {
824 let guard = slot.lock().expect("current_agent slot poisoned");
825 Ok(match *guard {
826 Some(id) => VmValue::Int(id as i64),
827 None => VmValue::Nil,
828 })
829}
830
831pub(super) fn run_cypher(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
834 let raw = dict_arg(BUILTIN_CYPHER, args)?;
835 let dict = raw.as_ref();
836 let query = require_string(BUILTIN_CYPHER, dict, "query")?;
837
838 let guard = index.lock().expect("code_index mutex poisoned");
839 let Some(state) = guard.as_ref() else {
840 return Ok(build_dict([
841 ("rows", VmValue::List(Arc::new(Vec::new()))),
842 ("overlay", VmValue::Nil),
843 ]));
844 };
845
846 let graph = state.overlays.graph(&state.symbols);
847 let rows = super::cypher::execute(&query, graph).map_err(|err| HostlibError::Backend {
848 builtin: BUILTIN_CYPHER,
849 message: err.to_string(),
850 })?;
851
852 let rows_vm: Vec<VmValue> = rows
853 .into_iter()
854 .map(|row| {
855 let mut map: BTreeMap<String, VmValue> = BTreeMap::new();
856 for (k, v) in row {
857 map.insert(k, v.to_vm());
858 }
859 VmValue::Dict(Arc::new(map))
860 })
861 .collect();
862
863 Ok(build_dict([
864 ("rows", VmValue::List(Arc::new(rows_vm))),
865 (
866 "overlay",
867 match state.overlays.active() {
868 Some(name) => str_value(name),
869 None => VmValue::Nil,
870 },
871 ),
872 ]))
873}
874
875pub(super) fn run_branch_overlay(
876 index: &SharedIndex,
877 args: &[VmValue],
878) -> Result<VmValue, HostlibError> {
879 let raw = dict_arg(BUILTIN_BRANCH_OVERLAY, args)?;
880 let dict = raw.as_ref();
881 let branch = optional_string(BUILTIN_BRANCH_OVERLAY, dict, "branch")?;
882 let activate = optional_bool(BUILTIN_BRANCH_OVERLAY, dict, "activate", true)?;
883 let action = optional_string(BUILTIN_BRANCH_OVERLAY, dict, "action")?;
884
885 let mut guard = index.lock().expect("code_index mutex poisoned");
886 let state = ensure_state(BUILTIN_BRANCH_OVERLAY, &mut guard)?;
887
888 let mut reuse: f64 = 1.0;
889 match action.as_deref().unwrap_or("activate") {
890 "deactivate" => {
891 state.overlays.activate(None);
892 }
893 "create" => {
894 let branch_name = branch.ok_or(HostlibError::MissingParameter {
895 builtin: BUILTIN_BRANCH_OVERLAY,
896 param: "branch",
897 })?;
898 let mut overlay = super::overlay::BranchOverlay::new(&branch_name);
899 overlay.materialize(&state.symbols);
900 state.overlays.set(overlay);
901 if activate {
902 state.overlays.activate(Some(branch_name));
903 }
904 reuse = state.overlays.reuse_fraction(&state.symbols);
905 }
906 "activate" => {
907 let branch_name = branch.ok_or(HostlibError::MissingParameter {
908 builtin: BUILTIN_BRANCH_OVERLAY,
909 param: "branch",
910 })?;
911 if state.overlays.get(&branch_name).is_none() {
915 let mut overlay = super::overlay::BranchOverlay::new(&branch_name);
916 overlay.materialize(&state.symbols);
917 state.overlays.set(overlay);
918 }
919 state.overlays.activate(Some(branch_name));
920 reuse = state.overlays.reuse_fraction(&state.symbols);
921 }
922 other => {
923 return Err(HostlibError::InvalidParameter {
924 builtin: BUILTIN_BRANCH_OVERLAY,
925 param: "action",
926 message: format!("expected one of activate|deactivate|create, got `{other}`"),
927 })
928 }
929 }
930
931 Ok(build_dict([
932 (
933 "active",
934 match state.overlays.active() {
935 Some(name) => str_value(name),
936 None => VmValue::Nil,
937 },
938 ),
939 ("reuse_fraction", VmValue::Float(reuse)),
940 ]))
941}
942
943pub(super) fn run_freshness(
944 index: &SharedIndex,
945 args: &[VmValue],
946) -> Result<VmValue, HostlibError> {
947 let raw = dict_arg(BUILTIN_FRESHNESS, args)?;
948 let dict = raw.as_ref();
949 let path = require_string(BUILTIN_FRESHNESS, dict, "path")?;
950
951 let guard = index.lock().expect("code_index mutex poisoned");
952 let state = guard.as_ref().ok_or_else(|| HostlibError::Backend {
953 builtin: BUILTIN_FRESHNESS,
954 message: "code index has not been initialised — call \
955 `hostlib_code_index_rebuild` first"
956 .to_string(),
957 })?;
958
959 let normalized = normalize_relative_path(state, &path);
960 let file = state
961 .lookup_path(&normalized)
962 .and_then(|id| state.files.get(&id));
963 let Some(file) = file else {
964 return Ok(unknown_freshness_response(&path));
965 };
966
967 let abs = state.root.join(&file.relative_path);
968 let (disk_mtime, disk_hash) = match std::fs::read(&abs) {
969 Ok(bytes) => {
970 let hash = fnv1a64(&bytes);
971 let mtime = std::fs::metadata(&abs)
972 .ok()
973 .and_then(|m| m.modified().ok())
974 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
975 .map(|d| d.as_millis() as i64)
976 .unwrap_or(0);
977 (mtime, Some(hash))
978 }
979 Err(_) => (0, None),
980 };
981 let stale = disk_hash != Some(file.content_hash);
982 Ok(build_dict([
983 ("path", str_value(&file.relative_path)),
984 ("known", VmValue::Bool(true)),
985 ("stale", VmValue::Bool(stale)),
986 (
987 "indexed_hash",
988 VmValue::String(Arc::from(format!("{:016x}", file.content_hash).as_str())),
989 ),
990 ("indexed_mtime_ms", VmValue::Int(file.mtime_ms)),
991 (
992 "disk_hash",
993 match disk_hash {
994 Some(h) => VmValue::String(Arc::from(format!("{h:016x}").as_str())),
995 None => VmValue::Nil,
996 },
997 ),
998 ("disk_mtime_ms", VmValue::Int(disk_mtime)),
999 ]))
1000}
1001
1002fn ensure_state<'a>(
1005 builtin: &'static str,
1006 guard: &'a mut std::sync::MutexGuard<'_, Option<IndexState>>,
1007) -> Result<&'a mut IndexState, HostlibError> {
1008 if guard.is_none() {
1009 return Err(HostlibError::Backend {
1010 builtin,
1011 message: "code index has not been initialised — call \
1012 `hostlib_code_index_rebuild` or restore from a snapshot first"
1013 .to_string(),
1014 });
1015 }
1016 Ok(guard.as_mut().unwrap())
1017}
1018
1019fn parse_hash(
1020 builtin: &'static str,
1021 dict: &BTreeMap<String, VmValue>,
1022 key: &'static str,
1023) -> Result<u64, HostlibError> {
1024 match dict.get(key) {
1025 None | Some(VmValue::Nil) => Ok(0),
1026 Some(VmValue::Int(n)) if *n >= 0 => Ok(*n as u64),
1027 Some(VmValue::Int(n)) => Err(HostlibError::InvalidParameter {
1028 builtin,
1029 param: key,
1030 message: format!("must be >= 0, got {n}"),
1031 }),
1032 Some(VmValue::String(s)) => s
1033 .parse::<u64>()
1034 .map_err(|_| HostlibError::InvalidParameter {
1035 builtin,
1036 param: key,
1037 message: format!("expected u64-parseable string, got {s:?}"),
1038 }),
1039 Some(other) => Err(HostlibError::InvalidParameter {
1040 builtin,
1041 param: key,
1042 message: format!(
1043 "expected integer or numeric string, got {}",
1044 other.type_name()
1045 ),
1046 }),
1047 }
1048}
1049
1050fn require_positive_u64(
1051 builtin: &'static str,
1052 dict: &BTreeMap<String, VmValue>,
1053 key: &'static str,
1054) -> Result<u64, HostlibError> {
1055 let raw = require_non_negative_u64(builtin, dict, key)?;
1056 if raw == 0 {
1057 return Err(HostlibError::InvalidParameter {
1058 builtin,
1059 param: key,
1060 message: "must be >= 1".to_string(),
1061 });
1062 }
1063 Ok(raw)
1064}
1065
1066fn require_positive_file_id(
1067 builtin: &'static str,
1068 dict: &BTreeMap<String, VmValue>,
1069 key: &'static str,
1070) -> Result<FileId, HostlibError> {
1071 let raw = require_positive_u64(builtin, dict, key)?;
1072 FileId::try_from(raw).map_err(|_| HostlibError::InvalidParameter {
1073 builtin,
1074 param: key,
1075 message: "does not fit in file id".to_string(),
1076 })
1077}
1078
1079fn require_non_negative_u64(
1080 builtin: &'static str,
1081 dict: &BTreeMap<String, VmValue>,
1082 key: &'static str,
1083) -> Result<u64, HostlibError> {
1084 match dict.get(key) {
1085 Some(VmValue::Int(n)) if *n >= 0 => Ok(*n as u64),
1086 Some(VmValue::Float(f)) if f.fract() == 0.0 && *f >= 0.0 => Ok(*f as u64),
1087 Some(VmValue::Int(n)) => Err(HostlibError::InvalidParameter {
1088 builtin,
1089 param: key,
1090 message: format!("must be >= 0, got {n}"),
1091 }),
1092 Some(other) => Err(HostlibError::InvalidParameter {
1093 builtin,
1094 param: key,
1095 message: format!("expected integer, got {}", other.type_name()),
1096 }),
1097 None => Err(HostlibError::MissingParameter {
1098 builtin,
1099 param: key,
1100 }),
1101 }
1102}
1103
1104fn optional_positive_u64(
1105 builtin: &'static str,
1106 dict: &BTreeMap<String, VmValue>,
1107 key: &'static str,
1108) -> Result<Option<u64>, HostlibError> {
1109 match dict.get(key) {
1110 None | Some(VmValue::Nil) => Ok(None),
1111 Some(_) => require_positive_u64(builtin, dict, key).map(Some),
1112 }
1113}
1114
1115fn optional_non_negative_u64(
1116 builtin: &'static str,
1117 dict: &BTreeMap<String, VmValue>,
1118 key: &'static str,
1119 default: u64,
1120) -> Result<u64, HostlibError> {
1121 match dict.get(key) {
1122 None | Some(VmValue::Nil) => Ok(default),
1123 Some(_) => require_non_negative_u64(builtin, dict, key),
1124 }
1125}
1126
1127fn optional_positive_i64(
1128 builtin: &'static str,
1129 dict: &BTreeMap<String, VmValue>,
1130 key: &'static str,
1131) -> Result<Option<i64>, HostlibError> {
1132 match dict.get(key) {
1133 None | Some(VmValue::Nil) => Ok(None),
1134 Some(VmValue::Int(n)) if *n >= 1 => Ok(Some(*n)),
1135 Some(VmValue::Float(f)) if f.fract() == 0.0 && *f >= 1.0 => Ok(Some(*f as i64)),
1136 Some(VmValue::Int(n)) => Err(HostlibError::InvalidParameter {
1137 builtin,
1138 param: key,
1139 message: format!("must be >= 1, got {n}"),
1140 }),
1141 Some(other) => Err(HostlibError::InvalidParameter {
1142 builtin,
1143 param: key,
1144 message: format!("expected integer, got {}", other.type_name()),
1145 }),
1146 }
1147}
1148
1149fn optional_positive_usize(
1150 builtin: &'static str,
1151 dict: &BTreeMap<String, VmValue>,
1152 key: &'static str,
1153) -> Result<Option<usize>, HostlibError> {
1154 match optional_positive_u64(builtin, dict, key)? {
1155 Some(value) => {
1156 usize::try_from(value)
1157 .map(Some)
1158 .map_err(|_| HostlibError::InvalidParameter {
1159 builtin,
1160 param: key,
1161 message: "does not fit in usize".to_string(),
1162 })
1163 }
1164 None => Ok(None),
1165 }
1166}
1167
1168pub(super) fn normalize_relative_path_for(state: &IndexState, path: &str) -> String {
1174 normalize_relative_path(state, path)
1175}
1176
1177fn normalize_relative_path(state: &IndexState, path: &str) -> String {
1178 if let Some(rel) = state
1179 .lookup_path(path)
1180 .and_then(|id| state.files.get(&id))
1181 .map(|f| f.relative_path.clone())
1182 {
1183 return rel;
1184 }
1185 let p = std::path::Path::new(path);
1186 if p.is_absolute() {
1187 if let Ok(rel) = p.strip_prefix(&state.root) {
1188 return rel.to_string_lossy().replace('\\', "/");
1189 }
1190 }
1191 path.to_string()
1192}
1193
1194fn candidates_for(state: &IndexState, needle: &str) -> Vec<FileId> {
1195 if needle.len() >= 3 {
1196 let trigrams = trigram::query_trigrams(needle);
1197 return state.trigrams.query(&trigrams).into_iter().collect();
1198 }
1199 state.files.keys().copied().collect()
1200}
1201
1202fn read_file_text(root: &std::path::Path, relative: &str) -> Option<String> {
1203 let path = root.join(relative);
1204 match crate::fs::read_to_string(&path, None) {
1205 Some(result) => result.ok(),
1206 None => std::fs::read_to_string(path).ok(),
1207 }
1208}
1209
1210fn count_matches(haystack: &str, needle: &str, case_sensitive: bool) -> u64 {
1211 if case_sensitive {
1212 haystack.matches(needle).count() as u64
1213 } else {
1214 let lower_h = haystack.to_lowercase();
1215 let lower_n = needle.to_lowercase();
1216 lower_h.matches(&lower_n).count() as u64
1217 }
1218}
1219
1220fn scope_allows(scope: &[String], relative: &str) -> bool {
1221 if scope.is_empty() {
1222 return true;
1223 }
1224 scope
1225 .iter()
1226 .any(|s| relative == s || relative.starts_with(&format!("{s}/")) || s.is_empty())
1227}
1228
1229struct Hit {
1230 path: String,
1231 score: f64,
1232 match_count: u64,
1233}
1234
1235fn hit_to_value(hit: Hit) -> VmValue {
1236 let Hit {
1237 path,
1238 score,
1239 match_count,
1240 } = hit;
1241 build_dict([
1242 ("path", str_value(&path)),
1243 ("score", VmValue::Float(score)),
1244 ("match_count", VmValue::Int(match_count as i64)),
1245 ])
1246}
1247
1248fn import_entry(module: &str, resolved: Option<&str>, kind: &str) -> VmValue {
1249 let mut map: BTreeMap<String, VmValue> = BTreeMap::new();
1250 map.insert("module".into(), str_value(module));
1251 map.insert(
1252 "resolved_path".into(),
1253 match resolved {
1254 Some(p) => str_value(p),
1255 None => VmValue::Nil,
1256 },
1257 );
1258 map.insert("kind".into(), str_value(kind));
1259 VmValue::Dict(Arc::new(map))
1260}
1261
1262fn empty_query_response() -> VmValue {
1263 build_dict([
1264 ("results", VmValue::List(Arc::new(Vec::new()))),
1265 ("truncated", VmValue::Bool(false)),
1266 ])
1267}
1268
1269fn empty_stats_response() -> VmValue {
1270 build_dict([
1271 ("indexed_files", VmValue::Int(0)),
1272 ("trigrams", VmValue::Int(0)),
1273 ("words", VmValue::Int(0)),
1274 ("memory_bytes", VmValue::Int(0)),
1275 ("last_rebuild_unix_ms", VmValue::Nil),
1276 ])
1277}
1278
1279fn empty_imports_response(path: &str) -> VmValue {
1280 build_dict([
1281 ("path", str_value(path)),
1282 ("imports", VmValue::List(Arc::new(Vec::new()))),
1283 ])
1284}
1285
1286fn empty_importers_response(module: &str) -> VmValue {
1287 build_dict([
1288 ("module", str_value(module)),
1289 ("importers", VmValue::List(Arc::new(Vec::new()))),
1290 ])
1291}
1292
1293fn unknown_freshness_response(path: &str) -> VmValue {
1294 build_dict([
1295 ("path", str_value(path)),
1296 ("known", VmValue::Bool(false)),
1297 ("stale", VmValue::Bool(true)),
1298 ("indexed_hash", VmValue::Nil),
1299 ("indexed_mtime_ms", VmValue::Nil),
1300 ("disk_hash", VmValue::Nil),
1301 ("disk_mtime_ms", VmValue::Nil),
1302 ])
1303}