diff --git a/assembly/src/assembler/instruction/mod.rs b/assembly/src/assembler/instruction/mod.rs index 11e64c4f30..06875bc411 100644 --- a/assembly/src/assembler/instruction/mod.rs +++ b/assembly/src/assembler/instruction/mod.rs @@ -325,6 +325,8 @@ impl Assembler { Instruction::CallMastRoot(root) => self.call_mast_root(root, ctx), Instruction::CallImported(id) => self.call_imported(id, ctx), Instruction::SysCall(id) => self.syscall(id, ctx), + Instruction::DynExec => self.dynexec(), + Instruction::DynCall => self.dyncall(), // ----- debug decorators ------------------------------------------------------------- Instruction::Breakpoint => { diff --git a/assembly/src/assembler/instruction/procedures.rs b/assembly/src/assembler/instruction/procedures.rs index fcd3d8ce48..43bcee08b2 100644 --- a/assembly/src/assembler/instruction/procedures.rs +++ b/assembly/src/assembler/instruction/procedures.rs @@ -123,4 +123,14 @@ impl Assembler { // create a new SYSCALL block for the procedure call and return Ok(Some(CodeBlock::new_syscall(proc.mast_root()))) } + + pub(super) fn dynexec(&self) -> Result, AssemblyError> { + // create a new DYN block for the dynamic code execution and return + Ok(Some(CodeBlock::new_dyn())) + } + + pub(super) fn dyncall(&self) -> Result, AssemblyError> { + // create a new CALL block whose target is DYN + Ok(Some(CodeBlock::new_dyncall())) + } } diff --git a/assembly/src/ast/nodes/mod.rs b/assembly/src/ast/nodes/mod.rs index d99304c36b..127ae16e2b 100644 --- a/assembly/src/ast/nodes/mod.rs +++ b/assembly/src/ast/nodes/mod.rs @@ -307,6 +307,8 @@ pub enum Instruction { CallMastRoot(RpoDigest), CallImported(ProcedureId), SysCall(ProcedureId), + DynExec, + DynCall, // ----- debug decorators --------------------------------------------------------------------- Breakpoint, @@ -591,6 +593,8 @@ impl fmt::Display for Instruction { } Self::CallImported(proc_id) => write!(f, "call.{proc_id}"), Self::SysCall(proc_id) => write!(f, "syscall.{proc_id}"), + Self::DynExec => write!(f, "dynexec"), + Self::DynCall => write!(f, "dyncall"), // ----- debug decorators ------------------------------------------------------------- Self::Breakpoint => write!(f, "breakpoint"), diff --git a/assembly/src/ast/nodes/serde/deserialization.rs b/assembly/src/ast/nodes/serde/deserialization.rs index 4418e404eb..c6a4209d70 100644 --- a/assembly/src/ast/nodes/serde/deserialization.rs +++ b/assembly/src/ast/nodes/serde/deserialization.rs @@ -361,6 +361,8 @@ impl Deserializable for Instruction { OpCode::CallMastRoot => Ok(Instruction::CallMastRoot(RpoDigest::read_from(source)?)), OpCode::CallImported => Ok(Instruction::CallImported(ProcedureId::read_from(source)?)), OpCode::SysCall => Ok(Instruction::SysCall(ProcedureId::read_from(source)?)), + OpCode::DynExec => Ok(Instruction::DynExec), + OpCode::DynCall => Ok(Instruction::DynCall), // ----- debugging -------------------------------------------------------------------- OpCode::Debug => { diff --git a/assembly/src/ast/nodes/serde/mod.rs b/assembly/src/ast/nodes/serde/mod.rs index 0a18f65c20..f555643ffb 100644 --- a/assembly/src/ast/nodes/serde/mod.rs +++ b/assembly/src/ast/nodes/serde/mod.rs @@ -279,9 +279,11 @@ pub enum OpCode { CallMastRoot = 244, CallImported = 245, SysCall = 246, + DynExec = 247, + DynCall = 248, // ----- debugging ---------------------------------------------------------------------------- - Debug = 247, + Debug = 249, // ----- control flow ------------------------------------------------------------------------- IfElse = 253, diff --git a/assembly/src/ast/nodes/serde/serialization.rs b/assembly/src/ast/nodes/serde/serialization.rs index 0e9279113c..9e73b67fab 100644 --- a/assembly/src/ast/nodes/serde/serialization.rs +++ b/assembly/src/ast/nodes/serde/serialization.rs @@ -519,6 +519,8 @@ impl Serializable for Instruction { OpCode::SysCall.write_into(target); imported.write_into(target) } + Self::DynExec => OpCode::DynExec.write_into(target), + Self::DynCall => OpCode::DynCall.write_into(target), // ----- debug decorators ------------------------------------------------------------- Self::Breakpoint => { diff --git a/assembly/src/ast/parsers/context.rs b/assembly/src/ast/parsers/context.rs index 908266a1da..f8ac5b1280 100644 --- a/assembly/src/ast/parsers/context.rs +++ b/assembly/src/ast/parsers/context.rs @@ -615,6 +615,8 @@ impl ParserContext<'_> { "exec" => self.parse_exec(op), "call" => self.parse_call(op), "syscall" => self.parse_syscall(op), + "dynexec" => simple_instruction(op, DynExec), + "dyncall" => simple_instruction(op, DynCall), // ----- constant statements ---------------------------------------------------------- "const" => Err(ParsingError::const_invalid_scope(op)), diff --git a/assembly/src/tests.rs b/assembly/src/tests.rs index 498715018e..562edcb835 100644 --- a/assembly/src/tests.rs +++ b/assembly/src/tests.rs @@ -806,6 +806,28 @@ fn program_with_exported_procedure() { assert!(assembler.compile(source).is_err()); } +// PROGRAMS WITH DYNAMIC CODE BLOCKS +// ================================================================================================ + +#[test] +fn program_with_dynamic_code_execution() { + let assembler = super::Assembler::default(); + let source = "begin dynexec end"; + let program = assembler.compile(source).unwrap(); + let expected = "begin dyn end"; + assert_eq!(expected, format!("{program}")); +} + +#[test] +fn program_with_dynamic_code_execution_in_new_context() { + let assembler = super::Assembler::default(); + let source = "begin dyncall end"; + let program = assembler.compile(source).unwrap(); + let expected = + "begin call.0xc75c340ec6a69e708457544d38783abbb604d881b7dc62d00bfc2b10f52808e6 end"; + assert_eq!(expected, format!("{program}")); +} + // MAST ROOT CALLS // ================================================================================================ diff --git a/core/src/program/blocks/dyn_block.rs b/core/src/program/blocks/dyn_block.rs index 2e35f1fe86..9f93b4069b 100644 --- a/core/src/program/blocks/dyn_block.rs +++ b/core/src/program/blocks/dyn_block.rs @@ -45,6 +45,11 @@ impl Dyn { /// Returns a hash of this code block. pub fn hash(&self) -> Digest { + Self::dyn_hash() + } + + /// Returns a hash of this code block. + pub fn dyn_hash() -> Digest { DYN_CONSTANT } } @@ -57,7 +62,7 @@ impl Default for Dyn { impl fmt::Display for Dyn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Dyn")?; + write!(f, "dyn")?; Ok(()) } diff --git a/core/src/program/blocks/mod.rs b/core/src/program/blocks/mod.rs index 604daac9a4..cce75235c0 100644 --- a/core/src/program/blocks/mod.rs +++ b/core/src/program/blocks/mod.rs @@ -79,6 +79,11 @@ impl CodeBlock { Self::Dyn(Dyn::new()) } + /// TODO: add comments + pub fn new_dyncall() -> Self { + Self::Call(Call::new(Dyn::dyn_hash())) + } + /// TODO: add comments pub fn new_proxy(code_hash: Digest) -> Self { Self::Proxy(Proxy::new(code_hash)) diff --git a/docs/src/user_docs/assembly/code_organization.md b/docs/src/user_docs/assembly/code_organization.md index 0fa407da0e..7e2eb2c1d5 100644 --- a/docs/src/user_docs/assembly/code_organization.md +++ b/docs/src/user_docs/assembly/code_organization.md @@ -42,6 +42,28 @@ begin end ``` +#### Dynamic procedure invocation +It is also possible to invoke procedures dynamically - i.e., without specifying target procedure labels at compile time. There are two instructions, `dynexec` and `dyncall`, which can be used to execute dynamically-specified code targets. Both instructions expect [MAST root](../../design/programs.md) of the target to be provided via the stack. The difference between `dynexec` and `dyncall` is that `dyncall` will [change context](./execution_contexts.md) before executing the dynamic code target, while `dynexec` will cause the code target to be executed in the current context. + +Dynamic code execution in the same context is achieved by setting the top $4$ elements of the stack to the hash of the dynamic code block and then executing the following instruction: + +``` +dynexec +``` + +This causes the VM to do the following: + +1. Read the top 4 elements of the stack to get the hash of the dynamic target (leaving the stack unchanged). +2. Execute the code block which hashes to the specified target. The VM must know the specified code block and hash (they must be in the CodeBlockTable of the executing Program). + +Dynamic code execution in a new context can be achieved similarly by setting the top $4$ elements of the stack to the hash of the dynamic code block and then executing the following instruction: + +``` +dyncall +``` + +> **Note**: In both cases, the stack is left unchanged. Therefore, if the dynamic code is intended to manipulate the stack, it should start by either dropping or moving the code block hash from the top of the stack. + ### Modules A *module* consists of one or more procedures. There are two types of modules: *library modules* and *executable modules* (also called *programs*). diff --git a/docs/src/user_docs/assembly/flow_control.md b/docs/src/user_docs/assembly/flow_control.md index 8d68bf2ae6..1800ea8700 100644 --- a/docs/src/user_docs/assembly/flow_control.md +++ b/docs/src/user_docs/assembly/flow_control.md @@ -65,4 +65,4 @@ while.true # push the boolean false to the stack, finishing the loop for the next iteration push.0 end -``` +``` \ No newline at end of file diff --git a/miden/tests/integration/flow_control/mod.rs b/miden/tests/integration/flow_control/mod.rs index 5d882ed42f..aec1e0c046 100644 --- a/miden/tests/integration/flow_control/mod.rs +++ b/miden/tests/integration/flow_control/mod.rs @@ -209,3 +209,146 @@ fn simple_syscall() { test.prove_and_verify(vec![1, 2], false); } + +// DYNAMIC CODE EXECUTION +// ================================================================================================ + +#[test] +fn simple_dyn_exec() { + let program_source = " + proc.foo + # drop the top 4 values, since that will be the code hash when we call this dynamically + dropw + add + end + + begin + # call foo directly so it will get added to the CodeBlockTable + padw + call.foo + + # move the first result of foo out of the way + movdn.4 + + # use dynexec to call foo again via its hash, which is on the stack + dynexec + end"; + + // The hash of foo can be obtained from the code block table by: + // let program = test.compile(); + // let cb_table = program.cb_table(); + // Result: + // [BaseElement(14592192105906586403), BaseElement(9256464248508904838), + // BaseElement(17436090329036592832), BaseElement(10814467189528518943)] + // Integer values can be obtained via Felt::from_mont(14592192105906586403).as_int(), etc. + // As ints: + // [16045159387802755434, 10308872899350860082, 17306481765929021384, 16642043361554117790] + + let test = Test { + source: program_source.to_string(), + kernel: None, + stack_inputs: StackInputs::try_from_values([ + 3, + // put the hash of foo on the stack + 16045159387802755434, + 10308872899350860082, + 17306481765929021384, + 16642043361554117790, + 1, + 2, + ]) + .unwrap(), + advice_inputs: AdviceInputs::default(), + in_debug_mode: false, + libraries: Vec::default(), + }; + + test.expect_stack(&[6]); + + test.prove_and_verify( + vec![ + 3, + 16045159387802755434, + 10308872899350860082, + 17306481765929021384, + 16642043361554117790, + 1, + 2, + ], + false, + ); +} + +#[test] +fn simple_dyncall() { + let program_source = " + proc.foo + # drop the top 4 values, since that will be the code hash when we call this dynamically + dropw + + # test that the execution context has changed + mem_load.0 assertz + + # add the two values on top of the stack + add + end + + begin + # write to memory so we can test that `call` and `dyncall` change the execution context + push.5 mem_store.0 + + # call foo directly so it will get added to the CodeBlockTable + padw + call.foo + + # move the first result of foo out of the way + movdn.4 + + # use dyncall to call foo again via its hash, which is on the stack + dyncall + end"; + + // The hash of foo can be obtained from the code block table by: + // let program = test.compile(); + // let cb_table = program.cb_table(); + // Result: + // [BaseElement(3961142802598954486), BaseElement(5305628994393606376), + // BaseElement(7971171833137344204), BaseElement(10465350313512331391)] + // Integer values can be obtained via Felt::from_mont(14592192105906586403).as_int(), etc. + // As ints: + // [8324248212344458853, 17691992706129158519, 18131640149172243086, 16129275750103409835] + + let test = Test { + source: program_source.to_string(), + kernel: None, + stack_inputs: StackInputs::try_from_values([ + 3, + // put the hash of foo on the stack + 8324248212344458853, + 17691992706129158519, + 18131640149172243086, + 16129275750103409835, + 1, + 2, + ]) + .unwrap(), + advice_inputs: AdviceInputs::default(), + in_debug_mode: false, + libraries: Vec::default(), + }; + + test.expect_stack(&[6]); + + test.prove_and_verify( + vec![ + 3, + 8324248212344458853, + 17691992706129158519, + 18131640149172243086, + 16129275750103409835, + 1, + 2, + ], + false, + ); +} diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 2698d4f261..045594820d 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -328,11 +328,16 @@ where self.start_call_block(block)?; - // get function body from the code block table and execute it - let fn_body = cb_table - .get(block.fn_hash()) - .ok_or_else(|| ExecutionError::CodeBlockNotFound(block.fn_hash()))?; - self.execute_code_block(fn_body, cb_table)?; + // if this is a dyncall, execute the dynamic code block + if block.fn_hash() == Dyn::dyn_hash() { + self.execute_dyn_block(&Dyn::new(), cb_table)?; + } else { + // get function body from the code block table and execute it + let fn_body = cb_table + .get(block.fn_hash()) + .ok_or_else(|| ExecutionError::CodeBlockNotFound(block.fn_hash()))?; + self.execute_code_block(fn_body, cb_table)?; + } self.end_call_block(block) }