compose_eval/lib.rs
1/*!
2The Compose Interpreter
3
4
5*/
6
7mod access;
8mod evaluated;
9mod expression;
10mod statement;
11pub mod test;
12mod vm;
13
14pub use crate::vm::Machine;
15use compose_library::diag::{IntoSourceDiagnostic, SourceDiagnostic, SourceResult, Warned, error};
16use compose_library::{Value, Vm};
17use compose_syntax::ast::Statement;
18use compose_syntax::{Source, Span};
19use ecow::{EcoVec, eco_vec};
20pub use evaluated::Evaluated;
21use std::cmp::min;
22use std::ops::Range;
23
24/**
25Represents a language construct that can be evaluated. Examples are expressions and statements.
26
27Evaluation may:
28 - Have side effects like writing to stdout or writing to a file.
29 - Allocate on the VM-managed heap.
30 - Trigger flow control (break, continue, return).
31 - Introduce lexical or flow bindings.
32
33# Contract
34
35## Scope management
36Implementors that enter **lexical** or **flow** scopes must leave these scopes before returning from [`Eval::eval`].
37
38To make this less error-prone, [`Machine`] provides several helpers:
39- [`Machine::in_lexical_scope`]
40- [`Machine::new_lexical_scope_guard`]
41- [`Machine::in_flow_scope_guard`]
42- [`Machine::new_flow_scope_guard`]
43
44To read more about the rationale behind scope usage see [`compose_library::foundations::scope::Scopes`].
45
46*Example: Evaluating a code block*
47
48```rust,ignore
49use compose_eval::{Eval, Machine};
50use compose_library::diag::SourceResult;
51use compose_syntax::ast::CodeBlock;
52use crate::Evaluated;
53
54impl Eval for CodeBlock<'_> {
55 fn eval(self, vm: &mut Machine) -> SourceResult<Evaluated> {
56 let flow = vm.flow.take();
57 let mut result = Evaluated::unit();
58
59 let statements = self.statements();
60
61 // in_lexical_scope enters a scope for the duration of the closure
62 vm.in_lexical_scope(|vm| {
63 for statement in statements {
64 result = statement.eval(vm)?;
65 if vm.flow.is_some() {
66 break;
67 }
68 }
69 SourceResult::Ok(())
70 })?;
71
72 if let Some(flow) = flow {
73 vm.flow = Some(flow);
74 }
75
76 Ok(result)
77 }
78}
79```
80
81
82## Any interaction with the outside world should go through the [`compose_library::World`] trait object in the [`Machine::engine`].
83
84This is required to uphold the sandbox in which compose is evaluated and allows implementations to work in
85any situation is used in.
86
87*Example: writing to stdout*
88
89```rust,ignore
90# use compose_eval::{Eval, Machine, Evaluated};
91# use compose_syntax::{Span};
92# use compose_library::diag::{At, SourceResult};
93struct WriteToStdout { span: Span }
94
95impl Eval for WriteToStdout {
96 fn eval(self, vm: &mut Machine) -> SourceResult<Evaluated> {
97 vm
98 .engine
99 .world
100 .write(&mut |write| write!(write, "Hello, world from Compose!"))
101 .map_err(|e| format!("An error occurred while writing to stdout: {}", e))
102 .at(self.span)?;
103
104 Ok(Evaluated::unit())
105 }
106}
107```
108
109## Temporary values need to be rooted while performing GC
110
111A value returned from an evaluation may not yet be rooted yet. Any values on the stack within the [`Eval::eval`]
112function must be rooted while performing GC.
113
114Use [`Machine::temp_root_guard`] or [`Machine::temp_root_scope`] to open a temporary root scope. Then track
115any temporary values with [`Machine::track_tmp_root`].
116
117*Example: GC after evaluating a statement*
118
119```rust,ignore
120use compose_eval::{Eval, Machine, Evaluated};
121use compose_library::{Binding, IntoValue, Module, diag::SourceResult};
122use compose_syntax::{ast};
123
124impl Eval for ast::Statement<'_> {
125 fn eval(self, vm: &mut Machine) -> SourceResult<Evaluated> {
126 // This might return a value with a heap allocation.
127 // The VM might not yet know about it if it is not bound in any scope.
128 // But the value might still be used after this statement.
129 let result = match self {
130 ast::Statement::Expr(e) => e.eval(vm),
131 // ...
132 _ => unimplemented!()
133 };
134
135 // Temporarily root the value to ensure it is not GCed before we use it.
136 vm.temp_root_scope(|vm| {
137 if let Ok(result) = &result {
138 vm.track_tmp_root(result.value());
139 }
140
141 vm.maybe_gc();
142 Ok(())
143 })?;
144
145 result
146 }
147}
148```
149*/
150
151pub trait Eval {
152 fn eval(self, vm: &mut Machine) -> SourceResult<Evaluated>;
153}
154
155pub fn eval_source(
156 source: &Source,
157 vm: &mut Machine,
158) -> Warned<SourceResult<Value>> {
159 eval_source_range(source, 0..usize::MAX, vm)
160}
161
162pub fn eval(vm: &mut Machine) -> Warned<SourceResult<Value>> {
163 let entry_point = vm.engine().world.entry_point();
164 let source = match vm.engine().world.source(entry_point) {
165 Ok(source) => source,
166 Err(err) => {
167 return Warned::new(Err(eco_vec!(err.into_source_diagnostic(Span::detached()))));
168 }
169 };
170
171 eval_source(&source, vm)
172}
173
174/// Eval a source file.
175///
176/// eval_range: eval these nodes
177pub fn eval_source_range(
178 source: &Source,
179 eval_range: Range<usize>,
180 vm: &mut Machine,
181) -> Warned<SourceResult<Value>> {
182 let mut result = Value::unit();
183
184 let range_start = min(eval_range.start, source.nodes().len());
185 let range_end = min(eval_range.end, source.nodes().len());
186
187 let nodes = source.nodes().get(range_start..range_end).unwrap();
188 let errors = nodes
189 .iter()
190 .flat_map(|n| n.errors())
191 .map(|e| e.into())
192 .collect::<EcoVec<SourceDiagnostic>>();
193
194 let syntax_warnings = nodes
195 .iter()
196 .flat_map(|n| n.warnings())
197 .map(|e| e.into())
198 .collect::<EcoVec<SourceDiagnostic>>();
199
200 if !errors.is_empty() {
201 return Warned::new(Err(errors)).with_warnings(syntax_warnings);
202 }
203
204 for node in nodes {
205 let statement: Statement = match node.cast() {
206 Some(expr) => expr,
207 None => {
208 let span = node.span();
209 let err = error!(span, "expected a statement, found {:?}", node);
210
211 return build_err(&syntax_warnings, vm, eco_vec![err]);
212 }
213 };
214 result = match statement.eval(vm) {
215 Ok(value) => value.value,
216 Err(err) => return build_err(&syntax_warnings, vm, err),
217 }
218 }
219
220 let mut warnings = vm.sink_mut().take_warnings();
221 warnings.extend_from_slice(&syntax_warnings);
222
223 Warned::new(Ok(result)).with_warnings(warnings)
224}
225
226pub fn build_err(
227 syntax_warnings: &[SourceDiagnostic],
228 vm: &mut Machine,
229 errs: EcoVec<SourceDiagnostic>,
230) -> Warned<SourceResult<Value>> {
231 let mut warnings = vm.sink_mut().take_warnings();
232 warnings.extend_from_slice(syntax_warnings);
233
234 Warned::new(Err(errs)).with_warnings(warnings)
235}