Skip to main content

o_toolchain_javascriptcore/
lib.rs

1use jstd::console;
2use o_core::engine::JSEngine;
3use o_core::error::{JSError, JSResult};
4use rust_jsc::{JSContext, JSFunction, JSObject, JSValue, PropertyDescriptor, callback};
5
6type HostResult = rust_jsc::JSResult<JSValue>;
7
8fn js_values_to_strings(arguments: &[JSValue]) -> Vec<String> {
9    arguments
10        .iter()
11        .map(|value| {
12            if value.is_string() {
13                value
14                    .as_string()
15                    .map(|s| s.to_string())
16                    .unwrap_or_else(|_| "[string]".to_string())
17            } else if value.is_number() {
18                value
19                    .as_number()
20                    .map(|n| {
21                        if n.fract() == 0.0 {
22                            (n as i64).to_string()
23                        } else {
24                            n.to_string()
25                        }
26                    })
27                    .unwrap_or_else(|_| "[number]".to_string())
28            } else if value.is_boolean() {
29                value.as_boolean().to_string()
30            } else if value.is_null() {
31                "null".to_string()
32            } else if value.is_undefined() {
33                "undefined".to_string()
34            } else {
35                value
36                    .as_string()
37                    .map(|s| s.to_string())
38                    .unwrap_or_else(|_| "[object]".to_string())
39            }
40        })
41        .collect()
42}
43
44fn first_label(arguments: &[JSValue]) -> String {
45    arguments
46        .first()
47        .map(|value| js_values_to_strings(std::slice::from_ref(value)).remove(0))
48        .unwrap_or_else(|| "default".to_string())
49}
50
51#[callback]
52fn host_console_log(
53    ctx: JSContext,
54    _function: JSObject,
55    _this: JSObject,
56    arguments: &[JSValue],
57) -> HostResult {
58    console::log(&js_values_to_strings(arguments));
59    Ok(JSValue::undefined(&ctx))
60}
61
62#[callback]
63fn host_console_info(
64    ctx: JSContext,
65    _function: JSObject,
66    _this: JSObject,
67    arguments: &[JSValue],
68) -> HostResult {
69    console::info(&js_values_to_strings(arguments));
70    Ok(JSValue::undefined(&ctx))
71}
72
73#[callback]
74fn host_console_warn(
75    ctx: JSContext,
76    _function: JSObject,
77    _this: JSObject,
78    arguments: &[JSValue],
79) -> HostResult {
80    console::warn(&js_values_to_strings(arguments));
81    Ok(JSValue::undefined(&ctx))
82}
83
84#[callback]
85fn host_console_error(
86    ctx: JSContext,
87    _function: JSObject,
88    _this: JSObject,
89    arguments: &[JSValue],
90) -> HostResult {
91    console::error(&js_values_to_strings(arguments));
92    Ok(JSValue::undefined(&ctx))
93}
94
95#[callback]
96fn host_console_debug(
97    ctx: JSContext,
98    _function: JSObject,
99    _this: JSObject,
100    arguments: &[JSValue],
101) -> HostResult {
102    console::debug(&js_values_to_strings(arguments));
103    Ok(JSValue::undefined(&ctx))
104}
105
106#[callback]
107fn host_console_trace(
108    ctx: JSContext,
109    _function: JSObject,
110    _this: JSObject,
111    arguments: &[JSValue],
112) -> HostResult {
113    console::trace(&js_values_to_strings(arguments));
114    Ok(JSValue::undefined(&ctx))
115}
116
117#[callback]
118fn host_console_assert(
119    ctx: JSContext,
120    _function: JSObject,
121    _this: JSObject,
122    arguments: &[JSValue],
123) -> HostResult {
124    let condition = arguments
125        .first()
126        .map(|value| value.as_boolean())
127        .unwrap_or(false);
128    let args = if arguments.len() > 1 {
129        js_values_to_strings(&arguments[1..])
130    } else {
131        Vec::new()
132    };
133    console::assert(condition, &args);
134    Ok(JSValue::undefined(&ctx))
135}
136
137#[callback]
138fn host_console_clear(
139    ctx: JSContext,
140    _function: JSObject,
141    _this: JSObject,
142    _arguments: &[JSValue],
143) -> HostResult {
144    console::clear();
145    Ok(JSValue::undefined(&ctx))
146}
147
148#[callback]
149fn host_console_count(
150    ctx: JSContext,
151    _function: JSObject,
152    _this: JSObject,
153    arguments: &[JSValue],
154) -> HostResult {
155    console::count(&first_label(arguments));
156    Ok(JSValue::undefined(&ctx))
157}
158
159#[callback]
160fn host_console_count_reset(
161    ctx: JSContext,
162    _function: JSObject,
163    _this: JSObject,
164    arguments: &[JSValue],
165) -> HostResult {
166    console::count_reset(&first_label(arguments));
167    Ok(JSValue::undefined(&ctx))
168}
169
170#[callback]
171fn host_console_time(
172    ctx: JSContext,
173    _function: JSObject,
174    _this: JSObject,
175    arguments: &[JSValue],
176) -> HostResult {
177    console::time(&first_label(arguments));
178    Ok(JSValue::undefined(&ctx))
179}
180
181#[callback]
182fn host_console_time_log(
183    ctx: JSContext,
184    _function: JSObject,
185    _this: JSObject,
186    arguments: &[JSValue],
187) -> HostResult {
188    console::time_log(&first_label(arguments));
189    Ok(JSValue::undefined(&ctx))
190}
191
192#[callback]
193fn host_console_time_end(
194    ctx: JSContext,
195    _function: JSObject,
196    _this: JSObject,
197    arguments: &[JSValue],
198) -> HostResult {
199    console::time_end(&first_label(arguments));
200    Ok(JSValue::undefined(&ctx))
201}
202
203#[callback]
204fn host_console_group(
205    ctx: JSContext,
206    _function: JSObject,
207    _this: JSObject,
208    arguments: &[JSValue],
209) -> HostResult {
210    console::group(&first_label(arguments));
211    Ok(JSValue::undefined(&ctx))
212}
213
214#[callback]
215fn host_console_group_collapsed(
216    ctx: JSContext,
217    _function: JSObject,
218    _this: JSObject,
219    arguments: &[JSValue],
220) -> HostResult {
221    console::group_collapsed(&first_label(arguments));
222    Ok(JSValue::undefined(&ctx))
223}
224
225#[callback]
226fn host_console_group_end(
227    ctx: JSContext,
228    _function: JSObject,
229    _this: JSObject,
230    _arguments: &[JSValue],
231) -> HostResult {
232    console::group_end();
233    Ok(JSValue::undefined(&ctx))
234}
235
236#[callback]
237fn host_print(
238    ctx: JSContext,
239    _function: JSObject,
240    _this: JSObject,
241    arguments: &[JSValue],
242) -> HostResult {
243    console::print(&js_values_to_strings(arguments));
244    Ok(JSValue::undefined(&ctx))
245}
246
247#[callback]
248fn host_println(
249    ctx: JSContext,
250    _function: JSObject,
251    _this: JSObject,
252    arguments: &[JSValue],
253) -> HostResult {
254    console::println(&js_values_to_strings(arguments));
255    Ok(JSValue::undefined(&ctx))
256}
257
258fn install_global_function(
259    ctx: &JSContext,
260    name: &str,
261    callback: rust_jsc::internal::JSObjectCallAsFunctionCallback,
262) -> Result<(), JSError> {
263    let function = JSFunction::callback(ctx, Some(name), callback);
264    ctx.global_object()
265        .set_property(name, &function, PropertyDescriptor::default())
266        .map_err(|err| JSError::internal(err.to_string()))
267}
268
269fn install_jstd(ctx: &JSContext) -> Result<(), JSError> {
270    install_global_function(ctx, "__jstd_console_log", Some(host_console_log))?;
271    install_global_function(ctx, "__jstd_console_info", Some(host_console_info))?;
272    install_global_function(ctx, "__jstd_console_warn", Some(host_console_warn))?;
273    install_global_function(ctx, "__jstd_console_error", Some(host_console_error))?;
274    install_global_function(ctx, "__jstd_console_debug", Some(host_console_debug))?;
275    install_global_function(ctx, "__jstd_console_trace", Some(host_console_trace))?;
276    install_global_function(ctx, "__jstd_console_assert", Some(host_console_assert))?;
277    install_global_function(ctx, "__jstd_console_clear", Some(host_console_clear))?;
278    install_global_function(ctx, "__jstd_console_count", Some(host_console_count))?;
279    install_global_function(
280        ctx,
281        "__jstd_console_count_reset",
282        Some(host_console_count_reset),
283    )?;
284    install_global_function(ctx, "__jstd_console_time", Some(host_console_time))?;
285    install_global_function(ctx, "__jstd_console_time_log", Some(host_console_time_log))?;
286    install_global_function(ctx, "__jstd_console_time_end", Some(host_console_time_end))?;
287    install_global_function(ctx, "__jstd_console_group", Some(host_console_group))?;
288    install_global_function(
289        ctx,
290        "__jstd_console_group_collapsed",
291        Some(host_console_group_collapsed),
292    )?;
293    install_global_function(
294        ctx,
295        "__jstd_console_group_end",
296        Some(host_console_group_end),
297    )?;
298    install_global_function(ctx, "__jstd_print", Some(host_print))?;
299    install_global_function(ctx, "__jstd_println", Some(host_println))?;
300    ctx.evaluate_script(jstd::bootstrap_script(), Some(1))
301        .map_err(|err| JSError::internal(err.to_string()).with_filename("<jstd>"))?;
302    Ok(())
303}
304
305pub struct JavaScriptCore {
306    ctx: JSContext,
307}
308
309impl JavaScriptCore {
310    pub fn new() -> Self {
311        let ctx = JSContext::new();
312        install_jstd(&ctx).expect("failed to initialize jstd builtins");
313        Self { ctx }
314    }
315}
316
317impl Default for JavaScriptCore {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323impl JSEngine for JavaScriptCore {
324    fn run(&self, code: &str, _filename: &str) -> Result<JSResult, JSError> {
325        let filename = _filename;
326        let value = self.ctx.evaluate_script(code, Some(1)).map_err(|err| {
327            JSError::runtime(err.to_string())
328                .with_filename(filename)
329                .with_source(code)
330        })?;
331
332        if value.is_string() {
333            return Ok(JSResult::String(
334                value
335                    .as_string()
336                    .map(|s| s.to_string())
337                    .map_err(|err| JSError::runtime(err.to_string()).with_filename(filename))?,
338            ));
339        }
340
341        if value.is_number() {
342            let number = value
343                .as_number()
344                .map_err(|err| JSError::runtime(err.to_string()).with_filename(filename))?;
345            if number.fract() == 0.0 {
346                return Ok(JSResult::String((number as i64).to_string()));
347            }
348            return Ok(JSResult::String(number.to_string()));
349        }
350
351        if value.is_boolean() {
352            return Ok(JSResult::String(value.as_boolean().to_string()));
353        }
354
355        if value.is_undefined() {
356            return Ok(JSResult::String("undefined".to_string()));
357        }
358
359        if value.is_null() {
360            return Ok(JSResult::String("null".to_string()));
361        }
362
363        Ok(JSResult::String("object".to_string()))
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_run_simple_expression() {
373        let engine = JavaScriptCore::new();
374        let result = engine.run("40 + 2", "<test>").unwrap();
375        match result {
376            JSResult::String(s) => assert_eq!(s, "42"),
377        }
378    }
379
380    #[test]
381    fn test_run_string_literal() {
382        let engine = JavaScriptCore::new();
383        let result = engine.run("'hello'", "<test>").unwrap();
384        match result {
385            JSResult::String(s) => assert_eq!(s, "hello"),
386        }
387    }
388
389    #[test]
390    fn test_run_boolean() {
391        let engine = JavaScriptCore::new();
392        let result = engine.run("true", "<test>").unwrap();
393        match result {
394            JSResult::String(s) => assert_eq!(s, "true"),
395        }
396    }
397
398    #[test]
399    fn test_console_log_builtin() {
400        let engine = JavaScriptCore::new();
401        let result = engine.run("console.log('hello'); 42", "<test>").unwrap();
402        match result {
403            JSResult::String(s) => assert_eq!(s, "42"),
404        }
405    }
406}