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
27 changes: 24 additions & 3 deletions silverscript-lang/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,9 +989,12 @@ fn format_expr_with_prec(expr: &Expr<'_>, parent_prec: u8, right_child: bool) ->
binary_op_str(*op),
format_expr_with_prec(right, binary_precedence(*op), true)
),
ExprKind::IfElse { condition, then_expr, else_expr } => {
format!("ifElse({}, {}, {})", format_expr(condition), format_expr(then_expr), format_expr(else_expr))
}
ExprKind::IfElse { condition, then_expr, else_expr } => format!(
"{} ? {} : {}",
format_expr_with_prec(condition, binary_precedence(BinaryOp::Or), false),
format_expr(then_expr),
format_expr_with_prec(else_expr, expr_precedence(&expr.kind), false)
),
ExprKind::Nullary(op) => nullary_op_str(*op).to_string(),
ExprKind::Introspection { kind, index, .. } => {
format!("{}[{}]{}", introspection_root(*kind), format_expr(index), introspection_field(*kind))
Expand Down Expand Up @@ -1796,6 +1799,7 @@ fn parse_state_typed_binding<'i>(pair: Pair<'i, Rule>) -> Result<StateBindingAst
fn parse_expression<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError> {
match pair.as_rule() {
Rule::expression => parse_expression(single_inner(pair)?),
Rule::conditional => parse_conditional(pair),
Rule::logical_or => parse_infix(pair, parse_expression, map_logical_or),
Rule::logical_and => parse_infix(pair, parse_expression, map_logical_and),
Rule::bit_or => parse_infix(pair, parse_expression, map_bit_or),
Expand Down Expand Up @@ -1839,6 +1843,23 @@ fn parse_expression<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError>
}
}

fn parse_conditional<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError> {
let span = Span::from(pair.as_span());
let mut inner = pair.into_inner();
let condition_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing conditional condition".to_string()))?;
let condition = parse_expression(condition_pair)?;
let Some(then_pair) = inner.next() else {
return Ok(condition);
};
let then_expr = parse_expression(then_pair)?;
let else_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing conditional else expression".to_string()))?;
let else_expr = parse_expression(else_pair)?;
Ok(Expr::new(
ExprKind::IfElse { condition: Box::new(condition), then_expr: Box::new(then_expr), else_expr: Box::new(else_expr) },
span,
))
}

fn parse_unary<'i>(pair: Pair<'i, Rule>) -> Result<Expr<'i>, CompilerError> {
let span = Span::from(pair.as_span());
let mut inner = pair.into_inner();
Expand Down
48 changes: 22 additions & 26 deletions silverscript-lang/src/compiler/inline_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ impl<'i, 'd> Inliner<'i, 'd> {
}

fn inline_target(&self, name: &str) -> Option<FunctionAst<'i>> {
self.functions.get(name).cloned().filter(|function| !function.entrypoint)
self.functions.get(name).cloned().filter(|function| !function.entrypoint) // TODO: Store this information in a separate set for efficiency
}

fn inline_call(
Expand Down Expand Up @@ -410,36 +410,32 @@ impl<'i, 'd> Inliner<'i, 'd> {
None => (&[][..], None),
};

let lowered_result = (|| -> Result<(), CompilerError> {
for statement in callee_body {
lowered.extend(self.lower_statement(statement, &mut local_scope, visited_functions)?);
}
for statement in callee_body {
lowered.extend(self.lower_statement(statement, &mut local_scope, visited_functions)?);
}

if let (Some(bindings), Some(return_exprs)) = (bindings, return_exprs) {
for (binding, expr) in bindings.iter().zip(return_exprs.iter()) {
let (prelude, renamed_expr) = self.lower_expr(expr, &local_scope, visited_functions)?;
lowered.extend(prelude);
self.push_lowered_statement(
&mut lowered,
Statement::VariableDefinition {
type_ref: binding.type_ref.clone(),
modifiers: Vec::new(),
name: binding.name.clone(),
expr: Some(renamed_expr),
span,
type_span: binding.type_span,
modifier_spans: Vec::new(),
name_span: binding.name_span,
},
);
}
if let (Some(bindings), Some(return_exprs)) = (bindings, return_exprs) {
for (binding, expr) in bindings.iter().zip(return_exprs.iter()) {
let (prelude, renamed_expr) = self.lower_expr(expr, &local_scope, visited_functions)?;
lowered.extend(prelude);
self.push_lowered_statement(
&mut lowered,
Statement::VariableDefinition {
type_ref: binding.type_ref.clone(),
modifiers: Vec::new(),
name: binding.name.clone(),
expr: Some(renamed_expr),
span,
type_span: binding.type_span,
modifier_spans: Vec::new(),
name_span: binding.name_span,
},
);
}
}

Ok(())
})();
let body_end_statement_index = self.debug_recorder.current_source_statement_index();
visited_functions.remove(&function.name);
lowered_result?;
self.debug_recorder.finish_inline_source_call(body_end_statement_index);
Ok(lowered)
}
Expand Down
71 changes: 70 additions & 1 deletion silverscript-lang/src/compiler/static_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,35 @@ fn validate_expr_semantics<'i>(
ExprKind::IfElse { condition, then_expr, else_expr } => {
validate_expr_semantics(condition, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?;
validate_expr_semantics(then_expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?;
validate_expr_semantics(else_expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields)
validate_expr_semantics(else_expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?;
let then_type = infer_expr_type_ref_for_comparison_ref(
then_expr,
env,
prefer_env_for_comparison,
types,
structs,
functions,
contract_fields,
);
let else_type = infer_expr_type_ref_for_comparison_ref(
else_expr,
env,
prefer_env_for_comparison,
types,
structs,
functions,
contract_fields,
);
if let (Some(then_type), Some(else_type)) = (then_type, else_type)
&& then_type != else_type
{
return Err(CompilerError::Unsupported(format!(
"ternary branch type mismatch: then expression is {}, else expression is {}",
type_name_from_ref(&then_type),
type_name_from_ref(&else_type)
)));
}
Ok(())
}
ExprKind::Array(values) => {
for value in values {
Expand Down Expand Up @@ -946,6 +974,27 @@ fn infer_expr_type_ref_for_comparison_ref<'i>(
ExprKind::Append { source, .. } => {
infer_expr_type_ref_for_comparison_ref(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields)
}
ExprKind::IfElse { then_expr, else_expr, .. } => {
let then_type = infer_expr_type_ref_for_comparison_ref(
then_expr,
env,
prefer_env_for_comparison,
types,
structs,
functions,
contract_fields,
)?;
let else_type = infer_expr_type_ref_for_comparison_ref(
else_expr,
env,
prefer_env_for_comparison,
types,
structs,
functions,
contract_fields,
)?;
(then_type == else_type).then_some(then_type)
}
ExprKind::Call { name, .. } if name == "readInputState" && !contract_fields.is_empty() => {
Some(TypeRef { base: TypeBase::Custom("State".to_string()), array_dims: Vec::new() })
}
Expand Down Expand Up @@ -1176,6 +1225,17 @@ fn validate_expr_assignable_to_type<'i>(
return Ok(());
}

if let ExprKind::IfElse { .. } = &expr.kind
&& let Some(actual_type) =
infer_expr_type_ref_for_comparison_ref(expr, &HashMap::new(), &HashSet::new(), types, structs, functions, contract_fields)
{
return if is_type_assignable_ref(&actual_type, type_ref, constants) {
Ok(())
} else {
Err(CompilerError::Unsupported("type mismatch".to_string()))
};
}

if type_ref.is_array()
&& let ExprKind::Array(values) = &expr.kind
{
Expand Down Expand Up @@ -1451,6 +1511,15 @@ pub(super) fn expr_matches_return_type_ref<'i>(
}

match &expr.kind {
ExprKind::IfElse { .. } => {
if let Ok(actual_type_name) =
super::debug_value_types::infer_debug_expr_value_type(expr, &HashMap::new(), types, &mut HashSet::new())
&& let Ok(actual_type) = parse_type_ref(&actual_type_name)
{
return is_type_assignable_ref(&actual_type, type_ref, constants);
}
false
}
ExprKind::Array(values) => {
expr_matches_declared_type_ref(expr, type_ref, structs)
|| (is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref))
Expand Down
3 changes: 2 additions & 1 deletion silverscript-lang/src/silverscript.pest
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ console_parameter_list = { "(" ~ (expression ~ ("," ~ expression)* ~ ","?)? ~ ")
function_call = { Identifier ~ expression_list }
expression_list = { "(" ~ (expression ~ ("," ~ expression)* ~ ","?)? ~ ")" }

expression = _{ logical_or }
expression = _{ conditional }

conditional = { logical_or ~ ("?" ~ expression ~ ":" ~ conditional)? }
logical_or = { logical_and ~ (logical_or_op ~ logical_and)* }
logical_and = { bit_or ~ (logical_and_op ~ bit_or)* }
bit_or = { bit_xor ~ (bit_or_op ~ bit_xor)* }
Expand Down
95 changes: 94 additions & 1 deletion silverscript-lang/tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use kaspa_txscript::{
EngineCtx, EngineFlags, SeqCommitAccessor, TxScriptEngine, parse_script, pay_to_address_script, pay_to_script_hash_script,
pay_to_script_hash_signature_script, script_to_str, serialize_i64,
};
use silverscript_lang::ast::{Expr, ExprKind, format_contract_ast, parse_contract_ast};
use silverscript_lang::ast::{Expr, ExprKind, Statement, format_contract_ast, parse_contract_ast};
use silverscript_lang::compiler::{
CompileOptions, CompiledContract, CovenantDeclCallOptions, FunctionAbiEntry, FunctionInputAbi, compile_contract,
compile_contract_ast, function_branch_index, struct_object,
Expand Down Expand Up @@ -8590,6 +8590,99 @@ fn function_param_shadows_constructor_constant_with_same_name() {
assert!(result_wrong.is_err(), "require(3==4) should fail, proving the param value matters");
}

#[test]
fn ternary_syntax_lowers_to_if_else_expr() {
let source = r#"
contract TernaryAst() {
entrypoint function main(bool flag) {
int value = flag ? 7 : 11;
require(value > 0);
}
}
"#;

let contract = parse_contract_ast(source).expect("contract parses");
let Statement::VariableDefinition { expr: Some(expr), .. } = &contract.functions[0].body[0] else {
panic!("expected variable definition");
};
assert!(matches!(&expr.kind, ExprKind::IfElse { .. }), "ternary should lower to ExprKind::IfElse: {expr:?}");
}

#[test]
fn ternary_expression_executes_selected_branch() {
let source = r#"
contract TernaryRuntime() {
entrypoint function main(int selector, int expected) {
int value = selector > 0 ? 7 : 11;
require(value == expected);
}
}
"#;

let compiled = compile_contract(source, &[], CompileOptions::default()).expect("ternary contract should compile");

let sigscript_then = compiled.build_sig_script("main", vec![Expr::int(1), Expr::int(7)]).expect("sigscript builds");
let result_then = run_script_with_sigscript(compiled.script.clone(), sigscript_then);
assert!(result_then.is_ok(), "then branch should execute successfully: {}", result_then.unwrap_err());

let sigscript_else = compiled.build_sig_script("main", vec![Expr::int(0), Expr::int(11)]).expect("sigscript builds");
let result_else = run_script_with_sigscript(compiled.script.clone(), sigscript_else);
assert!(result_else.is_ok(), "else branch should execute successfully: {}", result_else.unwrap_err());

let sigscript_wrong = compiled.build_sig_script("main", vec![Expr::int(0), Expr::int(7)]).expect("sigscript builds");
let result_wrong = run_script_with_sigscript(compiled.script, sigscript_wrong);
assert!(result_wrong.is_err(), "else branch should not produce the then value");
}

#[test]
fn ternary_expression_rejects_mismatched_branch_types() {
let source = r#"
contract TernaryTypes() {
entrypoint function main(bool flag) {
int value = flag ? 7 : false;
require(value > 0);
}
}
"#;

let err = compile_contract(source, &[], CompileOptions::default()).expect_err("mismatched ternary branches should fail");
assert!(err.to_string().contains("ternary branch type mismatch"), "unexpected error: {err}");
}

#[test]
fn ternary_expression_rejects_branch_type_that_does_not_match_declared_variable_type() {
let source = r#"
contract TernaryDeclaredType() {
entrypoint function main(bool cond, bool y, bool z) {
int x = cond ? y : z;
require(x > 0);
}
}
"#;

let err = compile_contract(source, &[], CompileOptions::default()).expect_err("ternary result type should match declared type");
assert!(err.to_string().contains("variable 'x' expects int"), "unexpected error: {err}");
}

#[test]
fn ternary_expression_rejects_branch_type_that_does_not_match_function_return_type() {
let source = r#"
contract TernaryReturnType() {
function choose(bool cond, bool y, bool z): int {
return cond ? y : z;
}

entrypoint function main(bool cond, bool y, bool z) {
int value = choose(cond, y, z);
require(value > 0);
}
}
"#;

let err = compile_contract(source, &[], CompileOptions::default()).expect_err("ternary result type should match return type");
assert!(err.to_string().contains("return value expects int"), "unexpected error: {err}");
}

#[test]
fn nested_inline_calls_with_args_compile_and_execute() {
// Nested inline calls must propagate synthetic __arg_ bindings so that
Expand Down