1use serde::{Deserialize, Serialize};
38use serde_json::{json, Value};
39use std::collections::HashMap;
40use std::io::{self, BufRead, BufReader, Read, Write};
41use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
42use std::sync::{Arc, Condvar, Mutex};
43use std::thread;
44
45use crate::debugger::DebugAction;
46
47const MAX_VAR_REPR: usize = 200;
48
49#[derive(Debug, Serialize, Deserialize)]
52struct DapRequest {
53 seq: u64,
54 #[serde(rename = "type")]
55 msg_type: String,
56 command: String,
57 #[serde(default)]
58 arguments: Value,
59}
60
61#[derive(Default, Clone)]
67pub(crate) struct PauseSnapshot {
68 pub file: String,
69 pub line: usize,
70 pub reason: String, pub frames: Vec<FrameSnap>,
72 pub locals: Vec<VarSnap>,
73 pub globals: Vec<VarSnap>,
74 pub var_ref_map: HashMap<u32, Vec<VarChild>>,
78}
79
80#[derive(Clone)]
81pub(crate) struct FrameSnap {
82 pub name: String,
83 pub file: String,
84 pub line: usize,
85}
86
87#[derive(Clone)]
88pub(crate) struct VarSnap {
89 pub name: String, pub repr: String,
91 pub kind: String, pub var_ref: u32,
96}
97
98#[derive(Clone)]
100pub(crate) struct VarChild {
101 pub name: String,
102 pub repr: String,
103 pub var_ref: u32,
106}
107
108struct SharedInner {
109 pending_action: Option<DebugAction>,
110 is_paused: bool,
111 snapshot: PauseSnapshot,
112 pause_request: bool, }
114pub struct DapShared {
116 inner: Mutex<SharedInner>,
118 cv: Condvar,
120 seq: AtomicU64,
122 writer: Mutex<Box<dyn Write + Send>>,
123 pub configuration_done: AtomicBool,
125 pub disconnected: AtomicBool,
127}
128
129impl DapShared {
130 fn new(writer: Box<dyn Write + Send>) -> Arc<Self> {
131 Arc::new(Self {
132 inner: Mutex::new(SharedInner {
133 pending_action: None,
134 is_paused: false,
135 snapshot: PauseSnapshot::default(),
136 pause_request: false,
137 }),
138 cv: Condvar::new(),
139 seq: AtomicU64::new(1),
140 writer: Mutex::new(writer),
141 configuration_done: AtomicBool::new(false),
142 disconnected: AtomicBool::new(false),
143 })
144 }
145
146 pub(crate) fn pause(&self, snap: PauseSnapshot) -> DebugAction {
149 let _ = std::io::Write::flush(&mut std::io::stdout());
156 let _ = std::io::Write::flush(&mut std::io::stderr());
157 {
158 let mut s = self.inner.lock().expect("dap lock");
159 s.snapshot = snap.clone();
160 s.is_paused = true;
161 s.pending_action = None;
162 s.pause_request = false;
163 }
164 self.emit_event(
165 "stopped",
166 json!({
167 "reason": snap.reason,
168 "threadId": 1,
169 "allThreadsStopped": true,
170 "preserveFocusHint": false,
171 "description": snap.reason,
172 "text": format!("{}:{}", snap.file, snap.line),
173 }),
174 );
175 let mut guard = self.inner.lock().expect("dap lock");
176 while guard.pending_action.is_none() && !self.disconnected.load(Ordering::SeqCst) {
177 guard = self.cv.wait(guard).expect("dap cv");
178 }
179 let action = guard.pending_action.take().unwrap_or(DebugAction::Continue);
180 guard.is_paused = false;
181 action
182 }
183 pub fn was_disconnected(&self) -> bool {
185 self.disconnected.load(Ordering::SeqCst)
186 }
187 pub fn want_pause(&self) -> bool {
189 self.inner.lock().map(|g| g.pause_request).unwrap_or(false)
190 }
191
192 fn resume_with(&self, action: DebugAction) {
193 let mut g = self.inner.lock().expect("dap lock");
194 g.pending_action = Some(action);
195 self.cv.notify_all();
196 }
197
198 fn next_seq(&self) -> u64 {
199 self.seq.fetch_add(1, Ordering::SeqCst)
200 }
201
202 fn write_message(&self, body: Value) {
203 let s = serde_json::to_string(&body).unwrap_or_else(|_| "{}".to_string());
204 let mut w = self.writer.lock().expect("dap writer");
205 let _ = write!(w, "Content-Length: {}\r\n\r\n{}", s.len(), s);
206 let _ = w.flush();
207 }
208
209 fn emit_response(&self, req: &DapRequest, success: bool, body: Value) {
210 let seq = self.next_seq();
211 let msg = json!({
212 "seq": seq,
213 "type": "response",
214 "request_seq": req.seq,
215 "success": success,
216 "command": req.command,
217 "body": body,
218 });
219 self.write_message(msg);
220 }
221 pub fn emit_event(&self, event: &str, body: Value) {
223 let seq = self.next_seq();
224 let milestone = matches!(
228 event,
229 "stopped" | "terminated" | "exited" | "initialized" | "process" | "breakpoint"
230 );
231 if milestone {
232 crate::slog_info!("dap.evt", "→ {} seq={}", event, seq);
233 } else {
234 crate::slog_trace!("dap.evt", "→ {} seq={}", event, seq);
235 }
236 let msg = json!({
237 "seq": seq,
238 "type": "event",
239 "event": event,
240 "body": body,
241 });
242 self.write_message(msg);
243 }
244}
245
246pub fn spawn_reader_with_input(
253 shared: Arc<DapShared>,
254 bp_state: Arc<Mutex<BreakpointState>>,
255 input: Box<dyn Read + Send>,
256) -> (
257 thread::JoinHandle<()>,
258 std::sync::mpsc::Receiver<LaunchParams>,
259) {
260 let (tx, rx) = std::sync::mpsc::channel::<LaunchParams>();
261 let h = thread::spawn(move || {
262 let mut reader = BufReader::new(input);
263 loop {
264 let body = match read_message(&mut reader) {
265 Ok(Some(b)) => b,
266 Ok(None) => break,
267 Err(_) => break,
268 };
269 let req: DapRequest = match serde_json::from_slice(&body) {
270 Ok(r) => r,
271 Err(_) => continue,
272 };
273 handle_request(&shared, &bp_state, &tx, &req);
274 if shared.was_disconnected() {
275 break;
276 }
277 }
278 shared.resume_with(DebugAction::Quit);
280 shared.disconnected.store(true, Ordering::SeqCst);
281 });
282 (h, rx)
283}
284
285fn read_message<R: Read>(reader: &mut BufReader<R>) -> io::Result<Option<Vec<u8>>> {
286 let mut content_length: Option<usize> = None;
287 loop {
288 let mut line = String::new();
289 let n = reader.read_line(&mut line)?;
290 if n == 0 {
291 return Ok(None);
292 }
293 let line = line.trim_end_matches(['\r', '\n']);
294 if line.is_empty() {
295 break;
296 }
297 if let Some(rest) = line.strip_prefix("Content-Length:") {
298 content_length = rest.trim().parse().ok();
299 }
300 }
301 let Some(len) = content_length else {
302 return Ok(Some(Vec::new()));
303 };
304 let mut body = vec![0u8; len];
305 reader.read_exact(&mut body)?;
306 Ok(Some(body))
307}
308
309#[derive(Debug, Clone, Default)]
312pub struct LaunchParams {
313 pub program: String,
315 pub args: Vec<String>,
317 pub cwd: Option<String>,
319 pub no_debug: bool,
321 pub stop_on_entry: bool,
323 pub interpreter_args: Vec<String>,
325 pub no_interop: bool,
327}
328#[derive(Debug, Default)]
330pub struct BreakpointState {
331 pub line_breakpoints: HashMap<String, Vec<usize>>,
333 pub function_breakpoints: Vec<String>,
335 pub pending_step: Option<StepKind>,
338}
339
340fn handle_request(
343 shared: &Arc<DapShared>,
344 bp_state: &Arc<Mutex<BreakpointState>>,
345 launch_tx: &std::sync::mpsc::Sender<LaunchParams>,
346 req: &DapRequest,
347) {
348 crate::slog_trace!("dap.req", "seq={} command={}", req.seq, req.command);
349 match req.command.as_str() {
350 "initialize" => {
351 shared.emit_response(
352 req,
353 true,
354 json!({
355 "supportsConfigurationDoneRequest": true,
356 "supportsFunctionBreakpoints": true,
357 "supportsConditionalBreakpoints": false,
358 "supportsHitConditionalBreakpoints": false,
359 "supportsEvaluateForHovers": true,
360 "supportsTerminateRequest": true,
361 "supportsRestartRequest": false,
362 "supportsStepInTargetsRequest": false,
363 "supportsSetVariable": false,
364 "supportsCompletionsRequest": false,
365 "supportsLoadedSourcesRequest": false,
366 "supportsExceptionInfoRequest": false,
367 "supportsExceptionOptions": false,
368 "supportsValueFormattingOptions": false,
369 "supportsLogPoints": false,
370 "supportsModulesRequest": false,
371 "supportsRestartFrame": false,
372 "supportsGotoTargetsRequest": false,
373 "supportsStepBack": false,
374 }),
375 );
376 shared.emit_event("initialized", json!({}));
377 }
378 "setBreakpoints" => {
379 let path = req
380 .arguments
381 .get("source")
382 .and_then(|s| s.get("path"))
383 .and_then(|p| p.as_str())
384 .unwrap_or("")
385 .to_string();
386 let bps = req
387 .arguments
388 .get("breakpoints")
389 .and_then(|b| b.as_array())
390 .map(|arr| {
391 arr.iter()
392 .filter_map(|b| b.get("line").and_then(|l| l.as_u64()))
393 .map(|l| l as usize)
394 .collect::<Vec<_>>()
395 })
396 .unwrap_or_default();
397 crate::slog_info!("dap.bp", "setBreakpoints path={} lines={:?}", path, bps);
398 {
399 let mut bp = bp_state.lock().expect("bp lock");
400 bp.line_breakpoints.insert(path.clone(), bps.clone());
401 }
402 let verified: Vec<Value> = bps
403 .iter()
404 .map(|l| {
405 json!({
406 "verified": true,
407 "line": *l,
408 "source": { "path": path }
409 })
410 })
411 .collect();
412 shared.emit_response(req, true, json!({ "breakpoints": verified }));
413 }
414 "setFunctionBreakpoints" => {
415 let fbps: Vec<String> = req
416 .arguments
417 .get("breakpoints")
418 .and_then(|b| b.as_array())
419 .map(|arr| {
420 arr.iter()
421 .filter_map(|b| b.get("name").and_then(|n| n.as_str()).map(String::from))
422 .collect()
423 })
424 .unwrap_or_default();
425 {
426 let mut bp = bp_state.lock().expect("bp lock");
427 bp.function_breakpoints = fbps.clone();
428 }
429 let body: Vec<Value> = fbps.iter().map(|_| json!({ "verified": true })).collect();
430 shared.emit_response(req, true, json!({ "breakpoints": body }));
431 }
432 "setExceptionBreakpoints" => {
433 shared.emit_response(req, true, json!({ "breakpoints": [] }));
434 }
435 "configurationDone" => {
436 shared.configuration_done.store(true, Ordering::SeqCst);
437 shared.emit_response(req, true, json!({}));
438 }
439 "launch" => {
440 let lp = LaunchParams {
441 program: req
442 .arguments
443 .get("program")
444 .and_then(|v| v.as_str())
445 .unwrap_or("")
446 .to_string(),
447 args: req
448 .arguments
449 .get("args")
450 .and_then(|v| v.as_array())
451 .map(|a| {
452 a.iter()
453 .filter_map(|s| s.as_str().map(String::from))
454 .collect()
455 })
456 .unwrap_or_default(),
457 cwd: req
458 .arguments
459 .get("cwd")
460 .and_then(|v| v.as_str())
461 .map(String::from),
462 no_debug: req
463 .arguments
464 .get("noDebug")
465 .and_then(|v| v.as_bool())
466 .unwrap_or(false),
467 stop_on_entry: req
468 .arguments
469 .get("stopOnEntry")
470 .and_then(|v| v.as_bool())
471 .unwrap_or(false),
472 interpreter_args: req
473 .arguments
474 .get("interpreterArgs")
475 .and_then(|v| v.as_array())
476 .map(|a| {
477 a.iter()
478 .filter_map(|s| s.as_str().map(String::from))
479 .collect()
480 })
481 .unwrap_or_default(),
482 no_interop: req
483 .arguments
484 .get("noInterop")
485 .and_then(|v| v.as_bool())
486 .unwrap_or(false),
487 };
488 crate::slog_info!(
489 "dap.launch",
490 "program={} stopOnEntry={} noDebug={} cwd={:?}",
491 lp.program,
492 lp.stop_on_entry,
493 lp.no_debug,
494 lp.cwd
495 );
496 let _ = launch_tx.send(lp);
497 shared.emit_response(req, true, json!({}));
498 }
499 "threads" => {
500 shared.emit_response(
501 req,
502 true,
503 json!({
504 "threads": [
505 { "id": 1, "name": "main" }
506 ]
507 }),
508 );
509 }
510 "stackTrace" => {
511 let snap = shared.inner.lock().expect("dap lock").snapshot.clone();
512 let frames: Vec<Value> = snap
513 .frames
514 .iter()
515 .enumerate()
516 .map(|(i, f)| {
517 json!({
518 "id": i + 1,
519 "name": f.name,
520 "line": f.line,
521 "column": 1,
522 "source": { "name": leaf(&f.file), "path": f.file }
523 })
524 })
525 .collect();
526 shared.emit_response(
527 req,
528 true,
529 json!({
530 "stackFrames": frames,
531 "totalFrames": frames.len(),
532 }),
533 );
534 }
535 "scopes" => {
536 shared.emit_response(
537 req,
538 true,
539 json!({
540 "scopes": [
541 { "name": "Locals", "variablesReference": 1000, "expensive": false },
542 { "name": "Globals", "variablesReference": 2000, "expensive": false }
543 ]
544 }),
545 );
546 }
547 "variables" => {
548 let var_ref = req
549 .arguments
550 .get("variablesReference")
551 .and_then(|v| v.as_u64())
552 .unwrap_or(0) as u32;
553 let snap = shared.inner.lock().expect("dap lock").snapshot.clone();
554 let vars: Vec<Value> = match var_ref {
555 1000 => snap
556 .locals
557 .iter()
558 .map(|v| {
559 json!({
560 "name": v.name,
561 "value": v.repr,
562 "type": v.kind,
563 "variablesReference": v.var_ref,
564 })
565 })
566 .collect(),
567 2000 => snap
568 .globals
569 .iter()
570 .map(|v| {
571 json!({
572 "name": v.name,
573 "value": v.repr,
574 "type": v.kind,
575 "variablesReference": v.var_ref,
576 })
577 })
578 .collect(),
579 _ => snap
580 .var_ref_map
581 .get(&var_ref)
582 .map(|children| {
583 children
584 .iter()
585 .map(|c| {
586 json!({
587 "name": c.name,
588 "value": c.repr,
589 "type": "",
590 "variablesReference": c.var_ref,
591 })
592 })
593 .collect::<Vec<Value>>()
594 })
595 .unwrap_or_default(),
596 };
597 shared.emit_response(req, true, json!({ "variables": vars }));
598 }
599 "continue" => {
600 crate::slog_debug!("dap.flow", "continue");
601 shared.resume_with(DebugAction::Continue);
602 shared.emit_response(req, true, json!({ "allThreadsContinued": true }));
603 }
604 "next" => {
605 crate::slog_debug!("dap.flow", "next (step over)");
609 request_step(bp_state, StepKind::Over);
610 shared.resume_with(DebugAction::Continue);
611 shared.emit_response(req, true, json!({}));
612 }
613 "stepIn" => {
614 crate::slog_debug!("dap.flow", "stepIn");
615 request_step(bp_state, StepKind::Into);
616 shared.resume_with(DebugAction::Continue);
617 shared.emit_response(req, true, json!({}));
618 }
619 "stepOut" => {
620 crate::slog_debug!("dap.flow", "stepOut");
621 request_step(bp_state, StepKind::Out);
622 shared.resume_with(DebugAction::Continue);
623 shared.emit_response(req, true, json!({}));
624 }
625 "pause" => {
626 crate::slog_debug!("dap.flow", "pause requested");
627 let mut g = shared.inner.lock().expect("dap lock");
628 g.pause_request = true;
629 shared.emit_response(req, true, json!({}));
630 }
631 "evaluate" => {
632 let expr = req
633 .arguments
634 .get("expression")
635 .and_then(|v| v.as_str())
636 .unwrap_or("")
637 .to_string();
638 let snap = shared.inner.lock().expect("dap lock").snapshot.clone();
639 let result = evaluate_expression(&expr, &snap);
640 shared.emit_response(
641 req,
642 true,
643 json!({
644 "result": result,
645 "variablesReference": 0,
646 }),
647 );
648 }
649 "terminate" | "disconnect" => {
650 crate::slog_info!("dap", "{} received, tearing down", req.command);
651 shared.disconnected.store(true, Ordering::SeqCst);
652 shared.resume_with(DebugAction::Quit);
653 shared.emit_response(req, true, json!({}));
654 shared.emit_event("terminated", json!({}));
655 }
656 other => {
657 crate::slog_trace!("dap", "unknown command={}", other);
658 shared.emit_response(req, true, json!({}));
659 }
660 }
661}
662#[derive(Debug, Clone, Copy)]
664pub enum StepKind {
665 Over,
667 Into,
669 Out,
671}
672
673fn request_step(bp_state: &Arc<Mutex<BreakpointState>>, kind: StepKind) {
676 if let Ok(mut g) = bp_state.lock() {
677 g.pending_step = Some(kind);
678 }
679}
680
681fn leaf(path: &str) -> String {
682 path.rsplit_once('/')
683 .map(|(_, t)| t.to_string())
684 .unwrap_or_else(|| path.to_string())
685}
686
687fn truncate(s: &str, n: usize) -> String {
688 if s.chars().count() <= n {
689 s.to_string()
690 } else {
691 let mut out: String = s.chars().take(n).collect();
692 out.push('…');
693 out
694 }
695}
696
697fn evaluate_expression(expr: &str, snap: &PauseSnapshot) -> String {
707 let needle = expr.trim();
708 if needle.is_empty() {
709 return String::new();
710 }
711 for src in [&snap.locals, &snap.globals] {
713 for v in src.iter() {
714 if v.name == needle {
715 return v.repr.clone();
716 }
717 }
718 }
719 let exe = match std::env::current_exe() {
721 Ok(p) => p,
722 Err(e) => return format!("eval: cannot locate stryke binary: {e}"),
723 };
724 let mut prelude = String::new();
728 for v in &snap.locals {
729 if v.kind != "scalar" {
730 continue;
731 }
732 if !v.name.starts_with('$') {
733 continue;
734 }
735 let bare = &v.name[1..];
736 if bare.is_empty() || is_builtin_like(bare) {
737 continue;
738 }
739 let repr = if v.repr.is_empty() {
742 "undef"
743 } else {
744 v.repr.as_str()
745 };
746 prelude.push_str(&format!("my ${bare} = {repr};\n"));
747 }
748 let wrapped = format!("{prelude}p ({needle})");
751 let output = std::process::Command::new(&exe)
752 .arg("-e")
753 .arg(&wrapped)
754 .stdin(std::process::Stdio::null())
755 .stdout(std::process::Stdio::piped())
756 .stderr(std::process::Stdio::piped())
757 .output();
758 match output {
759 Ok(out) => {
760 if out.status.success() {
761 let s = String::from_utf8_lossy(&out.stdout)
762 .trim_end_matches('\n')
763 .to_string();
764 if s.is_empty() {
765 "(no output)".to_string()
766 } else {
767 s
768 }
769 } else {
770 let err = String::from_utf8_lossy(&out.stderr);
771 let msg = err.lines().next().unwrap_or("").trim();
772 format!("error: {msg}")
773 }
774 }
775 Err(e) => format!("eval spawn failed: {e}"),
776 }
777}
778
779const CONTAINER_REF_BASE: u32 = 10_000;
786
787struct CaptureCtx<'a> {
789 next_ref: u32,
790 map: &'a mut HashMap<u32, Vec<VarChild>>,
791}
792
793impl<'a> CaptureCtx<'a> {
794 fn alloc_ref(&mut self) -> u32 {
795 let r = self.next_ref;
796 self.next_ref += 1;
797 r
798 }
799}
800
801const MAX_VAR_DEPTH: u32 = 12;
805
806fn build_child(
810 name: String,
811 value: &crate::value::StrykeValue,
812 depth: u32,
813 ctx: &mut CaptureCtx,
814) -> VarChild {
815 if let Some(rich) = try_sketch_child(&name, value, depth, ctx) {
821 return rich;
822 }
823 let hash_contents: Option<indexmap::IndexMap<String, crate::value::StrykeValue>> = value
825 .as_hash_map()
826 .or_else(|| value.as_hash_ref().map(|arc| arc.read().clone()));
827 if let Some(map) = hash_contents {
828 if depth >= MAX_VAR_DEPTH || map.is_empty() {
829 return VarChild {
833 name,
834 repr: truncate(&short_scalar_repr(value), MAX_VAR_REPR),
835 var_ref: 0,
836 };
837 }
838 let preview: Vec<String> = map
839 .iter()
840 .take(4)
841 .map(|(k, v)| format!("{k} => {}", short_scalar_repr(v)))
842 .collect();
843 let repr = format!(
844 "[{}] ({}{})",
845 map.len(),
846 preview.join(", "),
847 if map.len() > 4 {
848 format!(", … {} more", map.len() - 4)
849 } else {
850 String::new()
851 },
852 );
853 let var_ref = ctx.alloc_ref();
854 let children: Vec<VarChild> = map
855 .iter()
856 .take(2000)
857 .map(|(k, v)| build_child(k.clone(), v, depth + 1, ctx))
858 .collect();
859 ctx.map.insert(var_ref, children);
860 return VarChild {
861 name,
862 repr: truncate(&repr, MAX_VAR_REPR),
863 var_ref,
864 };
865 }
866 let array_contents: Option<Vec<crate::value::StrykeValue>> =
872 if let Some(arc) = value.as_array_ref() {
873 Some(arc.read().clone())
874 } else if value.as_array_vec().is_some() {
875 Some(value.to_list())
876 } else {
877 None
878 };
879 if let Some(list) = array_contents {
880 if depth >= MAX_VAR_DEPTH || list.is_empty() {
881 return VarChild {
882 name,
883 repr: truncate(&short_scalar_repr(value), MAX_VAR_REPR),
884 var_ref: 0,
885 };
886 }
887 let preview: Vec<String> = list.iter().take(6).map(short_scalar_repr).collect();
888 let repr = format!(
889 "[{}] ({}{})",
890 list.len(),
891 preview.join(", "),
892 if list.len() > 6 {
893 format!(", … {} more", list.len() - 6)
894 } else {
895 String::new()
896 },
897 );
898 let var_ref = ctx.alloc_ref();
899 let children: Vec<VarChild> = list
900 .iter()
901 .take(2000)
902 .enumerate()
903 .map(|(i, v)| build_child(format!("[{i}]"), v, depth + 1, ctx))
904 .collect();
905 ctx.map.insert(var_ref, children);
906 return VarChild {
907 name,
908 repr: truncate(&repr, MAX_VAR_REPR),
909 var_ref,
910 };
911 }
912 VarChild {
914 name,
915 repr: truncate(&crate::debugger::format_value(value), MAX_VAR_REPR),
916 var_ref: 0,
917 }
918}
919
920fn fmt_f(v: f64) -> String {
924 if !v.is_finite() {
925 return v.to_string();
926 }
927 let av = v.abs();
928 if av != 0.0 && !(1e-3..1e15).contains(&av) {
929 return format!("{:e}", v);
930 }
931 let s = format!("{:.6}", v);
933 if !s.contains('.') {
934 return s;
935 }
936 let trimmed = s.trim_end_matches('0').trim_end_matches('.');
937 if trimmed.is_empty() || trimmed == "-" {
938 "0".to_string()
939 } else {
940 trimmed.to_string()
941 }
942}
943
944fn sketch_leaf(name: &str, repr: String) -> VarChild {
946 VarChild {
947 name: name.to_string(),
948 repr: truncate(&repr, MAX_VAR_REPR),
949 var_ref: 0,
950 }
951}
952
953fn try_sketch_child(
958 name: &str,
959 value: &crate::value::StrykeValue,
960 depth: u32,
961 ctx: &mut CaptureCtx,
962) -> Option<VarChild> {
963 if depth >= MAX_VAR_DEPTH {
964 return None;
965 }
966
967 if let Some(arc) = value.as_tdigest_sketch() {
968 let mut g = arc.lock();
969 let n = g.count();
970 let (repr, children) = if n == 0 {
971 (
972 "TDigestSketch(empty)".to_string(),
973 vec![
974 sketch_leaf("count", "0".to_string()),
975 sketch_leaf("compression", g.compression().to_string()),
976 ],
977 )
978 } else {
979 let (mn, mx) = (g.min(), g.max());
980 let (mean, sum) = (g.mean(), g.sum());
981 let (p50, p90, p95, p99) = (
982 g.quantile(0.50),
983 g.quantile(0.90),
984 g.quantile(0.95),
985 g.quantile(0.99),
986 );
987 let compression = g.compression();
988 let repr = format!(
989 "TDigestSketch(n={}, min={}, max={}, p50={}, p99={})",
990 n,
991 fmt_f(mn),
992 fmt_f(mx),
993 fmt_f(p50),
994 fmt_f(p99)
995 );
996 let kids = vec![
997 sketch_leaf("count", n.to_string()),
998 sketch_leaf("min", fmt_f(mn)),
999 sketch_leaf("max", fmt_f(mx)),
1000 sketch_leaf("mean", fmt_f(mean)),
1001 sketch_leaf("sum", fmt_f(sum)),
1002 sketch_leaf("p50", fmt_f(p50)),
1003 sketch_leaf("p90", fmt_f(p90)),
1004 sketch_leaf("p95", fmt_f(p95)),
1005 sketch_leaf("p99", fmt_f(p99)),
1006 sketch_leaf("compression", compression.to_string()),
1007 ];
1008 (repr, kids)
1009 };
1010 let var_ref = ctx.alloc_ref();
1011 ctx.map.insert(var_ref, children);
1012 return Some(VarChild {
1013 name: name.to_string(),
1014 repr: truncate(&repr, MAX_VAR_REPR),
1015 var_ref,
1016 });
1017 }
1018
1019 if let Some(arc) = value.as_bloom_filter() {
1020 let g = arc.lock();
1021 let n = g.inserted();
1022 let bits = g.bit_count();
1023 let k = g.k();
1024 let fpr = g.estimated_fpr();
1025 let repr = format!(
1026 "BloomFilter(n={}, bits={}, k={}, fpr={})",
1027 n,
1028 bits,
1029 k,
1030 fmt_f(fpr)
1031 );
1032 let children = vec![
1033 sketch_leaf("inserted", n.to_string()),
1034 sketch_leaf("bit_count", bits.to_string()),
1035 sketch_leaf("k", k.to_string()),
1036 sketch_leaf("estimated_fpr", fmt_f(fpr)),
1037 ];
1038 let var_ref = ctx.alloc_ref();
1039 ctx.map.insert(var_ref, children);
1040 return Some(VarChild {
1041 name: name.to_string(),
1042 repr: truncate(&repr, MAX_VAR_REPR),
1043 var_ref,
1044 });
1045 }
1046
1047 if let Some(arc) = value.as_hll_sketch() {
1048 let g = arc.lock();
1049 let card = g.count();
1050 let p = g.precision();
1051 let m = g.registers_len();
1052 let repr = format!("HllSketch(cardinality={}, p={}, m={})", fmt_f(card), p, m);
1053 let children = vec![
1054 sketch_leaf("cardinality", fmt_f(card)),
1055 sketch_leaf("precision", p.to_string()),
1056 sketch_leaf("registers", m.to_string()),
1057 ];
1058 let var_ref = ctx.alloc_ref();
1059 ctx.map.insert(var_ref, children);
1060 return Some(VarChild {
1061 name: name.to_string(),
1062 repr: truncate(&repr, MAX_VAR_REPR),
1063 var_ref,
1064 });
1065 }
1066
1067 if let Some(arc) = value.as_cms_sketch() {
1068 let g = arc.lock();
1069 let repr = format!("CmsSketch(width={}, depth={})", g.width(), g.depth());
1070 let children = vec![
1071 sketch_leaf("width", g.width().to_string()),
1072 sketch_leaf("depth", g.depth().to_string()),
1073 ];
1074 let var_ref = ctx.alloc_ref();
1075 ctx.map.insert(var_ref, children);
1076 return Some(VarChild {
1077 name: name.to_string(),
1078 repr: truncate(&repr, MAX_VAR_REPR),
1079 var_ref,
1080 });
1081 }
1082
1083 if let Some(arc) = value.as_topk_sketch() {
1084 let g = arc.lock();
1085 let k = g.k();
1086 let n = g.size();
1087 let heavies = g.heavies(k.min(10));
1088 let preview: Vec<String> = heavies
1089 .iter()
1090 .take(3)
1091 .map(|(key, count, _err)| {
1092 let key_s = String::from_utf8_lossy(key);
1093 format!("({}, {})", key_s, count)
1094 })
1095 .collect();
1096 let repr = format!("TopKSketch(k={}, n={}, top=[{}])", k, n, preview.join(", "));
1097 let mut children = vec![
1098 sketch_leaf("k", k.to_string()),
1099 sketch_leaf("size", n.to_string()),
1100 ];
1101 for (i, (key, count, err)) in heavies.iter().enumerate() {
1102 let key_s = String::from_utf8_lossy(key);
1103 children.push(sketch_leaf(
1104 &format!("top[{}]", i),
1105 format!("({}, count={}, err={})", key_s, count, err),
1106 ));
1107 }
1108 let var_ref = ctx.alloc_ref();
1109 ctx.map.insert(var_ref, children);
1110 return Some(VarChild {
1111 name: name.to_string(),
1112 repr: truncate(&repr, MAX_VAR_REPR),
1113 var_ref,
1114 });
1115 }
1116
1117 if let Some(inst) = value.as_struct_inst() {
1120 let values = inst.values.read().clone();
1121 let preview: Vec<String> = inst
1122 .def
1123 .fields
1124 .iter()
1125 .zip(values.iter())
1126 .take(4)
1127 .map(|(f, v)| format!("{}={}", f.name, short_scalar_repr(v)))
1128 .collect();
1129 let repr = format!(
1130 "{}({}{})",
1131 inst.def.name,
1132 preview.join(", "),
1133 if inst.def.fields.len() > 4 {
1134 format!(", … {} more", inst.def.fields.len() - 4)
1135 } else {
1136 String::new()
1137 },
1138 );
1139 let var_ref = ctx.alloc_ref();
1140 let children: Vec<VarChild> = inst
1141 .def
1142 .fields
1143 .iter()
1144 .zip(values.iter())
1145 .map(|(f, v)| build_child(f.name.clone(), v, depth + 1, ctx))
1146 .collect();
1147 ctx.map.insert(var_ref, children);
1148 return Some(VarChild {
1149 name: name.to_string(),
1150 repr: truncate(&repr, MAX_VAR_REPR),
1151 var_ref,
1152 });
1153 }
1154
1155 if let Some(inst) = value.as_enum_inst() {
1159 let variant = inst.variant_name();
1160 let data_preview = if inst.data.is_undef() {
1161 String::new()
1162 } else {
1163 format!("({})", short_scalar_repr(&inst.data))
1164 };
1165 let repr = format!("{}::{}{}", inst.def.name, variant, data_preview);
1166 let mut children = vec![
1167 sketch_leaf("__variant", variant.to_string()),
1168 sketch_leaf("__variant_idx", inst.variant_idx.to_string()),
1169 ];
1170 if !inst.data.is_undef() {
1171 children.push(build_child("data".to_string(), &inst.data, depth + 1, ctx));
1172 }
1173 let var_ref = ctx.alloc_ref();
1174 ctx.map.insert(var_ref, children);
1175 return Some(VarChild {
1176 name: name.to_string(),
1177 repr: truncate(&repr, MAX_VAR_REPR),
1178 var_ref,
1179 });
1180 }
1181
1182 if let Some(inst) = value.as_class_inst() {
1187 let values = inst.values.read().clone();
1188 let preview: Vec<String> = inst
1189 .def
1190 .fields
1191 .iter()
1192 .zip(values.iter())
1193 .take(4)
1194 .map(|(f, v)| format!("{}={}", f.name, short_scalar_repr(v)))
1195 .collect();
1196 let repr = format!(
1197 "{}({}{})",
1198 inst.def.name,
1199 preview.join(", "),
1200 if inst.def.fields.len() > 4 {
1201 format!(", … {} more", inst.def.fields.len() - 4)
1202 } else {
1203 String::new()
1204 },
1205 );
1206 let mut children = vec![sketch_leaf("__class", inst.def.name.clone())];
1207 if !inst.isa_chain.is_empty() {
1208 children.push(sketch_leaf(
1209 "__isa",
1210 format!("[{}]", inst.isa_chain.join(", ")),
1211 ));
1212 }
1213 for (f, v) in inst.def.fields.iter().zip(values.iter()) {
1214 let vis_marker = match f.visibility {
1215 crate::ast::Visibility::Private => "-",
1216 crate::ast::Visibility::Protected => "#",
1217 crate::ast::Visibility::Public => "+",
1218 };
1219 children.push(build_child(
1220 format!("{}{}", vis_marker, f.name),
1221 v,
1222 depth + 1,
1223 ctx,
1224 ));
1225 }
1226 let var_ref = ctx.alloc_ref();
1227 ctx.map.insert(var_ref, children);
1228 return Some(VarChild {
1229 name: name.to_string(),
1230 repr: truncate(&repr, MAX_VAR_REPR),
1231 var_ref,
1232 });
1233 }
1234
1235 if let Some(arc) = crate::value::set_payload(value) {
1239 let len = arc.len();
1240 let preview: Vec<String> = arc.values().take(6).map(short_scalar_repr).collect();
1241 let repr = format!(
1242 "Set({}){}",
1243 len,
1244 if arc.is_empty() {
1245 String::new()
1246 } else {
1247 format!(
1248 " {{{}{}}}",
1249 preview.join(", "),
1250 if len > 6 {
1251 format!(", … {} more", len - 6)
1252 } else {
1253 String::new()
1254 }
1255 )
1256 },
1257 );
1258 let var_ref = if arc.is_empty() { 0 } else { ctx.alloc_ref() };
1259 if var_ref != 0 {
1260 let children: Vec<VarChild> = arc
1261 .values()
1262 .take(2000)
1263 .enumerate()
1264 .map(|(i, v)| build_child(format!("[{}]", i), v, depth + 1, ctx))
1265 .collect();
1266 ctx.map.insert(var_ref, children);
1267 }
1268 return Some(VarChild {
1269 name: name.to_string(),
1270 repr: truncate(&repr, MAX_VAR_REPR),
1271 var_ref,
1272 });
1273 }
1274
1275 None
1276}
1277
1278fn short_scalar_repr(v: &crate::value::StrykeValue) -> String {
1281 if v.as_hash_ref().is_some() || v.as_hash_map().is_some() {
1282 return "{…}".to_string();
1283 }
1284 if v.as_array_ref().is_some() || v.as_array_vec().is_some() {
1285 return "[…]".to_string();
1286 }
1287 crate::debugger::format_value(v)
1288}
1289
1290pub(crate) fn capture_locals_with_map(
1291 scope: &crate::scope::Scope,
1292 map: &mut HashMap<u32, Vec<VarChild>>,
1293) -> Vec<VarSnap> {
1294 let mut ctx = CaptureCtx {
1295 next_ref: CONTAINER_REF_BASE,
1296 map,
1297 };
1298 let mut user: Vec<VarSnap> = Vec::new();
1299 let mut topic: Vec<VarSnap> = Vec::new();
1300 let mut builtin: Vec<VarSnap> = Vec::new();
1301
1302 for name in scope.all_scalar_names().into_iter().take(256) {
1303 if should_hide(&name) {
1304 continue;
1305 }
1306 let v = scope.get_scalar(&name);
1307 let child = build_child(format!("${name}"), &v, 0, &mut ctx);
1309 let snap = VarSnap {
1310 name: child.name,
1311 repr: child.repr,
1312 kind: "scalar".into(),
1313 var_ref: child.var_ref,
1314 };
1315 if is_magic_block_param(&name) {
1316 topic.push(snap);
1317 } else if is_builtin_like(&name) {
1318 builtin.push(snap);
1319 } else {
1320 user.push(snap);
1321 }
1322 }
1323 for name in scope.all_array_names().into_iter().take(64) {
1324 if should_hide(&name) {
1325 continue;
1326 }
1327 let arr = scope.get_array(&name);
1328 let preview: Vec<String> = arr.iter().take(8).map(short_scalar_repr).collect();
1329 let repr = format!(
1330 "[{}]{}",
1331 arr.len(),
1332 if arr.is_empty() {
1333 String::new()
1334 } else {
1335 format!(
1336 " ({}{})",
1337 preview.join(", "),
1338 if arr.len() > 8 {
1339 format!(", … {} more", arr.len() - 8)
1340 } else {
1341 String::new()
1342 }
1343 )
1344 },
1345 );
1346 let var_ref = if arr.is_empty() { 0 } else { ctx.alloc_ref() };
1347 if var_ref != 0 {
1348 let children: Vec<VarChild> = arr
1349 .iter()
1350 .take(2000)
1351 .enumerate()
1352 .map(|(i, v)| build_child(format!("[{i}]"), v, 1, &mut ctx))
1353 .collect();
1354 ctx.map.insert(var_ref, children);
1355 }
1356 let snap = VarSnap {
1357 name: format!("@{name}"),
1358 repr: truncate(&repr, MAX_VAR_REPR),
1359 kind: "array".into(),
1360 var_ref,
1361 };
1362 if is_builtin_like(&name) {
1363 builtin.push(snap);
1364 } else {
1365 user.push(snap);
1366 }
1367 }
1368 for name in scope.all_hash_names().into_iter().take(64) {
1369 if should_hide(&name) {
1370 continue;
1371 }
1372 let h = scope.get_hash(&name);
1373 let preview: Vec<String> = h
1374 .iter()
1375 .take(6)
1376 .map(|(k, v)| format!("{k} => {}", short_scalar_repr(v)))
1377 .collect();
1378 let repr = format!(
1379 "[{}]{}",
1380 h.len(),
1381 if h.is_empty() {
1382 String::new()
1383 } else {
1384 format!(
1385 " ({}{})",
1386 preview.join(", "),
1387 if h.len() > 6 {
1388 format!(", … {} more", h.len() - 6)
1389 } else {
1390 String::new()
1391 }
1392 )
1393 },
1394 );
1395 let var_ref = if h.is_empty() { 0 } else { ctx.alloc_ref() };
1396 if var_ref != 0 {
1397 let children: Vec<VarChild> = h
1398 .iter()
1399 .take(2000)
1400 .map(|(k, v)| build_child(k.clone(), v, 1, &mut ctx))
1401 .collect();
1402 ctx.map.insert(var_ref, children);
1403 }
1404 let snap = VarSnap {
1405 name: format!("%{name}"),
1406 repr: truncate(&repr, MAX_VAR_REPR),
1407 kind: "hash".into(),
1408 var_ref,
1409 };
1410 if is_builtin_like(&name) {
1411 builtin.push(snap);
1412 } else {
1413 user.push(snap);
1414 }
1415 }
1416
1417 let by_sigil_name = |a: &VarSnap, b: &VarSnap| a.name.cmp(&b.name);
1418 user.sort_by(by_sigil_name);
1419 builtin.sort_by(by_sigil_name);
1420 topic.sort_by(|a, b| {
1427 let key = |n: &str| -> (u8, usize, String) {
1428 if n == "$a" || n == "$b" {
1430 (1, 0, n.to_string())
1431 } else {
1432 let bare = n.strip_prefix("$_").unwrap_or(n);
1433 (0, bare.parse::<usize>().unwrap_or(0), n.to_string())
1434 }
1435 };
1436 key(&a.name).cmp(&key(&b.name))
1437 });
1438 let mut out = user;
1439 out.extend(topic);
1440 out.extend(builtin);
1441 out
1442}
1443
1444fn is_magic_block_param(name: &str) -> bool {
1450 if name == "_" || name == "a" || name == "b" {
1451 return true;
1452 }
1453 if let Some(rest) = name.strip_prefix('_') {
1454 return !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit());
1455 }
1456 false
1457}
1458
1459fn should_hide(name: &str) -> bool {
1460 if name.is_empty() {
1461 return true;
1462 }
1463 if name.starts_with("__") && name.ends_with("__") && name.len() > 4 {
1467 return true;
1468 }
1469 false
1470}
1471
1472fn is_builtin_like(name: &str) -> bool {
1476 if name.is_empty() {
1477 return false;
1478 }
1479 if name.starts_with('^') {
1480 return true;
1481 }
1482 if name.starts_with('_') && name.len() > 1 && name[1..].contains('<') {
1484 return true;
1485 }
1486 matches!(
1488 name,
1489 "INC"
1490 | "ARGV"
1491 | "ENV"
1492 | "SIG"
1493 | "path"
1494 | "p"
1495 | "fpath"
1496 | "f"
1497 | "term"
1498 | "uname"
1499 | "limits"
1500 | "-"
1501 | "+"
1502 | "~"
1503 | "/"
1504 | "\\"
1505 | "\""
1506 | ","
1507 | "!"
1508 | "@"
1509 | "&"
1510 | "'"
1511 | "`"
1512 | "?"
1513 | "$"
1514 ) || name.starts_with("stryke::")
1515}
1516
1517pub fn run() -> i32 {
1529 run_with_args(&[])
1530}
1531pub fn run_with_args(args: &[String]) -> i32 {
1533 crate::slog_info!(
1534 "dap",
1535 "starting --dap pid={} args={:?} version={} log_level={:?}",
1536 std::process::id(),
1537 args,
1538 env!("CARGO_PKG_VERSION"),
1539 crate::stryke_log::current_level()
1540 );
1541 let connect_addr = args.iter().find(|a| a.contains(':')).cloned();
1542 let (reader, writer): (Box<dyn Read + Send>, Box<dyn Write + Send>) = match connect_addr {
1543 Some(addr) => {
1544 match std::net::TcpStream::connect(&addr) {
1546 Ok(s) => {
1547 crate::slog_info!("dap", "connected tcp {}", addr);
1548 let r = s.try_clone().expect("dap: tcp clone");
1549 (Box::new(r), Box::new(s))
1550 }
1551 Err(e) => {
1552 crate::slog_error!("dap", "tcp connect {} failed: {}", addr, e);
1553 eprintln!("stryke --dap: connect {addr}: {e}");
1554 return 2;
1555 }
1556 }
1557 }
1558 None => {
1559 crate::slog_info!("dap", "stdio mode");
1560 (Box::new(io::stdin()), Box::new(io::stdout()))
1561 }
1562 };
1563
1564 let shared = DapShared::new(writer);
1565 let bp_state = Arc::new(Mutex::new(BreakpointState::default()));
1566 let (_reader_handle, launch_rx) =
1567 spawn_reader_with_input(shared.clone(), bp_state.clone(), reader);
1568
1569 let lp = match launch_rx.recv() {
1571 Ok(p) => p,
1572 Err(_) => return 1,
1573 };
1574
1575 shared.emit_event(
1577 "process",
1578 json!({
1579 "name": lp.program,
1580 "isLocalProcess": true,
1581 "startMethod": "launch",
1582 }),
1583 );
1584 shared.emit_event("thread", json!({ "reason": "started", "threadId": 1 }));
1585
1586 let source = match std::fs::read_to_string(&lp.program) {
1588 Ok(s) => s,
1589 Err(e) => {
1590 shared.emit_event(
1591 "output",
1592 json!({ "category": "stderr", "output": format!("stryke --dap: cannot read {}: {}\n", lp.program, e) }),
1593 );
1594 shared.emit_event("terminated", json!({}));
1595 return 1;
1596 }
1597 };
1598
1599 let mut interp = crate::vm_helper::VMHelper::new();
1601 if let Some(cwd) = &lp.cwd {
1602 let _ = std::env::set_current_dir(cwd);
1603 }
1604 interp.file = lp.program.clone();
1605 let argv_vals: Vec<crate::value::StrykeValue> = lp
1607 .args
1608 .iter()
1609 .map(|s| crate::value::StrykeValue::string(s.clone()))
1610 .collect();
1611 interp.scope.declare_array("ARGV", argv_vals);
1612
1613 let mut inc_paths: Vec<String> = Vec::new();
1617 let vendor = crate::vendor_perl_inc_path();
1618 if vendor.is_dir() {
1619 crate::perl_inc::push_unique_string_paths(
1620 &mut inc_paths,
1621 vec![vendor.to_string_lossy().into_owned()],
1622 );
1623 }
1624 crate::perl_inc::push_unique_string_paths(
1625 &mut inc_paths,
1626 crate::perl_inc::paths_from_system_perl(),
1627 );
1628 if let Some(parent) = std::path::Path::new(&lp.program).parent() {
1629 if !parent.as_os_str().is_empty() {
1630 crate::perl_inc::push_unique_string_paths(
1631 &mut inc_paths,
1632 vec![parent.to_string_lossy().into_owned()],
1633 );
1634 }
1635 }
1636 if let Ok(extra) = std::env::var("STRYKE_INC") {
1637 let extra: Vec<String> = std::env::split_paths(&extra)
1638 .map(|p| p.to_string_lossy().into_owned())
1639 .collect();
1640 crate::perl_inc::push_unique_string_paths(&mut inc_paths, extra);
1641 }
1642 crate::perl_inc::push_unique_string_paths(&mut inc_paths, vec![".".to_string()]);
1643 let inc_dirs: Vec<crate::value::StrykeValue> = inc_paths
1644 .into_iter()
1645 .map(crate::value::StrykeValue::string)
1646 .collect();
1647 interp.scope.declare_array("INC", inc_dirs);
1648
1649 interp.materialize_env_if_needed();
1653
1654 interp.output_autoflush = true;
1660
1661 let mut dbg = crate::debugger::Debugger::new();
1670 dbg.set_file(&lp.program);
1671 dbg.load_source(&source);
1672 {
1674 let bp = bp_state.lock().expect("bp lock");
1675 if let Some(lines) = bp.line_breakpoints.get(&lp.program) {
1676 for l in lines {
1677 dbg.add_breakpoint_line(*l);
1678 }
1679 }
1680 for name in &bp.function_breakpoints {
1681 dbg.add_breakpoint_sub(name);
1682 }
1683 }
1684 dbg.set_dap_backend(shared.clone(), bp_state.clone());
1685 if !lp.stop_on_entry {
1686 dbg.set_step_mode(false);
1687 }
1688 interp.debugger = Some(dbg);
1689
1690 let _ = lp.no_interop;
1694 let program = match crate::parse_with_file(&source, &lp.program) {
1695 Ok(p) => p,
1696 Err(e) => {
1697 shared.emit_event(
1698 "output",
1699 json!({
1700 "category": "stderr",
1701 "output": format!("stryke: parse error: {}\n", e.message),
1702 }),
1703 );
1704 shared.emit_event(
1705 "stopped",
1706 json!({ "reason": "exception", "threadId": 1, "description": e.message }),
1707 );
1708 shared.emit_event("terminated", json!({}));
1709 return 1;
1710 }
1711 };
1712
1713 let result = interp.execute(&program);
1714
1715 let exit_code = match result {
1716 Ok(_) => 0,
1717 Err(e) => {
1718 if e.message != "debugger: quit" && !shared.was_disconnected() {
1721 shared.emit_event(
1722 "output",
1723 json!({
1724 "category": "stderr",
1725 "output": format!("stryke: runtime error: {}\n", e.message),
1726 }),
1727 );
1728 }
1729 if shared.was_disconnected() {
1730 0
1731 } else {
1732 1
1733 }
1734 }
1735 };
1736
1737 shared.emit_event("exited", json!({ "exitCode": exit_code }));
1738 shared.emit_event("terminated", json!({}));
1739 exit_code
1740}
1741
1742#[cfg(test)]
1743mod tests {
1744 use super::*;
1745
1746 #[test]
1754 fn magic_block_param_matches_underscore_topic_and_sort_pair() {
1755 assert!(is_magic_block_param("_"), "$_ topic");
1756 assert!(is_magic_block_param("_0"), "$_0 first positional");
1757 assert!(is_magic_block_param("_1"), "$_1 second positional");
1758 assert!(is_magic_block_param("_42"), "$_N N-th positional");
1759 assert!(is_magic_block_param("a"), "$a sort/reduce");
1760 assert!(is_magic_block_param("b"), "$b sort/reduce");
1761 }
1762
1763 #[test]
1764 fn magic_block_param_rejects_user_names() {
1765 assert!(!is_magic_block_param("name"));
1766 assert!(
1767 !is_magic_block_param("_name"),
1768 "underscore-prefix is not a topic alias"
1769 );
1770 assert!(!is_magic_block_param("a1"));
1771 assert!(
1772 !is_magic_block_param("ab"),
1773 "$ab is a user var, not a magic block param"
1774 );
1775 assert!(!is_magic_block_param(""));
1776 }
1777
1778 #[test]
1788 fn should_hide_dunder_synthetic_names() {
1789 assert!(should_hide("__foreach_i__"));
1790 assert!(should_hide("__INTERCEPT_NAME__"));
1791 assert!(should_hide("__list_assign_tmp__"));
1792 }
1793
1794 #[test]
1795 fn should_hide_keeps_user_visible_names() {
1796 assert!(!should_hide("x"));
1797 assert!(!should_hide("my_var"));
1798 assert!(!should_hide("_"));
1799 assert!(!should_hide("_0"));
1800 assert!(!should_hide("__"));
1801 assert!(!should_hide("_foo"));
1804 }
1805
1806 #[test]
1807 fn should_hide_empty_name() {
1808 assert!(should_hide(""));
1809 }
1810
1811 #[test]
1814 fn fmt_f_trims_trailing_zeros() {
1815 assert_eq!(fmt_f(1.0), "1");
1816 assert_eq!(fmt_f(1.5), "1.5");
1817 assert_eq!(fmt_f(0.0), "0");
1818 assert_eq!(fmt_f(-2.5), "-2.5");
1819 }
1820
1821 #[test]
1822 fn fmt_f_uses_scientific_for_extremes() {
1823 assert!(fmt_f(1e-10).contains('e'));
1825 assert!(fmt_f(1e20).contains('e'));
1827 }
1828
1829 #[test]
1830 fn fmt_f_handles_non_finite() {
1831 assert_eq!(fmt_f(f64::NAN), "NaN");
1833 assert_eq!(fmt_f(f64::INFINITY), "inf");
1834 }
1835
1836 #[test]
1839 fn is_builtin_like_matches_stryke_builtin_arrays_and_hashes() {
1840 assert!(is_builtin_like("INC"));
1841 assert!(is_builtin_like("ARGV"));
1842 assert!(is_builtin_like("ENV"));
1843 assert!(is_builtin_like("path"));
1844 assert!(is_builtin_like("p"));
1845 assert!(is_builtin_like("term"));
1846 }
1847
1848 #[test]
1849 fn is_builtin_like_matches_caret_prefixed_specials() {
1850 assert!(is_builtin_like("^O"));
1853 assert!(is_builtin_like("^X"));
1854 assert!(is_builtin_like("^HOOK"));
1855 }
1856
1857 #[test]
1858 fn is_builtin_like_matches_pipeline_outer_topic_chains() {
1859 assert!(is_builtin_like("_<"));
1861 assert!(is_builtin_like("_<<"));
1862 assert!(is_builtin_like("_0<"));
1863 assert!(is_builtin_like("_5<<<"));
1864 }
1865
1866 #[test]
1867 fn is_builtin_like_rejects_plain_user_names() {
1868 assert!(!is_builtin_like("x"));
1869 assert!(!is_builtin_like("my_var"));
1870 assert!(!is_builtin_like("_5"));
1871 assert!(!is_builtin_like(""));
1872 }
1873
1874 use std::sync::Arc;
1883 use parking_lot::Mutex;
1886
1887 fn drill(value: &crate::value::StrykeValue) -> (VarChild, HashMap<u32, Vec<VarChild>>) {
1888 let mut map: HashMap<u32, Vec<VarChild>> = HashMap::new();
1889 let mut ctx = CaptureCtx {
1890 next_ref: CONTAINER_REF_BASE,
1891 map: &mut map,
1892 };
1893 let child =
1894 try_sketch_child("$v", value, 0, &mut ctx).expect("sketch drill yields VarChild");
1895 (child, map)
1896 }
1897
1898 #[test]
1899 fn drill_tdigest_empty_summarises_compression() {
1900 let arc = Arc::new(Mutex::new(crate::sketches::TDigestSketch::new(100)));
1901 let v = crate::value::StrykeValue::tdigest_sketch(arc);
1902 let (row, map) = drill(&v);
1903 assert!(row.repr.contains("TDigestSketch"), "type tag in repr");
1904 assert!(row.repr.contains("empty"), "empty marker shown");
1905 let kids = map.get(&row.var_ref).expect("expandable");
1906 let names: Vec<&str> = kids.iter().map(|c| c.name.as_str()).collect();
1907 assert!(names.contains(&"count"), "count row");
1908 assert!(names.contains(&"compression"), "compression row");
1909 }
1910
1911 #[test]
1912 fn drill_tdigest_populated_emits_quantile_rows() {
1913 let s = crate::sketches::TDigestSketch::new(100);
1914 let arc = Arc::new(Mutex::new(s));
1915 {
1917 let mut g = arc.lock();
1918 for i in 1..=100 {
1919 g.add(i as f64);
1920 }
1921 }
1922 let v = crate::value::StrykeValue::tdigest_sketch(arc);
1923 let (row, map) = drill(&v);
1924 let kids = map.get(&row.var_ref).expect("expandable");
1925 let names: Vec<&str> = kids.iter().map(|c| c.name.as_str()).collect();
1926 for p in [
1927 "count", "min", "max", "mean", "sum", "p50", "p90", "p95", "p99",
1928 ] {
1929 assert!(names.contains(&p), "{p} row present in {names:?}");
1930 }
1931 assert!(
1933 row.repr.contains("n=100"),
1934 "count in inline repr: {}",
1935 row.repr
1936 );
1937 assert!(
1938 row.repr.contains("min="),
1939 "min in inline repr: {}",
1940 row.repr
1941 );
1942 assert!(
1943 row.repr.contains("p99="),
1944 "p99 in inline repr: {}",
1945 row.repr
1946 );
1947 }
1948
1949 #[test]
1950 fn drill_bloom_filter_exposes_inserted_bits_k_fpr() {
1951 let mut bf = crate::sketches::BloomFilter::new(1000, 0.01);
1952 bf.add(b"hello");
1953 bf.add(b"world");
1954 let v = crate::value::StrykeValue::bloom_filter(Arc::new(Mutex::new(bf)));
1955 let (row, map) = drill(&v);
1956 assert!(
1957 row.repr.starts_with("BloomFilter("),
1958 "type tag: {}",
1959 row.repr
1960 );
1961 let kids = map.get(&row.var_ref).expect("expandable");
1962 let names: Vec<&str> = kids.iter().map(|c| c.name.as_str()).collect();
1963 for f in ["inserted", "bit_count", "k", "estimated_fpr"] {
1964 assert!(names.contains(&f), "{f} row present in {names:?}");
1965 }
1966 }
1967
1968 #[test]
1969 fn drill_hll_exposes_cardinality_precision_registers() {
1970 let mut h = crate::sketches::HllSketch::new(12);
1971 for i in 0..50u32 {
1972 h.add(&i.to_le_bytes());
1973 }
1974 let v = crate::value::StrykeValue::hll_sketch(Arc::new(Mutex::new(h)));
1975 let (row, map) = drill(&v);
1976 assert!(row.repr.starts_with("HllSketch("), "type tag: {}", row.repr);
1977 let kids = map.get(&row.var_ref).expect("expandable");
1978 let names: Vec<&str> = kids.iter().map(|c| c.name.as_str()).collect();
1979 for f in ["cardinality", "precision", "registers"] {
1980 assert!(names.contains(&f), "{f} row present in {names:?}");
1981 }
1982 }
1983
1984 #[test]
1985 fn drill_non_sketch_returns_none() {
1986 let v = crate::value::StrykeValue::integer(42);
1989 let mut map: HashMap<u32, Vec<VarChild>> = HashMap::new();
1990 let mut ctx = CaptureCtx {
1991 next_ref: CONTAINER_REF_BASE,
1992 map: &mut map,
1993 };
1994 let child = try_sketch_child("$v", &v, 0, &mut ctx);
1995 assert!(child.is_none(), "non-sketch returns None");
1996 }
1997}