1use crate::{describe, install_console, Logs};
32use boa_engine::builtins::promise::PromiseState;
33use boa_engine::module::{ModuleLoader, Referrer};
34use boa_engine::object::builtins::{JsFunction, JsPromise};
35use boa_engine::{js_string, Context, JsError, JsNativeError, JsResult, JsValue, Module, NativeFunction, Source};
36use serde_json::{json, Value};
37use std::cell::RefCell;
38use std::collections::HashMap;
39#[cfg(not(target_arch = "wasm32"))]
40use std::io::{BufRead, Write};
41use std::rc::Rc;
42
43struct OnlySdk {
44 sdk: String,
45 parsed: RefCell<Option<Module>>,
46}
47
48impl ModuleLoader for OnlySdk {
49 async fn load_imported_module(
50 self: Rc<Self>,
51 _referrer: Referrer,
52 request: boa_engine::module::ModuleRequest,
53 context: &RefCell<&mut Context>,
54 ) -> JsResult<Module> {
55 let spec = request.specifier().to_std_string_escaped();
56 if spec != "@brep/cad" {
57 return Err(JsNativeError::typ().with_message(format!("cannot import `{spec}`: the only importable module is @brep/cad")).into());
58 }
59 if let Some(m) = self.parsed.borrow().as_ref() {
60 return Ok(m.clone());
61 }
62 let mut cx = context.borrow_mut();
63 let module = Module::parse(Source::from_bytes(&self.sdk).with_path(std::path::Path::new("@brep/cad")), None, &mut cx)?;
64 *self.parsed.borrow_mut() = Some(module.clone());
65 Ok(module)
66 }
67}
68
69thread_local! {
70 static PENDING: RefCell<HashMap<u64, (JsFunction, JsFunction)>> = RefCell::new(HashMap::new());
71 static NEXT: RefCell<u64> = const { RefCell::new(0) };
72 static SINK: RefCell<Option<Box<dyn Fn(&Value)>>> = RefCell::new(None);
75}
76
77fn emit(line: &Value) {
78 SINK.with(|s| {
79 if let Some(sink) = s.borrow().as_ref() {
80 sink(line);
81 }
82 });
83}
84
85fn done_error(code: &str, message: String) -> Value {
86 json!({ "done": { "error": { "code": code, "message": message } } })
87}
88
89fn host_call(_: &JsValue, args: &[JsValue], cx: &mut Context) -> JsResult<JsValue> {
92 let text = |i: usize| args.get(i).and_then(JsValue::as_string).map(|s| s.to_std_string_escaped()).unwrap_or_default();
93 let (method, body) = (text(0), text(1));
94 let (promise, resolvers) = JsPromise::new_pending(cx);
95 let id = NEXT.with(|n| {
96 *n.borrow_mut() += 1;
97 *n.borrow()
98 });
99 PENDING.with(|p| p.borrow_mut().insert(id, (resolvers.resolve, resolvers.reject)));
100 emit(&json!({ "call": id, "method": method, "body": body }));
101 Ok(promise.into())
102}
103
104fn fail(code: &str, e: JsError, cx: &mut Context) -> Value {
107 let limit = e.as_engine().is_some();
108 let message = describe(e, cx);
109 done_error(if limit || message.contains("RuntimeLimitError") { "resource_limit" } else { code }, message)
110}
111
112pub struct Realm {
114 cx: Context,
115 logs: Logs,
116 sent: usize,
117 main: JsPromise,
118}
119
120impl Realm {
121 pub fn begin(start: &str, sink: Box<dyn Fn(&Value)>) -> Option<Realm> {
125 SINK.with(|s| *s.borrow_mut() = Some(sink));
126 let start: Value = match serde_json::from_str(start) {
127 Ok(v) => v,
128 Err(e) => {
129 emit(&done_error("internal_error", format!("bad start message: {e}")));
130 return None;
131 }
132 };
133 let field = |k: &str| start.get(k).and_then(Value::as_str).unwrap_or_default().to_string();
134 let (source, sdk, manifest) = (field("source"), field("sdk"), field("manifest"));
135 let loader = Rc::new(OnlySdk { sdk, parsed: RefCell::new(None) });
136 let mut cx = match Context::builder().module_loader(loader).build() {
137 Ok(cx) => cx,
138 Err(e) => {
139 emit(&done_error("internal_error", e.to_string()));
140 return None;
141 }
142 };
143 cx.runtime_limits_mut().set_loop_iteration_limit(crate::LOOP_ITERATION_LIMIT);
144 cx.runtime_limits_mut().set_recursion_limit(crate::RECURSION_LIMIT);
145 let logs: Logs = Rc::new(RefCell::new(Vec::new()));
146 let setup = (|| -> JsResult<()> {
147 install_console(&mut cx, &logs)?;
148 cx.register_global_property(js_string!("__brep_manifest"), js_string!(manifest), boa_engine::property::Attribute::all())?;
149 cx.register_global_builtin_callable(js_string!("__brep_host"), 2, NativeFunction::from_fn_ptr(host_call))?;
150 Ok(())
151 })();
152 if let Err(e) = setup {
153 let line = done_error("internal_error", describe(e, &mut cx));
154 return Self::end(&logs, &mut 0, line);
155 }
156 let mut sent = 0;
157 match Self::start_main(&source, &mut cx) {
158 Ok(main) => {
159 let mut realm = Realm { cx, logs, sent, main };
160 if realm.progress() {
161 None
162 } else {
163 Some(realm)
164 }
165 }
166 Err(line) => Self::end(&logs, &mut sent, line),
167 }
168 }
169
170 fn start_main(source: &str, cx: &mut Context) -> Result<JsPromise, Value> {
173 let module = Module::parse(Source::from_bytes(source).with_path(std::path::Path::new("main.js")), None, cx).map_err(|e| fail("compile_failed", e, cx))?;
174 let evaluated = module.load_link_evaluate(cx);
175 cx.run_jobs().map_err(|e| fail("job_failed", e, cx))?;
176 match evaluated.state() {
177 PromiseState::Fulfilled(_) => {}
178 PromiseState::Rejected(v) => {
179 let message = describe(JsError::from_opaque(v), cx);
181 let code = if message.contains("cannot import") || message.contains("SyntaxError") { "compile_failed" } else { "script_failed" };
182 return Err(done_error(code, message));
183 }
184 PromiseState::Pending => {
185 return Err(done_error("invalid_arguments", "module evaluation is pending without progress: top-level code may not wait on the host".into()))
186 }
187 }
188 let main = module.namespace(cx).get(js_string!("default"), cx).map_err(|e| fail("script_failed", e, cx))?;
189 let Some(main) = main.as_callable() else {
190 return Err(done_error("invalid_arguments", "the program must `export default async function main()`".into()));
191 };
192 let returned = main.call(&JsValue::undefined(), &[], cx).map_err(|e| fail("script_failed", e, cx))?;
193 returned
194 .as_object()
195 .and_then(|o| JsPromise::from_object(o.clone()).ok())
196 .ok_or_else(|| done_error("invalid_arguments", "main() must be async (return a Promise)".into()))
197 }
198
199 pub fn answer(&mut self, message: &str) -> bool {
202 let Ok(msg) = serde_json::from_str::<Value>(message) else { return false };
203 let (Some(id), Some(body)) = (msg.get("reply").and_then(Value::as_u64), msg.get("body").and_then(Value::as_str)) else { return false };
204 if let Some((resolve, _)) = PENDING.with(|p| p.borrow_mut().remove(&id)) {
205 if let Err(e) = resolve.call(&JsValue::undefined(), &[js_string!(body).into()], &mut self.cx) {
206 let line = fail("job_failed", e, &mut self.cx);
207 Self::end(&self.logs, &mut self.sent, line);
208 return true;
209 }
210 }
211 self.progress()
212 }
213
214 pub fn abandon(&mut self) {
216 Self::end(&self.logs, &mut self.sent, done_error("internal_error", "the host closed the bridge".into()));
217 }
218
219 fn progress(&mut self) -> bool {
222 if let Err(e) = self.cx.run_jobs() {
223 let line = fail("job_failed", e, &mut self.cx);
224 Self::end(&self.logs, &mut self.sent, line);
225 return true;
226 }
227 flush_logs(&self.logs, &mut self.sent);
228 let line = match self.main.state() {
229 PromiseState::Fulfilled(v) => match v.to_json(&mut self.cx) {
230 Ok(json) => json!({ "done": { "ok": json.unwrap_or(Value::Null) } }),
231 Err(e) => done_error("invalid_arguments", format!("main's result is not JSON: {}", describe(e, &mut self.cx))),
232 },
233 PromiseState::Rejected(v) => {
234 let message = describe(JsError::from_opaque(v), &mut self.cx);
235 done_error(if message.contains("RuntimeLimitError") { "resource_limit" } else { "script_failed" }, message)
236 }
237 PromiseState::Pending if PENDING.with(|p| p.borrow().is_empty()) => {
238 done_error("invalid_arguments", "main is pending without progress: no host call is outstanding and no job can run".into())
239 }
240 PromiseState::Pending => return false,
242 };
243 Self::end(&self.logs, &mut self.sent, line);
244 true
245 }
246
247 fn end(logs: &Logs, sent: &mut usize, line: Value) -> Option<Realm> {
250 flush_logs(logs, sent);
251 emit(&line);
252 PENDING.with(|p| p.borrow_mut().clear());
253 None
254 }
255}
256
257fn flush_logs(logs: &Logs, sent: &mut usize) {
258 let lines = logs.borrow();
259 for line in &lines[*sent..] {
260 emit(&json!({ "log": line }));
261 }
262 *sent = lines.len();
263}
264
265#[cfg(not(target_arch = "wasm32"))]
268pub fn main() {
269 let stdin = std::io::stdin();
270 let mut lines = stdin.lock().lines();
271 let Some(Ok(first)) = lines.next() else { return };
272 let sink = Box::new(|line: &Value| {
273 let mut out = std::io::stdout().lock();
274 let _ = writeln!(out, "{line}");
275 let _ = out.flush();
276 });
277 let Some(mut realm) = Realm::begin(&first, sink) else { return };
278 loop {
279 let Some(Ok(line)) = lines.next() else {
280 realm.abandon();
281 return;
282 };
283 if realm.answer(&line) {
284 return;
285 }
286 }
287}