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:
copilot-swe-agent[bot]
2025-08-14 08:56:39 +00:00
parent 9b25330d94
commit 2091462441
9 changed files with 318 additions and 3 deletions

View File

@ -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)
},
} }
} }

View File

@ -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)

View File

@ -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));

View File

@ -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,

View File

@ -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]);
}
} }

View File

@ -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
View 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
}
}

View 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
View File

@ -0,0 +1,9 @@
# Simple test without external calls
static box Main {
init { result }
main() {
me.result = "Simple test"
return me.result
}
}