Implement Phase 9.7: ExternCall instruction and WASM runtime imports
Co-authored-by: moe-charm <217100418+moe-charm@users.noreply.github.com>
This commit is contained in:
@ -553,6 +553,37 @@ impl VM {
|
|||||||
Err(VMError::TypeError(format!("Expected Future, got {:?}", future_val)))
|
Err(VMError::TypeError(format!("Expected Future, got {:?}", future_val)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Phase 9.7: External Function Calls
|
||||||
|
MirInstruction::ExternCall { dst, iface_name, method_name, args, effects: _ } => {
|
||||||
|
// For VM backend, we implement a stub that logs the call
|
||||||
|
// Real implementation would route to native host functions
|
||||||
|
let arg_values: Result<Vec<_>, _> = args.iter().map(|id| self.get_value(*id)).collect();
|
||||||
|
let arg_values = arg_values?;
|
||||||
|
|
||||||
|
println!("ExternCall: {}.{}({:?})", iface_name, method_name, arg_values);
|
||||||
|
|
||||||
|
// For console.log, print the message
|
||||||
|
if iface_name == "env.console" && method_name == "log" {
|
||||||
|
for arg in &arg_values {
|
||||||
|
if let VMValue::String(s) = arg {
|
||||||
|
println!("Console: {}", s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For canvas operations, just log them for now
|
||||||
|
if iface_name == "env.canvas" {
|
||||||
|
println!("Canvas operation: {}", method_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store void result if destination is provided
|
||||||
|
if let Some(dst) = dst {
|
||||||
|
self.values.insert(*dst, VMValue::Void);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ControlFlow::Continue)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -371,6 +371,38 @@ impl WasmCodegen {
|
|||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Phase 9.7: External Function Calls
|
||||||
|
MirInstruction::ExternCall { dst, iface_name, method_name, args, effects: _ } => {
|
||||||
|
// Generate call to external function import
|
||||||
|
let call_target = match (iface_name.as_str(), method_name.as_str()) {
|
||||||
|
("env.console", "log") => "console_log",
|
||||||
|
("env.canvas", "fillRect") => "canvas_fillRect",
|
||||||
|
("env.canvas", "fillText") => "canvas_fillText",
|
||||||
|
_ => return Err(WasmError::UnsupportedInstruction(
|
||||||
|
format!("Unsupported extern call: {}.{}", iface_name, method_name)
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut instructions = Vec::new();
|
||||||
|
|
||||||
|
// Load all arguments onto stack in order
|
||||||
|
for arg in args {
|
||||||
|
instructions.push(format!("local.get ${}", self.get_local_index(*arg)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the external function
|
||||||
|
instructions.push(format!("call ${}", call_target));
|
||||||
|
|
||||||
|
// Store result if destination is provided
|
||||||
|
if let Some(dst) = dst {
|
||||||
|
// For void functions, we still need to provide a dummy value
|
||||||
|
instructions.push("i32.const 0".to_string()); // Void result
|
||||||
|
instructions.push(format!("local.set ${}", self.get_local_index(*dst)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(instructions)
|
||||||
|
},
|
||||||
|
|
||||||
// Unsupported instructions
|
// Unsupported instructions
|
||||||
_ => Err(WasmError::UnsupportedInstruction(
|
_ => Err(WasmError::UnsupportedInstruction(
|
||||||
format!("Instruction not yet supported: {:?}", instruction)
|
format!("Instruction not yet supported: {:?}", instruction)
|
||||||
|
|||||||
@ -51,6 +51,44 @@ impl RuntimeImports {
|
|||||||
result: None,
|
result: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Phase 9.7: Box FFI/ABI imports per BID specifications
|
||||||
|
|
||||||
|
// env.console_log for console.log(message) - (string_ptr, string_len)
|
||||||
|
self.imports.push(ImportFunction {
|
||||||
|
module: "env".to_string(),
|
||||||
|
name: "console_log".to_string(),
|
||||||
|
params: vec!["i32".to_string(), "i32".to_string()],
|
||||||
|
result: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// env.canvas_fillRect for canvas.fillRect(canvas_id, x, y, w, h, color)
|
||||||
|
// Parameters: (canvas_id_ptr, canvas_id_len, x, y, w, h, color_ptr, color_len)
|
||||||
|
self.imports.push(ImportFunction {
|
||||||
|
module: "env".to_string(),
|
||||||
|
name: "canvas_fillRect".to_string(),
|
||||||
|
params: vec![
|
||||||
|
"i32".to_string(), "i32".to_string(), // canvas_id (ptr, len)
|
||||||
|
"i32".to_string(), "i32".to_string(), "i32".to_string(), "i32".to_string(), // x, y, w, h
|
||||||
|
"i32".to_string(), "i32".to_string(), // color (ptr, len)
|
||||||
|
],
|
||||||
|
result: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// env.canvas_fillText for canvas.fillText(canvas_id, text, x, y, font, color)
|
||||||
|
// Parameters: (canvas_id_ptr, canvas_id_len, text_ptr, text_len, x, y, font_ptr, font_len, color_ptr, color_len)
|
||||||
|
self.imports.push(ImportFunction {
|
||||||
|
module: "env".to_string(),
|
||||||
|
name: "canvas_fillText".to_string(),
|
||||||
|
params: vec![
|
||||||
|
"i32".to_string(), "i32".to_string(), // canvas_id (ptr, len)
|
||||||
|
"i32".to_string(), "i32".to_string(), // text (ptr, len)
|
||||||
|
"i32".to_string(), "i32".to_string(), // x, y
|
||||||
|
"i32".to_string(), "i32".to_string(), // font (ptr, len)
|
||||||
|
"i32".to_string(), "i32".to_string(), // color (ptr, len)
|
||||||
|
],
|
||||||
|
result: None,
|
||||||
|
});
|
||||||
|
|
||||||
// Future: env.file_read, env.file_write for file I/O
|
// Future: env.file_read, env.file_write for file I/O
|
||||||
// Future: env.http_request for network access
|
// Future: env.http_request for network access
|
||||||
}
|
}
|
||||||
@ -120,6 +158,49 @@ impl RuntimeImports {
|
|||||||
"print" => {
|
"print" => {
|
||||||
js.push_str(" print: (value) => console.log(value),\n");
|
js.push_str(" print: (value) => console.log(value),\n");
|
||||||
},
|
},
|
||||||
|
"print_str" => {
|
||||||
|
js.push_str(" print_str: (ptr, len) => {\n");
|
||||||
|
js.push_str(" const memory = instance.exports.memory;\n");
|
||||||
|
js.push_str(" const str = new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, len));\n");
|
||||||
|
js.push_str(" console.log(str);\n");
|
||||||
|
js.push_str(" },\n");
|
||||||
|
},
|
||||||
|
"console_log" => {
|
||||||
|
js.push_str(" console_log: (ptr, len) => {\n");
|
||||||
|
js.push_str(" const memory = instance.exports.memory;\n");
|
||||||
|
js.push_str(" const str = new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, len));\n");
|
||||||
|
js.push_str(" console.log(str);\n");
|
||||||
|
js.push_str(" },\n");
|
||||||
|
},
|
||||||
|
"canvas_fillRect" => {
|
||||||
|
js.push_str(" canvas_fillRect: (canvasIdPtr, canvasIdLen, x, y, w, h, colorPtr, colorLen) => {\n");
|
||||||
|
js.push_str(" const memory = instance.exports.memory;\n");
|
||||||
|
js.push_str(" const canvasId = new TextDecoder().decode(new Uint8Array(memory.buffer, canvasIdPtr, canvasIdLen));\n");
|
||||||
|
js.push_str(" const color = new TextDecoder().decode(new Uint8Array(memory.buffer, colorPtr, colorLen));\n");
|
||||||
|
js.push_str(" const canvas = document.getElementById(canvasId);\n");
|
||||||
|
js.push_str(" if (canvas) {\n");
|
||||||
|
js.push_str(" const ctx = canvas.getContext('2d');\n");
|
||||||
|
js.push_str(" ctx.fillStyle = color;\n");
|
||||||
|
js.push_str(" ctx.fillRect(x, y, w, h);\n");
|
||||||
|
js.push_str(" }\n");
|
||||||
|
js.push_str(" },\n");
|
||||||
|
},
|
||||||
|
"canvas_fillText" => {
|
||||||
|
js.push_str(" canvas_fillText: (canvasIdPtr, canvasIdLen, textPtr, textLen, x, y, fontPtr, fontLen, colorPtr, colorLen) => {\n");
|
||||||
|
js.push_str(" const memory = instance.exports.memory;\n");
|
||||||
|
js.push_str(" const canvasId = new TextDecoder().decode(new Uint8Array(memory.buffer, canvasIdPtr, canvasIdLen));\n");
|
||||||
|
js.push_str(" const text = new TextDecoder().decode(new Uint8Array(memory.buffer, textPtr, textLen));\n");
|
||||||
|
js.push_str(" const font = new TextDecoder().decode(new Uint8Array(memory.buffer, fontPtr, fontLen));\n");
|
||||||
|
js.push_str(" const color = new TextDecoder().decode(new Uint8Array(memory.buffer, colorPtr, colorLen));\n");
|
||||||
|
js.push_str(" const canvas = document.getElementById(canvasId);\n");
|
||||||
|
js.push_str(" if (canvas) {\n");
|
||||||
|
js.push_str(" const ctx = canvas.getContext('2d');\n");
|
||||||
|
js.push_str(" ctx.font = font;\n");
|
||||||
|
js.push_str(" ctx.fillStyle = color;\n");
|
||||||
|
js.push_str(" ctx.fillText(text, x, y);\n");
|
||||||
|
js.push_str(" }\n");
|
||||||
|
js.push_str(" },\n");
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
js.push_str(&format!(" {}: () => {{ throw new Error('Not implemented: {}'); }},\n",
|
js.push_str(&format!(" {}: () => {{ throw new Error('Not implemented: {}'); }},\n",
|
||||||
function.name, function.name));
|
function.name, function.name));
|
||||||
|
|||||||
@ -855,7 +855,7 @@ impl MirBuilder {
|
|||||||
/// Build method call: object.method(arguments)
|
/// Build method call: object.method(arguments)
|
||||||
fn build_method_call(&mut self, object: ASTNode, method: String, arguments: Vec<ASTNode>) -> Result<ValueId, String> {
|
fn build_method_call(&mut self, object: ASTNode, method: String, arguments: Vec<ASTNode>) -> Result<ValueId, String> {
|
||||||
// Build the object expression
|
// Build the object expression
|
||||||
let object_value = self.build_expression(object)?;
|
let object_value = self.build_expression(object.clone())?;
|
||||||
|
|
||||||
// Build argument expressions
|
// Build argument expressions
|
||||||
let mut arg_values = Vec::new();
|
let mut arg_values = Vec::new();
|
||||||
@ -866,7 +866,70 @@ impl MirBuilder {
|
|||||||
// Create result value
|
// Create result value
|
||||||
let result_id = self.value_gen.next();
|
let result_id = self.value_gen.next();
|
||||||
|
|
||||||
// Emit a BoxCall instruction
|
// Check if this is an external call (console.log, canvas.fillRect, etc.)
|
||||||
|
if let ASTNode::Variable { name: object_name, .. } = object {
|
||||||
|
match (object_name.as_str(), method.as_str()) {
|
||||||
|
("console", "log") => {
|
||||||
|
// Generate ExternCall for console.log
|
||||||
|
self.emit_instruction(MirInstruction::ExternCall {
|
||||||
|
dst: None, // console.log is void
|
||||||
|
iface_name: "env.console".to_string(),
|
||||||
|
method_name: "log".to_string(),
|
||||||
|
args: arg_values,
|
||||||
|
effects: EffectMask::IO, // Console output is I/O
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Return void value
|
||||||
|
let void_id = self.value_gen.next();
|
||||||
|
self.emit_instruction(MirInstruction::Const {
|
||||||
|
dst: void_id,
|
||||||
|
value: ConstValue::Void,
|
||||||
|
})?;
|
||||||
|
return Ok(void_id);
|
||||||
|
},
|
||||||
|
("canvas", "fillRect") => {
|
||||||
|
// Generate ExternCall for canvas.fillRect
|
||||||
|
self.emit_instruction(MirInstruction::ExternCall {
|
||||||
|
dst: None, // canvas.fillRect is void
|
||||||
|
iface_name: "env.canvas".to_string(),
|
||||||
|
method_name: "fillRect".to_string(),
|
||||||
|
args: arg_values,
|
||||||
|
effects: EffectMask::IO, // Canvas operations are I/O
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Return void value
|
||||||
|
let void_id = self.value_gen.next();
|
||||||
|
self.emit_instruction(MirInstruction::Const {
|
||||||
|
dst: void_id,
|
||||||
|
value: ConstValue::Void,
|
||||||
|
})?;
|
||||||
|
return Ok(void_id);
|
||||||
|
},
|
||||||
|
("canvas", "fillText") => {
|
||||||
|
// Generate ExternCall for canvas.fillText
|
||||||
|
self.emit_instruction(MirInstruction::ExternCall {
|
||||||
|
dst: None, // canvas.fillText is void
|
||||||
|
iface_name: "env.canvas".to_string(),
|
||||||
|
method_name: "fillText".to_string(),
|
||||||
|
args: arg_values,
|
||||||
|
effects: EffectMask::IO, // Canvas operations are I/O
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Return void value
|
||||||
|
let void_id = self.value_gen.next();
|
||||||
|
self.emit_instruction(MirInstruction::Const {
|
||||||
|
dst: void_id,
|
||||||
|
value: ConstValue::Void,
|
||||||
|
})?;
|
||||||
|
return Ok(void_id);
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// Regular method call - continue with BoxCall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit a BoxCall instruction for regular method calls
|
||||||
self.emit_instruction(MirInstruction::BoxCall {
|
self.emit_instruction(MirInstruction::BoxCall {
|
||||||
dst: Some(result_id),
|
dst: Some(result_id),
|
||||||
box_val: object_value,
|
box_val: object_value,
|
||||||
|
|||||||
@ -273,6 +273,18 @@ pub enum MirInstruction {
|
|||||||
dst: ValueId,
|
dst: ValueId,
|
||||||
future: ValueId,
|
future: ValueId,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// === Phase 9.7: External Function Calls (Box FFI/ABI) ===
|
||||||
|
|
||||||
|
/// External function call through Box FFI/ABI
|
||||||
|
/// `%dst = extern_call interface.method(%args...)`
|
||||||
|
ExternCall {
|
||||||
|
dst: Option<ValueId>,
|
||||||
|
iface_name: String, // e.g., "env.console"
|
||||||
|
method_name: String, // e.g., "log"
|
||||||
|
args: Vec<ValueId>,
|
||||||
|
effects: EffectMask,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constant values in MIR
|
/// Constant values in MIR
|
||||||
@ -389,6 +401,9 @@ impl MirInstruction {
|
|||||||
MirInstruction::FutureNew { .. } => EffectMask::PURE.add(Effect::Alloc), // Creating future may allocate
|
MirInstruction::FutureNew { .. } => EffectMask::PURE.add(Effect::Alloc), // Creating future may allocate
|
||||||
MirInstruction::FutureSet { .. } => EffectMask::WRITE, // Setting future has write effects
|
MirInstruction::FutureSet { .. } => EffectMask::WRITE, // Setting future has write effects
|
||||||
MirInstruction::Await { .. } => EffectMask::READ.add(Effect::Async), // Await blocks and reads
|
MirInstruction::Await { .. } => EffectMask::READ.add(Effect::Async), // Await blocks and reads
|
||||||
|
|
||||||
|
// Phase 9.7: External Function Calls
|
||||||
|
MirInstruction::ExternCall { effects, .. } => *effects, // Use provided effect mask
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +429,8 @@ impl MirInstruction {
|
|||||||
MirInstruction::Await { dst, .. } => Some(*dst),
|
MirInstruction::Await { dst, .. } => Some(*dst),
|
||||||
|
|
||||||
MirInstruction::Call { dst, .. } |
|
MirInstruction::Call { dst, .. } |
|
||||||
MirInstruction::BoxCall { dst, .. } => *dst,
|
MirInstruction::BoxCall { dst, .. } |
|
||||||
|
MirInstruction::ExternCall { dst, .. } => *dst,
|
||||||
|
|
||||||
MirInstruction::Store { .. } |
|
MirInstruction::Store { .. } |
|
||||||
MirInstruction::Branch { .. } |
|
MirInstruction::Branch { .. } |
|
||||||
@ -500,6 +516,9 @@ impl MirInstruction {
|
|||||||
MirInstruction::FutureNew { value, .. } => vec![*value],
|
MirInstruction::FutureNew { value, .. } => vec![*value],
|
||||||
MirInstruction::FutureSet { future, value } => vec![*future, *value],
|
MirInstruction::FutureSet { future, value } => vec![*future, *value],
|
||||||
MirInstruction::Await { future, .. } => vec![*future],
|
MirInstruction::Await { future, .. } => vec![*future],
|
||||||
|
|
||||||
|
// Phase 9.7: External Function Calls
|
||||||
|
MirInstruction::ExternCall { args, .. } => args.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -572,6 +591,17 @@ impl fmt::Display for MirInstruction {
|
|||||||
write!(f, "ret void")
|
write!(f, "ret void")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
MirInstruction::ExternCall { dst, iface_name, method_name, args, effects } => {
|
||||||
|
if let Some(dst) = dst {
|
||||||
|
write!(f, "{} = extern_call {}.{}({}); effects: {}", dst, iface_name, method_name,
|
||||||
|
args.iter().map(|v| format!("{}", v)).collect::<Vec<_>>().join(", "),
|
||||||
|
effects)
|
||||||
|
} else {
|
||||||
|
write!(f, "extern_call {}.{}({}); effects: {}", iface_name, method_name,
|
||||||
|
args.iter().map(|v| format!("{}", v)).collect::<Vec<_>>().join(", "),
|
||||||
|
effects)
|
||||||
|
}
|
||||||
|
},
|
||||||
_ => write!(f, "{:?}", self), // Fallback for other instructions
|
_ => write!(f, "{:?}", self), // Fallback for other instructions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -730,4 +760,34 @@ mod tests {
|
|||||||
assert!(write_barrier.effects().contains(super::super::effect::Effect::Barrier));
|
assert!(write_barrier.effects().contains(super::super::effect::Effect::Barrier));
|
||||||
assert!(write_barrier.effects().contains(super::super::effect::Effect::WriteHeap));
|
assert!(write_barrier.effects().contains(super::super::effect::Effect::WriteHeap));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extern_call_instruction() {
|
||||||
|
let dst = ValueId::new(0);
|
||||||
|
let arg1 = ValueId::new(1);
|
||||||
|
let arg2 = ValueId::new(2);
|
||||||
|
let inst = MirInstruction::ExternCall {
|
||||||
|
dst: Some(dst),
|
||||||
|
iface_name: "env.console".to_string(),
|
||||||
|
method_name: "log".to_string(),
|
||||||
|
args: vec![arg1, arg2],
|
||||||
|
effects: super::super::effect::EffectMask::IO,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(inst.dst_value(), Some(dst));
|
||||||
|
assert_eq!(inst.used_values(), vec![arg1, arg2]);
|
||||||
|
assert_eq!(inst.effects(), super::super::effect::EffectMask::IO);
|
||||||
|
|
||||||
|
// Test void extern call
|
||||||
|
let void_inst = MirInstruction::ExternCall {
|
||||||
|
dst: None,
|
||||||
|
iface_name: "env.canvas".to_string(),
|
||||||
|
method_name: "fillRect".to_string(),
|
||||||
|
args: vec![arg1],
|
||||||
|
effects: super::super::effect::EffectMask::IO,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(void_inst.dst_value(), None);
|
||||||
|
assert_eq!(void_inst.used_values(), vec![arg1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -360,6 +360,16 @@ impl MirPrinter {
|
|||||||
MirInstruction::Await { dst, future } => {
|
MirInstruction::Await { dst, future } => {
|
||||||
format!("{} = await {}", dst, future)
|
format!("{} = await {}", dst, future)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Phase 9.7: External Function Calls
|
||||||
|
MirInstruction::ExternCall { dst, iface_name, method_name, args, effects } => {
|
||||||
|
let args_str = args.iter().map(|v| format!("{}", v)).collect::<Vec<_>>().join(", ");
|
||||||
|
if let Some(dst) = dst {
|
||||||
|
format!("{} = extern_call {}.{}({}) [effects: {}]", dst, iface_name, method_name, args_str, effects)
|
||||||
|
} else {
|
||||||
|
format!("extern_call {}.{}({}) [effects: {}]", iface_name, method_name, args_str, effects)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
test_direct_extern.nyash
Normal file
12
test_direct_extern.nyash
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Test direct external calls pattern
|
||||||
|
static box Main {
|
||||||
|
init { result }
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Direct external call (this should trigger ExternCall)
|
||||||
|
console.log("Direct console call test")
|
||||||
|
|
||||||
|
me.result = "Direct call demo completed"
|
||||||
|
return me.result
|
||||||
|
}
|
||||||
|
}
|
||||||
17
test_extern_call_demo.nyash
Normal file
17
test_extern_call_demo.nyash
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Phase 9.7 ExternCall Demo - Modified for explicit console creation
|
||||||
|
# Test console.log and canvas operations
|
||||||
|
|
||||||
|
static box Main {
|
||||||
|
init { result, console }
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Create console instance for testing
|
||||||
|
me.console = new ConsoleBox()
|
||||||
|
|
||||||
|
# Test console.log external call
|
||||||
|
me.console.log("Hello from Nyash via ExternCall!")
|
||||||
|
|
||||||
|
me.result = "ExternCall demo completed"
|
||||||
|
return me.result
|
||||||
|
}
|
||||||
|
}
|
||||||
9
test_simple.nyash
Normal file
9
test_simple.nyash
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Simple test without external calls
|
||||||
|
static box Main {
|
||||||
|
init { result }
|
||||||
|
|
||||||
|
main() {
|
||||||
|
me.result = "Simple test"
|
||||||
|
return me.result
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user