Skip to main content

hara_native/
resp.rs

1#![cfg(not(target_arch = "wasm32"))]
2
3//! RESP listener and wire codec for the native runtime broker. Wire format,
4//! dialects, and operation semantics are specified in
5//! `specs/01-lang/007-resp/draft/hal-resp-protocol.md`.
6
7use std::io::{BufRead, BufReader, BufWriter, Read, Write};
8use std::net::{TcpListener, TcpStream};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::thread::JoinHandle;
12use std::time::Duration;
13
14use crate::core::ExceptionSite;
15use crate::native_cli::{
16    Documentation, DocumentationValue, RuntimeBroker, RuntimeDiagnostic, RuntimeException,
17};
18
19const MAX_LINE: usize = 64 * 1024;
20const MAX_BULK: usize = 64 * 1024 * 1024;
21const MAX_NESTING: usize = 64;
22const MAX_DIAGNOSTIC_DATA_BYTES: usize = 16 * 1024;
23
24#[derive(Clone, Debug)]
25struct RespFailure {
26    code: &'static str,
27    message: String,
28    diagnostic: Option<RespValue>,
29}
30
31impl RespFailure {
32    fn new(code: &'static str, message: impl Into<String>) -> Self {
33        Self {
34            code,
35            message: message.into(),
36            diagnostic: None,
37        }
38    }
39
40    fn evaluation(diagnostic: RuntimeDiagnostic, origin: Option<SourceOrigin>) -> Self {
41        let message = diagnostic.message.clone();
42        Self {
43            code: "EVAL_ERROR",
44            message,
45            diagnostic: Some(diagnostic_payload(&diagnostic, origin.as_ref())),
46        }
47    }
48}
49
50#[derive(Clone, Debug)]
51struct SourceOrigin {
52    file: String,
53    line: usize,
54    column: usize,
55    source: String,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub enum RespValue {
60    Simple(String),
61    Error(String),
62    Integer(i64),
63    Bulk(Option<Vec<u8>>),
64    Array(Option<Vec<RespValue>>),
65}
66
67impl RespValue {
68    pub fn text(&self) -> Option<String> {
69        match self {
70            Self::Simple(value) | Self::Error(value) => Some(value.clone()),
71            Self::Integer(value) => Some(value.to_string()),
72            Self::Bulk(Some(value)) => String::from_utf8(value.clone()).ok(),
73            _ => None,
74        }
75    }
76
77    pub fn bulk(value: impl Into<String>) -> Self {
78        Self::Bulk(Some(value.into().into_bytes()))
79    }
80
81    pub fn array(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
82        Self::Array(Some(
83            values
84                .into_iter()
85                .map(|value| Self::bulk(value.into()))
86                .collect(),
87        ))
88    }
89}
90
91pub struct RespConnection {
92    input: BufReader<TcpStream>,
93    output: BufWriter<TcpStream>,
94}
95
96impl RespConnection {
97    pub fn new(stream: TcpStream) -> Result<Self, String> {
98        let output = stream
99            .try_clone()
100            .map(BufWriter::new)
101            .map_err(|error| format!("RESP socket clone failed: {error}"))?;
102        Ok(Self {
103            input: BufReader::new(stream),
104            output,
105        })
106    }
107
108    pub fn read(&mut self) -> Result<Option<RespValue>, String> {
109        let mut prefix = [0_u8; 1];
110        match self.input.read_exact(&mut prefix) {
111            Ok(()) => self.read_after_prefix(prefix[0], 0).map(Some),
112            Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
113            Err(error) => Err(format!("RESP read failed: {error}")),
114        }
115    }
116
117    fn read_after_prefix(&mut self, prefix: u8, depth: usize) -> Result<RespValue, String> {
118        if depth > MAX_NESTING {
119            return Err("RESP nesting limit exceeded".into());
120        }
121        match prefix {
122            b'+' => Ok(RespValue::Simple(self.line()?)),
123            b'-' => Ok(RespValue::Error(self.line()?)),
124            b':' => self
125                .line()?
126                .parse::<i64>()
127                .map(RespValue::Integer)
128                .map_err(|_| "Invalid RESP integer".into()),
129            b'$' => {
130                let length = self.length()?;
131                if length < 0 {
132                    return Ok(RespValue::Bulk(None));
133                }
134                let length = usize::try_from(length).map_err(|_| "Invalid RESP length")?;
135                if length > MAX_BULK {
136                    return Err("RESP bulk limit exceeded".into());
137                }
138                let mut bytes = vec![0; length];
139                self.input
140                    .read_exact(&mut bytes)
141                    .map_err(|error| format!("RESP read failed: {error}"))?;
142                self.crlf()?;
143                Ok(RespValue::Bulk(Some(bytes)))
144            }
145            b'*' => {
146                let length = self.length()?;
147                if length < 0 {
148                    return Ok(RespValue::Array(None));
149                }
150                let length = usize::try_from(length).map_err(|_| "Invalid RESP length")?;
151                if length > MAX_LINE {
152                    return Err("RESP array limit exceeded".into());
153                }
154                let mut values = Vec::with_capacity(length);
155                for _ in 0..length {
156                    let mut prefix = [0_u8; 1];
157                    self.input
158                        .read_exact(&mut prefix)
159                        .map_err(|error| format!("RESP read failed: {error}"))?;
160                    values.push(self.read_after_prefix(prefix[0], depth + 1)?);
161                }
162                Ok(RespValue::Array(Some(values)))
163            }
164            _ => Err("Unknown RESP type".into()),
165        }
166    }
167
168    fn length(&mut self) -> Result<i64, String> {
169        self.line()?
170            .parse()
171            .map_err(|_| "Invalid RESP length".into())
172    }
173
174    fn line(&mut self) -> Result<String, String> {
175        let mut bytes = Vec::new();
176        let read = self
177            .input
178            .read_until(b'\n', &mut bytes)
179            .map_err(|error| format!("RESP read failed: {error}"))?;
180        if read < 2 || bytes[read - 2..] != *b"\r\n" {
181            return Err("Invalid RESP line ending".into());
182        }
183        if bytes.len() > MAX_LINE {
184            return Err("RESP line limit exceeded".into());
185        }
186        bytes.truncate(read - 2);
187        String::from_utf8(bytes).map_err(|_| "RESP line is not UTF-8".into())
188    }
189
190    fn crlf(&mut self) -> Result<(), String> {
191        let mut ending = [0_u8; 2];
192        self.input
193            .read_exact(&mut ending)
194            .map_err(|error| format!("RESP read failed: {error}"))?;
195        if ending != *b"\r\n" {
196            return Err("Invalid RESP bulk ending".into());
197        }
198        Ok(())
199    }
200
201    pub fn write(&mut self, value: &RespValue) -> Result<(), String> {
202        write_value(&mut self.output, value)?;
203        self.output
204            .flush()
205            .map_err(|error| format!("RESP write failed: {error}"))
206    }
207}
208
209fn write_value(output: &mut impl Write, value: &RespValue) -> Result<(), String> {
210    match value {
211        RespValue::Simple(value) => line_value(output, b'+', value),
212        RespValue::Error(value) => line_value(output, b'-', value),
213        RespValue::Integer(value) => line_value(output, b':', &value.to_string()),
214        RespValue::Bulk(None) => output
215            .write_all(b"$-1\r\n")
216            .map_err(|error| format!("RESP write failed: {error}")),
217        RespValue::Bulk(Some(bytes)) => output
218            .write_all(format!("${}\r\n", bytes.len()).as_bytes())
219            .and_then(|_| output.write_all(bytes))
220            .and_then(|_| output.write_all(b"\r\n"))
221            .map_err(|error| format!("RESP write failed: {error}")),
222        RespValue::Array(None) => output
223            .write_all(b"*-1\r\n")
224            .map_err(|error| format!("RESP write failed: {error}")),
225        RespValue::Array(Some(values)) => {
226            output
227                .write_all(format!("*{}\r\n", values.len()).as_bytes())
228                .map_err(|error| format!("RESP write failed: {error}"))?;
229            for value in values {
230                write_value(output, value)?;
231            }
232            Ok(())
233        }
234    }
235}
236
237fn line_value(output: &mut impl Write, prefix: u8, value: &str) -> Result<(), String> {
238    if value.contains(['\r', '\n']) {
239        return Err("RESP line values cannot contain CR or LF".into());
240    }
241    output
242        .write_all(&[prefix])
243        .and_then(|_| output.write_all(value.as_bytes()))
244        .and_then(|_| output.write_all(b"\r\n"))
245        .map_err(|error| format!("RESP write failed: {error}"))
246}
247
248pub struct RespServer {
249    host: String,
250    port: u16,
251    running: Arc<AtomicBool>,
252    thread: Option<JoinHandle<()>>,
253}
254
255impl RespServer {
256    pub fn start(host: &str, port: u16, broker: RuntimeBroker) -> Result<Self, String> {
257        let listener = TcpListener::bind((host, port))
258            .map_err(|error| format!("RESP bind {host}:{port} failed: {error}"))?;
259        let address = listener
260            .local_addr()
261            .map_err(|error| format!("RESP address failed: {error}"))?;
262        listener
263            .set_nonblocking(true)
264            .map_err(|error| format!("RESP listener setup failed: {error}"))?;
265        let running = Arc::new(AtomicBool::new(true));
266        let active = running.clone();
267        let instance = format!("RUST-{}-{}", std::process::id(), address.port());
268        let root = std::env::current_dir()
269            .unwrap_or_default()
270            .display()
271            .to_string();
272        let thread = std::thread::Builder::new()
273            .name("hara-resp-listener".into())
274            .spawn(move || {
275                while active.load(Ordering::Acquire) {
276                    match listener.accept() {
277                        Ok((stream, _)) => {
278                            if stream.set_nonblocking(false).is_err() {
279                                continue;
280                            }
281                            let broker = broker.clone();
282                            let instance = instance.clone();
283                            let root = root.clone();
284                            let _ = std::thread::Builder::new()
285                                .name("hara-resp-client".into())
286                                .spawn(move || serve(stream, broker, &instance, &root));
287                        }
288                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
289                            std::thread::sleep(Duration::from_millis(10));
290                        }
291                        Err(_) => break,
292                    }
293                }
294            })
295            .map_err(|error| format!("RESP listener thread failed: {error}"))?;
296        Ok(Self {
297            host: host.into(),
298            port: address.port(),
299            running,
300            thread: Some(thread),
301        })
302    }
303
304    pub fn endpoint(&self) -> String {
305        format!("{}:{}", self.host, self.port)
306    }
307
308    pub fn stop(&mut self) {
309        self.running.store(false, Ordering::Release);
310        if let Some(thread) = self.thread.take() {
311            let _ = thread.join();
312        }
313    }
314}
315
316impl Drop for RespServer {
317    fn drop(&mut self) {
318        self.stop();
319    }
320}
321
322fn serve(stream: TcpStream, broker: RuntimeBroker, instance: &str, root: &str) {
323    let Ok(mut connection) = RespConnection::new(stream) else {
324        return;
325    };
326    let mut protocol = 3_u8;
327    let mut attached = "ROOT".to_owned();
328    loop {
329        let request = match connection.read() {
330            Ok(Some(RespValue::Array(Some(values)))) => values,
331            Ok(Some(_)) => {
332                let _ = connection.write(&RespValue::Error("BAD_REQUEST expected array".into()));
333                continue;
334            }
335            Ok(None) => return,
336            Err(error) => {
337                let _ = connection.write(&RespValue::Error(format!("BAD_REQUEST {error}")));
338                continue;
339            }
340        };
341        let words = request
342            .iter()
343            .map(RespValue::text)
344            .collect::<Option<Vec<_>>>();
345        let Some(words) = words else {
346            let _ = connection.write(&RespValue::Error(
347                "BAD_REQUEST textual arguments required".into(),
348            ));
349            continue;
350        };
351        if words.is_empty() {
352            continue;
353        }
354        let operation = words[0].to_ascii_uppercase();
355        if operation == "QUIT" {
356            let _ = connection.write(&RespValue::Simple("OK".into()));
357            return;
358        }
359        if operation == "HELLO" {
360            protocol = words
361                .get(1)
362                .and_then(|value| value.parse().ok())
363                .unwrap_or(3);
364            let hello = RespValue::array([
365                "SERVER",
366                "HARA",
367                "INSTANCE",
368                instance,
369                "PROTOCOL",
370                &protocol.to_string(),
371                "ROOT",
372                root,
373            ]);
374            let _ = connection.write(&hello);
375            continue;
376        }
377        if protocol >= 4 {
378            let id = words.get(1).cloned().unwrap_or_else(|| "?".into());
379            handle_v4(
380                &mut connection,
381                &broker,
382                &mut attached,
383                &operation,
384                &id,
385                &words[2..],
386            );
387        } else {
388            handle_legacy(
389                &mut connection,
390                &broker,
391                &mut attached,
392                &operation,
393                &words[1..],
394            );
395        }
396    }
397}
398
399fn handle_v4(
400    connection: &mut RespConnection,
401    broker: &RuntimeBroker,
402    attached: &mut String,
403    operation: &str,
404    id: &str,
405    arguments: &[String],
406) {
407    let result = operation_result(broker, attached, operation, arguments);
408    match result {
409        Ok(value) => {
410            let _ = connection.write(&RespValue::Array(Some(vec![
411                RespValue::bulk("RESULT"),
412                RespValue::bulk(id),
413                value,
414            ])));
415            let _ = connection.write(&RespValue::array(["DONE", id, "OK"]));
416        }
417        Err(failure) => {
418            let mut frame = vec![
419                RespValue::bulk("ERROR"),
420                RespValue::bulk(id),
421                RespValue::bulk(failure.code),
422                RespValue::bulk(failure.message),
423            ];
424            if let Some(diagnostic) = failure.diagnostic {
425                frame.push(diagnostic);
426            }
427            let _ = connection.write(&RespValue::Array(Some(frame)));
428            let _ = connection.write(&RespValue::array(["DONE", id, "ERROR"]));
429        }
430    }
431}
432
433fn handle_legacy(
434    connection: &mut RespConnection,
435    broker: &RuntimeBroker,
436    attached: &mut String,
437    operation: &str,
438    arguments: &[String],
439) {
440    let result = if operation == "EVAL" && arguments.len() >= 2 {
441        broker
442            .eval(&arguments[0], &arguments[1])
443            .map(RespValue::bulk)
444            .map_err(|message| RespFailure::new("EVAL_ERROR", message))
445    } else {
446        operation_result(broker, attached, operation, arguments)
447    };
448    let response = match result {
449        Ok(value) => legacy_value(value),
450        Err(failure) => RespValue::Error(format!("{} {}", failure.code, failure.message)),
451    };
452    let _ = connection.write(&response);
453}
454
455fn operation_result(
456    broker: &RuntimeBroker,
457    attached: &mut String,
458    operation: &str,
459    arguments: &[String],
460) -> Result<RespValue, RespFailure> {
461    match operation {
462        "EVAL" => {
463            let source = arguments
464                .first()
465                .ok_or_else(|| RespFailure::new("BAD_REQUEST", "EVAL requires source"))?;
466            broker
467                .eval_diagnostic(attached, source)
468                .map(RespValue::bulk)
469                .map_err(|diagnostic| RespFailure::evaluation(diagnostic, eval_origin(arguments)))
470        }
471        "COMPLETE" => {
472            let prefix = arguments.first().map_or("", String::as_str);
473            broker
474                .complete(attached, prefix)
475                .map(RespValue::array)
476                .map_err(|error| RespFailure::new("NO_SESSION", error))
477        }
478        "DOC" => {
479            let symbol = arguments
480                .first()
481                .ok_or_else(|| RespFailure::new("BAD_REQUEST", "DOC requires symbol"))?;
482            broker
483                .documentation(attached, symbol)
484                .map(documentation_value)
485                .map_err(|error| {
486                    if error.starts_with("No session:") {
487                        RespFailure::new("NO_SESSION", error)
488                    } else {
489                        RespFailure::new("DOC_NOT_FOUND", error)
490                    }
491                })
492        }
493        "SESSION" => session_operation(broker, attached, arguments),
494        "COMMANDS" => Ok(RespValue::bulk(
495            "HELLO EVAL COMPLETE DOC SESSION COMMANDS INFO QUIT",
496        )),
497        "INFO" => broker
498            .info(attached)
499            .map(RespValue::bulk)
500            .map_err(|error| RespFailure::new("NO_SESSION", error)),
501        _ => Err(RespFailure::new(
502            "UNKNOWN_OP",
503            format!("Unknown operation: {operation}"),
504        )),
505    }
506}
507
508fn session_operation(
509    broker: &RuntimeBroker,
510    attached: &mut String,
511    arguments: &[String],
512) -> Result<RespValue, RespFailure> {
513    let action = arguments
514        .first()
515        .map(|value| value.to_ascii_uppercase())
516        .ok_or_else(|| RespFailure::new("BAD_REQUEST", "SESSION requires an action"))?;
517    match action.as_str() {
518        "NEW" => broker
519            .create(
520                arguments
521                    .get(1)
522                    .ok_or_else(|| RespFailure::new("BAD_REQUEST", "SESSION NEW requires name"))?,
523            )
524            .map(RespValue::bulk)
525            .map_err(|error| RespFailure::new("BAD_REQUEST", error)),
526        "LIST" => broker
527            .list()
528            .map(RespValue::array)
529            .map_err(|error| RespFailure::new("INTERNAL_ERROR", error)),
530        "ATTACH" => {
531            let name = arguments
532                .get(1)
533                .ok_or_else(|| RespFailure::new("BAD_REQUEST", "SESSION ATTACH requires name"))?;
534            broker
535                .info(name)
536                .map_err(|error| RespFailure::new("NO_SESSION", error))?;
537            *attached = name.clone();
538            Ok(RespValue::bulk(name))
539        }
540        "DETACH" => {
541            *attached = "ROOT".into();
542            Ok(RespValue::bulk("ROOT"))
543        }
544        "INFO" => broker
545            .info(attached)
546            .map(RespValue::bulk)
547            .map_err(|error| RespFailure::new("NO_SESSION", error)),
548        "CLOSE" => {
549            broker
550                .close(arguments.get(1).ok_or_else(|| {
551                    RespFailure::new("BAD_REQUEST", "SESSION CLOSE requires name")
552                })?)
553                .map(RespValue::bulk)
554                .map_err(|error| RespFailure::new("BAD_REQUEST", error))
555        }
556        _ => Err(RespFailure::new(
557            "BAD_REQUEST",
558            format!("Unknown SESSION action: {action}"),
559        )),
560    }
561}
562
563fn eval_origin(arguments: &[String]) -> Option<SourceOrigin> {
564    let source = arguments.first()?.clone();
565    let mut file = None;
566    let mut line = None;
567    let mut column = None;
568    for pair in arguments[1..].chunks_exact(2) {
569        match pair[0].to_ascii_uppercase().as_str() {
570            "FILE" => file = Some(pair[1].clone()),
571            "LINE" => line = pair[1].parse::<usize>().ok().filter(|value| *value > 0),
572            "COLUMN" => column = pair[1].parse::<usize>().ok().filter(|value| *value > 0),
573            _ => {}
574        }
575    }
576    Some(SourceOrigin {
577        file: file?,
578        line: line?,
579        column: column.unwrap_or(1),
580        source,
581    })
582}
583
584fn truncated_text(value: String) -> String {
585    if value.len() <= MAX_DIAGNOSTIC_DATA_BYTES {
586        return value;
587    }
588    let mut end = MAX_DIAGNOSTIC_DATA_BYTES.saturating_sub(3);
589    while end > 0 && !value.is_char_boundary(end) {
590        end -= 1;
591    }
592    format!("{}...", &value[..end])
593}
594
595fn optional_bulk(value: Option<String>) -> RespValue {
596    value.map_or(RespValue::Bulk(None), RespValue::bulk)
597}
598
599fn optional_integer(value: Option<usize>) -> RespValue {
600    value.map_or(RespValue::Bulk(None), |value| {
601        RespValue::Integer(value as i64)
602    })
603}
604
605fn exception_site(exception: &RuntimeException) -> Option<ExceptionSite> {
606    exception.throws.last().cloned()
607}
608
609fn site_location(
610    site: Option<&ExceptionSite>,
611    origin: Option<&SourceOrigin>,
612    use_origin: bool,
613) -> (Option<String>, Option<usize>, Option<usize>) {
614    let Some(site) = site else {
615        return origin
616            .filter(|_| use_origin)
617            .map(|origin| {
618                (
619                    Some(origin.file.clone()),
620                    Some(origin.line),
621                    Some(origin.column),
622                )
623            })
624            .unwrap_or((None, None, None));
625    };
626    if let Some(resource) = &site.resource {
627        return (
628            Some(resource.clone()),
629            (site.line > 0).then_some(site.line),
630            (site.column > 0).then_some(site.column),
631        );
632    }
633    if use_origin {
634        if let Some(origin) = origin {
635            let line = (site.line > 0).then(|| origin.line + site.line - 1);
636            let column = if site.line <= 1 {
637                (site.column > 0).then(|| origin.column + site.column - 1)
638            } else {
639                (site.column > 0).then_some(site.column)
640            };
641            return (Some(origin.file.clone()), line, column);
642        }
643    }
644    (
645        None,
646        (site.line > 0).then_some(site.line),
647        (site.column > 0).then_some(site.column),
648    )
649}
650
651fn location_payload(site: Option<&ExceptionSite>, origin: Option<&SourceOrigin>) -> RespValue {
652    let use_origin = site.is_none_or(|site| site.resource.is_none());
653    let (file, line, column) = site_location(site, origin, use_origin);
654    RespValue::Array(Some(vec![
655        RespValue::bulk("FILE"),
656        optional_bulk(file),
657        RespValue::bulk("LINE"),
658        optional_integer(line),
659        RespValue::bulk("COLUMN"),
660        optional_integer(column),
661    ]))
662}
663
664fn exception_payload(exception: &RuntimeException) -> RespValue {
665    let class = exception.class.clone().map(truncated_text);
666    let code = exception.code.clone().map(truncated_text);
667    let cause = exception.cause.as_deref().map(exception_payload);
668    let throws = exception
669        .throws
670        .iter()
671        .map(|site| location_payload(Some(site), None))
672        .collect::<Vec<_>>();
673    RespValue::Array(Some(vec![
674        RespValue::bulk("MESSAGE"),
675        RespValue::bulk(truncated_text(exception.message.clone())),
676        RespValue::bulk("CLASS"),
677        optional_bulk(class),
678        RespValue::bulk("CODE"),
679        optional_bulk(code),
680        RespValue::bulk("DATA"),
681        RespValue::bulk(truncated_text(exception.data.clone())),
682        RespValue::bulk("CAUSE"),
683        cause.unwrap_or(RespValue::Bulk(None)),
684        RespValue::bulk("THROWS"),
685        RespValue::Array(Some(throws)),
686    ]))
687}
688
689fn frame_payload(frame: &crate::core::TraceFrame, origin: Option<&SourceOrigin>) -> RespValue {
690    let use_origin = frame.namespace.is_none()
691        && frame
692            .site
693            .as_ref()
694            .is_none_or(|site| site.resource.is_none());
695    let (file, line, column) = site_location(frame.site.as_ref(), origin, use_origin);
696    RespValue::Array(Some(vec![
697        RespValue::bulk("FUNCTION"),
698        RespValue::bulk(frame.name.clone()),
699        RespValue::bulk("NAMESPACE"),
700        optional_bulk(frame.namespace.clone()),
701        RespValue::bulk("FILE"),
702        optional_bulk(file),
703        RespValue::bulk("LINE"),
704        optional_integer(line),
705        RespValue::bulk("COLUMN"),
706        optional_integer(column),
707    ]))
708}
709
710fn evaluation_frame_payload(origin: &SourceOrigin) -> RespValue {
711    RespValue::Array(Some(vec![
712        RespValue::bulk("FUNCTION"),
713        RespValue::bulk("<eval>"),
714        RespValue::bulk("NAMESPACE"),
715        RespValue::Bulk(None),
716        RespValue::bulk("FILE"),
717        RespValue::bulk(origin.file.clone()),
718        RespValue::bulk("LINE"),
719        RespValue::Integer(origin.line as i64),
720        RespValue::bulk("COLUMN"),
721        RespValue::Integer(origin.column as i64),
722    ]))
723}
724
725fn source_excerpt(origin: Option<&SourceOrigin>, line: Option<usize>) -> RespValue {
726    let Some(origin) = origin else {
727        return RespValue::Bulk(None);
728    };
729    let Some(line) = line else {
730        return RespValue::Bulk(None);
731    };
732    let local_line = line.checked_sub(origin.line).map_or(0, |offset| offset + 1);
733    if local_line == 0 {
734        return RespValue::Bulk(None);
735    }
736    let lines = origin.source.lines().collect::<Vec<_>>();
737    if local_line > lines.len() {
738        return RespValue::Bulk(None);
739    }
740    let start = local_line.saturating_sub(3);
741    let end = usize::min(lines.len(), local_line + 2);
742    let text = lines[start..end].join("\n");
743    RespValue::Array(Some(vec![
744        RespValue::bulk("START-LINE"),
745        RespValue::Integer((origin.line + start) as i64),
746        RespValue::bulk("TEXT"),
747        RespValue::bulk(truncated_text(text)),
748    ]))
749}
750
751fn diagnostic_payload(diagnostic: &RuntimeDiagnostic, origin: Option<&SourceOrigin>) -> RespValue {
752    let exception = diagnostic.exception.as_ref();
753    let primary_site = exception.and_then(exception_site).or_else(|| {
754        diagnostic
755            .frames
756            .iter()
757            .rev()
758            .find_map(|frame| frame.site.clone())
759    });
760    let (_, primary_line, _) = site_location(primary_site.as_ref(), origin, true);
761    let mut frames = diagnostic
762        .frames
763        .iter()
764        .rev()
765        .map(|frame| frame_payload(frame, origin))
766        .collect::<Vec<_>>();
767    if frames.is_empty() {
768        if let Some(origin) = origin {
769            frames.push(evaluation_frame_payload(origin));
770        }
771    }
772    RespValue::Array(Some(vec![
773        RespValue::bulk("VERSION"),
774        RespValue::Integer(1),
775        RespValue::bulk("MESSAGE"),
776        RespValue::bulk(truncated_text(diagnostic.message.clone())),
777        RespValue::bulk("EXCEPTION"),
778        exception.map_or(RespValue::Bulk(None), exception_payload),
779        RespValue::bulk("PRIMARY"),
780        location_payload(primary_site.as_ref(), origin),
781        RespValue::bulk("EXCERPT"),
782        source_excerpt(origin, primary_line),
783        RespValue::bulk("FRAMES"),
784        RespValue::Array(Some(frames)),
785    ]))
786}
787
788fn documentation_part(value: DocumentationValue) -> RespValue {
789    match value {
790        DocumentationValue::Nil => RespValue::Bulk(None),
791        DocumentationValue::Boolean(value) => RespValue::bulk(value.to_string()),
792        DocumentationValue::Integer(value) => RespValue::Integer(value),
793        DocumentationValue::String(value) => RespValue::bulk(value),
794        DocumentationValue::Array(values) => {
795            RespValue::Array(Some(values.into_iter().map(documentation_part).collect()))
796        }
797    }
798}
799
800fn documentation_value(documentation: Documentation) -> RespValue {
801    RespValue::Array(Some(vec![
802        RespValue::bulk("SYMBOL"),
803        RespValue::bulk(documentation.symbol),
804        RespValue::bulk("DOC"),
805        documentation
806            .doc
807            .map_or(RespValue::Bulk(None), RespValue::bulk),
808        RespValue::bulk("ARGLISTS"),
809        documentation_part(documentation.arglists),
810        RespValue::bulk("FILE"),
811        documentation
812            .file
813            .map_or(RespValue::Bulk(None), RespValue::bulk),
814        RespValue::bulk("LINE"),
815        documentation
816            .line
817            .map_or(RespValue::Bulk(None), RespValue::Integer),
818        RespValue::bulk("COLUMN"),
819        documentation
820            .column
821            .map_or(RespValue::Bulk(None), RespValue::Integer),
822    ]))
823}
824
825fn legacy_value(value: RespValue) -> RespValue {
826    match value {
827        RespValue::Array(Some(values)) => RespValue::bulk(
828            values
829                .into_iter()
830                .filter_map(|value| value.text())
831                .collect::<Vec<_>>()
832                .join("\n"),
833        ),
834        value => value,
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use super::{RespConnection, RespValue};
841    use std::net::{TcpListener, TcpStream};
842
843    fn array(value: &RespValue) -> &[RespValue] {
844        match value {
845            RespValue::Array(Some(values)) => values,
846            value => panic!("expected RESP array, got {value:?}"),
847        }
848    }
849
850    fn field<'a>(values: &'a [RespValue], name: &str) -> &'a RespValue {
851        values
852            .chunks_exact(2)
853            .find_map(|pair| (pair[0].text().as_deref() == Some(name)).then_some(&pair[1]))
854            .unwrap_or_else(|| panic!("missing {name} in {values:?}"))
855    }
856
857    #[test]
858    fn resp2_values_round_trip() {
859        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
860        let address = listener.local_addr().unwrap();
861        let writer = std::thread::spawn(move || {
862            let mut connection = RespConnection::new(TcpStream::connect(address).unwrap()).unwrap();
863            connection
864                .write(&RespValue::Array(Some(vec![
865                    RespValue::Simple("OK".into()),
866                    RespValue::Integer(42),
867                    RespValue::Bulk(None),
868                    RespValue::bulk("hello"),
869                ])))
870                .unwrap();
871        });
872        let (stream, _) = listener.accept().unwrap();
873        let mut connection = RespConnection::new(stream).unwrap();
874        assert_eq!(
875            connection.read().unwrap().unwrap(),
876            RespValue::Array(Some(vec![
877                RespValue::Simple("OK".into()),
878                RespValue::Integer(42),
879                RespValue::Bulk(None),
880                RespValue::bulk("hello"),
881            ]))
882        );
883        writer.join().unwrap();
884    }
885    #[test]
886    fn server_streams_protocol_four_and_shares_root_with_legacy_clients() {
887        let broker = crate::native_cli::RuntimeBroker::start().unwrap();
888        broker.eval("ROOT", "(def answer 41)").unwrap();
889        let mut server = super::RespServer::start("127.0.0.1", 0, broker).unwrap();
890        let endpoint = server.endpoint();
891
892        let mut legacy = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
893        legacy
894            .write(&RespValue::array(["EVAL", "ROOT", "(+ answer 1)"]))
895            .unwrap();
896        assert_eq!(legacy.read().unwrap().unwrap().text().unwrap(), "42");
897        legacy
898            .write(&RespValue::array([
899                "EVAL",
900                "ROOT",
901                "(throw (ex :test/failed {:value 41}))",
902            ]))
903            .unwrap();
904        assert!(matches!(
905            legacy.read().unwrap().unwrap(),
906            RespValue::Error(_)
907        ));
908
909        let mut modern = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
910        modern.write(&RespValue::array(["HELLO", "4"])).unwrap();
911        let hello = modern.read().unwrap().unwrap();
912        assert!(matches!(hello, RespValue::Array(Some(_))));
913        modern
914            .write(&RespValue::array(["EVAL", "REQ-1", "answer"]))
915            .unwrap();
916        assert_eq!(
917            modern.read().unwrap().unwrap(),
918            RespValue::array(["RESULT", "REQ-1", "41"])
919        );
920        assert_eq!(
921            modern.read().unwrap().unwrap(),
922            RespValue::array(["DONE", "REQ-1", "OK"])
923        );
924        modern
925            .write(&RespValue::array(["COMPLETE", "REQ-2", "ans"]))
926            .unwrap();
927        assert_eq!(
928            modern.read().unwrap().unwrap(),
929            RespValue::Array(Some(vec![
930                RespValue::bulk("RESULT"),
931                RespValue::bulk("REQ-2"),
932                RespValue::array(["answer"]),
933            ]))
934        );
935        assert_eq!(
936            modern.read().unwrap().unwrap(),
937            RespValue::array(["DONE", "REQ-2", "OK"])
938        );
939        modern
940            .write(&RespValue::array([
941                "EVAL",
942                "REQ-3",
943                concat!(
944                    "(defn ^{:file \"/tmp/sample.hal\" :line 12 :column 3} located ",
945                    "\"A located function.\" [value] value)"
946                ),
947            ]))
948            .unwrap();
949        modern.read().unwrap().unwrap();
950        modern.read().unwrap().unwrap();
951        modern
952            .write(&RespValue::array(["DOC", "REQ-4", "located"]))
953            .unwrap();
954        assert_eq!(
955            modern.read().unwrap().unwrap(),
956            RespValue::Array(Some(vec![
957                RespValue::bulk("RESULT"),
958                RespValue::bulk("REQ-4"),
959                RespValue::Array(Some(vec![
960                    RespValue::bulk("SYMBOL"),
961                    RespValue::bulk("located"),
962                    RespValue::bulk("DOC"),
963                    RespValue::bulk("A located function."),
964                    RespValue::bulk("ARGLISTS"),
965                    RespValue::Array(Some(vec![RespValue::array(["value"])])),
966                    RespValue::bulk("FILE"),
967                    RespValue::bulk("/tmp/sample.hal"),
968                    RespValue::bulk("LINE"),
969                    RespValue::Integer(12),
970                    RespValue::bulk("COLUMN"),
971                    RespValue::Integer(3),
972                ])),
973            ]))
974        );
975        assert_eq!(
976            modern.read().unwrap().unwrap(),
977            RespValue::array(["DONE", "REQ-4", "OK"])
978        );
979        server.stop();
980    }
981
982    #[test]
983    fn server_v4_error_carries_a_structured_evaluation_diagnostic() {
984        let broker = crate::native_cli::RuntimeBroker::start().unwrap();
985        let mut server = super::RespServer::start("127.0.0.1", 0, broker).unwrap();
986        let endpoint = server.endpoint();
987        let mut client = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
988        client.write(&RespValue::array(["HELLO", "4"])).unwrap();
989        client.read().unwrap().unwrap();
990
991        let source = "(defn boom [] (throw (ex :test/failed {:value 41})))\n(boom)";
992        client
993            .write(&RespValue::array([
994                "EVAL",
995                "REQ-ERROR",
996                source,
997                "FILE",
998                "/tmp/request.hal",
999                "LINE",
1000                "10",
1001                "COLUMN",
1002                "5",
1003            ]))
1004            .unwrap();
1005        let error = client.read().unwrap().unwrap();
1006        let error_values = array(&error);
1007        assert_eq!(error_values.len(), 5);
1008        assert_eq!(error_values[0].text().as_deref(), Some("ERROR"));
1009        assert_eq!(error_values[1].text().as_deref(), Some("REQ-ERROR"));
1010        assert_eq!(error_values[2].text().as_deref(), Some("EVAL_ERROR"));
1011
1012        let diagnostic = array(&error_values[4]);
1013        assert_eq!(field(diagnostic, "VERSION"), &RespValue::Integer(1));
1014        let exception = array(field(diagnostic, "EXCEPTION"));
1015        assert_eq!(
1016            field(exception, "CODE").text().as_deref(),
1017            Some(":test/failed")
1018        );
1019        assert!(field(exception, "DATA")
1020            .text()
1021            .is_some_and(|data| data.contains(":value 41")));
1022        let primary = array(field(diagnostic, "PRIMARY"));
1023        assert_eq!(
1024            field(primary, "FILE").text().as_deref(),
1025            Some("/tmp/request.hal")
1026        );
1027        assert_eq!(field(primary, "LINE"), &RespValue::Integer(10));
1028        let excerpt = array(field(diagnostic, "EXCERPT"));
1029        assert_eq!(field(excerpt, "START-LINE"), &RespValue::Integer(10));
1030        assert!(field(excerpt, "TEXT")
1031            .text()
1032            .is_some_and(|text| text.contains("(boom)")));
1033        assert!(!array(field(diagnostic, "FRAMES")).is_empty());
1034        assert_eq!(
1035            client.read().unwrap().unwrap(),
1036            RespValue::array(["DONE", "REQ-ERROR", "ERROR"])
1037        );
1038        server.stop();
1039    }
1040
1041    #[test]
1042    fn server_v4_validation_errors_carry_a_clickable_evaluation_location() {
1043        let broker = crate::native_cli::RuntimeBroker::start().unwrap();
1044        let mut server = super::RespServer::start("127.0.0.1", 0, broker).unwrap();
1045        let endpoint = server.endpoint();
1046        let mut client = RespConnection::new(TcpStream::connect(&endpoint).unwrap()).unwrap();
1047        client.write(&RespValue::array(["HELLO", "4"])).unwrap();
1048        client.read().unwrap().unwrap();
1049
1050        client
1051            .write(&RespValue::array([
1052                "EVAL",
1053                "REQ-VALIDATION-ERROR",
1054                "(ex :unknown {})",
1055                "FILE",
1056                "/tmp/validation.hal",
1057                "LINE",
1058                "12",
1059                "COLUMN",
1060                "3",
1061            ]))
1062            .unwrap();
1063        let error = client.read().unwrap().unwrap();
1064        let error_values = array(&error);
1065        assert_eq!(error_values.len(), 5);
1066        assert_eq!(error_values[2].text().as_deref(), Some("EVAL_ERROR"));
1067        let diagnostic = array(&error_values[4]);
1068        let primary = array(field(diagnostic, "PRIMARY"));
1069        assert_eq!(
1070            field(primary, "FILE").text().as_deref(),
1071            Some("/tmp/validation.hal")
1072        );
1073        assert_eq!(field(primary, "LINE"), &RespValue::Integer(12));
1074        assert_eq!(field(primary, "COLUMN"), &RespValue::Integer(3));
1075        assert!(field(array(field(diagnostic, "EXCERPT")), "TEXT")
1076            .text()
1077            .is_some_and(|text| text.contains("(ex :unknown {})")));
1078        assert!(!array(field(diagnostic, "FRAMES")).is_empty());
1079        assert_eq!(
1080            client.read().unwrap().unwrap(),
1081            RespValue::array(["DONE", "REQ-VALIDATION-ERROR", "ERROR"])
1082        );
1083        server.stop();
1084    }
1085}