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