From 3b18fbe3e5a0ab28e5dd82a99a5f2dadc52ad483 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 24 Mar 2026 08:38:57 +0200 Subject: [PATCH 1/9] Add regression_allows_comparing_byte_array_to_byte_constant --- silverscript-lang/tests/compiler_tests.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 44a85a6..ab7577b 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -882,6 +882,26 @@ fn build_sig_script_rejects_unknown_function() { assert!(result.is_err()); } +#[test] +fn regression_allows_comparing_byte_array_to_byte_constant() { + let source = r#" + contract Reproduce(byte[32] genesisPk, byte genesisIdentifierType) { + byte[32] ownerIdentifier = genesisPk; + byte identifierType = genesisIdentifierType; + byte constant ZERO = 0x00; + + entrypoint function main() { + if (ownerIdentifier == ZERO) { + require(true); + } + } + } + "#; + + compile_contract(source, &[Expr::bytes(vec![1u8; 32]), Expr::byte(0)], CompileOptions::default()) + .expect("regression: byte[32] == byte currently compiles"); +} + #[test] fn build_sig_script_rejects_wrong_argument_count() { let source = r#" From 2dbe4d3404cf5d98c4dfaede38cd7e08575a7ad5 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 24 Mar 2026 10:39:09 +0200 Subject: [PATCH 2/9] Better typecheck for comparisons --- silverscript-lang/src/compiler.rs | 96 ++++++++++++++++++- .../src/compiler/debug_value_types.rs | 17 +++- silverscript-lang/tests/compiler_tests.rs | 93 +++++++++++++++++- 3 files changed, 197 insertions(+), 9 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index a47ce4b..43d3c79 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,9 +6,9 @@ use kaspa_txscript::serialize_i64; use serde::{Deserialize, Serialize}; use crate::ast::{ - ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, ExprKind, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, - StateBindingAst, StateFieldExpr, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, UnarySuffixKind, parse_contract_ast, - parse_type_ref, + ArrayDim, BinaryOp, ConstantAst, ContractAst, ContractFieldAst, Expr, ExprKind, FunctionAst, IntrospectionKind, NullaryOp, + ParamAst, SplitPart, StateBindingAst, StateFieldExpr, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, UnarySuffixKind, + parse_contract_ast, parse_type_ref, }; use crate::debug_info::{DebugInfo, RuntimeBinding, SourceSpan}; pub use crate::errors::{CompilerError, ErrorSpan}; @@ -955,7 +955,9 @@ fn compile_contract_impl<'i>( compiled_entrypoints.push(compile_entrypoint_function( func, index, + &lowered_contract.params, &lowered_contract.fields, + &lowered_contract.constants, contract_field_prefix_len, &constants, options, @@ -1300,6 +1302,52 @@ fn expr_matches_type_ref<'i>(expr: &Expr<'i>, type_ref: &TypeRef) -> bool { } } +fn infer_expr_type_ref_for_comparison<'i>( + expr: &Expr<'i>, + env: &HashMap>, + types: &HashMap, +) -> Option { + if let ExprKind::Identifier(name) = &expr.kind { + if let Some(type_ref) = types.get(name).and_then(|type_name| parse_type_ref(type_name).ok()) { + return Some(type_ref); + } + } + if let Some((name, _)) = env.iter().find(|(_, value)| value.kind == expr.kind) { + if let Some(type_ref) = types.get(name).and_then(|type_name| parse_type_ref(type_name).ok()) { + return Some(type_ref); + } + } + let type_name = infer_debug_expr_value_type(expr, env, types, &mut HashSet::new()).ok()?; + parse_type_ref(&type_name).ok() +} + +fn array_comparison_types_compatible(left_type: &TypeRef, right_type: &TypeRef) -> bool { + let (Some(left_element), Some(right_element)) = (array_element_type_ref(left_type), array_element_type_ref(right_type)) else { + return false; + }; + + if !comparison_types_compatible(&left_element, &right_element) { + return false; + } + + match (array_size_ref(left_type), array_size_ref(right_type)) { + (Some(left_size), Some(right_size)) => left_size == right_size, + _ => true, + } +} + +fn comparison_types_compatible(left_type: &TypeRef, right_type: &TypeRef) -> bool { + if type_name_from_ref(left_type) == type_name_from_ref(right_type) { + return true; + } + + if is_array_type_ref(left_type) && is_array_type_ref(right_type) { + return array_comparison_types_compatible(left_type, right_type); + } + + false +} + fn array_literal_matches_type_ref<'i>(values: &[Expr<'i>], type_ref: &TypeRef) -> bool { let Some(element_type) = array_element_type_ref(type_ref) else { return false; @@ -2439,7 +2487,9 @@ pub fn function_branch_index<'i>(contract: &ContractAst<'i>, function_name: &str fn compile_entrypoint_function<'i>( function: &FunctionAst<'i>, function_index: usize, + contract_params: &[ParamAst<'i>], contract_fields: &[ContractFieldAst<'i>], + contract_constants: &[ConstantAst<'i>], contract_field_prefix_len: usize, constants: &HashMap>, options: CompileOptions, @@ -2452,6 +2502,30 @@ fn compile_entrypoint_function<'i>( let contract_field_count = contract_fields.len(); let mut flattened_param_names = Vec::new(); let mut types = HashMap::new(); + for param in contract_params { + let param_type_name = type_name_from_ref(¶m.type_ref); + types.insert(param.name.clone(), param_type_name.clone()); + if struct_name_from_type_ref(¶m.type_ref, structs).is_some() + || struct_array_name_from_type_ref(¶m.type_ref, structs).is_some() + { + for (path, field_type) in flatten_type_ref_leaves(¶m.type_ref, structs)? { + let leaf_name = flattened_struct_name(¶m.name, &path); + types.insert(leaf_name, type_name_from_ref(&field_type)); + } + } + } + for constant in contract_constants { + let constant_type_name = type_name_from_ref(&constant.type_ref); + types.insert(constant.name.clone(), constant_type_name.clone()); + if struct_name_from_type_ref(&constant.type_ref, structs).is_some() + || struct_array_name_from_type_ref(&constant.type_ref, structs).is_some() + { + for (path, field_type) in flatten_type_ref_leaves(&constant.type_ref, structs)? { + let leaf_name = flattened_struct_name(&constant.name, &path); + types.insert(leaf_name, type_name_from_ref(&field_type)); + } + } + } for param in &function.params { let param_type_name = type_name_from_ref(¶m.type_ref); types.insert(param.name.clone(), param_type_name.clone()); @@ -6012,6 +6086,22 @@ fn compile_expr<'i>( Ok(()) } ExprKind::Binary { op, left, right } => { + if matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) { + if let (Some(left_type), Some(right_type)) = + ( + infer_expr_type_ref_for_comparison(left, env, types), + infer_expr_type_ref_for_comparison(right, env, types), + ) + { + if !comparison_types_compatible(&left_type, &right_type) { + return Err(CompilerError::Unsupported(format!( + "type mismatch: cannot compare {} and {}", + type_name_from_ref(&left_type), + type_name_from_ref(&right_type) + ))); + } + } + } let bytes_eq = matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); diff --git a/silverscript-lang/src/compiler/debug_value_types.rs b/silverscript-lang/src/compiler/debug_value_types.rs index 6cf43b6..a597cff 100644 --- a/silverscript-lang/src/compiler/debug_value_types.rs +++ b/silverscript-lang/src/compiler/debug_value_types.rs @@ -32,7 +32,13 @@ fn introspection_value_type(kind: IntrospectionKind) -> &'static str { fn builtin_call_value_type(name: &str) -> &'static str { match name { + "int" => "int", + "bool" => "bool", "byte" => "byte", + "string" => "string", + "pubkey" => "pubkey", + "sig" => "sig", + "datasig" => "datasig", "bytes" | "blake2b" | "sha256" @@ -102,9 +108,16 @@ pub(super) fn infer_debug_expr_value_type<'i>( Ok("int".to_string()) } } - BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => { - Ok("int".to_string()) + BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd => { + let left_type = infer_debug_expr_value_type(left, env, types, visiting)?; + let right_type = infer_debug_expr_value_type(right, env, types, visiting)?; + if left_type == right_type && is_bytes_type(&left_type) { + Ok(left_type) + } else { + Ok("int".to_string()) + } } + BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => Ok("int".to_string()), }, ExprKind::IfElse { then_expr, else_expr, .. } => { let then_type = infer_debug_expr_value_type(then_expr, env, types, visiting)?; diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index ab7577b..ca46d51 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -883,9 +883,9 @@ fn build_sig_script_rejects_unknown_function() { } #[test] -fn regression_allows_comparing_byte_array_to_byte_constant() { +fn disallow_comparing_byte_array_to_byte_constant() { let source = r#" - contract Reproduce(byte[32] genesisPk, byte genesisIdentifierType) { + contract Test(byte[32] genesisPk, byte genesisIdentifierType) { byte[32] ownerIdentifier = genesisPk; byte identifierType = genesisIdentifierType; byte constant ZERO = 0x00; @@ -898,8 +898,93 @@ fn regression_allows_comparing_byte_array_to_byte_constant() { } "#; - compile_contract(source, &[Expr::bytes(vec![1u8; 32]), Expr::byte(0)], CompileOptions::default()) - .expect("regression: byte[32] == byte currently compiles"); + assert!( + compile_contract(source, &[Expr::bytes(vec![1u8; 32]), Expr::byte(0)], CompileOptions::default()).is_err(), + "comparing byte[32] to byte should be rejected without cast" + ); +} + +#[test] +fn allow_comparing_byte_array_to_byte_constant_with_cast() { + let source = r#" + contract Test(byte[32] genesisPk, byte genesisIdentifierType) { + byte[32] ownerIdentifier = genesisPk; + byte identifierType = genesisIdentifierType; + byte constant ZERO = 0x00; + + entrypoint function main() { + if (byte[](ownerIdentifier) == byte[](ZERO)) { + require(true); + } + } + } + "#; + + let _ = compile_contract(source, &[Expr::bytes(vec![1u8; 32]), Expr::byte(0)], CompileOptions::default()) + .expect("comparing byte[32] to byte should be allowed with cast"); +} + +#[test] +fn rejects_comparing_different_scalar_types_without_cast() { + let source = r#" + contract Reproduce() { + entrypoint function main() { + if (1 == true) { + require(false); + } + } + } + "#; + + let result = compile_contract(source, &[], CompileOptions::default()); + assert!(result.is_err(), "int == bool should be rejected"); +} + +#[test] +fn allows_comparing_dynamic_and_fixed_arrays_with_same_element_type() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[] x = [1]; + int[1] y = [1]; + require(x == y); + } + } + "#; + + let result = compile_contract(source, &[], CompileOptions::default()); + assert!(result.is_ok(), "int[] == int[1] should compile"); +} + +#[test] +fn allows_comparing_contract_dynamic_array_field_with_fixed_array_local() { + let source = r#" + contract Arrays(byte[] b) { + entrypoint function main() { + byte[4] x = b.split(4)[0]; + require(x != b); + } + } + "#; + + let result = compile_contract(source, &[Expr::bytes(b"abcde".to_vec())], CompileOptions::default()); + assert!(result.is_ok(), "byte[4] != byte[] should compile even when the field value is byte[5]"); +} + +#[test] +fn rejects_comparing_fixed_arrays_with_different_sizes() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[1] x = [1]; + int[2] y = [1, 2]; + require(x != y); + } + } + "#; + + let result = compile_contract(source, &[], CompileOptions::default()); + assert!(result.is_err(), "int[1] != int[2] should be rejected"); } #[test] From c6c9a202cfb8cb7917f4b79d07e8775e852aea6c Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 24 Mar 2026 15:01:59 +0200 Subject: [PATCH 3/9] Add type[_] for inferred size, and make stricter compile rules for array comparison --- silverscript-lang/src/ast.rs | 21 +- silverscript-lang/src/compiler.rs | 139 +++++++++---- .../src/compiler/debug_value_types.rs | 49 +++-- silverscript-lang/src/silverscript.pest | 2 +- silverscript-lang/tests/compiler_tests.rs | 186 ++++++++++++++---- silverscript-lang/tests/examples/comments.sil | 2 +- silverscript-lang/tests/examples/covenant.sil | 2 +- .../tests/examples/covenant_escrow.sil | 4 +- .../tests/examples/covenant_mecenas.sil | 2 +- .../tests/examples/double_split.sil | 2 +- silverscript-lang/tests/examples/mecenas.sil | 2 +- .../tests/examples/mecenas_locktime.sil | 4 +- .../tests/examples/p2pkh_invalid.sil | 2 +- .../tests/examples/simple_splice.sil | 2 +- .../tests/examples/simulating_state.sil | 4 +- silverscript-lang/tests/examples/slice.sil | 2 +- .../examples/slice_variable_parameter.sil | 2 +- .../examples/split_or_slice_signature.sil | 2 +- .../tests/examples/split_size.sil | 2 +- .../tests/examples/split_typed.sil | 2 +- 20 files changed, 320 insertions(+), 113 deletions(-) diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 58dab38..ec6f3f8 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -214,6 +214,7 @@ impl<'de> Deserialize<'de> for TypeBase { #[serde(tag = "kind", content = "value", rename_all = "snake_case")] pub enum ArrayDim { Dynamic, + Inferred, Fixed(usize), Constant(String), } @@ -224,6 +225,7 @@ impl TypeRef { for dim in &self.array_dims { match dim { ArrayDim::Dynamic => out.push_str("[]"), + ArrayDim::Inferred => out.push_str("[_]"), ArrayDim::Fixed(size) => out.push_str(&format!("[{size}]")), ArrayDim::Constant(name) => out.push_str(&format!("[{name}]")), } @@ -1159,7 +1161,13 @@ fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result Some(size_pair) => match size_pair.as_rule() { Rule::array_size => { let raw = size_pair.as_str().trim(); - if let Ok(size) = raw.parse::() { ArrayDim::Fixed(size) } else { ArrayDim::Constant(raw.to_string()) } + if raw == "_" { + ArrayDim::Inferred + } else if let Ok(size) = raw.parse::() { + ArrayDim::Fixed(size) + } else { + ArrayDim::Constant(raw.to_string()) + } } Rule::Identifier => ArrayDim::Constant(size_pair.as_str().to_string()), _ => return Err(CompilerError::Unsupported("invalid array dimension".to_string())), @@ -2077,15 +2085,8 @@ fn parse_cast<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { return Ok(Expr::new(ExprKind::Call { name: type_name, args, name_span: type_span }, span)); } - // Handle single byte cast (duplicate check removed above) - // Support type[N] syntax - if let Some(bracket_pos) = type_name.find('[') { - if type_name.ends_with(']') { - let size_str = &type_name[bracket_pos + 1..type_name.len() - 1]; - if size_str.is_empty() || size_str.parse::().is_ok() { - return Ok(Expr::new(ExprKind::Call { name: type_name, args, name_span: type_span }, span)); - } - } + if parse_type_ref(&type_name).is_ok() { + return Ok(Expr::new(ExprKind::Call { name: type_name, args, name_span: type_span }, span)); } Err(CompilerError::Unsupported(format!("cast type not supported: {type_name}"))) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 43d3c79..b0bc4b9 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1317,35 +1317,63 @@ fn infer_expr_type_ref_for_comparison<'i>( return Some(type_ref); } } + if let ExprKind::Call { name, .. } = &expr.kind { + let is_builtin_cast = matches!(name.as_str(), "int" | "bool" | "byte" | "string" | "pubkey" | "sig" | "datasig") + || (name.contains('[') + && parse_type_ref(name).ok().is_some_and(|type_ref| !matches!(type_ref.base, TypeBase::Custom(_)))); + let is_known_builtin = matches!( + name.as_str(), + "int" + | "bool" + | "byte" + | "string" + | "pubkey" + | "sig" + | "datasig" + | "bytes" + | "blake2b" + | "sha256" + | "OpSha256" + | "OpTxSubnetId" + | "OpTxPayloadSubstr" + | "OpOutpointTxId" + | "OpTxInputScriptSigSubstr" + | "OpTxInputSeq" + | "OpTxInputSpkSubstr" + | "OpTxOutputSpkSubstr" + | "OpNum2Bin" + | "OpBin2Num" + | "OpChainblockSeqCommit" + | "LockingBytecodeNullData" + | "ScriptPubKeyP2PK" + | "ScriptPubKeyP2SH" + | "ScriptPubKeyP2SHFromRedeemScript" + | "OpInputCovenantId" + | "OpTxGas" + | "OpTxPayloadLen" + | "OpTxInputIndex" + | "OpTxInputIsCoinbase" + | "OpTxInputScriptSigLen" + | "OpTxInputSpkLen" + | "OpOutpointIndex" + | "OpTxOutputSpkLen" + | "OpAuthOutputCount" + | "OpAuthOutputIdx" + | "OpCovInputCount" + | "OpCovInputIdx" + | "OpCovOutputCount" + | "OpCovOutputIdx" + ); + if !is_builtin_cast && !is_known_builtin { + return None; + } + } let type_name = infer_debug_expr_value_type(expr, env, types, &mut HashSet::new()).ok()?; parse_type_ref(&type_name).ok() } -fn array_comparison_types_compatible(left_type: &TypeRef, right_type: &TypeRef) -> bool { - let (Some(left_element), Some(right_element)) = (array_element_type_ref(left_type), array_element_type_ref(right_type)) else { - return false; - }; - - if !comparison_types_compatible(&left_element, &right_element) { - return false; - } - - match (array_size_ref(left_type), array_size_ref(right_type)) { - (Some(left_size), Some(right_size)) => left_size == right_size, - _ => true, - } -} - fn comparison_types_compatible(left_type: &TypeRef, right_type: &TypeRef) -> bool { - if type_name_from_ref(left_type) == type_name_from_ref(right_type) { - return true; - } - - if is_array_type_ref(left_type) && is_array_type_ref(right_type) { - return array_comparison_types_compatible(left_type, right_type); - } - - false + type_name_from_ref(left_type) == type_name_from_ref(right_type) } fn array_literal_matches_type_ref<'i>(values: &[Expr<'i>], type_ref: &TypeRef) -> bool { @@ -1749,7 +1777,7 @@ fn array_size_with_constants_ref<'i>(type_ref: &TypeRef, constants: &HashMap None, + ArrayDim::Dynamic | ArrayDim::Inferred => None, } } @@ -1993,7 +2021,11 @@ fn validate_return_types<'i>( } fn has_explicit_array_size_ref(type_ref: &TypeRef) -> bool { - !matches!(type_ref.array_size(), Some(ArrayDim::Dynamic) | None) + !matches!(type_ref.array_size(), Some(ArrayDim::Dynamic | ArrayDim::Inferred) | None) +} + +fn has_inferred_array_size_ref(type_ref: &TypeRef) -> bool { + matches!(type_ref.array_size(), Some(ArrayDim::Inferred)) } fn is_array_type_assignable_ref<'i>(actual: &TypeRef, expected: &TypeRef, constants: &HashMap>) -> bool { @@ -2061,7 +2093,7 @@ fn infer_fixed_array_type_from_initializer_ref<'i>( types: &HashMap, constants: &HashMap>, ) -> Option { - if !declared_type.array_size().is_some_and(|dim| matches!(dim, ArrayDim::Dynamic)) { + if !has_inferred_array_size_ref(declared_type) { return None; } @@ -2833,13 +2865,16 @@ fn compile_statement<'i>( } let type_name = type_name_from_ref(type_ref); - let effective_type_name = - if is_array_type(&type_name) && array_size_with_constants(&type_name, contract_constants).is_none() { - infer_fixed_array_type_from_initializer(&type_name, expr.as_ref(), types, contract_constants) - .unwrap_or_else(|| type_name.clone()) - } else { - type_name.clone() - }; + let effective_type_name = if has_inferred_array_size_ref(type_ref) { + infer_fixed_array_type_from_initializer(&type_name, expr.as_ref(), types, contract_constants).ok_or_else(|| { + CompilerError::Unsupported(format!( + "variable '{}' requires an initializer with inferrable size for type {}", + name, type_name + )) + })? + } else { + type_name.clone() + }; // Check if this is a fixed-size array (e.g., byte[N]) or dynamic array (e.g., byte[]) let is_fixed_size_array = @@ -7096,6 +7131,24 @@ fn compile_call_expr<'i>( )?; Ok(()) } + "bool" | "string" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); + } + compile_expr( + &args[0], + scope.env, + scope.stack_bindings, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + Ok(()) + } "sig" | "pubkey" | "datasig" => { if args.len() != 1 { return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); @@ -7177,6 +7230,24 @@ fn compile_call_expr<'i>( Ok(()) } } + name if parse_type_ref(name).is_ok_and(|type_ref| is_array_type_ref(&type_ref)) => { + if args.len() != 1 { + return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); + } + compile_expr( + &args[0], + scope.env, + scope.stack_bindings, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + Ok(()) + } "blake2b" => { if args.len() != 1 { return Err(CompilerError::Unsupported("blake2b() expects a single argument".to_string())); diff --git a/silverscript-lang/src/compiler/debug_value_types.rs b/silverscript-lang/src/compiler/debug_value_types.rs index a597cff..349c6d9 100644 --- a/silverscript-lang/src/compiler/debug_value_types.rs +++ b/silverscript-lang/src/compiler/debug_value_types.rs @@ -1,9 +1,9 @@ use std::collections::{HashMap, HashSet}; -use crate::ast::{BinaryOp, Expr, ExprKind, IntrospectionKind, NullaryOp, UnaryOp, UnarySuffixKind}; +use crate::ast::{BinaryOp, Expr, ExprKind, IntrospectionKind, NullaryOp, TypeBase, UnaryOp, UnarySuffixKind}; use crate::errors::CompilerError; -use super::{array_element_type, is_bytes_type}; +use super::{array_element_type, is_bytes_type, parse_type_ref}; fn nullary_value_type(op: NullaryOp) -> &'static str { match op { @@ -39,10 +39,24 @@ fn builtin_call_value_type(name: &str) -> &'static str { "pubkey" => "pubkey", "sig" => "sig", "datasig" => "datasig", + "OpBin2Num" + | "OpTxInputDaaScore" + | "OpTxGas" + | "OpTxPayloadLen" + | "OpTxInputIndex" + | "OpTxInputScriptSigLen" + | "OpTxInputSpkLen" + | "OpOutpointIndex" + | "OpTxOutputSpkLen" + | "OpAuthOutputCount" + | "OpAuthOutputIdx" + | "OpCovInputCount" + | "OpCovInputIdx" + | "OpCovOutputCount" + | "OpCovOutputIdx" => "int", + "OpTxInputIsCoinbase" => "bool", + "blake2b" | "sha256" | "OpSha256" => "byte[32]", "bytes" - | "blake2b" - | "sha256" - | "OpSha256" | "OpTxSubnetId" | "OpTxPayloadSubstr" | "OpOutpointTxId" @@ -56,14 +70,25 @@ fn builtin_call_value_type(name: &str) -> &'static str { | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" | "ScriptPubKeyP2SHFromRedeemScript" => "byte[]", - "OpTxInputDaaScore" | "OpAuthOutputCount" | "OpCovInputCount" | "OpCovInputIdx" | "OpCovOutputCount" | "OpCovOutputIdx" => { - "int" - } "OpInputCovenantId" => "byte[32]", _ => "byte[]", } } +fn is_builtin_cast_type_name(name: &str) -> bool { + if matches!(name, "int" | "bool" | "byte" | "string" | "pubkey" | "sig" | "datasig") { + return true; + } + if !name.contains('[') { + return false; + } + let Ok(type_ref) = parse_type_ref(name) else { + return false; + }; + + !matches!(type_ref.base, TypeBase::Custom(_)) +} + pub(super) fn infer_debug_expr_value_type<'i>( expr: &Expr<'i>, env: &HashMap>, @@ -111,11 +136,7 @@ pub(super) fn infer_debug_expr_value_type<'i>( BinaryOp::BitOr | BinaryOp::BitXor | BinaryOp::BitAnd => { let left_type = infer_debug_expr_value_type(left, env, types, visiting)?; let right_type = infer_debug_expr_value_type(right, env, types, visiting)?; - if left_type == right_type && is_bytes_type(&left_type) { - Ok(left_type) - } else { - Ok("int".to_string()) - } + if left_type == right_type && is_bytes_type(&left_type) { Ok(left_type) } else { Ok("int".to_string()) } } BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => Ok("int".to_string()), }, @@ -143,7 +164,7 @@ pub(super) fn infer_debug_expr_value_type<'i>( ExprKind::Nullary(kind) => Ok(nullary_value_type(*kind).to_string()), ExprKind::Introspection { kind, .. } => Ok(introspection_value_type(*kind).to_string()), ExprKind::Call { name, .. } => { - if name.starts_with("byte[") { + if is_builtin_cast_type_name(name) { Ok(name.clone()) } else { Ok(builtin_call_value_type(name).to_string()) diff --git a/silverscript-lang/src/silverscript.pest b/silverscript-lang/src/silverscript.pest index ae9ca90..3920d21 100644 --- a/silverscript-lang/src/silverscript.pest +++ b/silverscript-lang/src/silverscript.pest @@ -144,7 +144,7 @@ type_name = { base_type ~ array_suffix* } base_type = { builtin_type | Identifier } builtin_type = { "int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte" } array_suffix = { "[" ~ array_size? ~ "]" } -array_size = { Identifier | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) } +array_size = { "_" | Identifier | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) } VersionLiteral = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ } diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index ca46d51..f866ff7 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -905,23 +905,37 @@ fn disallow_comparing_byte_array_to_byte_constant() { } #[test] -fn allow_comparing_byte_array_to_byte_constant_with_cast() { +fn disallow_comparing_dynamic_and_fixed_byte_arrays_without_cast_in_contract_scope() { let source = r#" - contract Test(byte[32] genesisPk, byte genesisIdentifierType) { - byte[32] ownerIdentifier = genesisPk; - byte identifierType = genesisIdentifierType; - byte constant ZERO = 0x00; + contract Test(byte[] x) { + byte[2] y = 0x1234; entrypoint function main() { - if (byte[](ownerIdentifier) == byte[](ZERO)) { - require(true); - } + require(x == y); } } "#; - let _ = compile_contract(source, &[Expr::bytes(vec![1u8; 32]), Expr::byte(0)], CompileOptions::default()) - .expect("comparing byte[32] to byte should be allowed with cast"); + assert!( + compile_contract(source, &[Expr::bytes(vec![0x12])], CompileOptions::default()).is_err(), + "comparing byte[] to byte[2] should be rejected without cast" + ); +} + +#[test] +fn allow_comparing_dynamic_and_fixed_byte_arrays_with_cast_in_contract_scope() { + let source = r#" + contract Test(byte[] x) { + byte[2] y = 0x1234; + + entrypoint function main() { + require(x == byte[](y)); + } + } + "#; + + compile_contract(source, &[Expr::bytes(vec![0x12])], CompileOptions::default()) + .expect("comparing byte[] to byte[2] should be allowed with cast"); } #[test] @@ -941,7 +955,7 @@ fn rejects_comparing_different_scalar_types_without_cast() { } #[test] -fn allows_comparing_dynamic_and_fixed_arrays_with_same_element_type() { +fn disallow_comparing_dynamic_and_fixed_int_arrays_without_cast() { let source = r#" contract Arrays() { entrypoint function main() { @@ -952,39 +966,116 @@ fn allows_comparing_dynamic_and_fixed_arrays_with_same_element_type() { } "#; - let result = compile_contract(source, &[], CompileOptions::default()); - assert!(result.is_ok(), "int[] == int[1] should compile"); + assert!(compile_contract(source, &[], CompileOptions::default()).is_err(), "int[] == int[1] should be rejected"); } #[test] -fn allows_comparing_contract_dynamic_array_field_with_fixed_array_local() { +fn allows_comparing_dynamic_and_fixed_int_arrays_with_cast() { let source = r#" - contract Arrays(byte[] b) { + contract Arrays() { entrypoint function main() { - byte[4] x = b.split(4)[0]; - require(x != b); + int[] x = [1]; + int[1] y = [1]; + require(x == int[](y)); } } "#; - let result = compile_contract(source, &[Expr::bytes(b"abcde".to_vec())], CompileOptions::default()); - assert!(result.is_ok(), "byte[4] != byte[] should compile even when the field value is byte[5]"); + assert!(compile_contract(source, &[], CompileOptions::default()).is_ok(), "int[] == int[](int[1]) should compile"); } #[test] -fn rejects_comparing_fixed_arrays_with_different_sizes() { +fn allows_comparing_inferred_and_fixed_byte_arrays_when_sizes_match() { let source = r#" contract Arrays() { entrypoint function main() { - int[1] x = [1]; - int[2] y = [1, 2]; - require(x != y); + byte[_] x = 0x1256; + byte[2] y = 0x1234; + require(x == y); } } "#; - let result = compile_contract(source, &[], CompileOptions::default()); - assert!(result.is_err(), "int[1] != int[2] should be rejected"); + assert!(compile_contract(source, &[], CompileOptions::default()).is_ok(), "byte[_] should infer to byte[2]"); +} + +#[test] +fn rejects_comparing_inferred_and_fixed_byte_arrays_when_sizes_differ() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[_] x = 0x12; + byte[2] y = 0x1234; + require(x == y); + } + } + "#; + + assert!( + compile_contract(source, &[], CompileOptions::default()).is_err(), + "byte[_] inferred as byte[1] should not compare to byte[2]" + ); +} + +#[test] +fn infers_fixed_sizes_for_multiple_array_element_types() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[_] ints = [1, 2, 3, 4]; + int[4] ints_expected = [1, 2, 3, 4]; + bool[_] flags = [true, false]; + bool[2] flags_expected = [true, false]; + pubkey[_] keys = [ + 0x0101010101010101010101010101010101010101010101010101010101010101, + 0x0202020202020202020202020202020202020202020202020202020202020202 + ]; + pubkey[2] keys_expected = [ + 0x0303030303030303030303030303030303030303030303030303030303030303, + 0x0404040404040404040404040404040404040404040404040404040404040404 + ]; + require(ints == ints_expected); + require(flags == flags_expected); + require(keys == keys_expected); + } + } + "#; + + assert!( + compile_contract(source, &[], CompileOptions::default()).is_ok(), + "type[_] should infer fixed sizes across supported element types" + ); +} + +#[test] +fn rejects_comparing_dynamic_and_fixed_arrays_without_cast_in_function_scope() { + let source = r#" + contract Arrays() { + entrypoint function main(byte[] x) { + byte[2] y = 0x1234; + require(x == y); + } + } + "#; + + assert!( + compile_contract(source, &[], CompileOptions::default()).is_err(), + "byte[] param should not compare to byte[2] without cast" + ); +} + +#[test] +fn allows_comparing_dynamic_and_fixed_arrays_with_cast_in_function_scope() { + let source = r#" + contract Arrays() { + entrypoint function main(byte[] x) { + byte[2] y = 0x1234; + require(x == byte[](y)); + } + } + "#; + + assert!(compile_contract(source, &[], CompileOptions::default()).is_ok(), "byte[] param should compare to byte[](byte[2])"); } #[test] @@ -3014,7 +3105,7 @@ fn runs_slice_with_explicit_end_bounds() { byte[] data = 0x0102030405060708090a; byte[] segment = data.slice(3, 8); require(segment.length == 5); - require(segment == 0x0405060708); + require(segment == byte[](0x0405060708)); } } "#; @@ -3034,8 +3125,8 @@ fn runs_slice_reconstruction_and_compare_runtime_example() { byte[] right = data.slice(4, 10); byte[] rebuilt = left + right; - require(left == 0x01020304); - require(right == 0x05060708090a); + require(left == byte[](0x01020304)); + require(right == byte[](0x05060708090a)); require(rebuilt.length == data.length); require(rebuilt == data); } @@ -4725,7 +4816,7 @@ fn read_input_state_accepts_pubkey_and_bool_fields_under_selector_dispatch() { entrypoint function main() { State s = readInputState(this.activeInputIndex); require(s.flag); - require(s.owner == 0x0202020202020202020202020202020202020202020202020202020202020202); + require(s.owner == pubkey(0x0202020202020202020202020202020202020202020202020202020202020202)); } } "#; @@ -5714,7 +5805,7 @@ fn compiles_opcode_builtins() { r#" contract Test() { entrypoint function main() { - require(OpSha256(bytes("msg")) == bytes("hash")); + require(byte[](OpSha256(bytes("msg"))) == byte[]("hash")); } } "#, @@ -5967,7 +6058,30 @@ fn compiles_opcode_builtins() { r#" contract Test() { entrypoint function main() { - require(OpTxInputIsCoinbase(0) == 0); + require(OpTxInputDaaScore(0) == 0); + } + } + "#, + ScriptBuilder::new() + .add_i64(0) + .unwrap() + .add_op(OpTxInputDaaScore) + .unwrap() + .add_i64(0) + .unwrap() + .add_op(OpNumEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(), + ), + ( + r#" + contract Test() { + entrypoint function main() { + require(OpTxInputIsCoinbase(0) == false); } } "#, @@ -6138,7 +6252,7 @@ fn compiles_opcode_builtins() { r#" contract Test() { entrypoint function main() { - require(OpInputCovenantId(0) == bytes("cov")); + require(byte[](OpInputCovenantId(0)) == bytes("cov")); } } "#, @@ -6451,7 +6565,7 @@ fn executes_opcode_builtins_basic() { r#" contract Test() { entrypoint function main() { - require(OpTxInputIsCoinbase(0) == 0); + require(OpTxInputIsCoinbase(0) == bool(0)); } } "#, @@ -6525,7 +6639,7 @@ fn executes_opcode_builtins_covenants() { entrypoint function main() { require(OpAuthOutputCount(0) == 2); require(OpAuthOutputIdx(0, 1) == 2); - require(OpInputCovenantId(0) == bytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")); + require(byte[](OpInputCovenantId(0)) == bytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")); require(OpCovInputCount(bytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) == 2); require(OpCovInputIdx(bytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 1) == 2); require(OpCovOutputCount(bytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) == 2); @@ -7008,8 +7122,8 @@ fn compile_time_length_for_inferred_array_sizes() { let source = r#" contract Test() { entrypoint function test() { - byte[] data = 0x1234abcd; - int[] nums = [1, 2, 3]; + byte[_] data = 0x1234abcd; + int[_] nums = [1, 2, 3]; require(data.length == 4); require(nums.length == 3); } diff --git a/silverscript-lang/tests/examples/comments.sil b/silverscript-lang/tests/examples/comments.sil index d54d0ab..8b28884 100644 --- a/silverscript-lang/tests/examples/comments.sil +++ b/silverscript-lang/tests/examples/comments.sil @@ -19,6 +19,6 @@ contract Test(int x) { require(i < 20); require(checkSig(s, pk)); } else - require(b == 0x01); + require(b == byte[](0x01)); } } diff --git a/silverscript-lang/tests/examples/covenant.sil b/silverscript-lang/tests/examples/covenant.sil index ec96743..ce3aac9 100644 --- a/silverscript-lang/tests/examples/covenant.sil +++ b/silverscript-lang/tests/examples/covenant.sil @@ -3,6 +3,6 @@ pragma silverscript ^0.1.0; contract Covenant(int requiredVersion) { entrypoint function spend() { require(tx.version == requiredVersion); - require(this.activeScriptPubKey == 0x00); + require(this.activeScriptPubKey == byte[](0x00)); } } diff --git a/silverscript-lang/tests/examples/covenant_escrow.sil b/silverscript-lang/tests/examples/covenant_escrow.sil index 3049cd3..ab7f80e 100644 --- a/silverscript-lang/tests/examples/covenant_escrow.sil +++ b/silverscript-lang/tests/examples/covenant_escrow.sil @@ -13,8 +13,8 @@ contract Escrow(byte[32] arbiter, pubkey buyer, pubkey seller) { // Check that the transaction sends to either the buyer or the seller byte[34] buyerLock = new ScriptPubKeyP2PK(buyer); byte[34] sellerLock = new ScriptPubKeyP2PK(seller); - bool sendsToBuyer = tx.outputs[0].scriptPubKey == buyerLock; - bool sendsToSeller = tx.outputs[0].scriptPubKey == sellerLock; + bool sendsToBuyer = tx.outputs[0].scriptPubKey == byte[](buyerLock); + bool sendsToSeller = tx.outputs[0].scriptPubKey == byte[](sellerLock); require(sendsToBuyer || sendsToSeller); } } diff --git a/silverscript-lang/tests/examples/covenant_mecenas.sil b/silverscript-lang/tests/examples/covenant_mecenas.sil index 2c6cb8f..f0edcdc 100644 --- a/silverscript-lang/tests/examples/covenant_mecenas.sil +++ b/silverscript-lang/tests/examples/covenant_mecenas.sil @@ -6,7 +6,7 @@ contract Mecenas(pubkey recipient, byte[32] funder, int pledge, int period) { // Check that the first output sends to the recipient byte[34] recipientScriptPubKey = new ScriptPubKeyP2PK(recipient); - require(tx.outputs[0].scriptPubKey == recipientScriptPubKey); + require(tx.outputs[0].scriptPubKey == byte[](recipientScriptPubKey)); // Calculate the value that's left int minerFee = 1000; diff --git a/silverscript-lang/tests/examples/double_split.sil b/silverscript-lang/tests/examples/double_split.sil index 6d748a6..a247e75 100644 --- a/silverscript-lang/tests/examples/double_split.sil +++ b/silverscript-lang/tests/examples/double_split.sil @@ -3,6 +3,6 @@ pragma silverscript ^0.1.0; contract DoubleSplit(byte[20] pkh) { entrypoint function spend() { byte[] actualPkh = tx.inputs[this.activeInputIndex].scriptPubKey.split(23)[0].split(3)[1]; - require(pkh == actualPkh); + require(byte[](pkh) == actualPkh); } } diff --git a/silverscript-lang/tests/examples/mecenas.sil b/silverscript-lang/tests/examples/mecenas.sil index b80c83e..519e8eb 100644 --- a/silverscript-lang/tests/examples/mecenas.sil +++ b/silverscript-lang/tests/examples/mecenas.sil @@ -5,7 +5,7 @@ contract Mecenas(pubkey recipient, byte[32] funder, int pledge/*, int period */) // require(this.age >= period); // Check that the first output sends to the recipient - require(tx.outputs[0].scriptPubKey == new ScriptPubKeyP2PK(recipient)); + require(tx.outputs[0].scriptPubKey == byte[](new ScriptPubKeyP2PK(recipient))); int minerFee = 1000; int currentValue = tx.inputs[this.activeInputIndex].value; diff --git a/silverscript-lang/tests/examples/mecenas_locktime.sil b/silverscript-lang/tests/examples/mecenas_locktime.sil index f275463..1310a49 100644 --- a/silverscript-lang/tests/examples/mecenas_locktime.sil +++ b/silverscript-lang/tests/examples/mecenas_locktime.sil @@ -8,7 +8,7 @@ contract Mecenas( ) { entrypoint function receive() { byte[34] recipientScriptPubKey = new ScriptPubKeyP2PK(recipient); - require(tx.outputs[0].scriptPubKey == recipientScriptPubKey); + require(tx.outputs[0].scriptPubKey == byte[](recipientScriptPubKey)); int initial = int(initialBlock); require(tx.time >= initial); @@ -28,7 +28,7 @@ contract Mecenas( byte[] bcValue = 8 + byte[8](tx.locktime) + this.activeScriptPubKey.split(9)[1]; byte[35] lockValue = new ScriptPubKeyP2SH(blake2b(bcValue)); - require(tx.outputs[1].scriptPubKey == lockValue); + require(tx.outputs[1].scriptPubKey == byte[](lockValue)); } } diff --git a/silverscript-lang/tests/examples/p2pkh_invalid.sil b/silverscript-lang/tests/examples/p2pkh_invalid.sil index 730ebf3..7b8cf92 100644 --- a/silverscript-lang/tests/examples/p2pkh_invalid.sil +++ b/silverscript-lang/tests/examples/p2pkh_invalid.sil @@ -2,7 +2,7 @@ pragma silverscript ^0.1.0; contract P2PKH(byte[20] pkh) { entrypoint function spend(pubkey pk, sig s) { - require(blake2b(pk) == s); + require(byte[](blake2b(pk)) == byte[](s)); require(checkSig(s, pk)); } } diff --git a/silverscript-lang/tests/examples/simple_splice.sil b/silverscript-lang/tests/examples/simple_splice.sil index 3ae463b..fca3bca 100644 --- a/silverscript-lang/tests/examples/simple_splice.sil +++ b/silverscript-lang/tests/examples/simple_splice.sil @@ -4,6 +4,6 @@ contract Test(byte[] b) { entrypoint function spend() { byte[] x = b.split(5)[1]; require(x != b); - require (b.split(4)[0] != x); + require (byte[](b.split(4)[0]) != x); } } diff --git a/silverscript-lang/tests/examples/simulating_state.sil b/silverscript-lang/tests/examples/simulating_state.sil index f71fdc9..641b22c 100644 --- a/silverscript-lang/tests/examples/simulating_state.sil +++ b/silverscript-lang/tests/examples/simulating_state.sil @@ -9,7 +9,7 @@ contract SimulatingState( entrypoint function receive() { // Check that the first output sends to the recipient byte[34] recipientScriptPubKey = new ScriptPubKeyP2PK(recipient); - require(tx.outputs[0].scriptPubKey == recipientScriptPubKey); + require(tx.outputs[0].scriptPubKey == byte[](recipientScriptPubKey)); // Check that time has passed and that time locks are enabled int initial = int(initialBlock); @@ -43,7 +43,7 @@ contract SimulatingState( // Create the locking bytecode for the new contract and check that // the change output sends to that contract byte[35] newContractLock = new ScriptPubKeyP2SH(blake2b(newContract)); - require(tx.outputs[1].scriptPubKey == newContractLock); + require(tx.outputs[1].scriptPubKey == byte[](newContractLock)); } } diff --git a/silverscript-lang/tests/examples/slice.sil b/silverscript-lang/tests/examples/slice.sil index ae35626..5db8106 100644 --- a/silverscript-lang/tests/examples/slice.sil +++ b/silverscript-lang/tests/examples/slice.sil @@ -3,6 +3,6 @@ pragma silverscript ^0.1.0; contract Slice(byte[20] pkh) { entrypoint function spend() { byte[] actualPkh = tx.inputs[this.activeInputIndex].scriptPubKey.slice(3, 23); - require(pkh == actualPkh); + require(byte[](pkh) == actualPkh); } } diff --git a/silverscript-lang/tests/examples/slice_variable_parameter.sil b/silverscript-lang/tests/examples/slice_variable_parameter.sil index ae0c04d..6d2c7f1 100644 --- a/silverscript-lang/tests/examples/slice_variable_parameter.sil +++ b/silverscript-lang/tests/examples/slice_variable_parameter.sil @@ -4,6 +4,6 @@ contract Slice(byte[20] pkh) { entrypoint function spend() { int x = 3; byte[] actualPkh = tx.inputs[this.activeInputIndex].scriptPubKey.slice(x, 23); - require(pkh == actualPkh); + require(byte[](pkh) == actualPkh); } } diff --git a/silverscript-lang/tests/examples/split_or_slice_signature.sil b/silverscript-lang/tests/examples/split_or_slice_signature.sil index 88c455c..b51c2f1 100644 --- a/silverscript-lang/tests/examples/split_or_slice_signature.sil +++ b/silverscript-lang/tests/examples/split_or_slice_signature.sil @@ -5,7 +5,7 @@ contract Test(sig signature) { // Assume Schnorr byte[] hashtype1 = signature.split(64)[1]; byte[1] hashtype2 = signature.slice(64, 65); - require(hashtype1 == 0x01); + require(hashtype1 == byte[](0x01)); require(hashtype2 == 0x01); } } diff --git a/silverscript-lang/tests/examples/split_size.sil b/silverscript-lang/tests/examples/split_size.sil index 934e243..3e4f02a 100644 --- a/silverscript-lang/tests/examples/split_size.sil +++ b/silverscript-lang/tests/examples/split_size.sil @@ -4,6 +4,6 @@ contract SplitSize(byte[] b) { entrypoint function spend() { byte[] x = b.split(b.length / 2)[1]; require(x != b); - require(b.split(4)[0] != x); + require(byte[](b.split(4)[0]) != x); } } diff --git a/silverscript-lang/tests/examples/split_typed.sil b/silverscript-lang/tests/examples/split_typed.sil index 9af7d48..66b04f7 100644 --- a/silverscript-lang/tests/examples/split_typed.sil +++ b/silverscript-lang/tests/examples/split_typed.sil @@ -3,6 +3,6 @@ pragma silverscript ^0.1.0; contract SplitTyped(byte[] b) { entrypoint function spend() { byte[4] x = b.split(4)[0]; - require(x != b); + require(byte[](x) != b); } } From 49696cb2afacc192daa6fb56a817224d27a8518b Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Tue, 24 Mar 2026 15:13:21 +0200 Subject: [PATCH 4/9] Fix push for initialized array --- silverscript-lang/src/compiler.rs | 4 +++- silverscript-lang/tests/compiler_tests.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index b0bc4b9..edad887 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6489,7 +6489,9 @@ fn expr_is_bytes_inner<'i>( match &expr.kind { ExprKind::Byte(_) => true, ExprKind::String(_) => true, - ExprKind::Array(values) => values.iter().all(|value| matches!(&value.kind, ExprKind::Byte(_))), + // Array literals are encoded to their packed byte representation at compile time, + // regardless of element type, so downstream bytewise ops must treat them as bytes. + ExprKind::Array(_) => true, ExprKind::Slice { .. } => true, ExprKind::New { name, .. } => matches!( name.as_str(), diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index f866ff7..effe0a2 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -3097,6 +3097,24 @@ fn runs_array_runtime_examples() { assert!(result.is_ok(), "array runtime example failed: {}", result.unwrap_err()); } +#[test] +fn runs_int_array_push_length_runtime_example() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[] x = [1, 2, 3]; + x.push(4); + require(x.length == 4); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "int[] push length runtime example failed: {}", result.unwrap_err()); +} + #[test] fn runs_slice_with_explicit_end_bounds() { let source = r#" From c3aeb06df16294867e77af3d795b70751e57ead3 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 26 Mar 2026 22:48:17 +0200 Subject: [PATCH 5/9] Allow conversion from byte[] to byte[N] without an extra opcode --- silverscript-lang/src/compiler.rs | 78 ++++++++++++++++++++--- silverscript-lang/tests/compiler_tests.rs | 60 +++++++++++++++++ 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index edad887..37eb512 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -393,14 +393,28 @@ fn lower_expr<'i>(expr: &Expr<'i>, scope: &LoweringScope, structs: &StructRegist ExprKind::StateObject(_) => { Err(CompilerError::Unsupported("struct literals are only supported in struct-typed positions".to_string())) } - ExprKind::Call { name, args, name_span } => Ok(Expr::new( - ExprKind::Call { - name: name.clone(), - args: args.iter().map(|arg| lower_expr(arg, scope, structs)).collect::, _>>()?, - name_span: *name_span, - }, - span, - )), + ExprKind::Call { name, args, name_span } => { + let lowered_args = args.iter().map(|arg| lower_expr(arg, scope, structs)).collect::, _>>()?; + if name.starts_with("byte[") && name.ends_with(']') { + let size_part = &name[5..name.len() - 1]; + if !size_part.is_empty() && lowered_args.len() == 1 { + let size = size_part + .parse::() + .map_err(|_| CompilerError::Unsupported(format!("{name}() is not supported")))?; + if let Some(source_type) = infer_lowered_expr_type_name(&lowered_args[0], scope) + && let Some(source_size) = byte_sequence_cast_size(&source_type) + && let Some(source_size) = source_size + && source_size != size + { + return Err(CompilerError::Unsupported(format!("cannot cast {source_type} to {name}"))); + } + } + } + Ok(Expr::new( + ExprKind::Call { name: name.clone(), args: lowered_args, name_span: *name_span }, + span, + )) + } ExprKind::New { name, args, name_span } => Ok(Expr::new( ExprKind::New { name: name.clone(), @@ -1469,6 +1483,15 @@ fn lower_runtime_expr<'i>( lower_expr(expr, &scope, structs) } +fn infer_lowered_expr_type_name<'i>(expr: &Expr<'i>, scope: &LoweringScope) -> Option { + let types = scope + .vars + .iter() + .map(|(name, type_ref)| (name.clone(), type_name_from_ref(type_ref))) + .collect::>(); + infer_debug_expr_value_type(expr, &HashMap::new(), &types, &mut HashSet::new()).ok() +} + fn lower_runtime_struct_expr<'i>( expr: &Expr<'i>, expected_type: &TypeRef, @@ -7213,6 +7236,31 @@ fn compile_call_expr<'i>( if args.len() != 1 { return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); } + let source_type = infer_debug_expr_value_type(&args[0], scope.env, scope.types, &mut HashSet::new()).ok(); + if let Some(source_type) = source_type.as_deref() { + if let Some(source_size) = byte_sequence_cast_size(source_type) { + if let Some(source_size) = source_size { + if source_size != size { + return Err(CompilerError::Unsupported(format!( + "cannot cast {source_type} to {name}" + ))); + } + } + compile_expr( + &args[0], + scope.env, + scope.stack_bindings, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + return Ok(()); + } + } compile_expr( &args[0], scope.env, @@ -7400,6 +7448,20 @@ fn is_bytes_type(type_name: &str) -> bool { is_array_type(type_name) } +fn byte_sequence_cast_size(type_name: &str) -> Option> { + match type_name { + "bytes" | "byte[]" | "string" => Some(None), + "byte" => Some(Some(1)), + "pubkey" => Some(Some(32)), + "sig" => Some(Some(65)), + "datasig" => Some(Some(64)), + _ => match array_element_type(type_name).as_deref() { + Some("byte") => Some(array_size(type_name).map(|size| size as i64)), + _ => None, + }, + } +} + fn build_null_data_script<'i>(arg: &Expr<'i>) -> Result, CompilerError> { let elements = match &arg.kind { ExprKind::Array(items) => items, diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index effe0a2..d3302e3 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -1078,6 +1078,66 @@ fn allows_comparing_dynamic_and_fixed_arrays_with_cast_in_function_scope() { assert!(compile_contract(source, &[], CompileOptions::default()).is_ok(), "byte[] param should compare to byte[](byte[2])"); } +#[test] +fn byte_array_to_fixed_byte_array_cast_compiles_without_num2bin() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[] route_templates = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f; + byte[32] target_template = byte[32](route_templates.slice(16, 48)); + require(byte[](target_template) == route_templates.slice(16, 48)); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("byte[] to byte[32] cast should compile"); + assert!( + !compiled.script.iter().copied().any(|op| op == OpNum2Bin), + "byte[] to byte[32] cast should not emit OpNum2Bin" + ); + assert!(run_script_with_selector(compiled.script, None).is_ok(), "byte[] to byte[32] cast should execute"); +} + +#[test] +fn rejects_cast_between_different_fixed_byte_array_sizes() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[32] hash = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f; + byte[31] truncated = byte[31](hash); + require(truncated.length == 31); + } + } + "#; + + let err = + compile_contract(source, &[], CompileOptions::default()).expect_err("byte[32] to byte[31] cast should be rejected"); + assert!( + err.to_string().contains("cannot cast byte[32] to byte[31]"), + "unexpected error: {err}" + ); +} + +#[test] +fn rejects_cast_from_smaller_fixed_byte_array_to_larger_fixed_byte_array() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[31] hash = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e; + byte[32] padded = byte[32](hash); + require(padded.length == 32); + } + } + "#; + + let err = + compile_contract(source, &[], CompileOptions::default()).expect_err("byte[31] to byte[32] cast should be rejected"); + assert!( + err.to_string().contains("cannot cast byte[31] to byte[32]"), + "unexpected error: {err}" + ); +} + #[test] fn build_sig_script_rejects_wrong_argument_count() { let source = r#" From 284102e7e3695914d7b2d2cdc4493797d4886b15 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 26 Mar 2026 23:00:34 +0200 Subject: [PATCH 6/9] Don't use minimal number encoding for byte values --- silverscript-lang/src/compiler.rs | 23 ++++++- .../src/compiler/debug_value_types.rs | 2 + silverscript-lang/tests/compiler_tests.rs | 69 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 37eb512..1823ee3 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -2110,6 +2110,16 @@ fn expr_matches_return_type_ref_hint<'i>(expr: &Expr<'i>, type_ref: &TypeRef) -> } } +fn coerce_expr_for_declared_scalar_type<'i>(expr: Expr<'i>, type_name: &str) -> Expr<'i> { + if type_name == "byte" + && let ExprKind::Int(value) = expr.kind + && (0..=255).contains(&value) + { + return Expr::new(ExprKind::Byte(value as u8), expr.span); + } + expr +} + fn infer_fixed_array_type_from_initializer_ref<'i>( declared_type: &TypeRef, initializer: Option<&Expr<'i>>, @@ -2979,7 +2989,7 @@ fn compile_statement<'i>( } else { let expr = expr.clone().ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; - let expr = lower_runtime_expr(&expr, types, structs)?; + let expr = coerce_expr_for_declared_scalar_type(lower_runtime_expr(&expr, types, structs)?, &effective_type_name); let expected_type_ref = parse_type_ref(&effective_type_name)?; if !expr_matches_return_type_ref(&expr, &expected_type_ref, types, contract_constants) { return Err(CompilerError::Unsupported(format!( @@ -3537,7 +3547,7 @@ fn compile_statement<'i>( // If this is a stack-bound scalar local, compile a real mutation instead of // rewriting `env[name]` (which can explode under unrolled control flow). if stack_bindings.contains(name) && is_not_struct { - let lowered_expr = lower_runtime_expr(expr, types, structs)?; + let lowered_expr = coerce_expr_for_declared_scalar_type(lower_runtime_expr(expr, types, structs)?, type_name); if !expr_matches_return_type_ref(&lowered_expr, &expected_type_ref, types, contract_constants) { return Err(CompilerError::Unsupported(format!( "variable '{}' expects {}{}", @@ -3667,7 +3677,7 @@ fn compile_statement<'i>( } } } - let lowered_expr = lower_runtime_expr(expr, types, structs)?; + let lowered_expr = coerce_expr_for_declared_scalar_type(lower_runtime_expr(expr, types, structs)?, type_name); if !expr_matches_return_type_ref(&lowered_expr, &expected_type_ref, types, contract_constants) { return Err(CompilerError::Unsupported(format!( "variable '{}' expects {}{}", @@ -6160,6 +6170,13 @@ fn compile_expr<'i>( } } } + let left_value_type = infer_debug_expr_value_type(left, env, types, &mut HashSet::new()).ok(); + let right_value_type = infer_debug_expr_value_type(right, env, types, &mut HashSet::new()).ok(); + if matches!(op, BinaryOp::Add) + && (left_value_type.as_deref() == Some("byte") || right_value_type.as_deref() == Some("byte")) + { + return Err(CompilerError::Unsupported("byte values do not support '+'".to_string())); + } let bytes_eq = matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); diff --git a/silverscript-lang/src/compiler/debug_value_types.rs b/silverscript-lang/src/compiler/debug_value_types.rs index 349c6d9..171eab2 100644 --- a/silverscript-lang/src/compiler/debug_value_types.rs +++ b/silverscript-lang/src/compiler/debug_value_types.rs @@ -125,6 +125,8 @@ pub(super) fn infer_debug_expr_value_type<'i>( let right_type = infer_debug_expr_value_type(right, env, types, visiting)?; if left_type == "string" || right_type == "string" { Ok("string".to_string()) + } else if left_type == "byte" || right_type == "byte" { + Ok("int".to_string()) } else if is_bytes_type(&left_type) { Ok(left_type) } else if is_bytes_type(&right_type) { diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index d3302e3..fdfe297 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -868,6 +868,75 @@ fn build_sig_script_builds_expected_script() { assert_eq!(sigscript, expected); } +#[test] +fn byte_variable_from_int_literal_uses_raw_byte_push() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 5; + require(OpBin2Num(x) == 5); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("byte int literal should compile"); + let expected = ScriptBuilder::new() + .add_data(&[5u8]) + .unwrap() + .add_op(OpBin2Num) + .unwrap() + .add_i64(5) + .unwrap() + .add_op(OpNumEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + assert_eq!(compiled.script, expected); + assert!(run_script_with_selector(compiled.script, None).is_ok(), "byte int literal script should execute"); +} + +#[test] +fn rejects_adding_byte_values() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 5; + byte y = 7; + require(x + y > 0); + } + } + "#; + + let err = compile_contract(source, &[], CompileOptions::default()).expect_err("byte addition should be rejected"); + assert!( + err.to_string().contains("byte values do not support '+'"), + "unexpected error: {err}" + ); +} + +#[test] +fn rejects_assigning_sum_of_byte_values_to_byte() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 5; + byte y = 7; + byte z = x + y; + require(OpBin2Num(z) == 12); + } + } + "#; + + let err = compile_contract(source, &[], CompileOptions::default()).expect_err("byte addition assignment should be rejected"); + assert!( + err.to_string().contains("byte values do not support '+'"), + "unexpected error: {err}" + ); +} + #[test] fn build_sig_script_rejects_unknown_function() { let source = r#" From ceae4111283f5051b553f1a7d364bcec4f3163a3 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 26 Mar 2026 23:21:18 +0200 Subject: [PATCH 7/9] Coerce literals to bytes on comparisons --- silverscript-lang/src/compiler.rs | 27 +++++-- silverscript-lang/tests/compiler_tests.rs | 85 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 1823ee3..19bc0ad 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -2120,6 +2120,16 @@ fn coerce_expr_for_declared_scalar_type<'i>(expr: Expr<'i>, type_name: &str) -> expr } +fn coerce_rhs_byte_literal_for_comparison<'i>(left_type: Option<&TypeRef>, right: &Expr<'i>) -> Expr<'i> { + if left_type.is_some_and(|type_ref| matches!(type_ref.base, TypeBase::Byte) && type_ref.array_dims.is_empty()) + && let ExprKind::Int(value) = right.kind + && (0..=255).contains(&value) + { + return Expr::new(ExprKind::Byte(value as u8), right.span); + } + right.clone() +} + fn infer_fixed_array_type_from_initializer_ref<'i>( declared_type: &TypeRef, initializer: Option<&Expr<'i>>, @@ -6154,12 +6164,15 @@ fn compile_expr<'i>( Ok(()) } ExprKind::Binary { op, left, right } => { + let left_cmp_type = infer_expr_type_ref_for_comparison(left, env, types); + let coerced_right = if matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) { + coerce_rhs_byte_literal_for_comparison(left_cmp_type.as_ref(), right) + } else { + right.as_ref().clone() + }; if matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) { if let (Some(left_type), Some(right_type)) = - ( - infer_expr_type_ref_for_comparison(left, env, types), - infer_expr_type_ref_for_comparison(right, env, types), - ) + (left_cmp_type.clone(), infer_expr_type_ref_for_comparison(&coerced_right, env, types)) { if !comparison_types_compatible(&left_type, &right_type) { return Err(CompilerError::Unsupported(format!( @@ -6171,14 +6184,14 @@ fn compile_expr<'i>( } } let left_value_type = infer_debug_expr_value_type(left, env, types, &mut HashSet::new()).ok(); - let right_value_type = infer_debug_expr_value_type(right, env, types, &mut HashSet::new()).ok(); + let right_value_type = infer_debug_expr_value_type(&coerced_right, env, types, &mut HashSet::new()).ok(); if matches!(op, BinaryOp::Add) && (left_value_type.as_deref() == Some("byte") || right_value_type.as_deref() == Some("byte")) { return Err(CompilerError::Unsupported("byte values do not support '+'".to_string())); } let bytes_eq = - matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); + matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(&coerced_right, env, types)); let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); if bytes_add { compile_concat_operand( @@ -6219,7 +6232,7 @@ fn compile_expr<'i>( contract_constants, )?; compile_expr( - right, + &coerced_right, env, stack_bindings, types, diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index fdfe297..b2b5acb 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -898,6 +898,91 @@ fn byte_variable_from_int_literal_uses_raw_byte_push() { assert!(run_script_with_selector(compiled.script, None).is_ok(), "byte int literal script should execute"); } +#[test] +fn byte_variable_from_out_of_range_int_literal_is_rejected() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 256; + require(true); + } + } + "#; + + assert!( + compile_contract(source, &[], CompileOptions::default()).is_err(), + "byte x = 256 should be rejected" + ); +} + +#[test] +fn byte_equality_uses_op_equal_not_op_numequal() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 5; + byte y = 7; + require(x == y); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("byte equality should compile"); + assert!( + compiled.script.iter().copied().any(|op| op == OpEqual), + "byte equality should use OP_EQUAL" + ); + assert!( + !compiled.script.iter().copied().any(|op| op == OpNumEqual), + "byte equality should not use OP_NUMEQUAL" + ); +} + +#[test] +fn byte_equality_with_rhs_int_literal_uses_raw_byte_push() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 1; + require(x == 1); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("byte equality with rhs literal should compile"); + let expected = ScriptBuilder::new() + .add_data(&[1u8]) + .unwrap() + .add_data(&[1u8]) + .unwrap() + .add_op(OpEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + assert_eq!(compiled.script, expected); + assert!(run_script_with_selector(compiled.script, None).is_ok(), "byte equality with rhs literal should execute"); +} + +#[test] +fn byte_equality_with_out_of_range_rhs_int_literal_is_rejected() { + let source = r#" + contract Bytes() { + entrypoint function main() { + byte x = 5; + require(x == 256); + } + } + "#; + + assert!( + compile_contract(source, &[], CompileOptions::default()).is_err(), + "x == 256 should be rejected when x is a byte" + ); +} + #[test] fn rejects_adding_byte_values() { let source = r#" From 41ce5ec3ce1e99aca0b2aa92a5bfebffe73fb587 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 26 Mar 2026 23:36:08 +0200 Subject: [PATCH 8/9] Add casts to chess files --- .../tests/apps/chess/chess_castle.sil | 2 +- .../apps/chess/chess_castle_challenge.sil | 4 +- .../tests/apps/chess/chess_diag.sil | 2 +- .../tests/apps/chess/chess_horiz.sil | 2 +- .../tests/apps/chess/chess_king.sil | 2 +- .../tests/apps/chess/chess_knight.sil | 2 +- .../tests/apps/chess/chess_mux.sil | 6 +-- .../tests/apps/chess/chess_pawn.sil | 2 +- .../tests/apps/chess/chess_vert.sil | 2 +- silverscript-lang/tests/apps/chess/player.sil | 2 +- silverscript-lang/tests/chess_apps_tests.rs | 46 +++++++++---------- 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/silverscript-lang/tests/apps/chess/chess_castle.sil b/silverscript-lang/tests/apps/chess/chess_castle.sil index 92803d8..9d308a4 100644 --- a/silverscript-lang/tests/apps/chess/chess_castle.sil +++ b/silverscript-lang/tests/apps/chess/chess_castle.sil @@ -226,7 +226,7 @@ contract ChessCastle( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_castle_challenge.sil b/silverscript-lang/tests/apps/chess/chess_castle_challenge.sil index 3fcdc5d..dddff28 100644 --- a/silverscript-lang/tests/apps/chess/chess_castle_challenge.sil +++ b/silverscript-lang/tests/apps/chess/chess_castle_challenge.sil @@ -201,7 +201,7 @@ contract ChessCastleChallengePrep( int hash_start = selector * 32; int hash_end = hash_start + 32; - byte[32] target_template = route_templates.slice(hash_start, hash_end); + byte[32] target_template = byte[32](route_templates.slice(hash_start, hash_end)); // After prep the challenger is committed to this proof move. The next // worker sees recent_castle != 0 and must settle immediately on success. @@ -252,7 +252,7 @@ contract ChessCastleChallengePrep( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_diag.sil b/silverscript-lang/tests/apps/chess/chess_diag.sil index e053f32..5cfdda6 100644 --- a/silverscript-lang/tests/apps/chess/chess_diag.sil +++ b/silverscript-lang/tests/apps/chess/chess_diag.sil @@ -245,7 +245,7 @@ contract ChessDiag( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_horiz.sil b/silverscript-lang/tests/apps/chess/chess_horiz.sil index 5b19a78..ba90d2d 100644 --- a/silverscript-lang/tests/apps/chess/chess_horiz.sil +++ b/silverscript-lang/tests/apps/chess/chess_horiz.sil @@ -260,7 +260,7 @@ contract ChessHoriz( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_king.sil b/silverscript-lang/tests/apps/chess/chess_king.sil index 3088081..9bd288c 100644 --- a/silverscript-lang/tests/apps/chess/chess_king.sil +++ b/silverscript-lang/tests/apps/chess/chess_king.sil @@ -245,7 +245,7 @@ contract ChessKing( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_knight.sil b/silverscript-lang/tests/apps/chess/chess_knight.sil index 5db4848..6caebad 100644 --- a/silverscript-lang/tests/apps/chess/chess_knight.sil +++ b/silverscript-lang/tests/apps/chess/chess_knight.sil @@ -231,7 +231,7 @@ contract ChessKnight( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_mux.sil b/silverscript-lang/tests/apps/chess/chess_mux.sil index 4c8cd99..52978f0 100644 --- a/silverscript-lang/tests/apps/chess/chess_mux.sil +++ b/silverscript-lang/tests/apps/chess/chess_mux.sil @@ -213,7 +213,7 @@ contract ChessMux( byte[] all_route_templates = move_route_templates + mux_template; int hash_start = selector * 32; int hash_end = hash_start + 32; - byte[32] target_template = all_route_templates.slice(hash_start, hash_end); + byte[32] target_template = byte[32](all_route_templates.slice(hash_start, hash_end)); validateOutputStateWithTemplate( output_idx, next_state, @@ -261,7 +261,7 @@ contract ChessMux( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, @@ -278,7 +278,7 @@ contract ChessMux( entrypoint function settle(byte[32] player_template, byte[] settle_prefix, byte[] settle_suffix) { require(status != 0 /*LIVE*/); byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_pawn.sil b/silverscript-lang/tests/apps/chess/chess_pawn.sil index eebe436..060eca9 100644 --- a/silverscript-lang/tests/apps/chess/chess_pawn.sil +++ b/silverscript-lang/tests/apps/chess/chess_pawn.sil @@ -343,7 +343,7 @@ contract ChessPawn( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/chess_vert.sil b/silverscript-lang/tests/apps/chess/chess_vert.sil index cea98b1..84a25a2 100644 --- a/silverscript-lang/tests/apps/chess/chess_vert.sil +++ b/silverscript-lang/tests/apps/chess/chess_vert.sil @@ -260,7 +260,7 @@ contract ChessVert( } byte[32] settle_template = blake2b(settle_prefix + settle_suffix); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState next_state = { player_template: player_template, diff --git a/silverscript-lang/tests/apps/chess/player.sil b/silverscript-lang/tests/apps/chess/player.sil index 184035e..52c48e7 100644 --- a/silverscript-lang/tests/apps/chess/player.sil +++ b/silverscript-lang/tests/apps/chess/player.sil @@ -189,7 +189,7 @@ contract Player( int leader_input_idx = OpCovInputIdx(cov_id, 0); require(blake2b(route_templates) == routes_commitment); - require(blake2b(settle_template + player_template) == route_templates.slice(256, 288)); + require(blake2b(settle_template + player_template) == byte[32](route_templates.slice(256, 288))); SettleState leader = readInputStateWithTemplate(leader_input_idx, settle_prefix_len, settle_suffix_len, settle_template); // Settlement is driven by a terminal settle worker state, not by a diff --git a/silverscript-lang/tests/chess_apps_tests.rs b/silverscript-lang/tests/chess_apps_tests.rs index 59ea464..23ddee3 100644 --- a/silverscript-lang/tests/chess_apps_tests.rs +++ b/silverscript-lang/tests/chess_apps_tests.rs @@ -678,58 +678,58 @@ fn size_snapshots() -> Vec { SizeSnapshot { name: "chess_pawn.sil", ctor: pawn_constructor_args, - expected_script_len: 1846, - expected_instruction_count: 1219, + expected_script_len: 1833, + expected_instruction_count: 1208, expected_charged_op_count: 794, }, SizeSnapshot { name: "chess_knight.sil", ctor: pawn_constructor_args, - expected_script_len: 1392, - expected_instruction_count: 801, - expected_charged_op_count: 529, + expected_script_len: 1383, + expected_instruction_count: 794, + expected_charged_op_count: 527, }, SizeSnapshot { name: "chess_vert.sil", ctor: pawn_constructor_args, - expected_script_len: 2060, - expected_instruction_count: 1416, - expected_charged_op_count: 925, + expected_script_len: 2035, + expected_instruction_count: 1393, + expected_charged_op_count: 915, }, SizeSnapshot { name: "chess_horiz.sil", ctor: pawn_constructor_args, - expected_script_len: 2060, - expected_instruction_count: 1416, - expected_charged_op_count: 925, + expected_script_len: 2035, + expected_instruction_count: 1393, + expected_charged_op_count: 915, }, SizeSnapshot { name: "chess_diag.sil", ctor: pawn_constructor_args, - expected_script_len: 1823, - expected_instruction_count: 1217, - expected_charged_op_count: 795, + expected_script_len: 1814, + expected_instruction_count: 1210, + expected_charged_op_count: 793, }, SizeSnapshot { name: "chess_king.sil", ctor: pawn_constructor_args, - expected_script_len: 1537, - expected_instruction_count: 944, - expected_charged_op_count: 621, + expected_script_len: 1512, + expected_instruction_count: 921, + expected_charged_op_count: 611, }, SizeSnapshot { name: "chess_castle.sil", ctor: pawn_constructor_args, - expected_script_len: 1548, - expected_instruction_count: 951, - expected_charged_op_count: 617, + expected_script_len: 1523, + expected_instruction_count: 928, + expected_charged_op_count: 608, }, SizeSnapshot { name: "chess_castle_challenge.sil", ctor: pawn_constructor_args, - expected_script_len: 1762, - expected_instruction_count: 1149, - expected_charged_op_count: 744, + expected_script_len: 1735, + expected_instruction_count: 1124, + expected_charged_op_count: 733, }, ] } From 5b15212a83c90c23a5a353502edf3179f80b8978 Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 26 Mar 2026 23:37:13 +0200 Subject: [PATCH 9/9] fmt --- silverscript-lang/src/compiler.rs | 38 +++++++---------- silverscript-lang/tests/compiler_tests.rs | 51 +++++------------------ 2 files changed, 25 insertions(+), 64 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 19bc0ad..b5038f1 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -398,9 +398,8 @@ fn lower_expr<'i>(expr: &Expr<'i>, scope: &LoweringScope, structs: &StructRegist if name.starts_with("byte[") && name.ends_with(']') { let size_part = &name[5..name.len() - 1]; if !size_part.is_empty() && lowered_args.len() == 1 { - let size = size_part - .parse::() - .map_err(|_| CompilerError::Unsupported(format!("{name}() is not supported")))?; + let size = + size_part.parse::().map_err(|_| CompilerError::Unsupported(format!("{name}() is not supported")))?; if let Some(source_type) = infer_lowered_expr_type_name(&lowered_args[0], scope) && let Some(source_size) = byte_sequence_cast_size(&source_type) && let Some(source_size) = source_size @@ -410,10 +409,7 @@ fn lower_expr<'i>(expr: &Expr<'i>, scope: &LoweringScope, structs: &StructRegist } } } - Ok(Expr::new( - ExprKind::Call { name: name.clone(), args: lowered_args, name_span: *name_span }, - span, - )) + Ok(Expr::new(ExprKind::Call { name: name.clone(), args: lowered_args, name_span: *name_span }, span)) } ExprKind::New { name, args, name_span } => Ok(Expr::new( ExprKind::New { @@ -1333,8 +1329,7 @@ fn infer_expr_type_ref_for_comparison<'i>( } if let ExprKind::Call { name, .. } = &expr.kind { let is_builtin_cast = matches!(name.as_str(), "int" | "bool" | "byte" | "string" | "pubkey" | "sig" | "datasig") - || (name.contains('[') - && parse_type_ref(name).ok().is_some_and(|type_ref| !matches!(type_ref.base, TypeBase::Custom(_)))); + || (name.contains('[') && parse_type_ref(name).ok().is_some_and(|type_ref| !matches!(type_ref.base, TypeBase::Custom(_)))); let is_known_builtin = matches!( name.as_str(), "int" @@ -1484,11 +1479,7 @@ fn lower_runtime_expr<'i>( } fn infer_lowered_expr_type_name<'i>(expr: &Expr<'i>, scope: &LoweringScope) -> Option { - let types = scope - .vars - .iter() - .map(|(name, type_ref)| (name.clone(), type_name_from_ref(type_ref))) - .collect::>(); + let types = scope.vars.iter().map(|(name, type_ref)| (name.clone(), type_name_from_ref(type_ref))).collect::>(); infer_debug_expr_value_type(expr, &HashMap::new(), &types, &mut HashSet::new()).ok() } @@ -6165,11 +6156,12 @@ fn compile_expr<'i>( } ExprKind::Binary { op, left, right } => { let left_cmp_type = infer_expr_type_ref_for_comparison(left, env, types); - let coerced_right = if matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) { - coerce_rhs_byte_literal_for_comparison(left_cmp_type.as_ref(), right) - } else { - right.as_ref().clone() - }; + let coerced_right = + if matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) { + coerce_rhs_byte_literal_for_comparison(left_cmp_type.as_ref(), right) + } else { + right.as_ref().clone() + }; if matches!(op, BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge) { if let (Some(left_type), Some(right_type)) = (left_cmp_type.clone(), infer_expr_type_ref_for_comparison(&coerced_right, env, types)) @@ -6190,8 +6182,8 @@ fn compile_expr<'i>( { return Err(CompilerError::Unsupported("byte values do not support '+'".to_string())); } - let bytes_eq = - matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(&coerced_right, env, types)); + let bytes_eq = matches!(op, BinaryOp::Eq | BinaryOp::Ne) + && (expr_is_bytes(left, env, types) || expr_is_bytes(&coerced_right, env, types)); let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); if bytes_add { compile_concat_operand( @@ -7271,9 +7263,7 @@ fn compile_call_expr<'i>( if let Some(source_size) = byte_sequence_cast_size(source_type) { if let Some(source_size) = source_size { if source_size != size { - return Err(CompilerError::Unsupported(format!( - "cannot cast {source_type} to {name}" - ))); + return Err(CompilerError::Unsupported(format!("cannot cast {source_type} to {name}"))); } } compile_expr( diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index b2b5acb..d2db2cb 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -909,10 +909,7 @@ fn byte_variable_from_out_of_range_int_literal_is_rejected() { } "#; - assert!( - compile_contract(source, &[], CompileOptions::default()).is_err(), - "byte x = 256 should be rejected" - ); + assert!(compile_contract(source, &[], CompileOptions::default()).is_err(), "byte x = 256 should be rejected"); } #[test] @@ -928,14 +925,8 @@ fn byte_equality_uses_op_equal_not_op_numequal() { "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("byte equality should compile"); - assert!( - compiled.script.iter().copied().any(|op| op == OpEqual), - "byte equality should use OP_EQUAL" - ); - assert!( - !compiled.script.iter().copied().any(|op| op == OpNumEqual), - "byte equality should not use OP_NUMEQUAL" - ); + assert!(compiled.script.iter().copied().any(|op| op == OpEqual), "byte equality should use OP_EQUAL"); + assert!(!compiled.script.iter().copied().any(|op| op == OpNumEqual), "byte equality should not use OP_NUMEQUAL"); } #[test] @@ -977,10 +968,7 @@ fn byte_equality_with_out_of_range_rhs_int_literal_is_rejected() { } "#; - assert!( - compile_contract(source, &[], CompileOptions::default()).is_err(), - "x == 256 should be rejected when x is a byte" - ); + assert!(compile_contract(source, &[], CompileOptions::default()).is_err(), "x == 256 should be rejected when x is a byte"); } #[test] @@ -996,10 +984,7 @@ fn rejects_adding_byte_values() { "#; let err = compile_contract(source, &[], CompileOptions::default()).expect_err("byte addition should be rejected"); - assert!( - err.to_string().contains("byte values do not support '+'"), - "unexpected error: {err}" - ); + assert!(err.to_string().contains("byte values do not support '+'"), "unexpected error: {err}"); } #[test] @@ -1016,10 +1001,7 @@ fn rejects_assigning_sum_of_byte_values_to_byte() { "#; let err = compile_contract(source, &[], CompileOptions::default()).expect_err("byte addition assignment should be rejected"); - assert!( - err.to_string().contains("byte values do not support '+'"), - "unexpected error: {err}" - ); + assert!(err.to_string().contains("byte values do not support '+'"), "unexpected error: {err}"); } #[test] @@ -1245,10 +1227,7 @@ fn byte_array_to_fixed_byte_array_cast_compiles_without_num2bin() { "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("byte[] to byte[32] cast should compile"); - assert!( - !compiled.script.iter().copied().any(|op| op == OpNum2Bin), - "byte[] to byte[32] cast should not emit OpNum2Bin" - ); + assert!(!compiled.script.iter().copied().any(|op| op == OpNum2Bin), "byte[] to byte[32] cast should not emit OpNum2Bin"); assert!(run_script_with_selector(compiled.script, None).is_ok(), "byte[] to byte[32] cast should execute"); } @@ -1264,12 +1243,8 @@ fn rejects_cast_between_different_fixed_byte_array_sizes() { } "#; - let err = - compile_contract(source, &[], CompileOptions::default()).expect_err("byte[32] to byte[31] cast should be rejected"); - assert!( - err.to_string().contains("cannot cast byte[32] to byte[31]"), - "unexpected error: {err}" - ); + let err = compile_contract(source, &[], CompileOptions::default()).expect_err("byte[32] to byte[31] cast should be rejected"); + assert!(err.to_string().contains("cannot cast byte[32] to byte[31]"), "unexpected error: {err}"); } #[test] @@ -1284,12 +1259,8 @@ fn rejects_cast_from_smaller_fixed_byte_array_to_larger_fixed_byte_array() { } "#; - let err = - compile_contract(source, &[], CompileOptions::default()).expect_err("byte[31] to byte[32] cast should be rejected"); - assert!( - err.to_string().contains("cannot cast byte[31] to byte[32]"), - "unexpected error: {err}" - ); + let err = compile_contract(source, &[], CompileOptions::default()).expect_err("byte[31] to byte[32] cast should be rejected"); + assert!(err.to_string().contains("cannot cast byte[31] to byte[32]"), "unexpected error: {err}"); } #[test]