use chumsky::{input::Stream, prelude::*}; use imbl::HashMap; use wasm_bindgen::prelude::*; use std::rc::Rc; use std::cell::RefCell; const DEBUG_SCRIPT_COMPILE: bool = false; const DEBUG_SCRIPT_RUN: bool = false; const DEBUG_PRELUDE_COMPILE: bool = false; const DEBUG_PRELUDE_RUN: bool = false; mod io; mod ast; use crate::ast::Ast; mod base; mod world; use crate::world::{World, Zoo}; mod spans; use crate::spans::Spanned; mod lexer; use crate::lexer::lexer; mod parser; use crate::parser::parser; mod validator; use crate::validator::Validator; mod errors; use crate::errors::report_invalidation; mod chunk; mod op; mod compiler; use crate::compiler::Compiler; mod value; use value::Value; mod vm; use vm::Creature; const PRELUDE: &str = include_str!("../assets/test_prelude.ld"); fn prelude() -> HashMap<&'static str, Value> { let tokens = lexer().parse(PRELUDE).into_output_errors().0.unwrap(); let (parsed, parse_errors) = parser() .parse(Stream::from_iter(tokens).map((0..PRELUDE.len()).into(), |(t, s)| (t, s))) .into_output_errors(); if !parse_errors.is_empty() { println!("ERROR PARSING PRELUDE:"); println!("{:?}", parse_errors); panic!(); } let parsed = parsed.unwrap(); let (ast, span) = &parsed; let base = base::make_base(); let mut base_env = imbl::HashMap::new(); base_env.insert("base", base.clone()); let mut validator = Validator::new(ast, span, "prelude", PRELUDE, base_env); validator.validate(); if !validator.errors.is_empty() { println!("VALIDATION ERRORS IN PRLUDE:"); report_invalidation(validator.errors); panic!("validator errors in prelude"); } let parsed: &'static Spanned = Box::leak(Box::new(parsed)); let mut compiler = Compiler::new( parsed, "prelude", PRELUDE, 0, HashMap::new(), DEBUG_PRELUDE_COMPILE, ); compiler.emit_constant(base); compiler.bind("base"); compiler.compile(); let chunk = compiler.chunk; log("compiled prelude"); let stub_zoo = Rc::new(RefCell::new(Zoo::new())); let mut prld_sync = Creature::new(chunk, stub_zoo, DEBUG_PRELUDE_RUN); prld_sync.interpret(); log("run prelude synchronously"); let prelude = prld_sync.result.unwrap().unwrap(); match prelude { Value::Dict(hashmap) => *hashmap, _ => unreachable!(), } } #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); } #[wasm_bindgen] pub async fn ludus(src: String) -> String { console_error_panic_hook::set_once(); log("successfully entered ludus fn in Rust"); let src = src.to_string().leak(); log(src); let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); if !lex_errs.is_empty() { return format!("{:?}", lex_errs); } let tokens = tokens.unwrap(); log("successfully tokenized source"); let (parse_result, parse_errors) = parser() .parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s))) .into_output_errors(); if !parse_errors.is_empty() { return format!("{:?}", parse_errors); } // ::sigh:: The AST should be 'static // This simplifies lifetimes, and // in any event, the AST should live forever let parsed: &'static Spanned = Box::leak(Box::new(parse_result.unwrap())); log("successfully parsed source"); let prelude = prelude(); log("successfully loaded prelude"); let postlude = prelude.clone(); // let prelude = imbl::HashMap::new(); let mut validator = Validator::new(&parsed.0, &parsed.1, "user input", src, prelude.clone()); validator.validate(); // TODO: validator should generate a string, not print to the console if !validator.errors.is_empty() { report_invalidation(validator.errors); return "Ludus found some validation errors.".to_string(); } let mut compiler = Compiler::new( parsed, "sandbox", src, 0, prelude.clone(), DEBUG_SCRIPT_COMPILE, ); // let base = base::make_base(); // compiler.emit_constant(base); // compiler.bind("base"); compiler.compile(); log("successfully compiled source"); if DEBUG_SCRIPT_COMPILE { println!("=== source code ==="); println!("{src}"); compiler.disassemble(); println!("\n\n") } if DEBUG_SCRIPT_RUN { println!("=== vm run ==="); } let vm_chunk = compiler.chunk; let mut world = World::new(vm_chunk, prelude.clone(), DEBUG_SCRIPT_RUN); let console = prelude .get("console") .expect("prelude must have a console") .clone(); log("loaded world and console"); world.run().await; let result = world.result.clone().unwrap(); log("ran script"); log(format!("{:?}", result).as_str()); let console = postlude.get("console").unwrap(); let Value::Box(console) = console else { unreachable!() }; let Value::List(ref lines) = *console.borrow() else { unreachable!() }; let mut console = lines .iter() .map(|line| line.stringify()) .collect::>() .join("\n"); let turtle_commands = postlude.get("turtle_commands").unwrap(); let Value::Box(commands) = turtle_commands else { unreachable!() }; let commands = commands.borrow(); let commands = commands.to_json().unwrap(); let output = match result { Ok(val) => val.show(), Err(panic) => { console = format!("{console}\nLudus panicked! {panic}"); "".to_string() } }; if DEBUG_SCRIPT_RUN { // vm.print_stack(); } // TODO: use serde_json to make this more robust? format!( "{{\"result\":\"{output}\",\"io\":{{\"stdout\":{{\"proto\":[\"text-stream\",\"0.1.0\"],\"data\":\"{console}\"}},\"turtle\":{{\"proto\":[\"turtle-graphics\",\"0.1.0\"],\"data\":{commands}}}}}}}" ) } pub fn fmt(src: &'static str) -> Result { let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); if !lex_errs.is_empty() { println!("{:?}", lex_errs); return Err(format!("{:?}", lex_errs)); } let tokens = tokens.unwrap(); let (parse_result, parse_errors) = parser() .parse(Stream::from_iter(tokens).map((0..src.len()).into(), |(t, s)| (t, s))) .into_output_errors(); if !parse_errors.is_empty() { return Err(format!("{:?}", parse_errors)); } // ::sigh:: The AST should be 'static // This simplifies lifetimes, and // in any event, the AST should live forever let parsed: &'static Spanned = Box::leak(Box::new(parse_result.unwrap())); Ok(parsed.0.show()) }