Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions silverscript-lang/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use crate::ast::{
pub use crate::errors::{CompilerError, ErrorSpan};
use crate::span;

/// Prefix used for synthetic argument bindings during inline function expansion.
pub const SYNTHETIC_ARG_PREFIX: &str = "__arg";

#[derive(Debug, Clone, Copy, Default)]
pub struct CompileOptions {
pub allow_yield: bool,
Expand Down Expand Up @@ -927,6 +930,10 @@ fn compile_function<'i>(
}
}
let mut env: HashMap<String, Expr<'i>> = constants.clone();
// Remove any constructor/constant names that collide with function param names (prioritizing function parameters on name collision).
for param in &function.params {
env.remove(&param.name);
}
let mut builder = ScriptBuilder::new();
let mut yields: Vec<Expr> = Vec::new();

Expand Down Expand Up @@ -1296,6 +1303,7 @@ fn compile_statement<'i>(
let returns = compile_inline_call(
name,
args,
params,
types,
env,
builder,
Expand Down Expand Up @@ -1362,6 +1370,7 @@ fn compile_statement<'i>(
let returns = compile_inline_call(
name,
args,
params,
types,
env,
builder,
Expand Down Expand Up @@ -1715,6 +1724,7 @@ fn compile_validate_output_state_statement(
fn compile_inline_call<'i>(
name: &str,
args: &[Expr<'i>],
params: &HashMap<String, i64>,
caller_types: &mut HashMap<String, String>,
caller_env: &mut HashMap<String, Expr<'i>>,
builder: &mut ScriptBuilder,
Expand Down Expand Up @@ -1752,9 +1762,15 @@ fn compile_inline_call<'i>(
}

let mut env: HashMap<String, Expr<'i>> = contract_constants.clone();
// Copy the caller's __arg_ (function param) bindings into the new inline call's env, allowing nested synthetic argument chain.
for (name, value) in caller_env.iter() {
if name.starts_with(SYNTHETIC_ARG_PREFIX) {
env.insert(name.clone(), value.clone());
}
}
for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() {
let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?;
let temp_name = format!("__arg_{name}_{index}");
let temp_name = format!("{SYNTHETIC_ARG_PREFIX}_{name}_{index}");
let param_type_name = type_name_from_ref(&param.type_ref);
env.insert(temp_name.clone(), resolved.clone());
types.insert(temp_name.clone(), param_type_name.clone());
Expand Down Expand Up @@ -1785,7 +1801,6 @@ fn compile_inline_call<'i>(
}

let mut yields: Vec<Expr<'i>> = Vec::new();
let params = HashMap::new();
let body_len = function.body.len();
for (index, stmt) in function.body.iter().enumerate() {
if let Statement::Return { exprs, .. } = stmt {
Expand All @@ -1803,7 +1818,7 @@ fn compile_inline_call<'i>(
compile_statement(
stmt,
&mut env,
&params,
params,
&mut types,
builder,
options,
Expand All @@ -1820,7 +1835,7 @@ fn compile_inline_call<'i>(
}

for (name, value) in env.iter() {
if name.starts_with("__arg_") {
if name.starts_with(SYNTHETIC_ARG_PREFIX) {
if let Some(type_name) = types.get(name) {
caller_types.entry(name.clone()).or_insert_with(|| type_name.clone());
}
Expand Down Expand Up @@ -2112,7 +2127,7 @@ fn resolve_expr<'i>(
let Expr { kind, span } = expr;
match kind {
ExprKind::Identifier(name) => {
if name.starts_with("__arg_") {
if name.starts_with(SYNTHETIC_ARG_PREFIX) {
return Ok(Expr::new(ExprKind::Identifier(name), span));
}
if let Some(value) = env.get(&name) {
Expand Down
59 changes: 59 additions & 0 deletions silverscript-lang/tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3559,3 +3559,62 @@ fn empty_array_statement_expr_evaluation_compiles_to_empty_array_data() {
assert_eq!(compiled.script[0], OpFalse);
assert_eq!(compiled.script[1], OpFalse);
}

#[test]
fn function_param_shadows_constructor_constant_with_same_name() {
// When a constructor constant and a function parameter share the same name,
// the function parameter value must be used (not the constant).
let source = r#"
contract Shadow(int fee) {
entrypoint function main(int fee) {
Comment on lines +3565 to +3569
Copy link
Collaborator

@IzioDev IzioDev Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this even be disallowed to reduce confusion?
Scope precedence is unclear

Bonus: how should contract-level state be treated as-well

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very common in most modern programming languages.

int local = fee + 1;
require(local == 4);
}
}
"#;

// Constructor fee=2, param fee=3 => local = 3+1 = 4 => pass
let compiled = compile_contract(source, &[Expr::int(2)], CompileOptions::default()).expect("compile succeeds");
let sigscript = compiled.build_sig_script("main", vec![Expr::int(3)]).expect("sigscript builds");
let result = run_script_with_sigscript(compiled.script.clone(), sigscript);
assert!(result.is_ok(), "function param should shadow constructor constant: {}", result.unwrap_err());

// Constructor fee=2, param fee=2 => local = 2+1 = 3 != 4 => fail (proves it's not always the constant)
let sigscript_wrong = compiled.build_sig_script("main", vec![Expr::int(2)]).expect("sigscript builds");
let result_wrong = run_script_with_sigscript(compiled.script, sigscript_wrong);
assert!(result_wrong.is_err(), "require(3==4) should fail, proving the param value matters");
}

#[test]
fn nested_inline_calls_with_args_compile_and_execute() {
// Nested inline calls must propagate synthetic __arg_ bindings so that
// deeply nested calls can resolve arguments that flow through outer calls.
let source = r#"
contract NestedArgs() {
function inner(int x) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add another level? To see the fix didn't push the bug one level further

int y = x + 1;
require(y > 0);
}

function outer(int v) {
inner(v);
require(v >= 0);
}

function top(int z) {
outer(z);
require(z >= 0);
}

entrypoint function main(int a) {
top(a);
require(a >= 0);
}
}
"#;

let compiled = compile_contract(source, &[], CompileOptions::default()).expect("nested inline calls should compile");
let sigscript = compiled.build_sig_script("main", vec![Expr::int(5)]).expect("sigscript builds");
let result = run_script_with_sigscript(compiled.script, sigscript);
assert!(result.is_ok(), "nested inline calls should execute correctly: {}", result.unwrap_err());
}