we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement Console API: console.log, console.error, console.warn

Add the Console API as built-in JS globals (Phase 11, issue 1 of 8).

- ConsoleOutput trait with configurable output sink (default: stdout/stderr)
- console.log/info/debug write to the log channel
- console.error writes to the error channel
- console.warn writes to the warn channel
- Rich formatting: arrays show contents, objects show properties
- Cycle detection and depth limiting for nested structures
- Vm::set_console_output() for redirecting output (dev tools, testing)
- 14 tests covering all methods, formatting, and channel routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+368 -3
+155 -3
crates/js/src/builtins.rs
··· 8 8 use crate::gc::{Gc, GcRef}; 9 9 use crate::vm::*; 10 10 use std::cell::RefCell; 11 - use std::collections::HashMap; 11 + use std::collections::{HashMap, HashSet}; 12 12 use std::time::{SystemTime, UNIX_EPOCH}; 13 13 14 14 /// Native callback type alias to satisfy clippy::type_complexity. ··· 252 252 253 253 // Create and register JSON object (static methods only). 254 254 init_json_object(vm); 255 + 256 + // Create and register console object. 257 + init_console_object(vm); 255 258 256 259 // Register global utility functions. 257 260 init_global_functions(vm); ··· 1282 1285 &mut NativeContext { 1283 1286 gc: ctx.gc, 1284 1287 this: ctx.this.clone(), 1288 + console_output: ctx.console_output, 1285 1289 }, 1286 1290 ) 1287 1291 .or_else(|_| Ok(Value::String(String::new()))) ··· 4496 4500 } 4497 4501 let this_val = ctx.this.clone(); 4498 4502 for (k, v) in pairs { 4499 - call_native_callback(ctx.gc, callback, &[v, k, this_val.clone()])?; 4503 + call_native_callback( 4504 + ctx.gc, 4505 + callback, 4506 + &[v, k, this_val.clone()], 4507 + ctx.console_output, 4508 + )?; 4500 4509 } 4501 4510 Ok(Value::Undefined) 4502 4511 } ··· 4600 4609 gc: &mut Gc<HeapObject>, 4601 4610 func_ref: GcRef, 4602 4611 args: &[Value], 4612 + console_output: &dyn ConsoleOutput, 4603 4613 ) -> Result<Value, RuntimeError> { 4604 4614 match gc.get(func_ref) { 4605 4615 Some(HeapObject::Function(fdata)) => match &fdata.kind { ··· 4608 4618 let mut ctx = NativeContext { 4609 4619 gc, 4610 4620 this: Value::Undefined, 4621 + console_output, 4611 4622 }; 4612 4623 cb(args, &mut ctx) 4613 4624 } ··· 4744 4755 } 4745 4756 let this_val = ctx.this.clone(); 4746 4757 for v in values { 4747 - call_native_callback(ctx.gc, callback, &[v.clone(), v, this_val.clone()])?; 4758 + call_native_callback( 4759 + ctx.gc, 4760 + callback, 4761 + &[v.clone(), v, this_val.clone()], 4762 + ctx.console_output, 4763 + )?; 4748 4764 } 4749 4765 Ok(Value::Undefined) 4750 4766 } ··· 6345 6361 } 6346 6362 out.push('"'); 6347 6363 out 6364 + } 6365 + 6366 + // ── Console object ────────────────────────────────────────── 6367 + 6368 + fn init_console_object(vm: &mut Vm) { 6369 + let mut data = ObjectData::new(); 6370 + if let Some(proto) = vm.object_prototype { 6371 + data.prototype = Some(proto); 6372 + } 6373 + let console_ref = vm.gc.alloc(HeapObject::Object(data)); 6374 + 6375 + let methods: &[NativeMethod] = &[ 6376 + ("log", console_log), 6377 + ("info", console_log), 6378 + ("debug", console_log), 6379 + ("error", console_error), 6380 + ("warn", console_warn), 6381 + ]; 6382 + for &(name, callback) in methods { 6383 + let func = make_native(&mut vm.gc, name, callback); 6384 + set_builtin_prop(&mut vm.gc, console_ref, name, Value::Function(func)); 6385 + } 6386 + 6387 + vm.set_global("console", Value::Object(console_ref)); 6388 + } 6389 + 6390 + /// Format a JS value for console output with richer detail than `to_js_string`. 6391 + /// Arrays show their contents and objects show their properties. 6392 + fn console_format_value( 6393 + value: &Value, 6394 + gc: &Gc<HeapObject>, 6395 + depth: usize, 6396 + seen: &mut HashSet<GcRef>, 6397 + ) -> String { 6398 + const MAX_DEPTH: usize = 4; 6399 + match value { 6400 + Value::Undefined => "undefined".to_string(), 6401 + Value::Null => "null".to_string(), 6402 + Value::Boolean(b) => b.to_string(), 6403 + Value::Number(n) => js_number_to_string(*n), 6404 + Value::String(s) => s.clone(), 6405 + Value::Function(gc_ref) => gc 6406 + .get(*gc_ref) 6407 + .and_then(|obj| match obj { 6408 + HeapObject::Function(f) => Some(format!("[Function: {}]", f.name)), 6409 + _ => None, 6410 + }) 6411 + .unwrap_or_else(|| "[Function]".to_string()), 6412 + Value::Object(gc_ref) => { 6413 + if depth > MAX_DEPTH || seen.contains(gc_ref) { 6414 + return "[Object]".to_string(); 6415 + } 6416 + seen.insert(*gc_ref); 6417 + let result = match gc.get(*gc_ref) { 6418 + Some(HeapObject::Object(obj_data)) => { 6419 + // Check if it's an array (has a "length" property). 6420 + if obj_data.properties.contains_key("length") { 6421 + format_array(obj_data, gc, depth, seen) 6422 + } else { 6423 + format_object(obj_data, gc, depth, seen) 6424 + } 6425 + } 6426 + _ => "[Object]".to_string(), 6427 + }; 6428 + seen.remove(gc_ref); 6429 + result 6430 + } 6431 + } 6432 + } 6433 + 6434 + fn format_array( 6435 + data: &ObjectData, 6436 + gc: &Gc<HeapObject>, 6437 + depth: usize, 6438 + seen: &mut HashSet<GcRef>, 6439 + ) -> String { 6440 + let len = data 6441 + .properties 6442 + .get("length") 6443 + .map(|p| p.value.to_number() as usize) 6444 + .unwrap_or(0); 6445 + let mut parts = Vec::with_capacity(len); 6446 + for i in 0..len { 6447 + let val = data 6448 + .properties 6449 + .get(&i.to_string()) 6450 + .map(|p| &p.value) 6451 + .unwrap_or(&Value::Undefined); 6452 + parts.push(console_format_value(val, gc, depth + 1, seen)); 6453 + } 6454 + format!("[ {} ]", parts.join(", ")) 6455 + } 6456 + 6457 + fn format_object( 6458 + data: &ObjectData, 6459 + gc: &Gc<HeapObject>, 6460 + depth: usize, 6461 + seen: &mut HashSet<GcRef>, 6462 + ) -> String { 6463 + if data.properties.is_empty() { 6464 + return "{}".to_string(); 6465 + } 6466 + let mut parts = Vec::new(); 6467 + for (key, prop) in &data.properties { 6468 + let val_str = console_format_value(&prop.value, gc, depth + 1, seen); 6469 + parts.push(format!("{}: {}", key, val_str)); 6470 + } 6471 + parts.sort(); 6472 + format!("{{ {} }}", parts.join(", ")) 6473 + } 6474 + 6475 + /// Format all arguments for a console method, separated by spaces. 6476 + fn console_format_args(args: &[Value], gc: &Gc<HeapObject>) -> String { 6477 + let mut seen = HashSet::new(); 6478 + args.iter() 6479 + .map(|v| console_format_value(v, gc, 0, &mut seen)) 6480 + .collect::<Vec<_>>() 6481 + .join(" ") 6482 + } 6483 + 6484 + fn console_log(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 6485 + let msg = console_format_args(args, ctx.gc); 6486 + ctx.console_output.log(&msg); 6487 + Ok(Value::Undefined) 6488 + } 6489 + 6490 + fn console_error(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 6491 + let msg = console_format_args(args, ctx.gc); 6492 + ctx.console_output.error(&msg); 6493 + Ok(Value::Undefined) 6494 + } 6495 + 6496 + fn console_warn(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 6497 + let msg = console_format_args(args, ctx.gc); 6498 + ctx.console_output.warn(&msg); 6499 + Ok(Value::Undefined) 6348 6500 } 6349 6501 6350 6502 // ── Global utility functions ─────────────────────────────────
+213
crates/js/src/vm.rs
··· 194 194 pub callback: fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>, 195 195 } 196 196 197 + /// Trait for console output. Allows redirecting console output to a dev tools 198 + /// panel or capturing it in tests. The default implementation writes to 199 + /// stdout/stderr. 200 + pub trait ConsoleOutput { 201 + fn log(&self, message: &str); 202 + fn error(&self, message: &str); 203 + fn warn(&self, message: &str); 204 + } 205 + 206 + /// Default console output that writes to stdout/stderr. 207 + pub struct StdConsoleOutput; 208 + 209 + impl ConsoleOutput for StdConsoleOutput { 210 + fn log(&self, message: &str) { 211 + println!("{}", message); 212 + } 213 + fn error(&self, message: &str) { 214 + eprintln!("{}", message); 215 + } 216 + fn warn(&self, message: &str) { 217 + eprintln!("{}", message); 218 + } 219 + } 220 + 197 221 /// Context passed to native functions, providing GC access and `this` binding. 198 222 pub struct NativeContext<'a> { 199 223 pub gc: &'a mut Gc<HeapObject>, 200 224 pub this: Value, 225 + pub console_output: &'a dyn ConsoleOutput, 201 226 } 202 227 203 228 // ── JS Value ────────────────────────────────────────────────── ··· 746 771 pub regexp_prototype: Option<GcRef>, 747 772 /// Built-in Promise.prototype (for Promise objects). 748 773 pub promise_prototype: Option<GcRef>, 774 + /// Console output sink (configurable for dev tools or testing). 775 + console_output: Box<dyn ConsoleOutput>, 749 776 } 750 777 751 778 /// Maximum register file size. ··· 770 797 date_prototype: None, 771 798 regexp_prototype: None, 772 799 promise_prototype: None, 800 + console_output: Box::new(StdConsoleOutput), 773 801 }; 774 802 crate::builtins::init_builtins(&mut vm); 775 803 vm 804 + } 805 + 806 + /// Replace the console output sink (e.g. for dev tools or testing). 807 + pub fn set_console_output(&mut self, output: Box<dyn ConsoleOutput>) { 808 + self.console_output = output; 776 809 } 777 810 778 811 /// Set an instruction limit. The VM will return a RuntimeError after ··· 831 864 let mut ctx = NativeContext { 832 865 gc: &mut self.gc, 833 866 this, 867 + console_output: &*self.console_output, 834 868 }; 835 869 let result = (native.callback)(args, &mut ctx)?; 836 870 ··· 2355 2389 let mut ctx = NativeContext { 2356 2390 gc: &mut self.gc, 2357 2391 this, 2392 + console_output: &*self.console_output, 2358 2393 }; 2359 2394 match callback(&args, &mut ctx) { 2360 2395 Ok(val) => { ··· 7449 7484 let prog = 7450 7485 crate::parser::Parser::parse("async function f() { for await (let x of iter) { } }"); 7451 7486 assert!(prog.is_ok()); 7487 + } 7488 + 7489 + // ── Console API tests ───────────────────────────────────── 7490 + 7491 + use std::cell::RefCell; 7492 + use std::rc::Rc; 7493 + 7494 + /// A console output sink that captures messages for testing. 7495 + struct CapturedConsole { 7496 + log_messages: RefCell<Vec<String>>, 7497 + error_messages: RefCell<Vec<String>>, 7498 + warn_messages: RefCell<Vec<String>>, 7499 + } 7500 + 7501 + impl CapturedConsole { 7502 + fn new() -> Self { 7503 + Self { 7504 + log_messages: RefCell::new(Vec::new()), 7505 + error_messages: RefCell::new(Vec::new()), 7506 + warn_messages: RefCell::new(Vec::new()), 7507 + } 7508 + } 7509 + } 7510 + 7511 + impl ConsoleOutput for CapturedConsole { 7512 + fn log(&self, message: &str) { 7513 + self.log_messages.borrow_mut().push(message.to_string()); 7514 + } 7515 + fn error(&self, message: &str) { 7516 + self.error_messages.borrow_mut().push(message.to_string()); 7517 + } 7518 + fn warn(&self, message: &str) { 7519 + self.warn_messages.borrow_mut().push(message.to_string()); 7520 + } 7521 + } 7522 + 7523 + /// Helper: compile and execute JS, capturing console output. 7524 + fn eval_with_console(source: &str) -> (Result<Value, RuntimeError>, Rc<CapturedConsole>) { 7525 + let console = Rc::new(CapturedConsole::new()); 7526 + let program = Parser::parse(source).expect("parse failed"); 7527 + let func = compiler::compile(&program).expect("compile failed"); 7528 + let mut vm = Vm::new(); 7529 + vm.set_console_output(Box::new(RcConsole(console.clone()))); 7530 + let result = vm.execute(&func); 7531 + (result, console) 7532 + } 7533 + 7534 + /// Wrapper to use Rc<CapturedConsole> as Box<dyn ConsoleOutput>. 7535 + struct RcConsole(Rc<CapturedConsole>); 7536 + 7537 + impl ConsoleOutput for RcConsole { 7538 + fn log(&self, message: &str) { 7539 + self.0.log(message); 7540 + } 7541 + fn error(&self, message: &str) { 7542 + self.0.error(message); 7543 + } 7544 + fn warn(&self, message: &str) { 7545 + self.0.warn(message); 7546 + } 7547 + } 7548 + 7549 + #[test] 7550 + fn test_console_log_string() { 7551 + let (result, console) = eval_with_console("console.log('hello')"); 7552 + assert!(result.is_ok()); 7553 + assert!(matches!(result.unwrap(), Value::Undefined)); 7554 + let logs = console.log_messages.borrow(); 7555 + assert_eq!(logs.len(), 1); 7556 + assert_eq!(logs[0], "hello"); 7557 + } 7558 + 7559 + #[test] 7560 + fn test_console_log_multiple_args() { 7561 + let (_, console) = eval_with_console("console.log('a', 1, true)"); 7562 + let logs = console.log_messages.borrow(); 7563 + assert_eq!(logs[0], "a 1 true"); 7564 + } 7565 + 7566 + #[test] 7567 + fn test_console_error_to_error_channel() { 7568 + let (_, console) = eval_with_console("console.error('oops')"); 7569 + assert!(console.log_messages.borrow().is_empty()); 7570 + let errors = console.error_messages.borrow(); 7571 + assert_eq!(errors.len(), 1); 7572 + assert_eq!(errors[0], "oops"); 7573 + } 7574 + 7575 + #[test] 7576 + fn test_console_warn_to_warn_channel() { 7577 + let (_, console) = eval_with_console("console.warn('warning')"); 7578 + assert!(console.log_messages.borrow().is_empty()); 7579 + let warns = console.warn_messages.borrow(); 7580 + assert_eq!(warns.len(), 1); 7581 + assert_eq!(warns[0], "warning"); 7582 + } 7583 + 7584 + #[test] 7585 + fn test_console_info_aliases_log() { 7586 + let (_, console) = eval_with_console("console.info('info msg')"); 7587 + let logs = console.log_messages.borrow(); 7588 + assert_eq!(logs.len(), 1); 7589 + assert_eq!(logs[0], "info msg"); 7590 + } 7591 + 7592 + #[test] 7593 + fn test_console_debug_aliases_log() { 7594 + let (_, console) = eval_with_console("console.debug('debug msg')"); 7595 + let logs = console.log_messages.borrow(); 7596 + assert_eq!(logs.len(), 1); 7597 + assert_eq!(logs[0], "debug msg"); 7598 + } 7599 + 7600 + #[test] 7601 + fn test_console_log_returns_undefined() { 7602 + let result = eval("console.log('test')").unwrap(); 7603 + assert!(matches!(result, Value::Undefined)); 7604 + } 7605 + 7606 + #[test] 7607 + fn test_console_log_no_args() { 7608 + let (_, console) = eval_with_console("console.log()"); 7609 + let logs = console.log_messages.borrow(); 7610 + assert_eq!(logs.len(), 1); 7611 + assert_eq!(logs[0], ""); 7612 + } 7613 + 7614 + #[test] 7615 + fn test_console_log_primitives() { 7616 + let (_, console) = eval_with_console( 7617 + "console.log(undefined); console.log(null); console.log(42); console.log(true);", 7618 + ); 7619 + let logs = console.log_messages.borrow(); 7620 + assert_eq!(logs.len(), 4); 7621 + assert_eq!(logs[0], "undefined"); 7622 + assert_eq!(logs[1], "null"); 7623 + assert_eq!(logs[2], "42"); 7624 + assert_eq!(logs[3], "true"); 7625 + } 7626 + 7627 + #[test] 7628 + fn test_console_log_array() { 7629 + let (_, console) = eval_with_console("console.log([1, 2, 3])"); 7630 + let logs = console.log_messages.borrow(); 7631 + assert_eq!(logs[0], "[ 1, 2, 3 ]"); 7632 + } 7633 + 7634 + #[test] 7635 + fn test_console_log_object() { 7636 + let (_, console) = eval_with_console("console.log({x: 1})"); 7637 + let logs = console.log_messages.borrow(); 7638 + assert_eq!(logs[0], "{ x: 1 }"); 7639 + } 7640 + 7641 + #[test] 7642 + fn test_console_typeof() { 7643 + match eval("typeof console.log").unwrap() { 7644 + Value::String(s) => assert_eq!(s, "function"), 7645 + v => panic!("expected 'function', got {v:?}"), 7646 + } 7647 + } 7648 + 7649 + #[test] 7650 + fn test_console_is_object() { 7651 + match eval("typeof console").unwrap() { 7652 + Value::String(s) => assert_eq!(s, "object"), 7653 + v => panic!("expected 'object', got {v:?}"), 7654 + } 7655 + } 7656 + 7657 + #[test] 7658 + fn test_console_never_throws() { 7659 + let result = eval("console.log({x: 1}); console.log([1,2]); console.log(undefined); 'ok'"); 7660 + assert!(result.is_ok()); 7661 + match result.unwrap() { 7662 + Value::String(s) => assert_eq!(s, "ok"), 7663 + v => panic!("expected 'ok', got {v:?}"), 7664 + } 7452 7665 } 7453 7666 }