use alloc::{
collections::BTreeSet,
string::{String, ToString},
vec::Vec,
};
use core::str::FromStr;
use std::{
eprintln,
sync::{Arc, LazyLock},
};
use miden_assembly_syntax::{
MAX_REPEAT_COUNT, ast::Path, diagnostics::WrapErr, library::LibraryExport,
};
use miden_core::{
Felt, Word, assert_matches,
events::EventId,
field::PrimeField64,
mast::{
CallNodeBuilder, JoinNodeBuilder, LoopNodeBuilder, MastForestContributor, MastNodeExt,
MastNodeId, SplitNodeBuilder,
},
operations::Operation,
program::Program,
serde::{Deserializable, Serializable},
};
use miden_mast_package::{
ConstantExport, MastForest, Package, PackageExport, PackageId, PackageManifest,
ProcedureExport, TargetType, TypeExport,
};
use proptest::{
prelude::*,
test_runner::{Config, TestRunner},
};
use crate::{
Assembler, Library, ModuleParser, PathBuf, ProjectSourceInputs, ProjectTargetSelector,
assembler::MAX_CONTROL_FLOW_NESTING,
ast::{Module, ModuleKind, ProcedureName, QualifiedProcedureName},
diagnostics::{IntoDiagnostic, Report},
fmp::fmp_initialization_sequence,
mast_forest_builder::MastForestBuilder,
report,
testing::{
TestContext, assert_diagnostic, assert_diagnostic_lines, parse_module, regex, source_file,
},
};
type TestResult = Result<(), Report>;
macro_rules! assert_assembler_diagnostic {
($context:ident, $source:expr, $($expected:literal),+) => {{
let error = $context
.assemble($source)
.expect_err("expected diagnostic to be raised, but compilation succeeded");
assert_diagnostic_lines!(error, $($expected),*);
}};
($context:ident, $source:expr, $($expected:expr),+) => {{
let error = $context
.assemble($source)
.expect_err("expected diagnostic to be raised, but compilation succeeded");
assert_diagnostic_lines!(error, $($expected),*);
}};
}
#[test]
fn simple_instructions() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.0 assertz end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let source = source_file!(&context, "begin push.10 push.50 push.2 u32wrapping_madd end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let source = source_file!(&context, "begin push.10 push.50 push.2 u32wrapping_add3 end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
#[ignore]
fn empty_program() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn empty_if() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin if.true end end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:15\]"#),
"1 | begin if.true end end",
" : ^|^",
" : `-- found a end here",
" `----",
" help: expected primitive opcode (e.g. \"add\"), or \"else\", or control flow opcode (e.g. \"if.true\")"
);
Ok(())
}
#[test]
fn empty_if_true_then_branch() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin if.true nop end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
#[ignore]
fn empty_while() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin while.true end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
#[ignore]
fn empty_repeat() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin repeat.5 end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn repeat_basic_blocks_merged() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin mul repeat.5 add end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
assert_eq!(program.mast_forest().num_nodes(), 1);
Ok(())
}
#[test]
fn repeat_dynamic_iteration_count() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "const A = 5 begin repeat.A add end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn single_basic_block() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 push.2 add end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn basic_block_and_simple_if_true() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.2 push.3 if.true add else mul end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let source = source_file!(&context, "begin push.2 push.3 if.true add end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn basic_block_and_simple_if_false() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.2 push.3 if.false add else mul end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let source = source_file!(&context, "begin push.2 push.3 if.false add end end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn library_exports() -> Result<(), Report> {
let context = TestContext::new();
let baz = r#"
pub proc baz1
push.7 push.8 sub
end
"#;
let baz = parse_module!(&context, "lib1::baz", baz);
let lib1 = Assembler::new(context.source_manager()).assemble_library([baz])?;
let foo = r#"
proc foo1
push.1 add
end
pub proc foo2
push.2 add
exec.foo1
end
pub proc foo3
push.3 mul
exec.foo1
exec.foo2
end
"#;
let foo = parse_module!(&context, "lib2::foo", foo);
let bar = r#"
use lib1::baz
use lib2::foo
pub use baz::baz1->bar1
pub use foo::foo2->bar2
pub proc bar3
exec.foo::foo2
end
proc bar4
push.1 push.2 mul
end
pub proc bar5
push.3 sub
exec.foo::foo2
exec.bar1
exec.bar2
exec.bar4
end
"#;
let bar = parse_module!(&context, "lib2::bar", bar);
let lib2_modules = [foo, bar];
let lib2 = Assembler::new(context.source_manager())
.with_dynamic_library(lib1)?
.assemble_library(lib2_modules.iter().cloned())?;
let foo2 = Path::new("::lib2::foo::foo2");
let foo3 = Path::new("::lib2::foo::foo3");
let bar1 = Path::new("::lib2::bar::bar1");
let bar2 = Path::new("::lib2::bar::bar2");
let bar3 = Path::new("::lib2::bar::bar3");
let bar5 = Path::new("::lib2::bar::bar5");
let expected_exports: BTreeSet<Arc<Path>> =
[foo2.into(), foo3.into(), bar1.into(), bar2.into(), bar3.into(), bar5.into()].into();
let actual_exports: BTreeSet<_> = lib2.exports().map(|export| export.path()).collect();
assert_eq!(expected_exports, actual_exports);
assert_eq!(lib2.get_export_node_id(foo2), lib2.get_export_node_id(bar2));
assert_eq!(lib2.get_export_node_id(foo2), lib2.get_export_node_id(bar3));
assert_eq!(lib2.mast_forest().num_procedures(), 6);
assert!(!lib2.is_reexport(foo2));
assert!(!lib2.is_reexport(foo3));
assert!(lib2.is_reexport(bar1));
assert!(!lib2.is_reexport(bar2));
assert!(!lib2.is_reexport(bar3));
assert!(!lib2.is_reexport(bar5));
Ok(())
}
#[test]
fn library_procedure_collision() -> Result<(), Report> {
let context = TestContext::new();
let foo = r#"
pub proc foo1
push.1
if.true
push.1 push.2 add
else
push.1 push.2 mul
end
end
"#;
let foo = parse_module!(&context, "lib1::foo", foo);
let lib1 = Assembler::new(context.source_manager()).assemble_library([foo])?;
let bar = r#"
use lib1::foo
pub use foo::foo1->bar1
pub proc bar2
push.1
if.true
push.1 push.2 add
else
push.1 push.2 mul
end
end
"#;
let bar = parse_module!(&context, "lib2::bar", bar);
let lib2 = Assembler::new(context.source_manager())
.with_dynamic_library(lib1)?
.assemble_library([bar])?;
assert_eq!(lib2.num_exports(), 2);
let lib2_bar_bar1 = QualifiedProcedureName::from_str("lib2::bar::bar1").unwrap();
let lib2_bar_bar2 = QualifiedProcedureName::from_str("lib2::bar::bar2").unwrap();
assert_ne!(lib2.get_export_node_id(&lib2_bar_bar1), lib2.get_export_node_id(&lib2_bar_bar2));
assert_eq!(lib2.mast_forest().num_nodes(), 6);
Ok(())
}
#[test]
fn library_serialization() -> Result<(), Report> {
let context = TestContext::new();
let foo = r#"
pub proc foo
add
end
pub proc foo_mul
mul
end
"#;
let foo = parse_module!(&context, "test::foo", foo);
let bar = r#"
pub proc bar
mtree_get
end
pub proc bar_mul
mul
end
"#;
let bar = parse_module!(&context, "test::bar", bar);
let modules = [foo, bar];
let bundle =
Assembler::new(context.source_manager()).assemble_library(modules.iter().cloned())?;
let bytes = bundle.to_bytes();
let deserialized = Library::read_from_bytes(&bytes).unwrap();
assert_eq!(bundle.as_ref(), &deserialized);
Ok(())
}
#[test]
fn get_module_by_path() -> Result<(), Report> {
let context = TestContext::new();
let foo_source = r#"
pub proc foo
add
end
"#;
let foo = parse_module!(&context, "test::foo", foo_source);
let modules = [foo];
let bundle = Assembler::new(context.source_manager())
.assemble_library(modules.iter().cloned())
.unwrap();
let foo_module_info = bundle.module_infos().next().unwrap();
assert_eq!(foo_module_info.path(), &PathBuf::new("::test::foo").unwrap());
let (_, foo_proc) = foo_module_info.procedures().next().unwrap();
assert_eq!(foo_proc.name, ProcedureName::new("foo").unwrap());
Ok(())
}
#[test]
fn get_proc_digest_by_name() -> Result<(), Report> {
let context = TestContext::new();
let testing_module_source = "
pub proc foo
push.1.2 add drop
end
pub proc bar
push.5.6 sub drop
end
";
let testing_module = parse_module!(&context, "test::names", testing_module_source);
let library = Assembler::new(context.source_manager())
.assemble_library([testing_module])
.context("failed to assemble library from testing module")?;
let library_procedure_digests = library
.exports()
.filter_map(|export| match export {
LibraryExport::Procedure(export) => Some(library.mast_forest()[export.node].digest()),
_ => None,
})
.collect::<Vec<Word>>();
assert!(
library_procedure_digests.contains(
&library
.get_procedure_root_by_path("test::names::foo")
.expect("procedure with name 'foo' must exist in the test library")
)
);
assert!(
library_procedure_digests.contains(
&library
.get_procedure_root_by_path("test::names::bar")
.expect("procedure with name 'bar' must exist in the test library")
)
);
assert_eq!(None, library.get_procedure_root_by_path("test::names::baz"));
assert_eq!(None, library.get_procedure_root_by_path("invalid::namespace::foo"));
Ok(())
}
#[test]
fn simple_main_call() -> TestResult {
let mut context = TestContext::default();
let account_path = PathBuf::new("context::account").unwrap();
let account_code = context.parse_module_with_path(
account_path,
source_file!(
&context,
"\
pub proc account_method_1
push.2.1 add
end
pub proc account_method_2
push.3.1 sub
end
"
),
)?;
context.add_module(account_code)?;
context.assemble(source_file!(
&context,
"
use context::account
begin
call.account::account_method_1
end
"
))?;
context.assemble(source_file!(
&context,
"
use context::account
begin
call.account::account_method_2
end
"
))?;
Ok(())
}
#[test]
fn call_without_path() -> TestResult {
let mut context = TestContext::default();
let project =
miden_project::Package::new("call_without_path", miden_project::Target::executable("main"));
let account_code1_src = source_file!(
&context,
"\
pub proc account_method_1
push.2.1 add
end
pub proc account_method_2
push.3.1 sub
end
"
);
let account_code2_src = source_file!(
&context,
"\
pub proc account_method_1
push.2.2 add
end
pub proc account_method_2
push.4.1 sub
end
"
);
let main_src = source_file!(
&context,
"
begin
# call the account_method_1 from the first module (account_code1)
call.0x81e0b1afdbd431e4c9d4b86599b82c3852ecf507ae318b71c099cdeba0169068
# call the account_method_2 from the first module (account_code1)
call.0x1bc375fc794af6637af3f428286bf6ac1a24617640ed29f8bc533f48316c6d75
# call the account_method_1 from the second module (account_code2)
call.0xcfadd74886ea075d15826a4f59fb4db3a10cde6e6e953603cba96b4dcbb94321
# call the account_method_2 from the second module (account_code2)
call.0x1976bf72d457bd567036d3648b7e3f3c22eca4096936931e59796ec05c0ecb10
end
"
);
let account_code1 = Module::parse(
"account_code1",
ModuleKind::Library,
account_code1_src,
context.source_manager(),
)?;
let account_code2 = Module::parse(
"account_code2",
ModuleKind::Library,
account_code2_src,
context.source_manager(),
)?;
let main = Module::parse(
Path::exec_path(),
ModuleKind::Executable,
main_src,
context.source_manager(),
)?;
let mut project_assembler = context.project_assembler(project.into())?;
project_assembler.assemble_with_sources(
ProjectTargetSelector::Executable("main"),
"dev",
ProjectSourceInputs {
root: main,
support: vec![account_code1, account_code2],
},
)?;
Ok(())
}
#[test]
fn procref_call() -> TestResult {
let mut context = TestContext::default();
context.add_module_from_source(
"module::path::one",
source_file!(
&context,
"
pub proc aaa
push.7.8
end
pub proc foo
push.1.2
end"
),
)?;
context.add_module_from_source(
"module::path::two",
source_file!(
&context,
"
use module::path::one
pub use one::foo
pub proc bar
procref.one::aaa
end"
),
)?;
context.assemble(source_file!(
&context,
"
use module::path::two
@locals(4)
proc baz
push.3.4
end
begin
procref.two::bar
procref.two::foo
procref.baz
end"
))?;
Ok(())
}
#[test]
fn get_proc_name_of_unknown_module() -> TestResult {
let context = TestContext::default();
let module_source1 = source_file!(
&context,
"
use module::path::two
pub proc foo
procref.two::bar
end"
);
let module_path_one = "module::path::one";
let module1 = context.parse_module_with_path(module_path_one, module_source1)?;
let report = Assembler::new(context.source_manager())
.assemble_library(core::iter::once(module1))
.expect_err("expected unknown module error");
assert_diagnostic_lines!(
report,
"undefined symbol reference",
regex!(r#",-\[test[\d]+:2:9\]"#),
"1 |",
"2 | use module::path::two",
" : ^^^^^^^^|^^^^^^^^",
" : `-- this symbol path could not be resolved",
"3 |",
" `----",
"help: maybe you are missing an import?"
);
Ok(())
}
#[test]
fn simple_constant() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
const TEST_CONSTANT = 7
begin
push.TEST_CONSTANT
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn enum_explicit_discriminants() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
r#"
enum Status : u16 {
OK = 200,
NOT_FOUND = 404,
SERVER_ERROR = 500,
}
begin
push.OK
push.NOT_FOUND
push.SERVER_ERROR
end
"#
);
let _program = context.assemble(source)?;
Ok(())
}
#[test]
fn enum_discriminants_can_reference_constants() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
r#"
const BASE = 10
enum Status : u16 {
OK = BASE,
NOT_FOUND = OK + 1,
}
begin
push.OK
push.NOT_FOUND
end
"#
);
let _program = context.assemble(source)?;
Ok(())
}
#[test]
fn enum_felt_repr_variants() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
r#"
enum Status : felt {
OK = 1,
}
begin
push.OK
end
"#
);
let _program = context.assemble(source)?;
Ok(())
}
#[test]
fn enum_felt_discriminant_negative_is_rejected() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
r#"
enum Status : felt {
BAD = 0 - 1,
}
begin
push.BAD
end
"#
);
let err = context
.assemble(source)
.expect_err("expected negative discriminant to be rejected");
assert_diagnostic!(err, "invalid constant expression: value is larger than expected range");
Ok(())
}
#[test]
fn enum_felt_discriminant_too_large_is_rejected() -> TestResult {
let context = TestContext::default();
let modulus = miden_core::Felt::ORDER_U64;
let source = source_file!(
&context,
format!(
r#"
enum Status : felt {{
BAD = {modulus},
}}
begin
push.BAD
end
"#
)
);
let err = context
.assemble(source)
.expect_err("expected out-of-range felt discriminant to be rejected");
assert_diagnostic!(err, "invalid literal: value overflowed the field modulus");
Ok(())
}
#[test]
fn constant_expression_overflow_is_rejected() -> TestResult {
let context = TestContext::default();
let modulus_minus_one = miden_core::Felt::ORDER_U64 - 1;
let source = source_file!(
&context,
format!(
"const TOO_BIG = {modulus_minus_one} + {modulus_minus_one}\nbegin\n push.TOO_BIG\nend\n"
)
);
let err = context
.assemble(source)
.expect_err("expected constant expression overflow to be rejected");
assert_diagnostic!(err, "invalid constant expression: value is larger than expected range");
Ok(())
}
#[test]
fn multiple_constants_push() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const CONSTANT_1 = 21 \
const CONSTANT_2 = 44 \
begin \
push.CONSTANT_1.64.CONSTANT_2.72 \
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn constant_numeric_expression() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const TEST_CONSTANT = 11-2+4*(12-(10+1))+9+8//4*2 \
begin \
push.TEST_CONSTANT \
end \
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn constant_alphanumeric_expression() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const TEST_CONSTANT_1 = (18-1+10)*6-((13+7)*2) \
const TEST_CONSTANT_2 = 11-2+4*(12-(10+1))+9
const TEST_CONSTANT_3 = (TEST_CONSTANT_1-(TEST_CONSTANT_2+10))//5+3
begin \
push.TEST_CONSTANT_3 \
end \
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn constant_hexadecimal_value() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const TEST_CONSTANT = 0xFF \
begin \
push.TEST_CONSTANT \
end \
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn constant_field_division() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const TEST_CONSTANT = (17//4)/4*(1//2)+2 \
begin \
push.TEST_CONSTANT \
end \
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn constant_err_const_not_initialized() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const TEST_CONSTANT = 5+A \
begin \
push.TEST_CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"undefined constant 'A'",
regex!(r#",-\[test[\d]+:1:25\]"#),
"1 | const TEST_CONSTANT = 5+A begin push.TEST_CONSTANT end",
" : |",
" : `-- the constant referenced here is not defined in the current scope",
" `----",
" help: are you missing an import?"
);
Ok(())
}
#[test]
fn constant_err_div_by_zero() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const TEST_CONSTANT = 5/0 \
begin \
push.TEST_CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid constant expression: division by zero",
regex!(r#",-\[test[\d]+:1:23\]"#),
"1 | const TEST_CONSTANT = 5/0 begin push.TEST_CONSTANT end",
" : ^^^",
" `----"
);
let source = source_file!(
&context,
"const TEST_CONSTANT = 5//0 \
begin \
push.TEST_CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid constant expression: division by zero",
regex!(r#",-\[test[\d]+:1:23\]"#),
"1 | const TEST_CONSTANT = 5//0 begin push.TEST_CONSTANT end",
" : ^^^^",
" `----"
);
Ok(())
}
#[test]
fn constant_err_div_by_zero_indirect() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"pub const NUMERATOR = 10
pub const DENOMINATOR = 0
pub const BAD_DIV = NUMERATOR / DENOMINATOR
begin
push.BAD_DIV
end"
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"invalid constant expression: division by zero",
regex!(r#",-\[test[\d]+:3:25\]"#),
"2 | pub const DENOMINATOR = 0",
"3 | pub const BAD_DIV = NUMERATOR / DENOMINATOR",
" : ^^^^^^^^^^^^^^^^^^^^^^^",
"4 |",
" `----"
);
Ok(())
}
#[test]
fn constant_err_div_by_zero_link_time() -> TestResult {
let mut context = TestContext::default();
let module_a = source_file!(
&context,
"pub const NUMERATOR = 10
pub const DENOMINATOR = 0"
);
context.add_module_from_source("module_a", module_a)?;
let source = source_file!(
&context,
"use module_a::NUMERATOR
use module_a::DENOMINATOR
const BAD_DIV = NUMERATOR / DENOMINATOR
begin
push.BAD_DIV
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid constant expression: division by zero",
regex!(r#",-\[test[\d]+:4:21\]"#),
"3 |",
"4 | const BAD_DIV = NUMERATOR / DENOMINATOR",
" : ^^^^^^^^^^^^^^^^^^^^^^^",
"5 |",
" `----"
);
Ok(())
}
#[test]
fn constants_must_be_uppercase() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const constant_1 = 12 \
begin \
push.constant_1 \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:7\]"#),
"1 | const constant_1 = 12 begin push.constant_1 end",
" : ^^^^^|^^^^",
" : `-- found a identifier here",
" `----",
" help: expected constant identifier"
);
Ok(())
}
#[test]
fn duplicate_constant_name() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const CONSTANT = 12 \
const CONSTANT = 14 \
begin \
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"symbol conflict: found duplicate definitions of the same name",
regex!(r#",-\[test[\d]+:1:1\]"#),
"1 | const CONSTANT = 12 const CONSTANT = 14 begin push.CONSTANT end",
" : ^^^^^^^^^|^^^^^^^^^ ^^^^^^^^^|^^^^^^^^^",
" : | `-- conflict occurs here",
" : `-- previously defined here",
" `----"
);
Ok(())
}
#[test]
fn constant_must_be_valid_felt() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const CONSTANT = 1122INVALID \
begin \
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:22\]"#),
"1 | const CONSTANT = 1122INVALID begin push.CONSTANT end",
" : ^^^|^^^",
" : `-- found a constant identifier here",
" `----",
" help: expected \"*\", or \"+\", or \"-\", or \"/\", or \"//\", or \"@\", or \"adv_map\", or \"begin\", or \"const\", or \"enum\", \
or \"proc\", or \"pub\", or \"type\", or \"use\", or end of file, or doc comment"
);
Ok(())
}
#[test]
fn constant_must_be_within_valid_felt_range() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"const CONSTANT = 18446744073709551615 \
begin \
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid literal: value overflowed the field modulus",
regex!(r#",-\[test[\d]+:1:18\]"#),
"1 | const CONSTANT = 18446744073709551615 begin push.CONSTANT end",
" : ^^^^^^^^^^^^^^^^^^^^",
" `----"
);
let source = source_file!(
&context,
"const CONSTANT = 18446744069414584321 \
begin \
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid literal: value overflowed the field modulus",
regex!(r#",-\[test[\d]+:1:18\]"#),
"1 | const CONSTANT = 18446744069414584321 begin push.CONSTANT end",
" : ^^^^^^^^^^^^^^^^^^^^",
" `----"
);
let source = source_file!(
&context,
"const CONSTANT = 0xFFFFFFFF00000001 \
begin \
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid literal: value overflowed the field modulus",
regex!(r#",-\[test[\d]+:1:18\]"#),
"1 | const CONSTANT = 0xFFFFFFFF00000001 begin push.CONSTANT end",
" : ^^^^^^^^^^^^^^^^^^",
" `----"
);
Ok(())
}
#[test]
fn constants_defined_in_global_scope() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"
begin \
const CONSTANT = 12
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:2:11\]"#),
"1 |",
"2 | begin const CONSTANT = 12",
" : ^^|^^",
" : `-- found a const here",
"3 | push.CONSTANT end",
" `----",
r#" help: expected primitive opcode (e.g. "add"), or control flow opcode (e.g. "if.true")"#
);
Ok(())
}
#[test]
fn constant_not_found() -> TestResult {
let context = TestContext::new();
let source = source_file!(
&context,
"
begin \
push.CONSTANT \
end"
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"undefined constant 'CONSTANT'",
regex!(r#",-\[test[\d]+:2:16\]"#),
"1 |",
"2 | begin push.CONSTANT end",
" : ^^^^|^^^",
" : `-- the constant referenced here is not defined in the current scope",
" `----",
"help: are you missing an import?"
);
Ok(())
}
#[test]
fn mem_operations_with_constants() -> TestResult {
let context = TestContext::default();
const PROC_LOC_STORE_PTR: u64 = 0;
const PROC_LOC_LOAD_PTR: u64 = 1;
const PROC_LOC_STOREW_PTR: u64 = 4;
const PROC_LOC_LOADW_PTR: u64 = 8;
const GLOBAL_STORE_PTR: u64 = 12;
const GLOBAL_LOAD_PTR: u64 = 13;
const GLOBAL_STOREW_PTR: u64 = 16;
const GLOBAL_LOADW_PTR: u64 = 20;
let source = source_file!(
&context,
format!(
"\
const PROC_LOC_STORE_PTR = {PROC_LOC_STORE_PTR}
const PROC_LOC_LOAD_PTR = {PROC_LOC_LOAD_PTR}
const PROC_LOC_STOREW_PTR = {PROC_LOC_STOREW_PTR}
const PROC_LOC_LOADW_PTR = {PROC_LOC_LOADW_PTR}
const GLOBAL_STORE_PTR = {GLOBAL_STORE_PTR}
const GLOBAL_LOAD_PTR = {GLOBAL_LOAD_PTR}
const GLOBAL_STOREW_PTR = {GLOBAL_STOREW_PTR}
const GLOBAL_LOADW_PTR = {GLOBAL_LOADW_PTR}
@locals(12)
proc test_const_loc
# constant should resolve using locaddr operation
locaddr.PROC_LOC_STORE_PTR
# constant should resolve using loc_store operation
loc_store.PROC_LOC_STORE_PTR
# constant should resolve using loc_load operation
loc_load.PROC_LOC_LOAD_PTR
# constant should resolve using loc_storew_be operation
loc_storew_be.PROC_LOC_STOREW_PTR
# constant should resolve using loc_loadw_be opeartion
loc_loadw_be.PROC_LOC_LOADW_PTR
end
begin
# inline procedure
exec.test_const_loc
# constant should resolve using mem_store operation
mem_store.GLOBAL_STORE_PTR
# constant should resolve using mem_load operation
mem_load.GLOBAL_LOAD_PTR
# constant should resolve using mem_storew_be operation
mem_storew_be.GLOBAL_STOREW_PTR
# constant should resolve using mem_loadw_be operation
mem_loadw_be.GLOBAL_LOADW_PTR
end
"
)
);
let program = context.assemble(source)?;
let expected = source_file!(
&context,
format!(
"\
@locals(12)
proc test_const_loc
# constant should resolve using locaddr operation
locaddr.{PROC_LOC_STORE_PTR}
# constant should resolve using loc_store operation
loc_store.{PROC_LOC_STORE_PTR}
# constant should resolve using loc_load operation
loc_load.{PROC_LOC_LOAD_PTR}
# constant should resolve using loc_storew_be operation
loc_storew_be.{PROC_LOC_STOREW_PTR}
# constant should resolve using loc_loadw_be opeartion
loc_loadw_be.{PROC_LOC_LOADW_PTR}
end
begin
# inline procedure
exec.test_const_loc
# constant should resolve using mem_store operation
mem_store.{GLOBAL_STORE_PTR}
# constant should resolve using mem_load operation
mem_load.{GLOBAL_LOAD_PTR}
# constant should resolve using mem_storew_be operation
mem_storew_be.{GLOBAL_STOREW_PTR}
# constant should resolve using mem_loadw_be operation
mem_loadw_be.{GLOBAL_LOADW_PTR}
end
"
)
);
let expected_program = context.assemble(expected)?;
assert_eq!(expected_program.to_string(), program.to_string());
Ok(())
}
#[test]
fn const_conversion_failed_to_u16() -> TestResult {
let constant_value: u64 = u16::MAX as u64 + 1;
let context = TestContext::default();
let source = source_file!(
&context,
format!(
"\
const CONSTANT = {constant_value}
@locals(1)
proc test_constant_overflow
loc_load.CONSTANT
end
begin
exec.test_constant_overflow
end
"
)
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"invalid immediate: value is larger than expected range",
regex!(r#",-\[test[\d]+:5:18\]"#),
"4 | proc test_constant_overflow",
"5 | loc_load.CONSTANT",
" : ^^^^^^^^",
"6 | end",
" `----"
);
Ok(())
}
#[test]
fn const_conversion_failed_to_u32() -> TestResult {
let context = TestContext::default();
let constant_value: u64 = u32::MAX as u64 + 1;
let source = source_file!(
&context,
format!(
"\
const CONSTANT = {constant_value}
begin
mem_load.CONSTANT
end
"
)
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"invalid immediate: value is larger than expected range",
regex!(r#",-\[test[\d]+:4:18\]"#),
"3 | begin",
"4 | mem_load.CONSTANT",
" : ^^^^^^^^",
"5 | end",
" `----"
);
Ok(())
}
#[test]
fn deprecated_mem_loadw_instruction() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
mem_loadw
end
"
);
assert_assembler_diagnostic!(
context,
source,
"deprecated instruction: `mem_loadw` has been removed",
regex!(r#",-\[test[\d]+:2:9\]"#),
"1 | begin",
"2 | mem_loadw",
regex!(r#"^ *: *\^+"#),
regex!(r#"this instruction is no longer supported"#),
"3 | end",
" `----",
regex!(r#"help:.*use.*mem_loadw_be.*instead"#)
);
Ok(())
}
#[test]
fn deprecated_loc_loadw_instruction() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
@locals(8)
proc foo
loc_loadw.0
end
begin
exec.foo
end
"
);
assert_assembler_diagnostic!(
context,
source,
"deprecated instruction: `loc_loadw` has been removed",
regex!(r#",-\[test[\d]+:3:9\]"#),
"2 | proc foo",
"3 | loc_loadw.0",
regex!(r#"^ *: *\^+"#),
regex!(r#"this instruction is no longer supported"#),
"4 | end",
" `----",
regex!(r#"help:.*use.*loc_loadw_be.*instead"#)
);
Ok(())
}
#[test]
fn deprecated_loc_storew_instruction() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
@locals(8)
proc foo
loc_storew.0
end
begin
exec.foo
end
"
);
assert_assembler_diagnostic!(
context,
source,
"deprecated instruction: `loc_storew` has been removed",
regex!(r#",-\[test[\d]+:3:9\]"#),
"2 | proc foo",
"3 | loc_storew.0",
regex!(r#"^ *: *\^+"#),
regex!(r#"this instruction is no longer supported"#),
"4 | end",
" `----",
regex!(r#"help:.*use.*loc_storew_be.*instead"#)
);
Ok(())
}
#[test]
fn const_word_from_string() -> TestResult {
let context = TestContext::default();
let sample_source_string = "lorem ipsum";
let source = source_file!(
&context,
format!(
r#"
const SAMPLE_WORD = word("{sample_source_string}")
begin
push.SAMPLE_WORD
end
"#
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn const_event_from_string() -> TestResult {
let context = TestContext::default();
let sample_event_name = "miden::test::constant";
let expected_felt = EventId::from_name(sample_event_name);
let source1 = source_file!(
&context,
format!(
r#"
begin
emit.event("{sample_event_name}")
end
"#
)
);
let source2 = source_file!(
&context,
format!(
r#"
begin
push.{expected_felt}
emit
drop
end
"#
)
);
let program1 = context.assemble(source1)?;
let program2 = context.assemble(source2)?;
assert_eq!(program1.hash(), program2.hash());
Ok(())
}
#[test]
fn test_push_word_slice() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
const SAMPLE_WORD = [2, 3, 4, 5]
const SAMPLE_HEX_WORD = 0x0600000000000000070000000000000008000000000000000900000000000000
begin
push.SAMPLE_WORD[1..3]
push.SAMPLE_WORD[0]
push.[10, 11, 12, 13][1..3]
push.SAMPLE_HEX_WORD[2..4]
push.0x0600000000000000070000000000000008000000000000000900000000000000[0..2]
end
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn test_push_word_slice_invalid() -> TestResult {
let context = TestContext::default();
let source_invalid_range = source_file!(
&context,
format!(
"\
const SAMPLE_WORD = [2, 3, 4, 5]
begin
push.SAMPLE_WORD[6..3]
end
"
)
);
assert!(context.assemble(source_invalid_range).is_err());
let source_empty_range = source_file!(
&context,
format!(
"\
const SAMPLE_WORD = [2, 3, 4, 5]
begin
push.SAMPLE_WORD[2..2]
end
"
)
);
assert!(context.assemble(source_empty_range).is_err());
let source_invalid_constant_type = source_file!(
&context,
format!(
"\
const SAMPLE_VALUE = 6
begin
push.SAMPLE_VALUE[1..3]
end
"
)
);
assert!(context.assemble(source_invalid_constant_type).is_err());
let source_invalid_constant_type = source_file!(
&context,
format!(
"\
begin
push.5[0..2]
end
"
)
);
assert!(context.assemble(source_invalid_constant_type).is_err());
Ok(())
}
#[test]
fn link_time_const_evaluation_succeeds() -> TestResult {
let context = TestContext::default();
let a = r#"
pub const FOO = 1
pub proc f
push.FOO
end
"#;
let a = parse_module!(&context, "lib::a", a);
let lib = Assembler::new(context.source_manager()).assemble_library([a])?;
let program_source = source_file!(
&context,
"\
use lib::a
use a::FOO
begin
push.FOO
exec.a::f
add
add
end"
);
let program = Assembler::new(context.source_manager())
.with_dynamic_library(lib)?
.assemble_program(program_source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn link_time_const_evaluation_undefined_symbol() -> TestResult {
let context = TestContext::default();
let a = r#"
pub proc f
push.1
end
"#;
let a = parse_module!(&context, "lib::a", a);
let lib = Assembler::new(context.source_manager()).assemble_library([a])?;
let source = source_file!(
&context,
"\
use lib::a::FOO
begin
push.FOO
exec.lib::a::f
add
end"
);
let error = Assembler::new(context.source_manager())
.with_dynamic_library(lib)?
.assemble_program(source)
.expect_err("expected diagnostic to be raised, but compilation succeeded");
assert_diagnostic_lines!(
error,
"undefined symbol reference",
regex!(r#",-\[test[\d]+:1:5\]"#),
"1 | use lib::a::FOO",
" : ^^^^^|^^^^^",
" : `-- this symbol path could not be resolved",
"2 | begin",
" `----",
"help: maybe you are missing an import?"
);
Ok(())
}
#[test]
fn link_time_const_evaluation_invalid_constant() -> TestResult {
let context = TestContext::default();
let a = r#"
pub proc f
push.1
end
"#;
let a = parse_module!(&context, "lib::a", a);
let lib = Assembler::new(context.source_manager()).assemble_library([a])?;
let source = source_file!(
&context,
"\
use lib::a::f
begin
push.f
end"
);
let error = Assembler::new(context.source_manager())
.with_dynamic_library(lib)?
.assemble_program(source)
.expect_err("expected diagnostic to be raised, but compilation succeeded");
assert_diagnostic_lines!(
error,
"invalid syntax",
regex!(r#",-\[test[\d]+:3:14\]"#),
"2 | begin",
"3 | push.f",
" : |",
" : `-- found a identifier here",
"4 | end",
" `----",
"help: expected \"[\", or constant identifier, or hex-encoded literal, or hex_word, or integer literal"
);
Ok(())
}
#[test]
fn decorators_basic_block() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0
add
trace.1
mul
trace.2
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn decorators_repeat_one_basic_block() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0
repeat.2 add end
trace.1
repeat.2 mul end
trace.2
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn decorators_repeat_split() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0
repeat.2
if.true
trace.1 push.42 trace.2
else
trace.3 push.22 trace.3
end
trace.4
end
trace.5
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn decorators_call() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0 trace.1
call.0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
trace.2
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn decorators_dyn() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0
dynexec
trace.1
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0 trace.1 trace.2 trace.3 trace.4
dynexec
trace.5 trace.6 trace.7 trace.8 trace.9
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn decorators_external() -> TestResult {
let context = TestContext::default();
let baz = r#"
pub proc f
push.7 push.8 sub
end
"#;
let baz = parse_module!(&context, "lib::baz", baz);
let lib = Assembler::new(context.source_manager()).assemble_library([baz])?;
let program_source = source_file!(
&context,
"\
use lib::baz
begin
trace.0
exec.baz::f
trace.1
end"
);
let program = Assembler::new(context.source_manager())
.with_dynamic_library(lib)?
.assemble_program(program_source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn decorators_join_and_split() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
begin
trace.0 trace.1
if.true
trace.2 add trace.3
else
trace.4 mul trace.5
end
trace.6
if.true
trace.7 push.42 trace.8
else
trace.9 push.22 trace.10
end
trace.11
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn assert_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
assert
assert.err=ERR1
assert.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn assertz_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
assertz
assertz.err=ERR1
assertz.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn assert_eq_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
assert_eq
assert_eq.err=ERR1
assert_eq.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn assert_eqw_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
assert_eqw
assert_eqw.err=ERR1
assert_eqw.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn u32assert_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
u32assert
u32assert.err=ERR1
u32assert.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn u32assert2_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
u32assert2
u32assert2.err=ERR1
u32assert2.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn u32assertw_with_code() -> TestResult {
let context = TestContext::default();
let err_msg = "Oh no";
let source = source_file!(
&context,
format!(
"\
const ERR1 = \"{err_msg}\"
begin
u32assertw
u32assertw.err=ERR1
u32assertw.err=\"{err_msg}\"
end
"
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn asserts_and_mpverify_with_code_in_duplicate_procedure() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
proc f1
u32assert.err=\"1\"
end
proc f2
u32assert.err=\"2\"
end
proc f12
u32assert.err=\"1\"
u32assert.err=\"2\"
end
proc f21
u32assert.err=\"2\"
u32assert.err=\"1\"
end
proc g1
assert.err=\"1\"
end
proc g2
assert.err=\"2\"
end
proc g12
assert.err=\"1\"
assert.err=\"2\"
end
proc g21
assert.err=\"2\"
assert.err=\"1\"
end
proc fg
assert.err=\"1\"
u32assert.err=\"1\"
assert.err=\"2\"
u32assert.err=\"2\"
u32assert.err=\"1\"
assert.err=\"1\"
u32assert.err=\"2\"
assert.err=\"2\"
end
proc mpverify
mtree_verify.err=\"1\"
mtree_verify.err=\"2\"
mtree_verify.err=\"2\"
mtree_verify.err=\"1\"
end
begin
exec.f1
exec.f2
exec.f12
exec.f21
exec.g1
exec.g2
exec.g12
exec.g21
exec.fg
exec.mpverify
end
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn mtree_verify_with_code() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
const ERR1 = \"1\"
begin
mtree_verify
mtree_verify.err=ERR1
mtree_verify.err=\"2\"
end
"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn nested_control_blocks() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"begin \
push.2 push.3 \
if.true \
add while.true push.7 push.11 add end \
else \
mul repeat.2 push.8 end if.true mul end \
end
push.3 add
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
fn nested_if_source(depth: usize) -> String {
let mut source = String::from("begin\n");
for _ in 0..depth {
source.push_str("push.1\nif.true\n");
}
source.push_str("push.1\n");
for _ in 0..depth {
source.push_str("end\n");
}
source.push_str("end\n");
source
}
#[test]
fn control_flow_nesting_depth_boundary() -> TestResult {
let context = TestContext::default();
let source = nested_if_source(MAX_CONTROL_FLOW_NESTING);
let source = source_file!(&context, source.as_str());
context.assemble(source)?;
Ok(())
}
#[test]
fn control_flow_nesting_depth_exceeded() -> TestResult {
let context = TestContext::default();
let source = nested_if_source(MAX_CONTROL_FLOW_NESTING + 1);
let source = source_file!(&context, source.as_str());
let error = context
.assemble(source)
.expect_err("expected diagnostic to be raised, but compilation succeeded");
assert_diagnostic!(&error, "control-flow nesting depth exceeded");
Ok(())
}
#[test]
fn ensure_correct_procedure_selection_on_collision() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"
proc f
add
end
proc g
trace.2
add
end
begin
if.true
exec.f
else
exec.g
end
end"
);
let program = context.assemble(source)?;
let expected_f_node_id =
MastNodeId::from_u32_safe(1_u32, program.mast_forest().as_ref()).unwrap();
let expected_g_node_id =
MastNodeId::from_u32_safe(0_u32, program.mast_forest().as_ref()).unwrap();
let (exec_f_node_id, exec_g_node_id) = {
let split_node_id = {
let root_join_id = program.entrypoint();
program.mast_forest()[root_join_id].unwrap_join().second()
};
let split_node = &program.mast_forest()[split_node_id].unwrap_split();
(split_node.on_true(), split_node.on_false())
};
assert_eq!(program.mast_forest()[expected_f_node_id], program.mast_forest()[exec_f_node_id]);
assert_eq!(program.mast_forest()[expected_g_node_id], program.mast_forest()[exec_g_node_id]);
Ok(())
}
#[test]
fn program_with_one_procedure() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"proc foo push.3 push.7 mul end begin push.2 push.3 add exec.foo end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_nested_procedure() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
proc foo push.3 push.7 mul end \
proc bar push.5 exec.foo add end \
begin push.2 push.4 add exec.foo push.11 exec.bar sub end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_proc_locals() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
@locals(4) proc foo \
loc_store.0 \
add \
loc_load.0 \
mul \
end \
begin \
push.10 push.9 push.8 \
exec.foo \
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_proc_locals_fail() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
proc foo
loc_store.0
add
loc_load.0
mul
end
begin
push.4 push.3 push.2
exec.foo
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid procedure local reference",
regex!(r#",-\[test[\d]+:1:1\]"#),
"1 | ,-> proc foo",
"2 | | loc_store.0",
" : | ^^^^^|^^^^^",
" : | `-- the procedure local index referenced here is invalid",
"3 | | add",
"4 | | loc_load.0",
"5 | | mul",
"6 | |-> end",
" : `---- this procedure definition does not allocate any locals",
"7 | begin",
" `----"
);
Ok(())
}
#[test]
fn program_with_exported_procedure() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"pub proc foo push.3 push.7 mul end begin push.2 push.3 add exec.foo end"
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"invalid program: procedure exports are not allowed",
regex!(r#",-\[test[\d]+:1:1\]"#),
"1 | pub proc foo push.3 push.7 mul end begin push.2 push.3 add exec.foo end",
" : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
" `----",
" help: perhaps you meant to use `proc` instead of `export`?"
);
Ok(())
}
#[test]
fn program_with_dynamic_code_execution() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin dynexec end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_dynamic_code_execution_in_new_context() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin dyncall end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_incorrect_mast_root_length() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin call.0x1234 end");
assert_assembler_diagnostic!(
context,
source,
"invalid MAST root literal",
regex!(r#",-\[test[\d]+:1:12\]"#),
"1 | begin call.0x1234 end",
" : ^^^^^^",
" `----"
);
Ok(())
}
#[test]
fn program_with_invalid_mast_root_chars() {
let context = TestContext::default();
let source = source_file!(
&context,
"begin call.0xc2545da99d3a1f3f38d957c7893c44d78998d8ea8b11aba7e22c8c2b2a21xyzb end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid literal: expected 2, 4, 8, 16, or 64 hex digits",
regex!(r#",-\[test[\d]+:1:12\]"#),
"1 | begin call.0xc2545da99d3a1f3f38d957c7893c44d78998d8ea8b11aba7e22c8c2b2a21xyzb end",
" : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
" `----"
);
}
#[test]
fn program_with_invalid_rpo_digest_call() {
let context = TestContext::default();
let source = source_file!(
&context,
"begin call.0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid literal: value overflowed the field modulus",
regex!(r#",-\[test[\d]+:1:12\]"#),
"1 | begin call.0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff end",
" : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^",
" `----"
);
}
#[test]
fn program_with_phantom_mast_call() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"begin call.0xc2545da99d3a1f3f38d957c7893c44d78998d8ea8b11aba7e22c8c2b2a213dae end"
);
let ast = context.parse_program(source)?;
let assembler = Assembler::new(context.source_manager());
assembler.assemble_program(ast)?;
Ok(())
}
#[test]
fn program_with_one_import_and_hex_call() -> TestResult {
const MODULE: &str = "dummy::math::u256";
const PROCEDURE: &str = r#"
pub proc iszero_unsafe
eq.0
repeat.7
swap
eq.0
and
end
end"#;
let mut context = TestContext::default();
let path = MODULE;
let ast =
context.parse_module_with_path(path, source_file!(&context, PROCEDURE.to_string()))?;
let library = Assembler::new(context.source_manager())
.assemble_library(core::iter::once(ast))
.unwrap();
context.add_library(&library)?;
let source = source_file!(
&context,
format!(
r#"
use {MODULE}
begin
push.4 push.3
exec.u256::iszero_unsafe
call.0x20234ee941e53a15886e733cc8e041198c6e90d2a16ea18ce1030e8c3596dd38
end"#
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_two_imported_procs_with_same_mast_root() -> TestResult {
const MODULE: &str = "dummy::math::u256";
const PROCEDURE: &str = r#"
pub proc iszero_unsafe_dup
eq.0
repeat.7
swap
eq.0
and
end
end
pub proc iszero_unsafe
eq.0
repeat.7
swap
eq.0
and
end
end"#;
let mut context = TestContext::default();
let path = MODULE;
let ast =
context.parse_module_with_path(path, source_file!(&context, PROCEDURE.to_string()))?;
let library = Assembler::new(context.source_manager())
.assemble_library(core::iter::once(ast))
.unwrap();
context.add_library(&library)?;
let source = source_file!(
&context,
format!(
r#"
use {MODULE}
begin
push.4 push.3
exec.u256::iszero_unsafe
exec.u256::iszero_unsafe_dup
end"#
)
);
context.assemble(source)?;
Ok(())
}
#[test]
fn program_with_reexported_proc_in_same_library() -> TestResult {
const REF_MODULE: &str = "dummy1::math::u64";
const REF_MODULE_BODY: &str = r#"
pub proc checked_eqz
u32assert2
eq.0
swap
eq.0
and
end
pub proc unchecked_eqz
eq.0
swap
eq.0
and
end
"#;
const MODULE: &str = "dummy1::math::u256";
const MODULE_BODY: &str = r#"
use dummy1::math::u64
#! checked_eqz checks if the value is u32 and zero and returns 1 if it is, 0 otherwise
pub use u64::checked_eqz # re-export
#! unchecked_eqz checks if the value is zero and returns 1 if it is, 0 otherwise
pub use u64::unchecked_eqz->notchecked_eqz # re-export with alias
"#;
let mut context = TestContext::new();
let mut parser = Module::parser(ModuleKind::Library);
let ast = parser.parse_str(MODULE, MODULE_BODY, context.source_manager()).unwrap();
let docs_checked_eqz = ast
.aliases()
.find(|p| p.name().as_str() == "checked_eqz")
.unwrap()
.docs()
.unwrap();
assert_eq!(
docs_checked_eqz,
"checked_eqz checks if the value is u32 and zero and returns 1 if it is, 0 otherwise\n"
);
let docs_unchecked_eqz = ast
.aliases()
.find(|p| p.name().as_str() == "notchecked_eqz")
.unwrap()
.docs()
.unwrap();
assert_eq!(
docs_unchecked_eqz,
"unchecked_eqz checks if the value is zero and returns 1 if it is, 0 otherwise\n"
);
let mut parser = Module::parser(ModuleKind::Library);
let ref_ast = parser.parse_str(REF_MODULE, REF_MODULE_BODY, context.source_manager()).unwrap();
let library = Assembler::new(context.source_manager())
.assemble_library([ast, ref_ast])
.unwrap();
context.add_library(&library)?;
let source = source_file!(
&context,
format!(
r#"
use {MODULE}
begin
push.4 push.3
exec.u256::checked_eqz
exec.u256::notchecked_eqz
end"#
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_reexported_custom_alias_in_same_library() -> TestResult {
const REF_MODULE: &str = "dummy1::math::u64";
const REF_MODULE_BODY: &str = r#"
pub proc checked_eqz
u32assert2
eq.0
swap
eq.0
and
end
pub proc unchecked_eqz
eq.0
swap
eq.0
and
end
"#;
const MODULE: &str = "dummy1::math::u256";
const MODULE_BODY: &str = r#"
use dummy1::math::u64->myu64
#! checked_eqz checks if the value is u32 and zero and returns 1 if it is, 0 otherwise
pub use myu64::checked_eqz # re-export
#! unchecked_eqz checks if the value is zero and returns 1 if it is, 0 otherwise
pub use myu64::unchecked_eqz->notchecked_eqz # re-export with alias
"#;
let mut context = TestContext::new();
let mut parser = Module::parser(ModuleKind::Library);
let ast = parser.parse_str(MODULE, MODULE_BODY, context.source_manager()).unwrap();
let mut parser = Module::parser(ModuleKind::Library);
let ref_ast = parser.parse_str(REF_MODULE, REF_MODULE_BODY, context.source_manager()).unwrap();
let library = Assembler::new(context.source_manager())
.assemble_library([ast, ref_ast])
.unwrap();
context.add_library(&library)?;
let source = source_file!(
&context,
format!(
r#"
use {MODULE}->myu256
begin
push.4 push.3
exec.myu256::checked_eqz
exec.myu256::notchecked_eqz
end"#
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn program_with_reexported_proc_in_another_library() -> TestResult {
const REF_MODULE: &str = "dummy2::math::u64";
const REF_MODULE_BODY: &str = r#"
pub proc checked_eqz
u32assert2
eq.0
swap
eq.0
and
end
pub proc unchecked_eqz
eq.0
swap
eq.0
and
end
"#;
const MODULE: &str = "dummy1::math::u256";
const MODULE_BODY: &str = r#"
use dummy2::math::u64
pub use u64::checked_eqz # re-export
pub use u64::unchecked_eqz->notchecked_eqz # re-export with alias
"#;
let mut context = TestContext::default();
let mut parser = Module::parser(ModuleKind::Library);
let source_manager = context.source_manager();
let ref_ast = parser.parse_str(REF_MODULE, REF_MODULE_BODY, source_manager.clone())?;
let ast = parser.parse_str(MODULE, MODULE_BODY, source_manager.clone())?;
let dummy_library = {
let mut assembler = Assembler::new(source_manager);
assembler.compile_and_statically_link(ref_ast)?;
assembler.assemble_library([ast])?
};
context.add_library(&dummy_library)?;
let source = source_file!(
&context,
format!(
r#"
use {MODULE}
begin
push.4 push.3
exec.u256::checked_eqz
exec.u256::notchecked_eqz
end"#
)
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let mut context = TestContext::default();
context.add_library(dummy_library)?;
let source = source_file!(
&context,
format!(
r#"
use {REF_MODULE}
begin
push.4 push.3
exec.u64::checked_eqz
exec.u64::notchecked_eqz
end"#
)
);
assert_assembler_diagnostic!(
context,
source,
"undefined symbol reference",
regex!(r#",-\[test[\d]+:2:13\]"#),
"1 |",
"2 | use dummy2::math::u64",
" : ^^^^^^^^|^^^^^^^^",
" : `-- this symbol path could not be resolved",
"3 | begin",
" `----",
"help: maybe you are missing an import?"
);
Ok(())
}
#[test]
fn module_alias() -> TestResult {
const MODULE: &str = "dummy::math::u64";
const PROCEDURE: &str = r#"
pub proc checked_add
swap
movup.3
u32assert2
u32widening_add
movup.3
movup.3
u32assert2
u32widening_add3
eq.0
assert
end"#;
let mut context = TestContext::default();
let source_manager = context.source_manager();
let mut parser = Module::parser(ModuleKind::Library);
let ast = parser.parse_str(MODULE, PROCEDURE, source_manager.clone()).unwrap();
let library = Assembler::new(source_manager).assemble_library([ast]).unwrap();
context.add_library(&library)?;
let source = source_file!(
&context,
"
use dummy::math::u64->bigint
begin
push.1.0
push.2.0
exec.bigint::checked_add
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
let source = source_file!(
&context,
"
use dummy::math::u64->bigint->invalidname
begin
push.1.0
push.2.0
exec.bigint->invalidname::checked_add
end"
);
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:2:37\]"#),
"1 |",
"2 | use dummy::math::u64->bigint->invalidname",
" : ^|",
" : `-- found a -> here",
"3 |",
" `----",
r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or end of file, or doc comment"#
);
Ok(())
}
#[test]
fn module_alias_unused_import() -> TestResult {
const MODULE: &str = "dummy::math::u64";
const PROCEDURE: &str = r#"
pub proc checked_add
swap
movup.3
u32assert2
u32widening_add
movup.3
movup.3
u32assert2
u32widening_add3
eq.0
assert
end"#;
let mut context = TestContext::default();
let source_manager = context.source_manager();
let mut parser = Module::parser(ModuleKind::Library);
let ast = parser.parse_str(MODULE, PROCEDURE, source_manager.clone()).unwrap();
let library = Assembler::new(source_manager).assemble_library([ast]).unwrap();
context.add_library(&library)?;
let source = source_file!(
&context,
"
use dummy::math::u64
use dummy::math::u64->bigint
begin
push.1.0
push.2.0
exec.bigint::checked_add
end"
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"unused import",
regex!(r#",-\[test[\d]+:2:13\]"#),
"1 |",
"2 | use dummy::math::u64",
" : ^^^^^^^^^^^^^^^^",
"3 | use dummy::math::u64->bigint",
" `----",
" help: this import is never used and can be safely removed"
);
Ok(())
}
#[test]
fn program_with_import_errors() {
let context = TestContext::default();
let source = source_file!(
&context,
"\
use miden::core::math::u512
begin \
push.4 push.3 \
exec.u512::iszero_unsafe \
end"
);
assert_assembler_diagnostic!(
context,
source,
"undefined symbol reference",
regex!(r#",-\[test[\d]+:1:5\]"#),
"1 | use miden::core::math::u512",
" : ^^^^^^^^^^^|^^^^^^^^^^^",
" : `-- this symbol path could not be resolved",
"2 | begin push.4 push.3 exec.u512::iszero_unsafe end",
" `----",
"help: maybe you are missing an import?"
);
let source = source_file!(
&context,
"\
use miden::core::math::u256
begin \
push.4 push.3 \
exec.u256::foo \
end"
);
assert_assembler_diagnostic!(
context,
source,
"undefined symbol reference",
regex!(r#",-\[test[\d]+:1:5\]"#),
"1 | use miden::core::math::u256",
" : ^^^^^^^^^^^|^^^^^^^^^^^",
" : `-- this symbol path could not be resolved",
"2 | begin push.4 push.3 exec.u256::foo end",
" `----",
"help: maybe you are missing an import?"
);
}
#[test]
fn comment_simple() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin # simple comment \n push.1 push.2 add end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn comment_in_nested_control_blocks() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"begin \
push.1 push.2 \
if.true \
# nested comment \n\
add while.true push.7 push.11 add end \
else \
mul repeat.2 push.8 end if.true mul end \
# nested comment \n\
end
push.3 add
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn comment_before_program() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, " # starting comment \n begin push.1 push.2 add end");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn comment_after_program() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 push.2 add end # closing comment");
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn can_push_constant_word() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
const A = 0x0200000000000000030000000000000004000000000000000500000000000000
begin
push.A
end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn test_advmap_push() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
adv_map A(0x0200000000000000020000000000000002000000000000000200000000000000) = [0x01]
begin push.A adv.push_mapval assert end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn test_advmap_push_nokey() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
adv_map A = [0x01]
begin push.A adv.push_mapval assert end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn test_adv_has_map_key() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
adv_map A(0x0200000000000000020000000000000002000000000000000200000000000000) = [0x01]
begin adv.has_mapkey assert end"
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn invalid_empty_program() {
let context = TestContext::default();
assert_assembler_diagnostic!(
context,
source_file!(&context, ""),
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:1\]"#),
"`----",
r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or doc comment"#
);
assert_assembler_diagnostic!(
context,
source_file!(&context, ""),
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:1\]"#),
" `----",
r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or doc comment"#
);
}
#[test]
fn invalid_program_unrecognized_token() {
let context = TestContext::default();
assert_assembler_diagnostic!(
context,
source_file!(&context, "none"),
"invalid syntax",
regex!(r#",-\[test[\d]+:1:1\]"#),
"1 | none",
" : ^^|^",
" : `-- found a identifier here",
" `----",
r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or doc comment"#
);
}
#[test]
fn invalid_program_unmatched_begin() {
let context = TestContext::default();
assert_assembler_diagnostic!(
context,
source_file!(&context, "begin add"),
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:10\]"#),
"1 | begin add",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
}
#[test]
fn invalid_program_invalid_top_level_token() {
let context = TestContext::default();
assert_assembler_diagnostic!(
context,
source_file!(&context, "begin add end mul"),
"invalid syntax",
regex!(r#",-\[test[\d]+:1:15\]"#),
"1 | begin add end mul",
" : ^|^",
" : `-- found a mul here",
" `----",
r#" help: expected "@", or "adv_map", or "begin", or "const", or "enum", or "proc", or "pub", or "type", or "use", or end of file, or doc comment"#
);
}
#[test]
fn invalid_proc_missing_end_unexpected_begin() {
let context = TestContext::default();
let source = source_file!(&context, "proc foo add mul begin push.1 end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:18\]"#),
"1 | proc foo add mul begin push.1 end",
" : ^^|^^",
" : `-- found a begin here",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
}
#[test]
fn invalid_proc_missing_end_unexpected_proc() {
let context = TestContext::default();
let source = source_file!(&context, "proc foo add mul proc bar push.3 end begin push.1 end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:18\]"#),
"1 | proc foo add mul proc bar push.3 end begin push.1 end",
" : ^^|^",
" : `-- found a proc here",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
}
#[test]
fn invalid_proc_undefined_local() {
let context = TestContext::default();
let source = source_file!(&context, "proc foo add mul end begin push.1 exec.bar end");
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"undefined symbol reference",
regex!(r#",-\[test[\d]+:1:40\]"#),
"1 | proc foo add mul end begin push.1 exec.bar end",
" : ^|^",
" : `-- this symbol path could not be resolved",
" `----",
" help: maybe you are missing an import?"
);
}
#[test]
fn missing_import() {
let context = TestContext::new();
let source = source_file!(
&context,
r#"
begin
exec.u64::add
end"#
);
assert_assembler_diagnostic!(
context,
source,
"undefined symbol reference",
regex!(r#",-\[test[\d]+:3:14\]"#),
"2 | begin",
"3 | exec.u64::add",
" : ^^^^|^^^",
" : `-- this symbol path could not be resolved",
"4 | end",
" `----",
"help: maybe you are missing an import?"
);
}
#[test]
fn invalid_proc_invalid_numeric_name() {
let context = TestContext::default();
let source = source_file!(&context, "proc 123 add mul end begin push.1 exec.123 end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:6\]"#),
"1 | proc 123 add mul end begin push.1 exec.123 end",
" : ^|^",
" : `-- found a integer here",
" `----",
" help: expected primitive opcode",
" identifier"
);
}
#[test]
fn invalid_proc_duplicate_procedure_name() {
let context = TestContext::default();
let source =
source_file!(&context, "proc foo add mul end proc foo push.3 end begin push.1 end");
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"symbol conflict: found duplicate definitions of the same name",
regex!(r#",-\[test[\d]+:1:6\]"#),
"1 | proc foo add mul end proc foo push.3 end begin push.1 end",
" : ^|^ ^^^^^^^^^|^^^^^^^^^",
" : | `-- conflict occurs here",
" : `-- previously defined here",
" `----"
);
}
#[test]
fn invalid_if_missing_end_no_else() {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 add if.true mul");
assert_assembler_diagnostic!(
context,
source,
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:29\]"#),
"1 | begin push.1 add if.true mul",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "else", or "end", or control flow opcode (e.g. "if.true")"#
);
}
#[test]
fn invalid_else_with_no_if() {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 add else mul end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:18\]"#),
"1 | begin push.1 add else mul end",
" : ^^|^",
" : `-- found a else here",
" `----",
r#" help: expected primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
let source = source_file!(&context, "begin push.1 while.true add else mul end end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:29\]"#),
"1 | begin push.1 while.true add else mul end end",
" : ^^|^",
" : `-- found a else here",
" `----",
r#" help: expected "end""#
);
}
#[test]
fn invalid_unmatched_else_within_if_else() {
let context = TestContext::default();
let source =
source_file!(&context, "begin push.1 if.true add else mul else push.1 end end end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:35\]"#),
"1 | begin push.1 if.true add else mul else push.1 end end end",
" : ^^|^",
" : `-- found a else here",
" `----",
r#" help: expected "end""#
);
}
#[test]
fn invalid_if_else_no_matching_end() {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 add if.true mul else add");
assert_assembler_diagnostic!(
context,
source,
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:38\]"#),
"1 | begin push.1 add if.true mul else add",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
}
#[test]
fn invalid_repeat() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 add repeat.10 mul");
assert_assembler_diagnostic!(
context,
source,
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:31\]"#),
"1 | begin push.1 add repeat.10 mul",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
let source = source_file!(&context, "begin push.1 add repeat.23x3 mul end end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:27\]"#),
"1 | begin push.1 add repeat.23x3 mul end end",
" : ^|",
" : `-- found a identifier here",
" `----",
r#" help: expected primitive opcode (e.g. "add"), or control flow opcode (e.g. "if.true")"#
);
let count: u64 = u32::MAX as u64 + 1;
let source = source_file!(
&context,
format!(
"\
const CONSTANT = {count}
begin
repeat.CONSTANT
add
end
end
"
)
);
assert_assembler_diagnostic!(
context,
source,
"syntax error",
"help: see emitted diagnostics for details",
"invalid immediate: value is larger than expected range",
regex!(r#",-\[test[\d]+:3:24\]"#),
"2 | begin",
"3 | repeat.CONSTANT",
" : ^^^^^^^^",
"4 | add",
" `----"
);
Ok(())
}
#[test]
fn invalid_repeat_count_zero() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin repeat.0 nop end end");
let error = context.assemble(source).expect_err("expected repeat.0 to be rejected");
let rendered =
format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
assert!(rendered.contains("invalid repeat count"));
Ok(())
}
#[test]
fn invalid_repeat_count_zero_with_decorator() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
"\
proc foo
trace.1
repeat.0
nop
end
end
begin
call.foo
end"
);
let error = context
.assemble(source)
.expect_err("expected repeat.0 with decorator to be rejected");
let rendered =
format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
assert!(rendered.contains("invalid repeat count"));
Ok(())
}
#[test]
fn invalid_repeat_count_too_large() -> TestResult {
let context = TestContext::default();
let repeat_count = MAX_REPEAT_COUNT + 1;
let source = source_file!(&context, format!("begin repeat.{repeat_count} nop end end"));
let error = context
.assemble(source)
.expect_err("expected repeat count above limit to be rejected");
let rendered =
format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
assert!(rendered.contains("invalid repeat count"));
Ok(())
}
#[test]
fn invalid_repeat_count_constant_zero() -> TestResult {
let context = TestContext::default();
let source =
source_file!(&context, "const REPEAT_COUNT = 0\nbegin repeat.REPEAT_COUNT nop end end");
let error = context
.assemble(source)
.expect_err("expected repeat.0 from constant to be rejected");
let rendered =
format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
assert!(rendered.contains("invalid repeat count"));
Ok(())
}
#[test]
fn invalid_repeat_count_constant_too_large() -> TestResult {
let context = TestContext::default();
let repeat_count = MAX_REPEAT_COUNT + 1;
let source = source_file!(
&context,
format!("const REPEAT_COUNT = {repeat_count}\nbegin repeat.REPEAT_COUNT nop end end")
);
let error = context
.assemble(source)
.expect_err("expected repeat count above limit from constant to be rejected");
let rendered =
format!("{}", crate::diagnostics::reporting::PrintDiagnostic::new_without_color(&error));
assert!(rendered.contains("invalid repeat count"));
Ok(())
}
#[test]
fn repeat_count_constant_at_limit_allowed() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
format!("const REPEAT_COUNT = {MAX_REPEAT_COUNT}\nbegin repeat.REPEAT_COUNT nop end end")
);
context
.assemble(source)
.expect("expected repeat count at limit from constant to be accepted");
Ok(())
}
#[test]
fn const_folding_modulus_aliasing_must_be_rejected() {
let program_src = r#"
const ALIAS = 18446744069414584320+1
begin
push.ALIAS
end
"#;
let assembled = Assembler::default().assemble_program(program_src);
assert!(
assembled.is_err(),
"expected constants >= field modulus to be rejected (must not silently alias to 0)"
);
}
#[test]
fn const_evaluator_modulus_aliasing_must_be_rejected() {
let program_src = r#"
const X = 18446744069414584320
const Y = 1
const ALIAS = X+Y
begin
push.ALIAS
end
"#;
let assembled = Assembler::default().assemble_program(program_src);
assert!(
assembled.is_err(),
"expected out-of-range constant results to be rejected (must not silently alias via `Felt::new`)"
);
}
#[test]
fn const_folding_u64_overflow_must_not_panic_and_must_error() {
let program_src = r#"
const WRAP = 18446744069414584320+18446744069414584320
begin
push.WRAP
end
"#;
let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Assembler::default().assemble_program(program_src)
}));
assert!(
assembled.is_ok(),
"assembler panicked while folding a constant expression with u64 overflow"
);
let assembled = assembled.unwrap();
assert!(
assembled.is_err(),
"expected the assembler to reject constant expressions which overflow u64 during folding"
);
}
#[test]
fn const_folding_subtraction_underflow_must_be_rejected() {
let program_src = r#"
const UNDERFLOW = 0-1
begin
push.UNDERFLOW
end
"#;
let assembled = Assembler::default().assemble_program(program_src);
assert!(
assembled.is_err(),
"expected subtraction underflow in constant expressions to be rejected"
);
}
#[test]
fn const_division_slash_must_not_match_int_division() {
let program_src = r#"
const A1 = 3/2
const B1 = 3//2
const X = 3
const Y = 2
const A2 = X/Y
const B2 = X//Y
begin
push.A1
push.B1
push.A2
push.B2
end
"#;
let program = Assembler::default()
.assemble_program(program_src)
.expect("program assembly must succeed");
let entry = program.get_node_by_id(program.entrypoint()).expect("missing entrypoint node");
let mast = format!("{}", entry.to_display(program.mast_forest()));
let toks: Vec<&str> = mast.split_whitespace().collect();
let pad_incr_pairs = toks.windows(2).filter(|w| w[0] == "pad" && w[1] == "incr").count();
assert_eq!(
pad_incr_pairs, 2,
"expected `/` (field division) to not fold to the same value as `//` (integer division)"
);
}
#[test]
fn const_division_by_zero_must_error() {
let program_src = r#"
const BAD = 1/0
begin
push.BAD
end
"#;
let assembled = Assembler::default().assemble_program(program_src);
assert!(
assembled.is_err(),
"expected division by zero in constant expressions to be rejected"
);
}
#[test]
fn push_word_slice_u64_max_must_not_panic_and_must_error() {
let program_src = r#"
const WORD = [1,2,3,4]
begin
push.WORD[18446744073709551615]
end
"#;
let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Assembler::default().assemble_program(program_src)
}));
assert!(
assembled.is_ok(),
"assembler panicked while parsing push.WORD[...] with an out-of-range index"
);
let assembled = assembled.unwrap();
assert!(
assembled.is_err(),
"expected push.WORD[...] with an out-of-range index to be rejected with an error"
);
}
#[test]
fn push_word_slice_range_u64_max_end_must_not_panic_and_must_error() {
let program_src = r#"
const WORD = [1,2,3,4]
begin
push.WORD[0..18446744073709551615]
end
"#;
let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Assembler::default().assemble_program(program_src)
}));
assert!(
assembled.is_ok(),
"assembler panicked while parsing push.WORD[0..] with an out-of-range index"
);
let assembled = assembled.unwrap();
assert!(
assembled.is_err(),
"expected push.WORD[0..] with an out-of-range index to be rejected with an error"
);
}
#[test]
fn push_word_slice_range_u64_max_start_must_not_panic_and_must_error() {
let program_src = r#"
const WORD = [1,2,3,4]
begin
push.WORD[18446744073709551615..0]
end
"#;
let assembled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Assembler::default().assemble_program(program_src)
}));
assert!(
assembled.is_ok(),
"assembler panicked while parsing push.WORD[..0] with an out-of-range index"
);
let assembled = assembled.unwrap();
assert!(
assembled.is_err(),
"expected push.WORD[..0] with an out-of-range index to be rejected with an error"
);
}
#[test]
fn invalid_while() -> TestResult {
let context = TestContext::default();
let source = source_file!(&context, "begin push.1 add while mul end end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:24\]"#),
"1 | begin push.1 add while mul end end",
" : ^|^",
" : `-- found a mul here",
" `----",
r#" help: expected ".""#
);
let source = source_file!(&context, "begin push.1 add while.abc mul end end");
assert_assembler_diagnostic!(
context,
source,
"invalid syntax",
regex!(r#",-\[test[\d]+:1:24\]"#),
"1 | begin push.1 add while.abc mul end end",
" : ^|^",
" : `-- found a identifier here",
" `----",
r#" help: expected "true""#
);
let source = source_file!(&context, "begin push.1 add while.true mul");
assert_assembler_diagnostic!(
context,
source,
"unexpected end of file",
regex!(r#",-\[test[\d]+:1:32\]"#),
"1 | begin push.1 add while.true mul",
" `----",
r#" help: expected ".", or primitive opcode (e.g. "add"), or "end", or control flow opcode (e.g. "if.true")"#
);
Ok(())
}
#[test]
fn test_compiled_library() {
let context = TestContext::new();
let mut mod_parser = ModuleParser::new(ModuleKind::Library);
let mod1 = {
let source = source_file!(
&context,
"
proc internal
push.5
end
pub proc foo
push.1
drop
end
pub proc bar
exec.internal
drop
end
"
);
mod_parser
.parse(PathBuf::new("mylib::mod1").unwrap(), source, context.source_manager())
.unwrap()
};
let mod2 = {
let source = source_file!(
&context,
"
pub proc foo
push.7
add.5
end
# Same definition as mod1::foo
pub proc bar
push.1
drop
end
"
);
mod_parser
.parse(PathBuf::new("mylib::mod2").unwrap(), source, context.source_manager())
.unwrap()
};
let compiled_library = {
let assembler = Assembler::new(context.source_manager());
assembler.assemble_library([mod1, mod2]).unwrap()
};
assert_eq!(compiled_library.exports().count(), 4);
let mut assembler = Assembler::new(context.source_manager());
assembler.link_dynamic_library(&compiled_library).unwrap();
let program_source = "
use mylib::mod1
use mylib::mod2
proc foo
push.1
drop
end
begin
exec.mod1::foo
exec.mod1::bar
exec.mod2::foo
exec.mod2::bar
exec.foo
end
";
let _program = assembler.assemble_program(program_source).unwrap();
}
#[test]
fn test_reexported_proc_with_same_name_as_local_proc_diff_locals() {
let context = TestContext::new();
let mut mod_parser = ModuleParser::new(ModuleKind::Library);
let mod1 = {
let source = source_file!(
&context,
"@locals(8) pub proc foo
push.1
drop
end
"
);
mod_parser
.parse(PathBuf::new("test::mod1").unwrap(), source, context.source_manager())
.unwrap()
};
let mod2 = {
let source = source_file!(
&context,
"use test::mod1
pub proc foo
exec.mod1::foo
end
"
);
mod_parser
.parse(PathBuf::new("test::mod2").unwrap(), source, context.source_manager())
.unwrap()
};
let compiled_library = {
let assembler = Assembler::new(context.source_manager());
assembler.assemble_library([mod1, mod2]).unwrap()
};
assert_eq!(compiled_library.exports().count(), 2);
let mut assembler = Assembler::new(context.source_manager());
assembler.link_dynamic_library(&compiled_library).unwrap();
let program_source = "
use test::mod1
use test::mod2
@locals(4)
proc foo
exec.mod1::foo
exec.mod2::foo
end
begin
exec.foo
end
";
let _program = assembler.assemble_program(program_source).unwrap();
}
#[test]
fn test_program_serde_simple() {
let source = "
begin
push.1.2
add
drop
end
";
let assembler = Assembler::default();
let original_program = assembler.assemble_program(source).unwrap();
let mut target = Vec::new();
original_program.write_into(&mut target);
let deserialized_program = Program::read_from_bytes(&target).unwrap();
assert_eq!(original_program, deserialized_program);
}
#[test]
fn test_program_serde_with_decorators() {
let source = "
const DEFAULT_CONST = 100
const EVENT_CONST = event(\"serde::evt\")
proc foo
push.1.2 add
debug.stack.8
end
begin
emit.EVENT_CONST
exec.foo
debug.stack.4
drop
trace.DEFAULT_CONST
end
";
let assembler = Assembler::default();
let original_program = assembler.assemble_program(source).unwrap();
let mut target = Vec::new();
original_program.write_into(&mut target);
let deserialized_program = Program::read_from_bytes(&target).unwrap();
assert_eq!(original_program, deserialized_program);
}
#[test]
fn vendoring() -> TestResult {
let context = TestContext::new();
let mut mod_parser = ModuleParser::new(ModuleKind::Library);
let vendor_lib = {
let source = source_file!(&context, "pub proc bar push.1 end pub proc prune push.2 end");
let mod1 = mod_parser
.parse(PathBuf::new("test::mod1").unwrap(), source, context.source_manager())
.unwrap();
Assembler::default().assemble_library([mod1]).unwrap()
};
let lib = {
let source = source_file!(&context, "pub proc foo exec.::test::mod1::bar end");
let mod2 = mod_parser
.parse(PathBuf::new("test::mod2").unwrap(), source, context.source_manager())
.unwrap();
let mut assembler = Assembler::default();
assembler.link_static_library(vendor_lib)?;
assembler.assemble_library([mod2]).unwrap()
};
let expected_lib = {
let source = source_file!(&context, "pub proc foo push.1 end");
let mod2 = mod_parser.parse("test::expected", source, context.source_manager()).unwrap();
Assembler::default().assemble_library([mod2]).unwrap()
};
assert!(
expected_lib.mast_forest().debug_info().num_asm_ops() > 0,
"Expected library should have AssemblyOps for instruction tracking"
);
let mut assembler_with_vendored_lib = Assembler::default();
let link_result = assembler_with_vendored_lib.link_static_library(lib.clone());
assert!(link_result.is_ok(), "Should be able to link the vendored library");
let program_with_lib_source = r#"
begin
push.1
push.2
add
end
"#;
let assemble_result = assembler_with_vendored_lib.assemble_program(program_with_lib_source);
assert!(
assemble_result.is_ok(),
"Should be able to assemble program with linked library"
);
let assembled_program = assemble_result.unwrap();
assert!(
assembled_program.mast_forest().debug_info().num_asm_ops() > 0,
"Assembled program with library should have AssemblyOps for instruction tracking"
);
let mast_forest = lib.mast_forest();
assert!(mast_forest.num_nodes() > 0, "Vendored library should have nodes");
let nodes = mast_forest.nodes();
assert!(!nodes.is_empty(), "Vendored library should have root procedures");
Ok(())
}
#[test]
fn emit_u32_immediate_is_rejected() {
let context = TestContext::new();
let program_source = r#"
begin
emit.32
end
"#;
context
.assemble(program_source)
.expect_err(r#"emit.<u32> should be rejected; only event("...") is allowed"#);
}
#[test]
fn emit_const_must_be_event_hash() {
let context = TestContext::new();
let program_source = r#"
const BAD = 100
begin
emit.BAD
end
"#;
context
.assemble(program_source)
.expect_err(r#"emit.CONST should require const defined via event("...")"#);
let program_source = r#"
const BADW = word("foo")
begin
emit.BADW
end
"#;
context
.assemble(program_source)
.expect_err(r#"emit.CONST should require const defined via event("...")"#);
}
#[test]
#[should_panic]
fn test_assert_diagnostic_lines() {
assert_diagnostic_lines!(report!("the error string"), "the error string", "other", "lines");
}
prop_compose! {
fn any_package()(name in ".*", artifact in any::<ArbitraryMastArtifact>(), manifest in any::<PackageManifest>()) -> Package {
let ArbitraryMastArtifact { ty, lib } = artifact;
let mut exports = Vec::default();
for export in lib.exports() {
match export {
LibraryExport::Procedure(export) => {
let digest = lib.mast_forest()[export.node].digest();
exports.push(PackageExport::Procedure(ProcedureExport {
path: export.path.clone(),digest,
signature: export.signature.clone(),
attributes: export.attributes.clone(),
}));
}
LibraryExport::Constant(export) => {
exports.push(PackageExport::Constant(ConstantExport {
path: export.path.clone(),
value: export.value.clone(),
}));
}
LibraryExport::Type(export) => {
exports.push(PackageExport::Type(TypeExport {
path: export.path.clone(),
ty: export.ty.clone(),
}));
}
}
}
let manifest = PackageManifest::new(exports)
.and_then(|package_manifest| {
package_manifest.with_dependencies(manifest.dependencies().cloned())
})
.expect("test package manifest should be valid");
let name = PackageId::from(name);
let version = miden_assembly_syntax::Version::new(0, 0, 0);
Package { name, version, description: None, kind: ty, mast: lib, manifest, sections: Default::default() }
}
}
#[derive(Debug, Clone)]
struct ArbitraryMastArtifact {
ty: TargetType,
lib: Arc<Library>,
}
impl ArbitraryMastArtifact {
fn library(lib: Arc<Library>) -> Self {
Self { ty: TargetType::Library, lib }
}
fn executable(lib: Arc<Library>) -> Self {
Self { ty: TargetType::Executable, lib }
}
}
impl Arbitrary for ArbitraryMastArtifact {
type Parameters = ();
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
prop_oneof![
Just(Self::library(LIB_EXAMPLE.clone())),
Just(Self::executable(PRG_EXAMPLE.clone()))
]
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
static LIB_EXAMPLE: LazyLock<Arc<Library>> = LazyLock::new(build_library_example);
static PRG_EXAMPLE: LazyLock<Arc<Library>> = LazyLock::new(build_program_example);
fn build_library_example() -> Arc<Library> {
let context = TestContext::new();
let foo_src = r#"
pub proc foo(a: felt, b: felt) -> felt
add
end
pub proc foo_mul(a: felt, b: felt) -> felt
mul
end
"#;
let foo_module = parse_module!(&context, "test::foo", foo_src);
let bar_src = r#"
pub proc bar
mtree_get
end
pub proc bar_mul
mul
end
"#;
let bar_module = parse_module!(&context, "test::bar", bar_src);
let modules = [foo_module, bar_module];
Assembler::new(context.source_manager())
.assemble_library(modules.iter().cloned())
.expect("failed to assemble library")
}
fn build_program_example() -> Arc<Library> {
use crate::{Parse, ParseOptions};
let source = "
begin
push.1.2
add
drop
end
";
let assembler = Assembler::default();
let options = ParseOptions {
kind: ModuleKind::Executable,
warnings_as_errors: assembler.warnings_as_errors(),
path: Some(Path::exec_path().into()),
};
let program = source.parse_with_options(assembler.source_manager(), options).unwrap();
assembler.assemble_executable_modules(program, []).unwrap().into_artifact()
}
#[test]
fn package_serialization_roundtrip() {
let cases = 128;
TestRunner::new(Config::with_cases(cases))
.run(&any_package(), move |package| {
let bytes = package.to_bytes();
let deserialized = Package::read_from_bytes(&bytes).unwrap();
prop_assert_eq!(package, deserialized);
Ok(())
})
.unwrap_or_else(|err| {
panic!("{err}");
});
}
#[test]
fn nested_blocks() -> Result<(), Report> {
const KERNEL: &str = r#"
pub proc foo
add
end"#;
const MODULE: &str = "libs::helpers";
const MODULE_PROCEDURE: &str = r#"
pub proc help
push.29
end"#;
let context = TestContext::new();
let assembler = {
let kernel_lib = Assembler::new(context.source_manager()).assemble_kernel(KERNEL).unwrap();
let dummy_module = context.parse_module_with_path(MODULE, MODULE_PROCEDURE)?;
let dummy_library = Assembler::new(context.source_manager())
.assemble_library([dummy_module])
.unwrap();
let mut assembler = Assembler::with_kernel(context.source_manager(), kernel_lib);
assembler.link_dynamic_library(dummy_library).unwrap();
assembler
};
let mut expected_mast_forest_builder = MastForestBuilder::default();
let syscall_foo_node_id = {
let kernel_foo_node_id = expected_mast_forest_builder
.ensure_block(vec![Operation::Add], Vec::new(), vec![], vec![], vec![], vec![])
.unwrap();
expected_mast_forest_builder
.ensure_node(
CallNodeBuilder::new_syscall(kernel_foo_node_id)
.with_before_enter(vec![])
.with_after_exit(vec![]),
)
.unwrap()
};
let program = r#"
use libs::helpers
proc foo
push.19
end
proc bar
push.17
exec.foo
end
begin
push.2
if.true
push.3
else
push.5
end
if.true
if.true
push.7
else
push.11
end
else
push.13
while.true
exec.bar
push.23
end
end
exec.helpers::help
syscall.foo
end"#;
let program = assembler.assemble_program(program).unwrap();
let exec_foo_bar_baz_node_id = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(29))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let fmp_initialization = expected_mast_forest_builder
.ensure_block(fmp_initialization_sequence(), Vec::new(), vec![], vec![], vec![], vec![])
.unwrap();
let before = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(2))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let r#true1 = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(3))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let r#false1 = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(5))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let r#if1 = expected_mast_forest_builder
.ensure_node(
SplitNodeBuilder::new([r#true1, r#false1])
.with_before_enter(vec![])
.with_after_exit(vec![]),
)
.unwrap();
let r#true3 = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(7))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let r#false3 = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(11))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let r#true2 = expected_mast_forest_builder
.ensure_node(
SplitNodeBuilder::new([r#true3, r#false3])
.with_before_enter(vec![])
.with_after_exit(vec![]),
)
.unwrap();
let r#while = {
let body_node_id = expected_mast_forest_builder
.ensure_block(
vec![
Operation::Push(Felt::from_u32(17)),
Operation::Push(Felt::from_u32(19)),
Operation::Push(Felt::from_u32(23)),
],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
expected_mast_forest_builder
.ensure_node(
LoopNodeBuilder::new(body_node_id)
.with_before_enter(vec![])
.with_after_exit(vec![]),
)
.unwrap()
};
let push_13_basic_block_id = expected_mast_forest_builder
.ensure_block(
vec![Operation::Push(Felt::from_u32(13))],
Vec::new(),
vec![],
vec![],
vec![],
vec![],
)
.unwrap();
let r#false2 = expected_mast_forest_builder
.ensure_node(
JoinNodeBuilder::new([push_13_basic_block_id, r#while])
.with_before_enter(vec![])
.with_after_exit(vec![]),
)
.unwrap();
let nested = expected_mast_forest_builder
.ensure_node(
SplitNodeBuilder::new([r#true2, r#false2])
.with_before_enter(vec![])
.with_after_exit(vec![]),
)
.unwrap();
let combined_node_id = expected_mast_forest_builder
.join_nodes(
vec![
fmp_initialization,
before,
r#if1,
nested,
exec_foo_bar_baz_node_id,
syscall_foo_node_id,
],
None,
)
.unwrap();
let (mut expected_mast_forest, node_remapping) = expected_mast_forest_builder.build();
expected_mast_forest.make_root(node_remapping[&combined_node_id]);
let expected_program =
Program::new(expected_mast_forest.into(), node_remapping[&combined_node_id]);
assert_eq!(expected_program.hash(), program.hash());
assert_eq!(program.num_procedures(), 3);
Ok(())
}
#[test]
fn emit_instruction_digest() {
let context = TestContext::new();
let program_source = r#"
const EVT1 = event("miden::test::event_one")
const EVT2 = event("miden::test::event_two")
proc foo
emit.EVT1
end
proc bar
emit.EVT2
end
begin
# specific impl irrelevant
exec.foo
exec.bar
end
"#;
let program = context.assemble(program_source).unwrap();
let procedure_digests: Vec<Word> = program.mast_forest().procedure_digests().collect();
assert_eq!(3, procedure_digests.len());
assert_ne!(procedure_digests[0], procedure_digests[1]);
assert_ne!(procedure_digests[0], procedure_digests[2]);
assert_ne!(procedure_digests[1], procedure_digests[2]);
}
#[test]
fn emit_syntax_equivalence() {
let context = TestContext::new();
let program1_source = r#"
const EVT = event("miden::test::equiv")
begin
emit.EVT
end
"#;
let program2_source = r#"
begin
emit.event("miden::test::equiv")
end
"#;
let program3_source = r#"
const EVT = event("miden::test::equiv")
begin
push.EVT
emit
drop
end
"#;
let program1 = context.assemble(program1_source).unwrap();
let program2 = context.assemble(program2_source).unwrap();
let program3 = context.assemble(program3_source).unwrap();
let digest1 = program1.hash();
let digest2 = program2.hash();
let digest3 = program3.hash();
assert_eq!(digest1, digest2, "MAST digests differ between programs 1 and 2");
assert_eq!(digest1, digest3, "MAST digests differ between programs 1 and 3");
assert_eq!(program1.num_procedures(), 1);
assert_eq!(program2.num_procedures(), 1);
assert_eq!(program3.num_procedures(), 1);
}
#[test]
fn duplicate_procedure() {
let context = TestContext::new();
let program_source = r#"
proc foo
add
mul
end
proc bar
add
mul
end
begin
# specific impl irrelevant
exec.foo
exec.bar
end
"#;
let program = context.assemble(program_source).unwrap();
assert_eq!(program.num_procedures(), 3);
}
#[test]
fn distinguish_grandchildren_correctly() {
let context = TestContext::new();
let program_source = r#"
begin
if.true
while.true
trace.1234
push.1
end
end
if.true
while.true
push.1
end
end
end
"#;
let program = context.assemble(program_source).unwrap();
let join_node = &program.mast_forest()[program.entrypoint()].unwrap_join();
assert_ne!(join_node.first(), join_node.second());
}
#[test]
fn explicit_fully_qualified_procedure_references() -> Result<(), Report> {
const BAR_NAME: &str = "foo::bar";
const BAR: &str = r#"
pub proc bar
add
end"#;
const BAZ_NAME: &str = "foo::baz";
const BAZ: &str = r#"
pub proc baz
exec.::foo::bar::bar
end"#;
let context = TestContext::default();
let bar = context.parse_module_with_path(BAR_NAME, BAR)?;
let baz = context.parse_module_with_path(BAZ_NAME, BAZ)?;
let library = context.assemble_library([bar, baz]).unwrap();
let assembler =
Assembler::new(context.source_manager()).with_dynamic_library(&library).unwrap();
let program = r#"
begin
exec.::foo::baz::baz
end"#;
assert_matches!(assembler.assemble_program(program), Ok(_));
Ok(())
}
#[test]
fn re_exports() -> Result<(), Report> {
const BAR_NAME: &str = "foo::bar";
const BAR: &str = r#"
pub proc baz
add
end"#;
const BAZ_NAME: &str = "foo::baz";
const BAZ: &str = r#"
use foo::bar
pub use bar::baz
pub proc qux
push.1 push.2 add
end"#;
let context = TestContext::new();
let bar = context.parse_module_with_path(BAR_NAME, BAR)?;
let baz = context.parse_module_with_path(BAZ_NAME, BAZ)?;
let library = context.assemble_library([bar, baz]).unwrap();
let assembler =
Assembler::new(context.source_manager()).with_dynamic_library(&library).unwrap();
let program = r#"
use foo::baz
begin
push.1 push.2
exec.baz::baz
push.3 push.4
exec.baz::qux
end"#;
assert_matches!(assembler.assemble_program(program), Ok(_));
Ok(())
}
#[test]
fn module_ordering_can_be_arbitrary() -> Result<(), Report> {
const A_NAME: &str = "a";
const A: &str = r#"
pub proc foo
add
end"#;
const B_NAME: &str = "b";
const B: &str = r#"
pub proc bar
push.1 push.2 exec.::a::foo
end"#;
const C_NAME: &str = "c";
const C: &str = r#"
pub proc baz
exec.::b::bar
end"#;
let context = TestContext::new();
let a = context.parse_module_with_path(A_NAME, A)?;
let b = context.parse_module_with_path(B_NAME, B)?;
let c = context.parse_module_with_path(C_NAME, C)?;
let mut assembler = Assembler::new(context.source_manager());
assembler.compile_and_statically_link(b)?.compile_and_statically_link(a)?;
assembler.assemble_library([c])?;
Ok(())
}
#[test]
fn can_assemble_a_multi_module_kernel() -> Result<(), Report> {
const KERNEL: &str = r#"
use kernellib::helpers->h
pub proc foo
exec.h::get_caller
end"#;
const HELPERS: &str = r#"
pub proc get_caller
caller
end"#;
const PROGRAM: &str = r#"
begin
syscall.foo
end"#;
let context = TestContext::new();
let kernel_lib = {
let helpers = context
.parse_module_with_path(PathBuf::new("::kernellib::helpers").unwrap(), HELPERS)?;
let kernel = context.parse_kernel(KERNEL).unwrap();
let mut assembler = Assembler::new(context.source_manager());
assembler.compile_and_statically_link(helpers)?;
assembler.assemble_kernel(kernel).unwrap()
};
assert_eq!(kernel_lib.kernel().proc_hashes().len(), 1);
Assembler::with_kernel(context.source_manager(), kernel_lib).assemble_program(PROGRAM)?;
Ok(())
}
#[test]
fn regression_empty_kernel_library_is_rejected() {
let context = TestContext::default();
let source_manager = context.source_manager();
let kernel_masm = "pub const FOO = 1\n";
let err = Assembler::new(source_manager)
.assemble_kernel(kernel_masm)
.expect_err("expected empty kernel to be rejected");
assert_diagnostic_lines!(err, "library must contain at least one exported procedure");
}
#[test]
fn issue_1644_single_forest_merge_identity() -> TestResult {
let context = TestContext::new();
let program_source = r#"
proc test
push.1
push.2
push.3
end
proc main
push.10
if.true
exec.test
push.20
else
push.30
end
push.40
end
begin
exec.main
end"#;
let program = context.assemble(program_source)?;
let original_forest = program.mast_forest().clone();
let (merged_forest, _) = MastForest::merge([&*original_forest]).into_diagnostic()?;
let original_join = original_forest.nodes()[6].unwrap_join();
let merged_join = merged_forest.nodes()[5].unwrap_join();
assert_eq!(original_join.first(), merged_join.first());
assert_eq!(original_join.second(), merged_join.second());
assert_eq!(original_join.digest(), merged_join.digest());
let (new_merged_forest, _) = MastForest::merge([&merged_forest]).into_diagnostic()?;
let mut should_panic = false;
if new_merged_forest.nodes().len() != merged_forest.nodes().len() {
eprintln!(
"Forest node count differs: original={}, merged={}",
new_merged_forest.nodes().len(),
merged_forest.nodes().len()
);
eprintln!("This violates the identity requirement for merge operation");
should_panic = true;
}
for (i, (orig_node, merged_node)) in
new_merged_forest.nodes().iter().zip(merged_forest.nodes().iter()).enumerate()
{
if orig_node.digest() != merged_node.digest() {
eprintln!("Node {} digest violation:", i);
eprintln!(" Original: {:?}", orig_node);
eprintln!(" Merged: {:?}", merged_node);
eprintln!(" Original digest: {:?}", orig_node.digest());
eprintln!(" Merged digest: {:?}", merged_node.digest());
should_panic = true;
}
}
for (i, (orig_root, merged_root)) in new_merged_forest
.procedure_roots()
.iter()
.zip(merged_forest.procedure_roots())
.enumerate()
{
if new_merged_forest[*orig_root].digest() != merged_forest[*merged_root].digest() {
eprintln!("Root {} digest violation:", i);
eprintln!(" Original: {:?}", original_forest[*orig_root].digest());
eprintln!(" Merged: {:?}", merged_forest[*merged_root].digest());
should_panic = true;
}
}
if should_panic {
panic!("Merge idempotence violation");
}
eprintln!("Merge identity test passed - no violations detected");
Ok(())
}
#[test]
fn overlong_total_path_is_rejected_without_panic() {
use std::{
panic::{AssertUnwindSafe, catch_unwind},
sync::Arc,
};
use miden_assembly_syntax::ast::Path;
use crate::{Parse, ParseOptions, ast::ModuleKind, testing::TestContext};
let component = "a".repeat(255);
let num_components: usize = 300;
let mut path_str = alloc::string::String::with_capacity(num_components * (component.len() + 2));
for i in 0..num_components {
if i > 0 {
path_str.push_str("::");
}
path_str.push_str(&component);
}
let context = TestContext::default();
let source_manager = context.source_manager();
let lib_src = r#"
pub proc add
add.1
end
"#;
let parsed = catch_unwind(AssertUnwindSafe(|| {
let path = Path::new(&path_str);
let options = ParseOptions {
kind: ModuleKind::Library,
warnings_as_errors: false,
path: Some(Arc::<Path>::from(path)),
};
<&str as Parse>::parse_with_options(lib_src, source_manager.clone(), options)
}));
assert!(
parsed.is_ok(),
"overlong total path caused a panic; expected a structured error"
);
let parsed = parsed.unwrap();
let err = parsed.expect_err("expected overlong path to be rejected");
assert_diagnostic!(err, "invalid item path: too long");
}
#[test]
fn imported_error_message_alias_is_resolved_without_panicking() {
use std::{
panic::{AssertUnwindSafe, catch_unwind},
sync::Arc,
};
use crate::{Assembler, DefaultSourceManager, Parse, ParseOptions, ast::ModuleKind};
let source_manager: Arc<dyn crate::SourceManager> = Arc::new(DefaultSourceManager::default());
let module_b_src = r#"
pub const ERR1 = "oops"
pub const ERR2 = ERR1
"#;
let module_b = module_b_src
.parse_with_options(source_manager.clone(), ParseOptions::new(ModuleKind::Library, "b"))
.expect("module b parsing must succeed");
let module_a_src = r#"
use b::ERR2
begin
assert.err=ERR2
end
"#;
let assembled = catch_unwind(AssertUnwindSafe(|| {
let mut assembler = Assembler::new(source_manager);
assembler
.compile_and_statically_link(module_b)
.expect("linking module b must succeed");
assembler.assemble_program(module_a_src)
}));
assert!(assembled.is_ok(), "assembler panicked during assembly");
assembled
.unwrap()
.expect("expected imported error message alias to assemble successfully");
}
#[test]
fn test_issue_2181_locaddr_bug_assembly() -> TestResult {
let context = TestContext::default();
let source = source_file!(
&context,
r#"
proc some_proc
debug.stack.4
nop
end
@locals(4)
proc main
locaddr.0 debug.stack.4
locaddr.0 debug.stack.4
locaddr.0 debug.stack.4
exec.some_proc
debug.stack.4
dropw
end
begin
exec.main
end"#
);
let program = context.assemble(source)?;
insta::assert_snapshot!(program);
Ok(())
}
#[test]
fn test_assembler_debug_info_present() {
let context = TestContext::default();
let source = r#"
pub proc foo
push.1 push.2 add
end
"#;
let module = parse_module!(&context, "test::foo", source);
let assembler = Assembler::default();
let library = assembler.assemble_library([module]).unwrap();
let mast_forest = library.mast_forest();
let has_asm_ops = mast_forest.debug_info().num_asm_ops() > 0;
assert!(has_asm_ops, "AssemblyOps should be present for tracking instructions");
}
#[test]
fn test_cross_module_constant_resolution() -> TestResult {
let context = TestContext::default();
let module_a = context.parse_module_with_path(
"cycle::module_a",
source_file!(
&context,
r#"
pub const A_VAL = 10
pub proc a_proc
push.A_VAL
end
"#
),
)?;
let module_b = context.parse_module_with_path(
"cycle::module_b",
source_file!(
&context,
r#"
use cycle::module_a
pub const B_VAL = module_a::A_VAL + 5 # <-- Should work but fails
pub proc b_proc
push.B_VAL
end
"#
),
)?;
let assembler = Assembler::new(context.source_manager());
let _ = assembler.assemble_library([module_a, module_b])?;
Ok(())
}
#[test]
fn test_cross_module_constant_resolution_as_local_definition() -> TestResult {
let context = TestContext::default();
let module_a = context.parse_module_with_path(
"cycle::module_a",
source_file!(
&context,
r#"
pub const A_VAL = 10
pub proc a_proc
push.A_VAL
end
"#
),
)?;
let module_b = context.parse_module_with_path(
"cycle::module_b",
source_file!(
&context,
r#"
use cycle::module_a::A_VAL
pub proc b_proc
push.A_VAL
end
"#
),
)?;
let assembler = Assembler::new(context.source_manager());
let _ = assembler.assemble_library([module_a, module_b])?;
Ok(())
}
#[test]
fn test_cross_module_constant_reexport_chain_in_procedure_scope() -> TestResult {
let context = TestContext::new();
let a = parse_module!(
&context,
"dcrc::a",
r#"
pub const VAL = 99
pub proc use_val
push.VAL
drop
end
"#
);
let b = parse_module!(
&context,
"dcrc::b",
r#"
use dcrc::a
pub const STEP = a::VAL + 1
pub proc dummy
push.STEP
drop
end
"#
);
let c = parse_module!(
&context,
"dcrc::c",
r#"
use dcrc::b
pub const FINAL_VAL = b::STEP + 1
pub proc dummy
push.FINAL_VAL
drop
end
"#
);
let lib = Assembler::new(context.source_manager()).assemble_library([a, b, c])?;
let src = source_file!(
&context,
r#"
use dcrc::c
const LOCAL = c::FINAL_VAL
begin
push.LOCAL
drop
end
"#
);
let _program = Assembler::new(context.source_manager())
.with_dynamic_library(lib)?
.assemble_program(src)?;
Ok(())
}
#[test]
fn test_issue_2696_imported_constant_with_private_dependency() -> TestResult {
let context = TestContext::new();
let memory = parse_module!(
&context,
"wallet::memory",
r#"
const ACCOUNT_ID_AND_NONCE_OFFSET = 4
pub const ACCOUNT_ID_SUFFIX_OFFSET = ACCOUNT_ID_AND_NONCE_OFFSET + 2
"#
);
let account = parse_module!(
&context,
"wallet::account",
r#"
use wallet::memory::ACCOUNT_ID_SUFFIX_OFFSET
pub proc use_suffix
push.ACCOUNT_ID_SUFFIX_OFFSET
drop
end
"#
);
Assembler::new(context.source_manager()).assemble_library([memory, account])?;
Ok(())
}
#[test]
fn test_cross_module_constant_cycle_in_procedure_scope_is_structured_error() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let context = TestContext::new();
let a = parse_module!(
&context,
"cycle::a",
r#"
use cycle::b
pub proc use_cycle
push.A
drop
end
pub const A = b::B + 1
"#
);
let b = parse_module!(
&context,
"cycle::b",
r#"
use cycle::a
pub const B = a::A + 1
"#
);
let assembled = catch_unwind(AssertUnwindSafe(|| {
Assembler::new(context.source_manager()).assemble_library([a, b])
}));
assert!(assembled.is_ok(), "assembler panicked during assembly");
let err = assembled.unwrap().expect_err("expected cyclic constants to be rejected");
assert_diagnostic!(&err, "constant evaluation terminated due to infinite recursion");
assert_diagnostic!(&err, "pub const A = b::B + 1");
assert_diagnostic!(&err, "pub const B = a::A + 1");
}
#[test]
fn imported_error_message_cycle_is_rejected_without_panicking() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let context = TestContext::new();
let a = parse_module!(
&context,
"cycle::errs::a",
r#"
use cycle::errs::b
pub proc use_cycle
assert.err=ERR_A
end
pub const ERR_A = b::ERR_B
"#
);
let b = parse_module!(
&context,
"cycle::errs::b",
r#"
use cycle::errs::a
pub const ERR_B = a::ERR_A
"#
);
let assembled = catch_unwind(AssertUnwindSafe(|| {
Assembler::new(context.source_manager()).assemble_library([a, b])
}));
assert!(assembled.is_ok(), "assembler panicked during assembly");
let err = assembled
.unwrap()
.expect_err("expected cyclic error message constants to be rejected");
assert_diagnostic!(&err, "constant evaluation terminated due to infinite recursion");
assert_diagnostic!(&err, "pub const ERR_A = b::ERR_B");
assert_diagnostic!(&err, "pub const ERR_B = a::ERR_A");
}
#[test]
fn test_cross_module_quoted_identifier_resolution() -> TestResult {
let context = TestContext::default();
let module_a = context.parse_module_with_path(
"cycle::\"module::a\"",
source_file!(
&context,
r#"
# Checks local path resolution
pub proc "$item::<T>::fun"
exec."$item::<T>::get"
end
# Checks absolute path resolution to a local item
proc "$item::<T>::get"
exec.::cycle::"module::a"::"$item::<T>::get_impl"
end
proc "$item::<T>::get_impl"
push.1
end
"#
),
)?;
let module_b = context.parse_module_with_path(
"cycle::module::b",
source_file!(
&context,
r#"
# Checks that import resolution with quoted path components works
use cycle::"module::a"->a
# Checks that link-time cross-module resolution with quoted path components works
pub proc b_proc
exec.a::"$item::<T>::fun"
end
"#
),
)?;
let assembler = Assembler::new(context.source_manager());
let _ = assembler.assemble_library([module_a, module_b])?;
Ok(())
}
#[test]
fn test_kernel_linking_against_its_own_library() -> TestResult {
let context = TestContext::default();
let kernel = context.parse_kernel(source_file!(
&context,
r#"
proc internal_proc
caller
drop
exec.$kernel::lib::lib_proc
end
pub proc kernel_proc
exec.internal_proc
end
"#
))?;
let lib = context.parse_module_with_path(
"$kernel::lib",
source_file!(
&context,
r#"
pub proc lib_proc
swap
end
"#
),
)?;
let mut assembler = Assembler::new(context.source_manager());
assembler.compile_and_statically_link(lib)?;
let _ = assembler.assemble_kernel(kernel)?;
Ok(())
}
#[test]
fn test_syscall_resolution_uses_kernel_module() -> TestResult {
let context = TestContext::default();
let kernel = context.parse_kernel(source_file!(
&context,
r#"
pub proc foo
caller
drop
push.1
end
pub proc bar
caller
drop
push.2
end
"#
))?;
let lib = context.parse_module_with_path(
"userspace",
source_file!(
&context,
r#"
pub proc bar
push.0
end
"#
),
)?;
let source = source_file!(
&context,
r#"
use userspace::bar
proc foo
push.0
end
begin
syscall.foo
syscall.bar
end
"#
);
let kernel = Assembler::new(context.source_manager()).assemble_kernel(kernel)?;
let kernel_bar_root = kernel.as_ref().get_procedure_root_by_path("::$kernel::bar").unwrap();
let kernel_foo_root = kernel.as_ref().get_procedure_root_by_path("::$kernel::foo").unwrap();
let mut assembler = Assembler::with_kernel(context.source_manager(), kernel);
assembler.compile_and_statically_link(lib)?;
let program = assembler.assemble_program(source)?;
let mast = {
let entry = program.get_node_by_id(program.entrypoint()).unwrap();
format!("{}", entry.to_display(program.mast_forest()))
};
let expected = format!(
r#"join
join
basic_block push(2147483648) push(4294967294) mstore drop noop end
syscall.{kernel_foo_root}
end
syscall.{kernel_bar_root}
end"#
);
assert_eq!(mast, expected);
Ok(())
}
#[test]
fn test_syscall_resolution_to_non_kernel_path_is_checked() -> TestResult {
let context = TestContext::default();
let kernel = context.parse_kernel(source_file!(
&context,
r#"
pub proc foo
caller
drop
push.1
end
"#
))?;
let lib = context.parse_module_with_path(
"userspace",
source_file!(
&context,
r#"
pub proc bar
push.0
end
"#
),
)?;
let source = source_file!(
&context,
r#"
begin
syscall.userspace::bar
end
"#
);
let kernel = Assembler::new(context.source_manager()).assemble_kernel(kernel)?;
let lib = Assembler::new(context.source_manager()).assemble_library([lib])?;
let error = Assembler::with_kernel(context.source_manager(), kernel)
.with_static_library(lib)?
.assemble_program(source)
.expect_err("expected diagnostic to be raised, but compilation succeeded");
assert_diagnostic_lines!(
error,
"syntax error",
"help: see emitted diagnostics for details",
"invalid syscall: callee must be resolvable to kernel module",
regex!(r#",-\[test[\d]+:3:21\]"#),
"2 | begin",
"3 | syscall.userspace::bar",
" : ^^^^^^^^^^^^^^",
"4 | end",
" `----"
);
Ok(())
}
#[test]
fn syscall_validation_does_not_panic_on_same_digest_userspace_procedure() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let context = TestContext::default();
let source_manager = context.source_manager();
let kernel_src = r#"
pub proc k1
push.1
end
"#;
let kernel_lib = Assembler::new(source_manager.clone())
.assemble_kernel(kernel_src)
.expect("kernel assembly must succeed");
let assembler = Assembler::with_kernel(source_manager, kernel_lib);
let program_src = r#"
proc dup
push.1
end
begin
exec.dup
syscall.k1
end
"#;
let assembled = catch_unwind(AssertUnwindSafe(|| assembler.assemble_program(program_src)));
assert!(assembled.is_ok(), "assembler panicked while assembling a valid program");
assert!(assembled.unwrap().is_ok(), "expected program assembly to succeed");
}
#[test]
fn syscall_by_unknown_digest_is_rejected_at_assembly_time_when_kernel_is_configured() {
let context = TestContext::default();
let source_manager = context.source_manager();
let kernel_src = r#"
pub proc k1
push.1
end
"#;
let kernel_lib = Assembler::new(source_manager.clone())
.assemble_kernel(kernel_src)
.expect("kernel assembly must succeed");
let assembler = Assembler::with_kernel(source_manager, kernel_lib);
let program_src = r#"
begin
syscall.0x0000000000000000000000000000000000000000000000000000000000000000
end
"#;
let err = assembler
.assemble_program(program_src)
.expect_err("expected unknown digest syscall to be rejected");
assert_diagnostic!(err, "invalid syscall");
}
#[test]
fn syscall_without_kernel_is_rejected_at_assembly_time() {
let context = TestContext::default();
let assembler = Assembler::new(context.source_manager());
let program_src = r#"
begin
syscall.0x0000000000000000000000000000000000000000000000000000000000000000
end
"#;
let err = assembler
.assemble_program(program_src)
.expect_err("expected syscall without kernel to be rejected");
assert_diagnostic!(err, "invalid syscall");
}
#[test]
fn test_linking_imported_symbols_with_duplicate_prefix_components() -> TestResult {
let context = TestContext::default();
let lib = context.parse_module_with_path(
"lib::lib",
source_file!(
&context,
r#"
pub proc lib_proc
swap
end
"#
),
)?;
let assembler = Assembler::new(context.source_manager());
let lib = assembler.assemble_library([lib])?;
let assembler = Assembler::new(context.source_manager());
let _ = assembler.with_static_library(lib)?.assemble_program(
r#"
use lib::lib
begin
exec.lib::lib_proc
end
"#,
)?;
Ok(())
}
#[test]
#[ignore = "leave disabled until either symbol resolution is rewritten or path semantics are refined"]
fn test_linking_recursive_expansion() -> TestResult {
let context = TestContext::default();
let a_lib = context.parse_module_with_path(
"a",
source_file!(
&context,
r#"
pub use b::a
pub proc x
push.1
end
"#
),
)?;
let b_lib = context.parse_module_with_path(
"b",
source_file!(
&context,
r#"
pub use a::a
pub proc foo
exec.a::x
end
"#
),
)?;
let assembler = Assembler::new(context.source_manager());
let _ = assembler.assemble_library([a_lib, b_lib])?;
Ok(())
}
#[test]
#[ignore = "leave disabled until either symbol resolution is rewritten or path semantics are refined"]
fn test_linking_recursive_expansion_via_renamed_aliases() -> TestResult {
let context = TestContext::default();
let a_lib = context.parse_module_with_path(
"a::a",
source_file!(
&context,
r#"
pub use b::a2
pub proc x
push.1
end
"#
),
)?;
let b_lib = context.parse_module_with_path(
"b",
source_file!(
&context,
r#"
pub use a::a->a2
pub proc foo
exec.a2::x
end
"#
),
)?;
let assembler = Assembler::new(context.source_manager());
let _ = assembler.assemble_library([a_lib, b_lib])?;
Ok(())
}