From 333f5c9518d4d9dd11f11e044ca1dcadc0cab76d Mon Sep 17 00:00:00 2001 From: Scott Richmond Date: Wed, 25 Jun 2025 15:41:44 -0400 Subject: [PATCH] start splitting rudus into lib & main --- src/lib.rs | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a170a56 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,218 @@ +use chumsky::{input::Stream, prelude::*}; +use imbl::HashMap; +use wasm_bindgen::prelude::*; + +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 base; + +mod spans; +use crate::spans::Spanned; + +mod lexer; +use crate::lexer::lexer; + +mod parser; +use crate::parser::{parser, Ast}; + +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::Vm; + +const PRELUDE: &str = include_str!("../assets/test_prelude.ld"); + +pub 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!(); + } + + 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; + let mut vm = Vm::new(chunk, DEBUG_PRELUDE_RUN); + let prelude = vm.run().clone().unwrap(); + match prelude { + Value::Dict(hashmap) => *hashmap, + _ => unreachable!(), + } +} + +#[wasm_bindgen] +pub fn run(src: String) -> String { + let src = src.leak(); + let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); + if !lex_errs.is_empty() { + return 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 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())); + + let prelude = 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, DEBUG_SCRIPT_COMPILE); + // let base = base::make_base(); + // compiler.emit_constant(base); + // compiler.bind("base"); + + compiler.compile(); + 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 vm = Vm::new(vm_chunk, DEBUG_SCRIPT_RUN); + let result = vm.run(); + + 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().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 ld_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()) +}