1fn os_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
2 let operation = operation.strip_prefix("os/").unwrap_or(operation);
3 let operation = operation
4 .strip_prefix("std.native.OS/")
5 .unwrap_or(operation);
6 let process_operation = operation.strip_prefix("std.native.Process/");
7 let operation = match process_operation.unwrap_or(operation) {
8 "alive?" if process_operation.is_some() => "process-alive?",
9 "write" if process_operation.is_some() => "process-write",
10 "close-input" if process_operation.is_some() => "process-close-input",
11 "stdout" if process_operation.is_some() => "process-stdout",
12 "stderr" if process_operation.is_some() => "process-stderr",
13 "stdout-stream" if process_operation.is_some() => "process-stdout-stream",
14 "stderr-stream" if process_operation.is_some() => "process-stderr-stream",
15 "wait" if process_operation.is_some() => "process-wait",
16 "kill" if process_operation.is_some() => "process-kill",
17 value => value,
18 };
19 match operation {
20 "time-ms" => {
21 if !values.is_empty() {
22 return Err("os/time-ms expects no arguments".into());
23 }
24 return Ok(Value::Number(crate::clock::time_ms()));
25 }
26 "time-ns" => {
27 if !values.is_empty() {
28 return Err("os/time-ns expects no arguments".into());
29 }
30 return Ok(Value::Number(crate::clock::time_ns()));
31 }
32 "platform" => {
33 if !values.is_empty() {
34 return Err("os/platform expects no arguments".into());
35 }
36 let platform = if cfg!(target_os = "linux") {
37 "linux"
38 } else if cfg!(target_os = "macos") {
39 "macos"
40 } else if cfg!(target_os = "windows") {
41 "windows"
42 } else {
43 "unknown"
44 };
45 return Ok(Value::Keyword(platform.into()));
46 }
47 "arch" => {
48 if !values.is_empty() {
49 return Err("os/arch expects no arguments".into());
50 }
51 let arch = match std::env::consts::ARCH {
52 "x86_64" => "x86-64",
53 value => value,
54 };
55 return Ok(Value::Keyword(arch.into()));
56 }
57 "cwd" => {
58 if !values.is_empty() {
59 return Err("os/cwd expects no arguments".into());
60 }
61 #[cfg(target_arch = "wasm32")]
62 return Ok(Value::String("/".into()));
63 #[cfg(not(target_arch = "wasm32"))]
64 return std::env::current_dir()
65 .map(|path| Value::String(path.to_string_lossy().into_owned()))
66 .map_err(|error| format!("os/cwd failed: {error}"));
67 }
68 "env" => {
69 if !values.is_empty() {
70 return Err("os/env expects no arguments".into());
71 }
72 #[cfg(target_arch = "wasm32")]
73 return Ok(Value::Map(PMap::new()));
74 #[cfg(not(target_arch = "wasm32"))]
75 return Ok(Value::Map(PMap::from_iter(
76 std::env::vars().map(|(key, value)| (Value::String(key), Value::String(value))),
77 )));
78 }
79 "getenv" => {
80 if values.len() != 1 {
81 return Err("os/getenv expects a name".into());
82 }
83 let Value::String(name) = &values[0] else {
84 return Err("os/getenv expects a string".into());
85 };
86 #[cfg(target_arch = "wasm32")]
87 {
88 let _ = name;
89 return Ok(Value::Nil);
90 }
91 #[cfg(not(target_arch = "wasm32"))]
92 return Ok(std::env::var(name).map(Value::String).unwrap_or(Value::Nil));
93 }
94 "process?" => {
95 if values.len() != 1 {
96 return Err("os/process? expects one argument".into());
97 }
98 let value = values[0].clone();
99 #[cfg(not(target_arch = "wasm32"))]
100 return Ok(Value::Bool(crate::native_process::is_process(&value)));
101 #[cfg(target_arch = "wasm32")]
102 return Ok(Value::Bool(false));
103 }
104 _ => {}
105 }
106 require_process_access(&format!("os/{operation}"))?;
107 #[cfg(target_arch = "wasm32")]
108 return Err(format!("os/{operation} is unsupported on wasm"));
109 #[cfg(not(target_arch = "wasm32"))]
110 match operation {
111 "spawn" => {
112 if !(1..=2).contains(&values.len()) {
113 return Err("os/spawn expects argv and optional options".into());
114 }
115 let argv = iterator_values(values[0].clone())?
116 .into_iter()
117 .map(|value| match value {
118 Value::String(value) => Ok(value),
119 _ => Err("os/spawn argv must contain strings".to_owned()),
120 })
121 .collect::<Result<Vec<_>, _>>()?;
122 let mut cwd = None;
123 let mut environment = Vec::new();
124 if values.len() == 2 {
125 let options = values[1].clone();
126 for (key, value) in map_entries(&options)
127 .ok_or_else(|| "os/spawn options must be a map".to_owned())?
128 {
129 match (key, value) {
130 (Value::Keyword(key), Value::String(value)) if key.as_str() == "cwd" => {
131 cwd = Some(value);
132 }
133 (Value::Keyword(key), value) if key.as_str() == "env" => {
134 for (name, value) in map_entries(&value)
135 .ok_or_else(|| "os/spawn :env must be a map".to_owned())?
136 {
137 let (Value::String(name), Value::String(value)) = (name, value)
138 else {
139 return Err("os/spawn :env must contain string pairs".into());
140 };
141 environment.push((name, value));
142 }
143 }
144 _ => {}
145 }
146 }
147 }
148 crate::native_process::spawn(&argv, cwd.as_deref(), &environment)
149 }
150 method @ ("process-alive?"
151 | "process-close-input"
152 | "process-stdout"
153 | "process-stderr"
154 | "process-wait"
155 | "process-kill") => {
156 if values.len() != 1 {
157 return Err(format!("os/{method} expects a process"));
158 }
159 let process = values[0].clone();
160 match method {
161 "process-alive?" => crate::native_process::alive(&process).map(Value::Bool),
162 "process-close-input" => {
163 crate::native_process::close_input(&process).map(|()| Value::Nil)
164 }
165 "process-stdout" => {
166 crate::native_process::promise(&process, "stdout").map(Value::Promise)
167 }
168 "process-stderr" => {
169 crate::native_process::promise(&process, "stderr").map(Value::Promise)
170 }
171 "process-wait" => {
172 crate::native_process::promise(&process, "wait").map(Value::Promise)
173 }
174 "process-kill" => crate::native_process::kill(&process).map(|()| process),
175 _ => unreachable!(),
176 }
177 }
178 method @ ("process-stdout-stream" | "process-stderr-stream") => {
179 if values.len() != 1 {
180 return Err(format!("os/{method} expects a process"));
181 }
182 let process = values[0].clone();
183 let kind = if method == "process-stderr-stream" {
184 "stderr"
185 } else {
186 "stdout"
187 };
188 let handle = crate::native_process::take_stream(&process, kind)?;
189 Ok(host_stream(
190 Rc::new(move || Ok(crate::native_process::stream_promise(handle, kind))),
191 Rc::new(|| Ok(())),
192 ))
193 }
194 "process-write" => {
195 if values.len() != 2 {
196 return Err("os/process-write expects a process and bytes".into());
197 }
198 let process = values[0].clone();
199 let bytes = match &values[1] {
200 Value::Bytes(value) => value.clone(),
201 Value::ByteBuffer(value) => value.borrow().clone(),
202 _ => return Err("os/process-write expects bytes".into()),
203 };
204 crate::native_process::write(&process, &bytes).map(|count| Value::Number(count as i64))
205 }
206 _ => Err(format!("unknown os operation: {operation}")),
207 }
208}
209
210fn native_test_events() -> Value {
211 Value::Vector(PVector::from_iter([
212 Value::Keyword("test/run-started".into()),
213 Value::Keyword("test/fact-started".into()),
214 Value::Keyword("test/fact-completed".into()),
215 Value::Keyword("test/run-completed".into()),
216 ]))
217}
218
219fn native_test_runner(value: Value) -> Result<Value, String> {
220 match value {
221 Value::Keyword(runner) if matches!(runner.as_str(), "code.test" | "native") => {
222 Ok(Value::Keyword(runner))
223 }
224 _ => Err("runtime test runner must be :code.test or :native".into()),
225 }
226}
227
228fn native_test_active_runner() -> Result<Value, String> {
229 ACTIVE_TEST_RUNNER
230 .with(|runner| native_test_runner(Value::Keyword(runner.borrow().clone().into())))
231}
232
233fn native_test_config(runner: Value, options: Value) -> Result<Value, String> {
234 if map_entries(&options).is_none() {
235 return Err("std.native.Test/config options must be a map".into());
236 }
237 if map_value(&options, &Value::Keyword("runner".into())).is_some() {
238 return Err("std.native.Test/config runner is owned by the runtime".into());
239 }
240 Ok(Value::Map(PMap::from_iter([
241 (Value::Keyword("runner".into()), runner),
242 (Value::Keyword("options".into()), options),
243 ])))
244}
245
246fn native_test_context(desc: Value, actual: Value, expected: Value, failures: Value) -> Value {
247 Value::Map(PMap::from_iter([
248 (
249 Value::Keyword("test".into()),
250 Value::Map(PMap::from_iter([
251 (Value::Keyword("desc".into()), desc.clone()),
252 (Value::Keyword("name".into()), desc),
255 (Value::Keyword("actual".into()), actual),
256 (Value::Keyword("expected".into()), expected),
257 ])),
258 ),
259 (Value::Keyword("failures".into()), failures),
260 ]))
261}
262
263fn native_test_failure(actual: Value, expected: Value) -> Value {
264 Value::Map(PMap::from_iter([
265 (
266 Value::Keyword("failure/code".into()),
267 Value::Keyword("test/not-equal".into()),
268 ),
269 (
270 Value::Keyword("failure/path".into()),
271 Value::Vector(PVector::new()),
272 ),
273 (
274 Value::Keyword("failure/in".into()),
275 Value::Vector(PVector::new()),
276 ),
277 (Value::Keyword("failure/actual".into()), actual),
278 (Value::Keyword("failure/expected".into()), expected),
279 (
280 Value::Keyword("failure/message".into()),
281 Value::String("values are not equal".into()),
282 ),
283 (
284 Value::Keyword("failure/context".into()),
285 Value::Map(PMap::new()),
286 ),
287 (
288 Value::Keyword("failure/children".into()),
289 Value::Vector(PVector::new()),
290 ),
291 ]))
292}
293
294fn native_test_compare(actual: Value, expected: Value) -> Result<Value, String> {
295 let pass = actual == expected;
296 let failures = if pass {
297 Value::Vector(PVector::new())
298 } else {
299 Value::Vector(PVector::from_iter([native_test_failure(
300 actual.clone(),
301 expected.clone(),
302 )]))
303 };
304 Ok(Value::Result(Rc::new(ResultValue::success(
305 Value::Bool(pass),
306 native_test_context(Value::Nil, actual, expected, failures),
307 )?)))
308}
309
310fn native_test_result(
311 desc: Value,
312 actual: Value,
313 expected: Value,
314 comparison: Value,
315) -> Result<Value, String> {
316 let Value::Result(comparison) = comparison else {
317 return Err("std.native.Test/result expects a comparison Result".into());
318 };
319 let failures = map_value(&comparison.context, &Value::Keyword("failures".into()))
320 .cloned()
321 .unwrap_or_else(|| Value::Vector(PVector::new()));
322 Ok(Value::Result(Rc::new(comparison.with_context(
323 native_test_context(desc, actual, expected, failures),
324 )?)))
325}
326
327fn native_test_error(desc: Value, actual: Value, expected: Value, error: String) -> Value {
328 Value::Result(Rc::new(
329 ResultValue::error(
330 caught_error(&error),
331 native_test_context(desc, actual, expected, Value::Vector(PVector::new())),
332 )
333 .expect("native Test error context is a map"),
334 ))
335}
336
337fn native_test_checked_result(desc: Value, metadata: Option<Value>, checked: Value) -> Value {
338 let Value::Result(result) = checked else {
339 return native_test_error(
340 desc,
341 Value::Nil,
342 Value::Nil,
343 "Test/check check function must return a Result".into(),
344 );
345 };
346 let mut context =
347 PMap::from_iter(map_entries(&result.context).expect("Result context is a map"));
348 let test = map_value(&result.context, &Value::Keyword("test".into()))
349 .cloned()
350 .unwrap_or_else(|| Value::Map(PMap::new()));
351 let mut test = PMap::from_iter(map_entries(&test).unwrap_or_default());
352 test = test.assoc_value(Value::Keyword("desc".into()), desc.clone());
353 test = test.assoc_value(Value::Keyword("name".into()), desc);
354 context = context.assoc_value(Value::Keyword("test".into()), Value::Map(test));
355 if let Some(metadata) = metadata {
356 context = context.assoc_value(Value::Keyword("meta".into()), metadata);
357 }
358 Value::Result(Rc::new(
359 result
360 .with_context(Value::Map(context))
361 .expect("native Test checked context is a map"),
362 ))
363}
364
365fn native_test_lifecycle_error(phase: &str, error: String) -> Value {
366 let desc = Value::String(format!("test {phase}"));
367 let phase = Value::Keyword(phase.into());
368 let Value::Result(result) = native_test_error(desc, Value::Nil, Value::Nil, error) else {
369 unreachable!()
370 };
371 Value::Result(Rc::new(
372 result
373 .with_context(Value::Map(PMap::from_iter([(
374 Value::Keyword("phase".into()),
375 phase,
376 )])))
377 .expect("native Test lifecycle context is a map"),
378 ))
379}
380
381fn native_test_lifecycle(lifecycle: &Value, phase: &str) -> Result<(), String> {
382 let Some(function) = map_value(lifecycle, &Value::Keyword(phase.into())).cloned() else {
383 return Ok(());
384 };
385 call_value(function, Vec::new())
386 .and_then(native_test_await)
387 .map(|_| ())
388}
389
390fn native_test_state() -> Result<crate::kernel::Namespace<Value>, String> {
391 Ok(namespace_registry()?.find_or_create("std.native.Test.state"))
392}
393
394fn native_test_state_value(key: &str) -> Result<Value, String> {
395 let state = native_test_state()?;
396 Ok(state
397 .resolve(&crate::lang::data::Symbol::parse(key))
398 .map(|var| var.deref_value())
399 .unwrap_or(Value::Nil))
400}
401
402fn native_test_set_state(key: &str, value: Value) -> Result<(), String> {
403 native_test_state()?.intern(key, value);
404 Ok(())
405}
406
407fn native_test_facts() -> Result<Vec<Value>, String> {
408 match native_test_state_value("facts")? {
409 Value::Vector(facts) => Ok(facts.iter().cloned().collect()),
410 Value::Nil => Ok(Vec::new()),
411 _ => Err("std.native.Test state facts must be a vector".into()),
412 }
413}
414
415fn native_test_set_facts(facts: Vec<Value>) -> Result<(), String> {
416 native_test_set_state("facts", Value::Vector(PVector::from_iter(facts)))
417}
418
419fn native_test_current_namespace() -> Result<String, String> {
420 Ok(namespace_registry()?.current().name().as_str().to_owned())
421}
422
423fn native_test_description(value: &Value, operation: &str) -> Result<Value, String> {
424 let desc = map_value(value, &Value::Keyword("desc".into())).cloned();
425 let name = map_value(value, &Value::Keyword("name".into())).cloned();
426 match (desc, name) {
427 (Some(Value::String(desc)), Some(Value::String(name)))
428 if !desc.is_empty() && desc == name =>
429 {
430 Ok(Value::String(desc))
431 }
432 (Some(Value::String(_)), Some(Value::String(_))) => Err(format!(
433 "std.native.Test/{operation} :desc and legacy :name must agree"
434 )),
435 (Some(Value::String(desc)), None) | (None, Some(Value::String(desc)))
436 if !desc.is_empty() =>
437 {
438 Ok(Value::String(desc))
439 }
440 (Some(_), _) | (_, Some(_)) => Err(format!(
441 "std.native.Test/{operation} :desc must be a non-empty string"
442 )),
443 (None, None) => Err(format!("std.native.Test/{operation} requires :desc")),
444 }
445}
446
447fn native_test_truthy(value: Option<&Value>) -> bool {
448 !matches!(value, None | Some(Value::Nil) | Some(Value::Bool(false)))
449}
450
451fn native_test_metadata(value: &Value, namespace: &str, order: i64) -> Result<Value, String> {
452 let metadata = map_value(value, &Value::Keyword("meta".into()))
453 .cloned()
454 .unwrap_or_else(|| Value::Map(PMap::new()));
455 let Some(entries) = map_entries(&metadata) else {
456 return Err("std.native.Test/register :meta must be a map".into());
457 };
458 let metadata = PMap::from_iter(entries)
459 .assoc_value(
460 Value::Keyword("test/namespace".into()),
461 Value::String(namespace.into()),
462 )
463 .assoc_value(Value::Keyword("test/order".into()), Value::Number(order));
464 Ok(Value::Map(metadata))
465}
466
467fn native_test_next_order() -> Result<i64, String> {
468 let current = match native_test_state_value("order")? {
469 Value::Number(value) if value >= 0 => value,
470 Value::Nil => 0,
471 _ => return Err("std.native.Test state order must be a non-negative number".into()),
472 };
473 let next = current + 1;
474 native_test_set_state("order", Value::Number(next))?;
475 Ok(next)
476}
477
478fn native_test_fact_value(
479 namespace: String,
480 desc: Value,
481 metadata: Value,
482 descriptor: &Value,
483) -> Result<Value, String> {
484 let function = map_value(descriptor, &Value::Keyword("function".into())).cloned();
485 let test = map_value(descriptor, &Value::Keyword("test".into())).cloned();
486 let expected = map_value(descriptor, &Value::Keyword("expected".into())).cloned();
487 if function.is_some() && (test.is_some() || expected.is_some()) {
488 return Err(
489 "std.native.Test/register accepts either :function or :test with :expected".into(),
490 );
491 }
492 if function.is_none() && (test.is_none() || expected.is_none()) {
493 return Err(
494 "std.native.Test/register requires :function or both :test and :expected".into(),
495 );
496 }
497 let mut fields = vec![
498 (Value::Keyword("namespace".into()), Value::String(namespace)),
499 (Value::Keyword("desc".into()), desc.clone()),
500 (Value::Keyword("name".into()), desc),
501 (Value::Keyword("meta".into()), metadata),
502 ];
503 if let Some(function) = function {
504 fields.push((Value::Keyword("function".into()), function));
505 } else {
506 fields.push((Value::Keyword("test".into()), test.expect("checked above")));
507 fields.push((
508 Value::Keyword("expected".into()),
509 expected.expect("checked above"),
510 ));
511 }
512 for key in ["before", "after"] {
513 if let Some(value) = map_value(descriptor, &Value::Keyword(key.into())).cloned() {
514 fields.push((Value::Keyword(key.into()), value));
515 }
516 }
517 Ok(Value::Map(PMap::from_iter(fields)))
518}
519
520fn native_test_register(descriptor: Value) -> Result<Value, String> {
521 if map_entries(&descriptor).is_none() {
522 return Err("std.native.Test/register expects a fact map".into());
523 }
524 let namespace = native_test_current_namespace()?;
525 let desc = native_test_description(&descriptor, "register")?;
526 let order = native_test_next_order()?;
527 let metadata = native_test_metadata(&descriptor, &namespace, order)?;
528 let fact = native_test_fact_value(namespace.clone(), desc.clone(), metadata, &descriptor)?;
529 let mut facts = native_test_facts()?;
530 facts.retain(|candidate| {
531 native_test_map_value(candidate, "namespace") != Some(Value::String(namespace.clone()))
532 || native_test_map_value(candidate, "desc") != Some(desc.clone())
533 });
534 facts.push(fact.clone());
535 native_test_set_facts(facts)?;
536 Ok(fact)
537}
538
539fn native_test_check(
540 cases: Value,
541 check_function: Option<Value>,
542 lifecycle: Option<Value>,
543) -> Result<Value, String> {
544 let cases = match cases {
545 Value::Vector(cases) => cases.iter().cloned().collect::<Vec<_>>(),
546 Value::Tuple(cases) => cases.iter().cloned().collect::<Vec<_>>(),
547 _ => return Err("std.native.Test/check expects a vector of test cases".into()),
548 };
549 let mut results = Vec::new();
550 let setup_ok = match &lifecycle {
551 Some(lifecycle) => match native_test_lifecycle(lifecycle, "setup") {
552 Ok(()) => true,
553 Err(error) => {
554 results.push(native_test_lifecycle_error("setup", error));
555 false
556 }
557 },
558 None => true,
559 };
560 if setup_ok {
561 for (index, case) in cases.iter().enumerate() {
562 let fallback_desc = Value::String(format!("invalid case {}", index + 1));
563 let Some(entries) = map_entries(case) else {
564 results.push(native_test_error(
565 fallback_desc,
566 Value::Nil,
567 Value::Nil,
568 "Test/check case must be a map".into(),
569 ));
570 continue;
571 };
572 let _ = entries;
573 let desc = native_test_description(case, "check").unwrap_or(fallback_desc);
574 let expected = map_value(case, &Value::Keyword("expected".into())).cloned();
575 let test = map_value(case, &Value::Keyword("test".into())).cloned();
576 let metadata = map_value(case, &Value::Keyword("meta".into())).cloned();
577 let result = match (test, expected) {
578 (Some(test), Some(expected)) => match &check_function {
579 Some(check) => match call_value(check.clone(), vec![test, expected])
580 .and_then(native_test_await)
581 {
582 Ok(checked) => native_test_checked_result(desc, metadata, checked),
583 Err(error) => {
584 let failed =
585 native_test_error(desc.clone(), Value::Nil, Value::Nil, error);
586 native_test_checked_result(desc, metadata, failed)
587 }
588 },
589 None => match call_value(test, Vec::new()).and_then(native_test_await) {
590 Ok(actual) => {
591 let comparison = native_test_compare(actual.clone(), expected.clone())?;
592 native_test_result(desc, actual, expected, comparison)?
593 }
594 Err(error) => native_test_error(desc, Value::Nil, expected, error),
595 },
596 },
597 (None, expected) => native_test_error(
598 desc,
599 Value::Nil,
600 expected.unwrap_or(Value::Nil),
601 "Test/check case requires :test".into(),
602 ),
603 (Some(_), None) => native_test_error(
604 desc,
605 Value::Nil,
606 Value::Nil,
607 "Test/check case requires :expected".into(),
608 ),
609 };
610 results.push(result);
611 }
612 }
613 if let Some(lifecycle) = &lifecycle {
614 if let Err(error) = native_test_lifecycle(lifecycle, "teardown") {
615 results.push(native_test_lifecycle_error("teardown", error));
616 }
617 }
618 let output = Value::Vector(PVector::from_iter(results.clone()));
619 let mut history = match native_test_state_value("results")? {
620 Value::Vector(values) => values.iter().cloned().collect::<Vec<_>>(),
621 Value::Nil => Vec::new(),
622 _ => return Err("std.native.Test state results must be a vector".into()),
623 };
624 history.extend(results);
625 native_test_set_state("results", Value::Vector(PVector::from_iter(history)))?;
626 Ok(output)
627}
628
629fn native_test_result_passed(value: &Value) -> bool {
630 matches!(
631 value,
632 Value::Result(result) if result.is_success() && matches!(result.data, Value::Bool(true))
633 )
634}
635
636fn native_test_result_timed_out(value: &Value) -> bool {
637 let Value::Result(result) = value else {
638 return false;
639 };
640 result.is_timeout()
641 || result
642 .error
643 .as_ref()
644 .is_some_and(|error| error.message == "asynchronous test did not settle")
645}
646
647fn native_test_checks(value: Value, desc: Value) -> Vec<Value> {
648 let values = match value {
649 Value::Vector(values) => values.iter().cloned().collect(),
650 Value::Tuple(values) => values.iter().cloned().collect(),
651 Value::Result(_) => vec![value],
652 value => {
653 let context = native_test_context(
654 desc,
655 value,
656 Value::Keyword("returned".into()),
657 Value::Vector(PVector::new()),
658 );
659 return vec![Value::Result(Rc::new(
660 ResultValue::success(Value::Bool(true), context)
661 .expect("native Test returned-value context is a map"),
662 ))];
663 }
664 };
665 values
666 .into_iter()
667 .map(|value| match value {
668 Value::Result(_) => value,
669 value => native_test_error(
670 desc.clone(),
671 value,
672 Value::Keyword("Result".into()),
673 "Test fact functions must return Results or a vector of Results".into(),
674 ),
675 })
676 .collect()
677}
678
679fn native_test_fact_identity(fact: &Value) -> Result<Vec<(Value, Value)>, String> {
680 let namespace = native_test_map_value(fact, "namespace")
681 .ok_or_else(|| "std.native.Test fact is missing :namespace".to_owned())?;
682 let desc = native_test_map_value(fact, "desc")
683 .ok_or_else(|| "std.native.Test fact is missing :desc".to_owned())?;
684 let metadata = native_test_map_value(fact, "meta")
685 .ok_or_else(|| "std.native.Test fact is missing :meta".to_owned())?;
686 Ok(vec![
687 (Value::Keyword("namespace".into()), namespace),
688 (Value::Keyword("desc".into()), desc.clone()),
689 (Value::Keyword("name".into()), desc),
690 (Value::Keyword("meta".into()), metadata),
691 ])
692}
693
694fn native_test_hook(value: Option<Value>) -> Result<(), String> {
695 let Some(function) = value else {
696 return Ok(());
697 };
698 call_value(function, Vec::new())
699 .and_then(native_test_await)
700 .map(|_| ())
701}
702
703fn native_test_cancelled(fact: &Value, options: &Value) -> Result<bool, String> {
704 let Some(value) = native_test_map_value(options, "cancelled") else {
705 return Ok(false);
706 };
707 match value {
708 Value::Bool(value) => Ok(value),
709 Value::Nil => Ok(false),
710 function => call_value(function, vec![fact.clone()])
711 .and_then(native_test_await)
712 .map(|value| native_test_truthy(Some(&value))),
713 }
714}
715
716fn native_test_fact_checks(fact: &Value, options: &Value) -> Result<Vec<Value>, String> {
717 let desc = native_test_map_value(fact, "desc")
718 .ok_or_else(|| "std.native.Test fact is missing :desc".to_owned())?;
719 if let Some(function) = native_test_map_value(fact, "function") {
720 return call_value(function, vec![options.clone()])
721 .and_then(native_test_await)
722 .map(|value| native_test_checks(value, desc));
723 }
724 let test = native_test_map_value(fact, "test")
725 .ok_or_else(|| "std.native.Test fact is missing :test".to_owned())?;
726 let expected = native_test_map_value(fact, "expected")
727 .ok_or_else(|| "std.native.Test fact is missing :expected".to_owned())?;
728 native_test_check(
729 Value::Vector(PVector::from_iter([Value::Map(PMap::from_iter([
730 (Value::Keyword("desc".into()), desc),
731 (Value::Keyword("test".into()), test),
732 (Value::Keyword("expected".into()), expected),
733 ]))])),
734 None,
735 None,
736 )
737 .and_then(|value| match value {
738 Value::Vector(values) => Ok(values.iter().cloned().collect()),
739 _ => unreachable!(),
740 })
741}
742
743fn native_test_status(checks: &[Value]) -> &'static str {
744 if checks.iter().any(native_test_result_timed_out) {
745 "timeout"
746 } else if checks
747 .iter()
748 .any(|value| matches!(value, Value::Result(result) if result.is_error()))
749 {
750 "error"
751 } else if checks.iter().all(native_test_result_passed) {
752 "passed"
753 } else {
754 "failed"
755 }
756}
757
758fn native_test_fact_result(
759 fact: &Value,
760 status: &str,
761 checks: Vec<Value>,
762 error: Option<String>,
763 elapsed: i64,
764) -> Result<Value, String> {
765 let mut fields = native_test_fact_identity(fact)?;
766 fields.push((
767 Value::Keyword("status".into()),
768 Value::Keyword(status.into()),
769 ));
770 fields.push((
771 Value::Keyword("checks".into()),
772 Value::Vector(PVector::from_iter(checks)),
773 ));
774 fields.push((
775 Value::Keyword("elapsed".into()),
776 Value::Number(elapsed.max(0)),
777 ));
778 if let Some(error) = error {
779 fields.push((Value::Keyword("error".into()), Value::String(error)));
780 }
781 Ok(Value::Map(PMap::from_iter(fields)))
782}
783
784fn native_test_run_fact(fact: Value, options: Value) -> Result<Value, String> {
785 if map_entries(&fact).is_none() {
786 return Err("std.native.Test/run-fact expects a fact map".into());
787 }
788 let started = crate::clock::time_ms();
789 let metadata = native_test_map_value(&fact, "meta")
790 .ok_or_else(|| "std.native.Test fact is missing :meta".to_owned())?;
791 if native_test_truthy(native_test_map_value(&metadata, "skip").as_ref()) {
792 return native_test_fact_result(&fact, "skipped", Vec::new(), None, 0);
793 }
794 if native_test_cancelled(&fact, &options)? {
795 return native_test_fact_result(&fact, "cancelled", Vec::new(), None, 0);
796 }
797
798 let mut checks = Vec::new();
799 let mut failure = None;
800 for hook in [
801 native_test_map_value(&options, "before-each"),
802 native_test_map_value(&fact, "before"),
803 ] {
804 if let Err(error) = native_test_hook(hook) {
805 failure = Some(error);
806 break;
807 }
808 }
809 if failure.is_none() {
810 match native_test_fact_checks(&fact, &options) {
811 Ok(output) => checks = output,
812 Err(error) => failure = Some(error),
813 }
814 }
815 for hook in [
816 native_test_map_value(&fact, "after"),
817 native_test_map_value(&options, "after-each"),
818 ] {
819 if let Err(error) = native_test_hook(hook) {
820 failure.get_or_insert(error);
821 }
822 }
823 let elapsed = crate::clock::time_ms() - started;
824 match failure {
825 Some(error) => native_test_fact_result(&fact, "error", checks, Some(error), elapsed),
826 None => native_test_fact_result(&fact, native_test_status(&checks), checks, None, elapsed),
827 }
828}
829
830fn native_test_summary(results: Vec<Value>) -> Result<Value, String> {
831 let mut counts = [0_i64; 6];
832 let mut check_total = 0_i64;
833 let mut check_passed = 0_i64;
834 let mut namespaces = HashSet::new();
835 for result in &results {
836 let status = native_test_map_value(result, "status")
837 .ok_or_else(|| "std.native.Test summary result is missing :status".to_owned())?;
838 let index = match status {
839 Value::Keyword(status) => match status.as_str() {
840 "passed" => 0,
841 "failed" => 1,
842 "error" => 2,
843 "timeout" => 3,
844 "skipped" => 4,
845 "cancelled" => 5,
846 value => {
847 return Err(format!(
848 "std.native.Test summary has unknown status :{value}"
849 ))
850 }
851 },
852 _ => return Err("std.native.Test summary status must be a keyword".into()),
853 };
854 counts[index] += 1;
855 if let Some(Value::String(namespace)) = native_test_map_value(result, "namespace") {
856 namespaces.insert(namespace);
857 }
858 if let Some(Value::Vector(checks)) = native_test_map_value(result, "checks") {
859 for check in checks.iter() {
860 check_total += 1;
861 if native_test_result_passed(check) {
862 check_passed += 1;
863 }
864 }
865 }
866 }
867 let check_failed = check_total - check_passed;
868 let status = if counts[1] + counts[2] + counts[3] == 0 {
869 "passed"
870 } else {
871 "failed"
872 };
873 Ok(Value::Map(PMap::from_iter([
874 (
875 Value::Keyword("status".into()),
876 Value::Keyword(status.into()),
877 ),
878 (
879 Value::Keyword("counts".into()),
880 Value::Map(PMap::from_iter([
881 (Value::Keyword("passed".into()), Value::Number(counts[0])),
882 (Value::Keyword("failed".into()), Value::Number(counts[1])),
883 (Value::Keyword("error".into()), Value::Number(counts[2])),
884 (Value::Keyword("timeout".into()), Value::Number(counts[3])),
885 (Value::Keyword("skipped".into()), Value::Number(counts[4])),
886 (Value::Keyword("cancelled".into()), Value::Number(counts[5])),
887 ])),
888 ),
889 (
890 Value::Keyword("check-counts".into()),
891 Value::Map(PMap::from_iter([
892 (Value::Keyword("total".into()), Value::Number(check_total)),
893 (Value::Keyword("passed".into()), Value::Number(check_passed)),
894 (Value::Keyword("failed".into()), Value::Number(check_failed)),
895 ])),
896 ),
897 (
898 Value::Keyword("files".into()),
899 Value::Number(namespaces.len() as i64),
900 ),
901 (
902 Value::Keyword("facts".into()),
903 Value::Number(results.len() as i64),
904 ),
905 (Value::Keyword("checks".into()), Value::Number(check_total)),
906 (Value::Keyword("passed".into()), Value::Number(check_passed)),
907 (Value::Keyword("failed".into()), Value::Number(check_failed)),
908 (Value::Keyword("throw".into()), Value::Number(counts[2])),
909 (Value::Keyword("timeout".into()), Value::Number(counts[3])),
910 (
911 Value::Keyword("results".into()),
912 Value::Vector(PVector::from_iter(results)),
913 ),
914 ])))
915}
916
917fn native_test_namespace(value: Value, operation: &str) -> Result<String, String> {
918 match value {
919 Value::String(value) => Ok(value),
920 Value::Symbol(value) => Ok(value.as_str().to_owned()),
921 _ => Err(format!(
922 "std.native.Test/{operation} namespace must be a string or symbol"
923 )),
924 }
925}
926
927fn native_test_run(options: Value) -> Result<Value, String> {
928 if map_entries(&options).is_none() {
929 return Err(
930 "std.native.Test/run expects an optional options map; use Test/check for cases".into(),
931 );
932 }
933 let namespace = match native_test_map_value(&options, "namespace") {
934 Some(value) => native_test_namespace(value, "run")?,
935 None => native_test_current_namespace()?,
936 };
937 let facts = native_test_facts()?
938 .into_iter()
939 .filter(|fact| {
940 native_test_map_value(fact, "namespace") == Some(Value::String(namespace.clone()))
941 })
942 .collect::<Vec<_>>();
943 let checks = match native_test_state_value("results")? {
944 Value::Vector(values) => values.iter().cloned().collect::<Vec<_>>(),
945 Value::Nil => Vec::new(),
946 _ => return Err("std.native.Test state results must be a vector".into()),
947 };
948 let mut results = checks
949 .into_iter()
950 .enumerate()
951 .map(|(index, check)| {
952 let desc = match &check {
953 Value::Result(result) => map_value(&result.context, &Value::Keyword("desc".into()))
954 .cloned()
955 .unwrap_or_else(|| Value::String(format!("test check {}", index + 1))),
956 _ => Value::String(format!("test check {}", index + 1)),
957 };
958 let fact = Value::Map(PMap::from_iter([
959 (
960 Value::Keyword("namespace".into()),
961 Value::String(namespace.clone()),
962 ),
963 (Value::Keyword("desc".into()), desc.clone()),
964 (Value::Keyword("name".into()), desc),
965 (Value::Keyword("meta".into()), Value::Map(PMap::new())),
966 ]));
967 native_test_fact_result(
968 &fact,
969 native_test_status(&[check.clone()]),
970 vec![check],
971 None,
972 0,
973 )
974 })
975 .collect::<Result<Vec<_>, _>>()?;
976 let mut suite_error = None;
977 if let Err(error) = native_test_hook(native_test_map_value(&options, "before-all")) {
978 suite_error = Some(error);
979 }
980 if suite_error.is_none() {
981 let mut fail_fast = false;
982 for fact in facts {
983 if fail_fast {
984 results.push(native_test_fact_result(
985 &fact,
986 "cancelled",
987 Vec::new(),
988 None,
989 0,
990 )?);
991 continue;
992 }
993 let result = native_test_run_fact(fact, options.clone())?;
994 fail_fast = native_test_truthy(native_test_map_value(&options, "fail-fast").as_ref())
995 && matches!(
996 native_test_map_value(&result, "status"),
997 Some(Value::Keyword(status)) if matches!(status.as_str(), "failed" | "error" | "timeout")
998 );
999 results.push(result);
1000 }
1001 } else {
1002 let synthetic = Value::Map(PMap::from_iter([
1003 (
1004 Value::Keyword("namespace".into()),
1005 Value::String(namespace.clone()),
1006 ),
1007 (
1008 Value::Keyword("desc".into()),
1009 Value::String("test before-all".into()),
1010 ),
1011 (
1012 Value::Keyword("name".into()),
1013 Value::String("test before-all".into()),
1014 ),
1015 (Value::Keyword("meta".into()), Value::Map(PMap::new())),
1016 ]));
1017 results.push(native_test_fact_result(
1018 &synthetic,
1019 "error",
1020 Vec::new(),
1021 suite_error,
1022 0,
1023 )?);
1024 }
1025 if let Err(error) = native_test_hook(native_test_map_value(&options, "after-all")) {
1026 let synthetic = Value::Map(PMap::from_iter([
1027 (Value::Keyword("namespace".into()), Value::String(namespace)),
1028 (
1029 Value::Keyword("desc".into()),
1030 Value::String("test after-all".into()),
1031 ),
1032 (
1033 Value::Keyword("name".into()),
1034 Value::String("test after-all".into()),
1035 ),
1036 (Value::Keyword("meta".into()), Value::Map(PMap::new())),
1037 ]));
1038 results.push(native_test_fact_result(
1039 &synthetic,
1040 "error",
1041 Vec::new(),
1042 Some(error),
1043 0,
1044 )?);
1045 }
1046 let summary = native_test_summary(results)?;
1047 native_test_set_state("last-run", summary.clone())?;
1048 Ok(summary)
1049}
1050
1051fn native_test_lookup(namespace: &str, desc: &Value) -> Result<Value, String> {
1052 Ok(native_test_facts()?
1053 .into_iter()
1054 .find(|fact| {
1055 native_test_map_value(fact, "namespace") == Some(Value::String(namespace.into()))
1056 && native_test_map_value(fact, "desc") == Some(desc.clone())
1057 })
1058 .unwrap_or(Value::Nil))
1059}
1060
1061fn native_test_desc_argument(value: Value, operation: &str) -> Result<Value, String> {
1062 match value {
1063 Value::String(value) if !value.is_empty() => Ok(Value::String(value)),
1064 _ => Err(format!(
1065 "std.native.Test/{operation} description must be a non-empty string"
1066 )),
1067 }
1068}
1069
1070fn native_test_reset() -> Result<Value, String> {
1071 native_test_set_state("facts", Value::Vector(PVector::new()))?;
1072 native_test_set_state("results", Value::Vector(PVector::new()))?;
1073 native_test_set_state("order", Value::Number(0))?;
1074 native_test_set_state("last-run", Value::Nil)?;
1075 Ok(Value::Nil)
1076}
1077
1078fn native_test_await(value: Value) -> Result<Value, String> {
1079 match value {
1080 Value::Promise(promise) => match promise.wait_state() {
1081 PromiseState::Fulfilled(value) => Ok(value),
1082 PromiseState::Rejected(error) => Err(promise_rejection_error(error)),
1083 PromiseState::Pending => Err("asynchronous test did not settle".into()),
1084 },
1085 value => Ok(value),
1086 }
1087}
1088
1089fn native_test_require_result(value: Value, operation: &str) -> Result<Rc<ResultValue>, String> {
1090 match value {
1091 Value::Result(result) => Ok(result),
1092 _ => Err(format!("std.native.Test/{operation} expects a Result")),
1093 }
1094}
1095
1096fn native_test_context_value(result: &ResultValue, key: &str) -> Value {
1097 native_test_map_value(&result.context, key).unwrap_or(Value::Nil)
1098}
1099
1100fn native_test_map_value(value: &Value, key: &str) -> Option<Value> {
1101 map_entries(value)?
1102 .into_iter()
1103 .find_map(|(candidate, value)| {
1104 matches!(candidate, Value::Keyword(keyword) if keyword.as_str() == key).then_some(value)
1105 })
1106}
1107
1108fn native_test_detail(result: &ResultValue, key: &str) -> Value {
1109 let test = native_test_context_value(result, "test");
1110 native_test_map_value(&test, key).unwrap_or(Value::Nil)
1111}
1112
1113fn native_test_failure_shape(value: &Value) -> bool {
1114 let Some(_) = map_entries(value) else {
1115 return false;
1116 };
1117 let keyword = |key: &str| matches!(native_test_map_value(value, key), Some(Value::Keyword(_)));
1118 let vector = |key: &str| {
1119 matches!(
1120 native_test_map_value(value, key),
1121 Some(Value::Vector(_) | Value::Tuple(_))
1122 )
1123 };
1124 let string = |key: &str| matches!(native_test_map_value(value, key), Some(Value::String(_)));
1125 let map = |key: &str| {
1126 native_test_map_value(value, key).is_some_and(|value| map_entries(&value).is_some())
1127 };
1128 let children_valid = match native_test_map_value(value, "failure/children") {
1129 Some(Value::Vector(children)) => children.iter().all(native_test_failure_shape),
1130 Some(Value::Tuple(children)) => children.iter().all(native_test_failure_shape),
1131 _ => false,
1132 };
1133 keyword("failure/code")
1134 && vector("failure/path")
1135 && vector("failure/in")
1136 && native_test_map_value(value, "failure/actual").is_some()
1137 && native_test_map_value(value, "failure/expected").is_some()
1138 && string("failure/message")
1139 && map("failure/context")
1140 && vector("failure/children")
1141 && children_valid
1142}
1143
1144fn native_test_failure_leaves(value: &Value, leaves: &mut Vec<Value>) {
1145 if !native_test_failure_shape(value) {
1146 return;
1147 }
1148 match native_test_map_value(value, "failure/children") {
1149 Some(Value::Vector(children)) => {
1150 if children.is_empty() {
1151 leaves.push(value.clone());
1152 } else {
1153 for child in children.iter() {
1154 native_test_failure_leaves(child, leaves);
1155 }
1156 }
1157 }
1158 Some(Value::Tuple(children)) => {
1159 if children.is_empty() {
1160 leaves.push(value.clone());
1161 } else {
1162 for child in children.iter() {
1163 native_test_failure_leaves(child, leaves);
1164 }
1165 }
1166 }
1167 _ => {}
1168 }
1169}
1170
1171fn native_test_failures(result: &ResultValue) -> Value {
1172 match native_test_context_value(result, "failures") {
1173 Value::Vector(failures) => Value::Vector(failures),
1174 Value::Tuple(failures) => Value::Vector(PVector::from_iter(failures.iter().cloned())),
1175 _ => Value::Vector(PVector::new()),
1176 }
1177}
1178
1179fn native_test_failure_seq(result: &ResultValue) -> Value {
1180 let mut leaves = Vec::new();
1181 if let Value::Vector(failures) = native_test_failures(result) {
1182 for failure in failures.iter() {
1183 native_test_failure_leaves(failure, &mut leaves);
1184 }
1185 }
1186 Value::Vector(PVector::from_iter(leaves))
1187}
1188
1189fn native_test_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
1190 let operation = operation
1191 .strip_prefix("std.native.Test/")
1192 .unwrap_or(operation);
1193 match operation {
1194 "events" => {
1195 if !values.is_empty() {
1196 return Err("std.native.Test/events expects no arguments".into());
1197 }
1198 Ok(native_test_events())
1199 }
1200 "catalog" => {
1201 if !values.is_empty() {
1202 return Err("std.native.Test/catalog expects no arguments".into());
1203 }
1204 Ok(Value::Map(PMap::from_iter([
1205 (
1206 Value::Keyword("runners".into()),
1207 Value::Vector(PVector::from_iter([
1208 Value::Keyword("code.test".into()),
1209 Value::Keyword("native".into()),
1210 ])),
1211 ),
1212 (
1213 Value::Keyword("default".into()),
1214 Value::Keyword("code.test".into()),
1215 ),
1216 (
1217 Value::Keyword("runner".into()),
1218 native_test_active_runner()?,
1219 ),
1220 (
1221 Value::Keyword("context".into()),
1222 Value::Keyword("test".into()),
1223 ),
1224 (Value::Keyword("events".into()), native_test_events()),
1225 ])))
1226 }
1227 "config" => {
1228 if values.len() > 1 {
1229 return Err("std.native.Test/config expects optional options".into());
1230 }
1231 let options = if values.is_empty() {
1232 Value::Map(PMap::new())
1233 } else {
1234 values[0].clone()
1235 };
1236 native_test_config(native_test_active_runner()?, options)
1237 }
1238 "context" => {
1239 if values.len() > 1 {
1240 return Err("std.native.Test/context expects an optional config".into());
1241 }
1242 let config = if values.is_empty() {
1243 native_test_config(native_test_active_runner()?, Value::Map(PMap::new()))?
1244 } else {
1245 let value = values[0].clone();
1246 let Some(runner) = map_value(&value, &Value::Keyword("runner".into())).cloned()
1247 else {
1248 return Err("std.native.Test/context expects a Test/config map".into());
1249 };
1250 let runner = native_test_runner(runner)?;
1251 if runner != native_test_active_runner()? {
1252 return Err(
1253 "std.native.Test/context config runner does not match the runtime".into(),
1254 );
1255 }
1256 value
1257 };
1258 Ok(Value::Pointer(PPointer::new(
1259 "test".into(),
1260 PMap::from_iter([
1261 (Value::Keyword("id".into()), Value::Keyword("test".into())),
1262 (Value::Keyword("config".into()), config),
1263 ]),
1264 )))
1265 }
1266 "compare" => {
1267 if values.len() != 2 {
1268 return Err("std.native.Test/compare expects actual and expected".into());
1269 }
1270 native_test_compare(values[0].clone(), values[1].clone())
1271 }
1272 "result" => {
1273 if values.len() != 4 {
1274 return Err(
1275 "std.native.Test/result expects name, actual, expected, and comparison Result"
1276 .into(),
1277 );
1278 }
1279 let name = values[0].clone();
1280 let actual = values[1].clone();
1281 let expected = values[2].clone();
1282 let comparison = values[3].clone();
1283 native_test_result(name, actual, expected, comparison)
1284 }
1285 "check" => {
1286 if values.is_empty() || values.len() > 3 {
1287 return Err(
1288 "std.native.Test/check expects cases, an optional check function, and an optional lifecycle map".into(),
1289 );
1290 }
1291 let cases = values[0].clone();
1292 let second = if values.len() >= 2 {
1293 Some(values[1].clone())
1294 } else {
1295 None
1296 };
1297 let third = if values.len() == 3 {
1298 Some(values[2].clone())
1299 } else {
1300 None
1301 };
1302 let (check_function, lifecycle) = match (second, third) {
1303 (Some(check), Some(lifecycle)) => (Some(check), Some(lifecycle)),
1304 (Some(value), None) if map_entries(&value).is_some() => (None, Some(value)),
1305 (check, None) => (check, None),
1306 (None, Some(_)) => unreachable!(),
1307 };
1308 if lifecycle
1309 .as_ref()
1310 .is_some_and(|value| map_entries(value).is_none())
1311 {
1312 return Err("std.native.Test/check lifecycle must be a map".into());
1313 }
1314 native_test_check(cases, check_function, lifecycle)
1315 }
1316 "register" => match values.as_slice() {
1317 [fact] => native_test_register(fact.clone()),
1318 _ => Err("std.native.Test/register expects one fact map".into()),
1319 },
1320 "facts" => {
1321 if values.len() > 1 {
1322 return Err("std.native.Test/facts expects an optional namespace".into());
1323 }
1324 let namespace = match values.as_slice() {
1325 [] => native_test_current_namespace()?,
1326 [namespace] => native_test_namespace(namespace.clone(), "facts")?,
1327 _ => unreachable!(),
1328 };
1329 Ok(Value::Vector(PVector::from_iter(
1330 native_test_facts()?.into_iter().filter(|fact| {
1331 native_test_map_value(fact, "namespace")
1332 == Some(Value::String(namespace.clone()))
1333 }),
1334 )))
1335 }
1336 "get" => {
1337 let (namespace, desc) = match values.as_slice() {
1338 [desc] => (
1339 native_test_current_namespace()?,
1340 native_test_desc_argument(desc.clone(), "get")?,
1341 ),
1342 [namespace, desc] => (
1343 native_test_namespace(namespace.clone(), "get")?,
1344 native_test_desc_argument(desc.clone(), "get")?,
1345 ),
1346 _ => {
1347 return Err(
1348 "std.native.Test/get expects a description and optional namespace".into(),
1349 )
1350 }
1351 };
1352 native_test_lookup(&namespace, &desc)
1353 }
1354 "remove" => {
1355 let (namespace, desc) = match values.as_slice() {
1356 [desc] => (
1357 native_test_current_namespace()?,
1358 native_test_desc_argument(desc.clone(), "remove")?,
1359 ),
1360 [namespace, desc] => (
1361 native_test_namespace(namespace.clone(), "remove")?,
1362 native_test_desc_argument(desc.clone(), "remove")?,
1363 ),
1364 _ => {
1365 return Err(
1366 "std.native.Test/remove expects a description and optional namespace"
1367 .into(),
1368 )
1369 }
1370 };
1371 let removed = native_test_lookup(&namespace, &desc)?;
1372 let mut facts = native_test_facts()?;
1373 facts.retain(|fact| {
1374 native_test_map_value(fact, "namespace") != Some(Value::String(namespace.clone()))
1375 || native_test_map_value(fact, "desc") != Some(desc.clone())
1376 });
1377 native_test_set_facts(facts)?;
1378 Ok(removed)
1379 }
1380 "purge" => {
1381 if values.len() > 1 {
1382 return Err("std.native.Test/purge expects an optional namespace".into());
1383 }
1384 let namespace = match values.as_slice() {
1385 [] => native_test_current_namespace()?,
1386 [namespace] => native_test_namespace(namespace.clone(), "purge")?,
1387 _ => unreachable!(),
1388 };
1389 let facts = native_test_facts()?;
1390 let removed = facts
1391 .iter()
1392 .filter(|fact| {
1393 native_test_map_value(fact, "namespace")
1394 == Some(Value::String(namespace.clone()))
1395 })
1396 .cloned()
1397 .collect::<Vec<_>>();
1398 native_test_set_facts(
1399 facts
1400 .into_iter()
1401 .filter(|fact| {
1402 native_test_map_value(fact, "namespace")
1403 != Some(Value::String(namespace.clone()))
1404 })
1405 .collect(),
1406 )?;
1407 Ok(Value::Vector(PVector::from_iter(removed)))
1408 }
1409 "reset" => {
1410 if !values.is_empty() {
1411 return Err("std.native.Test/reset expects no arguments".into());
1412 }
1413 native_test_reset()
1414 }
1415 "run-fact" => {
1416 let (fact, options) = match values.as_slice() {
1417 [fact] => (fact.clone(), Value::Map(PMap::new())),
1418 [fact, options] if map_entries(options).is_some() => {
1419 (fact.clone(), options.clone())
1420 }
1421 [_, _] => return Err("std.native.Test/run-fact options must be a map".into()),
1422 _ => return Err(
1423 "std.native.Test/run-fact expects a fact or description and optional options"
1424 .into(),
1425 ),
1426 };
1427 let fact = if map_entries(&fact).is_some() {
1428 fact
1429 } else {
1430 let desc = native_test_desc_argument(fact, "run-fact")?;
1431 let namespace = native_test_current_namespace()?;
1432 let fact = native_test_lookup(&namespace, &desc)?;
1433 if matches!(fact, Value::Nil) {
1434 return Err(format!(
1435 "std.native.Test/run-fact fact not found: {}",
1436 desc.display()
1437 ));
1438 }
1439 fact
1440 };
1441 native_test_run_fact(fact, options)
1442 }
1443 "run" => {
1444 if values.len() > 1 {
1445 return Err(
1446 "std.native.Test/run expects an optional options map; use Test/check for cases"
1447 .into(),
1448 );
1449 }
1450 let options = values
1451 .into_iter()
1452 .next()
1453 .unwrap_or_else(|| Value::Map(PMap::new()));
1454 native_test_run(options)
1455 }
1456 "summary" => match values.as_slice() {
1457 [Value::Vector(results)] => native_test_summary(results.iter().cloned().collect()),
1458 [Value::Tuple(results)] => native_test_summary(results.iter().cloned().collect()),
1459 [_] => Err("std.native.Test/summary expects a vector of fact results".into()),
1460 _ => Err("std.native.Test/summary expects one vector of fact results".into()),
1461 },
1462 "passed?" => {
1463 if values.len() != 1 {
1464 return Err("std.native.Test/passed? expects one result".into());
1465 }
1466 let result = native_test_require_result(values[0].clone(), "passed?")?;
1467 Ok(Value::Bool(
1468 result.is_success() && matches!(result.data, Value::Bool(true)),
1469 ))
1470 }
1471 "actual" | "expected" | "failures" | "failure-seq" | "failure-count" => {
1472 if values.len() != 1 {
1473 return Err(format!("std.native.Test/{operation} expects one Result"));
1474 }
1475 let result = native_test_require_result(values[0].clone(), operation)?;
1476 Ok(match operation {
1477 "actual" => native_test_detail(&result, "actual"),
1478 "expected" => native_test_detail(&result, "expected"),
1479 "failures" => native_test_failures(&result),
1480 "failure-seq" => native_test_failure_seq(&result),
1481 "failure-count" => match native_test_failure_seq(&result) {
1482 Value::Vector(values) => Value::Number(values.len() as i64),
1483 _ => unreachable!(),
1484 },
1485 _ => unreachable!(),
1486 })
1487 }
1488 "failure" => {
1489 if values.len() != 2 {
1490 return Err("std.native.Test/failure expects a Result and index".into());
1491 }
1492 let result = native_test_require_result(values[0].clone(), "failure")?;
1493 let index = match &values[1] {
1494 Value::Number(index) if *index >= 0 => *index as usize,
1495 _ => {
1496 return Err(
1497 "std.native.Test/failure index must be a non-negative integer".into(),
1498 )
1499 }
1500 };
1501 match native_test_failure_seq(&result) {
1502 Value::Vector(values) => Ok(values.get(index).cloned().unwrap_or(Value::Nil)),
1503 _ => unreachable!(),
1504 }
1505 }
1506 "failure?" => {
1507 if values.len() != 1 {
1508 return Err("std.native.Test/failure? expects one value".into());
1509 }
1510 Ok(Value::Bool(native_test_failure_shape(&values[0])))
1511 }
1512 _ => Err(format!("unknown std.native.Test operation: {operation}")),
1513 }
1514}
1515
1516struct NativeCommandRequest {
1517 app: u64,
1518 request: crate::command::Request,
1519 context: Value,
1520}
1521
1522struct NativeCommandSnapshot {
1523 app: u64,
1524 snapshot: crate::command::Snapshot<Value>,
1525}
1526
1527struct NativeCommandState {
1528 next_app: u64,
1529 next_request: u64,
1530 next_snapshot: u64,
1531 apps: HashMap<u64, crate::command::App<Value>>,
1532 requests: HashMap<u64, NativeCommandRequest>,
1533 snapshots: HashMap<u64, NativeCommandSnapshot>,
1534}
1535
1536impl Default for NativeCommandState {
1537 fn default() -> Self {
1538 Self {
1539 next_app: 1,
1540 next_request: 1,
1541 next_snapshot: 1,
1542 apps: HashMap::new(),
1543 requests: HashMap::new(),
1544 snapshots: HashMap::new(),
1545 }
1546 }
1547}
1548
1549thread_local! {
1550 static NATIVE_COMMAND_STATE: RefCell<NativeCommandState> = RefCell::new(NativeCommandState::default());
1551}
1552
1553fn native_command_keyword(name: &str) -> Value {
1554 Value::Keyword(Keyword::from(name))
1555}
1556
1557fn native_command_map(entries: impl IntoIterator<Item = (Value, Value)>) -> Value {
1558 Value::Map(PMap::from_iter(entries))
1559}
1560
1561fn native_command_pointer(
1562 context: &str,
1563 entries: impl IntoIterator<Item = (Value, Value)>,
1564) -> Value {
1565 Value::Pointer(PPointer::new(
1566 Keyword::from(context),
1567 PMap::from_iter(entries),
1568 ))
1569}
1570
1571fn native_command_handle(value: &Value, context: &str, operation: &str) -> Result<u64, String> {
1572 let Value::Pointer(pointer) = value else {
1573 return Err(format!(
1574 "std.native.Command/{operation} expects a {context} handle"
1575 ));
1576 };
1577 if pointer.context().as_str() != context {
1578 return Err(format!(
1579 "std.native.Command/{operation} expects a {context} handle"
1580 ));
1581 }
1582 match pointer.get(&native_command_keyword("id")) {
1583 Some(Value::Number(id)) if *id > 0 => Ok(*id as u64),
1584 _ => Err(format!(
1585 "std.native.Command/{operation} received an invalid {context} handle"
1586 )),
1587 }
1588}
1589
1590fn native_command_app(value: &Value, operation: &str) -> Result<u64, String> {
1591 native_command_handle(value, "command/app", operation)
1592}
1593
1594fn native_command_route(
1595 value: &Value,
1596 app: u64,
1597 operation: &str,
1598) -> Result<crate::command::RouteHandle, String> {
1599 let route = native_command_handle(value, "command/route", operation)?;
1600 let Value::Pointer(pointer) = value else {
1601 unreachable!()
1602 };
1603 match pointer.get(&native_command_keyword("app")) {
1604 Some(Value::Number(candidate)) if *candidate == app as i64 => {
1605 Ok(crate::command::RouteHandle::from_id(route))
1606 }
1607 _ => Err(format!(
1608 "std.native.Command/{operation} route belongs to a different application"
1609 )),
1610 }
1611}
1612
1613fn native_command_config_argument(
1614 value: &Value,
1615 operation: &str,
1616) -> Result<crate::command::AppConfig, String> {
1617 let Some(entries) = map_entries(value) else {
1618 return Err(format!(
1619 "std.native.Command/{operation} expects a config map"
1620 ));
1621 };
1622 let config = Value::Map(PMap::from_iter(entries));
1623 let id = match map_value(&config, &native_command_keyword("id")) {
1624 Some(Value::Symbol(id)) if !id.as_str().is_empty() => id.as_str().to_owned(),
1625 _ => {
1626 return Err(format!(
1627 "std.native.Command/{operation} config :id must be a symbol"
1628 ))
1629 }
1630 };
1631 let desc = match map_value(&config, &native_command_keyword("desc")) {
1632 Some(Value::String(desc)) if !desc.trim().is_empty() => desc.clone(),
1633 _ => {
1634 return Err(format!(
1635 "std.native.Command/{operation} config :desc must be a non-empty string"
1636 ))
1637 }
1638 };
1639 Ok(crate::command::AppConfig { id, desc })
1640}
1641
1642fn native_command_vector(
1643 value: &Value,
1644 operation: &str,
1645 field: &str,
1646) -> Result<Vec<Value>, String> {
1647 match value {
1648 Value::Vector(values) => Ok(values.iter().cloned().collect()),
1649 Value::Tuple(values) => Ok(values.iter().cloned().collect()),
1650 _ => Err(format!(
1651 "std.native.Command/{operation} {field} must be a vector"
1652 )),
1653 }
1654}
1655
1656fn native_command_string(value: &Value, operation: &str, field: &str) -> Result<String, String> {
1657 match value {
1658 Value::String(value) => Ok(value.clone()),
1659 _ => Err(format!(
1660 "std.native.Command/{operation} {field} must be a string"
1661 )),
1662 }
1663}
1664
1665fn native_command_strings(
1666 value: &Value,
1667 operation: &str,
1668 field: &str,
1669) -> Result<Vec<String>, String> {
1670 native_command_vector(value, operation, field)?
1671 .into_iter()
1672 .map(|value| native_command_string(&value, operation, field))
1673 .collect()
1674}
1675
1676fn native_command_keyword_argument(
1677 value: &Value,
1678 operation: &str,
1679 field: &str,
1680) -> Result<String, String> {
1681 match value {
1682 Value::Keyword(value) if !value.as_str().is_empty() => Ok(value.as_str().to_owned()),
1683 _ => Err(format!(
1684 "std.native.Command/{operation} {field} must be a keyword"
1685 )),
1686 }
1687}
1688
1689fn native_command_boolean(
1690 value: Option<&Value>,
1691 operation: &str,
1692 field: &str,
1693 fallback: bool,
1694) -> Result<bool, String> {
1695 match value {
1696 None | Some(Value::Nil) => Ok(fallback),
1697 Some(Value::Bool(value)) => Ok(*value),
1698 _ => Err(format!(
1699 "std.native.Command/{operation} {field} must be a boolean"
1700 )),
1701 }
1702}
1703
1704fn native_command_option(
1705 value: Value,
1706 operation: &str,
1707) -> Result<crate::command::OptionSpec, String> {
1708 let Some(entries) = map_entries(&value) else {
1709 return Err(format!(
1710 "std.native.Command/{operation} :options entries must be maps"
1711 ));
1712 };
1713 let value = Value::Map(PMap::from_iter(entries));
1714 let id = native_command_keyword_argument(
1715 map_value(&value, &native_command_keyword("id"))
1716 .ok_or_else(|| format!("std.native.Command/{operation} option :id is required"))?,
1717 operation,
1718 "option :id",
1719 )?;
1720 let long = match map_value(&value, &native_command_keyword("long")) {
1721 None | Some(Value::Nil) => None,
1722 Some(value) => Some(native_command_string(value, operation, "option :long")?),
1723 };
1724 let short = match map_value(&value, &native_command_keyword("short")) {
1725 None | Some(Value::Nil) => None,
1726 Some(Value::String(value)) if value.chars().count() == 1 => value.chars().next(),
1727 _ => {
1728 return Err(format!(
1729 "std.native.Command/{operation} option :short must be one character"
1730 ))
1731 }
1732 };
1733 let kind = match map_value(&value, &native_command_keyword("type")) {
1734 Some(Value::Keyword(kind)) if kind.as_str() == "boolean" => {
1735 crate::command::OptionKind::Boolean
1736 }
1737 Some(Value::Keyword(kind)) if kind.as_str() == "string" => {
1738 crate::command::OptionKind::String
1739 }
1740 _ => {
1741 return Err(format!(
1742 "std.native.Command/{operation} option :type must be :boolean or :string"
1743 ))
1744 }
1745 };
1746 let many = native_command_boolean(
1747 map_value(&value, &native_command_keyword("many?")),
1748 operation,
1749 "option :many?",
1750 false,
1751 )?;
1752 let default = match map_value(&value, &native_command_keyword("default")) {
1753 None | Some(Value::Nil) => None,
1754 Some(Value::Bool(value)) => Some(crate::command::ParsedValue::Boolean(*value)),
1755 Some(Value::String(value)) => Some(crate::command::ParsedValue::String(value.clone())),
1756 Some(value) => Some(crate::command::ParsedValue::Strings(
1757 native_command_strings(value, operation, "option :default")?,
1758 )),
1759 };
1760 Ok(crate::command::OptionSpec {
1761 id,
1762 long,
1763 short,
1764 kind,
1765 many,
1766 default,
1767 })
1768}
1769
1770fn native_command_argument(
1771 value: Value,
1772 operation: &str,
1773) -> Result<crate::command::ArgumentSpec, String> {
1774 let Some(entries) = map_entries(&value) else {
1775 return Err(format!(
1776 "std.native.Command/{operation} :arguments entries must be maps"
1777 ));
1778 };
1779 let value = Value::Map(PMap::from_iter(entries));
1780 Ok(crate::command::ArgumentSpec {
1781 id: native_command_keyword_argument(
1782 map_value(&value, &native_command_keyword("id")).ok_or_else(|| {
1783 format!("std.native.Command/{operation} argument :id is required")
1784 })?,
1785 operation,
1786 "argument :id",
1787 )?,
1788 required: native_command_boolean(
1789 map_value(&value, &native_command_keyword("required?")),
1790 operation,
1791 "argument :required?",
1792 true,
1793 )?,
1794 many: native_command_boolean(
1795 map_value(&value, &native_command_keyword("many?")),
1796 operation,
1797 "argument :many?",
1798 false,
1799 )?,
1800 })
1801}
1802
1803fn native_command_route_argument(
1804 value: Value,
1805 operation: &str,
1806) -> Result<crate::command::Route<Value>, String> {
1807 let Some(entries) = map_entries(&value) else {
1808 return Err(format!(
1809 "std.native.Command/{operation} expects a route map"
1810 ));
1811 };
1812 let value = Value::Map(PMap::from_iter(entries));
1813 let path = native_command_strings(
1814 map_value(&value, &native_command_keyword("path"))
1815 .ok_or_else(|| format!("std.native.Command/{operation} route :path is required"))?,
1816 operation,
1817 "route :path",
1818 )?;
1819 let aliases = match map_value(&value, &native_command_keyword("aliases")) {
1820 None | Some(Value::Nil) => Vec::new(),
1821 Some(value) => native_command_vector(value, operation, "route :aliases")?
1822 .into_iter()
1823 .map(|alias| native_command_strings(&alias, operation, "route :aliases"))
1824 .collect::<Result<Vec<_>, _>>()?,
1825 };
1826 let options = match map_value(&value, &native_command_keyword("options")) {
1827 None | Some(Value::Nil) => Vec::new(),
1828 Some(value) => native_command_vector(value, operation, "route :options")?
1829 .into_iter()
1830 .map(|value| native_command_option(value, operation))
1831 .collect::<Result<Vec<_>, _>>()?,
1832 };
1833 let arguments = match map_value(&value, &native_command_keyword("arguments")) {
1834 None | Some(Value::Nil) => Vec::new(),
1835 Some(value) => native_command_vector(value, operation, "route :arguments")?
1836 .into_iter()
1837 .map(|value| native_command_argument(value, operation))
1838 .collect::<Result<Vec<_>, _>>()?,
1839 };
1840 let handler = match map_value(&value, &native_command_keyword("handler")) {
1841 Some(Value::Function(function)) => Value::Function(function.clone()),
1842 _ => {
1843 return Err(format!(
1844 "std.native.Command/{operation} route :handler must be a function"
1845 ))
1846 }
1847 };
1848 Ok(crate::command::Route {
1849 spec: crate::command::RouteSpec {
1850 id: native_command_keyword_argument(
1851 map_value(&value, &native_command_keyword("id")).ok_or_else(|| {
1852 format!("std.native.Command/{operation} route :id is required")
1853 })?,
1854 operation,
1855 "route :id",
1856 )?,
1857 path,
1858 aliases,
1859 desc: native_command_string(
1860 map_value(&value, &native_command_keyword("desc")).ok_or_else(|| {
1861 format!("std.native.Command/{operation} route :desc is required")
1862 })?,
1863 operation,
1864 "route :desc",
1865 )?,
1866 options,
1867 arguments,
1868 passthrough: native_command_boolean(
1869 map_value(&value, &native_command_keyword("passthrough?")),
1870 operation,
1871 "route :passthrough?",
1872 false,
1873 )?,
1874 },
1875 handler,
1876 })
1877}
1878
1879fn native_command_parsed_value(value: &crate::command::ParsedValue) -> Value {
1880 match value {
1881 crate::command::ParsedValue::Boolean(value) => Value::Bool(*value),
1882 crate::command::ParsedValue::String(value) => Value::String(value.clone()),
1883 crate::command::ParsedValue::Strings(values) => Value::Vector(PVector::from_iter(
1884 values.iter().cloned().map(Value::String),
1885 )),
1886 }
1887}
1888
1889fn native_command_spec_value(spec: &crate::command::RouteSpec) -> Value {
1890 native_command_map([
1891 (
1892 native_command_keyword("id"),
1893 native_command_keyword(&spec.id),
1894 ),
1895 (
1896 native_command_keyword("path"),
1897 Value::Vector(PVector::from_iter(
1898 spec.path.iter().cloned().map(Value::String),
1899 )),
1900 ),
1901 (
1902 native_command_keyword("aliases"),
1903 Value::Vector(PVector::from_iter(spec.aliases.iter().map(|alias| {
1904 Value::Vector(PVector::from_iter(alias.iter().cloned().map(Value::String)))
1905 }))),
1906 ),
1907 (
1908 native_command_keyword("desc"),
1909 Value::String(spec.desc.clone()),
1910 ),
1911 (
1912 native_command_keyword("passthrough?"),
1913 Value::Bool(spec.passthrough),
1914 ),
1915 (
1916 native_command_keyword("options"),
1917 Value::Vector(PVector::from_iter(spec.options.iter().map(|option| {
1918 let mut entries = vec![
1919 (
1920 native_command_keyword("id"),
1921 native_command_keyword(&option.id),
1922 ),
1923 (
1924 native_command_keyword("long"),
1925 Value::String(option.long_name()),
1926 ),
1927 (
1928 native_command_keyword("short"),
1929 option
1930 .short
1931 .map(|short| Value::String(short.into()))
1932 .unwrap_or(Value::Nil),
1933 ),
1934 (
1935 native_command_keyword("type"),
1936 native_command_keyword(match option.kind {
1937 crate::command::OptionKind::Boolean => "boolean",
1938 crate::command::OptionKind::String => "string",
1939 }),
1940 ),
1941 (native_command_keyword("many?"), Value::Bool(option.many)),
1942 ];
1943 if let Some(default) = &option.default {
1944 entries.push((
1945 native_command_keyword("default"),
1946 native_command_parsed_value(default),
1947 ));
1948 }
1949 native_command_map(entries)
1950 }))),
1951 ),
1952 (
1953 native_command_keyword("arguments"),
1954 Value::Vector(PVector::from_iter(spec.arguments.iter().map(|argument| {
1955 native_command_map([
1956 (
1957 native_command_keyword("id"),
1958 native_command_keyword(&argument.id),
1959 ),
1960 (
1961 native_command_keyword("required?"),
1962 Value::Bool(argument.required),
1963 ),
1964 (native_command_keyword("many?"), Value::Bool(argument.many)),
1965 ])
1966 }))),
1967 ),
1968 ])
1969}
1970
1971fn native_command_invocation(
1972 value: Value,
1973 operation: &str,
1974) -> Result<(Vec<String>, Value), String> {
1975 let Some(entries) = map_entries(&value) else {
1976 return Err(format!(
1977 "std.native.Command/{operation} expects an invocation map"
1978 ));
1979 };
1980 let value = Value::Map(PMap::from_iter(entries));
1981 let argv = native_command_strings(
1982 map_value(&value, &native_command_keyword("argv")).ok_or_else(|| {
1983 format!("std.native.Command/{operation} invocation :argv is required")
1984 })?,
1985 operation,
1986 "invocation :argv",
1987 )?;
1988 let context = map_value(&value, &native_command_keyword("context"))
1989 .cloned()
1990 .unwrap_or_else(|| Value::Map(PMap::new()));
1991 if map_entries(&context).is_none() {
1992 return Err(format!(
1993 "std.native.Command/{operation} invocation :context must be a map"
1994 ));
1995 }
1996 Ok((argv, context))
1997}
1998
1999fn native_command_request_value(
2000 request_id: u64,
2001 request: &crate::command::Request,
2002 context: Value,
2003) -> Value {
2004 native_command_map([
2005 (
2006 native_command_keyword("app/id"),
2007 Value::Symbol(Symbol::parse(&request.app_id)),
2008 ),
2009 (
2010 native_command_keyword("route/id"),
2011 native_command_keyword(&request.route_id),
2012 ),
2013 (
2014 native_command_keyword("route/path"),
2015 Value::Vector(PVector::from_iter(
2016 request.route_path.iter().cloned().map(Value::String),
2017 )),
2018 ),
2019 (
2020 native_command_keyword("argv"),
2021 Value::Vector(PVector::from_iter(
2022 request.argv.iter().cloned().map(Value::String),
2023 )),
2024 ),
2025 (
2026 native_command_keyword("arguments"),
2027 native_command_map(request.arguments.iter().map(|(key, value)| {
2028 (
2029 native_command_keyword(key),
2030 native_command_parsed_value(value),
2031 )
2032 })),
2033 ),
2034 (
2035 native_command_keyword("options"),
2036 native_command_map(request.options.iter().map(|(key, value)| {
2037 (
2038 native_command_keyword(key),
2039 native_command_parsed_value(value),
2040 )
2041 })),
2042 ),
2043 (native_command_keyword("context"), context),
2044 (
2045 native_command_keyword("command/request"),
2046 native_command_pointer(
2047 "command/request",
2048 [(
2049 native_command_keyword("id"),
2050 Value::Number(request_id as i64),
2051 )],
2052 ),
2053 ),
2054 ])
2055}
2056
2057fn native_command_response_value(response: crate::command::Response) -> Value {
2058 native_command_map([
2059 (
2060 native_command_keyword("stdout"),
2061 Value::String(response.stdout),
2062 ),
2063 (
2064 native_command_keyword("stderr"),
2065 Value::String(response.stderr),
2066 ),
2067 (native_command_keyword("exit"), Value::Number(response.exit)),
2068 ])
2069}
2070
2071fn native_command_response_argument(
2072 value: Value,
2073 operation: &str,
2074) -> Result<crate::command::Response, String> {
2075 let Some(entries) = map_entries(&value) else {
2076 return Err(format!(
2077 "std.native.Command/{operation} handler must return a response map"
2078 ));
2079 };
2080 if entries.len() != 3 {
2081 return Err(format!(
2082 "std.native.Command/{operation} response must contain only :stdout, :stderr, and :exit"
2083 ));
2084 }
2085 let value = Value::Map(PMap::from_iter(entries));
2086 let stdout = native_command_string(
2087 map_value(&value, &native_command_keyword("stdout")).ok_or_else(|| {
2088 format!("std.native.Command/{operation} response :stdout is required")
2089 })?,
2090 operation,
2091 "response :stdout",
2092 )?;
2093 let stderr = native_command_string(
2094 map_value(&value, &native_command_keyword("stderr")).ok_or_else(|| {
2095 format!("std.native.Command/{operation} response :stderr is required")
2096 })?,
2097 operation,
2098 "response :stderr",
2099 )?;
2100 let exit = match map_value(&value, &native_command_keyword("exit")) {
2101 Some(Value::Number(value)) => *value,
2102 _ => {
2103 return Err(format!(
2104 "std.native.Command/{operation} response :exit must be an integer"
2105 ))
2106 }
2107 };
2108 crate::command::Response {
2109 stdout,
2110 stderr,
2111 exit,
2112 }
2113 .checked()
2114 .map_err(|error| error.to_string())
2115}
2116
2117fn native_command_parse(app: u64, invocation: Value, operation: &str) -> Result<Value, String> {
2118 let (argv, context) = native_command_invocation(invocation, operation)?;
2119 NATIVE_COMMAND_STATE.with(|state| {
2120 let mut state = state.borrow_mut();
2121 let request = state
2122 .apps
2123 .get(&app)
2124 .ok_or_else(|| format!("std.native.Command/{operation} application was not found"))?
2125 .parse(argv)
2126 .map_err(|error| error.to_string())?;
2127 let request_id = state.next_request;
2128 state.next_request += 1;
2129 state.requests.insert(
2130 request_id,
2131 NativeCommandRequest {
2132 app,
2133 request: request.clone(),
2134 context: context.clone(),
2135 },
2136 );
2137 Ok(native_command_request_value(request_id, &request, context))
2138 })
2139}
2140
2141fn native_command_request_id(value: &Value, operation: &str) -> Result<u64, String> {
2142 let Some(request) = map_value(value, &native_command_keyword("command/request")) else {
2143 return Err(format!(
2144 "std.native.Command/{operation} expects a Command/parse request"
2145 ));
2146 };
2147 native_command_handle(request, "command/request", operation)
2148}
2149
2150fn native_command_dispatch(
2151 app: u64,
2152 request_value: Value,
2153 operation: &str,
2154) -> Result<Value, String> {
2155 let request_id = native_command_request_id(&request_value, operation)?;
2156 let (handler, request, context) = NATIVE_COMMAND_STATE.with(|state| {
2157 let state = state.borrow();
2158 let stored = state
2159 .requests
2160 .get(&request_id)
2161 .ok_or_else(|| format!("std.native.Command/{operation} request was not found"))?;
2162 if stored.app != app {
2163 return Err(format!(
2164 "std.native.Command/{operation} request belongs to a different application"
2165 ));
2166 }
2167 let application = state
2168 .apps
2169 .get(&app)
2170 .ok_or_else(|| format!("std.native.Command/{operation} application was not found"))?;
2171 let handler = application
2172 .handler(&stored.request)
2173 .map_err(|error| error.to_string())?
2174 .clone();
2175 Ok::<_, String>((handler, stored.request.clone(), stored.context.clone()))
2176 })?;
2177 let output = call_value(
2178 handler,
2179 vec![native_command_request_value(request_id, &request, context)],
2180 )?;
2181 native_command_response_argument(output, operation).map(native_command_response_value)
2182}
2183
2184fn native_command_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
2185 let operation = operation
2186 .strip_prefix("std.native.Command/")
2187 .unwrap_or(operation);
2188 match operation {
2189 "create" => match values.as_slice() {
2190 [config] => {
2191 let config = native_command_config_argument(config, operation)?;
2192 NATIVE_COMMAND_STATE.with(|state| {
2193 let mut state = state.borrow_mut();
2194 let id = state.next_app;
2195 state.next_app += 1;
2196 state.apps.insert(
2197 id,
2198 crate::command::App::create(config).map_err(|error| error.to_string())?,
2199 );
2200 Ok(native_command_pointer(
2201 "command/app",
2202 [(native_command_keyword("id"), Value::Number(id as i64))],
2203 ))
2204 })
2205 }
2206 _ => Err("std.native.Command/create expects one config map".into()),
2207 },
2208 "config" => match values.as_slice() {
2209 [app] => {
2210 let app = native_command_app(app, operation)?;
2211 NATIVE_COMMAND_STATE.with(|state| {
2212 let state = state.borrow();
2213 let config = state
2214 .apps
2215 .get(&app)
2216 .ok_or_else(|| {
2217 "std.native.Command/config application was not found".to_owned()
2218 })?
2219 .config();
2220 Ok(native_command_map([
2221 (
2222 native_command_keyword("id"),
2223 Value::Symbol(Symbol::parse(&config.id)),
2224 ),
2225 (native_command_keyword("desc"), Value::String(config.desc)),
2226 ]))
2227 })
2228 }
2229 _ => Err("std.native.Command/config expects one application".into()),
2230 },
2231 "install" => match values.as_slice() {
2232 [app, route] => {
2233 let app = native_command_app(app, operation)?;
2234 let route = native_command_route_argument(route.clone(), operation)?;
2235 NATIVE_COMMAND_STATE.with(|state| {
2236 let mut state = state.borrow_mut();
2237 let handle = state
2238 .apps
2239 .get_mut(&app)
2240 .ok_or_else(|| {
2241 "std.native.Command/install application was not found".to_owned()
2242 })?
2243 .install(route)
2244 .map_err(|error| error.to_string())?;
2245 Ok(native_command_pointer(
2246 "command/route",
2247 [
2248 (
2249 native_command_keyword("id"),
2250 Value::Number(handle.id() as i64),
2251 ),
2252 (native_command_keyword("app"), Value::Number(app as i64)),
2253 ],
2254 ))
2255 })
2256 }
2257 _ => Err("std.native.Command/install expects an application and route map".into()),
2258 },
2259 "uninstall" => match values.as_slice() {
2260 [app, route] => {
2261 let app = native_command_app(app, operation)?;
2262 let route = native_command_route(route, app, operation)?;
2263 NATIVE_COMMAND_STATE.with(|state| {
2264 state
2265 .borrow_mut()
2266 .apps
2267 .get_mut(&app)
2268 .ok_or_else(|| {
2269 "std.native.Command/uninstall application was not found".to_owned()
2270 })?
2271 .uninstall(route)
2272 .map(Value::Bool)
2273 .map_err(|error| error.to_string())
2274 })
2275 }
2276 _ => Err("std.native.Command/uninstall expects an application and route handle".into()),
2277 },
2278 "routes" => match values.as_slice() {
2279 [app] => {
2280 let app = native_command_app(app, operation)?;
2281 NATIVE_COMMAND_STATE.with(|state| {
2282 state
2283 .borrow()
2284 .apps
2285 .get(&app)
2286 .ok_or_else(|| {
2287 "std.native.Command/routes application was not found".to_owned()
2288 })?
2289 .routes()
2290 .map(|routes| {
2291 Value::Vector(PVector::from_iter(
2292 routes.iter().map(native_command_spec_value),
2293 ))
2294 })
2295 .map_err(|error| error.to_string())
2296 })
2297 }
2298 _ => Err("std.native.Command/routes expects one application".into()),
2299 },
2300 "snapshot" => match values.as_slice() {
2301 [app] => {
2302 let app = native_command_app(app, operation)?;
2303 NATIVE_COMMAND_STATE.with(|state| {
2304 let mut state = state.borrow_mut();
2305 let snapshot = state
2306 .apps
2307 .get(&app)
2308 .ok_or_else(|| {
2309 "std.native.Command/snapshot application was not found".to_owned()
2310 })?
2311 .snapshot()
2312 .map_err(|error| error.to_string())?;
2313 let id = state.next_snapshot;
2314 state.next_snapshot += 1;
2315 state
2316 .snapshots
2317 .insert(id, NativeCommandSnapshot { app, snapshot });
2318 Ok(native_command_pointer(
2319 "command/snapshot",
2320 [
2321 (native_command_keyword("id"), Value::Number(id as i64)),
2322 (native_command_keyword("app"), Value::Number(app as i64)),
2323 ],
2324 ))
2325 })
2326 }
2327 _ => Err("std.native.Command/snapshot expects one application".into()),
2328 },
2329 "restore" => match values.as_slice() {
2330 [app, snapshot] => {
2331 let app = native_command_app(app, operation)?;
2332 let snapshot_id = native_command_handle(snapshot, "command/snapshot", operation)?;
2333 NATIVE_COMMAND_STATE.with(|state| {
2334 let mut state = state.borrow_mut();
2335 let snapshot = state
2336 .snapshots
2337 .get(&snapshot_id)
2338 .ok_or_else(|| "std.native.Command/restore snapshot was not found".to_owned())?;
2339 if snapshot.app != app {
2340 return Err("std.native.Command/restore snapshot belongs to a different application".into());
2341 }
2342 let snapshot = snapshot.snapshot.clone();
2343 state
2344 .apps
2345 .get_mut(&app)
2346 .ok_or_else(|| "std.native.Command/restore application was not found".to_owned())?
2347 .restore(snapshot)
2348 .map_err(|error| error.to_string())?;
2349 Ok(values[0].clone())
2350 })
2351 }
2352 _ => Err("std.native.Command/restore expects an application and snapshot".into()),
2353 },
2354 "reset" => match values.as_slice() {
2355 [app] => {
2356 let app = native_command_app(app, operation)?;
2357 NATIVE_COMMAND_STATE.with(|state| {
2358 let mut state = state.borrow_mut();
2359 state
2360 .apps
2361 .get_mut(&app)
2362 .ok_or_else(|| {
2363 "std.native.Command/reset application was not found".to_owned()
2364 })?
2365 .reset()
2366 .map_err(|error| error.to_string())?;
2367 state.requests.retain(|_, request| request.app != app);
2368 Ok(values[0].clone())
2369 })
2370 }
2371 _ => Err("std.native.Command/reset expects one application".into()),
2372 },
2373 "closed?" => match values.as_slice() {
2374 [app] => {
2375 let app = native_command_app(app, operation)?;
2376 NATIVE_COMMAND_STATE.with(|state| {
2377 state
2378 .borrow()
2379 .apps
2380 .get(&app)
2381 .map(|app| Value::Bool(app.closed()))
2382 .ok_or_else(|| {
2383 "std.native.Command/closed? application was not found".to_owned()
2384 })
2385 })
2386 }
2387 _ => Err("std.native.Command/closed? expects one application".into()),
2388 },
2389 "close" => match values.as_slice() {
2390 [app] => {
2391 let app = native_command_app(app, operation)?;
2392 NATIVE_COMMAND_STATE.with(|state| {
2393 let mut state = state.borrow_mut();
2394 state
2395 .apps
2396 .get_mut(&app)
2397 .ok_or_else(|| {
2398 "std.native.Command/close application was not found".to_owned()
2399 })?
2400 .close();
2401 state.requests.retain(|_, request| request.app != app);
2402 Ok(Value::Nil)
2403 })
2404 }
2405 _ => Err("std.native.Command/close expects one application".into()),
2406 },
2407 "parse" => match values.as_slice() {
2408 [app, invocation] => native_command_parse(
2409 native_command_app(app, operation)?,
2410 invocation.clone(),
2411 operation,
2412 ),
2413 _ => Err("std.native.Command/parse expects an application and invocation map".into()),
2414 },
2415 "dispatch" => match values.as_slice() {
2416 [app, request] => native_command_dispatch(
2417 native_command_app(app, operation)?,
2418 request.clone(),
2419 operation,
2420 ),
2421 _ => {
2422 Err("std.native.Command/dispatch expects an application and parsed request".into())
2423 }
2424 },
2425 "run" => match values.as_slice() {
2426 [app, invocation] => {
2427 let app = native_command_app(app, operation)?;
2428 match native_command_parse(app, invocation.clone(), operation) {
2429 Ok(request) => match native_command_dispatch(app, request, operation) {
2430 Ok(response) => Ok(response),
2431 Err(error) => Ok(native_command_response_value(
2432 crate::command::Response::failure(1, error),
2433 )),
2434 },
2435 Err(error) => Ok(native_command_response_value(
2436 crate::command::Response::failure(2, error),
2437 )),
2438 }
2439 }
2440 _ => Err("std.native.Command/run expects an application and invocation map".into()),
2441 },
2442 _ => Err(format!("unknown std.native.Command operation: {operation}")),
2443 }
2444}
2445
2446fn native_regex_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
2447 let operation = operation
2448 .strip_prefix("std.native.RegExp/")
2449 .unwrap_or(operation);
2450 match operation {
2451 "compile" => {
2452 if values.len() != 1 {
2453 return Err("std.native.RegExp/compile expects one string".into());
2454 }
2455 let pattern = match &values[0] {
2456 Value::String(pattern) => pattern.clone(),
2457 _ => return Err("std.native.RegExp/compile expects one string".into()),
2458 };
2459 regex::Regex::new(&pattern).map_err(|error| format!("invalid regexp: {error}"))?;
2460 Ok(Value::Regex(pattern))
2461 }
2462 "pattern" => {
2463 if values.len() != 1 {
2464 return Err("std.native.RegExp/pattern expects one regexp".into());
2465 }
2466 match &values[0] {
2467 Value::Regex(pattern) => Ok(Value::String(pattern.clone())),
2468 _ => Err("std.native.RegExp/pattern expects one regexp".into()),
2469 }
2470 }
2471 "find?" => {
2472 if values.len() != 2 {
2473 return Err("std.native.RegExp/find? expects a regexp and string".into());
2474 }
2475 let pattern = match &values[0] {
2476 Value::Regex(pattern) => pattern.clone(),
2477 _ => return Err("std.native.RegExp/find? expects a regexp and string".into()),
2478 };
2479 let input = match &values[1] {
2480 Value::String(input) => input.clone(),
2481 _ => return Err("std.native.RegExp/find? expects a regexp and string".into()),
2482 };
2483 let regexp =
2484 regex::Regex::new(&pattern).map_err(|error| format!("invalid regexp: {error}"))?;
2485 Ok(Value::Bool(regexp.is_match(&input)))
2486 }
2487 "find" => {
2488 if values.len() != 2 {
2489 return Err("std.native.RegExp/find expects a regexp and string".into());
2490 }
2491 let pattern = match &values[0] {
2492 Value::Regex(pattern) => pattern.clone(),
2493 _ => return Err("std.native.RegExp/find expects a regexp and string".into()),
2494 };
2495 let input = match &values[1] {
2496 Value::String(input) => input.clone(),
2497 _ => return Err("std.native.RegExp/find expects a regexp and string".into()),
2498 };
2499 let regexp =
2500 regex::Regex::new(&pattern).map_err(|error| format!("invalid regexp: {error}"))?;
2501 Ok(regexp
2502 .find(&input)
2503 .map(|matched| Value::String(matched.as_str().to_owned()))
2504 .unwrap_or(Value::Nil))
2505 }
2506 "matches" => {
2507 if values.len() != 2 {
2508 return Err("std.native.RegExp/matches expects a regexp and string".into());
2509 }
2510 let pattern = match &values[0] {
2511 Value::Regex(pattern) => pattern.clone(),
2512 _ => return Err("std.native.RegExp/matches expects a regexp and string".into()),
2513 };
2514 let input = match &values[1] {
2515 Value::String(input) => input.clone(),
2516 _ => return Err("std.native.RegExp/matches expects a regexp and string".into()),
2517 };
2518 let anchored = format!(r"\A(?:{pattern})\z");
2519 let regexp =
2520 regex::Regex::new(&anchored).map_err(|error| format!("invalid regexp: {error}"))?;
2521 Ok(Value::Bool(regexp.is_match(&input)))
2522 }
2523 "replace" => {
2524 if values.len() != 3 {
2525 return Err(
2526 "std.native.RegExp/replace expects a regexp, string, and replacement".into(),
2527 );
2528 }
2529 let pattern = match &values[0] {
2530 Value::Regex(pattern) => pattern.clone(),
2531 _ => {
2532 return Err(
2533 "std.native.RegExp/replace expects a regexp, string, and replacement"
2534 .into(),
2535 )
2536 }
2537 };
2538 let input = match &values[1] {
2539 Value::String(input) => input.clone(),
2540 _ => {
2541 return Err(
2542 "std.native.RegExp/replace expects a regexp, string, and replacement"
2543 .into(),
2544 )
2545 }
2546 };
2547 let replacement = match &values[2] {
2548 Value::String(replacement) => replacement.clone(),
2549 _ => {
2550 return Err(
2551 "std.native.RegExp/replace expects a regexp, string, and replacement"
2552 .into(),
2553 )
2554 }
2555 };
2556 let regexp =
2557 regex::Regex::new(&pattern).map_err(|error| format!("invalid regexp: {error}"))?;
2558 let replacement = native_regexp_replacement(&replacement, regexp.captures_len());
2559 Ok(Value::String(
2560 regexp
2561 .replace_all(&input, replacement.as_str())
2562 .into_owned(),
2563 ))
2564 }
2565 "split" => {
2566 if values.len() != 2 {
2567 return Err("std.native.RegExp/split expects a regexp and string".into());
2568 }
2569 let pattern = match &values[0] {
2570 Value::Regex(pattern) => pattern.clone(),
2571 _ => return Err("std.native.RegExp/split expects a regexp and string".into()),
2572 };
2573 let input = match &values[1] {
2574 Value::String(input) => input.clone(),
2575 _ => return Err("std.native.RegExp/split expects a regexp and string".into()),
2576 };
2577 if input.is_empty() {
2578 return Ok(Value::Nil);
2579 }
2580 if pattern.is_empty() {
2581 return Ok(Value::Vector(PVector::from_iter(
2582 input
2583 .chars()
2584 .map(|character| Value::String(character.to_string())),
2585 )));
2586 }
2587 let regexp =
2588 regex::Regex::new(&pattern).map_err(|error| format!("invalid regexp: {error}"))?;
2589 Ok(Value::Vector(PVector::from_iter(
2590 regexp
2591 .split(&input)
2592 .map(|part| Value::String(part.to_owned())),
2593 )))
2594 }
2595 _ => Err(format!("unknown std.native.RegExp operation: {operation}")),
2596 }
2597}
2598
2599fn native_regexp_replacement(replacement: &str, capture_count: usize) -> String {
2603 let mut output = String::with_capacity(replacement.len());
2604 let mut characters = replacement.chars().peekable();
2605
2606 while let Some(character) = characters.next() {
2607 if character != '$' {
2608 output.push(character);
2609 continue;
2610 }
2611
2612 let Some(first) = characters.peek().and_then(|value| value.to_digit(10)) else {
2613 output.push('$');
2614 continue;
2615 };
2616 let mut capture = first as usize;
2617 if capture >= capture_count {
2618 output.push('$');
2619 continue;
2620 }
2621 characters.next();
2622
2623 while let Some(next) = characters.peek().and_then(|value| value.to_digit(10)) {
2624 let Some(candidate) = capture
2625 .checked_mul(10)
2626 .and_then(|value| value.checked_add(next as usize))
2627 else {
2628 break;
2629 };
2630 if candidate >= capture_count {
2631 break;
2632 }
2633 capture = candidate;
2634 characters.next();
2635 }
2636
2637 output.push_str("${");
2638 output.push_str(&capture.to_string());
2639 output.push('}');
2640 }
2641
2642 output
2643}
2644
2645fn file_error(operation: &str, error: FileError) -> String {
2646 let method = operation
2647 .strip_prefix("std.native.File/")
2648 .unwrap_or(operation);
2649 format!("file/{method} failed: file/{}", error.code())
2650}
2651
2652fn socket_error(operation: &str, error: SocketError) -> String {
2653 format!("{operation} failed: socket/{}", error.code())
2654}
2655
2656fn active_file_provider() -> Option<Rc<dyn FileProvider>> {
2657 ACTIVE_FILE_PROVIDER.with(|active| active.borrow().clone())
2658}
2659
2660fn rejected_file_effect(
2661 operation: &str,
2662 path: &str,
2663 target: Option<&str>,
2664 error: FileError,
2665) -> Value {
2666 let promise = Promise::new();
2667 promise.reject_value(crate::file::file_error_value(
2668 operation, path, target, &error,
2669 ));
2670 Value::Promise(promise)
2671}
2672
2673fn file_effect(
2674 operation: &str,
2675 path: &str,
2676 target: Option<&str>,
2677 invoke: impl FnOnce(&dyn FileProvider) -> Result<Promise, FileError>,
2678) -> Value {
2679 let Some(provider) = active_file_provider() else {
2680 return rejected_file_effect(operation, path, target, FileError::Denied);
2681 };
2682 match invoke(provider.as_ref()) {
2683 Ok(promise) => Value::Promise(promise),
2684 Err(error) => rejected_file_effect(operation, path, target, error),
2685 }
2686}
2687
2688fn file_option(options: &Value, name: &str) -> Option<Value> {
2689 let key = Value::Keyword(name.into());
2690 map_entries(options)?
2691 .into_iter()
2692 .find_map(|(candidate, value)| (candidate == key).then_some(value))
2693}
2694
2695fn file_options_value(value: Value, operation: &str) -> Result<Value, String> {
2696 match value {
2697 Value::Nil => Ok(Value::Map(PMap::new())),
2698 value if map_entries(&value).is_some() => Ok(value),
2699 _ => Err(format!("{operation} options must be a map")),
2700 }
2701}
2702
2703fn file_bool_option(
2704 options: &Value,
2705 name: &str,
2706 default: bool,
2707 operation: &str,
2708) -> Result<bool, String> {
2709 match file_option(options, name) {
2710 None => Ok(default),
2711 Some(Value::Bool(value)) => Ok(value),
2712 Some(_) => Err(format!("{operation} :{name} must be boolean")),
2713 }
2714}
2715
2716fn file_string_option(
2717 options: &Value,
2718 name: &str,
2719 default: &str,
2720 operation: &str,
2721) -> Result<String, String> {
2722 match file_option(options, name) {
2723 None => Ok(default.into()),
2724 Some(Value::String(value)) => Ok(value),
2725 Some(_) => Err(format!("{operation} :{name} must be a string")),
2726 }
2727}
2728
2729fn file_write_options(options: &Value) -> Result<WriteOptions, String> {
2730 let mode = match file_option(options, "mode") {
2731 None => WriteMode::Create,
2732 Some(Value::Keyword(value)) if value.as_str() == "create" => WriteMode::Create,
2733 Some(Value::Keyword(value)) if value.as_str() == "replace" => WriteMode::Replace,
2734 Some(Value::Keyword(value)) if value.as_str() == "append" => WriteMode::Append,
2735 Some(_) => {
2736 return Err("std.native.File/write :mode must be :create, :replace, or :append".into())
2737 }
2738 };
2739 Ok(WriteOptions {
2740 mode,
2741 parents: file_bool_option(options, "parents?", false, "std.native.File/write")?,
2742 })
2743}
2744
2745fn file_mkdir_options(options: &Value) -> Result<MkdirOptions, String> {
2746 Ok(MkdirOptions {
2747 parents: file_bool_option(options, "parents?", true, "std.native.File/mkdir")?,
2748 exists_ok: file_bool_option(options, "exists-ok?", true, "std.native.File/mkdir")?,
2749 })
2750}
2751
2752fn file_delete_options(options: &Value) -> Result<DeleteOptions, String> {
2753 Ok(DeleteOptions {
2754 missing_ok: file_bool_option(options, "missing-ok?", false, "std.native.File/delete")?,
2755 })
2756}
2757
2758fn file_copy_options(options: &Value) -> Result<CopyOptions, String> {
2759 Ok(CopyOptions {
2760 replace: file_bool_option(options, "replace?", false, "std.native.File/copy")?,
2761 parents: file_bool_option(options, "parents?", false, "std.native.File/copy")?,
2762 preserve_modified: file_bool_option(
2763 options,
2764 "preserve-modified?",
2765 false,
2766 "std.native.File/copy",
2767 )?,
2768 })
2769}
2770
2771fn file_move_options(options: &Value) -> Result<MoveOptions, String> {
2772 Ok(MoveOptions {
2773 replace: file_bool_option(options, "replace?", false, "std.native.File/move")?,
2774 parents: file_bool_option(options, "parents?", false, "std.native.File/move")?,
2775 atomic: file_bool_option(options, "atomic?", false, "std.native.File/move")?,
2776 })
2777}
2778
2779fn file_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
2780 let effect_operation = operation
2781 .strip_prefix("std.native.File/")
2782 .map(|method| format!("file/{method}"))
2783 .unwrap_or_else(|| operation.to_owned());
2784 match operation {
2785 "std.native.File/join" | "std.native.File/resolve" => {
2786 if values.len() != 2 {
2787 return Err(format!("{operation} expects a base and path"));
2788 }
2789 let Value::String(base) = &values[0] else {
2790 return Err(format!("{operation} expects a base and path"));
2791 };
2792 let Value::String(path) = &values[1] else {
2793 return Err(format!("{operation} expects a base and path"));
2794 };
2795 let result = if operation == "std.native.File/join" {
2796 crate::file::logical_join(&base, &path)
2797 } else {
2798 crate::file::logical_resolve(&base, &path)
2799 };
2800 result
2801 .map(Value::String)
2802 .map_err(|error| file_error(operation, error))
2803 }
2804 "std.native.File/parent" => {
2805 if values.len() != 1 {
2806 return Err("std.native.File/parent expects a path".into());
2807 }
2808 let Value::String(path) = &values[0] else {
2809 return Err("std.native.File/parent expects a path".into());
2810 };
2811 crate::file::logical_parent(path)
2812 .map(|parent| parent.map(Value::String).unwrap_or(Value::Nil))
2813 .map_err(|error| file_error(operation, error))
2814 }
2815 "std.native.File/read"
2816 | "std.native.File/exists?"
2817 | "std.native.File/stat"
2818 | "std.native.File/entries"
2819 | "std.native.File/list"
2820 | "std.native.File/walk" => {
2821 if values.len() != 1 {
2822 return Err(format!("{operation} expects a path"));
2823 }
2824 let Value::String(path) = &values[0] else {
2825 return Err(format!("{operation} expects a path"));
2826 };
2827 Ok(file_effect(
2828 &effect_operation,
2829 &path,
2830 None,
2831 |provider| match operation {
2832 "std.native.File/read" => provider.read(&path),
2833 "std.native.File/exists?" => provider.exists(&path),
2834 "std.native.File/stat" => provider.stat(&path),
2835 "std.native.File/entries" => provider.entries(&path),
2836 "std.native.File/list" => provider.list(&path),
2837 "std.native.File/walk" => provider.walk(&path),
2838 _ => unreachable!(),
2839 },
2840 ))
2841 }
2842 "std.native.File/write" => {
2843 if !(2..=3).contains(&values.len()) {
2844 return Err(
2845 "std.native.File/write expects a path, bytes, and optional options".into(),
2846 );
2847 }
2848 let Value::String(path) = &values[0] else {
2849 return Err("std.native.File/write expects a path and bytes".into());
2850 };
2851 let bytes = match &values[1] {
2852 Value::Bytes(value) => value.clone(),
2853 Value::ByteBuffer(value) => value.borrow().clone(),
2854 _ => return Err("std.native.File/write expects a path and bytes".into()),
2855 };
2856 let options = if values.len() == 3 {
2857 file_options_value(values[2].clone(), operation)?
2858 } else {
2859 Value::Map(PMap::new())
2860 };
2861 let options = file_write_options(&options)?;
2862 Ok(file_effect(&effect_operation, &path, None, |provider| {
2863 provider.write_with_options(&path, bytes, options)
2864 }))
2865 }
2866 "std.native.File/mkdir" => {
2867 if !(1..=2).contains(&values.len()) {
2868 return Err("std.native.File/mkdir expects a path and optional options".into());
2869 }
2870 let Value::String(path) = &values[0] else {
2871 return Err("std.native.File/mkdir expects a path".into());
2872 };
2873 let options = if values.len() == 2 {
2874 file_options_value(values[1].clone(), operation)?
2875 } else {
2876 Value::Map(PMap::new())
2877 };
2878 let options = file_mkdir_options(&options)?;
2879 Ok(file_effect(&effect_operation, &path, None, |provider| {
2880 provider.mkdir_with_options(&path, options)
2881 }))
2882 }
2883 "std.native.File/delete" => {
2884 if !(1..=2).contains(&values.len()) {
2885 return Err("std.native.File/delete expects a path and optional options".into());
2886 }
2887 let Value::String(path) = &values[0] else {
2888 return Err("std.native.File/delete expects a path".into());
2889 };
2890 let options = if values.len() == 2 {
2891 file_options_value(values[1].clone(), operation)?
2892 } else {
2893 Value::Map(PMap::new())
2894 };
2895 let options = file_delete_options(&options)?;
2896 Ok(file_effect(&effect_operation, &path, None, |provider| {
2897 provider.delete_with_options(&path, options)
2898 }))
2899 }
2900 "std.native.File/copy" | "std.native.File/move" => {
2901 if !(2..=3).contains(&values.len()) {
2902 return Err(format!(
2903 "{operation} expects source, target, and optional options"
2904 ));
2905 }
2906 let Value::String(source) = &values[0] else {
2907 return Err(format!("{operation} expects source and target paths"));
2908 };
2909 let Value::String(target) = &values[1] else {
2910 return Err(format!("{operation} expects source and target paths"));
2911 };
2912 let options = if values.len() == 3 {
2913 file_options_value(values[2].clone(), operation)?
2914 } else {
2915 Value::Map(PMap::new())
2916 };
2917 Ok(if operation == "std.native.File/copy" {
2918 let options = file_copy_options(&options)?;
2919 file_effect(&effect_operation, &source, Some(&target), |provider| {
2920 provider.copy(&source, &target, options)
2921 })
2922 } else {
2923 let options = file_move_options(&options)?;
2924 file_effect(&effect_operation, &source, Some(&target), |provider| {
2925 provider.move_entry(&source, &target, options)
2926 })
2927 })
2928 }
2929 "std.native.File/temp-file" | "std.native.File/temp-directory" => {
2930 if !(1..=2).contains(&values.len()) {
2931 return Err(format!("{operation} expects a parent and optional options"));
2932 }
2933 let Value::String(parent) = &values[0] else {
2934 return Err(format!("{operation} expects a parent path"));
2935 };
2936 let options = if values.len() == 2 {
2937 file_options_value(values[1].clone(), operation)?
2938 } else {
2939 Value::Map(PMap::new())
2940 };
2941 Ok(if operation == "std.native.File/temp-file" {
2942 let options = TempFileOptions {
2943 prefix: file_string_option(&options, "prefix", "tmp", operation)?,
2944 suffix: file_string_option(&options, "suffix", "", operation)?,
2945 };
2946 file_effect(&effect_operation, &parent, None, |provider| {
2947 provider.temp_file(&parent, options)
2948 })
2949 } else {
2950 let options = TempDirectoryOptions {
2951 prefix: file_string_option(&options, "prefix", "tmp", operation)?,
2952 };
2953 file_effect(&effect_operation, &parent, None, |provider| {
2954 provider.temp_directory(&parent, options)
2955 })
2956 })
2957 }
2958 _ => Err(format!("unknown std.native.File operation: {operation}")),
2959 }
2960}
2961
2962fn socket_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
2963 let operation = operation
2964 .strip_prefix("std.native.Socket/")
2965 .unwrap_or(operation);
2966 match operation {
2967 "receive-stream" | "socket/receive-stream" => {
2968 if values.len() != 1 {
2969 return Err(format!("Socket/{operation} expects a socket connection"));
2970 }
2971 let socket = socket_handle(&values[0], &format!("Socket/{operation}"))?;
2972 let events = socket_provider(operation)?
2973 .events(socket)
2974 .map_err(|e| socket_error(operation, e))?;
2975 Ok(host_stream(
2976 Rc::new(move || socket_receive_promise(events)),
2977 Rc::new(|| Ok(())),
2978 ))
2979 }
2980 "socket/connect" => {
2981 if values.len() != 4 {
2982 return Err("socket/connect expects a host, port, options, and callback".into());
2983 }
2984 let host = match &values[0] {
2985 Value::String(value) => value.clone(),
2986 _ => {
2987 return Err("socket/connect expects a host, port, options, and callback".into())
2988 }
2989 };
2990 let port = value_u16_integer(&values[1], "socket/connect", false)?;
2991 let _options = &values[2];
2992 let callback = match &values[3] {
2993 Value::Function(value) => value.clone(),
2994 _ => return Err("socket/connect expects a callback".into()),
2995 };
2996 let callback = Rc::new(move |event| {
2997 let arguments = match event {
2998 SocketEvent::Connected(handle) => {
2999 vec![Value::Nil, Value::Number(handle as i64)]
3000 }
3001 SocketEvent::Failed(_, error) => vec![Value::String(error), Value::Nil],
3002 SocketEvent::Data(_, _) | SocketEvent::Closed(_) => return,
3003 };
3004 let _ = call_function(&callback, arguments);
3005 });
3006 socket_provider(operation)?
3007 .connect(&host, port, callback)
3008 .map(|handle| Value::Number(handle as i64))
3009 .map_err(|error| socket_error(operation, error))
3010 }
3011 "socket/listen" => {
3012 if values.len() != 4 {
3013 return Err("socket/listen expects a host, port, options, and callback".into());
3014 }
3015 let host = match &values[0] {
3016 Value::String(value) => value.clone(),
3017 _ => return Err("socket/listen expects a host string".into()),
3018 };
3019 let port = value_u16_integer(&values[1], "socket/listen", true)?;
3020 let _options = &values[2];
3021 let callback = match &values[3] {
3022 Value::Function(value) => value.clone(),
3023 _ => return Err("socket/listen expects a callback".into()),
3024 };
3025 let callback = Rc::new(move |event| {
3026 let _ = call_function(&callback, vec![socket_server_event_value(event)]);
3027 });
3028 socket_provider(operation)?
3029 .listen(&host, port, callback)
3030 .map(|handle| Value::Number(handle as i64))
3031 .map_err(|error| socket_error(operation, error))
3032 }
3033 "socket/endpoint" => {
3034 if values.len() != 1 {
3035 return Err("socket/endpoint expects a server".into());
3036 }
3037 let server = socket_handle(&values[0], "socket/endpoint")?;
3038 socket_provider(operation)?
3039 .endpoint(server)
3040 .map(|(host, port)| {
3041 Value::Map(PMap::from_iter([
3042 (Value::Keyword("host".into()), Value::String(host)),
3043 (Value::Keyword("port".into()), Value::Number(port as i64)),
3044 ]))
3045 })
3046 .map_err(|error| socket_error(operation, error))
3047 }
3048 "socket/events" => {
3049 if values.len() != 2 {
3050 return Err("socket/events expects a socket handle and options".into());
3051 }
3052 let handle = socket_handle(&values[0], "socket/events")?;
3053 let _options = &values[1];
3054 socket_provider(operation)?
3055 .events(handle)
3056 .map(|stream| Value::Number(stream as i64))
3057 .map_err(|error| socket_error(operation, error))
3058 }
3059 "socket/next" => {
3060 if values.len() != 1 {
3061 return Err("socket/next expects a socket stream".into());
3062 }
3063 let stream = socket_handle(&values[0], "socket/next")?;
3064 socket_provider(operation)?
3065 .next(stream)
3066 .map(Value::Promise)
3067 .map_err(|error| socket_error(operation, error))
3068 }
3069 "socket/send" => {
3070 if values.len() != 2 {
3071 return Err("socket/send expects a socket connection and bytes".into());
3072 }
3073 let socket = socket_handle(&values[0], "socket/send")?;
3074 let bytes = match &values[1] {
3075 Value::Bytes(value) => value.clone(),
3076 Value::ByteBuffer(value) => value.borrow().clone(),
3077 _ => return Err("socket/send expects a socket connection and bytes".into()),
3078 };
3079 socket_provider(operation)?
3080 .send(socket, &bytes)
3081 .map(|count| Value::Number(count as i64))
3082 .map_err(|error| socket_error(operation, error))
3083 }
3084 "socket/close" => {
3085 if values.len() != 1 {
3086 return Err("socket/close expects a socket connection".into());
3087 }
3088 let socket = socket_handle(&values[0], "socket/close")?;
3089 socket_provider(operation)?
3090 .close(socket)
3091 .map(|()| Value::Nil)
3092 .map_err(|error| socket_error(operation, error))
3093 }
3094 _ => Err(format!("unknown std.native.Socket operation: {operation}")),
3095 }
3096}
3097
3098fn socket_receive_promise(stream: SocketHandle) -> Result<Promise, String> {
3099 let source = socket_provider("Socket/receive-stream")?
3100 .next(stream)
3101 .map_err(|e| socket_error("Socket/receive-stream", e))?;
3102 let output = Promise::new();
3103 let settled = output.clone();
3104 source.on_settle(Rc::new(move |result| match result {
3105 PromiseState::Rejected(error) => {
3106 settled.reject_rejection(error);
3107 }
3108 PromiseState::Pending => {}
3109 PromiseState::Fulfilled(event) => {
3110 let entries = map_entries(&event).unwrap_or_default();
3111 let kind = entries.iter().find_map(|(k, v)| {
3112 if matches!(k, Value::Keyword(key) if key.as_str() == "type") {
3113 Some(v.clone())
3114 } else {
3115 None
3116 }
3117 });
3118 match kind {
3119 Some(Value::Keyword(kind)) if kind.as_str() == "data" => {
3120 let bytes = entries
3121 .into_iter()
3122 .find_map(|(k, v)| {
3123 if matches!(k, Value::Keyword(key) if key.as_str() == "bytes") {
3124 Some(v)
3125 } else {
3126 None
3127 }
3128 })
3129 .unwrap_or(Value::Nil);
3130 settled.resolve(bytes);
3131 }
3132 Some(Value::Keyword(kind)) if kind.as_str() == "close" => {
3133 settled.resolve(Value::Nil);
3134 }
3135 Some(Value::Keyword(kind)) if kind.as_str() == "error" => {
3136 settled.reject("socket receive failed");
3137 }
3138 _ => {
3139 settled.reject("Socket/receive-stream received an invalid event");
3140 }
3141 }
3142 }
3143 }));
3144 let poll = source.clone();
3145 output.set_poller(Rc::new(move || {
3146 poll.state();
3147 }));
3148 let wait = source.clone();
3149 output.set_waiter(Rc::new(move || {
3150 wait.wait_state();
3151 }));
3152 Ok(output)
3153}
3154
3155fn socket_handle(value: &Value, operation: &str) -> Result<SocketHandle, String> {
3156 value_u64_integer(value, operation)
3157 .map(|value| value as SocketHandle)
3158 .map_err(|_| format!("{operation} expects a socket handle"))
3159}
3160
3161fn native_host_values(operation: &str, values: Vec<Value>) -> Result<Value, String> {
3162 let method = operation
3163 .strip_prefix("std.native.Host/")
3164 .unwrap_or(operation);
3165 let (service, target, arguments) = match method {
3166 "call" => {
3167 if values.len() != 3 {
3168 return Err(
3169 "std.native.Host/call expects service, method, and an argument vector".into(),
3170 );
3171 }
3172 let service = match &values[0] {
3173 Value::String(value) => value.clone(),
3174 _ => return Err("std.native.Host/call service must be a string".into()),
3175 };
3176 let target = match &values[1] {
3177 Value::String(value) => value.clone(),
3178 _ => return Err("std.native.Host/call method must be a string".into()),
3179 };
3180 let arguments = match &values[2] {
3181 Value::Vector(values) => values.iter().cloned().collect(),
3182 Value::Tuple(values) => values.iter().cloned().collect(),
3183 _ => return Err("std.native.Host/call arguments must be a vector".into()),
3184 };
3185 (service, target, arguments)
3186 }
3187 "describe" | "capabilities" => {
3188 if !values.is_empty() {
3189 return Err(format!("std.native.Host/{method} expects no arguments"));
3190 }
3191 ("host".into(), method.into(), Vec::new())
3192 }
3193 "capability?" => {
3194 if values.len() != 1 {
3195 return Err("std.native.Host/capability? expects one capability".into());
3196 }
3197 ("host".into(), "capability?".into(), vec![values[0].clone()])
3198 }
3199 _ => return Err(format!("unknown std.native.Host method: {method}")),
3200 };
3201 HOST_CALL_HANDLER.with(|active| {
3202 let Some(handler) = active.borrow().as_ref().cloned() else {
3203 let promise = Promise::new();
3204 promise.reject_value(host_error(
3205 "host/unavailable",
3206 "Host capability provider is unavailable",
3207 ));
3208 return Ok(Value::Promise(promise));
3209 };
3210 handler(service, target, arguments)
3211 })
3212}
3213
3214pub(crate) fn namespace_identifier(value: Value, operation: &str) -> Result<String, String> {
3215 match value {
3216 Value::Symbol(name) if name.get_namespace().is_none() => Ok(name.as_str().to_owned()),
3217 Value::String(name) => Ok(name),
3218 Value::Namespace(namespace) => Ok(namespace.name().as_str().to_owned()),
3219 _ => Err(format!(
3220 "{operation} expects an unqualified namespace symbol, string, or Namespace"
3221 )),
3222 }
3223}
3224
3225fn namespace_descriptor(registry: &NamespaceRegistry<Value>, name: &str) -> Value {
3226 let state = registry
3227 .load_state(name)
3228 .or_else(|| registry.find(name).map(|_| NamespaceLoadState::Loaded))
3229 .map(NamespaceLoadState::as_str)
3230 .unwrap_or("unknown");
3231 let package = package_catalog().coordinate_for_namespace(name);
3232 let origin = if name.starts_with("std.native") {
3233 "embedded"
3234 } else if package.is_some() {
3235 "package"
3236 } else if registry.find(name).is_some() {
3237 "runtime"
3238 } else {
3239 "registered"
3240 };
3241 let mut fields = vec![
3242 (
3243 Value::Keyword("namespace/name".into()),
3244 Value::Symbol(Symbol::parse(name)),
3245 ),
3246 (
3247 Value::Keyword("namespace/state".into()),
3248 Value::Keyword(state.into()),
3249 ),
3250 (
3251 Value::Keyword("namespace/role".into()),
3252 Value::Keyword(
3253 registry
3254 .find(name)
3255 .map(|namespace| namespace.role())
3256 .unwrap_or_else(|| "standard".into())
3257 .into(),
3258 ),
3259 ),
3260 (
3261 Value::Keyword("namespace/revision".into()),
3262 Value::Number(registry.module_revision(name) as i64),
3263 ),
3264 (
3265 Value::Keyword("namespace/origin".into()),
3266 Value::Keyword(origin.into()),
3267 ),
3268 ];
3269 if let Some(package) = package {
3270 fields.push((
3271 Value::Keyword("namespace/package".into()),
3272 Value::String(package),
3273 ));
3274 }
3275 Value::OrderedMap(Box::new(POrderedMap::from_iter(fields)))
3276}
3277
3278fn native_runtime_values(
3279 operation: &str,
3280 values: Vec<Value>,
3281 env: &mut HashMap<String, Value>,
3282) -> Result<Value, String> {
3283 let method = operation
3284 .strip_prefix("std.native.Runtime/")
3285 .unwrap_or(operation);
3286 let registry = namespace_registry()?;
3287 match method {
3288 "ns-publics" => {
3289 let namespace = match values.as_slice() {
3290 [Value::Symbol(name)] if name.get_namespace().is_none() => name.as_str().to_owned(),
3291 [Value::String(name)] => name.clone(),
3292 [Value::Namespace(namespace)] => namespace.name().as_str().to_owned(),
3293 _ => {
3294 return Err(
3295 "std.native.Runtime/ns-publics expects a namespace symbol or string".into(),
3296 )
3297 }
3298 };
3299 let target = registry
3300 .find(&namespace)
3301 .ok_or_else(|| format!("No such namespace: {namespace}"))?;
3302 let mut mappings = target.mappings();
3303 mappings.retain(|(_, var)| var.symbol().get_namespace() == Some(namespace.as_str()));
3304 mappings.sort_by(|(left, _), (right, _)| left.as_str().cmp(right.as_str()));
3305 Ok(Value::OrderedMap(Box::new(POrderedMap::from_iter(
3306 mappings.into_iter().map(|(name, var)| {
3307 (
3308 Value::Symbol(Symbol::create(None, name.as_str())),
3309 Value::Var(var),
3310 )
3311 }),
3312 ))))
3313 }
3314 "ns-aliases" => {
3315 let name = match values.as_slice() {
3316 [value] => namespace_identifier(value.clone(), "std.native.Runtime/ns-aliases")?,
3317 _ => return Err("std.native.Runtime/ns-aliases expects one namespace".into()),
3318 };
3319 let target = registry
3320 .find(&name)
3321 .ok_or_else(|| format!("No such namespace: {name}"))?;
3322 let mut aliases = target.aliases();
3323 aliases.sort_by(|(left, _), (right, _)| left.as_str().cmp(right.as_str()));
3324 Ok(Value::OrderedMap(Box::new(POrderedMap::from_iter(
3325 aliases.into_iter().map(|(alias, namespace)| {
3326 (Value::Symbol(alias), Value::Namespace(Rc::new(namespace)))
3327 }),
3328 ))))
3329 }
3330 "ns-find" => {
3331 if values.len() != 1 {
3332 return Err("std.native.Runtime/ns-find expects one namespace".into());
3333 }
3334 let name = namespace_identifier(values[0].clone(), "std.native.Runtime/ns-find")?;
3335 Ok(registry
3336 .find(&name)
3337 .map(|namespace| Value::Namespace(Rc::new(namespace)))
3338 .unwrap_or(Value::Nil))
3339 }
3340 "ns-create" => match values.as_slice() {
3341 [Value::Symbol(name)] if name.get_namespace().is_none() => {
3342 let namespace = registry.find_or_create(name.as_str());
3343 Ok(Value::Namespace(Rc::new(namespace)))
3344 }
3345 _ => Err("std.native.Runtime/ns-create expects an unqualified symbol".into()),
3346 },
3347 "ns-name" => match values.as_slice() {
3348 [Value::Namespace(namespace)] => Ok(Value::Symbol(namespace.name().clone())),
3349 [Value::Symbol(name)]
3350 if name.get_namespace().is_none()
3351 && namespace_registry()?.find(name.as_str()).is_some() =>
3352 {
3353 Ok(Value::Symbol(name.clone()))
3354 }
3355 _ => Err("std.native.Runtime/ns-name expects a namespace".into()),
3356 },
3357 "current" => {
3358 if !values.is_empty() {
3359 return Err("std.native.Runtime/current expects no arguments".into());
3360 }
3361 Ok(Value::Symbol(registry.current().name().clone()))
3362 }
3363 "snapshot" => {
3364 if !values.is_empty() {
3365 return Err("std.native.Runtime/snapshot expects no arguments".into());
3366 }
3367 let namespaces = registry
3368 .known_names()
3369 .into_iter()
3370 .map(|name| namespace_descriptor(®istry, name.as_str()))
3371 .collect::<Vec<_>>();
3372 Ok(Value::OrderedMap(Box::new(POrderedMap::from_iter([
3373 (
3374 Value::Keyword("env/current".into()),
3375 Value::Symbol(registry.current().name().clone()),
3376 ),
3377 (
3378 Value::Keyword("env/namespaces".into()),
3379 Value::Vector(PVector::from(namespaces)),
3380 ),
3381 ]))))
3382 }
3383 "namespaces" => {
3384 if !values.is_empty() {
3385 return Err("std.native.Runtime/namespaces expects no arguments".into());
3386 }
3387 Ok(Value::Vector(PVector::from(
3388 registry
3389 .known_names()
3390 .into_iter()
3391 .map(|name| namespace_descriptor(®istry, name.as_str()))
3392 .collect::<Vec<_>>(),
3393 )))
3394 }
3395 "namespace" => {
3396 if values.len() != 1 {
3397 return Err("std.native.Runtime/namespace expects one namespace".into());
3398 }
3399 let name = namespace_identifier(values[0].clone(), operation)?;
3400 if registry.load_state(&name).is_none() && registry.find(&name).is_none() {
3401 Ok(Value::Nil)
3402 } else {
3403 Ok(namespace_descriptor(®istry, &name))
3404 }
3405 }
3406 "module" => {
3407 if values.len() != 1 {
3408 return Err("std.native.Runtime/module expects one module path".into());
3409 }
3410 let requested = match &values[0] {
3411 Value::String(path) => path.clone(),
3412 Value::Symbol(name) => name.as_str().to_owned(),
3413 _ => {
3414 return Err(
3415 "std.native.Runtime/module expects a path string or namespace symbol"
3416 .into(),
3417 )
3418 }
3419 };
3420 let source = requested.strip_prefix("classpath:").unwrap_or(&requested);
3421 let namespace = if source.ends_with(".hal") || source.ends_with(".hrl") {
3422 source
3423 .trim_end_matches(".hal")
3424 .trim_end_matches(".hrl")
3425 .trim_start_matches("./")
3426 .replace('/', ".")
3427 } else {
3428 source.to_owned()
3429 };
3430 let revision = registry.module_revision(&namespace);
3431 if revision == 0
3432 && registry.load_state(&namespace).is_none()
3433 && registry.find(&namespace).is_none()
3434 {
3435 return Ok(Value::Nil);
3436 }
3437 let dependencies = registry
3438 .module_dependencies(&namespace)
3439 .into_iter()
3440 .map(|dependency| {
3441 Value::String(format!("{}.hal", dependency.as_str().replace('.', "/")))
3442 })
3443 .collect::<Vec<_>>();
3444 Ok(Value::OrderedMap(Box::new(POrderedMap::from_iter([
3445 (
3446 Value::Keyword("module/path".into()),
3447 Value::String(requested),
3448 ),
3449 (
3450 Value::Keyword("module/namespace".into()),
3451 Value::Symbol(Symbol::parse(&namespace)),
3452 ),
3453 (
3454 Value::Keyword("module/revision".into()),
3455 Value::Number(revision as i64),
3456 ),
3457 (
3458 Value::Keyword("module/dependencies".into()),
3459 Value::Vector(PVector::from(dependencies)),
3460 ),
3461 ]))))
3462 }
3463 "vars" => {
3464 if values.len() > 1 {
3465 return Err("std.native.Runtime/vars expects zero or one namespace".into());
3466 }
3467 let name = if values.is_empty() {
3468 registry.current().name().as_str().to_owned()
3469 } else {
3470 namespace_identifier(values[0].clone(), operation)?
3471 };
3472 let namespace = registry
3473 .find(&name)
3474 .ok_or_else(|| format!("namespace/not-found: {name}"))?;
3475 let mut mappings = namespace.mappings();
3476 mappings.retain(|(_, var)| var.symbol().get_namespace() == Some(name.as_str()));
3477 mappings.sort_by(|(left, _), (right, _)| left.as_str().cmp(right.as_str()));
3478 Ok(Value::OrderedMap(Box::new(POrderedMap::from_iter(
3479 mappings.into_iter().map(|(symbol, var)| {
3480 (
3481 Value::Symbol(Symbol::create(None, symbol.as_str())),
3482 Value::Var(var),
3483 )
3484 }),
3485 ))))
3486 }
3487 "eval" => {
3488 if values.len() != 1 {
3489 return Err("std.native.Runtime/eval expects one form".into());
3490 }
3491 let mut environment = crate::core::current_namespace_environment()?;
3492 let result = eval_value(values[0].clone(), &mut environment);
3493 crate::core::save_namespace_environment(®istry, &mut environment);
3494 result
3495 }
3496 "load-string" => {
3497 let [Value::String(source)] = values.as_slice() else {
3498 return Err("std.native.Runtime/load-string expects one string".into());
3499 };
3500 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
3501 if direct_native_execution() {
3502 return eval_direct_native_source(source);
3503 }
3504 eval_value_text(source, env)
3505 }
3506 "var-sym" => {
3507 let [Value::Var(var)] = values.as_slice() else {
3508 return Err("std.native.Runtime/var-sym expects one Var".into());
3509 };
3510 Ok(Value::Symbol(var.symbol().clone()))
3511 }
3512 "macroexpand-1" => {
3513 if values.len() != 1 {
3514 return Err("std.native.Runtime/macroexpand-1 expects one form".into());
3515 }
3516 let form = value_to_form(&values[0])?;
3517 let mut environment = current_namespace_environment()?;
3518 form_to_value(¯oexpand_once(&form, &mut environment)?)
3519 }
3520 "gensym" => {
3521 let prefix = match values.as_slice() {
3522 [] => "G__".to_owned(),
3523 [Value::String(prefix)] => prefix.clone(),
3524 [value] => {
3525 return Err(format!(
3526 "gensym expects a string prefix, got {}",
3527 portable_type_name(value)
3528 ))
3529 }
3530 _ => return Err("gensym expects zero or one arguments".into()),
3531 };
3532 Ok(Value::Symbol(Symbol::from(gensym(&prefix))))
3533 }
3534 "eval-in" => {
3535 if values.len() != 2 {
3536 return Err("std.native.Runtime/eval-in expects namespace and forms".into());
3537 }
3538 let target = namespace_identifier(values[0].clone(), operation)?;
3539 if registry.find(&target).is_none() {
3540 return Err(format!(
3541 "std.native.Runtime/eval-in requires an existing namespace: {target}"
3542 ));
3543 }
3544 let forms = iterator_values(values[1].clone())?
3545 .into_iter()
3546 .map(|value| value_to_form(&value))
3547 .collect::<Result<Vec<_>, _>>()?;
3548 let previous = registry.current().name().as_str().to_owned();
3549 select_namespace_environment(®istry, env, &target);
3550 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
3551 let result = if direct_native_execution() {
3552 let source = if forms.is_empty() {
3553 "nil".to_owned()
3554 } else {
3555 Form::List(
3556 std::iter::once(Form::Symbol("do".into()))
3557 .chain(forms.iter().cloned())
3558 .collect(),
3559 )
3560 .to_string()
3561 };
3562 eval_direct_native_source(&source)
3563 } else {
3564 let mut result = Value::Nil;
3565 for form in &forms {
3566 result = eval(form, env)?;
3567 }
3568 Ok(result)
3569 };
3570 #[cfg(not(all(feature = "direct-native", not(target_arch = "wasm32"))))]
3571 let result = {
3572 let mut result = Value::Nil;
3573 for form in &forms {
3574 result = eval(form, env)?;
3575 }
3576 Ok(result)
3577 };
3578 select_namespace_environment(®istry, env, &previous);
3579 result
3580 }
3581 "alias-state" => {
3582 if values.len() != 1 && values.len() != 2 {
3583 return Err(
3584 "std.native.Runtime/alias-state expects alias or namespace and alias".into(),
3585 );
3586 }
3587 let (owner, alias_value) = if values.len() == 2 {
3588 (
3589 namespace_identifier(values[0].clone(), operation)?,
3590 &values[1],
3591 )
3592 } else {
3593 (registry.current().name().as_str().to_owned(), &values[0])
3594 };
3595 let Value::Symbol(alias) = alias_value else {
3596 return Err(
3597 "std.native.Runtime/alias-state expects an unqualified alias symbol".into(),
3598 );
3599 };
3600 if alias.get_namespace().is_some() {
3601 return Err(
3602 "std.native.Runtime/alias-state expects an unqualified alias symbol".into(),
3603 );
3604 }
3605 let Some(namespace) = registry.find(&owner) else {
3606 return Ok(Value::Nil);
3607 };
3608 let target = namespace
3612 .lazy_target(alias.as_str())
3613 .or_else(|| {
3614 namespace
3615 .aliases()
3616 .into_iter()
3617 .find(|(name, _)| name == alias)
3618 .map(|(_, target)| target.name().clone())
3619 })
3620 .or_else(|| {
3621 registry
3622 .global_aliases()
3623 .into_iter()
3624 .find(|(name, _)| name == alias)
3625 .map(|(_, target)| target)
3626 });
3627 let Some(target) = target else {
3628 return Ok(Value::Nil);
3629 };
3630 let state = registry
3631 .load_state(target.as_str())
3632 .or_else(|| {
3633 registry
3634 .find(target.as_str())
3635 .map(|_| NamespaceLoadState::Loaded)
3636 })
3637 .map(NamespaceLoadState::as_str)
3638 .unwrap_or("unknown");
3639 Ok(Value::Map(PMap::from_iter([
3640 (Value::Keyword("alias".into()), Value::Symbol(alias.clone())),
3641 (Value::Keyword("target".into()), Value::Symbol(target)),
3642 (Value::Keyword("state".into()), Value::Keyword(state.into())),
3643 ])))
3644 }
3645 "intern-var" => {
3646 if values.len() != 3 && values.len() != 4 {
3647 return Err(
3648 "std.native.Runtime/intern-var expects namespace, symbol, Var, and optional metadata"
3649 .into(),
3650 );
3651 }
3652 let target = namespace_identifier(values[0].clone(), operation)?;
3653 let Value::Symbol(name) = &values[1] else {
3654 return Err(
3655 "std.native.Runtime/intern-var expects an unqualified target symbol".into(),
3656 );
3657 };
3658 if name.get_namespace().is_some() {
3659 return Err(
3660 "std.native.Runtime/intern-var expects an unqualified target symbol".into(),
3661 );
3662 }
3663 let Value::Var(source) = &values[2] else {
3664 return Err("std.native.Runtime/intern-var expects a source Var".into());
3665 };
3666 let mut metadata = source.metadata();
3667 if let Some(extension) = values.get(3) {
3668 let Some(entries) = map_entries(extension) else {
3669 return Err(
3670 "std.native.Runtime/intern-var metadata extension must be a map".into(),
3671 );
3672 };
3673 for (key, value) in entries {
3674 metadata.extra.insert(key.display(), value.display());
3675 }
3676 }
3677 let value = source.deref_value();
3678 if let Value::Function(function) = &value {
3679 if function.is_macro {
3680 ACTIVE_MACROS.with(|active| {
3681 if let Some(macros) = active.borrow().as_ref() {
3682 macros.borrow_mut().insert(
3683 (target.clone(), name.as_str().to_owned()),
3684 function.clone(),
3685 );
3686 }
3687 });
3688 }
3689 }
3690 Ok(Value::Var(
3691 registry.find_or_create(&target).intern_with_metadata(
3692 name.as_str(),
3693 value,
3694 metadata,
3695 ),
3696 ))
3697 }
3698 _ => Err(format!("unknown std.native.Runtime method: {method}")),
3699 }
3700}
3701
3702#[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
3703fn eval_direct_native_source(source: &str) -> Result<Value, String> {
3704 let context = DirectNativeContext::capture();
3705 let forms = crate::kernel::read_forms(source).map_err(|error| error.to_string())?;
3706 let has_namespace_form = forms.iter().any(|form| {
3707 matches!(
3708 form_without_metadata(&form.form),
3709 Form::List(items)
3710 if matches!(items.first(), Some(Form::Symbol(operator)) if operator == "ns" || operator == "ns+")
3711 )
3712 });
3713 let config = if has_namespace_form {
3714 crate::vm::source_namespace_config(&forms).map_err(|error| error.to_string())?
3715 } else {
3716 crate::kernel::GeneratedNamespaceConfig::defaults()
3717 };
3718 let namespaces = context.namespaces.clone();
3719 let program = context
3720 .with(|| {
3721 without_direct_native_execution(|| {
3722 crate::vm::compile_source_with_config_allow_unbound_globals(
3723 source,
3724 &namespaces,
3725 config,
3726 )
3727 })
3728 })
3729 .map_err(|error| error.to_string())?;
3730 let mut program = program;
3731 if program.namespace.is_none() {
3732 program.namespace = Some(context.namespace.clone());
3733 }
3734 let engine = crate::direct_native::NativeEngine::new();
3735 let result = context.with(|| engine.execute_blocking(Rc::new(program)));
3740 let nested_multimethods = context.multimethods.borrow().clone();
3744 ACTIVE_MULTIMETHODS.with(|active| {
3745 active.borrow_mut().extend(nested_multimethods);
3746 });
3747 result.map(|report| report.value)
3748}
3749
3750fn eval_value(value: Value, env: &mut HashMap<String, Value>) -> Result<Value, String> {
3751 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
3752 if direct_native_execution() {
3753 return eval_direct_native_source(&value_to_form(&value)?.to_string());
3754 }
3755 eval(&value_to_form(&value)?, env)
3756}
3757
3758fn native_package_values(
3759 operation: &str,
3760 arguments: Vec<Value>,
3761 env: &mut HashMap<String, Value>,
3762) -> Result<Value, String> {
3763 let method = operation
3764 .strip_prefix("std.native.Package/")
3765 .unwrap_or(operation);
3766 if matches!(
3767 method,
3768 "build" | "inspect" | "seal" | "inspect-seal" | "verify-seal" | "distribute"
3769 ) {
3770 return native_package_artifact_values(method, arguments);
3771 }
3772 if method == "read" {
3773 if arguments.len() != 2 {
3774 return Err(
3775 "std.native.Package/read expects an exact descriptor and relative path".into(),
3776 );
3777 }
3778 let Value::OrderedMap(_) = &arguments[0] else {
3779 return Err("std.native.Package/read expects an exact package descriptor".into());
3780 };
3781 let Value::String(relative) = &arguments[1] else {
3782 return Err("std.native.Package/read expects a relative path string".into());
3783 };
3784 return package_catalog()
3785 .read(&arguments[0], relative)
3786 .map(Value::Bytes);
3787 }
3788 let expected = match method {
3789 "catalog" => 0..=0,
3790 "find" | "ensure" | "load" | "state" => 1..=1,
3791 "unload" => 1..=2,
3792 _ => return Err(format!("unknown std.native.Package method: {method}")),
3793 };
3794 if !expected.contains(&arguments.len()) {
3795 return Err(format!(
3796 "std.native.Package/{method} expects {} arguments",
3797 expected.start()
3798 ));
3799 }
3800 let catalog = package_catalog();
3801 if method == "catalog" {
3802 return Ok(catalog.catalog_value());
3803 }
3804 let target = match arguments.first() {
3805 Some(Value::Symbol(value)) => value.as_str().to_owned(),
3806 Some(Value::String(value)) => value.clone(),
3807 Some(Value::Keyword(value)) => value.as_str().to_owned(),
3808 Some(value @ Value::OrderedMap(_)) if method == "ensure" || method == "unload" => {
3809 package_descriptor_coordinate(value).ok_or_else(|| {
3810 format!("std.native.Package/{method} descriptor requires :package/coordinate")
3811 })?
3812 }
3813 _ => {
3814 return Err(format!(
3815 "std.native.Package/{method} expects a namespace, coordinate, or exact descriptor"
3816 ))
3817 }
3818 };
3819 let found = catalog.find(&target);
3820 if method == "find" {
3821 return Ok(found.map(|(_, value)| value).unwrap_or(Value::Nil));
3822 }
3823 let Some((coordinate, descriptor)) = found else {
3824 if method == "state" {
3825 return Ok(Value::Nil);
3826 }
3827 return Err(format!("package/not-locked: {target}"));
3828 };
3829 if method == "state" {
3830 return Ok(Value::Keyword(
3831 catalog
3832 .state(&coordinate)
3833 .unwrap_or_else(|| "available".into())
3834 .into(),
3835 ));
3836 }
3837 if method == "load" {
3838 if catalog.coordinate_for_namespace(&target).as_deref() != Some(&coordinate) {
3839 return Err("std.native.Package/load expects a locked namespace".into());
3840 }
3841 if catalog.state(&coordinate).as_deref() != Some("ready") {
3842 return Err(format!(
3843 "package/not-ready: {coordinate}; call Package/ensure first"
3844 ));
3845 }
3846 let registry = namespace_registry()?;
3847 require_namespace(®istry, env, &target)?;
3848 return Ok(Value::Symbol(Symbol::parse(&target)));
3849 }
3850 if method == "ensure" {
3851 if catalog.state(&coordinate).as_deref() == Some("ready") {
3852 let promise = Promise::new();
3853 promise.resolve(descriptor);
3854 return Ok(Value::Promise(promise));
3855 }
3856 if let Some(pending) = catalog.pending(&coordinate) {
3857 return Ok(Value::Promise(pending));
3858 }
3859 } else if catalog.state(&coordinate).as_deref() == Some("available") {
3860 let promise = Promise::new();
3861 promise.resolve(Value::Vector(PVector::new()));
3862 return Ok(Value::Promise(promise));
3863 } else if catalog.pending(&coordinate).is_some() {
3864 return Err(format!("package/busy: {coordinate}"));
3865 }
3866 if method == "unload" {
3867 if let Some(options) = arguments.get(1) {
3868 if map_entries(options).is_none() {
3869 return Err("std.native.Package/unload options must be a map".into());
3870 }
3871 if let Some(value) = map_value(options, &Value::Keyword("cascade".into())) {
3872 if !matches!(value, Value::Bool(_)) {
3873 return Err("std.native.Package/unload :cascade must be boolean".into());
3874 }
3875 }
3876 }
3877 }
3878 let previous_state = catalog
3879 .state(&coordinate)
3880 .unwrap_or_else(|| "available".into());
3881 catalog.set_state(
3882 &coordinate,
3883 if method == "ensure" {
3884 "ensuring"
3885 } else {
3886 "unloading"
3887 },
3888 );
3889 HOST_CALL_HANDLER.with(|active| {
3890 let Some(handler) = active.borrow().as_ref().cloned() else {
3891 let promise = Promise::new();
3892 promise.reject_value(host_error(
3893 "package/unsupported",
3894 "Package capability provider is unavailable",
3895 ));
3896 catalog.set_state(
3897 &coordinate,
3898 if method == "ensure" {
3899 "failed"
3900 } else {
3901 &previous_state
3902 },
3903 );
3904 return Ok(Value::Promise(promise));
3905 };
3906 let mut provider_arguments = vec![descriptor];
3907 provider_arguments.extend(arguments.iter().skip(1).cloned());
3908 let result = handler("package".into(), method.into(), provider_arguments);
3909 if let Ok(Value::Promise(promise)) = &result {
3910 let state = catalog.clone();
3911 let coordinate = coordinate.clone();
3912 let operation = method.to_owned();
3913 let rollback = previous_state.clone();
3914 state.set_pending(&coordinate, Some(promise.clone()));
3915 promise.on_settle(Rc::new(move |settlement| {
3916 let next = match (&operation[..], settlement) {
3917 ("ensure", PromiseState::Fulfilled(_)) => "ready",
3918 ("ensure", _) => "failed",
3919 ("unload", PromiseState::Fulfilled(_)) => "available",
3920 ("unload", _) => rollback.as_str(),
3921 _ => rollback.as_str(),
3922 };
3923 state.set_state(&coordinate, next);
3924 state.set_pending(&coordinate, None);
3925 }));
3926 } else if result.is_ok() {
3927 catalog.set_state(
3928 &coordinate,
3929 if method == "ensure" {
3930 "ready"
3931 } else {
3932 "available"
3933 },
3934 );
3935 } else {
3936 catalog.set_state(
3937 &coordinate,
3938 if method == "ensure" {
3939 "failed"
3940 } else {
3941 &previous_state
3942 },
3943 );
3944 }
3945 result
3946 })
3947}
3948
3949#[cfg(not(target_arch = "wasm32"))]
3950fn native_package_artifact_values(method: &str, arguments: Vec<Value>) -> Result<Value, String> {
3951 match method {
3952 "build" => {
3953 if !(1..=4).contains(&arguments.len()) {
3954 return Err(
3955 "std.native.Package/build expects input and up to three options".into(),
3956 );
3957 }
3958 let input =
3959 package_string_argument(&arguments, 0, "std.native.Package/build")?.to_owned();
3960 let output =
3961 package_optional_string_argument(&arguments, 1, "std.native.Package/build")?
3962 .map(str::to_owned);
3963 let package =
3964 package_optional_string_argument(&arguments, 2, "std.native.Package/build")?
3965 .map(str::to_owned);
3966 let profile =
3967 package_optional_string_argument(&arguments, 3, "std.native.Package/build")?
3968 .map(str::to_owned);
3969 let built = std::thread::Builder::new()
3970 .name("hara-package-build".into())
3971 .stack_size(crate::package::BUILD_THREAD_STACK_SIZE)
3972 .spawn(move || {
3973 crate::package::build_path_with_package(
3974 std::path::Path::new(&input),
3975 output.as_deref().map(std::path::Path::new),
3976 package.as_deref(),
3977 profile.as_deref().map(std::path::Path::new),
3978 )
3979 })
3980 .map_err(|error| format!("std.native.Package/build thread failed: {error}"))?
3981 .join()
3982 .map_err(|_| "std.native.Package/build thread panicked".to_owned())??;
3983 Ok(Value::String(built.to_string_lossy().into_owned()))
3984 }
3985 "inspect" => {
3986 if arguments.len() != 1 {
3987 return Err("std.native.Package/inspect expects one archive path".into());
3988 }
3989 Ok(Value::String(crate::package::inspect_path(
3990 std::path::Path::new(package_string_argument(
3991 &arguments,
3992 0,
3993 "std.native.Package/inspect",
3994 )?),
3995 )?))
3996 }
3997 "seal" => {
3998 if arguments.len() != 1 {
3999 return Err("std.native.Package/seal expects one descriptor map".into());
4000 }
4001 let manifest = crate::distribution::seal(&sealed_package_spec(&arguments[0])?)?;
4002 Ok(sealed_manifest_value(&manifest))
4003 }
4004 "distribute" => {
4005 if arguments.len() != 1 {
4006 return Err("std.native.Package/distribute expects one descriptor map".into());
4007 }
4008 let spec = distribution_build_spec(&arguments[0])?;
4009 let manifest = if spec.replace {
4010 crate::distribution::build_replace(&spec.project, &spec.host, &spec.output)?
4011 } else {
4012 crate::distribution::build(&spec.project, &spec.host, &spec.output)?
4013 };
4014 Ok(distribution_manifest_value(&manifest))
4015 }
4016 "inspect-seal" | "verify-seal" => {
4017 if arguments.len() != 1 {
4018 return Err(format!(
4019 "std.native.Package/{method} expects one executable path"
4020 ));
4021 }
4022 let path = std::path::Path::new(package_string_argument(
4023 &arguments,
4024 0,
4025 &format!("std.native.Package/{method}"),
4026 )?);
4027 let found = if method == "inspect-seal" {
4028 crate::distribution::inspect_sealed(path)?
4029 } else {
4030 crate::distribution::verify_sealed(path)?
4031 };
4032 Ok(found
4033 .as_ref()
4034 .map(sealed_manifest_value)
4035 .unwrap_or(Value::Nil))
4036 }
4037 _ => unreachable!("caller restricts native package artifact methods"),
4038 }
4039}
4040
4041#[cfg(target_arch = "wasm32")]
4042fn native_package_artifact_values(method: &str, _arguments: Vec<Value>) -> Result<Value, String> {
4043 Err(format!(
4044 "package/unsupported: Package/{method} is unavailable on wasm"
4045 ))
4046}
4047
4048#[cfg(not(target_arch = "wasm32"))]
4049fn sealed_package_spec(value: &Value) -> Result<crate::distribution::SealSpec, String> {
4050 let entries = map_entries(value)
4051 .ok_or_else(|| "std.native.Package/seal expects one descriptor map".to_owned())?;
4052 let descriptor = Value::OrderedMap(Box::new(POrderedMap::from_iter(entries)));
4053 let required_string = |key: &str| match map_value(&descriptor, &Value::Keyword(key.into())) {
4054 Some(Value::String(value)) if !value.is_empty() => Ok(value.clone()),
4055 Some(_) => Err(format!(
4056 "std.native.Package/seal :{key} must be a non-empty string"
4057 )),
4058 None => Err(format!("std.native.Package/seal is missing :{key}")),
4059 };
4060 let entry = match map_value(&descriptor, &Value::Keyword("entry".into())) {
4061 Some(Value::Symbol(value)) => value.as_str().to_owned(),
4062 Some(_) => return Err("std.native.Package/seal :entry must be a symbol".into()),
4063 None => return Err("std.native.Package/seal is missing :entry".into()),
4064 };
4065 let host = match map_value(&descriptor, &Value::Keyword("host".into())) {
4066 Some(Value::String(value)) if !value.is_empty() => std::path::PathBuf::from(value),
4067 Some(_) => return Err("std.native.Package/seal :host must be a non-empty string".into()),
4068 None => std::env::current_exe()
4069 .map_err(|error| format!("cannot determine current native executable: {error}"))?,
4070 };
4071 let archives = match map_value(&descriptor, &Value::Keyword("archives".into())) {
4072 Some(value) => iterator_values(value.clone())?,
4073 None => return Err("std.native.Package/seal is missing :archives".into()),
4074 }
4075 .into_iter()
4076 .map(|archive| {
4077 let archive_entries = map_entries(&archive)
4078 .ok_or_else(|| "std.native.Package/seal archives must be maps".to_owned())?;
4079 let archive = Value::OrderedMap(Box::new(POrderedMap::from_iter(archive_entries)));
4080 let path = match map_value(&archive, &Value::Keyword("path".into())) {
4081 Some(Value::String(value)) if !value.is_empty() => std::path::PathBuf::from(value),
4082 Some(_) => {
4083 return Err(
4084 "std.native.Package/seal archive :path must be a non-empty string".into(),
4085 )
4086 }
4087 None => return Err("std.native.Package/seal archive is missing :path".into()),
4088 };
4089 let primary = match map_value(&archive, &Value::Keyword("primary".into())) {
4090 Some(Value::Bool(value)) => *value,
4091 Some(_) => {
4092 return Err("std.native.Package/seal archive :primary must be boolean".into())
4093 }
4094 None => false,
4095 };
4096 Ok(crate::distribution::SealArchive { path, primary })
4097 })
4098 .collect::<Result<Vec<_>, String>>()?;
4099 Ok(crate::distribution::SealSpec {
4100 host,
4101 output: std::path::PathBuf::from(required_string("output")?),
4102 entry,
4103 archives,
4104 })
4105}
4106
4107#[cfg(not(target_arch = "wasm32"))]
4108struct DistributionBuildSpec {
4109 project: std::path::PathBuf,
4110 host: std::path::PathBuf,
4111 output: std::path::PathBuf,
4112 replace: bool,
4113}
4114
4115#[cfg(not(target_arch = "wasm32"))]
4116fn distribution_build_spec(value: &Value) -> Result<DistributionBuildSpec, String> {
4117 let entries = map_entries(value)
4118 .ok_or_else(|| "std.native.Package/distribute expects one descriptor map".to_owned())?;
4119 let descriptor = Value::OrderedMap(Box::new(POrderedMap::from_iter(entries)));
4120 let required_string = |key: &str| match map_value(&descriptor, &Value::Keyword(key.into())) {
4121 Some(Value::String(value)) if !value.is_empty() => Ok(value),
4122 Some(_) => Err(format!(
4123 "std.native.Package/distribute :{key} must be a non-empty string"
4124 )),
4125 None => Err(format!("std.native.Package/distribute is missing :{key}")),
4126 };
4127 let replace = match map_value(&descriptor, &Value::Keyword("replace?".into())) {
4128 Some(Value::Bool(value)) => *value,
4129 Some(_) => return Err("std.native.Package/distribute :replace? must be boolean".into()),
4130 None => false,
4131 };
4132 Ok(DistributionBuildSpec {
4133 project: std::path::PathBuf::from(required_string("project")?),
4134 host: std::path::PathBuf::from(required_string("host")?),
4135 output: std::path::PathBuf::from(required_string("output")?),
4136 replace,
4137 })
4138}
4139
4140#[cfg(not(target_arch = "wasm32"))]
4141fn sealed_manifest_value(manifest: &crate::distribution::SealedManifest) -> Value {
4142 let archives: Vec<Value> = manifest
4143 .archives
4144 .iter()
4145 .map(|archive| {
4146 Value::OrderedMap(Box::new(POrderedMap::from_iter([
4147 (
4148 Value::Keyword("identity".into()),
4149 Value::String(archive.identity.clone()),
4150 ),
4151 (
4152 Value::Keyword("version".into()),
4153 Value::String(archive.version.clone()),
4154 ),
4155 (
4156 Value::Keyword("sha256".into()),
4157 Value::String(archive.sha256.clone()),
4158 ),
4159 (
4160 Value::Keyword("offset".into()),
4161 Value::Number(i64::try_from(archive.offset).unwrap_or(i64::MAX)),
4162 ),
4163 (
4164 Value::Keyword("length".into()),
4165 Value::Number(i64::try_from(archive.length).unwrap_or(i64::MAX)),
4166 ),
4167 (
4168 Value::Keyword("primary".into()),
4169 Value::Bool(archive.primary),
4170 ),
4171 ])))
4172 })
4173 .collect();
4174 Value::OrderedMap(Box::new(POrderedMap::from_iter([
4175 (
4176 Value::Keyword("executable/format".into()),
4177 Value::String(crate::distribution::SEALED_FORMAT.into()),
4178 ),
4179 (
4180 Value::Keyword("entry".into()),
4181 Value::Symbol(Symbol::parse(&manifest.entry)),
4182 ),
4183 (
4184 Value::Keyword("archives".into()),
4185 Value::Vector(PVector::from(archives)),
4186 ),
4187 (
4188 Value::Keyword("host/sha256".into()),
4189 Value::String(manifest.host_sha256.clone()),
4190 ),
4191 (
4192 Value::Keyword("payload/sha256".into()),
4193 Value::String(manifest.payload_sha256.clone()),
4194 ),
4195 ])))
4196}
4197
4198#[cfg(not(target_arch = "wasm32"))]
4199fn distribution_manifest_value(manifest: &crate::distribution::Manifest) -> Value {
4200 Value::OrderedMap(Box::new(POrderedMap::from_iter([
4201 (
4202 Value::Keyword("distribution/format".into()),
4203 Value::String(crate::distribution::FORMAT.into()),
4204 ),
4205 (
4206 Value::Keyword("launcher".into()),
4207 Value::String(manifest.launcher.clone()),
4208 ),
4209 (
4210 Value::Keyword("entry".into()),
4211 Value::Symbol(Symbol::parse(&manifest.entry)),
4212 ),
4213 (
4214 Value::Keyword("archive".into()),
4215 Value::String(manifest.archive.to_string_lossy().into_owned()),
4216 ),
4217 (
4218 Value::Keyword("archive/sha256".into()),
4219 Value::String(manifest.archive_sha256.clone()),
4220 ),
4221 (
4222 Value::Keyword("source/identity".into()),
4223 Value::String(manifest.source_identity.clone()),
4224 ),
4225 (
4226 Value::Keyword("source/version".into()),
4227 Value::String(manifest.source_version.clone()),
4228 ),
4229 (
4230 Value::Keyword("native/version".into()),
4231 Value::String(manifest.native_version.clone()),
4232 ),
4233 (
4234 Value::Keyword("native/sha256".into()),
4235 Value::String(manifest.native_sha256.clone()),
4236 ),
4237 ])))
4238}
4239
4240#[cfg(not(target_arch = "wasm32"))]
4241fn package_string_argument<'a>(
4242 arguments: &'a [Value],
4243 index: usize,
4244 operation: &str,
4245) -> Result<&'a str, String> {
4246 match arguments.get(index) {
4247 Some(Value::String(value)) => Ok(value),
4248 _ => Err(format!(
4249 "{operation} expects a string argument at position {index}"
4250 )),
4251 }
4252}
4253
4254#[cfg(not(target_arch = "wasm32"))]
4255fn package_optional_string_argument<'a>(
4256 arguments: &'a [Value],
4257 index: usize,
4258 operation: &str,
4259) -> Result<Option<&'a str>, String> {
4260 match arguments.get(index) {
4261 None | Some(Value::Nil) => Ok(None),
4262 Some(Value::String(value)) => Ok(Some(value)),
4263 _ => Err(format!(
4264 "{operation} expects an optional string at position {index}"
4265 )),
4266 }
4267}
4268
4269pub fn call_host_value(service: Value, target: Value, arguments: Value) -> Result<Value, String> {
4273 let service = match service {
4274 Value::String(value) => value,
4275 _ => return Err("std.native.Host/call service must be a string".into()),
4276 };
4277 let target = match target {
4278 Value::String(value) => value,
4279 _ => return Err("std.native.Host/call method must be a string".into()),
4280 };
4281 let arguments = match arguments {
4282 Value::Vector(values) => values.iter().cloned().collect(),
4283 Value::Tuple(values) => values.iter().cloned().collect(),
4284 _ => return Err("std.native.Host/call arguments must be a vector".into()),
4285 };
4286 if !native_capability_granted("host-call") {
4287 return Ok(native_capability_denied_promise(
4288 "Host",
4289 "call",
4290 "host-call",
4291 ));
4292 }
4293 HOST_CALL_HANDLER.with(|active| {
4294 let Some(handler) = active.borrow().as_ref().cloned() else {
4295 let promise = Promise::new();
4296 promise.reject_value(host_error(
4297 "host/unavailable",
4298 "Host capability provider is unavailable",
4299 ));
4300 return Ok(Value::Promise(promise));
4301 };
4302 handler(service, target, arguments)
4303 })
4304}
4305
4306fn host_error(code: &str, message: &str) -> Value {
4307 Value::ExceptionInfo(Rc::new(ExceptionInfo {
4308 message: message.into(),
4309 data: Box::new(Value::Map(
4310 vec![
4311 (
4312 Value::Keyword("ex/code".into()),
4313 Value::Keyword(code.into()),
4314 ),
4315 (
4316 Value::Keyword("ex/class".into()),
4317 Value::Keyword("ex.class/host".into()),
4318 ),
4319 ]
4320 .into_iter()
4321 .collect(),
4322 )),
4323 cause: None,
4324 provenance: Rc::new(RefCell::new(Default::default())),
4325 }))
4326}
4327pub fn with_host_calls<R>(
4329 handler: Rc<dyn Fn(String, String, Vec<Value>) -> Result<Value, String>>,
4330 operation: impl FnOnce() -> R,
4331) -> R {
4332 HOST_CALL_HANDLER.with(|active| {
4333 let previous = active.replace(Some(handler));
4334 let result = operation();
4335 active.replace(previous);
4336 result
4337 })
4338}
4339
4340pub fn with_namespace_source<R>(
4342 provider: Rc<dyn Fn(&str) -> Option<NamespaceResource>>,
4343 action: impl FnOnce() -> R,
4344) -> R {
4345 NAMESPACE_SOURCE_PROVIDER.with(|active| {
4346 let previous = active.borrow_mut().replace(provider);
4347 let result = action();
4348 *active.borrow_mut() = previous;
4349 result
4350 })
4351}
4352
4353#[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4359pub(crate) fn with_direct_native_namespace_loader<R>(
4360 loader: Rc<dyn Fn(&str, NamespaceResource, &mut HashMap<String, Value>) -> Result<(), String>>,
4361 action: impl FnOnce() -> R,
4362) -> R {
4363 ACTIVE_DIRECT_NATIVE_NAMESPACE_LOADER.with(|active| {
4364 let previous = active.borrow_mut().replace(loader);
4365 let result = action();
4366 *active.borrow_mut() = previous;
4367 result
4368 })
4369}
4370
4371#[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4375pub(crate) fn with_direct_native_execution<R>(action: impl FnOnce() -> R) -> R {
4376 ACTIVE_DIRECT_NATIVE_EXECUTION.with(|active| {
4377 let previous = active.replace(true);
4378 let result = action();
4379 active.set(previous);
4380 result
4381 })
4382}
4383
4384#[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4389pub(crate) fn without_direct_native_execution<R>(action: impl FnOnce() -> R) -> R {
4390 ACTIVE_DIRECT_NATIVE_EXECUTION.with(|active| {
4391 let previous = active.replace(false);
4392 let result = action();
4393 active.set(previous);
4394 result
4395 })
4396}
4397
4398#[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4399fn direct_native_namespace_loader(
4400) -> Option<Rc<dyn Fn(&str, NamespaceResource, &mut HashMap<String, Value>) -> Result<(), String>>>
4401{
4402 ACTIVE_DIRECT_NATIVE_NAMESPACE_LOADER.with(|active| active.borrow().clone())
4403}
4404
4405#[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4406pub(crate) fn direct_native_execution() -> bool {
4407 ACTIVE_DIRECT_NATIVE_EXECUTION.with(Cell::get)
4408}
4409
4410#[derive(Clone, Default)]
4416pub(crate) struct NativeCallbackContext {
4417 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4418 scope: Option<crate::direct_native::NativeExecutionScope>,
4419 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4420 context: Option<DirectNativeContext>,
4421}
4422
4423impl NativeCallbackContext {
4424 pub(crate) fn capture() -> Self {
4425 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4426 {
4427 let scope = crate::direct_native::capture_execution_scope();
4428 let context = scope.as_ref().map(|_| DirectNativeContext::capture());
4429 return Self { scope, context };
4430 }
4431 #[cfg(not(all(feature = "direct-native", not(target_arch = "wasm32"))))]
4432 {
4433 Self::default()
4434 }
4435 }
4436
4437 pub(crate) fn with<R>(&self, action: impl FnOnce() -> R) -> R {
4438 #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
4439 {
4440 return crate::direct_native::with_captured_context(
4441 self.scope.as_ref(),
4442 self.context.as_ref(),
4443 action,
4444 );
4445 }
4446 #[cfg(not(all(feature = "direct-native", not(target_arch = "wasm32"))))]
4447 {
4448 action()
4449 }
4450 }
4451}