diff --git a/src/ast/compiler/macros.rs b/src/ast/compiler/macros.rs index 97b5e02..6d15d1b 100644 --- a/src/ast/compiler/macros.rs +++ b/src/ast/compiler/macros.rs @@ -662,7 +662,7 @@ mod tests { use crate::ast::compiler::binder::{CompilerScope, LocalInfo}; use crate::ast::compiler::Binder; use crate::ast::diagnostics::Diagnostics; - use crate::ast::nodes::{Address, GlobalIdx, VirtualId}; + use crate::ast::nodes::{Address, GlobalIdx}; use crate::ast::parser::Parser; use crate::ast::types::{NodeIdentity, Purity, SourceLocation, StaticType, Value}; diff --git a/src/ast/lexer.rs b/src/ast/lexer.rs index 170a489..64b77d1 100644 --- a/src/ast/lexer.rs +++ b/src/ast/lexer.rs @@ -309,7 +309,7 @@ mod tests { #[test] fn test_lex_invisible_chars() { // U+200B Zero Width Space should be ignored inside identifiers - let source = "a​b"; + let source = "a\u{200B}b"; let mut lexer = Lexer::new(source); let token = lexer.next_token().unwrap(); if let TokenKind::Identifier(id) = token.kind { diff --git a/src/ast/mcp_server.rs b/src/ast/mcp_server.rs index 1ec9cbc..a64613f 100644 --- a/src/ast/mcp_server.rs +++ b/src/ast/mcp_server.rs @@ -1,3 +1,5 @@ +use std::cell::RefCell; + use rmcp::{ ServerHandler, handler::server::{router::tool::ToolRouter, wrapper::Parameters}, @@ -24,6 +26,15 @@ pub struct SymbolInput { pub name: String, } +thread_local! { + /// Persistent environment for REPL-style evaluation across multiple calls. + /// Lives in a thread-local because `Environment` uses `Rc>` + /// internally and is not `Send`/`Sync`, while `rmcp` requires the server + /// struct to be `Send + Sync`. Safe because we run on a single-threaded + /// Tokio runtime. + static REPL_ENV: RefCell> = const { RefCell::new(None) }; +} + /// MCP server exposing the Myc compiler as a set of tools for LLMs. #[derive(Clone)] pub struct MycMcpServer { @@ -164,6 +175,34 @@ impl MycMcpServer { None => format!("No documentation found for symbol '{}'.", name), } } + + /// Evaluates a Myc expression in a persistent REPL session. + /// Bindings from previous calls are preserved. + #[tool(description = "Evaluate a Myc expression in a persistent REPL session. \ + Unlike eval_myc, bindings (variables, functions, macros) from previous calls \ + are preserved across invocations. Use repl_reset to start a fresh session. \ + Example workflow: call repl_eval with '(def x 42)', then '(+ x 1)' returns 43.")] + fn repl_eval(&self, Parameters(CodeInput { code }): Parameters) -> String { + REPL_ENV.with(|cell| { + let mut env_opt = cell.borrow_mut(); + let env = env_opt.get_or_insert_with(Self::make_env); + match env.run_script(&code) { + Ok(value) => format!("{}", value), + Err(e) => format!("Error: {}", e), + } + }) + } + + /// Resets the REPL session, discarding all accumulated bindings. + #[tool(description = "Reset the persistent REPL session, discarding all variables, \ + functions, and macros defined in previous repl_eval calls. \ + The next repl_eval call will start with a fresh environment.")] + fn repl_reset(&self) -> String { + REPL_ENV.with(|cell| { + *cell.borrow_mut() = None; + }); + "REPL session reset.".to_string() + } } #[tool_handler] @@ -180,7 +219,9 @@ impl ServerHandler for MycMcpServer { `list_builtins` to see all available built-in functions, \ `check_syntax` to validate code, \ `eval_myc` to run expressions, and \ - `dump_ast` to inspect the compiled AST.", + `dump_ast` to inspect the compiled AST. \ + For interactive sessions, use `repl_eval` to evaluate expressions with \ + persistent bindings across calls, and `repl_reset` to start fresh.", ) } }