feat(repl): Phase 288.1 session persistence + auto-display

実装内容:
- AST Rewriter (~430行): 未宣言変数を __repl.get/set に変換
- ExternCall Bridge: VM で __repl.get/set ハンドラー実装
- Rc<RefCell<>> セッション共有: VM と REPL runner 間で永続化
- 式自動表示: pure expression の結果を自動出力
- _ 変数: 最後の表示値を保存(Void は除外)
- .reset 実装: セッション変数の完全クリア
- Fail-Fast: 未定義変数読み取りで明示的エラー + ヒント

変更ファイル (8ファイル, +592行):
- src/runner/repl/ast_rewriter.rs (NEW, +430行)
- src/runner/repl/repl_runner.rs (+84/-35行)
- src/backend/mir_interpreter/handlers/externals.rs (+54行)
- src/mir/builder/calls/build.rs (+41行)
- src/backend/mir_interpreter/mod.rs (+12行)
- src/runner/repl/repl_session.rs (+11/-9行)
- src/runner/repl/mod.rs (+2行)
- src/runner/mod.rs (+2/-1行)

REPL専用設計(src/mir/builder/calls/build.rs の特別扱い理由):
- __repl.get/set は REPL mode 専用の橋渡し機能
- try_build_repl_method_call() で早期検出・ExternCall 変換
- file mode では決して使用されない(VM で "outside REPL mode" エラー)
- 将来的にも file mode への影響ゼロを保証

検証済み:
- 変数永続化: x = 42; print(x) → 42 
- 式自動表示: 1 + 1 → 2 
- _ 変数: 10 * 2 → 20; _ → 20 
- Fail-Fast: 未定義エラー + ヒント 
- 回帰テスト: 154/154 PASS 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 23:04:59 +09:00
parent e986e279b4
commit 46fbe12ce6
8 changed files with 597 additions and 35 deletions

View File

@ -264,6 +264,60 @@ impl MirInterpreter {
}
return Err(self.err_invalid("hostbridge.extern_invoke unsupported [externals]"));
}
// Phase 288.1: REPL session variable bridge
("__repl", "get") => {
// args: [name: String]
if args.len() != 1 {
return Err(self.err_invalid(format!(
"__repl.get expects 1 argument, got {}",
args.len()
)));
}
let name = self.reg_load(args[0])?.to_string();
// REPL session から取得
if let Some(session) = &self.repl_session {
// Clone the value before the borrow ends to avoid borrowing conflicts
let value_opt = session.borrow().get(&name).cloned();
match value_opt {
Some(value) => {
self.write_result(dst, value);
Ok(())
}
None => Err(self.err_invalid(format!(
"Undefined variable: '{}'\nHint: Variable not defined. Assign a value first.",
name
))),
}
} else {
Err(self.err_invalid("__repl.get called outside REPL mode".to_string()))
}
}
("__repl", "set") => {
// args: [name: String, value: Any]
if args.len() != 2 {
return Err(self.err_invalid(format!(
"__repl.set expects 2 arguments, got {}",
args.len()
)));
}
let name = self.reg_load(args[0])?.to_string();
let value = self.reg_load(args[1])?.clone();
// REPL session に保存
if let Some(session) = &self.repl_session {
session.borrow_mut().set(name, value);
self.write_void(dst);
Ok(())
} else {
Err(self.err_invalid("__repl.set called outside REPL mode".to_string()))
}
}
_ => Err(self.err_invalid(format!("ExternCall {}.{} not supported", iface, method))),
}
}

View File

@ -6,7 +6,9 @@
* Print/Debug (best-effort), Barrier/Safepoint (no-op).
*/
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
use crate::box_trait::NyashBox;
@ -46,6 +48,9 @@ pub struct MirInterpreter {
/// Call stack depth (exec_function_inner nesting). Used as a safety valve
/// to prevent Rust stack overflow on accidental infinite recursion in MIR.
pub(super) call_depth: usize,
/// Phase 288.1: REPL session reference (REPL mode only)
/// Enables variable persistence across REPL lines via __repl.get/set bridge
pub(super) repl_session: Option<Rc<RefCell<crate::runner::repl::ReplSessionBox>>>,
}
impl MirInterpreter {
@ -64,6 +69,7 @@ impl MirInterpreter {
branch_count: 0,
compare_count: 0,
call_depth: 0,
repl_session: None,
}
}
@ -95,6 +101,12 @@ impl MirInterpreter {
.detect_from_mir_functions(self.functions.keys());
}
/// Phase 288.1: Set REPL session for variable persistence
/// Enables __repl.get/set ExternCall handlers to access session state
pub fn set_repl_session(&mut self, session: Rc<RefCell<crate::runner::repl::ReplSessionBox>>) {
self.repl_session = Some(session);
}
/// Execute a BoxCall with VM's complete semantics (Phase 27-shortterm S-5.2-improved)
///
/// This wrapper allows external modules (e.g., JoinIR Runner) to invoke BoxCall

View File

@ -117,6 +117,11 @@ impl MirBuilder {
return Ok(result);
}
// Phase 288.1: REPL session variable bridge: __repl.get/set → ExternCall
if let Some(result) = self.try_build_repl_method_call(&object, &method, &arguments)? {
return Ok(result);
}
// 1. Static box method call: BoxName.method(args)
if let Some(result) = self.try_build_static_receiver_method_call(&object, &method, &arguments)? {
return Ok(result);
@ -181,6 +186,42 @@ impl MirBuilder {
self.try_build_mir_debug_call(method, arguments)
}
/// Phase 288.1: REPL session variable bridge
/// Transform __repl.get/set → ExternCall("__repl", "get/set", args)
fn try_build_repl_method_call(
&mut self,
object: &ASTNode,
method: &str,
arguments: &[ASTNode],
) -> Result<Option<ValueId>, String> {
let ASTNode::Variable { name: obj_name, .. } = object else {
return Ok(None);
};
if obj_name != "__repl" {
return Ok(None);
}
// Only handle get/set methods
if method != "get" && method != "set" {
return Err(format!("__repl.{} is not supported. Only __repl.get and __repl.set are allowed.", method));
}
// Build argument values
let arg_values = self.build_call_args(arguments)?;
// Emit ExternCall instruction
let dst = self.next_value_id();
self.emit_instruction(MirInstruction::ExternCall {
dst: Some(dst),
iface_name: "__repl".to_string(),
method_name: method.to_string(),
args: arg_values,
effects: EffectMask::PURE, // get/set are pure from MIR perspective
})?;
Ok(Some(dst))
}
fn try_build_static_receiver_method_call(
&mut self,
object: &ASTNode,

View File

@ -34,7 +34,7 @@ pub mod modes;
mod pipe_io;
mod pipeline;
mod plugins;
mod repl; // Phase 288: REPL module
pub(crate) mod repl; // Phase 288.1: Made pub(crate) for ExternCall bridge access // Phase 288: REPL module
mod selfhost;
mod stage1_bridge;
mod tasks;

View File

@ -0,0 +1,426 @@
//! REPL AST Rewriter - Phase 288.1
//!
//! Transforms AST to bridge session variables via __repl.get/set
//!
//! Box-First Design: REPL-only logic, completely isolated from file mode
//!
//! Key responsibilities:
//! - Collect declared names (local, function params, loop vars)
//! - Rewrite undeclared Variable → __repl.get("name")
//! - Rewrite undeclared Assignment → __repl.set("name", value)
//! - Skip nested scopes (function/method bodies)
//! - Exclude reserved words (me, true, false, null, _)
use crate::ast::{ASTNode, Span};
use std::collections::HashSet;
/// Reserved names that should NOT be rewritten
/// Phase 288.1: "_" is NOT reserved - it should be rewritten like any other session variable
const RESERVED_NAMES: &[&str] = &["me", "true", "false", "null"];
/// REPL AST Rewriter
pub struct ReplAstRewriter {
/// Names declared in the current input (local, params, loop vars)
declared_names: HashSet<String>,
/// Are we inside a nested scope (function/method body)?
in_nested_scope: bool,
}
impl ReplAstRewriter {
/// Create new rewriter
fn new() -> Self {
Self {
declared_names: HashSet::new(),
in_nested_scope: false,
}
}
/// Main entry point: rewrite AST for REPL session variable bridge
pub fn rewrite(ast: ASTNode) -> ASTNode {
let mut rewriter = Self::new();
rewriter.collect_declared_names(&ast);
rewriter.rewrite_node(ast)
}
/// Phase 1: Collect declared names in current input
fn collect_declared_names(&mut self, ast: &ASTNode) {
match ast {
ASTNode::Program { statements, .. } => {
for stmt in statements {
self.collect_from_node(stmt);
}
}
_ => self.collect_from_node(ast),
}
}
fn collect_from_node(&mut self, node: &ASTNode) {
match node {
// Local declarations (variables: Vec<String>)
ASTNode::Local { variables, .. } => {
for var in variables {
self.declared_names.insert(var.clone());
}
}
// Function declarations (params are declared)
ASTNode::FunctionDeclaration { params, .. } => {
for param in params {
self.declared_names.insert(param.clone());
}
}
// Box declarations (methods: HashMap<String, ASTNode>)
ASTNode::BoxDeclaration { methods, static_init, .. } => {
for (_method_name, method_node) in methods {
self.collect_from_node(method_node);
}
if let Some(init) = static_init {
for stmt in init {
self.collect_from_node(stmt);
}
}
}
// Recurse into compound nodes
ASTNode::If { then_body, else_body, .. } => {
for stmt in then_body {
self.collect_from_node(stmt);
}
if let Some(else_stmts) = else_body {
for stmt in else_stmts {
self.collect_from_node(stmt);
}
}
}
ASTNode::Loop { body, .. } => {
for stmt in body {
self.collect_from_node(stmt);
}
}
_ => {} // Other nodes don't declare names
}
}
/// Phase 2: Rewrite AST nodes
fn rewrite_node(&mut self, node: ASTNode) -> ASTNode {
match node {
// Program: recurse into statements
ASTNode::Program { statements, span } => {
let rewritten = statements.into_iter().map(|s| self.rewrite_node(s)).collect();
ASTNode::Program { statements: rewritten, span }
}
// Variable read: rewrite if undeclared and not in nested scope
ASTNode::Variable { name, span } => {
if self.should_rewrite_variable(&name) {
self.make_repl_get(name, span)
} else {
ASTNode::Variable { name, span }
}
}
// Assignment: rewrite target if undeclared Variable
ASTNode::Assignment { target, value, span } => {
let rewritten_value = Box::new(self.rewrite_node(*value));
// Check if target is a simple Variable
if let ASTNode::Variable { name, span: var_span } = *target {
if self.should_rewrite_variable(&name) {
// Rewrite to __repl.set("name", value)
return self.make_repl_set(name, *rewritten_value, span);
} else {
// Keep as Assignment
return ASTNode::Assignment {
target: Box::new(ASTNode::Variable { name, span: var_span }),
value: rewritten_value,
span,
};
}
} else {
// Complex assignment target (e.g., field access)
ASTNode::Assignment {
target: Box::new(self.rewrite_node(*target)),
value: rewritten_value,
span,
}
}
}
// Function/Method declarations: enter nested scope
// Phase 288.1: Special case - don't enter nested scope for "main" function (REPL wrapper)
ASTNode::FunctionDeclaration { name, params, body, is_static, is_override, span } => {
let prev_nested = self.in_nested_scope;
// Don't enter nested scope for "main" function (REPL wrapper)
let is_repl_main = name == "main";
if !is_repl_main {
self.in_nested_scope = true;
}
let rewritten_body = body.into_iter().map(|s| self.rewrite_node(s)).collect();
self.in_nested_scope = prev_nested;
ASTNode::FunctionDeclaration {
name,
params,
body: rewritten_body,
is_static,
is_override,
span,
}
}
// Box declarations: enter nested scope for methods (methods: HashMap)
// Phase 288.1: Special case - don't enter nested scope for static box "Main" (REPL wrapper)
ASTNode::BoxDeclaration { name, fields, public_fields, private_fields, methods, constructors, init_fields, weak_fields, is_interface, extends, implements, type_parameters, is_static, static_init, span } => {
let prev_nested = self.in_nested_scope;
// Don't enter nested scope for static box "Main" (REPL wrapper)
let is_repl_wrapper = is_static && name == "Main";
if !is_repl_wrapper {
self.in_nested_scope = true;
}
let rewritten_methods = methods.into_iter().map(|(k, v)| (k, self.rewrite_node(v))).collect();
let rewritten_constructors = constructors.into_iter().map(|(k, v)| (k, self.rewrite_node(v))).collect();
let rewritten_static_init = static_init.map(|stmts| {
stmts.into_iter().map(|s| self.rewrite_node(s)).collect()
});
self.in_nested_scope = prev_nested;
ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
methods: rewritten_methods,
constructors: rewritten_constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
static_init: rewritten_static_init,
span,
}
}
// If: recurse into branches
ASTNode::If { condition, then_body, else_body, span } => {
let rewritten_cond = Box::new(self.rewrite_node(*condition));
let rewritten_then = then_body.into_iter().map(|s| self.rewrite_node(s)).collect();
let rewritten_else = else_body.map(|stmts| {
stmts.into_iter().map(|s| self.rewrite_node(s)).collect()
});
ASTNode::If {
condition: rewritten_cond,
then_body: rewritten_then,
else_body: rewritten_else,
span,
}
}
// Loop: recurse into body
ASTNode::Loop { condition, body, span } => {
let rewritten_cond = Box::new(self.rewrite_node(*condition));
let rewritten_body = body.into_iter().map(|s| self.rewrite_node(s)).collect();
ASTNode::Loop {
condition: rewritten_cond,
body: rewritten_body,
span,
}
}
// Binary operation: recurse into operands
ASTNode::BinaryOp { operator, left, right, span } => {
ASTNode::BinaryOp {
operator,
left: Box::new(self.rewrite_node(*left)),
right: Box::new(self.rewrite_node(*right)),
span,
}
}
// Unary operation: recurse into operand
ASTNode::UnaryOp { operator, operand, span } => {
ASTNode::UnaryOp {
operator,
operand: Box::new(self.rewrite_node(*operand)),
span,
}
}
// Method call: recurse into object and arguments
ASTNode::MethodCall { object, method, arguments, span } => {
ASTNode::MethodCall {
object: Box::new(self.rewrite_node(*object)),
method,
arguments: arguments.into_iter().map(|a| self.rewrite_node(a)).collect(),
span,
}
}
// Function call: recurse into arguments
ASTNode::FunctionCall { name, arguments, span } => {
ASTNode::FunctionCall {
name,
arguments: arguments.into_iter().map(|a| self.rewrite_node(a)).collect(),
span,
}
}
// Field access: recurse into object
ASTNode::FieldAccess { object, field, span } => {
ASTNode::FieldAccess {
object: Box::new(self.rewrite_node(*object)),
field,
span,
}
}
// Return: recurse into value
ASTNode::Return { value, span } => {
ASTNode::Return {
value: value.map(|v| Box::new(self.rewrite_node(*v))),
span,
}
}
// Print: recurse into expression
ASTNode::Print { expression, span } => {
ASTNode::Print {
expression: Box::new(self.rewrite_node(*expression)),
span,
}
}
// New: recurse into arguments
ASTNode::New { class, arguments, type_arguments, span } => {
ASTNode::New {
class,
arguments: arguments.into_iter().map(|a| self.rewrite_node(a)).collect(),
type_arguments,
span,
}
}
// Match: recurse into scrutinee and arms
ASTNode::MatchExpr { scrutinee, arms, else_expr, span } => {
ASTNode::MatchExpr {
scrutinee: Box::new(self.rewrite_node(*scrutinee)),
arms: arms.into_iter().map(|(pattern, body)| {
(pattern, self.rewrite_node(body))
}).collect(),
else_expr: Box::new(self.rewrite_node(*else_expr)),
span,
}
}
// All other nodes pass through unchanged
other => other,
}
}
/// Should this variable be rewritten?
fn should_rewrite_variable(&self, name: &str) -> bool {
!self.declared_names.contains(name)
&& !self.in_nested_scope
&& !RESERVED_NAMES.contains(&name)
}
/// Create __repl.get("name") call
fn make_repl_get(&self, name: String, span: Span) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "__repl".to_string(),
span: span.clone(),
}),
method: "get".to_string(),
arguments: vec![ASTNode::Literal {
value: crate::ast::LiteralValue::String(name),
span: span.clone(),
}],
span,
}
}
/// Create __repl.set("name", value) call
fn make_repl_set(&self, name: String, value: ASTNode, span: Span) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(ASTNode::Variable {
name: "__repl".to_string(),
span: span.clone(),
}),
method: "set".to_string(),
arguments: vec![
ASTNode::Literal {
value: crate::ast::LiteralValue::String(name),
span: span.clone(),
},
value,
],
span,
}
}
/// Check if wrapper AST represents a pure expression (for auto-display)
pub fn is_pure_expression(ast: &ASTNode) -> bool {
match ast {
ASTNode::Program { statements, .. } => {
// Look for static box Main { ... }
for stmt in statements {
if let ASTNode::BoxDeclaration { name, methods, .. } = stmt {
if name == "Main" {
// Check if main() method has a single expression in its body
if let Some(main_fn) = methods.get("main") {
return Self::check_main_function_is_expression(main_fn);
}
}
}
}
false
}
_ => false,
}
}
fn check_main_function_is_expression(func: &ASTNode) -> bool {
// Check if main() function body has exactly 1 expression node
if let ASTNode::FunctionDeclaration { body, .. } = func {
return body.len() == 1 && Self::is_expression_node(&body[0]);
}
false
}
fn is_expression_node(node: &ASTNode) -> bool {
match node {
// Expressions: display target
ASTNode::Literal { .. } |
ASTNode::Variable { .. } |
ASTNode::BinaryOp { .. } |
ASTNode::UnaryOp { .. } |
ASTNode::FieldAccess { .. } |
ASTNode::MethodCall { .. } |
ASTNode::FunctionCall { .. } |
ASTNode::New { .. } |
ASTNode::MatchExpr { .. } => true,
// Statements: don't display
ASTNode::Assignment { .. } |
ASTNode::Local { .. } |
ASTNode::Return { .. } |
ASTNode::Print { .. } |
ASTNode::If { .. } |
ASTNode::Loop { .. } => false,
_ => false,
}
}
}

View File

@ -8,8 +8,10 @@
mod repl_runner;
mod repl_session;
mod ast_rewriter; // Phase 288.1: AST rewriting for session variable bridge
use repl_runner::ReplRunnerBox;
pub use repl_session::ReplSessionBox; // Phase 288.1: Export for ExternCall bridge
use crate::cli::CliConfig;
/// Phase 288: REPL モード起動公開API

View File

@ -9,14 +9,18 @@
//! - Session state management via ReplSessionBox
use super::repl_session::ReplSessionBox;
use crate::backend::VMValue; // Phase 288.1: For auto-display
use crate::cli::CliConfig;
use std::cell::RefCell;
use std::rc::Rc;
/// Phase 288: REPL実行器箱理論モジュール化
/// Phase 288.1: session を Rc<RefCell<>> で保持(永続化のため)
pub(super) struct ReplRunnerBox {
#[allow(dead_code)]
config: CliConfig,
session: RefCell<Option<ReplSessionBox>>,
/// Phase 288.1: Rc<RefCell<>> で保持clone は Rc のみ、中身は永続化)
session: Rc<RefCell<ReplSessionBox>>,
/// REPL mode での内部ログ抑制フラグ
/// verbose が false の場合に trueREPL 専用)
quiet_internal_logs: bool,
@ -29,7 +33,8 @@ impl ReplRunnerBox {
Self {
config,
session: RefCell::new(None),
// Phase 288.1: Rc<RefCell<>> で初期化(即座に生成)
session: Rc::new(RefCell::new(ReplSessionBox::new())),
quiet_internal_logs,
}
}
@ -65,14 +70,9 @@ impl ReplRunnerBox {
continue;
}
".reset" => {
// Phase 288 P3: Reset session
let mut session_ref = self.session.borrow_mut();
if let Some(ref mut session) = *session_ref {
session.reset();
println!("Session reset");
} else {
println!("Session reset (no active session)");
}
// Phase 288.1: Reset session
self.session.borrow_mut().reset();
println!("Session reset");
continue;
}
"" => continue,
@ -109,19 +109,20 @@ impl ReplRunnerBox {
// REPL mode では内部デバッグログを自動抑制
// quiet_internal_logs フラグで制御、環境変数操作不要)
// Initialize session on first use
{
let mut session_ref = self.session.borrow_mut();
if session_ref.is_none() {
*session_ref = Some(ReplSessionBox::new());
}
}
// Phase 288.1: No lazy initialization needed (session already created in new())
// Parse (minimal wrapper for REPL context - use Main for VM entry point)
let code = format!("static box Main {{ main() {{ {} }} }}", line);
let ast = NyashParser::parse_from_string(&code)
.map_err(|e| format!("Parse error: {}", e))?;
// Phase 288.1: Check if wrapper AST is a pure expression (for auto-display)
use super::ast_rewriter::ReplAstRewriter;
let is_expression = ReplAstRewriter::is_pure_expression(&ast);
// Phase 288.1: REPL AST rewrite (session variable bridge)
let rewritten_ast = ReplAstRewriter::rewrite(ast);
// Compile with REPL mode flag (暗黙 local 許可)
let mut compiler = MirCompiler::new();
compiler.set_repl_mode(true);
@ -129,29 +130,54 @@ impl ReplRunnerBox {
// MirCompiler に quiet フラグを渡す
compiler.set_quiet_internal_logs(self.quiet_internal_logs);
let mir_result = compiler.compile_with_source(ast, Some("<repl>"))
// Phase 288.1: Use rewritten AST for compilation
let mir_result = compiler.compile_with_source(rewritten_ast, Some("<repl>"))
.map_err(|e| format!("Compile error: {}", e))?;
// Execute
// Phase 288.1: Set REPL session in VM (Rc clone, not inner session clone)
let mut vm = MirInterpreter::new();
vm.set_repl_session(self.session.clone());
// Execute
let result_box = vm.execute_module(&mir_result.module)
.map_err(|e| format!("Runtime error: {}", e))?;
// Phase 288 P3: Convert to VMValue and store in session
use crate::backend::VMValue;
// Phase 288.1: Convert to VMValue and store in session
let vm_value = VMValue::from_nyash_box(result_box);
{
let mut session_ref = self.session.borrow_mut();
if let Some(ref mut session) = *session_ref {
session.set_last_value(vm_value);
session.eval_count += 1;
// Phase 288.1: Auto-display logic
let display_output = if is_expression {
match &vm_value {
VMValue::Void => String::new(), // Void は表示しない
value => {
// `_` 変数に保存
self.session.borrow_mut().set("_".to_string(), value.clone());
Self::format_vm_value(value)
}
}
} else {
String::new()
};
// Phase 288.1: Update session metadata
{
let mut session = self.session.borrow_mut();
session.set_last_value(vm_value);
session.eval_count += 1;
}
// Phase 288 P3: print() output already displayed via ExternCall
// Expression auto-display deferred to Phase 288.1
Ok(display_output)
}
Ok(String::new())
/// Phase 288.1: Format VMValue for auto-display
fn format_vm_value(value: &VMValue) -> String {
match value {
VMValue::Integer(i) => i.to_string(),
VMValue::Float(f) => f.to_string(),
VMValue::Bool(b) => b.to_string(),
VMValue::String(s) => s.clone(),
VMValue::Void => String::new(),
_ => format!("{:?}", value),
}
}
}

View File

@ -10,8 +10,9 @@ use crate::backend::VMValue;
use std::collections::BTreeMap;
/// REPL session context - isolated from file mode
/// Phase 288.1: Made public for ExternCall bridge access
#[derive(Debug, Default)]
pub(super) struct ReplSessionBox {
pub struct ReplSessionBox {
/// Session-level variables (runtime values, persists across evaluations)
pub variables: BTreeMap<String, VMValue>,
@ -28,14 +29,14 @@ impl ReplSessionBox {
}
/// REPL set: 変数に実行時の値を保存
#[allow(dead_code)]
pub(super) fn set(&mut self, name: String, value: VMValue) {
/// Phase 288.1: Made public for ExternCall bridge (__repl.set)
pub fn set(&mut self, name: String, value: VMValue) {
self.variables.insert(name, value);
}
/// REPL get: 変数の実行時の値を取得(未定義は None
#[allow(dead_code)]
pub(super) fn get(&self, name: &str) -> Option<&VMValue> {
/// Phase 288.1: Made public for ExternCall bridge (__repl.get)
pub fn get(&self, name: &str) -> Option<&VMValue> {
self.variables.get(name)
}