elly_host/io.rs
1//! Building `IO`, the root capability a runner hands a program, and running a
2//! program's `__main` against it.
3//!
4//! Two modules, each written in Elly (`io.ly`, `proc.ly`) over host callbacks
5//! supplied through its **frame**. That is the frame mechanism doing what it was
6//! built for, and it is the only sound route: a `ModuleData` is env-free so one
7//! compile can serve many instantiations, so an `Expr` holds no `Value` and code
8//! cannot carry a callback directly.
9//!
10//! Submodules fall out of the same shape. `IO.Proc` is built first and its
11//! instance handed to `IO`'s frame, where `Proc = proc_impl` is an ordinary
12//! forwarding item — the shape `Foo = import "spec"` already compiles to.
13
14use std::rc::Rc;
15
16use elly_core::{
17 apply_with, compile_module_framed, eval_main, instantiate, Ctx, Raised, Text, Value,
18};
19
20use crate::{Stream, Sys};
21
22const IO_SRC: &str = include_str!("io.ly");
23const PROC_SRC: &str = include_str!("proc.ly");
24
25/// The built capability, plus the one thing about it a runner has to recognize.
26pub struct Io {
27 /// The `IO` module instance — what `__main` is applied to, and the whole of
28 /// what the program is authorized to do.
29 pub value: Value,
30 /// The iota `IO.Proc.return` raises with. [`Io::run`] compares an uncaught
31 /// raise against it to tell "the program asked to exit with a status" from
32 /// "the program failed"; nothing else can mint this value.
33 pub returned: Value,
34}
35
36/// How running a program's `__main` against `IO` ended.
37pub enum Run {
38 /// The module has no `__main`: it is a library rather than a program.
39 Library,
40 /// `__main` returned this value. A runner ignores it — printing a result is
41 /// REPL behaviour — except to notice an entry still wanting arguments.
42 Returned(Value),
43 /// `IO.Proc.return` asked for this exit status.
44 Exit(u8),
45 /// A raise reached the top.
46 Raised(Raised),
47}
48
49/// Build `IO` over `sys` for a program invoked with `args` (the arguments after
50/// the script).
51///
52/// Panics only on a bug in the sources compiled in here — a mis-declared frame
53/// slot or a syntax error in `io.ly` / `proc.ly` — which is a build-time mistake
54/// in this crate, not anything a user's program can cause.
55pub fn io(sys: Rc<dyn Sys>, args: &[String], ctx: &Ctx) -> Io {
56 let proc = instantiate_host(
57 PROC_SRC,
58 &[
59 ("args_impl", Value::list(args.iter().map(str_value))),
60 ("env_impl", env_value(&*sys)),
61 ("exit_impl", exit_fn(sys.clone())),
62 ],
63 ctx,
64 );
65 let returned =
66 apply_with(&proc, symbol("returned"), ctx).expect("`proc.ly` declares `returned`");
67 let value = instantiate_host(
68 IO_SRC,
69 &[
70 ("proc_impl", proc),
71 ("print_impl", writer(&sys, "print", Stream::Out, true)),
72 ("write_impl", writer(&sys, "write", Stream::Out, false)),
73 ("error_impl", writer(&sys, "error", Stream::Err, true)),
74 ],
75 ctx,
76 );
77 Io { value, returned }
78}
79
80impl Io {
81 /// Run `instance`'s `__main`: evaluate the body in the module's own
82 /// environment, apply the result to `IO`, and say how it ended.
83 pub fn run(&self, instance: &Value, ctx: &Ctx) -> Run {
84 let entry = match eval_main(instance, ctx) {
85 Ok(Some(entry)) => entry,
86 Ok(None) => return Run::Library,
87 // The body itself raised, before the capability was ever applied.
88 Err(r) => return self.raised(r),
89 };
90 self.finish(apply_with(&entry, self.value.clone(), ctx))
91 }
92
93 /// Say how a computation that was given `IO` ended. [`run`](Self::run) uses
94 /// it for `__main`; a host that hands `IO` to something else, such as the
95 /// playground's bare expression, uses it directly.
96 pub fn finish(&self, result: Result<Value, Raised>) -> Run {
97 match result {
98 Ok(value) => Run::Returned(value),
99 Err(r) => self.raised(r),
100 }
101 }
102
103 /// Either `IO.Proc.return`'s unwind — the program asking for an exit
104 /// status — or a genuine failure.
105 fn raised(&self, r: Raised) -> Run {
106 match self.requested_exit(r.value()) {
107 Some(code) => Run::Exit(code),
108 None => Run::Raised(r),
109 }
110 }
111
112 /// The status `IO.Proc.return n` asked for, if that is what this raise is: a
113 /// two-element list led by the iota only `IO.Proc` can mint. An ordinary
114 /// raise cannot be mistaken for one, whatever shape it has, because it cannot
115 /// hold that value.
116 ///
117 /// A status outside a byte is taken modulo 256, as a shell reports one
118 /// anyway.
119 fn requested_exit(&self, payload: &Value) -> Option<u8> {
120 let mut items = payload.list_items()?;
121 if items.len() != 2 {
122 return None;
123 }
124 if items.next()? != &self.returned {
125 return None;
126 }
127 match items.next()? {
128 Value::I64(code) => Some(*code as u8),
129 _ => None,
130 }
131 }
132}
133
134/// Compile a host module's source against the named `slots` and instantiate it
135/// with their values — the two halves of one declaration, kept next to each other
136/// so a slot cannot drift out of the order the frame is filled in.
137fn instantiate_host(src: &str, slots: &[(&str, Value)], ctx: &Ctx) -> Value {
138 let names: Vec<Text> = slots.iter().map(|(n, _)| Text::from(*n)).collect();
139 let values: Vec<Value> = slots.iter().map(|(_, v)| v.clone()).collect();
140 let data = compile_module_framed(src, &names).expect("a host module compiles");
141 instantiate(data, &values, ctx).expect("a host module's frame is exactly its slots")
142}
143
144/// A `Str → []` callback that writes its argument to `stream`, optionally
145/// followed by a newline. This is the whole of `IO`'s output surface today.
146///
147/// It answers unit (the empty list) rather than the string, so a caller reads
148/// nothing back out of a write, and it raises `.io_error` rather than panicking
149/// on a closed pipe — a write failure is the program's to see, on the same
150/// channel every other host failure uses.
151fn writer(sys: &Rc<dyn Sys>, name: &'static str, stream: Stream, newline: bool) -> Value {
152 let sys = sys.clone();
153 Value::host_fn(name, 1, move |args, _ctx| {
154 let Value::Str(s) = &args[0] else {
155 return Err(raise("not_a_str"));
156 };
157 let written = if newline {
158 sys.write(stream, &format!("{s}\n"))
159 } else {
160 sys.write(stream, s)
161 };
162 written.map_err(|_| raise("io_error"))?;
163 Ok(Value::list([]))
164 })
165}
166
167/// `Int → ⊥`: exit the process immediately, with no unwinding and nothing running
168/// after it — `std::process::exit`, with no surprises. A status outside the range
169/// the platform can carry is the caller's mistake, so it raises rather than being
170/// silently truncated.
171fn exit_fn(sys: Rc<dyn Sys>) -> Value {
172 Value::host_fn("exit", 1, move |args, _ctx| {
173 let Value::I64(code) = args[0] else {
174 return Err(raise("not_an_int"));
175 };
176 let Ok(code) = i32::try_from(code) else {
177 return Err(raise("bad_exit_code"));
178 };
179 sys.exit(code)
180 })
181}
182
183/// The environment as a map from name to value, read once at startup.
184fn env_value(sys: &dyn Sys) -> Value {
185 Value::map(
186 sys.vars()
187 .into_iter()
188 .map(|(k, v)| (str_value(k), str_value(v))),
189 )
190}
191
192fn str_value(s: impl AsRef<str>) -> Value {
193 Value::Str(Text::from(s.as_ref()))
194}
195
196fn symbol(s: &str) -> Value {
197 Value::Symbol(Text::from(s))
198}
199
200/// One of the host-failure tags, raised on the single error channel everything
201/// else uses (see the *error handling* section of `docs/elly-spec.md`).
202fn raise(tag: &'static str) -> Raised {
203 Raised::Error(Value::Symbol(Text::from_static(tag)))
204}