Skip to content

Commit f683778

Browse files
committed
Refactor console implementation and enhance eval support
- Move console logic to js_console module with Node.js-style formatting. - Update core/eval.rs to delegate console methods to js_console. - Implement proper eval() function using parser. - Enable js_array, js_string, and js_regexp modules in lib.rs. - Add helper functions for property access and object manipulation in core/value.rs. - Improve error reporting in examples/js.rs.
1 parent cc7467c commit f683778

File tree

6 files changed

+353
-159
lines changed

6 files changed

+353
-159
lines changed

examples/js.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,20 @@ fn run_main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
5353
match evaluate_script(&script_content, script_path.as_ref()) {
5454
Ok(result) => println!("{result}"),
5555
Err(err) => {
56-
if let Some(file_path) = script_path.as_ref() {
57-
if let Some(line) = err.js_line() {
58-
eprintln!("{}:{}", file_path.display(), line);
59-
let lines: Vec<&str> = script_content.lines().collect();
60-
if line > 0 && line <= lines.len() {
61-
eprintln!("{}", lines[line - 1]);
62-
if let Some(col) = err.js_column() {
63-
if col > 0 {
64-
eprintln!("{}^", " ".repeat(col - 1));
65-
}
66-
}
56+
if let Some(file_path) = script_path.as_ref()
57+
&& let Some(line) = err.js_line()
58+
{
59+
eprintln!("{}:{}", file_path.display(), line);
60+
let lines: Vec<&str> = script_content.lines().collect();
61+
if line > 0 && line <= lines.len() {
62+
eprintln!("{}", lines[line - 1]);
63+
if let Some(col) = err.js_column()
64+
&& col > 0
65+
{
66+
eprintln!("{}^", " ".repeat(col - 1));
6767
}
68-
eprintln!();
6968
}
69+
eprintln!();
7070
}
7171

7272
eprintln!("{}", err.message());

src/core.rs

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
#![allow(clippy::collapsible_if, clippy::collapsible_match)]
1+
#![allow(warnings)]
22

33
use crate::error::JSError;
4+
use crate::js_array::initialize_array;
5+
use crate::js_console::initialize_console_object;
46
use crate::js_math::initialize_math;
7+
use crate::js_regexp::initialize_regexp;
8+
use crate::js_string::initialize_string;
59
use crate::raise_eval_error;
610
use crate::unicode::utf8_to_utf16;
7-
use gc_arena::Mutation as MutationContext;
8-
use gc_arena::lock::RefLock as GcCell;
9-
use gc_arena::{Collect, Gc};
11+
pub(crate) use gc_arena::Mutation as MutationContext;
12+
pub(crate) use gc_arena::lock::RefLock as GcCell;
13+
pub(crate) use gc_arena::{Collect, Gc};
1014
use std::collections::HashMap;
1115

1216
mod gc;
@@ -55,25 +59,22 @@ pub fn initialize_global_constructors<'gc>(mc: &MutationContext<'gc>, env: &JSOb
5559

5660
initialize_error_constructor(mc, env)?;
5761

58-
initialize_console(mc, env)?;
62+
let console_obj = initialize_console_object(mc)?;
63+
env_set(mc, env, "console", Value::Object(console_obj))?;
5964

6065
initialize_math(mc, env)?;
66+
initialize_string(mc, env)?;
67+
initialize_array(mc, env)?;
68+
initialize_regexp(mc, env)?;
6169

6270
env_set(mc, env, "undefined", Value::Undefined)?;
6371
env_set(mc, env, "NaN", Value::Number(f64::NAN))?;
6472
env_set(mc, env, "Infinity", Value::Number(f64::INFINITY))?;
73+
env_set(mc, env, "eval", Value::Function("eval".to_string()))?;
6574

6675
Ok(())
6776
}
6877

69-
pub fn initialize_console<'gc>(mc: &MutationContext<'gc>, env: &JSObjectDataPtr<'gc>) -> Result<(), JSError> {
70-
let console_obj = new_js_object_data(mc);
71-
obj_set_key_value(mc, &console_obj, &"log".into(), Value::Function("console.log".to_string()))?;
72-
obj_set_key_value(mc, &console_obj, &"error".into(), Value::Function("console.error".to_string()))?;
73-
env_set(mc, env, "console", Value::Object(console_obj))?;
74-
Ok(())
75-
}
76-
7778
pub fn evaluate_script<T, P>(script: T, script_path: Option<P>) -> Result<String, JSError>
7879
where
7980
T: AsRef<str>,
@@ -132,6 +133,98 @@ where
132133
})
133134
}
134135

136+
// Helper to resolve a constructor's prototype object if present in `env`.
137+
pub fn get_constructor_prototype<'gc>(
138+
mc: &MutationContext<'gc>,
139+
env: &JSObjectDataPtr<'gc>,
140+
name: &str,
141+
) -> Result<Option<JSObjectDataPtr<'gc>>, JSError> {
142+
// First try to find a constructor object already stored in the environment
143+
if let Some(val_rc) = obj_get_key_value(mc, env, &name.into())? {
144+
if let Value::Object(ctor_obj) = &*val_rc.borrow() {
145+
if let Some(proto_val_rc) = obj_get_key_value(mc, ctor_obj, &"prototype".into())? {
146+
if let Value::Object(proto_obj) = &*proto_val_rc.borrow() {
147+
return Ok(Some(proto_obj.clone()));
148+
}
149+
}
150+
}
151+
}
152+
153+
// If not found, attempt to evaluate the variable to force lazy creation
154+
match evaluate_expr(mc, env, &Expr::Var(name.to_string(), None, None)) {
155+
Ok(Value::Object(ctor_obj)) => {
156+
if let Some(proto_val_rc) = obj_get_key_value(mc, &ctor_obj, &"prototype".into())? {
157+
if let Value::Object(proto_obj) = &*proto_val_rc.borrow() {
158+
return Ok(Some(proto_obj.clone()));
159+
}
160+
}
161+
Ok(None)
162+
}
163+
_ => Ok(None),
164+
}
165+
}
166+
167+
// Helper to set an object's internal prototype from a constructor name.
168+
// If the constructor.prototype is available, sets `obj.borrow_mut().prototype`
169+
// to that object. This consolidates the common pattern used when boxing
170+
// primitives and creating instances.
171+
pub fn set_internal_prototype_from_constructor<'gc>(
172+
mc: &MutationContext<'gc>,
173+
obj: &JSObjectDataPtr<'gc>,
174+
env: &JSObjectDataPtr<'gc>,
175+
ctor_name: &str,
176+
) -> Result<(), JSError> {
177+
if let Some(proto_obj) = get_constructor_prototype(mc, env, ctor_name)? {
178+
// set internal prototype pointer (store Weak to avoid cycles)
179+
obj.borrow_mut(mc).prototype = Some(proto_obj.clone());
180+
}
181+
Ok(())
182+
}
183+
184+
// Helper to initialize a collection from an iterable argument.
185+
// Used by Map, Set, WeakMap, WeakSet constructors.
186+
pub fn initialize_collection_from_iterable<'gc, F>(
187+
mc: &MutationContext<'gc>,
188+
args: &[Expr],
189+
env: &JSObjectDataPtr<'gc>,
190+
constructor_name: &str,
191+
mut process_item: F,
192+
) -> Result<(), JSError>
193+
where
194+
F: FnMut(Value<'gc>) -> Result<(), JSError>,
195+
{
196+
if args.is_empty() {
197+
return Ok(());
198+
}
199+
if args.len() > 1 {
200+
let msg = format!("{constructor_name} constructor takes at most one argument",);
201+
return Err(raise_eval_error!(msg));
202+
}
203+
let iterable = evaluate_expr(mc, env, &args[0]).map_err(|e| match e {
204+
crate::core::js_error::EvalError::Js(e) => e,
205+
crate::core::js_error::EvalError::Throw(val, _, _) => {
206+
crate::raise_eval_error!(format!("Uncaught exception: {:?}", val))
207+
}
208+
})?;
209+
match iterable {
210+
Value::Object(obj) => {
211+
let mut i = 0;
212+
loop {
213+
let key = format!("{i}");
214+
if let Some(item_val) = obj_get_key_value(mc, &obj, &key.into())? {
215+
let item = item_val.borrow().clone();
216+
process_item(item)?;
217+
} else {
218+
break;
219+
}
220+
i += 1;
221+
}
222+
Ok(())
223+
}
224+
_ => Err(raise_eval_error!(format!("{constructor_name} constructor requires an iterable"))),
225+
}
226+
}
227+
135228
/// Read a script file from disk and decode it into a UTF-8 Rust `String`.
136229
/// Supports UTF-8 (with optional BOM) and UTF-16 (LE/BE) with BOM.
137230
pub fn read_script_file<P: AsRef<std::path::Path>>(path: P) -> Result<String, JSError> {

src/core/eval.rs

Lines changed: 79 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
#![allow(dead_code, unused_variables)]
1+
#![allow(warnings)]
22

3+
use crate::js_array::handle_array_static_method;
4+
use crate::js_string::{string_from_char_code, string_from_code_point, string_raw};
35
use crate::{
46
JSError, JSErrorKind, PropertyKey, Value,
57
core::{
@@ -11,6 +13,7 @@ use crate::{
1113
raise_eval_error, raise_reference_error,
1214
unicode::{utf8_to_utf16, utf16_to_utf8},
1315
};
16+
use crate::{Token, parse_statements, tokenize};
1417
use gc_arena::Gc;
1518
use gc_arena::Mutation as MutationContext;
1619
use gc_arena::lock::RefLock as GcCell;
@@ -603,54 +606,82 @@ pub fn evaluate_expr<'gc>(mc: &MutationContext<'gc>, env: &JSObjectDataPtr<'gc>,
603606

604607
match func_val {
605608
Value::Function(name) => {
606-
if name == "console.log" {
607-
let output = eval_args
608-
.iter()
609-
.map(|v| {
610-
if is_error(v)
611-
&& let Value::Object(obj) = v
612-
{
613-
// If it has a stack property, use it
614-
if let Some(stack) = obj.borrow().get_property(mc, "stack") {
615-
return stack;
616-
}
617-
}
618-
619-
if let Value::String(s) = v {
620-
utf16_to_utf8(s)
621-
} else {
622-
value_to_string(v)
623-
}
624-
})
625-
.collect::<Vec<_>>()
626-
.join(" ");
627-
println!("{}", output);
628-
Ok(Value::Undefined)
629-
} else if name.starts_with("Math.") {
630-
let method = &name[5..];
609+
if name == "eval" {
610+
let first_arg = eval_args.get(0).cloned().unwrap_or(Value::Undefined);
611+
if let Value::String(script_str) = first_arg {
612+
let script = utf16_to_utf8(&script_str);
613+
let mut tokens = tokenize(&script).map_err(EvalError::Js)?;
614+
if tokens.last().map(|td| td.token == Token::EOF).unwrap_or(false) {
615+
tokens.pop();
616+
}
617+
let mut index = 0;
618+
let mut statements = parse_statements(&tokens, &mut index).map_err(EvalError::Js)?;
619+
// eval executes in the current environment
620+
match evaluate_statements(mc, env, &mut statements) {
621+
Ok(v) => Ok(v),
622+
Err(e) => Err(e),
623+
}
624+
} else {
625+
Ok(first_arg)
626+
}
627+
} else if let Some(method_name) = name.strip_prefix("console.") {
628+
crate::js_console::handle_console_method(mc, method_name, &eval_args, env)
629+
} else if let Some(method) = name.strip_prefix("Math.") {
631630
Ok(handle_math_call(mc, method, &eval_args, env).map_err(EvalError::Js)?)
632-
} else if name == "console.error" {
633-
let output = eval_args
634-
.iter()
635-
.map(|v| {
636-
if is_error(v)
637-
&& let Value::Object(obj) = v
638-
{
639-
// If it has a stack property, use it
640-
if let Some(stack) = obj.borrow().get_property(mc, "stack") {
641-
return stack;
642-
}
643-
}
644-
if let Value::String(s) = v {
645-
utf16_to_utf8(s)
646-
} else {
647-
value_to_string(v)
648-
}
649-
})
650-
.collect::<Vec<_>>()
651-
.join(" ");
652-
println!("{}", output);
653-
Ok(Value::Undefined)
631+
} else if name.starts_with("String.") {
632+
if name == "String.fromCharCode" {
633+
Ok(string_from_char_code(mc, &eval_args, env)?)
634+
} else if name == "String.fromCodePoint" {
635+
Ok(string_from_code_point(mc, &eval_args, env)?)
636+
} else if name == "String.raw" {
637+
Ok(string_raw(mc, &eval_args, env)?)
638+
} else if name.starts_with("String.prototype.") {
639+
let method = &name[17..];
640+
// String instance methods need a 'this' value which should be the first argument if called directly?
641+
// But here we are calling the function object directly.
642+
// Usually instance methods are called via method call syntax (obj.method()), which sets 'this'.
643+
// If we are here, it means we called the function object directly, e.g. String.prototype.slice.call(str, ...)
644+
// But our current implementation of function calls doesn't handle 'this' binding for native functions well yet
645+
// unless it's a method call.
646+
// However, if we are calling it as a method of String.prototype, 'this' should be passed.
647+
// But here 'name' is just a string identifier we assigned to the function.
648+
// We need to know the 'this' value.
649+
// For now, let's assume the first argument is 'this' if it's called as a standalone function?
650+
// No, that's not how it works.
651+
// If we are here, it means we are executing the native function body.
652+
// We need to access the 'this' binding from the environment or context.
653+
// But our native functions don't have a captured environment with 'this'.
654+
// We need to change how we handle native function calls to include 'this'.
655+
656+
// Wait, the current architecture seems to rely on the caller to handle 'this' or pass it?
657+
// In `evaluate_expr` for `Expr::Call`, we don't seem to pass 'this' explicitly for native functions
658+
// unless it was a method call.
659+
660+
// Let's look at how `Expr::Call` handles method calls.
661+
// It evaluates `func_expr`. If it's a property access, it sets `this`.
662+
// But `evaluate_expr` returns a `Value`, not a reference.
663+
// So we lose the `this` context unless we handle `Expr::Call` specially for property access.
664+
665+
// Actually, `Expr::Call` implementation in `eval.rs` (lines 600+) just evaluates `func_expr`.
666+
// It doesn't seem to handle `this` binding for method calls properly yet?
667+
// Ah, I see `Expr::Call` logic is split.
668+
// Let's check `Expr::Call` implementation again.
669+
670+
Err(EvalError::Js(raise_eval_error!(
671+
"String prototype methods not fully supported in direct calls yet"
672+
)))
673+
} else {
674+
Err(EvalError::Js(raise_eval_error!(format!("Unknown String function: {}", name))))
675+
}
676+
} else if name.starts_with("Array.") {
677+
if name.starts_with("Array.prototype.") {
678+
Err(EvalError::Js(raise_eval_error!(
679+
"Array prototype methods not fully supported in direct calls yet"
680+
)))
681+
} else {
682+
let method = &name[6..];
683+
Ok(handle_array_static_method(mc, method, &eval_args, env)?)
684+
}
654685
} else {
655686
Err(EvalError::Js(raise_eval_error!(format!("Unknown native function: {}", name))))
656687
}

src/core/value.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,32 @@ pub fn value_to_string<'gc>(val: &Value<'gc>) -> String {
375375
}
376376
}
377377

378+
pub fn value_to_sort_string<'gc>(val: &Value<'gc>) -> String {
379+
match val {
380+
Value::Undefined => "undefined".to_string(),
381+
Value::Null => "null".to_string(),
382+
_ => value_to_string(val),
383+
}
384+
}
385+
386+
pub fn values_equal<'gc>(_mc: &MutationContext<'gc>, v1: &Value<'gc>, v2: &Value<'gc>) -> bool {
387+
match (v1, v2) {
388+
(Value::Number(n1), Value::Number(n2)) => {
389+
if n1.is_nan() && n2.is_nan() {
390+
true
391+
} else {
392+
n1 == n2
393+
}
394+
}
395+
(Value::String(s1), Value::String(s2)) => s1 == s2,
396+
(Value::Boolean(b1), Value::Boolean(b2)) => b1 == b2,
397+
(Value::Undefined, Value::Undefined) => true,
398+
(Value::Null, Value::Null) => true,
399+
(Value::Object(o1), Value::Object(o2)) => Gc::ptr_eq(*o1, *o2),
400+
_ => false,
401+
}
402+
}
403+
378404
pub fn obj_get_key_value<'gc>(
379405
_mc: &MutationContext<'gc>,
380406
obj: &JSObjectDataPtr<'gc>,
@@ -390,6 +416,10 @@ pub fn obj_get_key_value<'gc>(
390416
Ok(None)
391417
}
392418

419+
pub fn get_own_property<'gc>(obj: &JSObjectDataPtr<'gc>, key: &PropertyKey<'gc>) -> Option<GcPtr<'gc, Value<'gc>>> {
420+
obj.borrow().properties.get(key).cloned()
421+
}
422+
393423
pub fn obj_set_key_value<'gc>(
394424
mc: &MutationContext<'gc>,
395425
obj: &JSObjectDataPtr<'gc>,
@@ -401,6 +431,16 @@ pub fn obj_set_key_value<'gc>(
401431
Ok(())
402432
}
403433

434+
pub fn obj_set_rc<'gc>(
435+
mc: &MutationContext<'gc>,
436+
obj: &JSObjectDataPtr<'gc>,
437+
key: &PropertyKey<'gc>,
438+
val: GcPtr<'gc, Value<'gc>>,
439+
) -> Result<(), JSError> {
440+
obj.borrow_mut(mc).insert(key.clone(), val);
441+
Ok(())
442+
}
443+
404444
pub fn env_get<'gc>(env: &JSObjectDataPtr<'gc>, key: &str) -> Option<GcPtr<'gc, Value<'gc>>> {
405445
env.borrow().properties.get(&PropertyKey::String(key.to_string())).cloned()
406446
}

0 commit comments

Comments
 (0)