diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 8c3d473..6696471 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -21,6 +21,7 @@ - [Comparison Operators](#comparison-operators) - [Logical Operators](#logical-operators) - [Bitwise Operators](#bitwise-operators) + - [Ternary Operator](#ternary-operator) 6. [Control Flow](#control-flow) - [If Statements](#if-statements) - [Require Statements](#require-statements) @@ -403,6 +404,26 @@ int bitOr = x | y; // 0xFF (bitwise OR) int bitXor = x ^ y; // 0xFF (bitwise XOR) ``` +### Ternary Operator + +Use the ternary operator to choose between two expressions: + +```javascript +bool condition = true; +int thenValue = 100; +int elseValue = 50; +int value = condition ? thenValue : elseValue; +``` + +The condition must evaluate to `bool`, and both result branches must have the same type. The ternary expression's result must also match the declared type where it is assigned or returned: + +```javascript +entrypoint function example(int amount, bool useBonus) { + int payout = useBonus ? amount + 100 : amount; + require(payout >= amount); +} +``` + --- ## Control Flow diff --git a/silverscript-lang/src/compiler/compile.rs b/silverscript-lang/src/compiler/compile.rs index 3ae8e5e..0e8e1f3 100644 --- a/silverscript-lang/src/compiler/compile.rs +++ b/silverscript-lang/src/compiler/compile.rs @@ -5,6 +5,7 @@ use super::infer_array::lower_inferred_array_sizes; use super::inline_functions::lower_inline_functions; use super::locals::lower_local_aliases; use super::stack_bindings::StackBindings; +use super::static_check::static_check_contract; use super::*; use kaspa_txscript::opcodes::codes::*; use kaspa_txscript::script_builder::ScriptBuilder; @@ -98,14 +99,15 @@ pub(super) fn compile_contract_impl<'i>( } let mut debug_recorder = DebugRecorder::new(options, contract)?; - let covenant_lowered_contract = lower_covenant_declarations(contract, &constants)?; + let inferred_lowered_contract = lower_inferred_array_sizes(contract, &constants)?; + static_check_contract(&inferred_lowered_contract, constructor_args, options)?; + let covenant_lowered_contract = lower_covenant_declarations(&inferred_lowered_contract, &constants)?; let inline_lowered_contract = lower_inline_functions(&covenant_lowered_contract, &mut debug_recorder)?; let structs = build_struct_registry(&inline_lowered_contract)?; let struct_lowered_contract = lower_structs_contract(&inline_lowered_contract, &structs, &constants)?; let append_lowered_contract = lower_array_appends(&struct_lowered_contract)?; let for_lowered_contract = lower_for_loops(&append_lowered_contract, &constants)?; - let lowered_contract = lower_inferred_array_sizes(&for_lowered_contract, &constants)?; - let lowered_contract = if options.record_debug_infos { lowered_contract } else { lower_local_aliases(&lowered_contract)? }; + let lowered_contract = if options.record_debug_infos { for_lowered_contract } else { lower_local_aliases(&for_lowered_contract)? }; let mut lowered_constants = flatten_constructor_args_env(&covenant_lowered_contract.params, constructor_args, &structs)?; lowered_constants.extend(lowered_contract.constants.iter().map(|constant| (constant.name.clone(), constant.expr.clone()))); diff --git a/silverscript-lang/src/compiler/infer_array.rs b/silverscript-lang/src/compiler/infer_array.rs index 63a37d0..f3217cb 100644 --- a/silverscript-lang/src/compiler/infer_array.rs +++ b/silverscript-lang/src/compiler/infer_array.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use super::compile::{array_literal_matches_type_with_env_ref, type_name_from_ref}; +use super::compile::type_name_from_ref; use super::*; use crate::ast::{ArrayDim, ConstantAst, ContractAst, ContractFieldAst, FunctionAst, ParamAst, Statement, TypeRef}; @@ -8,6 +8,8 @@ pub(super) fn lower_inferred_array_sizes<'i>( contract: &ContractAst<'i>, contract_constants: &HashMap>, ) -> Result, CompilerError> { + let functions_by_name: HashMap> = + contract.functions.iter().map(|function| (function.name.clone(), function)).collect(); let mut top_level_types = HashMap::new(); for param in &contract.params { top_level_types.insert(param.name.clone(), type_name_from_ref(¶m.type_ref)); @@ -16,17 +18,17 @@ pub(super) fn lower_inferred_array_sizes<'i>( let constants = contract .constants .iter() - .map(|constant| lower_constant(constant, &mut top_level_types, contract_constants)) + .map(|constant| lower_constant(constant, &mut top_level_types, contract_constants, &functions_by_name)) .collect::, _>>()?; let fields = contract .fields .iter() - .map(|field| lower_field(field, &mut top_level_types, contract_constants)) + .map(|field| lower_field(field, &mut top_level_types, contract_constants, &functions_by_name)) .collect::, _>>()?; let functions = contract .functions .iter() - .map(|function| lower_function(function, &top_level_types, contract_constants)) + .map(|function| lower_function(function, &top_level_types, contract_constants, &functions_by_name)) .collect::, _>>()?; Ok(ContractAst { @@ -42,11 +44,12 @@ pub(super) fn lower_inferred_array_sizes<'i>( }) } -pub(super) fn infer_fixed_array_type_from_initializer_ref<'i>( +fn infer_fixed_array_type_from_initializer_ref<'i>( declared_type: &TypeRef, initializer: Option<&Expr<'i>>, types: &HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Option { if !matches!(declared_type.array_size(), Some(ArrayDim::Inferred)) { return None; @@ -54,33 +57,25 @@ pub(super) fn infer_fixed_array_type_from_initializer_ref<'i>( let element_type = declared_type.element_type()?; let init = initializer?; + let init_type = infer_expr_type_ref(init, types, constants, functions, Some(&element_type))?; - match &init.kind { - ExprKind::Array(values) => { - let mut inferred = element_type.clone(); - inferred.array_dims.push(ArrayDim::Fixed(values.len())); - if array_literal_matches_type_with_env_ref(values, &inferred, types, constants) { Some(inferred) } else { None } - } - ExprKind::Identifier(name) => { - let other_type = parse_type_ref(types.get(name)?).ok()?; - if !other_type.is_array() || other_type.element_type() != Some(element_type.clone()) { - return None; - } - let size = array_size_with_constants_ref(&other_type, constants)?; - let mut inferred = element_type; - inferred.array_dims.push(ArrayDim::Fixed(size)); - Some(inferred) - } - _ => None, + if !init_type.is_array() || init_type.element_type() != Some(element_type.clone()) { + return None; } + + let size = array_size_with_constants_ref(&init_type, constants)?; + let mut inferred = element_type; + inferred.array_dims.push(ArrayDim::Fixed(size)); + Some(inferred) } fn lower_constant<'i>( constant: &ConstantAst<'i>, types: &mut HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Result, CompilerError> { - let type_ref = infer_type_ref(&constant.type_ref, Some(&constant.expr), types, constants) + let type_ref = infer_type_ref(&constant.type_ref, Some(&constant.expr), types, constants, functions) .ok_or_else(|| CompilerError::Unsupported(format!("cannot infer fixed array size from constant '{}'", constant.name)))?; types.insert(constant.name.clone(), type_name_from_ref(&type_ref)); Ok(ConstantAst { type_ref, ..constant.clone() }) @@ -90,8 +85,9 @@ fn lower_field<'i>( field: &ContractFieldAst<'i>, types: &mut HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Result, CompilerError> { - let type_ref = infer_type_ref(&field.type_ref, Some(&field.expr), types, constants) + let type_ref = infer_type_ref(&field.type_ref, Some(&field.expr), types, constants, functions) .ok_or_else(|| CompilerError::Unsupported(format!("cannot infer fixed array size from contract field '{}'", field.name)))?; types.insert(field.name.clone(), type_name_from_ref(&type_ref)); Ok(ContractFieldAst { type_ref, ..field.clone() }) @@ -101,12 +97,13 @@ fn lower_function<'i>( function: &FunctionAst<'i>, top_level_types: &HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Result, CompilerError> { let mut types = top_level_types.clone(); for param in &function.params { types.insert(param.name.clone(), type_name_from_ref(¶m.type_ref)); } - let body = lower_block(&function.body, &mut types, constants)?; + let body = lower_block(&function.body, &mut types, constants, functions)?; Ok(FunctionAst { body, ..function.clone() }) } @@ -114,10 +111,11 @@ fn lower_block<'i>( statements: &[Statement<'i>], types: &mut HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Result>, CompilerError> { let mut lowered = Vec::with_capacity(statements.len()); for statement in statements { - lowered.push(lower_statement(statement, types, constants)?); + lowered.push(lower_statement(statement, types, constants, functions)?); } Ok(lowered) } @@ -126,10 +124,11 @@ fn lower_statement<'i>( statement: &Statement<'i>, types: &mut HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Result, CompilerError> { match statement { Statement::VariableDefinition { type_ref, name, expr, .. } => { - let lowered_type = infer_type_ref(type_ref, expr.as_ref(), types, constants) + let lowered_type = infer_type_ref(type_ref, expr.as_ref(), types, constants, functions) .ok_or_else(|| CompilerError::Unsupported(format!("cannot infer fixed array size from variable '{}'", name)))?; types.insert(name.clone(), type_name_from_ref(&lowered_type)); Ok(match statement { @@ -152,7 +151,7 @@ fn lower_statement<'i>( let lowered_bindings = bindings .iter() .map(|binding| { - let lowered_type = infer_type_ref(&binding.type_ref, None, types, constants).ok_or_else(|| { + let lowered_type = infer_type_ref(&binding.type_ref, None, types, constants, functions).ok_or_else(|| { CompilerError::Unsupported(format!("cannot infer fixed array size from binding '{}'", binding.name)) })?; types.insert(binding.name.clone(), type_name_from_ref(&lowered_type)); @@ -169,15 +168,15 @@ fn lower_statement<'i>( } Statement::Block { body, span } => { let mut block_types = types.clone(); - let lowered_body = lower_block(body, &mut block_types, constants)?; + let lowered_body = lower_block(body, &mut block_types, constants, functions)?; Ok(Statement::Block { body: lowered_body, span: *span }) } Statement::If { condition, then_branch, else_branch, span, then_span, else_span } => { let mut then_types = types.clone(); - let lowered_then = lower_block(then_branch, &mut then_types, constants)?; + let lowered_then = lower_block(then_branch, &mut then_types, constants, functions)?; let (lowered_else, merged_types) = if let Some(else_branch) = else_branch { let mut else_types = types.clone(); - let lowered_else = lower_block(else_branch, &mut else_types, constants)?; + let lowered_else = lower_block(else_branch, &mut else_types, constants, functions)?; let mut merged = then_types; merged.extend(else_types); (Some(lowered_else), merged) @@ -197,7 +196,7 @@ fn lower_statement<'i>( Statement::For { ident, start, end, max_iterations, body, span, ident_span, body_span } => { let mut body_types = types.clone(); body_types.insert(ident.clone(), "int".to_string()); - let lowered_body = lower_block(body, &mut body_types, constants)?; + let lowered_body = lower_block(body, &mut body_types, constants, functions)?; Ok(Statement::For { ident: ident.clone(), start: start.clone(), @@ -218,14 +217,87 @@ fn infer_type_ref<'i>( initializer: Option<&Expr<'i>>, types: &HashMap, constants: &HashMap>, + functions: &HashMap>, ) -> Option { if matches!(declared_type.array_size(), Some(ArrayDim::Inferred)) { - infer_fixed_array_type_from_initializer_ref(declared_type, initializer, types, constants) + infer_fixed_array_type_from_initializer_ref(declared_type, initializer, types, constants, functions) } else { Some(declared_type.clone()) } } +fn infer_expr_type_ref<'i>( + expr: &Expr<'i>, + types: &HashMap, + constants: &HashMap>, + functions: &HashMap>, + array_literal_element_type: Option<&TypeRef>, +) -> Option { + match &expr.kind { + ExprKind::Identifier(name) => parse_type_ref(types.get(name)?).ok(), + ExprKind::Array(values) => { + let mut inferred = array_literal_element_type + .cloned() + .or_else(|| infer_array_literal_element_type(values, types, constants, functions))?; + inferred.array_dims.push(ArrayDim::Fixed(values.len())); + Some(inferred) + } + ExprKind::Call { name, .. } => { + if let Some(function) = functions.get(name) { + if function.entrypoint || function.return_types.len() != 1 { + return None; + } + return Some(function.return_types[0].clone()); + } + parse_type_ref(name).ok() + } + ExprKind::Binary { op: BinaryOp::Add, left, right } => { + let left_type = infer_expr_type_ref(left, types, constants, functions, None)?; + let right_type = infer_expr_type_ref(right, types, constants, functions, None)?; + let left_element = left_type.element_type()?; + if right_type.element_type() != Some(left_element.clone()) { + return None; + } + let left_size = array_size_with_constants_ref(&left_type, constants)?; + let right_size = array_size_with_constants_ref(&right_type, constants)?; + let mut inferred = left_element; + inferred.array_dims.push(ArrayDim::Fixed(left_size.checked_add(right_size)?)); + Some(inferred) + } + ExprKind::IfElse { then_expr, else_expr, .. } => { + let then_type = infer_expr_type_ref(then_expr, types, constants, functions, None)?; + let else_type = infer_expr_type_ref(else_expr, types, constants, functions, None)?; + (then_type == else_type).then_some(then_type) + } + ExprKind::Append { source, args, .. } => { + let source_type = infer_expr_type_ref(source, types, constants, functions, None)?; + let element_type = source_type.element_type()?; + let source_size = array_size_with_constants_ref(&source_type, constants)?; + let mut inferred = element_type; + inferred.array_dims.push(ArrayDim::Fixed(source_size.checked_add(args.len())?)); + Some(inferred) + } + ExprKind::UnarySuffix { source, kind: UnarySuffixKind::Reverse, .. } => { + infer_expr_type_ref(source, types, constants, functions, None) + } + _ => None, + } +} + +fn infer_array_literal_element_type<'i>( + values: &[Expr<'i>], + types: &HashMap, + constants: &HashMap>, + functions: &HashMap>, +) -> Option { + let first_type = infer_expr_type_ref(values.first()?, types, constants, functions, None)?; + if values.iter().skip(1).all(|value| infer_expr_type_ref(value, types, constants, functions, None).as_ref() == Some(&first_type)) { + Some(first_type) + } else { + None + } +} + fn array_size_with_constants_ref<'i>(type_ref: &TypeRef, constants: &HashMap>) -> Option { match type_ref.array_size()? { ArrayDim::Fixed(size) => Some(*size), diff --git a/silverscript-lang/src/compiler/mod.rs b/silverscript-lang/src/compiler/mod.rs index 710352e..eb6fd9d 100644 --- a/silverscript-lang/src/compiler/mod.rs +++ b/silverscript-lang/src/compiler/mod.rs @@ -31,7 +31,7 @@ pub use compile::{compile_debug_expr, function_branch_index}; pub(crate) use debug_recording::DebugRecorder; use r#for::lower_for_loops; pub(crate) use static_check::expr_matches_declared_type_ref; -use static_check::{static_check_contract, value_matches_type_ref}; +use static_check::value_matches_type_ref; pub use structs::flattened_struct_name; pub(super) use structs::{ StructFieldSpec, StructRegistry, build_struct_registry, ensure_known_or_builtin_type, flatten_constructor_args_env, @@ -106,7 +106,6 @@ pub fn compile_contract<'i>( options: CompileOptions, ) -> Result, CompilerError> { let contract = parse_contract_ast(source)?; - static_check_contract(&contract, constructor_args, options)?; compile_contract_impl(&contract, constructor_args, options, Some(source)) } @@ -115,7 +114,6 @@ pub fn compile_contract_ast<'i>( constructor_args: &[Expr<'i>], options: CompileOptions, ) -> Result, CompilerError> { - static_check_contract(contract, constructor_args, options)?; compile_contract_impl(contract, constructor_args, options, None) } diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index dd559b6..dd945f2 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -1288,6 +1288,66 @@ fn rejects_comparing_inferred_and_fixed_byte_arrays_when_sizes_differ() { ); } +#[test] +fn rejects_inferred_array_size_when_initializer_cannot_provide_matching_fixed_array_type() { + let cases = [ + ( + "literal values do not match declared element type", + r#" + int[_] x = [1, true]; + "#, + "array element type mismatch", + ), + ( + "identifier is unknown", + r#" + int[_] x = y; + "#, + "cannot infer fixed array size from variable 'x'", + ), + ( + "identifier is not an array", + r#" + int y = 1; + int[_] x = y; + "#, + "cannot infer fixed array size from variable 'x'", + ), + ( + "identifier has a different array element type", + r#" + bool[2] y = [true, false]; + int[_] x = y; + "#, + "cannot infer fixed array size from variable 'x'", + ), + ( + "identifier has a dynamic array size", + r#" + int[] y = [1, 2]; + int[_] x = y; + "#, + "cannot infer fixed array size from variable 'x'", + ), + ]; + + for (name, body, expected_error) in cases { + let source = format!( + r#" + contract Arrays() {{ + entrypoint function main() {{ + {body} + require(true); + }} + }} + "# + ); + + let err = compile_contract(&source, &[], CompileOptions::default()).expect_err(&format!("{name} should fail")); + assert!(err.to_string().contains(expected_error), "{name}: expected error containing '{expected_error}', got: {err}"); + } +} + #[test] fn infers_fixed_sizes_for_multiple_array_element_types() { let source = r#" @@ -1318,6 +1378,71 @@ fn infers_fixed_sizes_for_multiple_array_element_types() { ); } +#[test] +fn infers_fixed_array_size_from_function_call_initializer_expression() { + let source = r#" + contract Arrays() { + function makeArray(): int[3] { + return [1, 2, 3]; + } + + entrypoint function main() { + int[_] x = makeArray(); + require(x.length == 3); + } + } + "#; + + compile_contract(source, &[], CompileOptions::default()).expect("int[_] x should infer from function call returning int[3]"); +} + +#[test] +fn infers_fixed_array_size_from_array_concat_initializer_expression() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[2] left = [1, 2]; + int[1] right = [3]; + int[_] x = left + right; + require(x.length == 3); + } + } + "#; + + compile_contract(source, &[], CompileOptions::default()).expect("int[_] x should infer from int[2] + int[1]"); +} + +#[test] +fn infers_fixed_array_size_from_ternary_initializer_expression() { + let source = r#" + contract Arrays() { + entrypoint function main(bool flag) { + int[3] left = [1, 2, 3]; + int[3] right = [4, 5, 6]; + int[_] x = flag ? left : right; + require(x.length == 3); + } + } + "#; + + compile_contract(source, &[], CompileOptions::default()).expect("int[_] x should infer from ternary branches typed int[3]"); +} + +#[test] +fn recursively_infers_fixed_array_size_from_inferred_array_identifier() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[_] x = [1, 2, 3]; + int[_] y = x; + require(y.length == 3); + } + } + "#; + + compile_contract(source, &[], CompileOptions::default()).expect("int[_] y should infer from previously inferred int[_] x"); +} + #[test] fn rejects_comparing_dynamic_and_fixed_arrays_without_cast_in_function_scope() { let source = r#"