1use lex_bytecode::vm::{EffectHandler, Vm};
8use lex_bytecode::{Program, Value};
9use smol_str::SmolStr;
10use std::path::PathBuf;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Mutex, OnceLock};
13use std::sync::Arc;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use crate::builtins::{call_pure_builtin, is_pure_call};
17use crate::policy::Policy;
18
19pub trait IoSink: Send {
22 fn print_line(&mut self, s: &str);
23}
24
25pub struct StdoutSink;
26impl IoSink for StdoutSink {
27 fn print_line(&mut self, s: &str) {
28 use std::io::Write;
29 print!("{s}");
30 let _ = std::io::stdout().flush();
31 }
32}
33
34#[derive(Default)]
35pub struct CapturedSink { pub lines: Vec<String> }
36impl IoSink for CapturedSink {
37 fn print_line(&mut self, s: &str) { self.lines.push(s.to_string()); }
38}
39
40pub type StreamRegistry =
43 std::collections::HashMap<String, Box<dyn Iterator<Item = String> + Send>>;
44
45pub struct DefaultHandler {
46 policy: Policy,
47 pub sink: Box<dyn IoSink>,
48 pub read_root: Option<PathBuf>,
51 pub budget_remaining: Arc<AtomicU64>,
58 pub budget_ceiling: Option<u64>,
62 pub program: Option<Arc<Program>>,
66 pub chat_registry: Option<Arc<crate::ws::ChatRegistry>>,
70 pub mcp_clients: crate::mcp_client::McpClientCache,
76 pub streams: Arc<std::sync::Mutex<StreamRegistry>>,
83 pub next_stream_id: Arc<std::sync::atomic::AtomicU64>,
85 arena_stack: Vec<(u64, crate::arena::Arena)>,
98 next_scope_id: u64,
103 pub program_args: Vec<String>,
106}
107
108impl DefaultHandler {
109 pub fn new(policy: Policy) -> Self {
110 let ceiling = policy.budget;
114 let initial = ceiling.unwrap_or(u64::MAX);
115 Self {
116 policy,
117 sink: Box::new(StdoutSink),
118 read_root: None,
119 budget_remaining: Arc::new(AtomicU64::new(initial)),
120 budget_ceiling: ceiling,
121 program: None,
122 chat_registry: None,
123 mcp_clients: crate::mcp_client::McpClientCache::with_capacity(16),
124 streams: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
125 next_stream_id: Arc::new(std::sync::atomic::AtomicU64::new(0)),
126 arena_stack: Vec::new(),
127 next_scope_id: 1,
128 program_args: Vec::new(),
129 }
130 }
131
132 pub fn active_arena(&self) -> Option<&crate::arena::Arena> {
138 self.arena_stack.last().map(|(_, a)| a)
139 }
140
141 pub fn arena_stack_depth(&self) -> usize {
144 self.arena_stack.len()
145 }
146
147 pub fn with_program(mut self, program: Arc<Program>) -> Self {
148 self.program = Some(program); self
149 }
150
151 pub fn with_chat_registry(mut self, registry: Arc<crate::ws::ChatRegistry>) -> Self {
152 self.chat_registry = Some(registry); self
153 }
154
155 pub fn with_sink(mut self, sink: Box<dyn IoSink>) -> Self {
156 self.sink = sink; self
157 }
158
159 pub fn with_read_root(mut self, root: PathBuf) -> Self {
160 self.read_root = Some(root); self
161 }
162
163 pub fn with_program_args(mut self, args: Vec<String>) -> Self {
164 self.program_args = args; self
165 }
166
167 fn ensure_kind_allowed(&self, kind: &str) -> Result<(), String> {
168 if self.policy.allow_effects.contains(kind) {
169 Ok(())
170 } else {
171 Err(format!("effect `{kind}` not in --allow-effects"))
172 }
173 }
174
175 fn resolve_read_path(&self, p: &str) -> PathBuf {
176 match &self.read_root {
177 Some(root) => root.join(p.trim_start_matches('/')),
178 None => PathBuf::from(p),
179 }
180 }
181
182 fn dispatch_log(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
183 match op {
184 "debug" | "info" | "warn" | "error" => {
185 let msg = expect_str(args.first())?;
186 let level = match op {
187 "debug" => LogLevel::Debug,
188 "info" => LogLevel::Info,
189 "warn" => LogLevel::Warn,
190 _ => LogLevel::Error,
191 };
192 emit_log(level, msg);
193 Ok(Value::Unit)
194 }
195 "set_level" => {
196 let s = expect_str(args.first())?;
197 match parse_log_level(s) {
198 Some(l) => {
199 log_state().lock().unwrap().level = l;
200 Ok(ok(Value::Unit))
201 }
202 None => Ok(err(Value::Str(format!(
203 "log.set_level: unknown level `{s}`; expected debug|info|warn|error").into()))),
204 }
205 }
206 "set_format" => {
207 let s = expect_str(args.first())?;
208 let fmt = match s {
209 "text" => LogFormat::Text,
210 "json" => LogFormat::Json,
211 other => return Ok(err(Value::Str(format!(
212 "log.set_format: unknown format `{other}`; expected text|json").into()))),
213 };
214 log_state().lock().unwrap().format = fmt;
215 Ok(ok(Value::Unit))
216 }
217 "set_sink" => {
218 let path = expect_str(args.first())?;
219 if path == "-" {
220 log_state().lock().unwrap().sink = LogSink::Stderr;
221 return Ok(ok(Value::Unit));
222 }
223 if let Err(e) = self.ensure_fs_write_path(path) {
224 return Ok(err(Value::Str(e.into())));
225 }
226 match std::fs::OpenOptions::new()
227 .create(true).append(true).open(path)
228 {
229 Ok(f) => {
230 log_state().lock().unwrap().sink = LogSink::File(std::sync::Arc::new(Mutex::new(f)));
231 Ok(ok(Value::Unit))
232 }
233 Err(e) => Ok(err(Value::Str(format!("log.set_sink `{path}`: {e}").into()))),
234 }
235 }
236 other => Err(format!("unsupported log.{other}")),
237 }
238 }
239
240 fn dispatch_process(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
241 match op {
242 "spawn" => {
243 let cmd = expect_str(args.first())?.to_string();
244 let raw_args = match args.get(1) {
245 Some(Value::List(items)) => items.clone(),
246 _ => return Err("process.spawn: args must be List[Str]".into()),
247 };
248 let str_args: Result<Vec<String>, String> = raw_args.iter().map(|v| match v {
249 Value::Str(s) => Ok(s.to_string()),
250 other => Err(format!("process.spawn: arg must be Str, got {other:?}")),
251 }).collect();
252 let str_args = str_args?;
253 let opts = match args.get(2) {
254 Some(Value::Record { fields: r, .. }) => r.clone(),
255 _ => return Err("process.spawn: missing or invalid opts record".into()),
256 };
257
258 if !self.policy.allow_proc.is_empty() {
260 let basename = std::path::Path::new(&cmd)
261 .file_name()
262 .and_then(|s| s.to_str())
263 .unwrap_or(&cmd);
264 if !self.policy.allow_proc.iter().any(|a| a == basename) {
265 return Ok(err(Value::Str(format!(
266 "process.spawn: `{cmd}` not in --allow-proc {:?}",
267 self.policy.allow_proc
268 ).into())));
269 }
270 }
271
272 let mut command = std::process::Command::new(&cmd);
273 command.args(&str_args);
274 command.stdin(std::process::Stdio::piped());
275 command.stdout(std::process::Stdio::piped());
276 command.stderr(std::process::Stdio::piped());
277
278 if let Some(Value::Variant { name, args: vargs }) = opts.get("cwd") {
279 if name == "Some" {
280 if let Some(Value::Str(s)) = vargs.first() {
281 command.current_dir(s);
282 }
283 }
284 }
285 if let Some(Value::Map(env)) = opts.get("env") {
286 for (k, v) in env {
287 if let (lex_bytecode::MapKey::Str(ks), Value::Str(vs)) = (k, v) {
288 command.env(ks, vs);
289 }
290 }
291 }
292
293 let stdin_payload: Option<Vec<u8>> = match opts.get("stdin") {
294 Some(Value::Variant { name, args: vargs }) if name == "Some" => {
295 match vargs.first() {
296 Some(Value::Bytes(b)) => Some(b.clone()),
297 _ => None,
298 }
299 }
300 _ => None,
301 };
302
303 let mut child = match command.spawn() {
304 Ok(c) => c,
305 Err(e) => return Ok(err(Value::Str(format!("process.spawn `{cmd}`: {e}").into()))),
306 };
307
308 if let Some(payload) = stdin_payload {
309 if let Some(mut stdin) = child.stdin.take() {
310 use std::io::Write;
311 let _ = stdin.write_all(&payload);
312 }
314 }
315
316 let stdout = child.stdout.take().map(std::io::BufReader::new);
317 let stderr = child.stderr.take().map(std::io::BufReader::new);
318 let handle = next_process_handle();
319 process_registry().lock().unwrap().insert(handle, ProcessState {
320 child,
321 stdout,
322 stderr,
323 });
324 Ok(ok(Value::Int(handle as i64)))
325 }
326 "read_stdout_line" => Self::read_line_op(args, true),
327 "read_stderr_line" => Self::read_line_op(args, false),
328 "wait" => {
329 let h = expect_process_handle(args.first())?;
330 let arc = process_registry().lock().unwrap()
334 .touch_get(h)
335 .ok_or_else(|| "process.wait: closed or unknown ProcessHandle".to_string())?;
336 let status = {
337 let mut state = arc.lock().unwrap();
338 state.child.wait().map_err(|e| format!("process.wait: {e}"))?
339 };
340 process_registry().lock().unwrap().remove(h);
344 let mut rec = indexmap::IndexMap::new();
345 rec.insert("code".into(), Value::Int(status.code().unwrap_or(-1) as i64));
346 #[cfg(unix)]
347 {
348 use std::os::unix::process::ExitStatusExt;
349 rec.insert("signaled".into(), Value::Bool(status.signal().is_some()));
350 }
351 #[cfg(not(unix))]
352 {
353 rec.insert("signaled".into(), Value::Bool(false));
354 }
355 Ok(Value::record_dynamic(rec))
356 }
357 "kill" => {
358 let h = expect_process_handle(args.first())?;
359 let _signal = expect_str(args.get(1))?;
360 let arc = process_registry().lock().unwrap()
361 .touch_get(h)
362 .ok_or_else(|| "process.kill: closed or unknown ProcessHandle".to_string())?;
363 let mut state = arc.lock().unwrap();
364 match state.child.kill() {
367 Ok(_) => Ok(ok(Value::Unit)),
368 Err(e) => Ok(err(Value::Str(format!("process.kill: {e}").into()))),
369 }
370 }
371 "run" => {
372 let cmd = expect_str(args.first())?.to_string();
373 let raw_args = match args.get(1) {
374 Some(Value::List(items)) => items.clone(),
375 _ => return Err("process.run: args must be List[Str]".into()),
376 };
377 let str_args: Result<Vec<String>, String> = raw_args.iter().map(|v| match v {
378 Value::Str(s) => Ok(s.to_string()),
379 other => Err(format!("process.run: arg must be Str, got {other:?}")),
380 }).collect();
381 let str_args = str_args?;
382 if !self.policy.allow_proc.is_empty() {
383 let basename = std::path::Path::new(&cmd)
384 .file_name()
385 .and_then(|s| s.to_str())
386 .unwrap_or(&cmd);
387 if !self.policy.allow_proc.iter().any(|a| a == basename) {
388 return Ok(err(Value::Str(format!(
389 "process.run: `{cmd}` not in --allow-proc {:?}",
390 self.policy.allow_proc
391 ).into())));
392 }
393 }
394 match std::process::Command::new(&cmd).args(&str_args).output() {
395 Ok(o) => {
396 let mut rec = indexmap::IndexMap::new();
397 rec.insert("stdout".into(), Value::Str(
398 String::from_utf8_lossy(&o.stdout).into_owned().into()));
399 rec.insert("stderr".into(), Value::Str(
400 String::from_utf8_lossy(&o.stderr).into_owned().into()));
401 rec.insert("exit_code".into(), Value::Int(
402 o.status.code().unwrap_or(-1) as i64));
403 Ok(ok(Value::record_dynamic(rec)))
404 }
405 Err(e) => Ok(err(Value::Str(format!("process.run `{cmd}`: {e}").into()))),
406 }
407 }
408 other => Err(format!("unsupported process.{other}")),
409 }
410 }
411
412 fn read_line_op(args: Vec<Value>, is_stdout: bool) -> Result<Value, String> {
418 let h = expect_process_handle(args.first())?;
419 let arc = process_registry().lock().unwrap()
420 .touch_get(h)
421 .ok_or_else(|| format!(
422 "process.read_{}_line: closed or unknown ProcessHandle",
423 if is_stdout { "stdout" } else { "stderr" }))?;
424 let mut state = arc.lock().unwrap();
425 let reader_opt = if is_stdout {
426 state.stdout.as_mut().map(|r| -> &mut dyn std::io::BufRead { r })
427 } else {
428 state.stderr.as_mut().map(|r| -> &mut dyn std::io::BufRead { r })
429 };
430 let reader = match reader_opt {
431 Some(r) => r,
432 None => return Ok(none()),
433 };
434 let mut line = String::new();
435 match reader.read_line(&mut line) {
436 Ok(0) => Ok(none()),
437 Ok(_) => {
438 if line.ends_with('\n') { line.pop(); }
439 if line.ends_with('\r') { line.pop(); }
440 Ok(some(Value::Str(line.into())))
441 }
442 Err(e) => Err(format!("process.read_*_line: {e}")),
443 }
444 }
445
446 fn dispatch_fs(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
447 match op {
448 "exists" => {
449 let path = expect_str(args.first())?.to_string();
450 if let Err(e) = self.ensure_fs_walk_path(&path) {
451 return Ok(err(Value::Str(e.into())));
452 }
453 Ok(Value::Bool(std::path::Path::new(&path).exists()))
454 }
455 "is_file" => {
456 let path = expect_str(args.first())?.to_string();
457 if let Err(e) = self.ensure_fs_walk_path(&path) {
458 return Ok(err(Value::Str(e.into())));
459 }
460 Ok(Value::Bool(std::path::Path::new(&path).is_file()))
461 }
462 "is_dir" => {
463 let path = expect_str(args.first())?.to_string();
464 if let Err(e) = self.ensure_fs_walk_path(&path) {
465 return Ok(err(Value::Str(e.into())));
466 }
467 Ok(Value::Bool(std::path::Path::new(&path).is_dir()))
468 }
469 "stat" => {
470 let path = expect_str(args.first())?.to_string();
471 if let Err(e) = self.ensure_fs_walk_path(&path) {
472 return Ok(err(Value::Str(e.into())));
473 }
474 match std::fs::metadata(&path) {
475 Ok(md) => {
476 let mtime = md.modified()
477 .ok()
478 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
479 .map(|d| d.as_secs() as i64)
480 .unwrap_or(0);
481 let mut rec = indexmap::IndexMap::new();
482 rec.insert("size".into(), Value::Int(md.len() as i64));
483 rec.insert("mtime".into(), Value::Int(mtime));
484 rec.insert("is_dir".into(), Value::Bool(md.is_dir()));
485 rec.insert("is_file".into(), Value::Bool(md.is_file()));
486 Ok(ok(Value::record_dynamic(rec)))
487 }
488 Err(e) => Ok(err(Value::Str(format!("fs.stat `{path}`: {e}").into()))),
489 }
490 }
491 "list_dir" => {
492 let path = expect_str(args.first())?.to_string();
493 if let Err(e) = self.ensure_fs_walk_path(&path) {
494 return Ok(err(Value::Str(e.into())));
495 }
496 match std::fs::read_dir(&path) {
497 Ok(rd) => {
498 let mut entries: Vec<Value> = Vec::new();
499 for ent in rd {
500 match ent {
501 Ok(e) => {
502 let p = e.path();
503 entries.push(Value::Str(p.to_string_lossy().into_owned().into()));
504 }
505 Err(e) => return Ok(err(Value::Str(format!("fs.list_dir: {e}").into()))),
506 }
507 }
508 Ok(ok(Value::List(entries.into())))
509 }
510 Err(e) => Ok(err(Value::Str(format!("fs.list_dir `{path}`: {e}").into()))),
511 }
512 }
513 "walk" => {
514 let path = expect_str(args.first())?.to_string();
515 if let Err(e) = self.ensure_fs_walk_path(&path) {
516 return Ok(err(Value::Str(e.into())));
517 }
518 let mut paths: Vec<Value> = Vec::new();
519 for ent in walkdir::WalkDir::new(&path) {
520 match ent {
521 Ok(e) => paths.push(Value::Str(
522 e.path().to_string_lossy().into_owned().into())),
523 Err(e) => return Ok(err(Value::Str(format!("fs.walk: {e}").into()))),
524 }
525 }
526 Ok(ok(Value::List(paths.into())))
527 }
528 "glob" => {
529 let pattern = expect_str(args.first())?.to_string();
530 let entries = match glob::glob(&pattern) {
535 Ok(e) => e,
536 Err(e) => return Ok(err(Value::Str(format!("fs.glob: {e}").into()))),
537 };
538 let mut paths: Vec<Value> = Vec::new();
539 for ent in entries {
540 match ent {
541 Ok(p) => {
542 let s = p.to_string_lossy().into_owned();
543 if self.policy.allow_fs_read.is_empty()
544 || self.policy.allow_fs_read.iter().any(|root| p.starts_with(root))
545 {
546 paths.push(Value::Str(s.into()));
547 }
548 }
549 Err(e) => return Ok(err(Value::Str(format!("fs.glob: {e}").into()))),
550 }
551 }
552 Ok(ok(Value::List(paths.into())))
553 }
554 "mkdir_p" => {
555 let path = expect_str(args.first())?.to_string();
556 if let Err(e) = self.ensure_fs_write_path(&path) {
557 return Ok(err(Value::Str(e.into())));
558 }
559 match std::fs::create_dir_all(&path) {
560 Ok(_) => Ok(ok(Value::Unit)),
561 Err(e) => Ok(err(Value::Str(format!("fs.mkdir_p `{path}`: {e}").into()))),
562 }
563 }
564 "remove" => {
565 let path = expect_str(args.first())?.to_string();
566 if let Err(e) = self.ensure_fs_write_path(&path) {
567 return Ok(err(Value::Str(e.into())));
568 }
569 let p = std::path::Path::new(&path);
570 let result = if p.is_dir() {
571 std::fs::remove_dir_all(p)
572 } else {
573 std::fs::remove_file(p)
574 };
575 match result {
576 Ok(_) => Ok(ok(Value::Unit)),
577 Err(e) => Ok(err(Value::Str(format!("fs.remove `{path}`: {e}").into()))),
578 }
579 }
580 "copy" => {
581 let src = expect_str(args.first())?.to_string();
582 let dst = expect_str(args.get(1))?.to_string();
583 if let Err(e) = self.ensure_fs_walk_path(&src) {
584 return Ok(err(Value::Str(e.into())));
585 }
586 if let Err(e) = self.ensure_fs_write_path(&dst) {
587 return Ok(err(Value::Str(e.into())));
588 }
589 match std::fs::copy(&src, &dst) {
590 Ok(_) => Ok(ok(Value::Unit)),
591 Err(e) => Ok(err(Value::Str(format!("fs.copy {src} -> {dst}: {e}").into()))),
592 }
593 }
594 other => Err(format!("unsupported fs.{other}")),
595 }
596 }
597
598 fn ensure_fs_walk_path(&self, path: &str) -> Result<(), String> {
603 if self.policy.allow_fs_read.is_empty() {
604 return Ok(());
605 }
606 let p = std::path::Path::new(path);
607 if self.policy.allow_fs_read.iter().any(|a| p.starts_with(a)) {
608 Ok(())
609 } else {
610 Err(format!("fs path `{path}` outside --allow-fs-read"))
611 }
612 }
613
614 fn ensure_fs_write_path(&self, path: &str) -> Result<(), String> {
617 if self.policy.allow_fs_write.is_empty() {
618 return Ok(());
619 }
620 let p = std::path::Path::new(path);
621 if self.policy.allow_fs_write.iter().any(|a| p.starts_with(a)) {
622 Ok(())
623 } else {
624 Err(format!("fs path `{path}` outside --allow-fs-write"))
625 }
626 }
627
628 fn ensure_host_allowed(&self, url: &str) -> Result<(), String> {
632 if self.policy.allow_net_host.is_empty() { return Ok(()); }
633 let host = extract_host(url).unwrap_or("");
634 if self.policy.allow_net_host.iter().any(|h| host == h) {
635 Ok(())
636 } else {
637 Err(format!(
638 "net call to host `{host}` not in --allow-net-host {:?}",
639 self.policy.allow_net_host,
640 ))
641 }
642 }
643}
644
645fn extract_host(url: &str) -> Option<&str> {
646 let rest = url
647 .strip_prefix("http://")
648 .or_else(|| url.strip_prefix("https://"))
649 .or_else(|| url.strip_prefix("redis://"))
650 .or_else(|| url.strip_prefix("rediss://"))
651 .map(|r| r.split_once('@').map(|(_, after)| after).unwrap_or(r))?;
653 let host_port = match rest.find('/') {
654 Some(i) => &rest[..i],
655 None => rest,
656 };
657 Some(match host_port.rsplit_once(':') {
658 Some((h, _)) => h,
659 None => host_port,
660 })
661}
662
663impl EffectHandler for DefaultHandler {
664 fn enter_request_scope(&mut self) -> u64 {
668 let id = self.next_scope_id;
669 self.next_scope_id = self.next_scope_id.wrapping_add(1);
670 self.arena_stack.push((id, crate::arena::Arena::new()));
671 id
672 }
673
674 fn exit_request_scope(&mut self, scope_id: u64) {
680 if let Some(pos) = self.arena_stack.iter().position(|(id, _)| *id == scope_id) {
681 self.arena_stack.truncate(pos);
686 }
687 }
688
689 fn note_call_budget(&mut self, cost: u64) -> Result<(), String> {
694 let Some(ceiling) = self.budget_ceiling else { return Ok(()); };
697 loop {
703 let cur = self.budget_remaining.load(Ordering::SeqCst);
704 if cost > cur {
705 let used = ceiling.saturating_sub(cur);
706 return Err(format!(
707 "budget exceeded: requested {cost}, used so far {used}, ceiling {ceiling}"));
708 }
709 let next = cur - cost;
710 if self.budget_remaining.compare_exchange(cur, next,
713 Ordering::SeqCst, Ordering::SeqCst).is_ok() {
714 return Ok(());
715 }
716 }
717 }
718
719 fn dispatch(&mut self, kind: &str, op: &str, args: Vec<Value>) -> Result<Value, String> {
720 if is_pure_call(kind, op) {
724 return call_pure_builtin(kind, op, args);
725 }
726 if kind == "process" {
730 self.ensure_kind_allowed("proc")?;
731 return self.dispatch_process(op, args);
732 }
733 if kind == "log" {
734 let effect_kind = match op {
737 "debug" | "info" | "warn" | "error" => "log",
738 "set_level" | "set_format" => "io",
739 "set_sink" => {
740 self.ensure_kind_allowed("io")?;
741 self.ensure_kind_allowed("fs_write")?;
742 return self.dispatch_log(op, args);
743 }
744 other => return Err(format!("unsupported log.{other}")),
745 };
746 self.ensure_kind_allowed(effect_kind)?;
747 return self.dispatch_log(op, args);
748 }
749 if kind == "fs" {
750 let effect_kind = match op {
751 "exists" | "is_file" | "is_dir" | "stat"
752 | "list_dir" | "walk" | "glob" => "fs_walk",
753 "mkdir_p" | "remove" => "fs_write",
754 "copy" => {
755 self.ensure_kind_allowed("fs_walk")?;
756 self.ensure_kind_allowed("fs_write")?;
757 return self.dispatch_fs(op, args);
758 }
759 other => return Err(format!("unsupported fs.{other}")),
760 };
761 self.ensure_kind_allowed(effect_kind)?;
762 return self.dispatch_fs(op, args);
763 }
764 if kind == "datetime" && op == "now" {
771 self.ensure_kind_allowed("time")?;
772 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
774 if let Ok(secs) = s.trim().parse::<i64>() {
775 return Ok(Value::Int(secs.saturating_mul(1_000_000_000)));
776 }
777 }
778 let now = chrono::Utc::now();
779 let nanos = now.timestamp_nanos_opt().unwrap_or(i64::MAX);
780 return Ok(Value::Int(nanos));
781 }
782 if kind == "crypto" && op == "random" {
783 self.ensure_kind_allowed("random")?;
784 let n = expect_int(args.first())?;
785 if !(0..=1_048_576).contains(&n) {
786 return Err("crypto.random: n must be in 0..=1048576".into());
787 }
788 use rand::{rngs::SysRng, TryRng};
789 let mut buf = vec![0u8; n as usize];
790 SysRng.try_fill_bytes(&mut buf)
791 .map_err(|e| format!("crypto.random: OS RNG: {e}"))?;
792 return Ok(Value::Bytes(buf));
793 }
794 if kind == "crypto" && op == "random_str_hex" {
799 self.ensure_kind_allowed("random")?;
800 let n = expect_int(args.first())?;
801 if !(0..=1_048_576).contains(&n) {
802 return Err("crypto.random_str_hex: n must be in 0..=1048576".into());
803 }
804 use rand::{rngs::SysRng, TryRng};
805 let mut buf = vec![0u8; n as usize];
806 SysRng.try_fill_bytes(&mut buf)
807 .map_err(|e| format!("crypto.random_str_hex: OS RNG: {e}"))?;
808 return Ok(Value::Str(hex::encode(&buf).into()));
809 }
810 if kind == "agent" {
823 let effect_kind = match op {
824 "local_complete" => "llm_local",
825 "cloud_complete" => "llm_cloud",
826 "cloud_stream" => "llm_cloud",
827 "send_a2a" => "a2a",
828 "call_mcp" => "mcp",
829 other => return Err(format!("unsupported agent.{other}")),
830 };
831 self.ensure_kind_allowed(effect_kind)?;
832 return match op {
840 "call_mcp" => Ok(self.dispatch_call_mcp(args)),
841 "local_complete" => Ok(dispatch_llm_local(args)),
842 "cloud_complete" => Ok(dispatch_llm_cloud(args)),
843 "cloud_stream" => Ok(self.dispatch_cloud_stream(args)),
844 _ => Ok(ok(Value::Str(format!("<{effect_kind} stub>").into()))),
845 };
846 }
847 if kind == "stream" {
848 self.ensure_kind_allowed("stream")?;
855 return match op {
856 "next" => Ok(self.dispatch_stream_next(args)),
857 "collect" => Ok(self.dispatch_stream_collect(args)),
858 other => Err(format!("unsupported stream.{other}")),
859 };
860 }
861 if kind == "http" && matches!(op, "send" | "get" | "post" | "stream_lines") {
862 self.ensure_kind_allowed("net")?;
863 return match op {
864 "send" => {
865 let req = expect_record(args.first())?;
866 Ok(http_send_record(self, req))
867 }
868 "get" => {
869 let url = expect_str(args.first())?.to_string();
870 self.ensure_host_allowed(&url)?;
871 Ok(http_send_simple("GET", &url, None, "", None))
872 }
873 "post" => {
874 let url = expect_str(args.first())?.to_string();
875 let body = expect_bytes(args.get(1))?.clone();
876 let content_type = expect_str(args.get(2))?.to_string();
877 self.ensure_host_allowed(&url)?;
878 Ok(http_send_simple("POST", &url, Some(body), &content_type, None))
879 }
880 "stream_lines" => {
881 let url = expect_str(args.first())?.to_string();
882 let headers_val = args.get(1).cloned().unwrap_or(Value::Map(Default::default()));
883 let body = expect_str(args.get(2))?.to_string();
884 self.ensure_host_allowed(&url)?;
885 Ok(http_stream_lines_impl(self, &url, &headers_val, &body))
886 }
887 _ => unreachable!(),
888 };
889 }
890 if kind == "arrow" && op == "read_csv" {
896 self.ensure_kind_allowed("fs_read")?;
897 let path = expect_str(args.first())?.to_string();
898 let resolved = self.resolve_read_path(&path);
899 if !self.policy.allow_fs_read.is_empty()
900 && !self.policy.allow_fs_read.iter().any(|a| resolved.starts_with(a))
901 {
902 return Err(format!("arrow.read_csv: `{path}` outside --allow-fs-read"));
903 }
904 return match crate::arrow::read_csv_at(&resolved) {
905 Ok(v) => Ok(ok(v)),
906 Err(e) => Ok(err(Value::Str(e.into()))),
907 };
908 }
909 if kind == "arrow" && (op == "read_parquet" || op == "read_parquet_cols") {
913 self.ensure_kind_allowed("fs_read")?;
914 let path = expect_str(args.first())?.to_string();
915 let resolved = self.resolve_read_path(&path);
916 if !self.policy.allow_fs_read.is_empty()
917 && !self.policy.allow_fs_read.iter().any(|a| resolved.starts_with(a))
918 {
919 return Err(format!("arrow.{op}: `{path}` outside --allow-fs-read"));
920 }
921 let r = if op == "read_parquet" {
922 crate::arrow::read_parquet_at(&resolved)
923 } else {
924 let cols = match args.get(1) {
925 Some(Value::List(items)) => {
926 let mut out = Vec::with_capacity(items.len());
927 for v in items.iter() {
928 match v {
929 Value::Str(s) => out.push(s.to_string()),
930 other => return Err(format!(
931 "arrow.read_parquet_cols: column name not Str: {other:?}")),
932 }
933 }
934 out
935 }
936 other => return Err(format!(
937 "arrow.read_parquet_cols: expected List[Str], got {other:?}")),
938 };
939 crate::arrow::read_parquet_cols_at(&resolved, &cols)
940 };
941 return match r {
942 Ok(v) => Ok(ok(v)),
943 Err(e) => Ok(err(Value::Str(e.into()))),
944 };
945 }
946 if kind == "arrow" && (op == "write_parquet" || op == "write_csv") {
949 self.ensure_kind_allowed("fs_write")?;
950 let table_v = args.first().cloned().unwrap_or(Value::Unit);
951 let rb = match &table_v {
952 Value::ArrowTable(t) => Arc::clone(t),
953 other => return Err(format!("arrow.{op}: first arg must be arrow.Table, got {other:?}")),
954 };
955 let path = expect_str(args.get(1))?.to_string();
956 if let Err(e) = self.ensure_fs_write_path(&path) {
957 return Ok(err(Value::Str(format!("arrow.{op}: {e}").into())));
958 }
959 let r = if op == "write_parquet" {
960 crate::arrow::write_parquet_at(&rb, std::path::Path::new(&path))
961 } else {
962 crate::arrow::write_csv_at(&rb, std::path::Path::new(&path))
963 };
964 return match r {
965 Ok(_) => Ok(ok(Value::Unit)),
966 Err(e) => Ok(err(Value::Str(e.into()))),
967 };
968 }
969 if kind == "net" && op == "default_opts" {
974 return Ok(ServeOpts::lex_defaults().to_value());
975 }
976 if kind == "tls" {
984 return match op {
985 "from_pem_files" => {
986 self.ensure_kind_allowed("fs_read")?;
987 dispatch_tls_from_pem_files(self, args)
988 }
989 "self_signed" => dispatch_tls_self_signed(args),
990 other => Err(format!("unsupported tls.{other}")),
991 };
992 }
993 if kind == "redis" {
997 self.ensure_kind_allowed("net")?;
998 } else {
999 self.ensure_kind_allowed(kind)?;
1000 }
1001 match (kind, op) {
1002 ("io", "print") => {
1003 let line = expect_str(args.first())?;
1004 self.sink.print_line(line);
1005 Ok(Value::Unit)
1006 }
1007 ("io", "read") => {
1008 let path = expect_str(args.first())?.to_string();
1009 let resolved = self.resolve_read_path(&path);
1010 if !self.policy.allow_fs_read.is_empty()
1017 && !self.policy.allow_fs_read.iter().any(|a| resolved.starts_with(a))
1018 {
1019 return Err(format!("read of `{path}` outside --allow-fs-read"));
1020 }
1021 match std::fs::read_to_string(&resolved) {
1022 Ok(s) => Ok(ok(Value::Str(s.into()))),
1023 Err(e) => Ok(err(Value::Str(format!("{e}").into()))),
1024 }
1025 }
1026 ("io", "readline") => {
1027 use std::io::BufRead;
1028 let stdin = std::io::stdin();
1029 let mut line = String::new();
1030 match stdin.lock().read_line(&mut line) {
1031 Ok(0) => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1032 Ok(_) => {
1033 if line.ends_with('\n') { line.pop(); }
1034 if line.ends_with('\r') { line.pop(); }
1035 Ok(Value::Variant { name: "Some".into(), args: vec![Value::Str(line.into())] })
1036 }
1037 Err(_) => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1038 }
1039 }
1040 ("io", "argv") => {
1041 let list: Vec<Value> = self.program_args.iter()
1042 .map(|s| Value::Str(s.as_str().into()))
1043 .collect();
1044 Ok(Value::List(list.into()))
1045 }
1046 ("io", "write") => {
1047 let path = expect_str(args.first())?.to_string();
1048 let contents = expect_str(args.get(1))?.to_string();
1049 if !self.policy.allow_fs_write.is_empty() {
1053 let raw = std::env::current_dir()
1054 .map(|cwd| cwd.join(&path))
1055 .unwrap_or_else(|_| std::path::PathBuf::from(&path));
1056 let p = std::fs::canonicalize(&raw).unwrap_or_else(|_| {
1060 raw.parent()
1061 .and_then(|par| std::fs::canonicalize(par).ok())
1062 .map(|par| par.join(raw.file_name().unwrap_or_default()))
1063 .unwrap_or(raw)
1064 });
1065 let allowed = self.policy.allow_fs_write.iter().any(|a| {
1066 let ca = std::fs::canonicalize(a).unwrap_or_else(|_| a.clone());
1067 p.starts_with(&ca)
1068 });
1069 if !allowed {
1070 return Err(format!("write to `{path}` outside --allow-fs-write"));
1071 }
1072 }
1073 match std::fs::write(&path, contents) {
1074 Ok(_) => Ok(ok(Value::Unit)),
1075 Err(e) => Ok(err(Value::Str(format!("{e}").into()))),
1076 }
1077 }
1078 ("time", "now") => {
1079 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
1081 if let Ok(secs) = s.trim().parse::<i64>() {
1082 return Ok(Value::Int(secs));
1083 }
1084 }
1085 let secs = SystemTime::now().duration_since(UNIX_EPOCH)
1086 .map_err(|e| format!("time: {e}"))?.as_secs();
1087 Ok(Value::Int(secs as i64))
1088 }
1089 ("time", "now_ms") => {
1090 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
1095 if let Ok(secs) = s.trim().parse::<i64>() {
1096 return Ok(Value::Int(secs.saturating_mul(1000)));
1097 }
1098 }
1099 let ms = SystemTime::now().duration_since(UNIX_EPOCH)
1100 .map_err(|e| format!("time: {e}"))?.as_millis();
1101 Ok(Value::Int(ms as i64))
1102 }
1103 ("time", "now_str") => {
1104 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
1108 if let Ok(secs) = s.trim().parse::<i64>() {
1109 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0)
1110 .unwrap_or_else(chrono::Utc::now);
1111 return Ok(Value::Str(dt.to_rfc3339().into()));
1112 }
1113 }
1114 Ok(Value::Str(chrono::Utc::now().to_rfc3339().into()))
1115 }
1116 ("time", "mono_ns") => {
1117 static MONO_START: OnceLock<std::time::Instant> = OnceLock::new();
1125 let start = MONO_START.get_or_init(std::time::Instant::now);
1126 let dur = std::time::Instant::now().duration_since(*start);
1127 Ok(Value::Int(dur.as_nanos() as i64))
1128 }
1129 ("time", "sleep_ms") => {
1130 let n = expect_int(args.first())?;
1138 if n > 0 {
1139 let ms = (n as u64).min(60_000);
1140 std::thread::sleep(std::time::Duration::from_millis(ms));
1141 }
1142 Ok(Value::Unit)
1143 }
1144 ("time", "sleep") => {
1145 let nanos = expect_int(args.first())?;
1151 if nanos > 0 {
1152 let bounded_nanos = (nanos as u64).min(60_000 * 1_000_000);
1153 std::thread::sleep(std::time::Duration::from_nanos(bounded_nanos));
1154 }
1155 Ok(Value::Unit)
1156 }
1157 ("rand", "int_in") => {
1158 let lo = expect_int(args.first())?;
1160 let hi = expect_int(args.get(1))?;
1161 Ok(Value::Int((lo + hi) / 2))
1162 }
1163 ("env", "get") => {
1168 let name = expect_str(args.first())?;
1169 Ok(match std::env::var(name) {
1170 Ok(v) => Value::Variant {
1171 name: "Some".into(),
1172 args: vec![Value::Str(v.into())],
1173 },
1174 Err(_) => Value::Variant { name: "None".into(), args: Vec::new() },
1175 })
1176 }
1177 ("budget", _) => {
1178 Ok(Value::Unit)
1181 }
1182 ("net", "get") => {
1183 let url = expect_str(args.first())?.to_string();
1184 self.ensure_host_allowed(&url)?;
1185 Ok(http_request("GET", &url, None))
1186 }
1187 ("net", "post") => {
1188 let url = expect_str(args.first())?.to_string();
1189 let body = expect_str(args.get(1))?.to_string();
1190 self.ensure_host_allowed(&url)?;
1191 Ok(http_request("POST", &url, Some(&body)))
1192 }
1193 ("net", "serve") => {
1194 let port = match args.first() {
1195 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1196 _ => return Err("net.serve(port, handler): port must be Int 0..=65535".into()),
1197 };
1198 let handler_name = expect_str(args.get(1))?.to_string();
1199 let program = self.program.clone()
1200 .ok_or_else(|| "net.serve requires a Program reference; use DefaultHandler::with_program".to_string())?;
1201 let policy = self.policy.clone();
1202 serve_http(port, handler_name, program, policy, None, ServeOpts::from_env())
1203 }
1204 ("net", "serve_fn") => {
1205 let port = match args.first() {
1206 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1207 _ => return Err("net.serve_fn(port, handler): port must be Int 0..=65535".into()),
1208 };
1209 let closure = match args.into_iter().nth(1) {
1210 Some(c @ Value::Closure { .. }) => c,
1211 _ => return Err("net.serve_fn(port, handler): handler must be a closure".into()),
1212 };
1213 let program = self.program.clone()
1214 .ok_or_else(|| "net.serve_fn requires a Program reference; use DefaultHandler::with_program".to_string())?;
1215 let policy = self.policy.clone();
1216 serve_http_fn(port, closure, program, policy, ServeOpts::from_env())
1217 }
1218 ("net", "serve_routed") => {
1219 let port = match args.first() {
1220 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1221 _ => return Err("net.serve_routed(port, routes, fallback): port must be Int 0..=65535".into()),
1222 };
1223 let routes_val = args.get(1).cloned()
1224 .ok_or_else(|| "net.serve_routed(port, routes, fallback): missing routes".to_string())?;
1225 let fallback = match args.into_iter().nth(2) {
1226 Some(c @ Value::Closure { .. }) => c,
1227 _ => return Err("net.serve_routed(port, routes, fallback): fallback must be a closure".into()),
1228 };
1229 let routes = decode_routes_arg(routes_val)?;
1230 let program = self.program.clone()
1231 .ok_or_else(|| "net.serve_routed requires a Program reference; use DefaultHandler::with_program".to_string())?;
1232 let policy = self.policy.clone();
1233 serve_http_routed(port, routes, fallback, program, policy, ServeOpts::from_env())
1234 }
1235 ("net", "serve_with") => {
1236 let port = match args.first() {
1238 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1239 _ => return Err("net.serve_with(port, handler, opts): port must be Int 0..=65535".into()),
1240 };
1241 let handler_name = expect_str(args.get(1))?.to_string();
1242 let opts = decode_serve_opts(args.get(2)
1243 .ok_or_else(|| "net.serve_with(port, handler, opts): missing opts".to_string())?)?;
1244 let program = self.program.clone()
1245 .ok_or_else(|| "net.serve_with requires a Program reference; use DefaultHandler::with_program".to_string())?;
1246 let policy = self.policy.clone();
1247 serve_http(port, handler_name, program, policy, None, opts)
1248 }
1249 ("net", "serve_fn_with") => {
1250 let port = match args.first() {
1252 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1253 _ => return Err("net.serve_fn_with(port, handler, opts): port must be Int 0..=65535".into()),
1254 };
1255 let opts = decode_serve_opts(args.get(2)
1256 .ok_or_else(|| "net.serve_fn_with(port, handler, opts): missing opts".to_string())?)?;
1257 let closure = match args.into_iter().nth(1) {
1258 Some(c @ Value::Closure { .. }) => c,
1259 _ => return Err("net.serve_fn_with(port, handler, opts): handler must be a closure".into()),
1260 };
1261 let program = self.program.clone()
1262 .ok_or_else(|| "net.serve_fn_with requires a Program reference; use DefaultHandler::with_program".to_string())?;
1263 let policy = self.policy.clone();
1264 serve_http_fn(port, closure, program, policy, opts)
1265 }
1266 ("net", "serve_routed_with") => {
1267 let port = match args.first() {
1269 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1270 _ => return Err("net.serve_routed_with(port, routes, fallback, opts): port must be Int 0..=65535".into()),
1271 };
1272 let routes_val = args.get(1).cloned()
1273 .ok_or_else(|| "net.serve_routed_with(port, routes, fallback, opts): missing routes".to_string())?;
1274 let opts = decode_serve_opts(args.get(3)
1275 .ok_or_else(|| "net.serve_routed_with(port, routes, fallback, opts): missing opts".to_string())?)?;
1276 let fallback = match args.into_iter().nth(2) {
1277 Some(c @ Value::Closure { .. }) => c,
1278 _ => return Err("net.serve_routed_with(port, routes, fallback, opts): fallback must be a closure".into()),
1279 };
1280 let routes = decode_routes_arg(routes_val)?;
1281 let program = self.program.clone()
1282 .ok_or_else(|| "net.serve_routed_with requires a Program reference; use DefaultHandler::with_program".to_string())?;
1283 let policy = self.policy.clone();
1284 serve_http_routed(port, routes, fallback, program, policy, opts)
1285 }
1286 ("net", "serve_quic") => self.dispatch_serve_quic_named(args),
1287 ("net", "serve_quic_fn") => self.dispatch_serve_quic_fn(args),
1288 ("net", "serve_quic_routed") => self.dispatch_serve_quic_routed(args),
1289 ("net", "serve_tls") => {
1290 let port = match args.first() {
1291 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1292 _ => return Err("net.serve_tls(port, cert, key, handler): port must be Int 0..=65535".into()),
1293 };
1294 let cert_path = expect_str(args.get(1))?.to_string();
1295 let key_path = expect_str(args.get(2))?.to_string();
1296 let handler_name = expect_str(args.get(3))?.to_string();
1297 let program = self.program.clone()
1298 .ok_or_else(|| "net.serve_tls requires a Program reference".to_string())?;
1299 let policy = self.policy.clone();
1300 let cert = std::fs::read(&cert_path)
1301 .map_err(|e| format!("net.serve_tls: read cert {cert_path}: {e}"))?;
1302 let key = std::fs::read(&key_path)
1303 .map_err(|e| format!("net.serve_tls: read key {key_path}: {e}"))?;
1304 serve_http(port, handler_name, program, policy, Some(TlsConfig { cert, key }), ServeOpts::from_env())
1305 }
1306 ("net", "serve_ws") => {
1307 let port = match args.first() {
1308 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1309 _ => return Err("net.serve_ws(port, on_message): port must be Int 0..=65535".into()),
1310 };
1311 let handler_name = expect_str(args.get(1))?.to_string();
1312 let program = self.program.clone()
1313 .ok_or_else(|| "net.serve_ws requires a Program reference".to_string())?;
1314 let policy = self.policy.clone();
1315 let registry = Arc::new(crate::ws::ChatRegistry::default());
1316 crate::ws::serve_ws(port, handler_name, program, policy, registry)
1317 }
1318 ("net", "serve_ws_fn") => {
1319 let port = match args.first() {
1320 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1321 _ => return Err("net.serve_ws_fn(port, subprotocol, handler): port must be Int 0..=65535".into()),
1322 };
1323 let subprotocol = expect_str(args.get(1))?.to_string();
1324 let closure = match args.into_iter().nth(2) {
1325 Some(c @ Value::Closure { .. }) => c,
1326 _ => return Err("net.serve_ws_fn(port, subprotocol, handler): handler must be a closure".into()),
1327 };
1328 let program = self.program.clone()
1329 .ok_or_else(|| "net.serve_ws_fn requires a Program reference; use DefaultHandler::with_program".to_string())?;
1330 let policy = self.policy.clone();
1331 let registry = Arc::new(crate::ws::ChatRegistry::default());
1332 crate::ws::serve_ws_fn(port, subprotocol, closure, program, policy, registry)
1333 }
1334 ("net", "serve_ws_fn_auth") => {
1335 let port = match args.first() {
1337 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1338 _ => return Err("net.serve_ws_fn_auth(port, subprotocol, auth, on_message): port must be Int 0..=65535".into()),
1339 };
1340 let subprotocol = expect_str(args.get(1))?.to_string();
1341 let mut it = args.into_iter().skip(2);
1342 let auth_closure = match it.next() {
1343 Some(c @ Value::Closure { .. }) => c,
1344 _ => return Err("net.serve_ws_fn_auth(port, subprotocol, auth, on_message): auth must be a closure".into()),
1345 };
1346 let handler_closure = match it.next() {
1347 Some(c @ Value::Closure { .. }) => c,
1348 _ => return Err("net.serve_ws_fn_auth(port, subprotocol, auth, on_message): on_message must be a closure".into()),
1349 };
1350 let program = self.program.clone()
1351 .ok_or_else(|| "net.serve_ws_fn_auth requires a Program reference; use DefaultHandler::with_program".to_string())?;
1352 let policy = self.policy.clone();
1353 let registry = Arc::new(crate::ws::ChatRegistry::default());
1354 crate::ws::serve_ws_fn_auth(
1355 port, subprotocol, auth_closure, handler_closure,
1356 program, policy, registry,
1357 )
1358 }
1359 ("net", "serve_ws_fn_actor") => {
1360 let port = match args.first() {
1362 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1363 _ => return Err("net.serve_ws_fn_actor(port, subprotocol, name_of, on_message): port must be Int 0..=65535".into()),
1364 };
1365 let subprotocol = expect_str(args.get(1))?.to_string();
1366 let mut it = args.into_iter().skip(2);
1367 let name_of_closure = match it.next() {
1368 Some(c @ Value::Closure { .. }) => c,
1369 _ => return Err("net.serve_ws_fn_actor(port, subprotocol, name_of, on_message): name_of must be a closure".into()),
1370 };
1371 let on_message_closure = match it.next() {
1372 Some(c @ Value::Closure { .. }) => c,
1373 _ => return Err("net.serve_ws_fn_actor(port, subprotocol, name_of, on_message): on_message must be a closure".into()),
1374 };
1375 let program = self.program.clone()
1376 .ok_or_else(|| "net.serve_ws_fn_actor requires a Program reference; use DefaultHandler::with_program".to_string())?;
1377 let policy = self.policy.clone();
1378 let registry = Arc::new(crate::ws::ChatRegistry::default());
1379 crate::ws::serve_ws_fn_actor(
1380 port, subprotocol, name_of_closure, on_message_closure,
1381 program, policy, registry,
1382 )
1383 }
1384 ("net", "dial_ws") => {
1385 let url = expect_str(args.first())?.to_string();
1387 let subprotocol = expect_str(args.get(1))?.to_string();
1388 let on_open = match args.get(2).cloned() {
1389 Some(c @ Value::Closure { .. }) => c,
1390 _ => return Err(
1391 "net.dial_ws(url, subprotocol, on_open, on_message): on_open must be a closure".into(),
1392 ),
1393 };
1394 let on_message = match args.into_iter().nth(3) {
1395 Some(c @ Value::Closure { .. }) => c,
1396 _ => return Err(
1397 "net.dial_ws(url, subprotocol, on_open, on_message): on_message must be a closure".into(),
1398 ),
1399 };
1400 let program = self.program.clone().ok_or_else(|| {
1401 "net.dial_ws requires a Program reference; use DefaultHandler::with_program".to_string()
1402 })?;
1403 let policy = self.policy.clone();
1404 crate::ws::dial_ws(url, subprotocol, on_open, on_message, program, policy)
1405 }
1406 ("net", "dial_ws_actor") => {
1407 let url = expect_str(args.first())?.to_string();
1409 let subprotocol = expect_str(args.get(1))?.to_string();
1410 let name = expect_str(args.get(2))?.to_string();
1411 let on_open = match args.get(3).cloned() {
1412 Some(c @ Value::Closure { .. }) => c,
1413 _ => return Err(
1414 "net.dial_ws_actor(url, subprotocol, name, on_open, on_message): on_open must be a closure".into(),
1415 ),
1416 };
1417 let on_message = match args.into_iter().nth(4) {
1418 Some(c @ Value::Closure { .. }) => c,
1419 _ => return Err(
1420 "net.dial_ws_actor(url, subprotocol, name, on_open, on_message): on_message must be a closure".into(),
1421 ),
1422 };
1423 let program = self.program.clone().ok_or_else(|| {
1424 "net.dial_ws_actor requires a Program reference; use DefaultHandler::with_program".to_string()
1425 })?;
1426 let policy = self.policy.clone();
1427 crate::ws::dial_ws_actor(url, subprotocol, name, on_open, on_message, program, policy)
1428 }
1429 ("chat", "broadcast") => {
1430 let registry = self.chat_registry.as_ref()
1431 .ok_or_else(|| "chat.broadcast called outside a net.serve_ws handler".to_string())?;
1432 let room = expect_str(args.first())?;
1433 let body = expect_str(args.get(1))?;
1434 crate::ws::chat_broadcast(registry, room, body);
1435 Ok(Value::Unit)
1436 }
1437 ("chat", "send") => {
1438 let registry = self.chat_registry.as_ref()
1439 .ok_or_else(|| "chat.send called outside a net.serve_ws handler".to_string())?;
1440 let conn_id = match args.first() {
1441 Some(Value::Int(n)) if *n >= 0 => *n as u64,
1442 _ => return Err("chat.send: conn_id must be non-negative Int".into()),
1443 };
1444 let body = expect_str(args.get(1))?;
1445 Ok(Value::Bool(crate::ws::chat_send(registry, conn_id, body)))
1446 }
1447 ("kv", "open") => {
1448 let path = expect_str(args.first())?.to_string();
1449 if !self.policy.allow_fs_write.is_empty() {
1453 let p = std::path::Path::new(&path);
1454 if !self.policy.allow_fs_write.iter().any(|a| p.starts_with(a)) {
1455 return Ok(err(Value::Str(format!(
1456 "kv.open: `{path}` outside --allow-fs-write").into())));
1457 }
1458 }
1459 match sled::open(&path) {
1460 Ok(db) => {
1461 let handle = next_kv_handle();
1462 kv_registry().lock().unwrap().insert(handle, db);
1463 Ok(ok(Value::Int(handle as i64)))
1464 }
1465 Err(e) => Ok(err(Value::Str(format!("kv.open: {e}").into()))),
1466 }
1467 }
1468 ("kv", "close") => {
1469 let h = expect_kv_handle(args.first())?;
1470 kv_registry().lock().unwrap().remove(h);
1471 Ok(Value::Unit)
1472 }
1473 ("kv", "get") => {
1474 let h = expect_kv_handle(args.first())?;
1475 let key = expect_str(args.get(1))?;
1476 let mut reg = kv_registry().lock().unwrap();
1477 let db = reg.touch_get(h).ok_or_else(|| "kv.get: closed or unknown Kv handle".to_string())?;
1478 match db.get(key.as_bytes()) {
1479 Ok(Some(ivec)) => Ok(some(Value::Bytes(ivec.to_vec()))),
1480 Ok(None) => Ok(none()),
1481 Err(e) => Err(format!("kv.get: {e}")),
1482 }
1483 }
1484 ("kv", "put") => {
1485 let h = expect_kv_handle(args.first())?;
1486 let key = expect_str(args.get(1))?.to_string();
1487 let val = expect_bytes(args.get(2))?.clone();
1488 let mut reg = kv_registry().lock().unwrap();
1489 let db = reg.touch_get(h).ok_or_else(|| "kv.put: closed or unknown Kv handle".to_string())?;
1490 match db.insert(key.as_bytes(), val) {
1491 Ok(_) => Ok(ok(Value::Unit)),
1492 Err(e) => Ok(err(Value::Str(format!("kv.put: {e}").into()))),
1493 }
1494 }
1495 ("kv", "delete") => {
1496 let h = expect_kv_handle(args.first())?;
1497 let key = expect_str(args.get(1))?;
1498 let mut reg = kv_registry().lock().unwrap();
1499 let db = reg.touch_get(h).ok_or_else(|| "kv.delete: closed or unknown Kv handle".to_string())?;
1500 match db.remove(key.as_bytes()) {
1501 Ok(_) => Ok(ok(Value::Unit)),
1502 Err(e) => Ok(err(Value::Str(format!("kv.delete: {e}").into()))),
1503 }
1504 }
1505 ("kv", "contains") => {
1506 let h = expect_kv_handle(args.first())?;
1507 let key = expect_str(args.get(1))?;
1508 let mut reg = kv_registry().lock().unwrap();
1509 let db = reg.touch_get(h).ok_or_else(|| "kv.contains: closed or unknown Kv handle".to_string())?;
1510 match db.contains_key(key.as_bytes()) {
1511 Ok(present) => Ok(Value::Bool(present)),
1512 Err(e) => Err(format!("kv.contains: {e}")),
1513 }
1514 }
1515 ("kv", "list_prefix") => {
1516 let h = expect_kv_handle(args.first())?;
1517 let prefix = expect_str(args.get(1))?;
1518 let mut reg = kv_registry().lock().unwrap();
1519 let db = reg.touch_get(h).ok_or_else(|| "kv.list_prefix: closed or unknown Kv handle".to_string())?;
1520 let mut keys: Vec<Value> = Vec::new();
1521 for kv in db.scan_prefix(prefix.as_bytes()) {
1522 let (k, _) = kv.map_err(|e| format!("kv.list_prefix: {e}"))?;
1523 let s = String::from_utf8_lossy(&k).to_string();
1524 keys.push(Value::Str(s.into()));
1525 }
1526 Ok(Value::List(keys.into()))
1527 }
1528 ("sql", "open") => {
1529 let path = expect_str(args.first())?.to_string();
1530 if path.starts_with("postgres://") || path.starts_with("postgresql://") {
1531 match postgres::Client::connect(&path, postgres::NoTls) {
1533 Ok(client) => {
1534 let handle = next_sql_handle();
1535 sql_registry().lock().unwrap().insert(handle, SqlConn::Postgres(client));
1536 Ok(ok(Value::Int(handle as i64)))
1537 }
1538 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.open"))),
1539 }
1540 } else {
1541 if path != ":memory:" && !self.policy.allow_fs_write.is_empty() {
1544 let p = std::path::Path::new(&path);
1545 if !self.policy.allow_fs_write.iter().any(|a| p.starts_with(a)) {
1546 return Ok(err(sql_error(
1547 format!("sql.open: `{path}` outside --allow-fs-write"),
1548 None, None,
1549 )));
1550 }
1551 }
1552 match rusqlite::Connection::open(&path) {
1553 Ok(conn) => {
1554 let handle = next_sql_handle();
1555 sql_registry().lock().unwrap().insert(handle, SqlConn::Sqlite(conn));
1556 Ok(ok(Value::Int(handle as i64)))
1557 }
1558 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.open"))),
1559 }
1560 }
1561 }
1562 ("sql", "close") => {
1563 let h = expect_sql_handle(args.first())?;
1564 sql_registry().lock().unwrap().remove(h);
1565 Ok(Value::Unit)
1566 }
1567 ("sql", "exec") => {
1568 let h = expect_sql_handle(args.first())?;
1569 let stmt = expect_str(args.get(1))?.to_string();
1570 let params = expect_sql_params(args.get(2))?;
1571 let arc = sql_registry().lock().unwrap()
1572 .touch_get(h)
1573 .ok_or_else(|| "sql.exec: closed or unknown Db handle".to_string())?;
1574 let mut conn = arc.lock().unwrap();
1575 match &mut *conn {
1576 SqlConn::Sqlite(c) => {
1577 let bound = sqlite_params(¶ms);
1578 let bind: Vec<&dyn rusqlite::ToSql> =
1579 bound.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
1580 match c.execute(&stmt, rusqlite::params_from_iter(bind.iter())) {
1581 Ok(n) => Ok(ok(Value::Int(n as i64))),
1582 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.exec"))),
1583 }
1584 }
1585 SqlConn::Postgres(c) => {
1586 let pg = pg_param_refs(¶ms);
1587 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
1588 pg.iter().map(|b| b.as_ref()).collect();
1589 match c.execute(stmt.as_str(), &refs) {
1590 Ok(n) => Ok(ok(Value::Int(n as i64))),
1591 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.exec"))),
1592 }
1593 }
1594 }
1595 }
1596 ("sql", "query") => {
1597 let h = expect_sql_handle(args.first())?;
1598 let stmt_str = expect_str(args.get(1))?.to_string();
1599 let params = expect_sql_params(args.get(2))?;
1600 let arc = sql_registry().lock().unwrap()
1601 .touch_get(h)
1602 .ok_or_else(|| "sql.query: closed or unknown Db handle".to_string())?;
1603 let mut conn = arc.lock().unwrap();
1604 Ok(match &mut *conn {
1605 SqlConn::Sqlite(c) => sql_run_query_sqlite(c, &stmt_str, ¶ms),
1606 SqlConn::Postgres(c) => sql_run_query_pg(c, &stmt_str, ¶ms),
1607 })
1608 }
1609 ("sql", "query_iter") => {
1615 let h = expect_sql_handle(args.first())?;
1616 let stmt_str = expect_str(args.get(1))?.to_string();
1617 let params = expect_sql_params(args.get(2))?;
1618 let arc = sql_registry().lock().unwrap()
1619 .touch_get(h)
1620 .ok_or_else(|| "sql.query_iter: closed or unknown Db handle".to_string())?;
1621
1622 let (sender, receiver) = std::sync::mpsc::sync_channel::<Result<Value, String>>(
1626 CURSOR_CHANNEL_CAPACITY,
1627 );
1628 let cursor_h = next_cursor_handle();
1629 cursor_registry().lock().unwrap().insert(cursor_h, receiver);
1630
1631 let arc_for_thread = Arc::clone(&arc);
1632 let is_sqlite = matches!(*arc.lock().unwrap(), SqlConn::Sqlite(_));
1638 std::thread::spawn(move || {
1639 if is_sqlite {
1640 sqlite_cursor_producer(arc_for_thread, stmt_str, params, sender);
1641 } else {
1642 pg_cursor_producer(arc_for_thread, stmt_str, params, sender);
1643 }
1644 });
1645
1646 Ok(ok(Value::Variant {
1647 name: "__IterCursor".into(),
1648 args: vec![Value::Int(cursor_h as i64)],
1649 }))
1650 }
1651 ("sql", "cursor_next") => {
1657 let h = match args.first() {
1658 Some(Value::Int(n)) if *n >= 0 => *n as u64,
1659 _ => return Err("sql.cursor_next: expected cursor handle (Int)".into()),
1660 };
1661 let rx_arc = match cursor_registry().lock().unwrap().touch_get(h) {
1662 Some(a) => a,
1663 None => return Ok(Value::Variant { name: "None".into(), args: vec![] }),
1664 };
1665 let recv_result = {
1670 let rx = match rx_arc.lock() {
1671 Ok(g) => g,
1672 Err(p) => p.into_inner(),
1673 };
1674 rx.recv()
1675 };
1676 match recv_result {
1677 Ok(Ok(row)) => Ok(Value::Variant {
1678 name: "Some".into(),
1679 args: vec![row],
1680 }),
1681 Ok(Err(_)) | Err(_) => {
1682 cursor_registry().lock().unwrap().remove(h);
1686 Ok(Value::Variant { name: "None".into(), args: vec![] })
1687 }
1688 }
1689 }
1690 ("sql", "begin") => {
1695 let h = expect_sql_handle(args.first())?;
1696 let arc = sql_registry().lock().unwrap()
1697 .touch_get(h)
1698 .ok_or_else(|| "sql.begin: closed or unknown Db handle".to_string())?;
1699 let mut conn = arc.lock().unwrap();
1700 match &mut *conn {
1701 SqlConn::Sqlite(c) => match c.execute_batch("BEGIN") {
1702 Ok(()) => Ok(ok(Value::Int(h as i64))),
1703 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.begin"))),
1704 },
1705 SqlConn::Postgres(c) => match c.batch_execute("BEGIN") {
1706 Ok(()) => Ok(ok(Value::Int(h as i64))),
1707 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.begin"))),
1708 },
1709 }
1710 }
1711 ("sql", "commit") => {
1712 let h = expect_sql_handle(args.first())?;
1713 let arc = sql_registry().lock().unwrap()
1714 .touch_get(h)
1715 .ok_or_else(|| "sql.commit: closed or unknown SqlTx handle".to_string())?;
1716 let mut conn = arc.lock().unwrap();
1717 match &mut *conn {
1718 SqlConn::Sqlite(c) => match c.execute_batch("COMMIT") {
1719 Ok(()) => Ok(ok(Value::Unit)),
1720 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.commit"))),
1721 },
1722 SqlConn::Postgres(c) => match c.batch_execute("COMMIT") {
1723 Ok(()) => Ok(ok(Value::Unit)),
1724 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.commit"))),
1725 },
1726 }
1727 }
1728 ("sql", "rollback") => {
1729 let h = expect_sql_handle(args.first())?;
1730 let arc = sql_registry().lock().unwrap()
1731 .touch_get(h)
1732 .ok_or_else(|| "sql.rollback: closed or unknown SqlTx handle".to_string())?;
1733 let mut conn = arc.lock().unwrap();
1734 match &mut *conn {
1735 SqlConn::Sqlite(c) => match c.execute_batch("ROLLBACK") {
1736 Ok(()) => Ok(ok(Value::Unit)),
1737 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.rollback"))),
1738 },
1739 SqlConn::Postgres(c) => match c.batch_execute("ROLLBACK") {
1740 Ok(()) => Ok(ok(Value::Unit)),
1741 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.rollback"))),
1742 },
1743 }
1744 }
1745 ("sql", "exec_tx") => {
1746 let h = expect_sql_handle(args.first())?;
1747 let stmt = expect_str(args.get(1))?.to_string();
1748 let params = expect_sql_params(args.get(2))?;
1749 let arc = sql_registry().lock().unwrap()
1750 .touch_get(h)
1751 .ok_or_else(|| "sql.exec_tx: closed or unknown SqlTx handle".to_string())?;
1752 let mut conn = arc.lock().unwrap();
1753 match &mut *conn {
1754 SqlConn::Sqlite(c) => {
1755 let bound = sqlite_params(¶ms);
1756 let bind: Vec<&dyn rusqlite::ToSql> =
1757 bound.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
1758 match c.execute(&stmt, rusqlite::params_from_iter(bind.iter())) {
1759 Ok(n) => Ok(ok(Value::Int(n as i64))),
1760 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.exec_tx"))),
1761 }
1762 }
1763 SqlConn::Postgres(c) => {
1764 let pg = pg_param_refs(¶ms);
1765 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
1766 pg.iter().map(|b| b.as_ref()).collect();
1767 match c.execute(stmt.as_str(), &refs) {
1768 Ok(n) => Ok(ok(Value::Int(n as i64))),
1769 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.exec_tx"))),
1770 }
1771 }
1772 }
1773 }
1774 ("sql", "query_tx") => {
1775 let h = expect_sql_handle(args.first())?;
1776 let stmt_str = expect_str(args.get(1))?.to_string();
1777 let params = expect_sql_params(args.get(2))?;
1778 let arc = sql_registry().lock().unwrap()
1779 .touch_get(h)
1780 .ok_or_else(|| "sql.query_tx: closed or unknown SqlTx handle".to_string())?;
1781 let mut conn = arc.lock().unwrap();
1782 Ok(match &mut *conn {
1783 SqlConn::Sqlite(c) => sql_run_query_sqlite(c, &stmt_str, ¶ms),
1784 SqlConn::Postgres(c) => sql_run_query_pg(c, &stmt_str, ¶ms),
1785 })
1786 }
1787 ("sql", "get_str") => Ok(sql_get_col(&args, |v| match v {
1788 Value::Str(s) => Some(Value::Str(s.clone())),
1789 Value::Int(n) => Some(Value::Str(n.to_string().into())),
1790 _ => None,
1791 })?),
1792 ("sql", "get_int") => Ok(sql_get_col(&args, |v| match v {
1793 Value::Int(n) => Some(Value::Int(*n)),
1794 Value::Float(f) => Some(Value::Int(*f as i64)),
1795 _ => None,
1796 })?),
1797 ("sql", "get_float") => Ok(sql_get_col(&args, |v| match v {
1798 Value::Float(f) => Some(Value::Float(*f)),
1799 Value::Int(n) => Some(Value::Float(*n as f64)),
1800 _ => None,
1801 })?),
1802 ("sql", "get_bool") => Ok(sql_get_col(&args, |v| match v {
1803 Value::Bool(b) => Some(Value::Bool(*b)),
1804 Value::Int(n) => Some(Value::Bool(*n != 0)),
1805 _ => None,
1806 })?),
1807
1808 ("redis", "connect") => {
1817 let url = expect_str(args.first())?.to_string();
1818 self.ensure_host_allowed(&url)?;
1819 match redis::Client::open(url.as_str()) {
1820 Ok(client) => match client.get_connection() {
1821 Ok(conn) => {
1822 let handle = next_redis_handle();
1823 redis_registry().lock().unwrap().insert(handle, RedisEntry { url, conn });
1824 Ok(ok(Value::Int(handle as i64)))
1825 }
1826 Err(e) => Ok(err(Value::Str(format!("redis.connect: {e}").into()))),
1827 },
1828 Err(e) => Ok(err(Value::Str(format!("redis.connect: {e}").into()))),
1829 }
1830 }
1831 ("redis", "close") => {
1832 let h = expect_redis_handle(args.first())?;
1833 redis_registry().lock().unwrap().remove(h);
1834 Ok(Value::Unit)
1835 }
1836 ("redis", "get") => {
1837 let h = expect_redis_handle(args.first())?;
1838 let key = expect_str(args.get(1))?.to_string();
1839 let mut reg = redis_registry().lock().unwrap();
1840 let entry = reg.touch_get_mut(h)
1841 .ok_or_else(|| "redis.get: closed or unknown ConnRedis handle".to_string())?;
1842 use redis::Commands;
1843 match entry.conn.get::<_, Option<String>>(&key) {
1844 Ok(Some(v)) => Ok(some(Value::Str(v.into()))),
1845 Ok(None) => Ok(none()),
1846 Err(e) => Err(format!("redis.get: {e}")),
1847 }
1848 }
1849 ("redis", "set") => {
1850 let h = expect_redis_handle(args.first())?;
1851 let key = expect_str(args.get(1))?.to_string();
1852 let val = expect_str(args.get(2))?.to_string();
1853 let mut reg = redis_registry().lock().unwrap();
1854 let entry = reg.touch_get_mut(h)
1855 .ok_or_else(|| "redis.set: closed or unknown ConnRedis handle".to_string())?;
1856 use redis::Commands;
1857 entry.conn.set::<_, _, ()>(&key, &val)
1858 .map_err(|e| format!("redis.set: {e}"))?;
1859 Ok(Value::Unit)
1860 }
1861 ("redis", "set_ex") => {
1862 let h = expect_redis_handle(args.first())?;
1863 let key = expect_str(args.get(1))?.to_string();
1864 let val = expect_str(args.get(2))?.to_string();
1865 let ttl = expect_int(args.get(3))?;
1866 let mut reg = redis_registry().lock().unwrap();
1867 let entry = reg.touch_get_mut(h)
1868 .ok_or_else(|| "redis.set_ex: closed or unknown ConnRedis handle".to_string())?;
1869 use redis::Commands;
1870 entry.conn.set_ex::<_, _, ()>(&key, &val, ttl as u64)
1871 .map_err(|e| format!("redis.set_ex: {e}"))?;
1872 Ok(Value::Unit)
1873 }
1874 ("redis", "del") => {
1875 let h = expect_redis_handle(args.first())?;
1876 let key = expect_str(args.get(1))?.to_string();
1877 let mut reg = redis_registry().lock().unwrap();
1878 let entry = reg.touch_get_mut(h)
1879 .ok_or_else(|| "redis.del: closed or unknown ConnRedis handle".to_string())?;
1880 use redis::Commands;
1881 entry.conn.del::<_, ()>(&key)
1882 .map_err(|e| format!("redis.del: {e}"))?;
1883 Ok(Value::Unit)
1884 }
1885 ("redis", "exists") => {
1886 let h = expect_redis_handle(args.first())?;
1887 let key = expect_str(args.get(1))?.to_string();
1888 let mut reg = redis_registry().lock().unwrap();
1889 let entry = reg.touch_get_mut(h)
1890 .ok_or_else(|| "redis.exists: closed or unknown ConnRedis handle".to_string())?;
1891 use redis::Commands;
1892 let present: bool = entry.conn.exists(&key)
1893 .map_err(|e| format!("redis.exists: {e}"))?;
1894 Ok(Value::Bool(present))
1895 }
1896 ("redis", "expire") => {
1897 let h = expect_redis_handle(args.first())?;
1898 let key = expect_str(args.get(1))?.to_string();
1899 let ttl = expect_int(args.get(2))?;
1900 let mut reg = redis_registry().lock().unwrap();
1901 let entry = reg.touch_get_mut(h)
1902 .ok_or_else(|| "redis.expire: closed or unknown ConnRedis handle".to_string())?;
1903 use redis::Commands;
1904 entry.conn.expire::<_, ()>(&key, ttl)
1905 .map_err(|e| format!("redis.expire: {e}"))?;
1906 Ok(Value::Unit)
1907 }
1908 ("redis", "publish") => {
1909 let h = expect_redis_handle(args.first())?;
1910 let channel = expect_str(args.get(1))?.to_string();
1911 let msg = expect_str(args.get(2))?.to_string();
1912 let mut reg = redis_registry().lock().unwrap();
1913 let entry = reg.touch_get_mut(h)
1914 .ok_or_else(|| "redis.publish: closed or unknown ConnRedis handle".to_string())?;
1915 use redis::Commands;
1916 let n: i64 = entry.conn.publish(&channel, &msg)
1917 .map_err(|e| format!("redis.publish: {e}"))?;
1918 Ok(Value::Int(n))
1919 }
1920 ("redis", "subscribe") => {
1925 let h = expect_redis_handle(args.first())?;
1926 let channel = expect_str(args.get(1))?.to_string();
1927 let closure = match args.into_iter().nth(2) {
1928 Some(c @ Value::Closure { .. }) => c,
1929 _ => return Err("redis.subscribe: handler must be a Closure".into()),
1930 };
1931 let program = self.program.clone()
1932 .ok_or("redis.subscribe: no program; call DefaultHandler::with_program")?;
1933 let policy = self.policy.clone();
1934 let url = redis_registry().lock().unwrap()
1935 .get_url(h)
1936 .ok_or("redis.subscribe: closed or unknown ConnRedis handle")?;
1937 let client = redis::Client::open(url.as_str())
1938 .map_err(|e| format!("redis.subscribe: {e}"))?;
1939 let mut conn = client.get_connection()
1940 .map_err(|e| format!("redis.subscribe: {e}"))?;
1941 let mut pubsub = conn.as_pubsub();
1942 pubsub.subscribe(&channel)
1943 .map_err(|e| format!("redis.subscribe: {e}"))?;
1944 loop {
1945 let msg = pubsub.get_message()
1946 .map_err(|e| format!("redis.subscribe: {e}"))?;
1947 let ch: String = msg.get_channel_name().to_string();
1948 let payload: String = msg.get_payload()
1949 .map_err(|e| format!("redis.subscribe: payload: {e}"))?;
1950 let handler = DefaultHandler::new(policy.clone())
1951 .with_program(Arc::clone(&program));
1952 let mut vm = Vm::with_handler(&program, Box::new(handler));
1953 vm.invoke_closure_value(closure.clone(), vec![
1954 Value::Str(ch.into()),
1955 Value::Str(payload.into()),
1956 ]).map_err(|e| format!("redis.subscribe: handler: {e:?}"))?;
1957 }
1958 }
1959 ("redis", "psubscribe") => {
1960 let h = expect_redis_handle(args.first())?;
1961 let pattern = expect_str(args.get(1))?.to_string();
1962 let closure = match args.into_iter().nth(2) {
1963 Some(c @ Value::Closure { .. }) => c,
1964 _ => return Err("redis.psubscribe: handler must be a Closure".into()),
1965 };
1966 let program = self.program.clone()
1967 .ok_or("redis.psubscribe: no program; call DefaultHandler::with_program")?;
1968 let policy = self.policy.clone();
1969 let url = redis_registry().lock().unwrap()
1970 .get_url(h)
1971 .ok_or("redis.psubscribe: closed or unknown ConnRedis handle")?;
1972 let client = redis::Client::open(url.as_str())
1973 .map_err(|e| format!("redis.psubscribe: {e}"))?;
1974 let mut conn = client.get_connection()
1975 .map_err(|e| format!("redis.psubscribe: {e}"))?;
1976 let mut pubsub = conn.as_pubsub();
1977 pubsub.psubscribe(&pattern)
1978 .map_err(|e| format!("redis.psubscribe: {e}"))?;
1979 loop {
1980 let msg = pubsub.get_message()
1981 .map_err(|e| format!("redis.psubscribe: {e}"))?;
1982 let pat: String = msg.get_pattern()
1983 .ok()
1984 .and_then(|v: Option<String>| v)
1985 .unwrap_or_else(|| pattern.clone());
1986 let ch: String = msg.get_channel_name().to_string();
1987 let payload: String = msg.get_payload()
1988 .map_err(|e| format!("redis.psubscribe: payload: {e}"))?;
1989 let handler = DefaultHandler::new(policy.clone())
1990 .with_program(Arc::clone(&program));
1991 let mut vm = Vm::with_handler(&program, Box::new(handler));
1992 vm.invoke_closure_value(closure.clone(), vec![
1993 Value::Str(pat.into()),
1994 Value::Str(ch.into()),
1995 Value::Str(payload.into()),
1996 ]).map_err(|e| format!("redis.psubscribe: handler: {e:?}"))?;
1997 }
1998 }
1999 ("redis", "lpush") => {
2000 let h = expect_redis_handle(args.first())?;
2001 let key = expect_str(args.get(1))?.to_string();
2002 let val = expect_str(args.get(2))?.to_string();
2003 let mut reg = redis_registry().lock().unwrap();
2004 let entry = reg.touch_get_mut(h)
2005 .ok_or_else(|| "redis.lpush: closed or unknown ConnRedis handle".to_string())?;
2006 use redis::Commands;
2007 let n: i64 = entry.conn.lpush(&key, &val)
2008 .map_err(|e| format!("redis.lpush: {e}"))?;
2009 Ok(Value::Int(n))
2010 }
2011 ("redis", "rpush") => {
2012 let h = expect_redis_handle(args.first())?;
2013 let key = expect_str(args.get(1))?.to_string();
2014 let val = expect_str(args.get(2))?.to_string();
2015 let mut reg = redis_registry().lock().unwrap();
2016 let entry = reg.touch_get_mut(h)
2017 .ok_or_else(|| "redis.rpush: closed or unknown ConnRedis handle".to_string())?;
2018 use redis::Commands;
2019 let n: i64 = entry.conn.rpush(&key, &val)
2020 .map_err(|e| format!("redis.rpush: {e}"))?;
2021 Ok(Value::Int(n))
2022 }
2023 ("redis", "brpop") => {
2024 let h = expect_redis_handle(args.first())?;
2027 let key = expect_str(args.get(1))?.to_string();
2028 let timeout = expect_int(args.get(2))?;
2029 let mut reg = redis_registry().lock().unwrap();
2030 let entry = reg.touch_get_mut(h)
2031 .ok_or_else(|| "redis.brpop: closed or unknown ConnRedis handle".to_string())?;
2032 use redis::Commands;
2033 let result: Option<(String, String)> = entry.conn
2036 .brpop(&key, timeout as f64)
2037 .map_err(|e| format!("redis.brpop: {e}"))?;
2038 match result {
2039 Some((_, v)) => Ok(some(Value::Str(v.into()))),
2040 None => Ok(none()),
2041 }
2042 }
2043 ("redis", "llen") => {
2044 let h = expect_redis_handle(args.first())?;
2045 let key = expect_str(args.get(1))?.to_string();
2046 let mut reg = redis_registry().lock().unwrap();
2047 let entry = reg.touch_get_mut(h)
2048 .ok_or_else(|| "redis.llen: closed or unknown ConnRedis handle".to_string())?;
2049 use redis::Commands;
2050 let n: i64 = entry.conn.llen(&key)
2051 .map_err(|e| format!("redis.llen: {e}"))?;
2052 Ok(Value::Int(n))
2053 }
2054 ("redis", "hset") => {
2055 let h = expect_redis_handle(args.first())?;
2056 let key = expect_str(args.get(1))?.to_string();
2057 let field = expect_str(args.get(2))?.to_string();
2058 let val = expect_str(args.get(3))?.to_string();
2059 let mut reg = redis_registry().lock().unwrap();
2060 let entry = reg.touch_get_mut(h)
2061 .ok_or_else(|| "redis.hset: closed or unknown ConnRedis handle".to_string())?;
2062 use redis::Commands;
2063 entry.conn.hset::<_, _, _, ()>(&key, &field, &val)
2064 .map_err(|e| format!("redis.hset: {e}"))?;
2065 Ok(Value::Unit)
2066 }
2067 ("redis", "hget") => {
2068 let h = expect_redis_handle(args.first())?;
2069 let key = expect_str(args.get(1))?.to_string();
2070 let field = expect_str(args.get(2))?.to_string();
2071 let mut reg = redis_registry().lock().unwrap();
2072 let entry = reg.touch_get_mut(h)
2073 .ok_or_else(|| "redis.hget: closed or unknown ConnRedis handle".to_string())?;
2074 use redis::Commands;
2075 match entry.conn.hget::<_, _, Option<String>>(&key, &field) {
2076 Ok(Some(v)) => Ok(some(Value::Str(v.into()))),
2077 Ok(None) => Ok(none()),
2078 Err(e) => Err(format!("redis.hget: {e}")),
2079 }
2080 }
2081 ("redis", "hdel") => {
2082 let h = expect_redis_handle(args.first())?;
2083 let key = expect_str(args.get(1))?.to_string();
2084 let field = expect_str(args.get(2))?.to_string();
2085 let mut reg = redis_registry().lock().unwrap();
2086 let entry = reg.touch_get_mut(h)
2087 .ok_or_else(|| "redis.hdel: closed or unknown ConnRedis handle".to_string())?;
2088 use redis::Commands;
2089 entry.conn.hdel::<_, _, ()>(&key, &field)
2090 .map_err(|e| format!("redis.hdel: {e}"))?;
2091 Ok(Value::Unit)
2092 }
2093 ("redis", "hgetall") => {
2094 let h = expect_redis_handle(args.first())?;
2095 let key = expect_str(args.get(1))?.to_string();
2096 let mut reg = redis_registry().lock().unwrap();
2097 let entry = reg.touch_get_mut(h)
2098 .ok_or_else(|| "redis.hgetall: closed or unknown ConnRedis handle".to_string())?;
2099 use redis::Commands;
2100 let map: std::collections::HashMap<String, String> = entry.conn
2101 .hgetall(&key)
2102 .map_err(|e| format!("redis.hgetall: {e}"))?;
2103 let pairs: Vec<Value> = map.into_iter()
2104 .map(|(k, v)| Value::Tuple(vec![Value::Str(k.into()), Value::Str(v.into())]))
2105 .collect();
2106 Ok(Value::List(pairs.into()))
2107 }
2108
2109 ("proc", "spawn") => {
2110 let cmd = expect_str(args.first())?.to_string();
2124 let raw_args = match args.get(1) {
2125 Some(Value::List(items)) => items,
2126 Some(other) => return Err(format!(
2127 "proc.spawn: args must be List[Str], got {other:?}")),
2128 None => return Err("proc.spawn: missing args list".into()),
2129 };
2130 let str_args: Vec<String> = raw_args.iter().map(|v| match v {
2131 Value::Str(s) => Ok(s.to_string()),
2132 other => Err(format!("proc.spawn: arg must be Str, got {other:?}")),
2133 }).collect::<Result<Vec<_>, _>>()?;
2134
2135 if !self.policy.allow_proc.is_empty() {
2139 let basename = std::path::Path::new(&cmd)
2140 .file_name()
2141 .and_then(|s| s.to_str())
2142 .unwrap_or(&cmd);
2143 if !self.policy.allow_proc.iter().any(|a| a == basename) {
2144 return Ok(err(Value::Str(format!(
2145 "proc.spawn: `{cmd}` not in --allow-proc {:?}",
2146 self.policy.allow_proc
2147 ).into())));
2148 }
2149 }
2150
2151 if str_args.len() > 1024 {
2154 return Ok(err(Value::Str(
2155 SmolStr::new_inline("proc.spawn: arg-count exceeds 1024"))));
2156 }
2157 if str_args.iter().any(|a| a.len() > 65_536) {
2158 return Ok(err(Value::Str(
2159 "proc.spawn: per-arg length exceeds 64 KiB".into())));
2160 }
2161
2162 let output = std::process::Command::new(&cmd)
2163 .args(&str_args)
2164 .output();
2165 match output {
2166 Ok(o) => {
2167 let mut rec = indexmap::IndexMap::new();
2168 rec.insert("stdout".into(), Value::Str(
2169 String::from_utf8_lossy(&o.stdout).into_owned().into()));
2170 rec.insert("stderr".into(), Value::Str(
2171 String::from_utf8_lossy(&o.stderr).into_owned().into()));
2172 rec.insert("exit_code".into(), Value::Int(
2173 o.status.code().unwrap_or(-1) as i64));
2174 Ok(ok(Value::record_dynamic(rec)))
2175 }
2176 Err(e) => Ok(err(Value::Str(format!("spawn `{cmd}`: {e}").into()))),
2177 }
2178 }
2179 other => Err(format!("unsupported effect {}.{}", other.0, other.1)),
2180 }
2181 }
2182
2183 fn spawn_for_worker(&self) -> Option<Box<dyn lex_bytecode::vm::EffectHandler + Send>> {
2206 let mut fresh = DefaultHandler::new(self.policy.clone());
2207 fresh.budget_remaining = std::sync::Arc::clone(&self.budget_remaining);
2210 fresh.budget_ceiling = self.budget_ceiling;
2211 fresh.read_root = self.read_root.clone();
2212 fresh.program = self.program.clone();
2213 fresh.chat_registry = self.chat_registry.clone();
2214 fresh.streams = std::sync::Arc::clone(&self.streams);
2219 fresh.next_stream_id = std::sync::Arc::clone(&self.next_stream_id);
2220 fresh.program_args = self.program_args.clone();
2221 Some(Box::new(fresh))
2222 }
2223}
2224
2225pub struct TlsConfig {
2235 pub cert: Vec<u8>,
2236 pub key: Vec<u8>,
2237}
2238
2239fn serve_http(
2240 port: u16,
2241 handler_name: String,
2242 program: Arc<Program>,
2243 policy: Policy,
2244 tls: Option<TlsConfig>,
2245 opts: ServeOpts,
2246) -> Result<Value, String> {
2247 match tls {
2248 None => serve_http_plain(port, handler_name, program, policy, opts),
2249 Some(cfg) => serve_http_tls_legacy(port, handler_name, program, policy, cfg),
2250 }
2251}
2252
2253fn serve_http_plain(
2263 port: u16,
2264 handler_name: String,
2265 program: Arc<Program>,
2266 policy: Policy,
2267 opts: ServeOpts,
2268) -> Result<Value, String> {
2269 use http_body_util::BodyExt as _;
2270 use hyper::server::conn::http1;
2271 use hyper::service::service_fn;
2272 use hyper_util::rt::{TokioExecutor, TokioIo};
2273 use hyper_util::server::conn::auto;
2274 use tokio::net::TcpListener as TokioTcpListener;
2275
2276 let inline_vm = opts.inline_vm;
2277 let http2 = opts.http2;
2278 let host = opts.host.clone();
2279 let rt = tokio::runtime::Builder::new_multi_thread()
2280 .enable_all()
2281 .build()
2282 .map_err(|e| format!("net.serve: tokio runtime: {e}"))?;
2283 rt.block_on(async move {
2284 let listener = TokioTcpListener::bind((host.as_str(), port))
2285 .await
2286 .map_err(|e| format!("net.serve bind {host}:{port}: {e}"))?;
2287 eprintln!(
2288 "net.serve: listening on http://{host}:{port}{}{}",
2289 if inline_vm { " (inline-vm)" } else { "" },
2290 if http2 { " (http1+http2)" } else { "" }
2291 );
2292 loop {
2293 let (stream, _) = listener
2294 .accept()
2295 .await
2296 .map_err(|e| format!("net.serve accept: {e}"))?;
2297 let io = TokioIo::new(stream);
2298 let program = Arc::clone(&program);
2299 let policy = policy.clone();
2300 let handler_name = handler_name.clone();
2301 tokio::spawn(async move {
2302 let program2 = Arc::clone(&program);
2303 let policy2 = policy.clone();
2304 let handler_name2 = handler_name.clone();
2305 let svc = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
2306 let program = Arc::clone(&program2);
2307 let policy = policy2.clone();
2308 let handler_name = handler_name2.clone();
2309 async move {
2310 let (parts, body) = req.into_parts();
2311 let body_bytes = body
2312 .collect()
2313 .await
2314 .map(|c| c.to_bytes())
2315 .unwrap_or_default();
2316 let result = if inline_vm {
2317 let lex_req = build_request_value_parts(&parts, &body_bytes);
2321 let handler = DefaultHandler::new(policy)
2322 .with_program(Arc::clone(&program));
2323 let mut vm = Vm::with_handler(&program, Box::new(handler));
2324 let r = vm.call(&handler_name, vec![lex_req]);
2325 Ok(r.map(|v| unpack_response(&mut vm, &v)))
2328 } else {
2329 tokio::task::spawn_blocking(move || {
2330 let lex_req = build_request_value_parts(&parts, &body_bytes);
2331 let handler = DefaultHandler::new(policy)
2332 .with_program(Arc::clone(&program));
2333 let mut vm = Vm::with_handler(&program, Box::new(handler));
2334 let r = vm.call(&handler_name, vec![lex_req]);
2335 r.map(|v| unpack_response(&mut vm, &v))
2336 })
2337 .await
2338 };
2339 Ok::<_, std::convert::Infallible>(match result {
2340 Ok(Ok(unpacked)) => build_hyper_response(unpacked),
2341 Ok(Err(e)) => error_response(500, &format!("internal error: {e}")),
2342 Err(e) => error_response(500, &format!("task panicked: {e}")),
2343 })
2344 }
2345 });
2346 let result = if http2 {
2347 auto::Builder::new(TokioExecutor::new())
2348 .serve_connection(io, svc)
2349 .await
2350 .map_err(|e| e.to_string())
2351 } else {
2352 http1::Builder::new()
2353 .serve_connection(io, svc)
2354 .await
2355 .map_err(|e| e.to_string())
2356 };
2357 if let Err(e) = result {
2358 eprintln!("net.serve: connection error: {e}");
2359 }
2360 });
2361 }
2362 })
2363}
2364
2365fn serve_http_tls_legacy(
2367 port: u16,
2368 handler_name: String,
2369 program: Arc<Program>,
2370 policy: Policy,
2371 cfg: TlsConfig,
2372) -> Result<Value, String> {
2373 let ssl = tiny_http::SslConfig {
2374 certificate: cfg.cert,
2375 private_key: cfg.key,
2376 };
2377 let server = tiny_http::Server::https(("0.0.0.0", port), ssl)
2378 .map_err(|e| format!("net.serve_tls bind {port}: {e}"))?;
2379 eprintln!("net.serve: listening on https://0.0.0.0:{port}");
2380 for req in server.incoming_requests() {
2381 let program = Arc::clone(&program);
2382 let policy = policy.clone();
2383 let handler_name = handler_name.clone();
2384 std::thread::spawn(move || handle_request_tls(req, program, policy, handler_name));
2385 }
2386 Ok(Value::Unit)
2387}
2388
2389fn handle_request_tls(
2390 mut req: tiny_http::Request,
2391 program: Arc<Program>,
2392 policy: Policy,
2393 handler_name: String,
2394) {
2395 let lex_req = build_request_value_tiny(&mut req);
2396 let handler = DefaultHandler::new(policy).with_program(Arc::clone(&program));
2397 let mut vm = Vm::with_handler(&program, Box::new(handler));
2398 match vm.call(&handler_name, vec![lex_req]) {
2399 Ok(resp) => {
2400 let (status, body, headers) = unpack_response(&mut vm, &resp);
2406 respond_with_body_tls(req, status, body, headers);
2407 }
2408 Err(e) => {
2409 let response = tiny_http::Response::from_string(format!("internal error: {e}"))
2410 .with_status_code(500);
2411 let _ = req.respond(response);
2412 }
2413 }
2414}
2415
2416fn serve_http_fn(
2421 port: u16,
2422 closure: Value,
2423 program: Arc<Program>,
2424 policy: Policy,
2425 opts: ServeOpts,
2426) -> Result<Value, String> {
2427 use http_body_util::BodyExt as _;
2428 use hyper::server::conn::http1;
2429 use hyper::service::service_fn;
2430 use hyper_util::rt::{TokioExecutor, TokioIo};
2431 use hyper_util::server::conn::auto;
2432 use tokio::net::TcpListener as TokioTcpListener;
2433
2434 let inline_vm = opts.inline_vm;
2435 let http2 = opts.http2;
2436 let host = opts.host.clone();
2437 let rt = tokio::runtime::Builder::new_multi_thread()
2438 .enable_all()
2439 .build()
2440 .map_err(|e| format!("net.serve_fn: tokio runtime: {e}"))?;
2441 rt.block_on(async move {
2442 let listener = TokioTcpListener::bind((host.as_str(), port))
2443 .await
2444 .map_err(|e| format!("net.serve_fn bind {host}:{port}: {e}"))?;
2445 eprintln!(
2446 "net.serve_fn: listening on http://{host}:{port}{}{}",
2447 if inline_vm { " (inline-vm)" } else { "" },
2448 if http2 { " (http1+http2)" } else { "" }
2449 );
2450 loop {
2451 let (stream, _) = listener
2452 .accept()
2453 .await
2454 .map_err(|e| format!("net.serve_fn accept: {e}"))?;
2455 let io = TokioIo::new(stream);
2456 let program = Arc::clone(&program);
2457 let policy = policy.clone();
2458 let closure = closure.clone();
2459 tokio::spawn(async move {
2460 let program2 = Arc::clone(&program);
2461 let policy2 = policy.clone();
2462 let closure2 = closure.clone();
2463 let svc = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
2464 let program = Arc::clone(&program2);
2465 let policy = policy2.clone();
2466 let closure = closure2.clone();
2467 async move {
2468 let (parts, body) = req.into_parts();
2469 let body_bytes = body
2470 .collect()
2471 .await
2472 .map(|c| c.to_bytes())
2473 .unwrap_or_default();
2474 let result = if inline_vm {
2475 let lex_req = build_request_value_parts(&parts, &body_bytes);
2476 let handler = DefaultHandler::new(policy)
2477 .with_program(Arc::clone(&program));
2478 let mut vm = Vm::with_handler(&program, Box::new(handler));
2479 let scope = vm.enter_request_scope();
2486 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2487 let r = r.map(|v| unpack_response(&mut vm, &v));
2491 vm.exit_request_scope(scope);
2492 Ok(r)
2493 } else {
2494 tokio::task::spawn_blocking(move || {
2495 let lex_req = build_request_value_parts(&parts, &body_bytes);
2496 let handler = DefaultHandler::new(policy)
2497 .with_program(Arc::clone(&program));
2498 let mut vm = Vm::with_handler(&program, Box::new(handler));
2499 let scope = vm.enter_request_scope();
2500 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2501 let r = r.map(|v| unpack_response(&mut vm, &v));
2502 vm.exit_request_scope(scope);
2503 r
2504 })
2505 .await
2506 };
2507 Ok::<_, std::convert::Infallible>(match result {
2508 Ok(Ok(unpacked)) => build_hyper_response(unpacked),
2509 Ok(Err(e)) => error_response(500, &format!("internal error: {e}")),
2510 Err(e) => error_response(500, &format!("task panicked: {e}")),
2511 })
2512 }
2513 });
2514 let result = if http2 {
2515 auto::Builder::new(TokioExecutor::new())
2516 .serve_connection(io, svc)
2517 .await
2518 .map_err(|e| e.to_string())
2519 } else {
2520 http1::Builder::new()
2521 .serve_connection(io, svc)
2522 .await
2523 .map_err(|e| e.to_string())
2524 };
2525 if let Err(e) = result {
2526 eprintln!("net.serve_fn: connection error: {e}");
2527 }
2528 });
2529 }
2530 })
2531}
2532
2533#[derive(Clone, Debug)]
2537pub(crate) enum RouteSeg {
2538 Literal(String),
2539 Param(String),
2542}
2543
2544fn compile_path_pattern(pat: &str) -> Result<Vec<RouteSeg>, String> {
2548 if pat.is_empty() {
2549 return Err("path pattern must be non-empty (use \"/\" for the root)".into());
2550 }
2551 if !pat.starts_with('/') {
2552 return Err(format!("path pattern must start with '/' (got {pat:?})"));
2553 }
2554 let mut segs = Vec::new();
2555 for raw in pat.split('/') {
2556 if let Some(name) = raw.strip_prefix(':') {
2557 if name.is_empty() {
2558 return Err(format!(
2559 ":-segment in pattern {pat:?} must have a name (e.g. :id)"
2560 ));
2561 }
2562 segs.push(RouteSeg::Param(name.to_string()));
2563 } else {
2564 segs.push(RouteSeg::Literal(raw.to_string()));
2565 }
2566 }
2567 Ok(segs)
2568}
2569
2570fn match_path_pattern(
2576 segs: &[RouteSeg],
2577 path: &str,
2578) -> Option<std::collections::BTreeMap<lex_bytecode::MapKey, Value>> {
2579 let path_segs: Vec<&str> = path.split('/').collect();
2580 if path_segs.len() != segs.len() {
2581 return None;
2582 }
2583 let mut params = std::collections::BTreeMap::new();
2584 for (pat, p) in segs.iter().zip(path_segs.iter()) {
2585 match pat {
2586 RouteSeg::Literal(lit) => {
2587 if lit != p {
2588 return None;
2589 }
2590 }
2591 RouteSeg::Param(name) => {
2592 params.insert(
2593 lex_bytecode::MapKey::Str(name.clone()),
2594 Value::Str((*p).into()),
2595 );
2596 }
2597 }
2598 }
2599 Some(params)
2600}
2601
2602fn decode_routes_arg(
2607 v: Value,
2608) -> Result<Vec<(String, Vec<RouteSeg>, Value)>, String> {
2609 let list = match v {
2610 Value::List(xs) => xs,
2611 _ => return Err("net.serve_routed: routes must be a List".into()),
2612 };
2613 let mut out = Vec::with_capacity(list.len());
2614 for (i, item) in list.into_iter().enumerate() {
2615 let tup = match item {
2616 Value::Tuple(xs) if xs.len() == 3 => xs,
2617 other => return Err(format!(
2618 "net.serve_routed: route #{i} must be a (method, pattern, handler) 3-tuple, got {other:?}"
2619 )),
2620 };
2621 let mut it = tup.into_iter();
2622 let method_raw = match it.next() {
2623 Some(Value::Str(s)) => s.to_string(),
2624 _ => return Err(format!("net.serve_routed: route #{i} method must be Str")),
2625 };
2626 let method = if method_raw == "*" { method_raw } else { method_raw.to_uppercase() };
2628 let pattern = match it.next() {
2629 Some(Value::Str(s)) => s.to_string(),
2630 _ => return Err(format!("net.serve_routed: route #{i} path-pattern must be Str")),
2631 };
2632 let segs = compile_path_pattern(&pattern)
2633 .map_err(|e| format!("net.serve_routed: route #{i} ({pattern:?}): {e}"))?;
2634 let closure = match it.next() {
2635 Some(c @ Value::Closure { .. }) => c,
2636 _ => return Err(format!("net.serve_routed: route #{i} handler must be a closure")),
2637 };
2638 out.push((method, segs, closure));
2639 }
2640 Ok(out)
2641}
2642
2643pub(crate) fn dispatch_route<'a>(
2648 routes: &'a [(String, Vec<RouteSeg>, Value)],
2649 req_method: &str,
2650 req_path: &str,
2651) -> Option<(&'a Value, std::collections::BTreeMap<lex_bytecode::MapKey, Value>)> {
2652 let req_method_upper = req_method.to_ascii_uppercase();
2653 for (m, segs, closure) in routes {
2654 if m != "*" && m != &req_method_upper {
2655 continue;
2656 }
2657 if let Some(params) = match_path_pattern(segs, req_path) {
2658 return Some((closure, params));
2659 }
2660 }
2661 None
2662}
2663
2664pub(crate) fn stamp_path_params(
2668 req: &mut Value,
2669 params: std::collections::BTreeMap<lex_bytecode::MapKey, Value>,
2670) {
2671 if let Value::Record { fields: rec, .. } = req {
2672 rec.insert("path_params".into(), Value::Map(params));
2673 }
2674}
2675
2676fn serve_http_routed(
2682 port: u16,
2683 routes: Vec<(String, Vec<RouteSeg>, Value)>,
2684 fallback: Value,
2685 program: Arc<Program>,
2686 policy: Policy,
2687 opts: ServeOpts,
2688) -> Result<Value, String> {
2689 use http_body_util::BodyExt as _;
2690 use hyper::server::conn::http1;
2691 use hyper::service::service_fn;
2692 use hyper_util::rt::{TokioExecutor, TokioIo};
2693 use hyper_util::server::conn::auto;
2694 use tokio::net::TcpListener as TokioTcpListener;
2695
2696 let inline_vm = opts.inline_vm;
2697 let http2 = opts.http2;
2698 let host = opts.host.clone();
2699 let routes = Arc::new(routes);
2700 let rt = tokio::runtime::Builder::new_multi_thread()
2701 .enable_all()
2702 .build()
2703 .map_err(|e| format!("net.serve_routed: tokio runtime: {e}"))?;
2704 rt.block_on(async move {
2705 let listener = TokioTcpListener::bind((host.as_str(), port))
2706 .await
2707 .map_err(|e| format!("net.serve_routed bind {host}:{port}: {e}"))?;
2708 eprintln!(
2709 "net.serve_routed: listening on http://{host}:{port} ({} routes{}{})",
2710 routes.len(),
2711 if inline_vm { ", inline-vm" } else { "" },
2712 if http2 { ", http1+http2" } else { "" }
2713 );
2714 loop {
2715 let (stream, _) = listener
2716 .accept()
2717 .await
2718 .map_err(|e| format!("net.serve_routed accept: {e}"))?;
2719 let io = TokioIo::new(stream);
2720 let program = Arc::clone(&program);
2721 let policy = policy.clone();
2722 let routes = Arc::clone(&routes);
2723 let fallback = fallback.clone();
2724 tokio::spawn(async move {
2725 let program2 = Arc::clone(&program);
2726 let policy2 = policy.clone();
2727 let routes2 = Arc::clone(&routes);
2728 let fallback2 = fallback.clone();
2729 let svc = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
2730 let program = Arc::clone(&program2);
2731 let policy = policy2.clone();
2732 let routes = Arc::clone(&routes2);
2733 let fallback = fallback2.clone();
2734 async move {
2735 let (parts, body) = req.into_parts();
2736 let body_bytes = body
2737 .collect()
2738 .await
2739 .map(|c| c.to_bytes())
2740 .unwrap_or_default();
2741 let method = parts.method.as_str().to_string();
2742 let path = match parts.uri.path() {
2743 "" => "/".to_string(),
2744 p => p.to_string(),
2745 };
2746 let result = if inline_vm {
2747 let mut lex_req = build_request_value_parts(&parts, &body_bytes);
2748 let (closure, params) = match dispatch_route(&routes, &method, &path) {
2749 Some((c, p)) => (c.clone(), p),
2750 None => (fallback.clone(), std::collections::BTreeMap::new()),
2751 };
2752 stamp_path_params(&mut lex_req, params);
2753 let handler = DefaultHandler::new(policy)
2754 .with_program(Arc::clone(&program));
2755 let mut vm = Vm::with_handler(&program, Box::new(handler));
2756 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2757 Ok(r.map(|v| unpack_response(&mut vm, &v)))
2760 } else {
2761 tokio::task::spawn_blocking(move || {
2762 let mut lex_req = build_request_value_parts(&parts, &body_bytes);
2763 let (closure, params) = match dispatch_route(&routes, &method, &path) {
2764 Some((c, p)) => (c.clone(), p),
2765 None => (fallback.clone(), std::collections::BTreeMap::new()),
2766 };
2767 stamp_path_params(&mut lex_req, params);
2768 let handler = DefaultHandler::new(policy)
2769 .with_program(Arc::clone(&program));
2770 let mut vm = Vm::with_handler(&program, Box::new(handler));
2771 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2772 r.map(|v| unpack_response(&mut vm, &v))
2773 })
2774 .await
2775 };
2776 Ok::<_, std::convert::Infallible>(match result {
2777 Ok(Ok(unpacked)) => build_hyper_response(unpacked),
2778 Ok(Err(e)) => error_response(500, &format!("internal error: {e}")),
2779 Err(e) => error_response(500, &format!("task panicked: {e}")),
2780 })
2781 }
2782 });
2783 let result = if http2 {
2784 auto::Builder::new(TokioExecutor::new())
2785 .serve_connection(io, svc)
2786 .await
2787 .map_err(|e| e.to_string())
2788 } else {
2789 http1::Builder::new()
2790 .serve_connection(io, svc)
2791 .await
2792 .map_err(|e| e.to_string())
2793 };
2794 if let Err(e) = result {
2795 eprintln!("net.serve_routed: connection error: {e}");
2796 }
2797 });
2798 }
2799 })
2800}
2801
2802fn env_inline_vm() -> bool {
2807 match std::env::var("LEX_NET_INLINE_VM") {
2808 Ok(v) => {
2809 let s = v.trim().to_ascii_lowercase();
2810 s == "1" || s == "true"
2811 }
2812 Err(_) => false,
2813 }
2814}
2815
2816#[derive(Debug, Clone)]
2822pub(crate) struct ServeOpts {
2823 pub(crate) http2: bool,
2824 pub(crate) inline_vm: bool,
2825 pub(crate) host: String,
2826}
2827
2828impl ServeOpts {
2829 fn from_env() -> Self {
2833 Self {
2834 http2: env_http2(),
2835 inline_vm: env_inline_vm(),
2836 host: "0.0.0.0".to_string(),
2837 }
2838 }
2839
2840 fn lex_defaults() -> Self {
2845 Self {
2846 http2: false,
2847 inline_vm: false,
2848 host: "0.0.0.0".to_string(),
2849 }
2850 }
2851
2852 fn to_value(&self) -> Value {
2854 let mut rec = indexmap::IndexMap::new();
2855 rec.insert("http2".to_string(), Value::Bool(self.http2));
2856 rec.insert("inline_vm".to_string(), Value::Bool(self.inline_vm));
2857 rec.insert("host".to_string(), Value::Str(self.host.clone().into()));
2858 Value::record_dynamic(rec)
2859 }
2860}
2861
2862fn decode_serve_opts(v: &Value) -> Result<ServeOpts, String> {
2867 let rec = match v {
2868 Value::Record { fields: r, .. } => r,
2869 other => return Err(format!("opts must be a Record, got {other:?}")),
2870 };
2871 let http2 = match rec.get("http2") {
2872 Some(Value::Bool(b)) => *b,
2873 _ => return Err("opts.http2 must be Bool".into()),
2874 };
2875 let inline_vm = match rec.get("inline_vm") {
2876 Some(Value::Bool(b)) => *b,
2877 _ => return Err("opts.inline_vm must be Bool".into()),
2878 };
2879 let host = match rec.get("host") {
2880 Some(Value::Str(s)) => s.to_string(),
2881 _ => return Err("opts.host must be Str".into()),
2882 };
2883 Ok(ServeOpts { http2, inline_vm, host })
2884}
2885
2886fn make_tls_config_value(cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Value {
2896 let mut rec = indexmap::IndexMap::new();
2897 rec.insert("cert".into(), Value::Bytes(cert_pem));
2898 rec.insert("key".into(), Value::Bytes(key_pem));
2899 Value::record_dynamic(rec)
2900}
2901
2902#[cfg(feature = "quic")]
2903fn decode_tls_config(v: &Value) -> Result<crate::quic::QuicTls, String> {
2904 let rec = match v {
2905 Value::Record { fields: r, .. } => r,
2906 other => return Err(format!("TlsConfig: expected Record, got {other:?}")),
2907 };
2908 let cert = match rec.get("cert") {
2909 Some(Value::Bytes(b)) => b.to_vec(),
2910 _ => return Err("TlsConfig.cert: must be Bytes".into()),
2911 };
2912 let key = match rec.get("key") {
2913 Some(Value::Bytes(b)) => b.to_vec(),
2914 _ => return Err("TlsConfig.key: must be Bytes".into()),
2915 };
2916 Ok(crate::quic::QuicTls { cert_pem: cert, key_pem: key })
2917}
2918
2919fn dispatch_tls_from_pem_files(
2920 handler: &DefaultHandler,
2921 args: Vec<Value>,
2922) -> Result<Value, String> {
2923 let cert_path = expect_str(args.first())?.to_string();
2924 let key_path = expect_str(args.get(1))?.to_string();
2925 let cert_resolved = handler.resolve_read_path(&cert_path);
2926 let key_resolved = handler.resolve_read_path(&key_path);
2927 if !handler.policy.allow_fs_read.is_empty() {
2928 let allowed = |p: &std::path::Path| -> bool {
2929 handler.policy.allow_fs_read.iter().any(|a| p.starts_with(a))
2930 };
2931 if !allowed(&cert_resolved) {
2932 return Ok(err(Value::Str(
2933 format!("tls.from_pem_files: cert `{cert_path}` outside --allow-fs-read").into(),
2934 )));
2935 }
2936 if !allowed(&key_resolved) {
2937 return Ok(err(Value::Str(
2938 format!("tls.from_pem_files: key `{key_path}` outside --allow-fs-read").into(),
2939 )));
2940 }
2941 }
2942 let cert = match std::fs::read(&cert_resolved) {
2943 Ok(b) => b,
2944 Err(e) => return Ok(err(Value::Str(format!("read cert {cert_path}: {e}").into()))),
2945 };
2946 let key = match std::fs::read(&key_resolved) {
2947 Ok(b) => b,
2948 Err(e) => return Ok(err(Value::Str(format!("read key {key_path}: {e}").into()))),
2949 };
2950 Ok(ok(make_tls_config_value(cert, key)))
2951}
2952
2953#[cfg(feature = "quic")]
2954fn dispatch_tls_self_signed(args: Vec<Value>) -> Result<Value, String> {
2955 let hostname = expect_str(args.first())?.to_string();
2956 match crate::quic::self_signed_pem(&hostname) {
2957 Ok((cert, key)) => Ok(ok(make_tls_config_value(cert, key))),
2958 Err(e) => Ok(err(Value::Str(format!("tls.self_signed: {e}").into()))),
2959 }
2960}
2961
2962#[cfg(not(feature = "quic"))]
2963fn dispatch_tls_self_signed(_args: Vec<Value>) -> Result<Value, String> {
2964 Ok(err(Value::Str(
2965 "tls.self_signed: lex-runtime was compiled without the `quic` feature (needed for rcgen)".into(),
2966 )))
2967}
2968
2969impl DefaultHandler {
2970 #[cfg(feature = "quic")]
2971 fn dispatch_serve_quic_named(&self, args: Vec<Value>) -> Result<Value, String> {
2972 let port = match args.first() {
2973 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
2974 _ => return Err("net.serve_quic(port, tls, handler): port must be Int 0..=65535".into()),
2975 };
2976 let tls = decode_tls_config(args.get(1)
2977 .ok_or_else(|| "net.serve_quic(port, tls, handler): missing tls".to_string())?)?;
2978 let handler_name = expect_str(args.get(2))?.to_string();
2979 let program = self.program.clone()
2980 .ok_or_else(|| "net.serve_quic requires a Program reference; use DefaultHandler::with_program".to_string())?;
2981 let policy = self.policy.clone();
2982 crate::quic::serve_http3_named(port, handler_name, tls, program, policy, ServeOpts::from_env())
2983 }
2984
2985 #[cfg(feature = "quic")]
2986 fn dispatch_serve_quic_fn(&self, args: Vec<Value>) -> Result<Value, String> {
2987 let port = match args.first() {
2988 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
2989 _ => return Err("net.serve_quic_fn(port, tls, handler): port must be Int 0..=65535".into()),
2990 };
2991 let tls = decode_tls_config(args.get(1)
2992 .ok_or_else(|| "net.serve_quic_fn(port, tls, handler): missing tls".to_string())?)?;
2993 let closure = match args.into_iter().nth(2) {
2994 Some(c @ Value::Closure { .. }) => c,
2995 _ => return Err("net.serve_quic_fn(port, tls, handler): handler must be a closure".into()),
2996 };
2997 let program = self.program.clone()
2998 .ok_or_else(|| "net.serve_quic_fn requires a Program reference; use DefaultHandler::with_program".to_string())?;
2999 let policy = self.policy.clone();
3000 crate::quic::serve_http3_fn(port, closure, tls, program, policy, ServeOpts::from_env())
3001 }
3002
3003 #[cfg(feature = "quic")]
3004 fn dispatch_serve_quic_routed(&self, args: Vec<Value>) -> Result<Value, String> {
3005 let port = match args.first() {
3006 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
3007 _ => return Err("net.serve_quic_routed(port, tls, routes, fallback): port must be Int 0..=65535".into()),
3008 };
3009 let tls = decode_tls_config(args.get(1)
3010 .ok_or_else(|| "net.serve_quic_routed(port, tls, routes, fallback): missing tls".to_string())?)?;
3011 let routes_val = args.get(2).cloned()
3012 .ok_or_else(|| "net.serve_quic_routed(port, tls, routes, fallback): missing routes".to_string())?;
3013 let fallback = match args.into_iter().nth(3) {
3014 Some(c @ Value::Closure { .. }) => c,
3015 _ => return Err("net.serve_quic_routed(port, tls, routes, fallback): fallback must be a closure".into()),
3016 };
3017 let routes = decode_routes_arg(routes_val)?;
3018 let program = self.program.clone()
3019 .ok_or_else(|| "net.serve_quic_routed requires a Program reference; use DefaultHandler::with_program".to_string())?;
3020 let policy = self.policy.clone();
3021 crate::quic::serve_http3_routed(port, routes, fallback, tls, program, policy, ServeOpts::from_env())
3022 }
3023
3024 #[cfg(not(feature = "quic"))]
3025 fn dispatch_serve_quic_named(&self, _args: Vec<Value>) -> Result<Value, String> {
3026 Err("net.serve_quic: lex-runtime was compiled without the `quic` feature (needed for quinn + h3)".into())
3027 }
3028 #[cfg(not(feature = "quic"))]
3029 fn dispatch_serve_quic_fn(&self, _args: Vec<Value>) -> Result<Value, String> {
3030 Err("net.serve_quic_fn: lex-runtime was compiled without the `quic` feature (needed for quinn + h3)".into())
3031 }
3032 #[cfg(not(feature = "quic"))]
3033 fn dispatch_serve_quic_routed(&self, _args: Vec<Value>) -> Result<Value, String> {
3034 Err("net.serve_quic_routed: lex-runtime was compiled without the `quic` feature (needed for quinn + h3)".into())
3035 }
3036}
3037
3038fn env_http2() -> bool {
3048 match std::env::var("LEX_NET_HTTP2") {
3049 Ok(v) => {
3050 let s = v.trim().to_ascii_lowercase();
3051 s == "1" || s == "true"
3052 }
3053 Err(_) => false,
3054 }
3055}
3056
3057pub(crate) fn build_request_value_parts(
3059 parts: &hyper::http::request::Parts,
3060 body: &bytes::Bytes,
3061) -> Value {
3062 let method = parts.method.as_str().to_string();
3063 let path = parts.uri.path().to_string();
3071 let query = parts.uri.query().map(str::to_string).unwrap_or_default();
3072 let mut headers_map = std::collections::BTreeMap::new();
3073 for (name, val) in &parts.headers {
3074 if let Ok(v) = val.to_str() {
3075 headers_map.insert(
3076 lex_bytecode::MapKey::Str(name.as_str().to_ascii_lowercase()),
3077 Value::Str(v.to_string().into()),
3078 );
3079 }
3080 }
3081 let body_str = String::from_utf8_lossy(body).into_owned();
3082 let mut rec = indexmap::IndexMap::new();
3083 rec.insert("method".into(), Value::Str(method.into()));
3084 rec.insert("path".into(), Value::Str(path.into()));
3085 rec.insert("query".into(), Value::Str(query.into()));
3086 rec.insert("body".into(), Value::Str(body_str.into()));
3087 rec.insert("headers".into(), Value::Map(headers_map));
3088 rec.insert("path_params".into(), Value::Map(std::collections::BTreeMap::new()));
3089 Value::record_dynamic(rec)
3090}
3091
3092fn build_request_value_tiny(req: &mut tiny_http::Request) -> Value {
3094 let method = format!("{:?}", req.method()).to_uppercase();
3095 let url = req.url().to_string();
3096 let (path, query) = match url.split_once('?') {
3097 Some((p, q)) => (p.to_string(), q.to_string()),
3098 None => (url, String::new()),
3099 };
3100 let mut headers_map = std::collections::BTreeMap::new();
3101 for h in req.headers() {
3102 headers_map.insert(
3103 lex_bytecode::MapKey::Str(h.field.as_str().as_str().to_ascii_lowercase()),
3104 Value::Str(h.value.as_str().to_string().into()),
3105 );
3106 }
3107 let mut body = String::new();
3108 let _ = req.as_reader().read_to_string(&mut body);
3109 let mut rec = indexmap::IndexMap::new();
3110 rec.insert("method".into(), Value::Str(method.into()));
3111 rec.insert("path".into(), Value::Str(path.into()));
3112 rec.insert("query".into(), Value::Str(query.into()));
3113 rec.insert("body".into(), Value::Str(body.into()));
3114 rec.insert("headers".into(), Value::Map(headers_map));
3115 rec.insert("path_params".into(), Value::Map(std::collections::BTreeMap::new()));
3116 Value::record_dynamic(rec)
3117}
3118
3119pub(crate) fn unpack_response(vm: &mut Vm, v: &Value) -> UnpackedResponse {
3120 if !matches!(v, Value::Record { .. } | Value::ArenaRecord { .. }) {
3126 return (
3127 500,
3128 ResponseBodyOut::Str(format!("handler returned non-record: {v:?}")),
3129 vec![],
3130 );
3131 }
3132
3133 let status = vm.get_record_field(v, "status").and_then(|s| match s {
3134 Value::Int(n) => Some(n as u16),
3135 _ => None,
3136 }).unwrap_or(200);
3137
3138 let body = match vm.get_record_field(v, "body") {
3142 Some(Value::Variant { name, mut args }) if args.len() == 1 => {
3143 let inner = args.pop().unwrap();
3144 match (name.as_str(), inner) {
3145 ("BodyStr", Value::Str(s)) => ResponseBodyOut::Str(s.to_string()),
3147 ("BodyStream", iter_v) => {
3148 let drained = materialize_lazy_iter(vm, iter_v);
3149 ResponseBodyOut::TextChunks(drain_iter_str(&drained))
3150 }
3151 ("BodyBytes", iter_v) => {
3152 let drained = materialize_lazy_iter(vm, iter_v);
3153 ResponseBodyOut::BytesChunks(drain_iter_bytes(&drained))
3154 }
3155 _ => ResponseBodyOut::Str(String::new()),
3156 }
3157 }
3158 Some(Value::Str(s)) => ResponseBodyOut::Str(s.to_string()),
3164 _ => ResponseBodyOut::Str(String::new()),
3165 };
3166
3167 let headers: Vec<(String, String)> = match vm.get_record_field(v, "headers") {
3168 Some(Value::Map(hmap)) => hmap.iter().filter_map(|(k, val)| {
3169 if let (lex_bytecode::MapKey::Str(name), Value::Str(s)) = (k, val) {
3170 Some((name.clone(), s.to_string()))
3171 } else {
3172 None
3173 }
3174 }).collect(),
3175 _ => vec![],
3176 };
3177
3178 (status, body, headers)
3179}
3180
3181type HyperRespBody =
3182 http_body_util::combinators::BoxBody<bytes::Bytes, std::convert::Infallible>;
3183
3184fn build_hyper_response(
3194 (status, body, headers): UnpackedResponse,
3195) -> hyper::Response<HyperRespBody> {
3196 use http_body_util::BodyExt as _;
3197 let boxed_body: HyperRespBody = match body {
3198 ResponseBodyOut::Str(s) => {
3199 http_body_util::Full::new(bytes::Bytes::from(s.into_bytes())).boxed()
3200 }
3201 ResponseBodyOut::TextChunks(chunks) | ResponseBodyOut::BytesChunks(chunks) => {
3202 HyperChunkedBody::from(chunks).boxed()
3203 }
3204 };
3205 let mut builder = hyper::Response::builder().status(status);
3206 for (name, val) in headers {
3207 builder = builder.header(name, val);
3208 }
3209 builder
3210 .body(boxed_body)
3211 .unwrap_or_else(|_| error_response(500, "response build error"))
3212}
3213
3214fn error_response(status: u16, msg: &str) -> hyper::Response<HyperRespBody> {
3215 use http_body_util::BodyExt as _;
3216 hyper::Response::builder()
3217 .status(status)
3218 .body(
3219 http_body_util::Full::new(bytes::Bytes::from(msg.to_owned()))
3220 .boxed(),
3221 )
3222 .unwrap_or_else(|_| {
3223 use http_body_util::BodyExt as _;
3224 hyper::Response::new(http_body_util::Empty::new().map_err(|e| match e {}).boxed())
3225 })
3226}
3227
3228struct HyperChunkedBody {
3231 chunks: std::collections::VecDeque<Vec<u8>>,
3232}
3233
3234impl From<Vec<Vec<u8>>> for HyperChunkedBody {
3235 fn from(chunks: Vec<Vec<u8>>) -> Self {
3236 Self {
3237 chunks: chunks.into_iter().filter(|c| !c.is_empty()).collect(),
3238 }
3239 }
3240}
3241
3242impl hyper::body::Body for HyperChunkedBody {
3243 type Data = bytes::Bytes;
3244 type Error = std::convert::Infallible;
3245
3246 fn poll_frame(
3247 mut self: std::pin::Pin<&mut Self>,
3248 _cx: &mut std::task::Context<'_>,
3249 ) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
3250 match self.chunks.pop_front() {
3251 Some(chunk) => std::task::Poll::Ready(Some(Ok(hyper::body::Frame::data(
3252 bytes::Bytes::from(chunk),
3253 )))),
3254 None => std::task::Poll::Ready(None),
3255 }
3256 }
3257}
3258
3259fn respond_with_body_tls(
3263 req: tiny_http::Request,
3264 status: u16,
3265 body: ResponseBodyOut,
3266 headers: Vec<(String, String)>,
3267) {
3268 let tiny_headers: Vec<tiny_http::Header> = headers
3269 .into_iter()
3270 .filter_map(|(name, val)| format!("{name}: {val}").parse::<tiny_http::Header>().ok())
3271 .collect();
3272 match body {
3273 ResponseBodyOut::Str(s) => {
3274 let mut response = tiny_http::Response::from_string(s).with_status_code(status);
3275 for h in tiny_headers {
3276 response.add_header(h);
3277 }
3278 let _ = req.respond(response);
3279 }
3280 ResponseBodyOut::TextChunks(chunks) | ResponseBodyOut::BytesChunks(chunks) => {
3281 let reader = ChunkReader::new(chunks);
3282 let response = tiny_http::Response::new(
3283 tiny_http::StatusCode(status),
3284 tiny_headers,
3285 reader,
3286 None,
3287 None,
3288 );
3289 let _ = req.respond(response);
3290 }
3291 }
3292}
3293
3294pub(crate) type UnpackedResponse = (u16, ResponseBodyOut, Vec<(String, String)>);
3304
3305pub(crate) enum ResponseBodyOut {
3306 Str(String),
3307 TextChunks(Vec<Vec<u8>>),
3311 BytesChunks(Vec<Vec<u8>>),
3314}
3315
3316fn drain_iter_str(v: &Value) -> Vec<Vec<u8>> {
3329 match v {
3330 Value::Variant { name, args }
3331 if name == "__IterEager" && args.len() == 2 =>
3332 {
3333 if let (Value::List(items), Value::Int(idx)) = (&args[0], &args[1]) {
3334 items.iter().skip(*idx as usize).filter_map(|item| {
3335 if let Value::Str(s) = item { Some(s.as_bytes().to_vec()) } else { None }
3336 }).collect()
3337 } else {
3338 Vec::new()
3339 }
3340 }
3341 _ => Vec::new(),
3342 }
3343}
3344
3345fn drain_iter_bytes(v: &Value) -> Vec<Vec<u8>> {
3349 match v {
3350 Value::Variant { name, args }
3351 if name == "__IterEager" && args.len() == 2 =>
3352 {
3353 if let (Value::List(items), Value::Int(idx)) = (&args[0], &args[1]) {
3354 items.iter().skip(*idx as usize).filter_map(|item| {
3355 if let Value::List(ints) = item {
3356 Some(ints.iter().filter_map(|i| match i {
3357 Value::Int(n) => Some((*n & 0xff) as u8),
3358 _ => None,
3359 }).collect::<Vec<u8>>())
3360 } else {
3361 None
3362 }
3363 }).collect()
3364 } else {
3365 Vec::new()
3366 }
3367 }
3368 _ => Vec::new(),
3369 }
3370}
3371
3372fn materialize_lazy_iter(vm: &mut Vm, v: Value) -> Value {
3385 let mut current = v;
3386 let mut items: Vec<Value> = Vec::new();
3387 loop {
3388 match current {
3389 Value::Variant { name, args } if name == "__IterLazy" && args.len() == 2 => {
3390 let seed = args[0].clone();
3391 let step = args[1].clone();
3392 match vm.invoke_closure_value(step.clone(), vec![seed]) {
3393 Ok(Value::Variant { name: opt, args: opt_args })
3394 if opt == "None" =>
3395 {
3396 let _ = opt_args;
3397 break;
3398 }
3399 Ok(Value::Variant { name: opt, args: opt_args })
3400 if opt == "Some" && opt_args.len() == 1 =>
3401 {
3402 if let Value::Tuple(pair) = &opt_args[0] {
3403 if pair.len() == 2 {
3404 items.push(pair[0].clone());
3405 current = Value::Variant {
3406 name: "__IterLazy".to_string(),
3407 args: vec![pair[1].clone(), step],
3408 };
3409 continue;
3410 }
3411 }
3412 break;
3414 }
3415 _ => break,
3416 }
3417 }
3418 other => {
3421 if items.is_empty() {
3422 return other;
3423 }
3424 let _ = other;
3427 break;
3428 }
3429 }
3430 }
3431 Value::Variant {
3432 name: "__IterEager".to_string(),
3433 args: vec![
3434 Value::List(items.into_iter().collect()),
3435 Value::Int(0),
3436 ],
3437 }
3438}
3439
3440
3441struct ChunkReader {
3447 chunks: std::collections::VecDeque<Vec<u8>>,
3448 cursor: usize,
3449}
3450
3451impl ChunkReader {
3452 fn new(chunks: Vec<Vec<u8>>) -> Self {
3453 Self {
3454 chunks: chunks.into_iter().filter(|c| !c.is_empty()).collect(),
3455 cursor: 0,
3456 }
3457 }
3458}
3459
3460impl std::io::Read for ChunkReader {
3461 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3462 loop {
3463 let Some(front) = self.chunks.front() else {
3464 return Ok(0);
3465 };
3466 let remaining = &front[self.cursor..];
3467 if remaining.is_empty() {
3468 self.chunks.pop_front();
3469 self.cursor = 0;
3470 continue;
3471 }
3472 let n = remaining.len().min(buf.len());
3473 buf[..n].copy_from_slice(&remaining[..n]);
3474 self.cursor += n;
3475 if self.cursor >= front.len() {
3476 self.chunks.pop_front();
3477 self.cursor = 0;
3478 }
3479 return Ok(n);
3480 }
3481 }
3482}
3483
3484fn http_request(method: &str, url: &str, body: Option<&str>) -> Value {
3490 use std::time::Duration;
3491 let agent: ureq::Agent = ureq::Agent::config_builder()
3496 .timeout_connect(Some(Duration::from_secs(10)))
3497 .timeout_recv_body(Some(Duration::from_secs(30)))
3498 .timeout_send_body(Some(Duration::from_secs(10)))
3499 .http_status_as_error(false)
3500 .build()
3501 .into();
3502 let resp = match (method, body) {
3503 ("GET", _) => agent.get(url).call(),
3504 ("POST", Some(b)) => agent.post(url).send(b),
3505 ("POST", None) => agent.post(url).send(""),
3506 (m, _) => return err_value(format!("unsupported method: {m}")),
3507 };
3508 match resp {
3509 Ok(mut r) => {
3510 let status = r.status().as_u16();
3511 let body = r.body_mut().read_to_string().unwrap_or_default();
3512 if (200..300).contains(&status) {
3513 Value::Variant { name: "Ok".into(), args: vec![Value::Str(body.into())] }
3514 } else {
3515 err_value(format!("status {status}: {body}"))
3516 }
3517 }
3518 Err(e) => err_value(format!("transport: {e}")),
3519 }
3520}
3521
3522fn http_stream_agent() -> ureq::Agent {
3529 use std::time::Duration;
3530 ureq::Agent::config_builder()
3531 .timeout_global(Some(Duration::from_secs(600)))
3532 .http_status_as_error(false)
3533 .build()
3534 .into()
3535}
3536
3537fn http_agent(timeout_ms: Option<u64>) -> ureq::Agent {
3542 use std::time::Duration;
3543 let mut b = ureq::Agent::config_builder()
3544 .timeout_connect(Some(Duration::from_secs(10)))
3545 .timeout_recv_body(Some(Duration::from_secs(30)))
3546 .timeout_send_body(Some(Duration::from_secs(10)))
3547 .http_status_as_error(false);
3548 if let Some(ms) = timeout_ms {
3549 let d = Duration::from_millis(ms);
3550 b = b.timeout_global(Some(d));
3551 }
3552 b.build().into()
3553}
3554
3555fn http_error_value(e: ureq::Error) -> Value {
3559 let (ctor, payload): (&str, Option<String>) = match &e {
3560 ureq::Error::Timeout(_) => ("TimeoutError", None),
3561 ureq::Error::Tls(s) => ("TlsError", Some((*s).into())),
3562 ureq::Error::Pem(p) => ("TlsError", Some(format!("{p}"))),
3563 ureq::Error::Rustls(r) => ("TlsError", Some(format!("{r}"))),
3564 _ => ("NetworkError", Some(format!("{e}"))),
3565 };
3566 let args = match payload { Some(s) => vec![Value::Str(s.into())], None => vec![] };
3567 let inner = Value::Variant { name: ctor.into(), args };
3568 Value::Variant { name: "Err".into(), args: vec![inner] }
3569}
3570
3571fn http_decode_err(msg: String) -> Value {
3572 let inner = Value::Variant {
3573 name: "DecodeError".into(),
3574 args: vec![Value::Str(msg.into())],
3575 };
3576 Value::Variant { name: "Err".into(), args: vec![inner] }
3577}
3578
3579fn http_send_simple(
3584 method: &str,
3585 url: &str,
3586 body: Option<Vec<u8>>,
3587 content_type: &str,
3588 timeout_ms: Option<u64>,
3589) -> Value {
3590 http_send_full(method, url, body, content_type, &[], timeout_ms)
3591}
3592
3593fn http_send_full(
3594 method: &str,
3595 url: &str,
3596 body: Option<Vec<u8>>,
3597 content_type: &str,
3598 headers: &[(String, String)],
3599 timeout_ms: Option<u64>,
3600) -> Value {
3601 let agent = http_agent(timeout_ms);
3602 let method_upper = method.to_ascii_uppercase();
3608 let body_bytes: Vec<u8> = body.unwrap_or_default();
3609 let resp = match method_upper.as_str() {
3610 "GET" => {
3615 let mut req = agent.get(url);
3616 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3617 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3618 req.call()
3619 }
3620 "HEAD" => {
3621 let mut req = agent.head(url);
3622 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3623 req.call()
3624 }
3625 "DELETE" => {
3626 let mut req = agent.delete(url);
3627 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3628 req.call()
3629 }
3630 "POST" => {
3635 let mut req = agent.post(url);
3636 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3637 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3638 req.send(&body_bytes[..])
3639 }
3640 "PUT" => {
3641 let mut req = agent.put(url);
3642 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3643 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3644 req.send(&body_bytes[..])
3645 }
3646 "PATCH" => {
3647 let mut req = agent.patch(url);
3648 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3649 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3650 req.send(&body_bytes[..])
3651 }
3652 m => {
3653 return http_decode_err(format!("unsupported method: {m}"));
3654 }
3655 };
3656 match resp {
3657 Ok(mut r) => {
3658 let status = r.status().as_u16() as i64;
3659 let headers_map = collect_response_headers(r.headers());
3660 let body_bytes = match r.body_mut().with_config().limit(10 * 1024 * 1024).read_to_vec() {
3661 Ok(b) => b,
3662 Err(e) => return http_decode_err(format!("body read: {e}")),
3663 };
3664 let mut rec = indexmap::IndexMap::new();
3665 rec.insert("status".into(), Value::Int(status));
3666 rec.insert("headers".into(), Value::Map(headers_map));
3667 rec.insert("body".into(), Value::Bytes(body_bytes));
3668 Value::Variant { name: "Ok".into(), args: vec![Value::record_dynamic(rec)] }
3669 }
3670 Err(e) => http_error_value(e),
3671 }
3672}
3673
3674fn collect_response_headers(
3675 headers: &ureq::http::HeaderMap,
3676) -> std::collections::BTreeMap<lex_bytecode::MapKey, Value> {
3677 let mut out = std::collections::BTreeMap::new();
3678 for (name, value) in headers.iter() {
3679 let v = value.to_str().unwrap_or("").to_string();
3680 out.insert(lex_bytecode::MapKey::Str(name.as_str().to_string()), Value::Str(v.into()));
3681 }
3682 out
3683}
3684
3685fn http_send_record(handler: &DefaultHandler, req: &indexmap::IndexMap<smol_str::SmolStr, Value>) -> Value {
3689 let method = match req.get("method") {
3690 Some(Value::Str(s)) => s.to_string(),
3691 _ => return http_decode_err("HttpRequest.method must be Str".into()),
3692 };
3693 let url = match req.get("url") {
3694 Some(Value::Str(s)) => s.to_string(),
3695 _ => return http_decode_err("HttpRequest.url must be Str".into()),
3696 };
3697 if let Err(e) = handler.ensure_host_allowed(&url) {
3698 return http_decode_err(e);
3699 }
3700 let body = match req.get("body") {
3701 Some(Value::Variant { name, args }) if name == "None" => None,
3702 Some(Value::Variant { name, args }) if name == "Some" => match args.as_slice() {
3703 [Value::Bytes(b)] => Some(b.clone()),
3704 _ => return http_decode_err("HttpRequest.body Some payload must be Bytes".into()),
3705 },
3706 _ => return http_decode_err("HttpRequest.body must be Option[Bytes]".into()),
3707 };
3708 let timeout_ms = match req.get("timeout_ms") {
3709 Some(Value::Variant { name, .. }) if name == "None" => None,
3710 Some(Value::Variant { name, args }) if name == "Some" => match args.as_slice() {
3711 [Value::Int(n)] if *n >= 0 => Some(*n as u64),
3712 _ => return http_decode_err(
3713 "HttpRequest.timeout_ms Some payload must be a non-negative Int".into()),
3714 },
3715 _ => return http_decode_err("HttpRequest.timeout_ms must be Option[Int]".into()),
3716 };
3717 let headers: Vec<(String, String)> = match req.get("headers") {
3718 Some(Value::Map(m)) => m.iter().filter_map(|(k, v)| {
3719 let kk = match k { lex_bytecode::MapKey::Str(s) => s.clone(), _ => return None };
3720 let vv = match v { Value::Str(s) => s.to_string(), _ => return None };
3721 Some((kk, vv))
3722 }).collect(),
3723 _ => return http_decode_err("HttpRequest.headers must be Map[Str, Str]".into()),
3724 };
3725 http_send_full(&method, &url, body, "", &headers, timeout_ms)
3726}
3727
3728fn expect_record(v: Option<&Value>) -> Result<&indexmap::IndexMap<smol_str::SmolStr, Value>, String> {
3729 match v {
3730 Some(Value::Record { fields: r, .. }) => Ok(r),
3731 Some(other) => Err(format!("expected Record, got {other:?}")),
3732 None => Err("missing Record argument".into()),
3733 }
3734}
3735
3736fn err_value(msg: String) -> Value {
3737 Value::Variant { name: "Err".into(), args: vec![Value::Str(msg.into())] }
3738}
3739
3740fn expect_str(v: Option<&Value>) -> Result<&str, String> {
3741 match v {
3742 Some(Value::Str(s)) => Ok(s),
3743 Some(other) => Err(format!("expected Str arg, got {other:?}")),
3744 None => Err("missing argument".into()),
3745 }
3746}
3747
3748fn expect_int(v: Option<&Value>) -> Result<i64, String> {
3749 match v {
3750 Some(Value::Int(n)) => Ok(*n),
3751 Some(other) => Err(format!("expected Int arg, got {other:?}")),
3752 None => Err("missing argument".into()),
3753 }
3754}
3755
3756fn ok(v: Value) -> Value {
3757 Value::Variant { name: "Ok".into(), args: vec![v] }
3758}
3759fn err(v: Value) -> Value {
3760 Value::Variant { name: "Err".into(), args: vec![v] }
3761}
3762
3763fn http_stream_lines_impl(_handler: &DefaultHandler, url: &str, headers_val: &Value, body: &str) -> Value {
3773 let body_bytes = body.as_bytes().to_vec();
3774 let agent = http_stream_agent();
3777 let mut req = agent.post(url);
3778 if let Value::Map(headers) = headers_val {
3779 for (k, v) in headers {
3780 let key_str = match k {
3781 lex_bytecode::MapKey::Str(s) => s.as_str(),
3782 _ => continue,
3783 };
3784 if let Value::Str(val) = v {
3785 req = req.header(key_str, val.as_str());
3786 }
3787 }
3788 }
3789 match req.send(&body_bytes[..]) {
3790 Ok(resp) => {
3791 let bytes = match resp.into_body().with_config().read_to_vec() {
3792 Ok(b) => b,
3793 Err(e) => return err(Value::Str(format!("http.stream_lines: body read: {e}").into())),
3794 };
3795 let raw_text = String::from_utf8_lossy(&bytes).into_owned();
3796 let text = decode_unicode_escapes(&raw_text);
3797 let items: std::collections::VecDeque<Value> = text.lines()
3798 .map(|l| Value::Str(l.to_string().into()))
3799 .collect();
3800 let iter_val = Value::Variant {
3801 name: "__IterEager".into(),
3802 args: vec![Value::List(items), Value::Int(0)],
3803 };
3804 ok(iter_val)
3805 }
3806 Err(e) => err(Value::Str(format!("http.stream_lines: {e}").into())),
3807 }
3808}
3809
3810fn decode_unicode_escapes(s: &str) -> String {
3811 let mut result = String::with_capacity(s.len());
3812 let mut chars = s.chars().peekable();
3813 while let Some(c) = chars.next() {
3814 if c != '\\' {
3815 result.push(c);
3816 continue;
3817 }
3818 match chars.peek() {
3819 Some('u') => {
3820 chars.next();
3821 let hex: String = (0..4).filter_map(|_| chars.next()).collect();
3822 if hex.len() == 4 {
3823 if let Ok(n) = u32::from_str_radix(&hex, 16) {
3824 if let Some(ch) = char::from_u32(n) {
3825 result.push(ch);
3826 continue;
3827 }
3828 }
3829 }
3830 result.push('\\');
3831 result.push('u');
3832 result.push_str(&hex);
3833 }
3834 _ => result.push(c),
3835 }
3836 }
3837 result
3838}
3839
3840fn sql_error(message: impl Into<String>, code: Option<String>, detail: Option<String>) -> Value {
3844 let some = |s: String| Value::Variant { name: "Some".into(), args: vec![Value::Str(s.into())] };
3845 let none = || Value::Variant { name: "None".into(), args: vec![] };
3846 let mut rec = indexmap::IndexMap::new();
3847 let msg: String = message.into();
3848 rec.insert("message".into(), Value::Str(msg.into()));
3849 rec.insert("code".into(), match code {
3850 Some(c) => some(c),
3851 None => none(),
3852 });
3853 rec.insert("detail".into(), match detail {
3854 Some(d) => some(d),
3855 None => none(),
3856 });
3857 Value::record_dynamic(rec)
3858}
3859
3860fn sqlite_err_to_sql_error(e: rusqlite::Error, op: &str) -> Value {
3870 let message = format!("{op}: {e}");
3871 match &e {
3872 rusqlite::Error::SqliteFailure(ffi, detail_opt) => {
3873 sql_error(
3874 message,
3875 Some(sqlite_extended_code_name(ffi.extended_code)),
3876 detail_opt.clone(),
3877 )
3878 }
3879 rusqlite::Error::SqlInputError { error, msg, .. } => {
3880 sql_error(
3881 message,
3882 Some(sqlite_extended_code_name(error.extended_code)),
3883 Some(msg.clone()),
3884 )
3885 }
3886 _ => sql_error(message, None, None),
3887 }
3888}
3889
3890fn sqlite_extended_code_name(code: i32) -> String {
3896 use rusqlite::ffi::*;
3897 let s = match code {
3898 SQLITE_BUSY => "SQLITE_BUSY",
3899 SQLITE_LOCKED => "SQLITE_LOCKED",
3900 SQLITE_READONLY => "SQLITE_READONLY",
3901 SQLITE_IOERR => "SQLITE_IOERR",
3902 SQLITE_CORRUPT => "SQLITE_CORRUPT",
3903 SQLITE_NOTFOUND => "SQLITE_NOTFOUND",
3904 SQLITE_FULL => "SQLITE_FULL",
3905 SQLITE_CANTOPEN => "SQLITE_CANTOPEN",
3906 SQLITE_PROTOCOL => "SQLITE_PROTOCOL",
3907 SQLITE_SCHEMA => "SQLITE_SCHEMA",
3908 SQLITE_TOOBIG => "SQLITE_TOOBIG",
3909 SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT",
3910 SQLITE_CONSTRAINT_CHECK => "SQLITE_CONSTRAINT_CHECK",
3911 SQLITE_CONSTRAINT_FOREIGNKEY => "SQLITE_CONSTRAINT_FOREIGNKEY",
3912 SQLITE_CONSTRAINT_NOTNULL => "SQLITE_CONSTRAINT_NOTNULL",
3913 SQLITE_CONSTRAINT_PRIMARYKEY => "SQLITE_CONSTRAINT_PRIMARYKEY",
3914 SQLITE_CONSTRAINT_TRIGGER => "SQLITE_CONSTRAINT_TRIGGER",
3915 SQLITE_CONSTRAINT_UNIQUE => "SQLITE_CONSTRAINT_UNIQUE",
3916 SQLITE_CONSTRAINT_VTAB => "SQLITE_CONSTRAINT_VTAB",
3917 SQLITE_CONSTRAINT_ROWID => "SQLITE_CONSTRAINT_ROWID",
3918 SQLITE_MISMATCH => "SQLITE_MISMATCH",
3919 SQLITE_RANGE => "SQLITE_RANGE",
3920 SQLITE_NOTADB => "SQLITE_NOTADB",
3921 SQLITE_AUTH => "SQLITE_AUTH",
3922 _ => return format!("SQLITE_ERROR_{code}"),
3923 };
3924 s.to_string()
3925}
3926
3927fn pg_err_to_sql_error(e: postgres::Error, op: &str) -> Value {
3931 let message = format!("{op}: {e}");
3932 let code = e.as_db_error().map(|db| db.code().code().to_string());
3933 let detail = e.as_db_error().and_then(|db| db.detail().map(|s| s.to_string()));
3934 sql_error(message, code, detail)
3935}
3936
3937impl DefaultHandler {
3938 fn dispatch_call_mcp(&mut self, args: Vec<Value>) -> Value {
3944 let server = match args.first() {
3945 Some(Value::Str(s)) => s.clone(),
3946 _ => return err(Value::Str(
3947 "agent.call_mcp(server, tool, args_json): server must be Str".into())),
3948 };
3949 let tool = match args.get(1) {
3950 Some(Value::Str(s)) => s.clone(),
3951 _ => return err(Value::Str(
3952 "agent.call_mcp(server, tool, args_json): tool must be Str".into())),
3953 };
3954 let args_json = match args.get(2) {
3955 Some(Value::Str(s)) => s.clone(),
3956 _ => return err(Value::Str(
3957 "agent.call_mcp(server, tool, args_json): args_json must be Str".into())),
3958 };
3959 let parsed: serde_json::Value = match serde_json::from_str(&args_json) {
3960 Ok(v) => v,
3961 Err(e) => return err(Value::Str(format!(
3962 "agent.call_mcp: args_json is not valid JSON: {e}").into())),
3963 };
3964 match self.mcp_clients.call(&server, &tool, parsed) {
3965 Ok(result) => ok(Value::Str(
3966 serde_json::to_string(&result).unwrap_or_else(|_| "null".into()).into())),
3967 Err(e) => err(Value::Str(e.into())),
3968 }
3969 }
3970
3971 fn dispatch_cloud_stream(&mut self, args: Vec<Value>) -> Value {
3977 let _prompt = match args.first() {
3978 Some(Value::Str(s)) => s.clone(),
3979 _ => return err(Value::Str(
3980 "agent.cloud_stream(prompt): prompt must be Str".into())),
3981 };
3982 let chunks: Vec<String> = match std::env::var("LEX_LLM_STREAM_FIXTURE") {
3983 Ok(v) => v.split('|').map(|s| s.to_string()).collect(),
3984 Err(_) => return err(Value::Str(
3985 "agent.cloud_stream: live streaming not yet implemented; \
3986 set LEX_LLM_STREAM_FIXTURE='chunk1|chunk2|…' for tests".into())),
3987 };
3988 let handle = self.register_stream(chunks.into_iter());
3989 ok(stream_handle_value(handle))
3990 }
3991
3992 fn dispatch_stream_next(&mut self, args: Vec<Value>) -> Value {
3998 let handle = match args.first().and_then(stream_handle_id) {
3999 Some(h) => h,
4000 None => return Value::Variant { name: "None".into(), args: vec![] },
4001 };
4002 let mut streams = match self.streams.lock() {
4003 Ok(g) => g,
4004 Err(_) => return Value::Variant { name: "None".into(), args: vec![] },
4005 };
4006 match streams.get_mut(&handle).and_then(|it| it.next()) {
4007 Some(chunk) => some(Value::Str(chunk.into())),
4008 None => {
4009 streams.remove(&handle);
4010 Value::Variant { name: "None".into(), args: vec![] }
4011 }
4012 }
4013 }
4014
4015 fn dispatch_stream_collect(&mut self, args: Vec<Value>) -> Value {
4020 let handle = match args.first().and_then(stream_handle_id) {
4021 Some(h) => h,
4022 None => return Value::List(std::collections::VecDeque::new()),
4023 };
4024 let mut iter = {
4025 let mut streams = match self.streams.lock() {
4026 Ok(g) => g,
4027 Err(_) => return Value::List(std::collections::VecDeque::new()),
4028 };
4029 match streams.remove(&handle) {
4030 Some(it) => it,
4031 None => return Value::List(std::collections::VecDeque::new()),
4032 }
4033 };
4034 let mut out: std::collections::VecDeque<Value> = std::collections::VecDeque::new();
4035 for chunk in iter.by_ref() {
4036 out.push_back(Value::Str(chunk.into()));
4037 }
4038 Value::List(out)
4039 }
4040
4041 fn register_stream<I>(&self, iter: I) -> String
4045 where
4046 I: Iterator<Item = String> + Send + 'static,
4047 {
4048 let id = self
4049 .next_stream_id
4050 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4051 let handle = format!("stream_{id}");
4052 if let Ok(mut streams) = self.streams.lock() {
4053 streams.insert(handle.clone(), Box::new(iter));
4054 }
4055 handle
4056 }
4057}
4058
4059fn stream_handle_value(handle: String) -> Value {
4064 Value::Variant {
4065 name: "__StreamHandle".into(),
4066 args: vec![Value::Str(handle.into())],
4067 }
4068}
4069
4070fn stream_handle_id(v: &Value) -> Option<String> {
4074 match v {
4075 Value::Variant { name, args } if name == "__StreamHandle" => match args.first() {
4076 Some(Value::Str(h)) => Some(h.to_string()),
4077 _ => None,
4078 },
4079 _ => None,
4080 }
4081}
4082
4083fn dispatch_llm_local(args: Vec<Value>) -> Value {
4088 let prompt = match args.first() {
4089 Some(Value::Str(s)) => s.clone(),
4090 _ => return err(Value::Str(
4091 "agent.local_complete(prompt): prompt must be Str".into())),
4092 };
4093 match crate::llm::local_complete(&prompt) {
4094 Ok(text) => ok(Value::Str(text.into())),
4095 Err(e) => err(Value::Str(e.into())),
4096 }
4097}
4098
4099fn dispatch_llm_cloud(args: Vec<Value>) -> Value {
4106 let prompt = match args.first() {
4107 Some(Value::Str(s)) => s.clone(),
4108 _ => return err(Value::Str(
4109 "agent.cloud_complete(prompt): prompt must be Str".into())),
4110 };
4111 match crate::llm::cloud_complete(&prompt) {
4112 Ok(text) => ok(Value::Str(text.into())),
4113 Err(e) => err(Value::Str(e.into())),
4114 }
4115}
4116
4117fn some(v: Value) -> Value {
4118 Value::Variant { name: "Some".into(), args: vec![v] }
4119}
4120fn none() -> Value {
4121 Value::Variant { name: "None".into(), args: vec![] }
4122}
4123
4124fn expect_bytes(v: Option<&Value>) -> Result<&Vec<u8>, String> {
4125 match v {
4126 Some(Value::Bytes(b)) => Ok(b),
4127 Some(other) => Err(format!("expected Bytes arg, got {other:?}")),
4128 None => Err("missing argument".into()),
4129 }
4130}
4131
4132fn expect_kv_handle(v: Option<&Value>) -> Result<u64, String> {
4133 match v {
4134 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4135 Some(other) => Err(format!("expected Kv handle (Int), got {other:?}")),
4136 None => Err("missing Kv argument".into()),
4137 }
4138}
4139
4140fn expect_sql_handle(v: Option<&Value>) -> Result<u64, String> {
4141 match v {
4142 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4143 Some(other) => Err(format!("expected Db handle (Int), got {other:?}")),
4144 None => Err("missing Db argument".into()),
4145 }
4146}
4147
4148#[allow(dead_code)]
4149fn expect_str_list(v: Option<&Value>) -> Result<Vec<String>, String> {
4150 match v {
4151 Some(Value::List(items)) => items.iter().map(|x| match x {
4152 Value::Str(s) => Ok(s.to_string()),
4153 other => Err(format!("expected List[Str] element, got {other:?}")),
4154 }).collect(),
4155 Some(other) => Err(format!("expected List[Str], got {other:?}")),
4156 None => Err("missing List[Str] argument".into()),
4157 }
4158}
4159
4160fn expect_sql_params(v: Option<&Value>) -> Result<Vec<SqlParamValue>, String> {
4163 let items = match v {
4164 Some(Value::List(xs)) => xs,
4165 Some(other) => return Err(format!("expected List[SqlParam], got {other:?}")),
4166 None => return Err("missing params argument".into()),
4167 };
4168 items.iter().map(|item| {
4169 match item {
4170 Value::Variant { name, args } => match name.as_str() {
4171 "PStr" => match args.first() {
4172 Some(Value::Str(s)) => Ok(SqlParamValue::Text(s.to_string())),
4173 _ => Err("PStr requires a Str argument".into()),
4174 },
4175 "PInt" => match args.first() {
4176 Some(Value::Int(n)) => Ok(SqlParamValue::Integer(*n)),
4177 _ => Err("PInt requires an Int argument".into()),
4178 },
4179 "PFloat" => match args.first() {
4180 Some(Value::Float(f)) => Ok(SqlParamValue::Real(*f)),
4181 _ => Err("PFloat requires a Float argument".into()),
4182 },
4183 "PBool" => match args.first() {
4184 Some(Value::Bool(b)) => Ok(SqlParamValue::Bool(*b)),
4185 _ => Err("PBool requires a Bool argument".into()),
4186 },
4187 "PNull" => Ok(SqlParamValue::Null),
4188 other => Err(format!("unknown SqlParam constructor `{other}`")),
4189 },
4190 Value::Str(s) => Ok(SqlParamValue::Text(s.to_string())),
4192 other => Err(format!("expected SqlParam variant, got {other:?}")),
4193 }
4194 }).collect()
4195}
4196
4197fn sqlite_params(params: &[SqlParamValue]) -> Vec<rusqlite::types::Value> {
4199 params.iter().map(|p| match p {
4200 SqlParamValue::Text(s) => rusqlite::types::Value::Text(s.clone()),
4201 SqlParamValue::Integer(n) => rusqlite::types::Value::Integer(*n),
4202 SqlParamValue::Real(f) => rusqlite::types::Value::Real(*f),
4203 SqlParamValue::Bool(b) => rusqlite::types::Value::Integer(*b as i64),
4204 SqlParamValue::Null => rusqlite::types::Value::Null,
4205 }).collect()
4206}
4207
4208fn pg_param_refs(params: &[SqlParamValue]) -> Vec<Box<dyn postgres::types::ToSql + Sync>> {
4210 params.iter().map(|p| -> Box<dyn postgres::types::ToSql + Sync> {
4211 match p {
4212 SqlParamValue::Text(s) => Box::new(s.clone()),
4213 SqlParamValue::Integer(n) => Box::new(*n),
4214 SqlParamValue::Real(f) => Box::new(*f),
4215 SqlParamValue::Bool(b) => Box::new(*b),
4216 SqlParamValue::Null => Box::new(Option::<String>::None),
4217 }
4218 }).collect()
4219}
4220
4221fn sql_run_query_sqlite(
4223 conn: &rusqlite::Connection,
4224 stmt_str: &str,
4225 params: &[SqlParamValue],
4226) -> Value {
4227 let mut stmt = match conn.prepare(stmt_str) {
4228 Ok(s) => s,
4229 Err(e) => return err(sqlite_err_to_sql_error(e, "sql.query")),
4230 };
4231 let column_count = stmt.column_count();
4232 let column_names: Vec<String> = (0..column_count)
4233 .map(|i| stmt.column_name(i).unwrap_or("").to_string())
4234 .collect();
4235 let bound = sqlite_params(params);
4236 let bind: Vec<&dyn rusqlite::ToSql> = bound.iter()
4237 .map(|p| p as &dyn rusqlite::ToSql)
4238 .collect();
4239 let mut rows = match stmt.query(rusqlite::params_from_iter(bind.iter())) {
4240 Ok(r) => r,
4241 Err(e) => return err(sqlite_err_to_sql_error(e, "sql.query")),
4242 };
4243 let mut out: Vec<Value> = Vec::new();
4244 loop {
4245 let row = match rows.next() {
4246 Ok(Some(r)) => r,
4247 Ok(None) => break,
4248 Err(e) => return err(sqlite_err_to_sql_error(e, "sql.query")),
4249 };
4250 let mut rec = indexmap::IndexMap::new();
4251 for (i, name) in column_names.iter().enumerate() {
4252 let cell = match row.get_ref(i) {
4253 Ok(c) => sql_value_ref_to_lex(c),
4254 Err(e) => return err(sqlite_err_to_sql_error(e, &format!("sql.query: column {i}"))),
4255 };
4256 rec.insert(name.clone(), cell);
4257 }
4258 out.push(Value::record_dynamic(rec));
4259 }
4260 ok(Value::List(out.into()))
4261}
4262
4263fn sql_run_query_pg(
4265 client: &mut postgres::Client,
4266 stmt_str: &str,
4267 params: &[SqlParamValue],
4268) -> Value {
4269 let pg = pg_param_refs(params);
4270 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
4271 pg.iter().map(|b| b.as_ref()).collect();
4272 let rows = match client.query(stmt_str, &refs) {
4273 Ok(r) => r,
4274 Err(e) => return err(pg_err_to_sql_error(e, "sql.query")),
4275 };
4276 let out: std::collections::VecDeque<Value> = rows.iter().map(|row| {
4277 Value::record_dynamic(pg_row_to_lex_record(row))
4278 }).collect();
4279 ok(Value::List(out))
4280}
4281
4282fn pg_row_to_lex_record(row: &postgres::Row) -> indexmap::IndexMap<String, Value> {
4284 use postgres::types::Type;
4285 let mut rec = indexmap::IndexMap::new();
4286 for (i, col) in row.columns().iter().enumerate() {
4287 let ty = col.type_();
4288 let val = if *ty == Type::INT2 || *ty == Type::INT4 || *ty == Type::INT8 {
4289 row.get::<_, Option<i64>>(i).map(Value::Int).unwrap_or(Value::Unit)
4290 } else if *ty == Type::FLOAT4 || *ty == Type::FLOAT8 {
4291 row.get::<_, Option<f64>>(i).map(Value::Float).unwrap_or(Value::Unit)
4292 } else if *ty == Type::BOOL {
4293 row.get::<_, Option<bool>>(i).map(Value::Bool).unwrap_or(Value::Unit)
4294 } else if *ty == Type::BYTEA {
4295 row.get::<_, Option<Vec<u8>>>(i).map(Value::Bytes).unwrap_or(Value::Unit)
4296 } else {
4297 row.get::<_, Option<String>>(i).map(|s| Value::Str(s.into())).unwrap_or(Value::Unit)
4298 };
4299 rec.insert(col.name().to_string(), val);
4300 }
4301 rec
4302}
4303
4304fn sql_get_col<F>(args: &[Value], convert: F) -> Result<Value, String>
4306where
4307 F: Fn(&Value) -> Option<Value>,
4308{
4309 let row = args.first().ok_or("sql.get_*: missing row argument")?;
4310 let col = match args.get(1) {
4311 Some(Value::Str(s)) => s.as_str(),
4312 Some(other) => return Err(format!("sql.get_*: column name must be Str, got {other:?}")),
4313 None => return Err("sql.get_*: missing column name argument".into()),
4314 };
4315 let cell = match row {
4316 Value::Record { fields: rec, .. } => rec.get(col).cloned(),
4317 other => return Err(format!("sql.get_*: row must be a Record, got {other:?}")),
4318 };
4319 Ok(match cell.and_then(|v| convert(&v)) {
4320 Some(v) => Value::Variant { name: "Some".into(), args: vec![v] },
4321 None => Value::Variant { name: "None".into(), args: vec![] },
4322 })
4323}
4324
4325fn sql_value_ref_to_lex(v: rusqlite::types::ValueRef<'_>) -> Value {
4326 use rusqlite::types::ValueRef;
4327 match v {
4328 ValueRef::Null => Value::Unit,
4329 ValueRef::Integer(n) => Value::Int(n),
4330 ValueRef::Real(f) => Value::Float(f),
4331 ValueRef::Text(s) => Value::Str(String::from_utf8_lossy(s).into_owned().into()),
4332 ValueRef::Blob(b) => Value::Bytes(b.to_vec()),
4333 }
4334}
4335
4336#[derive(Clone, Copy, PartialEq, PartialOrd)]
4339enum LogLevel { Debug, Info, Warn, Error }
4340
4341#[derive(Clone, Copy, PartialEq)]
4342enum LogFormat { Text, Json }
4343
4344#[derive(Clone)]
4345enum LogSink {
4346 Stderr,
4347 File(std::sync::Arc<Mutex<std::fs::File>>),
4348}
4349
4350struct LogState {
4351 level: LogLevel,
4352 format: LogFormat,
4353 sink: LogSink,
4354}
4355
4356fn log_state() -> &'static Mutex<LogState> {
4357 static STATE: OnceLock<Mutex<LogState>> = OnceLock::new();
4358 STATE.get_or_init(|| Mutex::new(LogState {
4359 level: LogLevel::Info,
4360 format: LogFormat::Text,
4361 sink: LogSink::Stderr,
4362 }))
4363}
4364
4365fn parse_log_level(s: &str) -> Option<LogLevel> {
4366 match s {
4367 "debug" => Some(LogLevel::Debug),
4368 "info" => Some(LogLevel::Info),
4369 "warn" => Some(LogLevel::Warn),
4370 "error" => Some(LogLevel::Error),
4371 _ => None,
4372 }
4373}
4374
4375fn level_label(l: LogLevel) -> &'static str {
4376 match l {
4377 LogLevel::Debug => "debug",
4378 LogLevel::Info => "info",
4379 LogLevel::Warn => "warn",
4380 LogLevel::Error => "error",
4381 }
4382}
4383
4384fn emit_log(level: LogLevel, msg: &str) {
4385 let state = log_state().lock().unwrap();
4386 if level < state.level {
4387 return;
4388 }
4389 let ts = chrono::Utc::now().to_rfc3339();
4390 let line = match state.format {
4391 LogFormat::Text => format!("[{}] {}: {}\n", ts, level_label(level), msg),
4392 LogFormat::Json => {
4393 let escaped = msg
4397 .replace('\\', "\\\\")
4398 .replace('"', "\\\"")
4399 .replace('\n', "\\n")
4400 .replace('\r', "\\r");
4401 format!(
4402 "{{\"ts\":\"{ts}\",\"level\":\"{}\",\"msg\":\"{escaped}\"}}\n",
4403 level_label(level),
4404 )
4405 }
4406 };
4407 let sink = state.sink.clone();
4408 drop(state);
4409 match sink {
4410 LogSink::Stderr => {
4411 use std::io::Write;
4412 let _ = std::io::stderr().write_all(line.as_bytes());
4413 }
4414 LogSink::File(f) => {
4415 use std::io::Write;
4416 if let Ok(mut g) = f.lock() {
4417 let _ = g.write_all(line.as_bytes());
4418 }
4419 }
4420 }
4421}
4422
4423pub(crate) struct ProcessState {
4424 child: std::process::Child,
4425 stdout: Option<std::io::BufReader<std::process::ChildStdout>>,
4426 stderr: Option<std::io::BufReader<std::process::ChildStderr>>,
4427}
4428
4429fn process_registry() -> &'static Mutex<ProcessRegistry> {
4443 static REGISTRY: OnceLock<Mutex<ProcessRegistry>> = OnceLock::new();
4444 REGISTRY.get_or_init(|| Mutex::new(ProcessRegistry::with_capacity(MAX_PROCESS_HANDLES)))
4445}
4446
4447const MAX_PROCESS_HANDLES: usize = 256;
4448
4449type SharedProcessState = Arc<Mutex<ProcessState>>;
4450
4451pub(crate) struct ProcessRegistry {
4452 entries: indexmap::IndexMap<u64, SharedProcessState>,
4453 cap: usize,
4454}
4455
4456impl ProcessRegistry {
4457 pub(crate) fn with_capacity(cap: usize) -> Self {
4458 Self { entries: indexmap::IndexMap::new(), cap }
4459 }
4460
4461 pub(crate) fn insert(&mut self, handle: u64, state: ProcessState) {
4465 if self.entries.len() >= self.cap {
4466 self.entries.shift_remove_index(0);
4467 }
4468 self.entries.insert(handle, Arc::new(Mutex::new(state)));
4469 }
4470
4471 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<SharedProcessState> {
4475 let idx = self.entries.get_index_of(&handle)?;
4476 self.entries.move_index(idx, self.entries.len() - 1);
4477 self.entries.get(&handle).cloned()
4478 }
4479
4480 pub(crate) fn remove(&mut self, handle: u64) {
4485 self.entries.shift_remove(&handle);
4486 }
4487
4488 #[cfg(test)]
4489 pub(crate) fn len(&self) -> usize { self.entries.len() }
4490}
4491
4492fn next_process_handle() -> u64 {
4493 static COUNTER: AtomicU64 = AtomicU64::new(1);
4494 COUNTER.fetch_add(1, Ordering::SeqCst)
4495}
4496
4497#[cfg(all(test, unix))]
4498mod process_registry_tests {
4499 use super::{ProcessRegistry, ProcessState};
4500
4501 fn fresh_state() -> ProcessState {
4505 let child = std::process::Command::new("true")
4506 .stdout(std::process::Stdio::null())
4507 .stderr(std::process::Stdio::null())
4508 .spawn()
4509 .expect("spawn `true`");
4510 ProcessState { child, stdout: None, stderr: None }
4511 }
4512
4513 #[test]
4514 fn insert_and_get_round_trip() {
4515 let mut r = ProcessRegistry::with_capacity(4);
4516 r.insert(1, fresh_state());
4517 assert!(r.touch_get(1).is_some());
4518 assert!(r.touch_get(2).is_none());
4519 }
4520
4521 #[test]
4522 fn touch_get_returns_distinct_arcs_for_distinct_handles() {
4523 let mut r = ProcessRegistry::with_capacity(4);
4524 r.insert(1, fresh_state());
4525 r.insert(2, fresh_state());
4526 let a = r.touch_get(1).unwrap();
4527 let b = r.touch_get(2).unwrap();
4528 assert!(!std::sync::Arc::ptr_eq(&a, &b));
4530 }
4531
4532 #[test]
4533 fn cap_evicts_lru_on_overflow() {
4534 let mut r = ProcessRegistry::with_capacity(2);
4535 r.insert(1, fresh_state());
4536 r.insert(2, fresh_state());
4537 let _ = r.touch_get(1);
4538 r.insert(3, fresh_state());
4539 assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
4540 assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
4541 assert!(r.touch_get(3).is_some(), "3 just inserted, should survive");
4542 assert_eq!(r.len(), 2);
4543 }
4544
4545 #[test]
4546 fn cap_with_no_touches_evicts_in_insertion_order() {
4547 let mut r = ProcessRegistry::with_capacity(2);
4548 r.insert(10, fresh_state());
4549 r.insert(20, fresh_state());
4550 r.insert(30, fresh_state());
4551 assert!(r.touch_get(10).is_none());
4552 assert!(r.touch_get(20).is_some());
4553 assert!(r.touch_get(30).is_some());
4554 }
4555
4556 #[test]
4557 fn remove_drops_entry() {
4558 let mut r = ProcessRegistry::with_capacity(4);
4559 r.insert(1, fresh_state());
4560 r.remove(1);
4561 assert!(r.touch_get(1).is_none());
4562 assert_eq!(r.len(), 0);
4563 }
4564
4565 #[test]
4566 fn many_inserts_stay_bounded_at_cap() {
4567 let cap = 8;
4568 let mut r = ProcessRegistry::with_capacity(cap);
4569 for i in 0..(cap as u64 * 3) {
4570 r.insert(i, fresh_state());
4571 assert!(r.len() <= cap);
4572 }
4573 assert_eq!(r.len(), cap);
4574 }
4575
4576 #[test]
4577 fn outstanding_arc_outlives_remove() {
4578 let mut r = ProcessRegistry::with_capacity(4);
4582 r.insert(1, fresh_state());
4583 let arc = r.touch_get(1).expect("entry exists");
4584 r.remove(1);
4585 assert!(r.touch_get(1).is_none());
4587 let _state = arc.lock().unwrap();
4588 }
4589}
4590
4591fn expect_process_handle(v: Option<&Value>) -> Result<u64, String> {
4592 match v {
4593 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4594 Some(other) => Err(format!("expected ProcessHandle (Int), got {other:?}")),
4595 None => Err("missing ProcessHandle argument".into()),
4596 }
4597}
4598
4599fn kv_registry() -> &'static Mutex<KvRegistry> {
4611 static REGISTRY: OnceLock<Mutex<KvRegistry>> = OnceLock::new();
4612 REGISTRY.get_or_init(|| Mutex::new(KvRegistry::with_capacity(MAX_KV_HANDLES)))
4613}
4614
4615const MAX_KV_HANDLES: usize = 256;
4621
4622pub(crate) struct KvRegistry {
4627 entries: indexmap::IndexMap<u64, sled::Db>,
4628 cap: usize,
4629}
4630
4631impl KvRegistry {
4632 pub(crate) fn with_capacity(cap: usize) -> Self {
4633 Self { entries: indexmap::IndexMap::new(), cap }
4634 }
4635
4636 pub(crate) fn insert(&mut self, handle: u64, db: sled::Db) {
4639 if self.entries.len() >= self.cap {
4640 self.entries.shift_remove_index(0);
4641 }
4642 self.entries.insert(handle, db);
4643 }
4644
4645 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<&sled::Db> {
4647 let idx = self.entries.get_index_of(&handle)?;
4648 self.entries.move_index(idx, self.entries.len() - 1);
4649 self.entries.get(&handle)
4650 }
4651
4652 pub(crate) fn remove(&mut self, handle: u64) {
4654 self.entries.shift_remove(&handle);
4655 }
4656
4657 #[cfg(test)]
4658 pub(crate) fn len(&self) -> usize { self.entries.len() }
4659}
4660
4661fn next_kv_handle() -> u64 {
4662 static COUNTER: AtomicU64 = AtomicU64::new(1);
4663 COUNTER.fetch_add(1, Ordering::SeqCst)
4664}
4665
4666struct RedisEntry {
4681 url: String,
4682 conn: redis::Connection,
4683}
4684
4685struct RedisRegistry {
4686 entries: indexmap::IndexMap<u64, RedisEntry>,
4687 cap: usize,
4688}
4689
4690impl RedisRegistry {
4691 fn with_capacity(cap: usize) -> Self {
4692 Self { entries: indexmap::IndexMap::new(), cap }
4693 }
4694
4695 fn insert(&mut self, handle: u64, entry: RedisEntry) {
4696 if self.entries.len() >= self.cap {
4697 self.entries.shift_remove_index(0);
4698 }
4699 self.entries.insert(handle, entry);
4700 }
4701
4702 fn touch_get_mut(&mut self, handle: u64) -> Option<&mut RedisEntry> {
4703 let idx = self.entries.get_index_of(&handle)?;
4704 self.entries.move_index(idx, self.entries.len() - 1);
4705 self.entries.get_mut(&handle)
4706 }
4707
4708 fn get_url(&self, handle: u64) -> Option<String> {
4711 self.entries.get(&handle).map(|e| e.url.clone())
4712 }
4713
4714 fn remove(&mut self, handle: u64) {
4715 self.entries.shift_remove(&handle);
4716 }
4717}
4718
4719fn redis_registry() -> &'static Mutex<RedisRegistry> {
4720 static REGISTRY: OnceLock<Mutex<RedisRegistry>> = OnceLock::new();
4721 REGISTRY.get_or_init(|| Mutex::new(RedisRegistry::with_capacity(MAX_REDIS_HANDLES)))
4722}
4723
4724const MAX_REDIS_HANDLES: usize = 256;
4725
4726fn next_redis_handle() -> u64 {
4727 static COUNTER: AtomicU64 = AtomicU64::new(1);
4728 COUNTER.fetch_add(1, Ordering::SeqCst)
4729}
4730
4731fn expect_redis_handle(v: Option<&Value>) -> Result<u64, String> {
4732 match v {
4733 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4734 Some(other) => Err(format!("expected ConnRedis (Int), got {other:?}")),
4735 None => Err("missing ConnRedis argument".into()),
4736 }
4737}
4738
4739fn sql_registry() -> &'static Mutex<SqlRegistry> {
4746 static REGISTRY: OnceLock<Mutex<SqlRegistry>> = OnceLock::new();
4747 REGISTRY.get_or_init(|| Mutex::new(SqlRegistry::with_capacity(MAX_SQL_HANDLES)))
4748}
4749
4750const MAX_SQL_HANDLES: usize = 256;
4751
4752const CURSOR_CHANNEL_CAPACITY: usize = 64;
4775const MAX_CURSOR_HANDLES: usize = 256;
4776
4777type CursorReceiver = std::sync::mpsc::Receiver<Result<Value, String>>;
4778
4779pub(crate) struct CursorRegistry {
4780 entries: indexmap::IndexMap<u64, Arc<Mutex<CursorReceiver>>>,
4785 cap: usize,
4786}
4787
4788impl CursorRegistry {
4789 pub(crate) fn with_capacity(cap: usize) -> Self {
4790 Self { entries: indexmap::IndexMap::new(), cap }
4791 }
4792
4793 pub(crate) fn insert(&mut self, handle: u64, rx: CursorReceiver) {
4794 if self.entries.len() >= self.cap {
4795 self.entries.shift_remove_index(0);
4796 }
4797 self.entries.insert(handle, Arc::new(Mutex::new(rx)));
4798 }
4799
4800 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<Arc<Mutex<CursorReceiver>>> {
4801 let idx = self.entries.get_index_of(&handle)?;
4802 self.entries.move_index(idx, self.entries.len() - 1);
4803 self.entries.get(&handle).cloned()
4804 }
4805
4806 pub(crate) fn remove(&mut self, handle: u64) {
4807 self.entries.shift_remove(&handle);
4808 }
4809}
4810
4811fn cursor_registry() -> &'static Mutex<CursorRegistry> {
4812 static REGISTRY: OnceLock<Mutex<CursorRegistry>> = OnceLock::new();
4813 REGISTRY.get_or_init(|| Mutex::new(CursorRegistry::with_capacity(MAX_CURSOR_HANDLES)))
4814}
4815
4816fn next_cursor_handle() -> u64 {
4817 static COUNTER: AtomicU64 = AtomicU64::new(1);
4818 COUNTER.fetch_add(1, Ordering::SeqCst)
4819}
4820
4821fn sqlite_cursor_producer(
4827 conn_arc: Arc<Mutex<SqlConn>>,
4828 stmt_str: String,
4829 params: Vec<SqlParamValue>,
4830 sender: std::sync::mpsc::SyncSender<Result<Value, String>>,
4831) {
4832 let mut conn_guard = match conn_arc.lock() {
4833 Ok(g) => g,
4834 Err(p) => p.into_inner(),
4835 };
4836 let SqlConn::Sqlite(c) = &mut *conn_guard else {
4837 let _ = sender.send(Err("sqlite_cursor_producer called on non-sqlite conn".into()));
4838 return;
4839 };
4840 let mut stmt = match c.prepare(&stmt_str) {
4841 Ok(s) => s,
4842 Err(e) => { let _ = sender.send(Err(format!("prepare: {e}"))); return; }
4843 };
4844 let column_count = stmt.column_count();
4845 let column_names: Vec<String> = (0..column_count)
4846 .map(|i| stmt.column_name(i).unwrap_or("").to_string())
4847 .collect();
4848 let bound = sqlite_params(¶ms);
4849 let bind: Vec<&dyn rusqlite::ToSql> =
4850 bound.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
4851 let mut rows = match stmt.query(rusqlite::params_from_iter(bind.iter())) {
4852 Ok(r) => r,
4853 Err(e) => { let _ = sender.send(Err(format!("query: {e}"))); return; }
4854 };
4855 loop {
4856 match rows.next() {
4857 Ok(None) => break,
4858 Err(e) => {
4859 let _ = sender.send(Err(format!("row: {e}")));
4860 break;
4861 }
4862 Ok(Some(row)) => {
4863 let mut rec = indexmap::IndexMap::new();
4864 for (i, name) in column_names.iter().enumerate() {
4865 let val = match row.get_ref(i) {
4866 Ok(vr) => sql_value_ref_to_lex(vr),
4867 Err(_) => Value::Unit,
4868 };
4869 rec.insert(name.clone(), val);
4870 }
4871 if sender.send(Ok(Value::record_dynamic(rec))).is_err() {
4872 break;
4873 }
4874 }
4875 }
4876 }
4877}
4878
4879fn pg_cursor_producer(
4883 conn_arc: Arc<Mutex<SqlConn>>,
4884 stmt_str: String,
4885 params: Vec<SqlParamValue>,
4886 sender: std::sync::mpsc::SyncSender<Result<Value, String>>,
4887) {
4888 let mut conn_guard = match conn_arc.lock() {
4889 Ok(g) => g,
4890 Err(p) => p.into_inner(),
4891 };
4892 let SqlConn::Postgres(c) = &mut *conn_guard else {
4893 let _ = sender.send(Err("pg_cursor_producer called on non-postgres conn".into()));
4894 return;
4895 };
4896 let pg = pg_param_refs(¶ms);
4897 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
4898 pg.iter().map(|b| b.as_ref()).collect();
4899 let mut tx = match c.transaction() {
4900 Ok(t) => t,
4901 Err(e) => { let _ = sender.send(Err(format!("begin: {e}"))); return; }
4902 };
4903 let cur_name = format!("__lex_cur_{}", next_cursor_handle());
4906 if let Err(e) = tx.execute(
4907 &format!("DECLARE \"{cur_name}\" NO SCROLL CURSOR FOR {stmt_str}"),
4908 &refs,
4909 ) {
4910 let _ = sender.send(Err(format!("declare: {e}")));
4911 return;
4912 }
4913 let fetch_sql = format!("FETCH 64 FROM \"{cur_name}\"");
4914 'outer: loop {
4915 let batch = match tx.query(&fetch_sql, &[]) {
4916 Ok(r) => r,
4917 Err(e) => { let _ = sender.send(Err(format!("fetch: {e}"))); break; }
4918 };
4919 if batch.is_empty() {
4920 break;
4921 }
4922 for row in batch.iter() {
4923 let rec = pg_row_to_lex_record(row);
4924 if sender.send(Ok(Value::record_dynamic(rec))).is_err() {
4925 break 'outer;
4926 }
4927 }
4928 }
4929 let _ = tx.execute(&format!("CLOSE \"{cur_name}\""), &[]);
4930 let _ = tx.commit();
4931}
4932
4933#[derive(Debug, Clone)]
4935enum SqlParamValue {
4936 Text(String),
4937 Integer(i64),
4938 Real(f64),
4939 Bool(bool),
4940 Null,
4941}
4942
4943pub(crate) enum SqlConn {
4945 Sqlite(rusqlite::Connection),
4946 Postgres(postgres::Client),
4947}
4948
4949type SharedConn = Arc<Mutex<SqlConn>>;
4950
4951pub(crate) struct SqlRegistry {
4952 entries: indexmap::IndexMap<u64, SharedConn>,
4953 cap: usize,
4954}
4955
4956impl SqlRegistry {
4957 pub(crate) fn with_capacity(cap: usize) -> Self {
4958 Self { entries: indexmap::IndexMap::new(), cap }
4959 }
4960
4961 pub(crate) fn insert(&mut self, handle: u64, conn: SqlConn) {
4962 if self.entries.len() >= self.cap {
4963 self.entries.shift_remove_index(0);
4964 }
4965 self.entries.insert(handle, Arc::new(Mutex::new(conn)));
4966 }
4967
4968 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<SharedConn> {
4972 let idx = self.entries.get_index_of(&handle)?;
4973 self.entries.move_index(idx, self.entries.len() - 1);
4974 self.entries.get(&handle).cloned()
4975 }
4976
4977 pub(crate) fn remove(&mut self, handle: u64) {
4978 self.entries.shift_remove(&handle);
4979 }
4980
4981 #[cfg(test)]
4982 pub(crate) fn len(&self) -> usize { self.entries.len() }
4983}
4984
4985fn next_sql_handle() -> u64 {
4986 static COUNTER: AtomicU64 = AtomicU64::new(1);
4987 COUNTER.fetch_add(1, Ordering::SeqCst)
4988}
4989
4990#[cfg(test)]
4991mod sql_registry_tests {
4992 use super::{SqlConn, SqlRegistry};
4993
4994 fn fresh() -> SqlConn {
4995 SqlConn::Sqlite(rusqlite::Connection::open_in_memory().expect("open in-memory sqlite"))
4996 }
4997
4998 #[test]
4999 fn insert_and_get_round_trip() {
5000 let mut r = SqlRegistry::with_capacity(4);
5001 r.insert(1, fresh());
5002 assert!(r.touch_get(1).is_some());
5003 assert!(r.touch_get(2).is_none());
5004 }
5005
5006 #[test]
5007 fn cap_evicts_lru_on_overflow() {
5008 let mut r = SqlRegistry::with_capacity(2);
5009 r.insert(1, fresh());
5010 r.insert(2, fresh());
5011 let _ = r.touch_get(1);
5012 r.insert(3, fresh());
5013 assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
5014 assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
5015 assert!(r.touch_get(3).is_some(), "3 just inserted");
5016 assert_eq!(r.len(), 2);
5017 }
5018
5019 #[test]
5020 fn remove_drops_entry() {
5021 let mut r = SqlRegistry::with_capacity(4);
5022 r.insert(1, fresh());
5023 r.remove(1);
5024 assert!(r.touch_get(1).is_none());
5025 assert_eq!(r.len(), 0);
5026 }
5027
5028 #[test]
5029 fn many_inserts_stay_bounded_at_cap() {
5030 let cap = 8;
5031 let mut r = SqlRegistry::with_capacity(cap);
5032 for i in 0..(cap as u64 * 3) {
5033 r.insert(i, fresh());
5034 assert!(r.len() <= cap);
5035 }
5036 assert_eq!(r.len(), cap);
5037 }
5038}
5039
5040#[cfg(test)]
5041mod kv_registry_tests {
5042 use super::KvRegistry;
5043
5044 fn fresh_db(tag: &str) -> sled::Db {
5047 let dir = std::env::temp_dir().join(format!(
5048 "lex-kv-reg-{}-{}-{}",
5049 std::process::id(),
5050 tag,
5051 std::time::SystemTime::now()
5052 .duration_since(std::time::UNIX_EPOCH)
5053 .unwrap()
5054 .as_nanos()
5055 ));
5056 sled::open(&dir).expect("sled open")
5057 }
5058
5059 #[test]
5060 fn insert_and_get_round_trip() {
5061 let mut r = KvRegistry::with_capacity(4);
5062 r.insert(1, fresh_db("a"));
5063 assert!(r.touch_get(1).is_some());
5064 assert!(r.touch_get(2).is_none());
5065 }
5066
5067 #[test]
5068 fn cap_evicts_lru_on_overflow() {
5069 let mut r = KvRegistry::with_capacity(2);
5071 r.insert(1, fresh_db("c1"));
5072 r.insert(2, fresh_db("c2"));
5073 let _ = r.touch_get(1);
5074 r.insert(3, fresh_db("c3"));
5075 assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
5076 assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
5077 assert!(r.touch_get(3).is_some(), "3 just inserted, should survive");
5078 assert_eq!(r.len(), 2);
5079 }
5080
5081 #[test]
5082 fn cap_with_no_touches_evicts_in_insertion_order() {
5083 let mut r = KvRegistry::with_capacity(2);
5085 r.insert(10, fresh_db("f1"));
5086 r.insert(20, fresh_db("f2"));
5087 r.insert(30, fresh_db("f3"));
5088 assert!(r.touch_get(10).is_none());
5089 assert!(r.touch_get(20).is_some());
5090 assert!(r.touch_get(30).is_some());
5091 }
5092
5093 #[test]
5094 fn remove_drops_entry() {
5095 let mut r = KvRegistry::with_capacity(4);
5096 r.insert(1, fresh_db("r1"));
5097 r.remove(1);
5098 assert!(r.touch_get(1).is_none());
5099 assert_eq!(r.len(), 0);
5100 }
5101
5102 #[test]
5103 fn remove_unknown_handle_is_noop() {
5104 let mut r = KvRegistry::with_capacity(4);
5105 r.insert(1, fresh_db("u1"));
5106 r.remove(999);
5107 assert!(r.touch_get(1).is_some());
5108 }
5109
5110 #[test]
5111 fn many_inserts_stay_bounded_at_cap() {
5112 let cap = 8;
5115 let mut r = KvRegistry::with_capacity(cap);
5116 for i in 0..(cap as u64 * 3) {
5117 r.insert(i, fresh_db(&format!("b{i}")));
5118 assert!(r.len() <= cap);
5119 }
5120 assert_eq!(r.len(), cap);
5121 }
5122}
5123
5124#[cfg(test)]
5131mod unpack_response_tests {
5132 use super::*;
5133 use std::sync::Arc;
5134 use indexmap::IndexMap;
5135 use lex_bytecode::{Const, Op, Program, Value};
5136 use lex_bytecode::program::{Function, ZERO_BODY_HASH};
5137 use lex_bytecode::vm::Vm;
5138
5139 fn build_arena_response_program() -> Arc<Program> {
5144 let constants = vec![
5145 Const::FieldName("status".into()), Const::FieldName("body".into()), Const::Int(200), Const::VariantName("BodyStr".into()), Const::Str("hello".into()), ];
5151 let mut function_names = IndexMap::new();
5152 function_names.insert("handler".to_string(), 0);
5153 Arc::new(Program {
5154 constants,
5155 functions: vec![Function {
5156 name: "handler".into(),
5157 arity: 0,
5158 locals_count: 0,
5159 code: vec![
5160 Op::PushConst(2), Op::PushConst(4), Op::MakeVariant { name_idx: 3, arity: 1 }, Op::AllocArenaRecord { shape_idx: 0, field_count: 2 }, Op::Return,
5165 ],
5166 effects: vec![],
5167 body_hash: ZERO_BODY_HASH,
5168 refinements: vec![],
5169 field_ic_sites: 0,
5170 }],
5171 function_names,
5172 module_aliases: IndexMap::new(),
5173 entry: Some(0),
5174 record_shapes: vec![vec![0, 1]], })
5176 }
5177
5178 #[test]
5184 fn unpack_response_reads_arena_record_via_slab() {
5185 let p = build_arena_response_program();
5186 let mut vm = Vm::new(&p);
5187 let scope = vm.enter_request_scope();
5188
5189 let resp = vm.invoke(p.function_names["handler"], vec![]).unwrap();
5190 assert!(matches!(resp, Value::ArenaRecord { .. }),
5193 "expected ArenaRecord (slab path), got {resp:?}");
5194
5195 let (status, body, headers) = unpack_response(&mut vm, &resp);
5196 vm.exit_request_scope(scope);
5197
5198 assert_eq!(status, 200);
5199 assert!(headers.is_empty());
5200 match body {
5201 ResponseBodyOut::Str(s) => assert_eq!(s, "hello"),
5202 _ => panic!("expected BodyStr"),
5203 }
5204 }
5205
5206 #[test]
5211 fn unpack_response_reads_heap_record() {
5212 let p = build_arena_response_program();
5213 let mut vm = Vm::new(&p);
5214
5215 let resp = vm.invoke(p.function_names["handler"], vec![]).unwrap();
5217 assert!(matches!(resp, Value::Record { .. }),
5218 "expected heap Record (fallback path), got {resp:?}");
5219
5220 let (status, body, headers) = unpack_response(&mut vm, &resp);
5221 assert_eq!(status, 200);
5222 assert!(headers.is_empty());
5223 match body {
5224 ResponseBodyOut::Str(s) => assert_eq!(s, "hello"),
5225 _ => panic!("expected BodyStr"),
5226 }
5227 }
5228
5229 #[test]
5232 fn unpack_response_falls_back_to_500_on_non_record() {
5233 let p = build_arena_response_program();
5234 let mut vm = Vm::new(&p);
5235 let v = Value::Int(7);
5236 let (status, _body, _headers) = unpack_response(&mut vm, &v);
5237 assert_eq!(status, 500);
5238 }
5239}