use std::collections::{HashMap, HashSet};
use super::ast::*;
use crate::ir::{ANFBinding, ANFMethod, ANFParam, ANFProgram, ANFProperty, ANFValue, SourceLocation};
pub fn lower_to_anf(contract: &ContractNode) -> ANFProgram {
let properties = lower_properties(contract);
let mut methods = lower_methods(contract);
for method in &mut methods {
method.body = lift_branch_update_props(method.body.clone());
}
ANFProgram {
contract_name: contract.name.clone(),
properties,
methods,
}
}
fn lower_properties(contract: &ContractNode) -> Vec<ANFProperty> {
contract
.properties
.iter()
.map(|prop| ANFProperty {
name: prop.name.clone(),
prop_type: type_node_to_string(&prop.prop_type),
readonly: prop.readonly,
initial_value: prop.initializer.as_ref().and_then(extract_literal_value),
})
.collect()
}
fn extract_literal_value(expr: &Expression) -> Option<serde_json::Value> {
match expr {
Expression::BigIntLiteral { value } => Some(serde_json::Value::Number(
serde_json::Number::from(*value),
)),
Expression::BoolLiteral { value } => Some(serde_json::Value::Bool(*value)),
Expression::ByteStringLiteral { value } => {
Some(serde_json::Value::String(value.clone()))
}
Expression::UnaryExpr {
op: UnaryOp::Neg,
operand,
} => {
if let Expression::BigIntLiteral { value } = operand.as_ref() {
Some(serde_json::Value::Number(serde_json::Number::from(-*value)))
} else {
None
}
}
_ => None,
}
}
fn lower_methods(contract: &ContractNode) -> Vec<ANFMethod> {
let mut result = Vec::new();
let mut ctor_ctx = LoweringContext::new(contract);
ctor_ctx.set_method_param_types(&contract.constructor.params);
lower_statements(&contract.constructor.body, &mut ctor_ctx);
result.push(ANFMethod {
name: "constructor".to_string(),
params: lower_params(&contract.constructor.params),
body: ctor_ctx.bindings,
is_public: false,
});
for method in &contract.methods {
let mut method_ctx = LoweringContext::new(contract);
method_ctx.set_method_param_types(&method.params);
if contract.parent_class == "StatefulSmartContract"
&& method.visibility == Visibility::Public
{
let needs_change_output =
method_mutates_state(method, contract) || method_has_add_output(method);
let needs_new_amount =
method_mutates_state(method, contract) && !method_has_add_output(method);
if needs_change_output {
method_ctx.add_param("_changePKH");
method_ctx.add_param("_changeAmount");
}
if needs_new_amount {
method_ctx.add_param("_newAmount");
}
method_ctx.add_param("txPreimage");
let preimage_ref = method_ctx.emit(ANFValue::LoadParam {
name: "txPreimage".to_string(),
});
let check_result = method_ctx.emit(ANFValue::CheckPreimage {
preimage: preimage_ref,
});
method_ctx.emit(ANFValue::Assert {
value: check_result,
});
let has_state_prop = contract.properties.iter().any(|p| !p.readonly);
if has_state_prop {
let preimage_ref3 = method_ctx.emit(ANFValue::LoadParam {
name: "txPreimage".to_string(),
});
method_ctx.emit(ANFValue::DeserializeState {
preimage: preimage_ref3,
});
}
lower_statements(&method.body, &mut method_ctx);
let add_output_refs = method_ctx.add_output_refs.clone();
if !add_output_refs.is_empty() || method_mutates_state(method, contract) {
let change_pkh_ref = method_ctx.emit(ANFValue::LoadParam {
name: "_changePKH".to_string(),
});
let change_amount_ref = method_ctx.emit(ANFValue::LoadParam {
name: "_changeAmount".to_string(),
});
let change_output_ref = method_ctx.emit(ANFValue::Call {
func: "buildChangeOutput".to_string(),
args: vec![change_pkh_ref, change_amount_ref],
});
if !add_output_refs.is_empty() {
let mut accumulated = add_output_refs[0].clone();
for i in 1..add_output_refs.len() {
accumulated = method_ctx.emit(ANFValue::Call {
func: "cat".to_string(),
args: vec![accumulated, add_output_refs[i].clone()],
});
}
accumulated = method_ctx.emit(ANFValue::Call {
func: "cat".to_string(),
args: vec![accumulated, change_output_ref],
});
let hash_ref = method_ctx.emit(ANFValue::Call {
func: "hash256".to_string(),
args: vec![accumulated],
});
let preimage_ref2 = method_ctx.emit(ANFValue::LoadParam {
name: "txPreimage".to_string(),
});
let output_hash_ref = method_ctx.emit(ANFValue::Call {
func: "extractOutputHash".to_string(),
args: vec![preimage_ref2],
});
let eq_ref = method_ctx.emit(ANFValue::BinOp {
op: "===".to_string(),
left: hash_ref,
right: output_hash_ref,
result_type: Some("bytes".to_string()),
});
method_ctx.emit(ANFValue::Assert { value: eq_ref });
} else {
let state_script_ref = method_ctx.emit(ANFValue::GetStateScript {});
let preimage_ref2 = method_ctx.emit(ANFValue::LoadParam {
name: "txPreimage".to_string(),
});
let new_amount_ref = method_ctx.emit(ANFValue::LoadParam {
name: "_newAmount".to_string(),
});
let contract_output_ref = method_ctx.emit(ANFValue::Call {
func: "computeStateOutput".to_string(),
args: vec![preimage_ref2.clone(), state_script_ref, new_amount_ref],
});
let all_outputs = method_ctx.emit(ANFValue::Call {
func: "cat".to_string(),
args: vec![contract_output_ref, change_output_ref],
});
let hash_ref = method_ctx.emit(ANFValue::Call {
func: "hash256".to_string(),
args: vec![all_outputs],
});
let preimage_ref4 = method_ctx.emit(ANFValue::LoadParam {
name: "txPreimage".to_string(),
});
let output_hash_ref = method_ctx.emit(ANFValue::Call {
func: "extractOutputHash".to_string(),
args: vec![preimage_ref4],
});
let eq_ref = method_ctx.emit(ANFValue::BinOp {
op: "===".to_string(),
left: hash_ref,
right: output_hash_ref,
result_type: Some("bytes".to_string()),
});
method_ctx.emit(ANFValue::Assert { value: eq_ref });
}
}
let mut augmented_params = lower_params(&method.params);
if needs_change_output {
augmented_params.push(ANFParam {
name: "_changePKH".to_string(),
param_type: "Ripemd160".to_string(),
});
augmented_params.push(ANFParam {
name: "_changeAmount".to_string(),
param_type: "bigint".to_string(),
});
}
if needs_new_amount {
augmented_params.push(ANFParam {
name: "_newAmount".to_string(),
param_type: "bigint".to_string(),
});
}
augmented_params.push(ANFParam {
name: "txPreimage".to_string(),
param_type: "SigHashPreimage".to_string(),
});
result.push(ANFMethod {
name: method.name.clone(),
params: augmented_params,
body: method_ctx.bindings,
is_public: true,
});
} else {
lower_statements(&method.body, &mut method_ctx);
result.push(ANFMethod {
name: method.name.clone(),
params: lower_params(&method.params),
body: method_ctx.bindings,
is_public: method.visibility == Visibility::Public,
});
}
}
result
}
fn lower_params(params: &[ParamNode]) -> Vec<ANFParam> {
params
.iter()
.map(|p| ANFParam {
name: p.name.clone(),
param_type: type_node_to_string(&p.param_type),
})
.collect()
}
struct LoweringContext<'a> {
bindings: Vec<ANFBinding>,
counter: usize,
contract: &'a ContractNode,
param_names: HashSet<String>,
method_param_types: HashMap<String, String>,
local_names: HashSet<String>,
add_output_refs: Vec<String>,
local_aliases: HashMap<String, String>,
local_byte_vars: HashSet<String>,
current_source_loc: Option<SourceLocation>,
}
impl<'a> LoweringContext<'a> {
fn new(contract: &'a ContractNode) -> Self {
LoweringContext {
bindings: Vec::new(),
counter: 0,
contract,
param_names: HashSet::new(),
method_param_types: HashMap::new(),
local_names: HashSet::new(),
add_output_refs: Vec::new(),
local_aliases: HashMap::new(),
local_byte_vars: HashSet::new(),
current_source_loc: None,
}
}
fn set_method_param_types(&mut self, params: &[ParamNode]) {
self.method_param_types.clear();
for p in params {
self.method_param_types
.insert(p.name.clone(), type_node_to_string(&p.param_type));
}
}
fn fresh_temp(&mut self) -> String {
let name = format!("t{}", self.counter);
self.counter += 1;
name
}
fn emit(&mut self, value: ANFValue) -> String {
let name = self.fresh_temp();
self.bindings.push(ANFBinding {
name: name.clone(),
value,
source_loc: self.current_source_loc.clone(),
});
name
}
fn emit_named(&mut self, name: &str, value: ANFValue) {
self.bindings.push(ANFBinding {
name: name.to_string(),
value,
source_loc: self.current_source_loc.clone(),
});
}
fn add_param(&mut self, name: &str) {
self.param_names.insert(name.to_string());
}
fn is_param(&self, name: &str) -> bool {
self.param_names.contains(name)
}
fn add_local(&mut self, name: &str) {
self.local_names.insert(name.to_string());
}
fn is_local(&self, name: &str) -> bool {
self.local_names.contains(name)
}
fn set_local_alias(&mut self, local_name: &str, binding_name: &str) {
self.local_aliases
.insert(local_name.to_string(), binding_name.to_string());
}
fn get_local_alias(&self, name: &str) -> Option<&String> {
self.local_aliases.get(name)
}
fn is_property(&self, name: &str) -> bool {
self.contract.properties.iter().any(|p| p.name == name)
}
fn sub_context(&self) -> LoweringContext<'a> {
let mut sub = LoweringContext::new(self.contract);
sub.counter = self.counter;
sub.param_names = self.param_names.clone();
sub.method_param_types = self.method_param_types.clone();
sub.local_names = self.local_names.clone();
sub.local_aliases = self.local_aliases.clone();
sub.local_byte_vars = self.local_byte_vars.clone();
sub.current_source_loc = self.current_source_loc.clone();
sub
}
fn sync_counter(&mut self, sub: &LoweringContext) {
if sub.counter > self.counter {
self.counter = sub.counter;
}
}
}
fn lower_statements(stmts: &[Statement], ctx: &mut LoweringContext) {
for i in 0..stmts.len() {
let stmt = &stmts[i];
if let Statement::IfStatement {
condition,
then_branch,
else_branch: None,
source_location,
} = stmt
{
if i + 1 < stmts.len() && branch_ends_with_return(then_branch) {
let remaining = stmts[i + 1..].to_vec();
let modified_if = Statement::IfStatement {
condition: condition.clone(),
then_branch: then_branch.clone(),
else_branch: Some(remaining),
source_location: source_location.clone(),
};
lower_statement(&modified_if, ctx);
return;
}
}
lower_statement(stmt, ctx);
}
}
fn branch_ends_with_return(stmts: &[Statement]) -> bool {
if stmts.is_empty() {
return false;
}
let last = &stmts[stmts.len() - 1];
match last {
Statement::ReturnStatement { .. } => true,
Statement::IfStatement {
then_branch,
else_branch: Some(else_branch),
..
} => branch_ends_with_return(then_branch) && branch_ends_with_return(else_branch),
_ => false,
}
}
fn statement_source_location(stmt: &Statement) -> &super::ast::SourceLocation {
match stmt {
Statement::VariableDecl { source_location, .. }
| Statement::Assignment { source_location, .. }
| Statement::IfStatement { source_location, .. }
| Statement::ForStatement { source_location, .. }
| Statement::ReturnStatement { source_location, .. }
| Statement::ExpressionStatement { source_location, .. } => source_location,
}
}
fn lower_statement(stmt: &Statement, ctx: &mut LoweringContext) {
let ast_loc = statement_source_location(stmt);
ctx.current_source_loc = Some(SourceLocation {
file: ast_loc.file.clone(),
line: ast_loc.line,
column: ast_loc.column,
});
match stmt {
Statement::VariableDecl {
name, init, ..
} => {
lower_variable_decl(name, init, ctx);
}
Statement::Assignment { target, value, .. } => {
lower_assignment(target, value, ctx);
}
Statement::IfStatement {
condition,
then_branch,
else_branch,
..
} => {
lower_if_statement(condition, then_branch, else_branch.as_deref(), ctx);
}
Statement::ForStatement {
init,
condition,
body,
..
} => {
lower_for_statement(init, condition, body, ctx);
}
Statement::ExpressionStatement { expression, .. } => {
lower_expr_to_ref(expression, ctx);
}
Statement::ReturnStatement { value, .. } => {
if let Some(v) = value {
let ref_name = lower_expr_to_ref(v, ctx);
if let Some(last) = ctx.bindings.last() {
if last.name != ref_name {
ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", ref_name)),
});
}
}
}
}
}
}
fn lower_variable_decl(name: &str, init: &Expression, ctx: &mut LoweringContext) {
let value_ref = lower_expr_to_ref(init, ctx);
ctx.add_local(name);
if is_byte_typed_expr(init, ctx) {
ctx.local_byte_vars.insert(name.to_string());
}
ctx.emit_named(
name,
ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", value_ref)),
},
);
}
fn lower_assignment(target: &Expression, value: &Expression, ctx: &mut LoweringContext) {
let value_ref = lower_expr_to_ref(value, ctx);
if let Expression::PropertyAccess { property } = target {
ctx.emit(ANFValue::UpdateProp {
name: property.clone(),
value: value_ref,
});
return;
}
if let Expression::Identifier { name } = target {
ctx.emit_named(
name,
ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", value_ref)),
},
);
return;
}
lower_expr_to_ref(target, ctx);
}
fn lower_if_statement(
condition: &Expression,
then_branch: &[Statement],
else_branch: Option<&[Statement]>,
ctx: &mut LoweringContext,
) {
let cond_ref = lower_expr_to_ref(condition, ctx);
let mut then_ctx = ctx.sub_context();
lower_statements(then_branch, &mut then_ctx);
ctx.sync_counter(&then_ctx);
let mut else_ctx = ctx.sub_context();
if let Some(else_stmts) = else_branch {
lower_statements(else_stmts, &mut else_ctx);
}
ctx.sync_counter(&else_ctx);
let then_bindings = then_ctx.bindings;
let else_bindings = else_ctx.bindings;
let then_last = then_bindings.last();
let else_last = else_bindings.last();
let alias_local = match (then_last, else_last) {
(Some(tl), Some(el)) if tl.name == el.name && ctx.is_local(&tl.name) => {
Some(tl.name.clone())
}
_ => None,
};
let then_has_outputs = !then_ctx.add_output_refs.is_empty();
let else_has_outputs = !else_ctx.add_output_refs.is_empty();
let if_name = ctx.emit(ANFValue::If {
cond: cond_ref,
then: then_bindings,
else_branch: else_bindings,
});
if then_has_outputs || else_has_outputs {
ctx.add_output_refs.push(if_name.clone());
}
if let Some(local_name) = alias_local {
ctx.set_local_alias(&local_name, &if_name);
}
}
fn lower_for_statement(
init: &Statement,
condition: &Expression,
body: &[Statement],
ctx: &mut LoweringContext,
) {
let count = extract_loop_count(init, condition);
let iter_var = if let Statement::VariableDecl { name, .. } = init {
name.clone()
} else {
"_i".to_string()
};
let mut body_ctx = ctx.sub_context();
lower_statements(body, &mut body_ctx);
ctx.sync_counter(&body_ctx);
ctx.emit(ANFValue::Loop {
count,
body: body_ctx.bindings,
iter_var,
});
}
fn extract_loop_count(init: &Statement, condition: &Expression) -> usize {
let start_val = if let Statement::VariableDecl { init: init_expr, .. } = init {
extract_bigint_value(init_expr)
} else {
None
};
if let Expression::BinaryExpr { op, right, .. } = condition {
let bound_val = extract_bigint_value(right);
if let (Some(start), Some(bound)) = (start_val, bound_val) {
match op {
BinaryOp::Lt => return (bound - start).max(0) as usize,
BinaryOp::Le => return (bound - start + 1).max(0) as usize,
BinaryOp::Gt => return (start - bound).max(0) as usize,
BinaryOp::Ge => return (start - bound + 1).max(0) as usize,
_ => {}
}
}
if let Some(bound) = bound_val {
match op {
BinaryOp::Lt => return bound as usize,
BinaryOp::Le => return (bound + 1) as usize,
_ => {}
}
}
}
0
}
fn extract_bigint_value(expr: &Expression) -> Option<i64> {
match expr {
Expression::BigIntLiteral { value } => Some(*value),
Expression::UnaryExpr { op, operand } if *op == UnaryOp::Neg => {
extract_bigint_value(operand).map(|v| -v)
}
_ => None,
}
}
fn lower_expr_to_ref(expr: &Expression, ctx: &mut LoweringContext) -> String {
match expr {
Expression::BigIntLiteral { value } => ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::Number(serde_json::Number::from(*value)),
}),
Expression::BoolLiteral { value } => ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::Bool(*value),
}),
Expression::ByteStringLiteral { value } => ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::String(value.clone()),
}),
Expression::Identifier { name } => lower_identifier(name, ctx),
Expression::PropertyAccess { property } => {
if ctx.is_param(property) {
return ctx.emit(ANFValue::LoadParam {
name: property.clone(),
});
}
ctx.emit(ANFValue::LoadProp {
name: property.clone(),
})
}
Expression::MemberExpr { object, property } => lower_member_expr(object, property, ctx),
Expression::BinaryExpr { op, left, right } => lower_binary_expr(op, left, right, ctx),
Expression::UnaryExpr { op, operand } => lower_unary_expr(op, operand, ctx),
Expression::CallExpr { callee, args } => lower_call_expr(callee, args, ctx),
Expression::TernaryExpr {
condition,
consequent,
alternate,
} => lower_ternary_expr(condition, consequent, alternate, ctx),
Expression::IndexAccess { object, index } => lower_index_access(object, index, ctx),
Expression::IncrementExpr { operand, prefix } => {
lower_increment_expr(operand, *prefix, ctx)
}
Expression::DecrementExpr { operand, prefix } => {
lower_decrement_expr(operand, *prefix, ctx)
}
Expression::ArrayLiteral { elements } => {
let element_refs: Vec<String> = elements
.iter()
.map(|elem| lower_expr_to_ref(elem, ctx))
.collect();
ctx.emit(ANFValue::ArrayLiteral {
elements: element_refs,
})
}
}
}
fn lower_identifier(name: &str, ctx: &mut LoweringContext) -> String {
if name == "this" {
return ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::String("@this".to_string()),
});
}
if ctx.is_param(name) {
return ctx.emit(ANFValue::LoadParam {
name: name.to_string(),
});
}
if ctx.is_local(name) {
return ctx
.get_local_alias(name)
.cloned()
.unwrap_or_else(|| name.to_string());
}
if ctx.is_property(name) {
return ctx.emit(ANFValue::LoadProp {
name: name.to_string(),
});
}
ctx.emit(ANFValue::LoadParam {
name: name.to_string(),
})
}
fn lower_member_expr(
object: &Expression,
property: &str,
ctx: &mut LoweringContext,
) -> String {
if let Expression::Identifier { name } = object {
if name == "this" {
if ctx.is_param(property) {
return ctx.emit(ANFValue::LoadParam {
name: property.to_string(),
});
}
return ctx.emit(ANFValue::LoadProp {
name: property.to_string(),
});
}
}
if let Expression::Identifier { name } = object {
if name == "SigHash" {
let val = match property {
"ALL" => 0x01i64,
"NONE" => 0x02,
"SINGLE" => 0x03,
"FORKID" => 0x40,
"ANYONECANPAY" => 0x80,
_ => 0,
};
return ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::Number(serde_json::Number::from(val)),
});
}
}
let obj_ref = lower_expr_to_ref(object, ctx);
ctx.emit(ANFValue::MethodCall {
object: obj_ref,
method: property.to_string(),
args: Vec::new(),
})
}
fn lower_binary_expr(
op: &BinaryOp,
left: &Expression,
right: &Expression,
ctx: &mut LoweringContext,
) -> String {
let left_ref = lower_expr_to_ref(left, ctx);
let right_ref = lower_expr_to_ref(right, ctx);
let result_type = if op.as_str() == "===" || op.as_str() == "!==" {
if is_byte_typed_expr(left, ctx) || is_byte_typed_expr(right, ctx) {
Some("bytes".to_string())
} else {
None
}
} else if op.as_str() == "&" || op.as_str() == "|" || op.as_str() == "^" {
if is_byte_typed_expr(left, ctx) || is_byte_typed_expr(right, ctx) {
Some("bytes".to_string())
} else {
None
}
} else {
None
};
ctx.emit(ANFValue::BinOp {
op: op.as_str().to_string(),
left: left_ref,
right: right_ref,
result_type,
})
}
fn lower_unary_expr(
op: &UnaryOp,
operand: &Expression,
ctx: &mut LoweringContext,
) -> String {
let operand_ref = lower_expr_to_ref(operand, ctx);
let result_type = if op.as_str() == "~" && is_byte_typed_expr(operand, ctx) {
Some("bytes".to_string())
} else {
None
};
ctx.emit(ANFValue::UnaryOp {
op: op.as_str().to_string(),
operand: operand_ref,
result_type,
})
}
fn lower_call_expr(
callee: &Expression,
args: &[Expression],
ctx: &mut LoweringContext,
) -> String {
if let Expression::Identifier { name } = callee {
if name == "super" {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
return ctx.emit(ANFValue::Call {
func: "super".to_string(),
args: arg_refs,
});
}
}
if let Expression::Identifier { name } = callee {
if name == "assert" {
if !args.is_empty() {
let value_ref = lower_expr_to_ref(&args[0], ctx);
return ctx.emit(ANFValue::Assert { value: value_ref });
}
let false_ref = ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::Bool(false),
});
return ctx.emit(ANFValue::Assert { value: false_ref });
}
}
if let Expression::Identifier { name } = callee {
if name == "checkPreimage" {
if !args.is_empty() {
let preimage_ref = lower_expr_to_ref(&args[0], ctx);
return ctx.emit(ANFValue::CheckPreimage {
preimage: preimage_ref,
});
}
}
}
if let Expression::PropertyAccess { property } = callee {
if property == "addOutput" {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
let satoshis = arg_refs.first().cloned().unwrap_or_default();
let state_values = if arg_refs.len() > 1 { arg_refs[1..].to_vec() } else { Vec::new() };
let r = ctx.emit(ANFValue::AddOutput { satoshis, state_values, preimage: String::new() });
ctx.add_output_refs.push(r.clone());
return r;
}
}
if let Expression::MemberExpr { object, property } = callee {
if let Expression::Identifier { name } = object.as_ref() {
if name == "this" && property == "addOutput" {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
let satoshis = arg_refs.first().cloned().unwrap_or_default();
let state_values = if arg_refs.len() > 1 { arg_refs[1..].to_vec() } else { Vec::new() };
let r = ctx.emit(ANFValue::AddOutput { satoshis, state_values, preimage: String::new() });
ctx.add_output_refs.push(r.clone());
return r;
}
}
}
if let Expression::PropertyAccess { property } = callee {
if property == "addRawOutput" {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
let satoshis = arg_refs.first().cloned().unwrap_or_default();
let script_bytes = if arg_refs.len() > 1 { arg_refs[1].clone() } else { String::new() };
let r = ctx.emit(ANFValue::AddRawOutput { satoshis, script_bytes });
ctx.add_output_refs.push(r.clone());
return r;
}
}
if let Expression::MemberExpr { object, property } = callee {
if let Expression::Identifier { name } = object.as_ref() {
if name == "this" && property == "addRawOutput" {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
let satoshis = arg_refs.first().cloned().unwrap_or_default();
let script_bytes = if arg_refs.len() > 1 { arg_refs[1].clone() } else { String::new() };
let r = ctx.emit(ANFValue::AddRawOutput { satoshis, script_bytes });
ctx.add_output_refs.push(r.clone());
return r;
}
}
}
if let Expression::PropertyAccess { property } = callee {
if property == "getStateScript" {
return ctx.emit(ANFValue::GetStateScript {});
}
}
if let Expression::MemberExpr { object, property } = callee {
if let Expression::Identifier { name } = object.as_ref() {
if name == "this" && property == "getStateScript" {
return ctx.emit(ANFValue::GetStateScript {});
}
}
}
if let Expression::PropertyAccess { property } = callee {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
let this_ref = ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::String("@this".to_string()),
});
return ctx.emit(ANFValue::MethodCall {
object: this_ref,
method: property.clone(),
args: arg_refs,
});
}
if let Expression::MemberExpr { object, property } = callee {
if let Expression::Identifier { name } = object.as_ref() {
if name == "this" {
let arg_refs: Vec<String> =
args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
let this_ref = ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::String("@this".to_string()),
});
return ctx.emit(ANFValue::MethodCall {
object: this_ref,
method: property.clone(),
args: arg_refs,
});
}
}
}
if let Expression::Identifier { name } = callee {
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
return ctx.emit(ANFValue::Call {
func: name.clone(),
args: arg_refs,
});
}
let callee_ref = lower_expr_to_ref(callee, ctx);
let arg_refs: Vec<String> = args.iter().map(|a| lower_expr_to_ref(a, ctx)).collect();
ctx.emit(ANFValue::MethodCall {
object: callee_ref,
method: "call".to_string(),
args: arg_refs,
})
}
fn lower_ternary_expr(
condition: &Expression,
consequent: &Expression,
alternate: &Expression,
ctx: &mut LoweringContext,
) -> String {
let cond_ref = lower_expr_to_ref(condition, ctx);
let mut then_ctx = ctx.sub_context();
lower_expr_to_ref(consequent, &mut then_ctx);
ctx.sync_counter(&then_ctx);
let mut else_ctx = ctx.sub_context();
lower_expr_to_ref(alternate, &mut else_ctx);
ctx.sync_counter(&else_ctx);
ctx.emit(ANFValue::If {
cond: cond_ref,
then: then_ctx.bindings,
else_branch: else_ctx.bindings,
})
}
fn lower_index_access(
object: &Expression,
index: &Expression,
ctx: &mut LoweringContext,
) -> String {
let obj_ref = lower_expr_to_ref(object, ctx);
let index_ref = lower_expr_to_ref(index, ctx);
ctx.emit(ANFValue::Call {
func: "__array_access".to_string(),
args: vec![obj_ref, index_ref],
})
}
fn lower_increment_expr(
operand: &Expression,
prefix: bool,
ctx: &mut LoweringContext,
) -> String {
let operand_ref = lower_expr_to_ref(operand, ctx);
let one_ref = ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::Number(serde_json::Number::from(1i64)),
});
let result = ctx.emit(ANFValue::BinOp {
op: "+".to_string(),
left: operand_ref.clone(),
right: one_ref,
result_type: None,
});
if let Expression::Identifier { name } = operand {
ctx.emit_named(
name,
ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", result)),
},
);
}
if let Expression::PropertyAccess { property } = operand {
ctx.emit(ANFValue::UpdateProp {
name: property.clone(),
value: result.clone(),
});
}
if prefix {
result
} else {
operand_ref
}
}
fn lower_decrement_expr(
operand: &Expression,
prefix: bool,
ctx: &mut LoweringContext,
) -> String {
let operand_ref = lower_expr_to_ref(operand, ctx);
let one_ref = ctx.emit(ANFValue::LoadConst {
value: serde_json::Value::Number(serde_json::Number::from(1i64)),
});
let result = ctx.emit(ANFValue::BinOp {
op: "-".to_string(),
result_type: None,
left: operand_ref.clone(),
right: one_ref,
});
if let Expression::Identifier { name } = operand {
ctx.emit_named(
name,
ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", result)),
},
);
}
if let Expression::PropertyAccess { property } = operand {
ctx.emit(ANFValue::UpdateProp {
name: property.clone(),
value: result.clone(),
});
}
if prefix {
result
} else {
operand_ref
}
}
const BYTE_TYPES: &[&str] = &[
"ByteString", "PubKey", "Sig", "Sha256", "Ripemd160", "Addr", "SigHashPreimage",
"RabinSig", "RabinPubKey", "Point",
];
const BYTE_RETURNING_FUNCTIONS: &[&str] = &[
"sha256", "ripemd160", "hash160", "hash256", "cat", "num2bin", "int2str",
"reverseBytes", "substr", "left", "right",
"ecAdd", "ecMul", "ecMulGen", "ecNegate", "ecMakePoint", "ecEncodeCompressed",
"sha256Compress", "sha256Finalize", "blake3Compress", "blake3Hash",
];
fn is_byte_typed_expr(expr: &Expression, ctx: &LoweringContext) -> bool {
match expr {
Expression::ByteStringLiteral { .. } => true,
Expression::Identifier { name } => {
if let Some(t) = get_param_type(name, ctx) {
if BYTE_TYPES.contains(&t.as_str()) {
return true;
}
}
if let Some(t) = get_property_type(name, ctx) {
if BYTE_TYPES.contains(&t.as_str()) {
return true;
}
}
if ctx.local_byte_vars.contains(name.as_str()) {
return true;
}
false
}
Expression::PropertyAccess { property } => {
if let Some(t) = get_property_type(property, ctx) {
if BYTE_TYPES.contains(&t.as_str()) {
return true;
}
}
false
}
Expression::MemberExpr { object, property } => {
if let Expression::Identifier { name } = object.as_ref() {
if name == "this" {
if let Some(t) = get_property_type(property, ctx) {
if BYTE_TYPES.contains(&t.as_str()) {
return true;
}
}
}
}
false
}
Expression::CallExpr { callee, .. } => {
if let Expression::Identifier { name } = callee.as_ref() {
if BYTE_RETURNING_FUNCTIONS.contains(&name.as_str()) {
return true;
}
}
false
}
_ => false,
}
}
fn get_param_type(name: &str, ctx: &LoweringContext) -> Option<String> {
ctx.method_param_types.get(name).cloned()
}
fn get_property_type(name: &str, ctx: &LoweringContext) -> Option<String> {
for p in &ctx.contract.properties {
if p.name == name {
return Some(type_node_to_string(&p.prop_type));
}
}
None
}
fn type_node_to_string(node: &TypeNode) -> String {
match node {
TypeNode::Primitive(name) => name.as_str().to_string(),
TypeNode::FixedArray { element, length } => {
format!("FixedArray<{}, {}>", type_node_to_string(element), length)
}
TypeNode::Custom(name) => name.clone(),
}
}
fn method_mutates_state(method: &MethodNode, contract: &ContractNode) -> bool {
let mutable_prop_names: HashSet<String> = contract
.properties
.iter()
.filter(|p| !p.readonly)
.map(|p| p.name.clone())
.collect();
if mutable_prop_names.is_empty() {
return false;
}
body_mutates_state(&method.body, &mutable_prop_names)
}
fn body_mutates_state(stmts: &[Statement], mutable_props: &HashSet<String>) -> bool {
for stmt in stmts {
if stmt_mutates_state(stmt, mutable_props) {
return true;
}
}
false
}
fn stmt_mutates_state(stmt: &Statement, mutable_props: &HashSet<String>) -> bool {
match stmt {
Statement::Assignment { target, .. } => {
if let Expression::PropertyAccess { property } = target {
if mutable_props.contains(property) {
return true;
}
}
false
}
Statement::ExpressionStatement { expression, .. } => {
expr_mutates_state(expression, mutable_props)
}
Statement::IfStatement {
then_branch,
else_branch,
..
} => {
body_mutates_state(then_branch, mutable_props)
|| else_branch
.as_ref()
.map_or(false, |e| body_mutates_state(e, mutable_props))
}
Statement::ForStatement { body, .. } => body_mutates_state(body, mutable_props),
_ => false,
}
}
fn expr_mutates_state(expr: &Expression, mutable_props: &HashSet<String>) -> bool {
match expr {
Expression::IncrementExpr { operand, .. } | Expression::DecrementExpr { operand, .. } => {
if let Expression::PropertyAccess { property } = operand.as_ref() {
if mutable_props.contains(property) {
return true;
}
}
false
}
_ => false,
}
}
fn method_has_add_output(method: &MethodNode) -> bool {
body_has_add_output(&method.body)
}
fn body_has_add_output(stmts: &[Statement]) -> bool {
for stmt in stmts {
if stmt_has_add_output(stmt) {
return true;
}
}
false
}
fn stmt_has_add_output(stmt: &Statement) -> bool {
match stmt {
Statement::ExpressionStatement { expression, .. } => expr_has_add_output(expression),
Statement::IfStatement {
then_branch,
else_branch,
..
} => {
body_has_add_output(then_branch)
|| else_branch
.as_ref()
.map_or(false, |e| body_has_add_output(e))
}
Statement::ForStatement { body, .. } => body_has_add_output(body),
_ => false,
}
}
fn expr_has_add_output(expr: &Expression) -> bool {
if let Expression::CallExpr { callee, .. } = expr {
if let Expression::PropertyAccess { property } = callee.as_ref() {
if property == "addOutput" || property == "addRawOutput" {
return true;
}
}
if let Expression::MemberExpr { object, property } = callee.as_ref() {
if let Expression::Identifier { name } = object.as_ref() {
if name == "this" && (property == "addOutput" || property == "addRawOutput") {
return true;
}
}
}
}
false
}
struct UpdateBranch {
cond_setup_bindings: Vec<ANFBinding>,
cond_ref: Option<String>,
prop_name: String,
value_bindings: Vec<ANFBinding>,
#[allow(dead_code)]
value_ref: String,
}
fn max_temp_index(bindings: &[ANFBinding]) -> i64 {
let mut max = -1i64;
for b in bindings {
if b.name.starts_with('t') {
if let Ok(n) = b.name[1..].parse::<i64>() {
if n > max {
max = n;
}
}
}
match &b.value {
ANFValue::If { then, else_branch, .. } => {
let t = max_temp_index(then);
if t > max { max = t; }
let e = max_temp_index(else_branch);
if e > max { max = e; }
}
ANFValue::Loop { body, .. } => {
let l = max_temp_index(body);
if l > max { max = l; }
}
_ => {}
}
}
max
}
fn is_side_effect_free(value: &ANFValue) -> bool {
matches!(
value,
ANFValue::LoadProp { .. }
| ANFValue::LoadParam { .. }
| ANFValue::LoadConst { .. }
| ANFValue::BinOp { .. }
| ANFValue::UnaryOp { .. }
)
}
fn all_bindings_side_effect_free(bindings: &[ANFBinding]) -> bool {
bindings.iter().all(|b| is_side_effect_free(&b.value))
}
fn extract_branch_update(bindings: &[ANFBinding]) -> Option<(String, Vec<ANFBinding>, String)> {
if bindings.is_empty() {
return None;
}
let last = &bindings[bindings.len() - 1];
if let ANFValue::UpdateProp { name: prop_name, value: val_ref } = &last.value {
let value_bindings = bindings[..bindings.len() - 1].to_vec();
if !all_bindings_side_effect_free(&value_bindings) {
return None;
}
Some((prop_name.clone(), value_bindings, val_ref.clone()))
} else {
None
}
}
fn is_assert_false_else(bindings: &[ANFBinding]) -> bool {
if bindings.is_empty() {
return false;
}
let last = &bindings[bindings.len() - 1];
if let ANFValue::Assert { value: assert_ref } = &last.value {
for b in bindings {
if b.name == *assert_ref {
if let ANFValue::LoadConst { value: v } = &b.value {
return v == &serde_json::Value::Bool(false);
}
}
}
}
false
}
fn collect_update_branches(
if_cond: &str,
then_bindings: &[ANFBinding],
else_bindings: &[ANFBinding],
) -> Option<Vec<UpdateBranch>> {
let then_update = extract_branch_update(then_bindings)?;
let mut branches = vec![UpdateBranch {
cond_setup_bindings: Vec::new(),
cond_ref: Some(if_cond.to_string()),
prop_name: then_update.0,
value_bindings: then_update.1,
value_ref: then_update.2,
}];
if else_bindings.is_empty() {
return None;
}
let last_else = &else_bindings[else_bindings.len() - 1];
if let ANFValue::If { cond, then, else_branch } = &last_else.value {
let cond_setup = &else_bindings[..else_bindings.len() - 1];
if !all_bindings_side_effect_free(cond_setup) {
return None;
}
let mut inner_branches = collect_update_branches(cond, then, else_branch)?;
let mut new_setup = cond_setup.to_vec();
new_setup.extend(inner_branches[0].cond_setup_bindings.drain(..));
inner_branches[0].cond_setup_bindings = new_setup;
branches.extend(inner_branches);
return Some(branches);
}
if let Some(else_update) = extract_branch_update(else_bindings) {
branches.push(UpdateBranch {
cond_setup_bindings: Vec::new(),
cond_ref: None,
prop_name: else_update.0,
value_bindings: else_update.1,
value_ref: else_update.2,
});
return Some(branches);
}
if is_assert_false_else(else_bindings) {
return Some(branches);
}
None
}
fn remap_value_refs(value: &ANFValue, map: &HashMap<String, String>) -> ANFValue {
let r = |s: &str| -> String { map.get(s).cloned().unwrap_or_else(|| s.to_string()) };
match value {
ANFValue::LoadParam { .. } | ANFValue::LoadProp { .. } | ANFValue::GetStateScript {} => {
value.clone()
}
ANFValue::LoadConst { value: v } => {
if let Some(s) = v.as_str() {
if s.starts_with("@ref:") {
let target = &s[5..];
if let Some(remapped) = map.get(target) {
return ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", remapped)),
};
}
}
}
value.clone()
}
ANFValue::BinOp { op, left, right, result_type } => ANFValue::BinOp {
op: op.clone(),
left: r(left),
right: r(right),
result_type: result_type.clone(),
},
ANFValue::UnaryOp { op, operand, result_type } => ANFValue::UnaryOp {
op: op.clone(),
operand: r(operand),
result_type: result_type.clone(),
},
ANFValue::Call { func, args } => ANFValue::Call {
func: func.clone(),
args: args.iter().map(|a| r(a)).collect(),
},
ANFValue::MethodCall { object, method, args } => ANFValue::MethodCall {
object: r(object),
method: method.clone(),
args: args.iter().map(|a| r(a)).collect(),
},
ANFValue::Assert { value: v } => ANFValue::Assert { value: r(v) },
ANFValue::UpdateProp { name, value: v } => ANFValue::UpdateProp {
name: name.clone(),
value: r(v),
},
ANFValue::CheckPreimage { preimage } => ANFValue::CheckPreimage {
preimage: r(preimage),
},
ANFValue::DeserializeState { preimage } => ANFValue::DeserializeState {
preimage: r(preimage),
},
ANFValue::AddOutput { satoshis, state_values, preimage } => ANFValue::AddOutput {
satoshis: r(satoshis),
state_values: state_values.iter().map(|a| r(a)).collect(),
preimage: r(preimage),
},
ANFValue::AddRawOutput { satoshis, script_bytes } => ANFValue::AddRawOutput {
satoshis: r(satoshis),
script_bytes: r(script_bytes),
},
ANFValue::ArrayLiteral { elements } => ANFValue::ArrayLiteral {
elements: elements.iter().map(|e| r(e)).collect(),
},
ANFValue::If { cond, then, else_branch } => ANFValue::If {
cond: r(cond),
then: then.clone(),
else_branch: else_branch.clone(),
},
ANFValue::Loop { count, body, iter_var } => ANFValue::Loop {
count: *count,
body: body.clone(),
iter_var: iter_var.clone(),
},
}
}
fn lift_branch_update_props(bindings: Vec<ANFBinding>) -> Vec<ANFBinding> {
let mut next_idx = (max_temp_index(&bindings) + 1) as usize;
let mut fresh = || -> String {
let name = format!("t{}", next_idx);
next_idx += 1;
name
};
let mut result: Vec<ANFBinding> = Vec::new();
for binding in &bindings {
let if_val = match &binding.value {
ANFValue::If { cond, then, else_branch } => Some((cond, then, else_branch)),
_ => None,
};
if if_val.is_none() {
result.push(binding.clone());
continue;
}
let (cond, then_bindings, else_bindings) = if_val.unwrap();
let branches = collect_update_branches(cond, then_bindings, else_bindings);
if branches.is_none() || branches.as_ref().map_or(true, |b| b.len() < 2) {
result.push(binding.clone());
continue;
}
let branches = branches.unwrap();
let mut name_map: HashMap<String, String> = HashMap::new();
let mut cond_refs: Vec<Option<String>> = Vec::new();
for branch in &branches {
for csb in &branch.cond_setup_bindings {
let new_name = fresh();
name_map.insert(csb.name.clone(), new_name.clone());
result.push(ANFBinding {
name: new_name,
value: remap_value_refs(&csb.value, &name_map),
source_loc: None,
});
}
cond_refs.push(
branch.cond_ref.as_ref().map(|cr| {
name_map.get(cr).cloned().unwrap_or_else(|| cr.clone())
}),
);
}
let mut effective_conds: Vec<String> = Vec::new();
let mut negated_conds: Vec<String> = Vec::new();
for i in 0..branches.len() {
if i == 0 {
effective_conds.push(cond_refs[0].clone().unwrap());
continue;
}
for j in negated_conds.len()..i {
if cond_refs[j].is_none() {
continue;
}
let neg_name = fresh();
result.push(ANFBinding {
name: neg_name.clone(),
value: ANFValue::UnaryOp {
op: "!".to_string(),
operand: cond_refs[j].clone().unwrap(),
result_type: None,
},
source_loc: None,
});
negated_conds.push(neg_name);
}
let mut and_ref = negated_conds[0].clone();
for j in 1..std::cmp::min(i, negated_conds.len()) {
let and_name = fresh();
result.push(ANFBinding {
name: and_name.clone(),
value: ANFValue::BinOp {
op: "&&".to_string(),
left: and_ref,
right: negated_conds[j].clone(),
result_type: None,
},
source_loc: None,
});
and_ref = and_name;
}
if cond_refs[i].is_some() {
let final_name = fresh();
result.push(ANFBinding {
name: final_name.clone(),
value: ANFValue::BinOp {
op: "&&".to_string(),
left: and_ref,
right: cond_refs[i].clone().unwrap(),
result_type: None,
},
source_loc: None,
});
effective_conds.push(final_name);
} else {
effective_conds.push(and_ref);
}
}
for (i, branch) in branches.iter().enumerate() {
let old_prop_ref = fresh();
result.push(ANFBinding {
name: old_prop_ref.clone(),
value: ANFValue::LoadProp {
name: branch.prop_name.clone(),
},
source_loc: None,
});
let mut branch_map = name_map.clone();
let mut then_bindings: Vec<ANFBinding> = Vec::new();
for vb in &branch.value_bindings {
let new_name = fresh();
branch_map.insert(vb.name.clone(), new_name.clone());
then_bindings.push(ANFBinding {
name: new_name,
value: remap_value_refs(&vb.value, &branch_map),
source_loc: None,
});
}
let keep_name = fresh();
let else_bindings = vec![ANFBinding {
name: keep_name,
value: ANFValue::LoadConst {
value: serde_json::Value::String(format!("@ref:{}", old_prop_ref)),
},
source_loc: None,
}];
let cond_if_ref = fresh();
result.push(ANFBinding {
name: cond_if_ref.clone(),
value: ANFValue::If {
cond: effective_conds[i].clone(),
then: then_bindings,
else_branch: else_bindings,
},
source_loc: None,
});
result.push(ANFBinding {
name: fresh(),
value: ANFValue::UpdateProp {
name: branch.prop_name.clone(),
value: cond_if_ref,
},
source_loc: None,
});
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frontend::parser::parse_source;
use crate::frontend::typecheck::typecheck;
use crate::frontend::validator::validate;
fn must_lower_to_anf(source: &str) -> ContractNode {
let result = parse_source(source, Some("test.runar.ts"));
assert!(
result.errors.is_empty(),
"parse errors: {:?}",
result.errors
);
let contract = result.contract.expect("expected a contract from parse");
let val_result = validate(&contract);
assert!(
val_result.errors.is_empty(),
"validation errors: {:?}",
val_result.errors
);
let tc_result = typecheck(&contract);
assert!(
tc_result.errors.is_empty(),
"type check errors: {:?}",
tc_result.errors
);
contract
}
#[test]
fn test_p2pkh_has_properties() {
let source = r#"
import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';
class P2PKH extends SmartContract {
readonly pubKeyHash: Addr;
constructor(pubKeyHash: Addr) {
super(pubKeyHash);
this.pubKeyHash = pubKeyHash;
}
public unlock(sig: Sig, pubKey: PubKey): void {
assert(hash160(pubKey) === this.pubKeyHash);
assert(checkSig(sig, pubKey));
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
assert_eq!(program.contract_name, "P2PKH");
assert_eq!(
program.properties.len(),
1,
"expected 1 property, got {}",
program.properties.len()
);
let prop = &program.properties[0];
assert_eq!(
prop.name, "pubKeyHash",
"expected property name 'pubKeyHash', got '{}'",
prop.name
);
assert_eq!(
prop.prop_type, "Addr",
"expected property type 'Addr', got '{}'",
prop.prop_type
);
assert!(prop.readonly, "expected property to be readonly");
}
#[test]
fn test_p2pkh_unlock_has_bindings() {
let source = r#"
import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';
class P2PKH extends SmartContract {
readonly pubKeyHash: Addr;
constructor(pubKeyHash: Addr) {
super(pubKeyHash);
this.pubKeyHash = pubKeyHash;
}
public unlock(sig: Sig, pubKey: PubKey): void {
assert(hash160(pubKey) === this.pubKeyHash);
assert(checkSig(sig, pubKey));
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
let unlock = program
.methods
.iter()
.find(|m| m.name == "unlock")
.expect("could not find 'unlock' method in ANF output");
assert!(unlock.is_public, "expected unlock method to be public");
assert_eq!(
unlock.params.len(),
2,
"expected 2 params (sig, pubKey), got {}",
unlock.params.len()
);
assert_eq!(unlock.params[0].name, "sig");
assert_eq!(unlock.params[0].param_type, "Sig");
assert_eq!(unlock.params[1].name, "pubKey");
assert_eq!(unlock.params[1].param_type, "PubKey");
let mut load_param_count = 0usize;
let mut call_count = 0usize;
let mut load_prop_count = 0usize;
let mut bin_op_count = 0usize;
let mut assert_count = 0usize;
for b in &unlock.body {
match &b.value {
ANFValue::LoadParam { .. } => load_param_count += 1,
ANFValue::LoadProp { .. } => load_prop_count += 1,
ANFValue::Call { .. } => call_count += 1,
ANFValue::BinOp { .. } => bin_op_count += 1,
ANFValue::Assert { .. } => assert_count += 1,
_ => {}
}
}
assert!(
load_param_count >= 2,
"expected at least 2 load_param bindings, got {}",
load_param_count
);
assert!(
call_count >= 2,
"expected at least 2 call bindings (hash160, checkSig), got {}",
call_count
);
assert!(
load_prop_count >= 1,
"expected at least 1 load_prop binding (pubKeyHash), got {}",
load_prop_count
);
assert!(
bin_op_count >= 1,
"expected at least 1 bin_op binding (===), got {}",
bin_op_count
);
assert!(
assert_count >= 2,
"expected at least 2 assert bindings, got {}",
assert_count
);
}
#[test]
fn test_p2pkh_binding_details() {
let source = r#"
import { SmartContract, assert, PubKey, Sig, Addr, hash160, checkSig } from 'runar-lang';
class P2PKH extends SmartContract {
readonly pubKeyHash: Addr;
constructor(pubKeyHash: Addr) {
super(pubKeyHash);
this.pubKeyHash = pubKeyHash;
}
public unlock(sig: Sig, pubKey: PubKey): void {
assert(hash160(pubKey) === this.pubKeyHash);
assert(checkSig(sig, pubKey));
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
let unlock = program
.methods
.iter()
.find(|m| m.name == "unlock")
.expect("could not find 'unlock' method");
let hash160_binding = unlock.body.iter().find(|b| {
matches!(&b.value, ANFValue::Call { func, .. } if func == "hash160")
});
assert!(
hash160_binding.is_some(),
"expected a call to hash160 in unlock method bindings"
);
if let Some(b) = hash160_binding {
if let ANFValue::Call { args, .. } = &b.value {
assert_eq!(
args.len(),
1,
"hash160 should have 1 arg, got {}",
args.len()
);
}
}
let checksig_binding = unlock.body.iter().find(|b| {
matches!(&b.value, ANFValue::Call { func, .. } if func == "checkSig")
});
assert!(
checksig_binding.is_some(),
"expected a call to checkSig in unlock method bindings"
);
if let Some(b) = checksig_binding {
if let ANFValue::Call { args, .. } = &b.value {
assert_eq!(
args.len(),
2,
"checkSig should have 2 args, got {}",
args.len()
);
}
}
let eq_binding = unlock.body.iter().find(|b| {
matches!(&b.value, ANFValue::BinOp { op, .. } if op == "===")
});
assert!(
eq_binding.is_some(),
"expected a bin_op === in unlock method bindings"
);
if let Some(b) = eq_binding {
if let ANFValue::BinOp { result_type, .. } = &b.value {
assert_eq!(
result_type.as_deref(),
Some("bytes"),
"expected bin_op === to have result_type 'bytes' (byte-typed equality), got {:?}",
result_type
);
}
}
}
#[test]
fn test_constructor_included() {
let source = r#"
import { SmartContract, assert } from 'runar-lang';
class Simple extends SmartContract {
readonly x: bigint;
constructor(x: bigint) {
super(x);
this.x = x;
}
public check(val: bigint): void {
assert(val === this.x);
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
assert!(
program.methods.len() >= 2,
"expected at least 2 methods (constructor + check), got {}",
program.methods.len()
);
let ctor = &program.methods[0];
assert_eq!(
ctor.name, "constructor",
"expected first method to be 'constructor', got '{}'",
ctor.name
);
assert!(!ctor.is_public, "constructor should not be public");
}
#[test]
fn test_arithmetic_bindings() {
let source = r#"
import { SmartContract, assert } from 'runar-lang';
class ArithTest extends SmartContract {
readonly target: bigint;
constructor(target: bigint) {
super(target);
this.target = target;
}
public verify(a: bigint, b: bigint): void {
assert(a + b === this.target);
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
let verify = program
.methods
.iter()
.find(|m| m.name == "verify")
.expect("could not find 'verify' method");
let add_binding = verify.body.iter().find(|b| {
matches!(&b.value, ANFValue::BinOp { op, .. } if op == "+")
});
assert!(
add_binding.is_some(),
"expected bin_op + in verify method for 'a + b'"
);
let eq_binding = verify.body.iter().find(|b| {
matches!(&b.value, ANFValue::BinOp { op, .. } if op == "===")
});
assert!(
eq_binding.is_some(),
"expected bin_op === in verify method"
);
}
#[test]
fn test_if_else_lowering() {
let source = r#"
import { SmartContract, assert } from 'runar-lang';
class IfElse extends SmartContract {
readonly limit: bigint;
constructor(limit: bigint) {
super(limit);
this.limit = limit;
}
public check(value: bigint, mode: boolean): void {
let result: bigint = 0n;
if (mode) {
result = value + this.limit;
} else {
result = value - this.limit;
}
assert(result > 0n);
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
let check = program
.methods
.iter()
.find(|m| m.name == "check")
.expect("could not find 'check' method");
let has_if_binding = check
.body
.iter()
.any(|b| matches!(b.value, ANFValue::If { .. }));
assert!(
has_if_binding,
"expected an 'if' binding in the ANF output for the if/else construct, got: {:?}",
check.body.iter().map(|b| format!("{:?}", b.value)).collect::<Vec<_>>()
);
}
#[test]
fn test_stateful_has_implicit_params() {
let source = r#"
import { StatefulSmartContract, assert } from 'runar-lang';
class Counter extends StatefulSmartContract {
count: bigint;
constructor(count: bigint) {
super(count);
this.count = count;
}
public increment(amount: bigint): void {
this.count = this.count + amount;
assert(this.count > 0n);
}
}
"#;
let contract = must_lower_to_anf(source);
let program = lower_to_anf(&contract);
let increment = program
.methods
.iter()
.find(|m| m.name == "increment")
.expect("could not find 'increment' method");
let param_names: Vec<&str> = increment.params.iter().map(|p| p.name.as_str()).collect();
assert!(
param_names.contains(&"txPreimage"),
"stateful method should have 'txPreimage' as an implicit param, got: {:?}",
param_names
);
assert!(
param_names.contains(&"_changePKH"),
"stateful method should have '_changePKH' as an implicit param, got: {:?}",
param_names
);
assert!(
param_names.contains(&"_changeAmount"),
"stateful method should have '_changeAmount' as an implicit param, got: {:?}",
param_names
);
}
}