From febdd6f1dcf80016368bf090268b2399cc99761d Mon Sep 17 00:00:00 2001 From: Sean Young Date: Fri, 15 Sep 2023 11:55:56 +0100 Subject: [PATCH 1/9] Fix type(int256).min in comparisons (#1527) When resolving comparisons, we don't know what the types of the operands are so we resolve with `ResolveTo::Unknown`. In this path, there is an mistake in the bounds check for negative integers. Fixes https://github.com/hyperledger/solang/issues/1523 Signed-off-by: Sean Young --- src/sema/expression/integers.rs | 10 ++++-- .../solana/large_negative_ints.sol | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/contract_testcases/solana/large_negative_ints.sol diff --git a/src/sema/expression/integers.rs b/src/sema/expression/integers.rs index a40976b8e..eb9862b76 100644 --- a/src/sema/expression/integers.rs +++ b/src/sema/expression/integers.rs @@ -188,8 +188,6 @@ pub fn bigint_to_expression( resolve_to: ResolveTo, hex_str_len: Option, ) -> Result { - let bits = n.bits(); - if let ResolveTo::Type(resolve_to) = resolve_to { if *resolve_to != Type::Unresolved { if !(resolve_to.is_integer(ns) || matches!(resolve_to, Type::Bytes(_)) && n.is_zero()) { @@ -208,6 +206,14 @@ pub fn bigint_to_expression( } } + // BigInt bits() returns the bits used without the sign. Negative value is allowed to be one + // larger than positive value, e.g int8 has inclusive range -128 to 127. + let bits = if n.sign() == Sign::Minus { + (n + 1u32).bits() + } else { + n.bits() + }; + // Return smallest type; hex literals with odd length are not allowed. let int_size = hex_str_len .map(|v| if v % 2 == 0 { v as u64 * 4 } else { bits }) diff --git a/tests/contract_testcases/solana/large_negative_ints.sol b/tests/contract_testcases/solana/large_negative_ints.sol new file mode 100644 index 000000000..727c3ca21 --- /dev/null +++ b/tests/contract_testcases/solana/large_negative_ints.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.8.0; + +library Issue1523 { + function i1523_fail1(int256 x) internal pure returns (bool) { + return x <= type(int256).min; + } + + function i1523_fail2(int256 x) internal pure returns (bool) { + return x <= type(int256).min + 1; + } + + function i1523_fail3(int256 x) internal pure returns (bool) { + // Actual min value inlined + return x <= -57896044618658097711785492504343953926634992332820282019728792003956564819968; + } + + function i1523_fail4(int256 x) internal pure returns (bool) { + // Actual min value + 1 + return x <= -57896044618658097711785492504343953926634992332820282019728792003956564819967; + } + + function i1523_pass1() internal pure returns (int256) { + return type(int256).min + 1; + } + + function i1523_pass2() internal pure returns (int256) { + return type(int256).min; + } +} + +// ---- Expect: diagnostics ---- From f8d5a142c7b098ed3d140a5c13ccfe6902b4751e Mon Sep 17 00:00:00 2001 From: Sean Young Date: Fri, 15 Sep 2023 13:20:07 +0100 Subject: [PATCH 2/9] Add support for custom tags (#1529) Fixes https://github.com/hyperledger/solang/issues/1524 Signed-off-by: Sean Young --- src/sema/tags.rs | 52 +++++++++++++++++++++++++------------- tests/solana_tests/tags.rs | 11 ++++++++ 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/sema/tags.rs b/src/sema/tags.rs index a7aa8bcd5..8c86f76c9 100644 --- a/src/sema/tags.rs +++ b/src/sema/tags.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 use super::ast::{Diagnostic, Namespace, Parameter, Tag}; -use solang_parser::{doccomment::DocComment, pt}; +use solang_parser::{ + doccomment::{DocComment, DocCommentTag}, + pt, +}; use std::fmt::Write; /// Resolve the tags for a type from parsed doccomment @@ -23,18 +26,7 @@ pub fn resolve_tags( match c.tag.as_str() { "notice" | "author" | "title" | "dev" => { - // fold fields with the same name - if let Some(prev) = res.iter_mut().find(|e| e.tag == c.tag) { - prev.value.push(' '); - prev.value.push_str(&c.value); - } else { - res.push(Tag { - loc, - tag: c.tag.to_owned(), - value: c.value.to_owned(), - no: 0, - }) - } + add_tag(loc, &mut res, c); } "param" if params.is_some() => { let v: Vec<&str> = c.value.splitn(2, char::is_whitespace).collect(); @@ -167,10 +159,21 @@ pub fn resolve_tags( } } _ => { - ns.diagnostics.push(Diagnostic::error( - tag_loc, - format!("tag '@{}' is not valid for {}", c.tag, ty), - )); + if let Some(custom) = c.tag.strip_prefix("custom:") { + if custom.is_empty() { + ns.diagnostics.push(Diagnostic::error( + tag_loc, + format!("custom tag '@{}' is missing a name", c.tag), + )); + } else { + add_tag(loc, &mut res, c); + } + } else { + ns.diagnostics.push(Diagnostic::error( + tag_loc, + format!("tag '@{}' is not valid for {}", c.tag, ty), + )); + } } } } @@ -178,6 +181,21 @@ pub fn resolve_tags( res } +/// Add a new doc comment as a tag, or append to existing one +fn add_tag(loc: pt::Loc, res: &mut Vec, doc_comment: &DocCommentTag) { + if let Some(prev) = res.iter_mut().find(|e| e.tag == doc_comment.tag) { + prev.value.push(' '); + prev.value.push_str(&doc_comment.value); + } else { + res.push(Tag { + loc, + tag: doc_comment.tag.to_owned(), + value: doc_comment.value.to_owned(), + no: 0, + }) + } +} + /// Render tags as plain text string pub fn render(tags: &[Tag]) -> String { let mut s = String::new(); diff --git a/tests/solana_tests/tags.rs b/tests/solana_tests/tags.rs index 2b96453aa..18d7f33ee 100644 --- a/tests/solana_tests/tags.rs +++ b/tests/solana_tests/tags.rs @@ -15,6 +15,9 @@ fn contract() { /// @author Mr Foo /// @dev this is /// a contract + /// @custom:meh words for + /// @custom:meh custom tag + /// @custom: custom tag @program_id("Seed23VDZ9HFCfKvFwmemB6dpi25n5XjZdP52B2RUmh") contract test { /// @dev construct this @@ -24,6 +27,11 @@ fn contract() { Target::Solana, ); + assert_eq!( + ns.diagnostics.first_error(), + "custom tag '@custom:' is missing a name" + ); + assert_eq!(ns.contracts[0].tags[0].tag, "notice"); assert_eq!(ns.contracts[0].tags[0].value, "So Hello, World"); @@ -36,6 +44,9 @@ fn contract() { assert_eq!(ns.contracts[0].tags[3].tag, "dev"); assert_eq!(ns.contracts[0].tags[3].value, "this is\na contract"); + assert_eq!(ns.contracts[0].tags[4].tag, "custom:meh"); + assert_eq!(ns.contracts[0].tags[4].value, "words for custom tag"); + let constructor = ns .functions .iter() From ea4baceaa22cc49624f431ba37c5ffd40a9da13d Mon Sep 17 00:00:00 2001 From: Sean Young Date: Fri, 15 Sep 2023 15:07:00 +0100 Subject: [PATCH 3/9] Permit memory-safe flag on assembly (#1530) Fixes https://github.com/hyperledger/solang/issues/1526 Signed-off-by: Sean Young --- src/sema/statements.rs | 33 ++++++++++++++++---- src/sema/tests/mod.rs | 1 + src/sema/yul/ast.rs | 4 +++ src/sema/yul/mod.rs | 2 ++ tests/contract_testcases/evm/memory_safe.sol | 14 +++++++++ tests/evm.rs | 2 +- 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 tests/contract_testcases/evm/memory_safe.sol diff --git a/src/sema/statements.rs b/src/sema/statements.rs index 01a148335..91d2b0512 100644 --- a/src/sema/statements.rs +++ b/src/sema/statements.rs @@ -888,17 +888,38 @@ fn statement( return Err(()); } + let mut memory_safe = None; + if let Some(flags) = flags { for flag in flags { - ns.diagnostics.push(Diagnostic::error( - flag.loc, - format!("flag '{}' not supported", flag.string), - )); + if flag.string == "memory-safe" && ns.target == Target::EVM { + if let Some(prev) = &memory_safe { + ns.diagnostics.push(Diagnostic::error_with_note( + flag.loc, + format!("flag '{}' already specified", flag.string), + *prev, + "previous location".into(), + )); + } else { + memory_safe = Some(flag.loc); + } + } else { + ns.diagnostics.push(Diagnostic::error( + flag.loc, + format!("flag '{}' not supported", flag.string), + )); + } } } - let resolved_asm = - resolve_inline_assembly(loc, &block.statements, context, symtable, ns); + let resolved_asm = resolve_inline_assembly( + loc, + memory_safe.is_some(), + &block.statements, + context, + symtable, + ns, + ); res.push(Statement::Assembly(resolved_asm.0, resolved_asm.1)); Ok(resolved_asm.1) } diff --git a/src/sema/tests/mod.rs b/src/sema/tests/mod.rs index 0c04d70a2..13e976b44 100644 --- a/src/sema/tests/mod.rs +++ b/src/sema/tests/mod.rs @@ -128,6 +128,7 @@ fn test_statement_reachable() { Statement::Assembly( InlineAssembly { loc, + memory_safe: false, body: vec![], functions: std::ops::Range { start: 0, end: 0 }, }, diff --git a/src/sema/yul/ast.rs b/src/sema/yul/ast.rs index 998df37ba..5263ca456 100644 --- a/src/sema/yul/ast.rs +++ b/src/sema/yul/ast.rs @@ -12,6 +12,10 @@ use std::sync::Arc; #[derive(Debug, Clone)] pub struct InlineAssembly { pub loc: pt::Loc, + /// is the assembly ("memory-safe") { .. } flag specified + /// This flag is only permitted on EVM. It is not used by solang itself, however external + /// tools that wish to use our AST can use it. + pub memory_safe: bool, pub body: Vec, // (begin, end) offset for Namespace::yul_functions pub functions: std::ops::Range, diff --git a/src/sema/yul/mod.rs b/src/sema/yul/mod.rs index 704859433..ab4a64cbd 100644 --- a/src/sema/yul/mod.rs +++ b/src/sema/yul/mod.rs @@ -24,6 +24,7 @@ mod unused_variable; /// Returns the resolved block and a bool to indicate if the next statement is reachable. pub fn resolve_inline_assembly( loc: &pt::Loc, + memory_safe: bool, statements: &[pt::YulStatement], context: &ExprContext, symtable: &mut Symtable, @@ -54,6 +55,7 @@ pub fn resolve_inline_assembly( ( InlineAssembly { loc: *loc, + memory_safe, body, functions: std::ops::Range { start, end }, }, diff --git a/tests/contract_testcases/evm/memory_safe.sol b/tests/contract_testcases/evm/memory_safe.sol new file mode 100644 index 000000000..215411357 --- /dev/null +++ b/tests/contract_testcases/evm/memory_safe.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +library Issue1526 { + function extSloads() public pure { + assembly ("memory-safe", "foo", "memory-safe") { + + } + } +} + +// ---- Expect: diagnostics ---- +// error: 5:34-39: flag 'foo' not supported +// error: 5:41-54: flag 'memory-safe' already specified +// note 5:19-32: previous location diff --git a/tests/evm.rs b/tests/evm.rs index eaad9b8c1..8f5f30187 100644 --- a/tests/evm.rs +++ b/tests/evm.rs @@ -249,7 +249,7 @@ fn ethereum_solidity_tests() { }) .sum(); - assert_eq!(errors, 1032); + assert_eq!(errors, 1030); } fn set_file_contents(source: &str, path: &Path) -> (FileResolver, Vec) { From 512f3f2387de101e06e986b4003291b984ecaaf4 Mon Sep 17 00:00:00 2001 From: Lucas Steuernagel <38472950+LucasSte@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:53:29 -0300 Subject: [PATCH 4/9] New syntax for contracts on Solana (#1517) Now that we represent contracts by their program id on Solana, we decided to elaborate a new syntax to handle them. Contracts cannot be a type in Solidity, consequently, they cannot be function arguments, function returns nor variables. --------- Signed-off-by: Lucas Steuernagel --- docs/conf.py | 3 + docs/examples/base_contract_function_call.sol | 2 + .../{ => polkadot}/expression_this.sol | 0 .../expression_this_external_call.sol | 0 .../examples/{ => polkadot}/function_call.sol | 0 .../{ => polkadot}/function_call_external.sol | 0 .../{ => polkadot}/function_type_callback.sol | 0 docs/examples/solana/bobcat.sol | 4 +- docs/examples/solana/contract_address.sol | 5 +- docs/examples/solana/contract_call.sol | 21 + docs/examples/solana/contract_new.sol | 21 + .../solana/create_contract_with_metas.sol | 7 +- docs/examples/solana/expression_this.sol | 6 + .../solana/expression_this_external_call.sol | 10 + docs/examples/solana/function_call.sol | 27 + .../solana/function_call_external.sol | 16 + .../solana/function_type_callback.sol | 24 + docs/examples/solana/payer_annotation.sol | 5 +- docs/examples/solana/program_id.sol | 17 +- docs/language/contracts.rst | 51 +- docs/language/expressions.rst | 28 +- docs/language/functions.rst | 37 +- docs/language/types.rst | 23 +- docs/requirements.txt | 1 + docs/targets/solana.rst | 8 +- integration/anchor/tests/call_anchor.spec.ts | 10 +- integration/solana/create_contract.sol | 20 +- integration/solana/external_call.sol | 20 +- integration/solana/runtime_errors.sol | 4 +- solang-parser/src/solidity.lalrpop | 4 + src/abi/anchor.rs | 2 +- src/abi/tests.rs | 90 +- src/bin/idl/mod.rs | 13 +- src/codegen/constructor.rs | 26 +- src/codegen/mod.rs | 2 +- .../solana_accounts/account_collection.rs | 52 +- .../solana_accounts/account_management.rs | 25 + src/codegen/statements/mod.rs | 22 +- src/emit/solana/target.rs | 13 +- src/sema/ast.rs | 12 +- src/sema/builtin.rs | 5 +- src/sema/contracts.rs | 2 +- src/sema/expression/constructor.rs | 87 +- src/sema/expression/function_call.rs | 1075 +++++++++++------ src/sema/expression/member_access.rs | 3 +- src/sema/functions.rs | 17 +- src/sema/namespace.rs | 54 +- src/sema/statements.rs | 10 +- src/sema/tests/mod.rs | 20 +- src/sema/types.rs | 35 +- src/sema/unused_variable.rs | 3 + src/sema/using.rs | 9 +- src/sema/variables.rs | 9 +- tests/codegen_testcases/import_test.sol | 6 + .../accounts_for_default_constructor.sol | 16 + .../solidity/borsh_decoding_simple_types.sol | 199 ++- .../solidity/constructor_with_metas.sol | 7 +- .../solidity/import_ext_call.sol | 25 + .../solidity/solana_base_versus_external.sol | 46 + .../solidity/solana_payer_account.sol | 7 +- .../polkadot/contracts/program_id.sol | 14 + .../solana/abstract_interface.sol | 4 +- .../solana/accounts/constructor_in_loop.sol | 34 +- .../solana/accounts/double_calls.sol | 31 +- .../annotations/account_name_collision.sol | 8 +- .../constructor_external_function.sol | 14 +- .../solana/call/call_args_three_ways.sol | 18 +- .../solana/constant/not_constant.sol | 3 +- .../solana/contracts/abstract_constructor.sol | 23 + .../solana/contracts/circular_reference.sol | 48 + .../contracts/constructor_freestanding.sol | 29 + .../contracts/contract_as_variables.sol | 51 + .../contracts/contract_call_freestanding.sol | 28 + .../solana/create_contract/syntax.sol | 4 +- .../solana/create_contract/syntax_01.sol | 4 +- .../destructure_assign_struct_member_2.sol | 2 +- tests/contract_testcases/solana/error.sol | 5 +- .../solana/expressions/contract_compare.sol | 19 - .../solana/expressions/contract_no_init.sol | 7 +- .../selector_in_free_function_02.sol | 6 +- .../solana/functions/external_functions.sol | 5 +- .../solana/garbage_function_args.sol | 11 +- .../solana/mapping_deletion.sol | 10 +- .../solana/type_decl_import.sol | 4 +- ...619457eea3be2790d0fa66cba06d2e9a36f17.json | 9 - ...e619457eea3be2790d0fa66cba06d2e9a36f17.sol | 21 - tests/solana_tests/abi_decode.rs | 4 +- tests/solana_tests/abi_encode.rs | 5 +- tests/solana_tests/accessor.rs | 13 +- tests/solana_tests/call.rs | 23 +- tests/solana_tests/create_contract.rs | 82 +- tests/solana_tests/mappings.rs | 12 +- tests/solana_tests/runtime_errors.rs | 33 +- tests/solana_tests/using.rs | 211 ++-- 94 files changed, 2046 insertions(+), 1015 deletions(-) rename docs/examples/{ => polkadot}/expression_this.sol (100%) rename docs/examples/{ => polkadot}/expression_this_external_call.sol (100%) rename docs/examples/{ => polkadot}/function_call.sol (100%) rename docs/examples/{ => polkadot}/function_call_external.sol (100%) rename docs/examples/{ => polkadot}/function_type_callback.sol (100%) create mode 100644 docs/examples/solana/contract_call.sol create mode 100644 docs/examples/solana/contract_new.sol create mode 100644 docs/examples/solana/expression_this.sol create mode 100644 docs/examples/solana/expression_this_external_call.sol create mode 100644 docs/examples/solana/function_call.sol create mode 100644 docs/examples/solana/function_call_external.sol create mode 100644 docs/examples/solana/function_type_callback.sol create mode 100644 tests/codegen_testcases/import_test.sol create mode 100644 tests/codegen_testcases/solidity/accounts_for_default_constructor.sol create mode 100644 tests/codegen_testcases/solidity/import_ext_call.sol create mode 100644 tests/codegen_testcases/solidity/solana_base_versus_external.sol create mode 100644 tests/contract_testcases/polkadot/contracts/program_id.sol create mode 100644 tests/contract_testcases/solana/contracts/abstract_constructor.sol create mode 100644 tests/contract_testcases/solana/contracts/circular_reference.sol create mode 100644 tests/contract_testcases/solana/contracts/constructor_freestanding.sol create mode 100644 tests/contract_testcases/solana/contracts/contract_as_variables.sol create mode 100644 tests/contract_testcases/solana/contracts/contract_call_freestanding.sol delete mode 100644 tests/contract_testcases/solana/expressions/contract_compare.sol delete mode 100644 tests/optimization_testcases/calls/54e619457eea3be2790d0fa66cba06d2e9a36f17.json delete mode 100644 tests/optimization_testcases/programs/54e619457eea3be2790d0fa66cba06d2e9a36f17.sol diff --git a/docs/conf.py b/docs/conf.py index 49e35b9db..3e3bf47cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,8 +39,11 @@ def setup(sphinx): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'sphinx_tabs.tabs' ] +# Do not allow tabs to be closed +sphinx_tabs_disable_tab_closing = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/examples/base_contract_function_call.sol b/docs/examples/base_contract_function_call.sol index 798b8b1dc..a2642ca7a 100644 --- a/docs/examples/base_contract_function_call.sol +++ b/docs/examples/base_contract_function_call.sol @@ -22,6 +22,8 @@ abstract contract a { function bar2() internal returns (uint64) { // this explicitly says "call foo of base contract a", and dispatch is not virtual + // however, if the call is written as a.foo{program_id: id_var}(), this represents + // an external call to contract 'a' on Solana. return a.foo(); } } diff --git a/docs/examples/expression_this.sol b/docs/examples/polkadot/expression_this.sol similarity index 100% rename from docs/examples/expression_this.sol rename to docs/examples/polkadot/expression_this.sol diff --git a/docs/examples/expression_this_external_call.sol b/docs/examples/polkadot/expression_this_external_call.sol similarity index 100% rename from docs/examples/expression_this_external_call.sol rename to docs/examples/polkadot/expression_this_external_call.sol diff --git a/docs/examples/function_call.sol b/docs/examples/polkadot/function_call.sol similarity index 100% rename from docs/examples/function_call.sol rename to docs/examples/polkadot/function_call.sol diff --git a/docs/examples/function_call_external.sol b/docs/examples/polkadot/function_call_external.sol similarity index 100% rename from docs/examples/function_call_external.sol rename to docs/examples/polkadot/function_call_external.sol diff --git a/docs/examples/function_type_callback.sol b/docs/examples/polkadot/function_type_callback.sol similarity index 100% rename from docs/examples/function_type_callback.sol rename to docs/examples/polkadot/function_type_callback.sol diff --git a/docs/examples/solana/bobcat.sol b/docs/examples/solana/bobcat.sol index 6f9ee6889..f1e5ad188 100644 --- a/docs/examples/solana/bobcat.sol +++ b/docs/examples/solana/bobcat.sol @@ -1,5 +1,5 @@ -anchor_anchor constant bobcat = anchor_anchor(address'z7FbDfQDfucxJz5o8jrGLgvSbdoeSqX5VrxBb5TVjHq'); -interface anchor_anchor { +@program_id("z7FbDfQDfucxJz5o8jrGLgvSbdoeSqX5VrxBb5TVjHq") +interface bobcat { @selector([0xaf, 0xaf, 0x6d, 0x1f, 0x0d, 0x98, 0x9b, 0xed]) function pounce() view external returns(int64); } diff --git a/docs/examples/solana/contract_address.sol b/docs/examples/solana/contract_address.sol index 4626baf93..9c18d1089 100644 --- a/docs/examples/solana/contract_address.sol +++ b/docs/examples/solana/contract_address.sol @@ -1,4 +1,3 @@ -@program_id("5kQ3iJ43gHNDjqmSAtE1vDu18CiSAfNbRe4v5uoobh3U") contract hatchling { string name; @@ -9,7 +8,7 @@ contract hatchling { } contract adult { - function test(address addr) external { - hatchling h = new hatchling("luna"); + function test(address id) external { + hatchling.new{program_id: id}("luna"); } } diff --git a/docs/examples/solana/contract_call.sol b/docs/examples/solana/contract_call.sol new file mode 100644 index 000000000..eb6e649d8 --- /dev/null +++ b/docs/examples/solana/contract_call.sol @@ -0,0 +1,21 @@ +contract Polymath { + function call_math() external returns (uint) { + return Math.sum(1, 2); + } + function call_english(address english_id) external returns (string) { + return English.concatenate{program_id: english_id}("Hello", "world"); + } +} + +@program_id("5afzkvPkrshqu4onwBCsJccb1swrt4JdAjnpzK8N4BzZ") +contract Math { + function sum(uint a, uint b) external returns (uint) { + return a + b; + } +} + +contract English { + function concatenate(string a, string b) external returns (string) { + return a + b; + } +} \ No newline at end of file diff --git a/docs/examples/solana/contract_new.sol b/docs/examples/solana/contract_new.sol new file mode 100644 index 000000000..28f342147 --- /dev/null +++ b/docs/examples/solana/contract_new.sol @@ -0,0 +1,21 @@ +@program_id("5afzkvPkrshqu4onwBCsJccb1swrt4JdAjnpzK8N4BzZ") +contract hatchling { + string name; + address private origin; + + constructor(string id, address parent) { + require(id != "", "name must be provided"); + name = id; + origin = parent; + } + + function root() public returns (address) { + return origin; + } +} + +contract adult { + function test() external { + hatchling.new("luna", address(this)); + } +} diff --git a/docs/examples/solana/create_contract_with_metas.sol b/docs/examples/solana/create_contract_with_metas.sol index 163826b7c..55fd859f9 100644 --- a/docs/examples/solana/create_contract_with_metas.sol +++ b/docs/examples/solana/create_contract_with_metas.sol @@ -1,9 +1,6 @@ import 'solana'; contract creator { - Child public c; - Child public c_metas; - function create_with_metas(address data_account_to_initialize, address payer) public { AccountMeta[3] metas = [ AccountMeta({ @@ -20,9 +17,9 @@ contract creator { is_signer: false}) ]; - c_metas = new Child{accounts: metas}(payer); + Child.new{accounts: metas}(payer); - c_metas.use_metas(); + Child.use_metas(); } } diff --git a/docs/examples/solana/expression_this.sol b/docs/examples/solana/expression_this.sol new file mode 100644 index 000000000..bbec97cfc --- /dev/null +++ b/docs/examples/solana/expression_this.sol @@ -0,0 +1,6 @@ +contract kadowari { + function nomi() public { + // Contracts are not allowed as variables on Solana + address a = address(this); + } +} diff --git a/docs/examples/solana/expression_this_external_call.sol b/docs/examples/solana/expression_this_external_call.sol new file mode 100644 index 000000000..82ffbe40f --- /dev/null +++ b/docs/examples/solana/expression_this_external_call.sol @@ -0,0 +1,10 @@ +@program_id("H3AthiA2C1pcMahg17nEwqr9628gkXUnnzWJJ3iSDekL") +contract kadowari { + function nomi() public { + this.nokogiri(102); + } + + function nokogiri(int256 a) public { + // ... + } +} diff --git a/docs/examples/solana/function_call.sol b/docs/examples/solana/function_call.sol new file mode 100644 index 000000000..3acc5cf7c --- /dev/null +++ b/docs/examples/solana/function_call.sol @@ -0,0 +1,27 @@ +contract A { + function test(address v) public { + // the following four lines are equivalent to "uint32 res = v.foo(3,5);" + + // Note that the signature is only hashed and not parsed. So, ensure that the + // arguments are of the correct type. + bytes data = abi.encodeWithSignature( + "global:foo", + uint32(3), + uint32(5) + ); + + (bool success, bytes rawresult) = v.call(data); + + assert(success == true); + + uint32 res = abi.decode(rawresult, (uint32)); + + assert(res == 8); + } +} + +contract B { + function foo(uint32 a, uint32 b) pure public returns (uint32) { + return a + b; + } +} diff --git a/docs/examples/solana/function_call_external.sol b/docs/examples/solana/function_call_external.sol new file mode 100644 index 000000000..b355ff720 --- /dev/null +++ b/docs/examples/solana/function_call_external.sol @@ -0,0 +1,16 @@ +contract foo { + function bar1(uint32 x, bool y) public returns (address, bytes32) { + return (address(3), hex"01020304"); + } + + function bar2(uint32 x, bool y) public returns (bool) { + return !y; + } +} + +contract bar { + function test(address f) public { + (address f1, bytes32 f2) = foo.bar1{program_id: f}(102, false); + bool f3 = foo.bar2{program_id: f}({x: 255, y: true}); + } +} diff --git a/docs/examples/solana/function_type_callback.sol b/docs/examples/solana/function_type_callback.sol new file mode 100644 index 000000000..befae9c17 --- /dev/null +++ b/docs/examples/solana/function_type_callback.sol @@ -0,0 +1,24 @@ +contract ft { + function test(address p) public { + // this.callback can be used as an external function type value + paffling.set_callback{program_id: p}(this.callback); + } + + function callback(int32 count, string foo) public { + // ... + } +} + +contract paffling { + // the first visibility "external" is for the function type, the second "internal" is + // for the callback variables + function(int32, string) external internal callback; + + function set_callback(function(int32, string) external c) public { + callback = c; + } + + function piffle() public { + callback(1, "paffled"); + } +} diff --git a/docs/examples/solana/payer_annotation.sol b/docs/examples/solana/payer_annotation.sol index bc6b44b78..74c3a09c2 100644 --- a/docs/examples/solana/payer_annotation.sol +++ b/docs/examples/solana/payer_annotation.sol @@ -2,11 +2,10 @@ import 'solana'; @program_id("SoLDxXQ9GMoa15i4NavZc61XGkas2aom4aNiWT6KUER") contract Builder { - BeingBuilt other; function build_this() external { // When calling a constructor from an external function, the data account for the contract // 'BeingBuilt' should be passed as the 'BeingBuilt_dataAccount' in the client code. - other = new BeingBuilt("my_seed"); + BeingBuilt.new("my_seed"); } function build_that(address data_account, address payer_account) public { @@ -30,7 +29,7 @@ contract Builder { is_signer: false }) ]; - other = new BeingBuilt{accounts: metas}("my_seed"); + BeingBuilt.new{accounts: metas}("my_seed"); } } diff --git a/docs/examples/solana/program_id.sol b/docs/examples/solana/program_id.sol index 8152d5f87..4a9a2a513 100644 --- a/docs/examples/solana/program_id.sol +++ b/docs/examples/solana/program_id.sol @@ -5,14 +5,23 @@ contract Foo { } } -contract Bar { - Foo public foo; +contract OtherFoo { + function say_bye() public pure { + print("Bye from other foo"); + } +} +contract Bar { function create_foo() external { - foo = new Foo(); + Foo.new(); } function call_foo() public { - foo.say_hello(); + Foo.say_hello(); + } + + function foo_at_another_address(address other_foo_id) external { + OtherFoo.new{program_id: other_foo_id}(); + OtherFoo.say_bye{program_id: other_foo_id}(); } } diff --git a/docs/language/contracts.rst b/docs/language/contracts.rst index cf8ea4990..283791b17 100644 --- a/docs/language/contracts.rst +++ b/docs/language/contracts.rst @@ -31,10 +31,24 @@ Instantiation using new _______________________ Contracts can be created using the ``new`` keyword. The contract that is being created might have -constructor arguments, which need to be provided. +constructor arguments, which need to be provided. While on Polkadot and Ethereum constructors return the address +of the instantiated contract, on Solana, the address is either passed to the call using the ``{program_id: ...}`` call +argument or is declared above a contract with the ``@program_id`` annotation. As the constructor does not return +anything and its purpose is only to initialize the data account, the syntax ``new Contract()``is not idiomatic on Solana. +Instead, a function ``new`` is made available to call the constructor. + +.. tabs:: + + .. group-tab:: Polkadot + + .. include:: ../examples/polkadot/contract_new.sol + :code: solidity -.. include:: ../examples/polkadot/contract_new.sol - :code: solidity + + .. group-tab:: Solana + + .. include:: ../examples/solana/contract_new.sol + :code: solidity The constructor might fail for various reasons, for example ``require()`` might fail here. This can be handled using the :ref:`try-catch` statement, else errors cause the transaction to fail. @@ -87,15 +101,16 @@ can use. gas is a ``uint64``. .. include:: ../examples/polkadot/contract_gas_limit.sol :code: solidity + .. _solana_constructor: -Instantiating a contract on Solana -__________________________________ +Solana constructors +___________________ -On Solana, the contract being created must have the ``@program_id()`` annotation that specifies the program account to -which the contract code has been deployed. This account holds only the contract's executable binary. -When calling a constructor only once from an external function, no call arguments are needed. The data account -necessary to initialize the contract should be present in the IDL and is identified as ``contractName_dataAccount``. +Solidity contracts are coupled to a data account, which stores the contract's state variables on the blockchain. +This account must be initialized before calling other contract functions, if they require one. A contract constructor +initializes the data account and can be called with the ``new`` function. When invoking the constructor from another +contract, the data account to initialize appears in the IDL file and is identified as ``contractName_dataAccount``. In the example below, the IDL for the instruction ``test`` requires the ``hatchling_dataAccount`` account to be initialized as the new contract's data account. @@ -125,6 +140,24 @@ The sequence of the accounts in the ``AccountMeta`` array matters and must follo :ref:`IDL ordering `. +.. _solana_contract_call: + +Calling a contract on Solana +____________________________ + +A call to a contract on Solana follows a different syntax than that of Solidity on Ethereum or Polkadot. As contracts +cannot be a variable, calling a contract's function follows the syntax ``Contract.function()``. If the contract +definition contains the ``@program_id`` annotation, the CPI will be directed to the address declared inside the +annotation. + +If that annotation is not present, the program address must be manually specified with the ``{program_id: ... }`` call +argument. When both the annotation and the call argument are present, the compiler will forward the call to the address +specified in the call argument. + +.. include:: ../examples/solana/contract_call.sol + :code: solidity + + Base contracts, abstract contracts and interfaces ------------------------------------------------- diff --git a/docs/language/expressions.rst b/docs/language/expressions.rst index 1c5e04c94..a975a902b 100644 --- a/docs/language/expressions.rst +++ b/docs/language/expressions.rst @@ -112,15 +112,35 @@ ____ The keyword ``this`` evaluates to the current contract. The type of this is the type of the current contract. It can be cast to ``address`` or ``address payable`` using a cast. -.. include:: ../examples/expression_this.sol - :code: solidity +.. tabs:: + + .. group-tab:: Polkadot + + .. include:: ../examples/polkadot/expression_this.sol + :code: solidity + + + .. group-tab:: Solana + + .. include:: ../examples/solana/expression_this.sol + :code: solidity Function calls made via this are function calls through the external call mechanism; i.e. they have to serialize and deserialise the arguments and have the external call overhead. In addition, this only works with public functions. -.. include:: ../examples/expression_this_external_call.sol - :code: solidity +.. tabs:: + + .. group-tab:: Polkadot + + .. include:: ../examples/polkadot/expression_this_external_call.sol + :code: solidity + + + .. group-tab:: Solana + + .. include:: ../examples/solana/expression_this_external_call.sol + :code: solidity .. note:: diff --git a/docs/language/functions.rst b/docs/language/functions.rst index 9cc637ab0..07123ef7f 100644 --- a/docs/language/functions.rst +++ b/docs/language/functions.rst @@ -66,12 +66,25 @@ external functions. The called function must be declared public. Calling external functions requires ABI encoding the arguments, and ABI decoding the return values. This much more costly than an internal function call. -.. include:: ../examples/function_call_external.sol - :code: solidity -The syntax for calling external call is the same as the external call, except for -that it must be done on a contract type variable. Any error in an external call can -be handled with :ref:`try-catch`. +.. tabs:: + + .. group-tab:: Polkadot + + .. include:: ../examples/polkadot/function_call_external.sol + :code: solidity + + + .. group-tab:: Solana + + .. include:: ../examples/solana/function_call_external.sol + :code: solidity + + + +The syntax for calling a contract is the same as that of the external call, except +that it must be done on a contract type variable. Errors in external calls can +be handled with :ref:`try-catch` only on Polkadot. Internal calls and externals calls ___________________________________ @@ -289,8 +302,18 @@ This takes a single argument, which should be the ABI encoded arguments. The ret values are a ``boolean`` which indicates success if true, and the ABI encoded return value in ``bytes``. -.. include:: ../examples/function_call.sol - :code: solidity +.. tabs:: + + .. group-tab:: Polkadot + + .. include:: ../examples/polkadot/function_call.sol + :code: solidity + + + .. group-tab:: Solana + + .. include:: ../examples/solana/function_call.sol + :code: solidity Any value or gas limit can be specified for the external call. Note that no check is done to see if the called function is ``payable``, since the compiler does not know what function you are diff --git a/docs/language/types.rst b/docs/language/types.rst index 4635ee3c6..0e46b67ea 100644 --- a/docs/language/types.rst +++ b/docs/language/types.rst @@ -460,6 +460,14 @@ The expression ``this`` evaluates to the current contract, which can be cast to .. include:: ../examples/contract_type_cast_address.sol :code: solidity +.. _contracts_not_types: + +.. note:: + On Solana, contracts cannot exist as types, so contracts cannot be function parameters, function returns + or variables. Contracts on Solana are deployed to a defined address, which is often known during compile time, + so there is no need to hold that address as a variable underneath a contract type. + + Function Types ______________ @@ -485,8 +493,19 @@ the contract, and the function selector. An internal function type only stores t assigning a value to an external function selector, the contract and function must be specified, by using a function on particular contract instance. -.. include:: ../examples/function_type_callback.sol - :code: solidity +.. tabs:: + + .. group-tab:: Polkadot + + .. include:: ../examples/polkadot/function_type_callback.sol + :code: solidity + + + .. group-tab:: Solana + + .. include:: ../examples/solana/function_type_callback.sol + :code: solidity + Storage References __________________ diff --git a/docs/requirements.txt b/docs/requirements.txt index efd6c94e0..3696da652 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,5 @@ sphinx_rtd_theme>=0.5.2 pygments-lexer-solidity>=0.7.0 sphinx-a4doc>=1.2.1 sphinx>=2.1.0 +sphinx-tabs>=3.4.1 diff --git a/docs/targets/solana.rst b/docs/targets/solana.rst index 3995281d0..49564cf36 100644 --- a/docs/targets/solana.rst +++ b/docs/targets/solana.rst @@ -46,9 +46,11 @@ Runtime - The Solana target requires `Solana `_ v1.8.1. - Function selectors are eight bytes wide and known as *discriminators*. - Solana provides different builtins, e.g. ``block.slot`` and ``tx.accounts``. -- When calling an external function or instantiating a contract using ``new``, one +- When calling an external function or invoking a contract's constructor, one :ref:`needs to provide ` the necessary accounts for the transaction. - The keyword ``this`` returns the contract's program account, also know as program id. +- Contracts :ref:`cannot be types ` on Solana and :ref:`calls to contracts ` + follow a different syntax. Compute budget @@ -127,8 +129,8 @@ _____________________________________ When developing contracts for Solana, programs are usually deployed to a well known account. The account can be specified in the source code using an annotation -``@program_id``. If you want to instantiate a contract using the -``new ContractName()`` syntax, then the contract must have a program_id annotation. +``@program_id`` if it is known beforehand. If you want to call a contract via an external call, +either the contract must have a ``@program_id`` annotation or the ``{program_id: ..}`` call argument must be present. .. include:: ../examples/solana/program_id.sol :code: solidity diff --git a/integration/anchor/tests/call_anchor.spec.ts b/integration/anchor/tests/call_anchor.spec.ts index 36d3f1e91..13ac1e4ec 100644 --- a/integration/anchor/tests/call_anchor.spec.ts +++ b/integration/anchor/tests/call_anchor.spec.ts @@ -50,11 +50,8 @@ describe('Call Anchor program from Solidity via IDL', () => { const ret = await program.methods.data().accounts({ dataAccount: storage.publicKey }).view(); expect(ret).toEqual(data.publicKey); + const anchor_program_id = new PublicKey("z7FbDfQDfucxJz5o8jrGLgvSbdoeSqX5VrxBb5TVjHq"); const remainingAccounts: AccountMeta[] = [{ - pubkey: new PublicKey("z7FbDfQDfucxJz5o8jrGLgvSbdoeSqX5VrxBb5TVjHq"), - isSigner: false, - isWritable: false, - }, { pubkey: data.publicKey, isSigner: true, isWritable: true, @@ -65,7 +62,10 @@ describe('Call Anchor program from Solidity via IDL', () => { }]; await program.methods.test(payer.publicKey) - .accounts({ dataAccount: storage.publicKey }) + .accounts({ + dataAccount: storage.publicKey, + anchor_programId: anchor_program_id, + }) .remainingAccounts(remainingAccounts) .signers([data, payer]) .rpc(); diff --git a/integration/solana/create_contract.sol b/integration/solana/create_contract.sol index 4b12718b6..b195ff79b 100644 --- a/integration/solana/create_contract.sol +++ b/integration/solana/create_contract.sol @@ -1,27 +1,25 @@ import 'solana'; contract creator { - Child public c; - Child public c_metas; function create_child() external { print("Going to create child"); - c = new Child(); + Child.new(); - c.say_hello(); + Child.say_hello(); } function create_seed1(bytes seed, bytes1 bump, uint64 space) external { print("Going to create Seed1"); - Seed1 s = new Seed1(seed, bump, space); + Seed1.new(seed, bump, space); - s.say_hello(); + Seed1.say_hello(); } function create_seed2(bytes seed, uint32 space) external { print("Going to create Seed2"); - new Seed2(seed, space); + Seed2.new(seed, space); } function create_child_with_metas(address child, address payer) public { @@ -32,13 +30,13 @@ contract creator { AccountMeta({pubkey: address"11111111111111111111111111111111", is_writable: false, is_signer: false}) ]; - c_metas = new Child{accounts: metas}(); - c_metas.use_metas(); + Child.new{accounts: metas}(); + Child.use_metas(); } function create_without_annotation() external { - MyCreature cc = new MyCreature(); - cc.say_my_name(); + MyCreature.new(); + MyCreature.say_my_name(); } } diff --git a/integration/solana/external_call.sol b/integration/solana/external_call.sol index b4d194b8d..c95f812dd 100644 --- a/integration/solana/external_call.sol +++ b/integration/solana/external_call.sol @@ -1,21 +1,21 @@ contract caller { - function do_call(callee e, int64 v) public { - e.set_x(v); + function do_call(address e, int64 v) public { + callee.set_x{program_id: e}(v); } - function do_call2(callee e, int64 v) view public returns (int64) { - return v + e.get_x(); + function do_call2(address e, int64 v) view public returns (int64) { + return v + callee.get_x{program_id: e}(); } // call two different functions - function do_call3(callee e, callee2 e2, int64[4] memory x, string memory y) pure public returns (int64, string memory) { - return (e2.do_stuff(x), e.get_name()); + function do_call3(address e, address e2, int64[4] memory x, string memory y) pure public returns (int64, string memory) { + return (callee2.do_stuff{program_id: e2}(x), callee.get_name{program_id: e}()); } // call two different functions - function do_call4(callee e, callee2 e2, int64[4] memory x, string memory y) pure public returns (int64, string memory) { - return (e2.do_stuff(x), e.call2(e2, y)); + function do_call4(address e, address e2, int64[4] memory x, string memory y) pure public returns (int64, string memory) { + return (callee2.do_stuff{program_id: e2}(x), callee.call2{program_id: e}(e2, y)); } function who_am_i() public view returns (address) { @@ -34,8 +34,8 @@ contract callee { return x; } - function call2(callee2 e2, string s) public pure returns (string) { - return e2.do_stuff2(s); + function call2(address e2, string s) public pure returns (string) { + return callee2.do_stuff2{program_id: e2}(s); } function get_name() public pure returns (string) { diff --git a/integration/solana/runtime_errors.sol b/integration/solana/runtime_errors.sol index 6115b0935..cb76206a7 100644 --- a/integration/solana/runtime_errors.sol +++ b/integration/solana/runtime_errors.sol @@ -61,8 +61,8 @@ contract RuntimeErrors { } // external call failed - function call_ext(Creature e) public { - e.say_my_name(); + function call_ext() public { + Creature.say_my_name(); } function i_will_revert() public { diff --git a/solang-parser/src/solidity.lalrpop b/solang-parser/src/solidity.lalrpop index 7f5552c48..f9901c29a 100644 --- a/solang-parser/src/solidity.lalrpop +++ b/solang-parser/src/solidity.lalrpop @@ -483,6 +483,10 @@ NoFunctionTyPrecedence0: Expression = { Expression::MemberAccess(Loc::File(file_no, a, b), Box::new(e), Identifier { loc: Loc::File(file_no, al, b), name: "address".to_string() }) }, + "." "new" => { + Expression::MemberAccess(Loc::File(file_no, a, b), Box::new(e), + Identifier { loc: Loc::File(file_no, al, b), name: "new".to_string() }) + }, => Expression::Type(Loc::File(file_no, l, r), ty), "[" > "]" => { Expression::ArrayLiteral(Loc::File(file_no, a, b), v) diff --git a/src/abi/anchor.rs b/src/abi/anchor.rs index 3e9e983d5..28e013b36 100644 --- a/src/abi/anchor.rs +++ b/src/abi/anchor.rs @@ -103,7 +103,7 @@ fn idl_instructions( ) -> Vec { let mut instructions: Vec = Vec::new(); - if !contract.have_constructor(ns) { + if contract.constructors(ns).is_empty() { instructions.push(IdlInstruction { name: "new".to_string(), docs: None, diff --git a/src/abi/tests.rs b/src/abi/tests.rs index a657746a8..6e7f010d6 100644 --- a/src/abi/tests.rs +++ b/src/abi/tests.rs @@ -1697,30 +1697,25 @@ fn other_collected_public_keys() { let src = r#" import 'solana'; -anchor_anchor constant anchor = anchor_anchor(address'SysvarRent111111111111111111111111111111111'); - -interface anchor_anchor { +@program_id("SysvarRent111111111111111111111111111111111") +interface anchor { @selector([0xaf,0xaf,0x6d,0x1f,0x0d,0x98,0x9b,0xed]) function initialize(bool data1) view external; } -associated constant ass = associated(address'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); - +@program_id("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") interface associated { @selector([0xaf,0xaf,0x6d,0x1f,0x0d,0x98,0x9b,0xed]) function initialize(bool data1) view external; } - -clock_interface constant my_clock = clock_interface(address'SysvarC1ock11111111111111111111111111111111'); - +@program_id("SysvarC1ock11111111111111111111111111111111") interface clock_interface { @selector([0xaf,0xaf,0x6d,0x1f,0x0d,0x98,0x9b,0xed]) function initialize(bool data1) view external; } -other_interface constant other_inter = other_interface(address'z7FbDfQDfucxJz5o8jrGLgvSbdoeSqX5VrxBb5TVjHq'); - +@program_id("z7FbDfQDfucxJz5o8jrGLgvSbdoeSqX5VrxBb5TVjHq") interface other_interface { @selector([0xaf,0xaf,0x6d,0x1f,0x0d,0x98,0x9b,0xed]) function initialize(bool data1) view external; @@ -1732,15 +1727,15 @@ contract Test { } function call_2() public { - ass.initialize(false); + associated.initialize(false); } function call_3() public { - my_clock.initialize(true); + clock_interface.initialize(true); } function call_4() public { - other_inter.initialize(false); + other_interface.initialize(false); } } "#; @@ -1798,13 +1793,10 @@ fn multiple_contracts() { import 'solana'; contract creator { - Child public c; - function create_child() external returns (uint64) { print("Going to create child"); - c = new Child(); - - return c.say_hello(); + Child.new(); + return Child.say_hello(); } } @@ -1828,17 +1820,15 @@ contract Child { let idl = generate_anchor_idl(0, &ns, "0.1.0"); assert_eq!(idl.instructions[0].name, "new"); - assert_eq!(idl.instructions[1].name, "c"); - assert_eq!(idl.instructions[2].name, "create_child"); + assert_eq!(idl.instructions[1].name, "create_child"); assert_eq!( - idl.instructions[2].accounts, + idl.instructions[1].accounts, vec![ - idl_account("dataAccount", true, false), + idl_account("systemProgram", false, false), + idl_account("Child_programId", false, false), idl_account("payer", true, true), idl_account("Child_dataAccount", true, true), - idl_account("Child_programId", false, false), - idl_account("systemProgram", false, false), idl_account("clock", false, false), ] ); @@ -1851,11 +1841,9 @@ fn constructor_double_payer() { @program_id("SoLDxXQ9GMoa15i4NavZc61XGkas2aom4aNiWT6KUER") contract Builder { - BeingBuilt other; - @payer(payer_account) constructor() { - other = new BeingBuilt("abc"); + BeingBuilt.new("abc"); } } @@ -1883,9 +1871,9 @@ contract BeingBuilt { idl_account("dataAccount", true, true), idl_account("payer_account", true, true), idl_account("systemProgram", false, false), + idl_account("BeingBuilt_programId", false, false), idl_account("other_account", true, true), idl_account("BeingBuilt_dataAccount", true, false), - idl_account("BeingBuilt_programId", false, false), ] ); } @@ -1956,19 +1944,17 @@ contract starter { fn account_transfer_recursive() { let src = r#" contract CT3 { - CT2 ct2; @payer(three_payer) constructor() { - ct2 = new CT2(); + CT2.new(); } } @program_id("Ha2EGxARbSYpqNZkkvZUUGEyx3pu7Mg9pvMsuEJuWNjH") contract CT2 { - CT1 ct1; @payer(two_payer) constructor() { - ct1 = new CT1(block.timestamp); + CT1.new(block.timestamp); } } @@ -2005,10 +1991,10 @@ contract CT1 { idl_account("dataAccount", true, true), idl_account("two_payer", true, true), idl_account("clock", false, false), + idl_account("systemProgram", false, false), + idl_account("CT1_programId", false, false), idl_account("one_payer", true, true), idl_account("CT1_dataAccount", true, true), - idl_account("CT1_programId", false, false), - idl_account("systemProgram", false, false), ] ); @@ -2019,13 +2005,45 @@ contract CT1 { idl_account("dataAccount", true, true), idl_account("three_payer", true, true), idl_account("systemProgram", false, false), + idl_account("CT2_programId", false, false), idl_account("two_payer", true, true), idl_account("CT2_dataAccount", true, true), - idl_account("CT2_programId", false, false), idl_account("clock", false, false), + idl_account("CT1_programId", false, false), idl_account("one_payer", true, true), idl_account("CT1_dataAccount", true, true), - idl_account("CT1_programId", false, false), + ] + ); +} + +#[test] +fn default_constructor() { + let src = r#" +contract Foo { + uint b; + function get_b() public returns (uint) { + return b; + } +} + +contract Other { + function call_foo(address id) external { + Foo.new{program_id: id}(); + } +} + "#; + + let mut ns = generate_namespace(src); + codegen(&mut ns, &Options::default()); + let idl = generate_anchor_idl(1, &ns, "0.0.1"); + + assert_eq!(idl.instructions[1].name, "call_foo"); + assert_eq!( + idl.instructions[1].accounts, + vec![ + idl_account("Foo_dataAccount", true, false), + idl_account("Foo_programId", false, false), + idl_account("systemProgram", false, false) ] ); } diff --git a/src/bin/idl/mod.rs b/src/bin/idl/mod.rs index d5ff7d491..db688203f 100644 --- a/src/bin/idl/mod.rs +++ b/src/bin/idl/mod.rs @@ -63,14 +63,6 @@ fn idl_file(file: &OsStr, output: &Option) { } fn write_solidity(idl: &Idl, mut f: File) -> Result<(), std::io::Error> { - if let Some(program_id) = program_id(idl) { - writeln!( - f, - "anchor_{} constant {} = anchor_{}(address'{}');\n", - idl.name, idl.name, idl.name, program_id - )?; - } - let mut ty_names = idl .types .iter() @@ -212,7 +204,10 @@ fn write_solidity(idl: &Idl, mut f: File) -> Result<(), std::io::Error> { docs(&mut f, 0, &idl.docs)?; - writeln!(f, "interface anchor_{} {{", idl.name)?; + if let Some(program_id) = program_id(idl) { + writeln!(f, "@program_id(\"{}\")", program_id)?; + } + writeln!(f, "interface {} {{", idl.name)?; let mut instruction_names = idl .instructions diff --git a/src/codegen/constructor.rs b/src/codegen/constructor.rs index 2616c8ddc..160223479 100644 --- a/src/codegen/constructor.rs +++ b/src/codegen/constructor.rs @@ -47,14 +47,24 @@ pub(super) fn call_constructor( .as_ref() .map(|e| expression(e, cfg, callee_contract_no, func, ns, vartab, opt)); let address = if ns.target == Target::Solana { - Some(Expression::NumberLiteral { - loc: Loc::Codegen, - ty: Type::Address(false), - value: BigInt::from_bytes_be( - Sign::Plus, - ns.contracts[contract_no].program_id.as_ref().unwrap(), - ), - }) + if let Some(literal_id) = &ns.contracts[contract_no].program_id { + Some(Expression::NumberLiteral { + loc: Loc::Codegen, + ty: Type::Address(false), + value: BigInt::from_bytes_be(Sign::Plus, literal_id), + }) + } else { + let address = expression( + call_args.program_id.as_ref().unwrap(), + cfg, + callee_contract_no, + func, + ns, + vartab, + opt, + ); + Some(address) + } } else { None }; diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index cf22f3ed1..047f79002 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -234,7 +234,7 @@ fn contract(contract_no: usize, ns: &mut Namespace, opt: &Options) { all_cfg.push(cfg); ns.contracts[contract_no].initializer = Some(pos); - if !ns.contracts[contract_no].have_constructor(ns) { + if ns.contracts[contract_no].constructors(ns).is_empty() { // generate the default constructor let func = ns.default_constructor(contract_no); let cfg_no = all_cfg.len(); diff --git a/src/codegen/solana_accounts/account_collection.rs b/src/codegen/solana_accounts/account_collection.rs index dc94a2dd8..d31fd4c92 100644 --- a/src/codegen/solana_accounts/account_collection.rs +++ b/src/codegen/solana_accounts/account_collection.rs @@ -72,6 +72,18 @@ impl RecurseData<'_> { }, ); } + + fn add_program_id(&mut self, contract_name: &String) { + self.add_account( + format!("{}_programId", contract_name), + &SolanaAccount { + loc: Loc::Codegen, + is_signer: false, + is_writer: false, + generated: true, + }, + ) + } } /// Collect the accounts this contract needs @@ -333,10 +345,21 @@ fn check_instruction(instr: &Instr, data: &mut RecurseData) { // If the one passes the AccountMeta vector to the constructor call, there is no // need to collect accounts for the IDL. if let Some(constructor_no) = constructor_no { - transfer_accounts(loc, *contract_no, *constructor_no, data, false); + transfer_accounts(loc, *contract_no, *constructor_no, data); + } else { + data.add_account( + format!("{}_dataAccount", data.contracts[*contract_no].name), + &SolanaAccount { + loc: *loc, + is_signer: false, + is_writer: true, + generated: true, + }, + ); } } + data.add_program_id(&data.contracts[*contract_no].name); data.add_system_account(); } Instr::ExternalCall { @@ -387,8 +410,15 @@ fn check_instruction(instr: &Instr, data: &mut RecurseData) { if let Some(accounts) = accounts { accounts.recurse(data, check_expression); - } else if let Some((contract_no, function_no)) = contract_function_no { - transfer_accounts(loc, *contract_no, *function_no, data, program_id_populated); + } + + if let Some((contract_no, function_no)) = contract_function_no { + if !program_id_populated { + data.add_program_id(&data.contracts[*contract_no].name); + } + if accounts.is_none() { + transfer_accounts(loc, *contract_no, *function_no, data); + } } } Instr::EmitEvent { @@ -464,7 +494,6 @@ fn transfer_accounts( contract_no: usize, function_no: usize, data: &mut RecurseData, - program_id_present: bool, ) { let accounts_to_add = data.functions[function_no].solana_accounts.borrow().clone(); @@ -515,21 +544,6 @@ fn transfer_accounts( data.add_account(name, &account); } - if !program_id_present { - data.functions[data.ast_no] - .solana_accounts - .borrow_mut() - .insert( - format!("{}_programId", data.contracts[contract_no].name), - SolanaAccount { - is_signer: false, - is_writer: false, - generated: true, - loc: *loc, - }, - ); - } - let cfg_no = data.contracts[contract_no].all_functions[&function_no]; data.next_queue.insert((contract_no, cfg_no)); data.next_queue.insert((data.contract_no, data.cfg_func_no)); diff --git a/src/codegen/solana_accounts/account_management.rs b/src/codegen/solana_accounts/account_management.rs index a35f6c7e1..84f21a977 100644 --- a/src/codegen/solana_accounts/account_management.rs +++ b/src/codegen/solana_accounts/account_management.rs @@ -152,6 +152,31 @@ fn process_instruction( }; *accounts = Some(metas_vector); } + Instr::Constructor { + contract_no, + constructor_no: None, + accounts, + .. + } => { + let name_to_index = format!("{}_dataAccount", contracts[*contract_no].name); + let account_index = functions[ast_no] + .solana_accounts + .borrow() + .get_index_of(&name_to_index) + .unwrap(); + let ptr_to_address = accounts_vector_key_at_index(account_index); + let account_metas = vec![account_meta_literal(ptr_to_address, false, true)]; + let metas_vector = Expression::ArrayLiteral { + loc: Loc::Codegen, + ty: Type::Array( + Box::new(Type::Struct(StructType::AccountMeta)), + vec![ArrayLength::Fixed(BigInt::from(account_metas.len()))], + ), + dimensions: vec![1], + values: account_metas, + }; + *accounts = Some(metas_vector); + } Instr::AccountAccess { loc, name, var_no } => { // This could have been an Expression::AccountAccess if we had a three-address form. // The amount of code necessary to traverse all Instructions and all expressions recursively diff --git a/src/codegen/statements/mod.rs b/src/codegen/statements/mod.rs index 74ab65688..5d25a3776 100644 --- a/src/codegen/statements/mod.rs +++ b/src/codegen/statements/mod.rs @@ -12,16 +12,15 @@ use super::{ yul::inline_assembly_cfg, Builtin, Expression, Options, }; -use crate::sema::{ - ast::{ - self, ArrayLength, DestructureField, Function, Namespace, RetrieveType, Statement, Type, - Type::Uint, - }, - Recurse, +use crate::sema::ast::{ + self, ArrayLength, DestructureField, Function, Namespace, RetrieveType, SolanaAccount, + Statement, Type, Type::Uint, }; +use crate::sema::solana_accounts::BuiltinAccounts; +use crate::sema::Recurse; use num_bigint::BigInt; use num_traits::Zero; -use solang_parser::pt::{self, CodeLocation, Loc::Codegen}; +use solang_parser::pt::{self, CodeLocation, Loc, Loc::Codegen}; mod try_catch; @@ -1154,6 +1153,15 @@ impl Namespace { func.body = vec![Statement::Return(Codegen, None)]; func.has_body = true; + func.solana_accounts.borrow_mut().insert( + BuiltinAccounts::DataAccount.to_string(), + SolanaAccount { + loc: Loc::Codegen, + is_signer: false, + is_writer: true, + generated: true, + }, + ); func } diff --git a/src/emit/solana/target.rs b/src/emit/solana/target.rs index f0e08a9c3..d113e4d19 100644 --- a/src/emit/solana/target.rs +++ b/src/emit/solana/target.rs @@ -1227,20 +1227,15 @@ impl<'a> TargetRuntime<'a> for SolanaTarget { binary: &Binary<'b>, function: FunctionValue<'b>, _success: Option<&mut BasicValueEnum<'b>>, - contract_no: usize, - _address: PointerValue<'b>, + _contract_no: usize, + address: PointerValue<'b>, encoded_args: BasicValueEnum<'b>, encoded_args_len: BasicValueEnum<'b>, mut contract_args: ContractArgs<'b>, - ns: &ast::Namespace, + _ns: &ast::Namespace, _loc: Loc, ) { - let const_program_id = binary.emit_global_string( - "const_program_id", - ns.contracts[contract_no].program_id.as_ref().unwrap(), - true, - ); - contract_args.program_id = Some(const_program_id); + contract_args.program_id = Some(address); let payload = binary.vector_bytes(encoded_args); let payload_len = encoded_args_len.into_int_value(); diff --git a/src/sema/ast.rs b/src/sema/ast.rs index cd4dd592d..fe26c21f0 100644 --- a/src/sema/ast.rs +++ b/src/sema/ast.rs @@ -804,14 +804,17 @@ impl Contract { /// Does the constructor require arguments. Should be false is there is no constructor pub fn constructor_needs_arguments(&self, ns: &Namespace) -> bool { - self.have_constructor(ns) && self.no_args_constructor(ns).is_none() + !self.constructors(ns).is_empty() && self.no_args_constructor(ns).is_none() } - /// Does the contract have a constructor defined - pub fn have_constructor(&self, ns: &Namespace) -> bool { + /// Does the contract have a constructor defined? + /// Returns all the constructor function numbers if any + pub fn constructors(&self, ns: &Namespace) -> Vec { self.functions .iter() - .any(|func_no| ns.functions[*func_no].is_constructor()) + .copied() + .filter(|func_no| ns.functions[*func_no].is_constructor()) + .collect::>() } /// Return the constructor with no arguments @@ -1226,6 +1229,7 @@ pub struct CallArgs { pub accounts: Option>, pub seeds: Option>, pub flags: Option>, + pub program_id: Option>, } impl Recurse for CallArgs { diff --git a/src/sema/builtin.rs b/src/sema/builtin.rs index 2b8e38018..1908322d1 100644 --- a/src/sema/builtin.rs +++ b/src/sema/builtin.rs @@ -10,6 +10,7 @@ use super::expression::{ExprContext, ResolveTo}; use super::symtable::Symtable; use crate::sema::ast::{RetrieveType, Tag, UserTypeDecl}; use crate::sema::expression::resolve_expression::expression; +use crate::sema::namespace::ResolveTypeContext; use crate::Target; use num_bigint::BigInt; use num_traits::One; @@ -1082,7 +1083,7 @@ pub(super) fn resolve_namespace_call( let ty = ns.resolve_type( context.file_no, context.contract_no, - false, + ResolveTypeContext::None, ¶m.ty, diagnostics, )?; @@ -1123,7 +1124,7 @@ pub(super) fn resolve_namespace_call( let ty = ns.resolve_type( context.file_no, context.contract_no, - false, + ResolveTypeContext::None, args[1].remove_parenthesis(), diagnostics, )?; diff --git a/src/sema/contracts.rs b/src/sema/contracts.rs index abaf4a6b6..61fdb5e0c 100644 --- a/src/sema/contracts.rs +++ b/src/sema/contracts.rs @@ -1090,7 +1090,7 @@ fn check_base_args(contract_no: usize, ns: &mut ast::Namespace) { }) .collect::>(); - if contract.have_constructor(ns) { + if !contract.constructors(ns).is_empty() { for constructor_no in contract .functions .iter() diff --git a/src/sema/expression/constructor.rs b/src/sema/expression/constructor.rs index 09ed0ee8a..8977b7fb0 100644 --- a/src/sema/expression/constructor.rs +++ b/src/sema/expression/constructor.rs @@ -5,9 +5,9 @@ use crate::sema::diagnostics::Diagnostics; use crate::sema::expression::function_call::{collect_call_args, parse_call_args}; use crate::sema::expression::resolve_expression::expression; use crate::sema::expression::{ExprContext, ResolveTo}; +use crate::sema::namespace::ResolveTypeContext; use crate::sema::symtable::Symtable; use crate::sema::unused_variable::used_variable; -use crate::Target; use solang_parser::diagnostics::Diagnostic; use solang_parser::pt; use solang_parser::pt::{CodeLocation, Visibility}; @@ -60,8 +60,6 @@ fn constructor( return Err(()); } - solana_constructor_check(loc, no, diagnostics, context, &call_args, ns); - // check for circular references if circular_reference(no, context_contract_no, ns) { diagnostics.push(Diagnostic::error( @@ -214,9 +212,13 @@ pub fn constructor_named_args( ) -> Result { let (ty, call_args, _) = collect_call_args(ty, diagnostics)?; - let call_args = parse_call_args(loc, &call_args, false, context, ns, symtable, diagnostics)?; - - let no = match ns.resolve_type(context.file_no, context.contract_no, false, ty, diagnostics)? { + let no = match ns.resolve_type( + context.file_no, + context.contract_no, + ResolveTypeContext::None, + ty, + diagnostics, + )? { Type::Contract(n) => n, _ => { diagnostics.push(Diagnostic::error(*loc, "contract expected".to_string())); @@ -224,6 +226,17 @@ pub fn constructor_named_args( } }; + let call_args = parse_call_args( + loc, + &call_args, + Some(no), + false, + context, + ns, + symtable, + diagnostics, + )?; + // The current contract cannot be constructed with new. In order to create // the contract, we need the code hash of the contract. Part of that code // will be code we're emitted here. So we end up with a crypto puzzle. @@ -260,8 +273,6 @@ pub fn constructor_named_args( return Err(()); } - solana_constructor_check(loc, no, diagnostics, context, &call_args, ns); - // check for circular references if circular_reference(no, context_contract_no, ns) { diagnostics.push(Diagnostic::error( @@ -436,7 +447,13 @@ pub fn new( ty }; - let ty = ns.resolve_type(context.file_no, context.contract_no, false, ty, diagnostics)?; + let ty = ns.resolve_type( + context.file_no, + context.contract_no, + ResolveTypeContext::None, + ty, + diagnostics, + )?; match &ty { Type::Array(ty, dim) => { @@ -461,8 +478,16 @@ pub fn new( } Type::String | Type::DynamicBytes => {} Type::Contract(n) => { - let call_args = - parse_call_args(loc, &call_args, false, context, ns, symtable, diagnostics)?; + let call_args = parse_call_args( + loc, + &call_args, + Some(*n), + false, + context, + ns, + symtable, + diagnostics, + )?; return constructor(loc, *n, args, call_args, context, ns, symtable, diagnostics); } @@ -577,29 +602,51 @@ pub(super) fn deprecated_constructor_arguments( /// When calling a constructor on Solana, we must verify it the contract we are instantiating has /// a program id annotation and require the accounts call argument if the call is inside a loop. -fn solana_constructor_check( +pub(super) fn solana_constructor_check( loc: &pt::Loc, constructor_contract_no: usize, diagnostics: &mut Diagnostics, context: &ExprContext, call_args: &CallArgs, - ns: &Namespace, + ns: &mut Namespace, ) { - if ns.target != Target::Solana { - return; - } - - if ns.contracts[constructor_contract_no].program_id.is_none() { + if !ns.contracts[constructor_contract_no].instantiable { diagnostics.push(Diagnostic::error( *loc, format!( - "in order to instantiate contract '{}', a @program_id is required on contract '{}'", + "cannot construct '{}' of type '{}'", ns.contracts[constructor_contract_no].name, - ns.contracts[constructor_contract_no].name + ns.contracts[constructor_contract_no].ty ), )); } + if let Some(context_contract) = context.contract_no { + if circular_reference(constructor_contract_no, context_contract, ns) { + diagnostics.push(Diagnostic::error( + *loc, + format!( + "circular reference creating contract '{}'", + ns.contracts[constructor_contract_no].name + ), + )); + } + + if !ns.contracts[context_contract] + .creates + .contains(&constructor_contract_no) + { + ns.contracts[context_contract] + .creates + .push(constructor_contract_no); + } + } else { + diagnostics.push(Diagnostic::error( + *loc, + "constructors not allowed in free standing functions".to_string(), + )); + } + if !context.in_a_loop() || call_args.accounts.is_some() { return; } diff --git a/src/sema/expression/function_call.rs b/src/sema/expression/function_call.rs index 784012f72..2cf923595 100644 --- a/src/sema/expression/function_call.rs +++ b/src/sema/expression/function_call.rs @@ -6,15 +6,19 @@ use crate::sema::ast::{ }; use crate::sema::contracts::is_base; use crate::sema::diagnostics::Diagnostics; -use crate::sema::expression::constructor::{deprecated_constructor_arguments, new}; +use crate::sema::expression::constructor::{ + deprecated_constructor_arguments, new, solana_constructor_check, +}; use crate::sema::expression::literals::{named_struct_literal, struct_literal}; use crate::sema::expression::resolve_expression::expression; use crate::sema::expression::{ExprContext, ResolveTo}; use crate::sema::format::string_format; +use crate::sema::namespace::ResolveTypeContext; use crate::sema::symtable::Symtable; use crate::sema::unused_variable::check_function_call; use crate::sema::{builtin, using}; use crate::Target; +use num_bigint::{BigInt, Sign}; use solang_parser::diagnostics::Diagnostic; use solang_parser::pt; use solang_parser::pt::{CodeLocation, Loc, Visibility}; @@ -101,7 +105,16 @@ pub(super) fn call_function_type( mutability, } = ty { - let call_args = parse_call_args(loc, call_args, true, context, ns, symtable, diagnostics)?; + let call_args = parse_call_args( + loc, + call_args, + None, + true, + context, + ns, + symtable, + diagnostics, + )?; if let Some(value) = &call_args.value { if !value.const_zero(ns) && !matches!(mutability, Mutability::Payable(_)) { @@ -495,6 +508,7 @@ fn try_namespace( var: &pt::Expression, func: &pt::Identifier, args: &[pt::Expression], + call_args: &[&pt::NamedArgument], call_args_loc: Option, context: &ExprContext, ns: &mut Namespace, @@ -592,7 +606,22 @@ fn try_namespace( // is a base contract of us if let Some(contract_no) = context.contract_no { if is_base(call_contract_no, contract_no, ns) { - if let Some(loc) = call_args_loc { + if ns.target == Target::Solana && call_args_loc.is_some() { + // On Solana, assume this is an external call + return contract_call_pos_args( + loc, + call_contract_no, + func, + None, + args, + call_args, + context, + ns, + symtable, + diagnostics, + resolve_to, + ); + } else if let Some(loc) = call_args_loc { diagnostics.push(Diagnostic::error( loc, "call arguments not allowed on internal calls".to_string(), @@ -619,13 +648,31 @@ fn try_namespace( symtable, diagnostics, )?)); - } else { + } else if ns.target != Target::Solana { diagnostics.push(Diagnostic::error( *loc, "function calls via contract name are only valid for base contracts".into(), )); } } + + if ns.target == Target::Solana { + // If the symbol resolves to a contract, this is an external call on Solana + // regardless of whether we are inside a contract or not. + return contract_call_pos_args( + loc, + call_contract_no, + func, + None, + args, + call_args, + context, + ns, + symtable, + diagnostics, + resolve_to, + ); + } } } @@ -873,7 +920,7 @@ fn try_user_type( if let Ok(Type::UserType(no)) = ns.resolve_type( context.file_no, context.contract_no, - false, + ResolveTypeContext::None, var, &mut Diagnostics::default(), ) { @@ -1069,148 +1116,19 @@ fn try_type_method( } Type::Contract(ext_contract_no) => { - let call_args = - parse_call_args(loc, call_args, true, context, ns, symtable, diagnostics)?; - - let mut errors = Diagnostics::default(); - let mut name_matches: Vec = Vec::new(); - - for function_no in ns.contracts[*ext_contract_no].all_functions.keys() { - if func.name != ns.functions[*function_no].name - || ns.functions[*function_no].ty != pt::FunctionTy::Function - { - continue; - } - - name_matches.push(*function_no); - } - - for function_no in &name_matches { - let params_len = ns.functions[*function_no].params.len(); - - if params_len != args.len() { - errors.push(Diagnostic::error( - *loc, - format!( - "function expects {} arguments, {} provided", - params_len, - args.len() - ), - )); - continue; - } - - let mut matches = true; - let mut cast_args = Vec::new(); - - // check if arguments can be implicitly casted - for (i, arg) in args.iter().enumerate() { - let ty = ns.functions[*function_no].params[i].ty.clone(); - - let arg = match expression( - arg, - context, - ns, - symtable, - &mut errors, - ResolveTo::Type(&ty), - ) { - Ok(e) => e, - Err(_) => { - matches = false; - continue; - } - }; - - match arg.cast(&arg.loc(), &ty, true, ns, &mut errors) { - Ok(expr) => cast_args.push(expr), - Err(()) => { - matches = false; - continue; - } - } - } - - if matches { - if !ns.functions[*function_no].is_public() { - diagnostics.push(Diagnostic::error( - *loc, - format!("function '{}' is not 'public' or 'external'", func.name), - )); - return Err(()); - } - - if let Some(value) = &call_args.value { - if !value.const_zero(ns) && !ns.functions[*function_no].is_payable() { - diagnostics.push(Diagnostic::error( - *loc, - format!( - "sending value to function '{}' which is not payable", - func.name - ), - )); - return Err(()); - } - } - - let func = &ns.functions[*function_no]; - let returns = function_returns(func, resolve_to); - let ty = function_type(func, true, resolve_to); - - return Ok(Some(Expression::ExternalFunctionCall { - loc: *loc, - returns, - function: Box::new(Expression::ExternalFunction { - loc: *loc, - ty, - function_no: *function_no, - address: Box::new(var_expr.cast( - &var.loc(), - &Type::Contract(func.contract_no.unwrap()), - true, - ns, - diagnostics, - )?), - }), - args: cast_args, - call_args, - })); - } else if name_matches.len() > 1 && diagnostics.extend_non_casting(&errors) { - return Err(()); - } - } - - // what about call args - match using::try_resolve_using_call( + return contract_call_pos_args( loc, + *ext_contract_no, func, - var_expr, - context, + Some(var_expr), args, + call_args, + context, + ns, symtable, diagnostics, - ns, resolve_to, - ) { - Ok(Some(expr)) => { - return Ok(Some(expr)); - } - Ok(None) => (), - Err(_) => { - return Err(()); - } - } - - if name_matches.len() == 1 { - diagnostics.extend(errors); - } else if name_matches.len() != 1 { - diagnostics.push(Diagnostic::error( - *loc, - "cannot find overloaded function which matches signature".to_string(), - )); - } - - return Err(()); + ); } Type::Address(is_payable) => { @@ -1287,8 +1205,16 @@ fn try_type_method( }; if let Some(ty) = ty { - let call_args = - parse_call_args(loc, call_args, true, context, ns, symtable, diagnostics)?; + let call_args = parse_call_args( + loc, + call_args, + None, + true, + context, + ns, + symtable, + diagnostics, + )?; if ty != CallTy::Regular && call_args.value.is_some() { diagnostics.push(Diagnostic::error( @@ -1389,6 +1315,7 @@ pub(super) fn method_call_pos_args( var, func, args, + call_args, call_args_loc, context, ns, @@ -1397,6 +1324,7 @@ pub(super) fn method_call_pos_args( resolve_to, )? { return Ok(resolved_call); + } else { } if let Some(resolved_call) = try_user_type( @@ -1429,6 +1357,26 @@ pub(super) fn method_call_pos_args( &path, &mut Diagnostics::default(), ) { + if let Some(callee_contract) = + is_solana_external_call(&list, context.contract_no, &call_args_loc, ns) + { + if let Some(resolved_call) = contract_call_pos_args( + &var.loc(), + callee_contract, + func, + None, + args, + call_args, + context, + ns, + symtable, + diagnostics, + resolve_to, + )? { + return Ok(resolved_call); + } + } + if let Some(loc) = call_args_loc { diagnostics.push(Diagnostic::error( loc, @@ -1620,7 +1568,22 @@ pub(super) fn method_call_named_args( // is a base contract of us if let Some(contract_no) = context.contract_no { if is_base(call_contract_no, contract_no, ns) { - if let Some(loc) = call_args_loc { + if ns.target == Target::Solana && call_args_loc.is_some() { + // If on Solana, assume this is an external call + return contract_call_named_args( + loc, + None, + func_name, + args, + call_args, + call_contract_no, + context, + symtable, + ns, + diagnostics, + resolve_to, + ); + } else if let Some(loc) = call_args_loc { diagnostics.push(Diagnostic::error( loc, "call arguments not allowed on internal calls".to_string(), @@ -1646,13 +1609,31 @@ pub(super) fn method_call_named_args( symtable, diagnostics, ); - } else { + } else if ns.target != Target::Solana { diagnostics.push(Diagnostic::error( *loc, "function calls via contract name are only valid for base contracts".into(), )); } } + + if ns.target == Target::Solana { + // If the identifier symbol resolves to a contract, this an external call on Solana + // regardless of whether we are inside a contract or not. + return contract_call_named_args( + loc, + None, + func_name, + args, + call_args, + call_contract_no, + context, + symtable, + ns, + diagnostics, + resolve_to, + ); + } } } @@ -1664,6 +1645,24 @@ pub(super) fn method_call_named_args( &path, &mut Diagnostics::default(), ) { + if let Some(callee_contract) = + is_solana_external_call(&list, context.contract_no, &call_args_loc, ns) + { + return contract_call_named_args( + &var.loc(), + None, + func_name, + args, + call_args, + callee_contract, + context, + symtable, + ns, + diagnostics, + resolve_to, + ); + } + if let Some(loc) = call_args_loc { diagnostics.push(Diagnostic::error( loc, @@ -1690,192 +1689,19 @@ pub(super) fn method_call_named_args( let var_ty = var_expr.ty(); if let Type::Contract(external_contract_no) = &var_ty.deref_any() { - let call_args = parse_call_args(loc, call_args, true, context, ns, symtable, diagnostics)?; - - let mut arguments = HashMap::new(); - - // check if the arguments are not garbage - for arg in args { - if arguments.contains_key(arg.name.name.as_str()) { - diagnostics.push(Diagnostic::error( - arg.name.loc, - format!("duplicate argument with name '{}'", arg.name.name), - )); - - let _ = expression( - &arg.expr, - context, - ns, - symtable, - diagnostics, - ResolveTo::Unknown, - ); - - continue; - } - - arguments.insert(arg.name.name.as_str(), &arg.expr); - } - - let mut errors = Diagnostics::default(); - let mut name_matches: Vec = Vec::new(); - - // function call - for function_no in ns.contracts[*external_contract_no].all_functions.keys() { - if ns.functions[*function_no].name != func_name.name - || ns.functions[*function_no].ty != pt::FunctionTy::Function - { - continue; - } - - name_matches.push(*function_no); - } - - for function_no in &name_matches { - let func = &ns.functions[*function_no]; - - let unnamed_params = func.params.iter().filter(|p| p.id.is_none()).count(); - let params_len = func.params.len(); - - let mut matches = true; - - if unnamed_params > 0 { - errors.push(Diagnostic::cast_error_with_note( - *loc, - format!( - "function cannot be called with named arguments as {unnamed_params} of its parameters do not have names" - ), - func.loc, - format!("definition of {}", func.name), - )); - matches = false; - } else if params_len != args.len() { - errors.push(Diagnostic::cast_error( - *loc, - format!( - "function expects {} arguments, {} provided", - params_len, - args.len() - ), - )); - matches = false; - } - let mut cast_args = Vec::new(); - - for i in 0..params_len { - let param = ns.functions[*function_no].params[i].clone(); - if param.id.is_none() { - continue; - } - - let arg = match arguments.get(param.name_as_str()) { - Some(a) => a, - None => { - matches = false; - diagnostics.push(Diagnostic::cast_error( - *loc, - format!( - "missing argument '{}' to function '{}'", - param.name_as_str(), - func_name.name, - ), - )); - continue; - } - }; - - let arg = match expression( - arg, - context, - ns, - symtable, - &mut errors, - ResolveTo::Type(¶m.ty), - ) { - Ok(e) => e, - Err(()) => { - matches = false; - continue; - } - }; - - match arg.cast(&arg.loc(), ¶m.ty, true, ns, &mut errors) { - Ok(expr) => cast_args.push(expr), - Err(()) => { - matches = false; - break; - } - } - } - - if matches { - if !ns.functions[*function_no].is_public() { - diagnostics.push(Diagnostic::error( - *loc, - format!( - "function '{}' is not 'public' or 'external'", - func_name.name - ), - )); - } else if let Some(value) = &call_args.value { - if !value.const_zero(ns) && !ns.functions[*function_no].is_payable() { - diagnostics.push(Diagnostic::error( - *loc, - format!( - "sending value to function '{}' which is not payable", - func_name.name - ), - )); - } - } - - let func = &ns.functions[*function_no]; - let returns = function_returns(func, resolve_to); - let ty = function_type(func, true, resolve_to); - - return Ok(Expression::ExternalFunctionCall { - loc: *loc, - returns, - function: Box::new(Expression::ExternalFunction { - loc: *loc, - ty, - function_no: *function_no, - address: Box::new(var_expr.cast( - &var.loc(), - &Type::Contract(func.contract_no.unwrap()), - true, - ns, - diagnostics, - )?), - }), - args: cast_args, - call_args, - }); - } else if name_matches.len() > 1 && diagnostics.extend_non_casting(&errors) { - return Err(()); - } - } - - match name_matches.len() { - 0 => { - diagnostics.push(Diagnostic::error( - *loc, - format!( - "contract '{}' does not have function '{}'", - var_ty.deref_any().to_string(ns), - func_name.name - ), - )); - } - 1 => diagnostics.extend(errors), - _ => { - diagnostics.push(Diagnostic::error( - *loc, - "cannot find overloaded function which matches signature".to_string(), - )); - } - } - return Err(()); + return contract_call_named_args( + loc, + Some(var_expr), + func_name, + args, + call_args, + *external_contract_no, + context, + symtable, + ns, + diagnostics, + resolve_to, + ); } diagnostics.push(Diagnostic::error( @@ -1941,6 +1767,7 @@ pub fn collect_call_args<'a>( pub(super) fn parse_call_args( loc: &pt::Loc, call_args: &[&pt::NamedArgument], + callee_contract: Option, external_call: bool, context: &ExprContext, ns: &mut Namespace, @@ -2148,13 +1975,37 @@ pub(super) fn parse_call_args( res.seeds = Some(Box::new(expr)); } - "flags" => { - if !(ns.target.is_polkadot() && external_call) { + "program_id" => { + if ns.target != Target::Solana { diagnostics.push(Diagnostic::error( arg.loc, - "'flags' are only permitted for external calls on polkadot".into(), - )); - return Err(()); + format!( + "'program_id' not permitted for external calls or constructors on {}", + ns.target + ), + )); + return Err(()); + } + + let ty = Type::Address(false); + let expr = expression( + &arg.expr, + context, + ns, + symtable, + diagnostics, + ResolveTo::Type(&ty), + )?; + + res.program_id = Some(Box::new(expr)); + } + "flags" => { + if !(ns.target.is_polkadot() && external_call) { + diagnostics.push(Diagnostic::error( + arg.loc, + "'flags' are only permitted for external calls on polkadot".into(), + )); + return Err(()); } let ty = Type::Uint(32); @@ -2179,24 +2030,37 @@ pub(super) fn parse_call_args( } } - // address is required on solana constructors - if ns.target == Target::Solana - && !external_call - && res.accounts.is_none() - && !matches!( - ns.functions[context.function_no.unwrap()].visibility, - Visibility::External(_) - ) - && !ns.functions[context.function_no.unwrap()].is_constructor() - { - diagnostics.push(Diagnostic::error( - *loc, - "accounts are required for calling a contract. You can either provide the \ + if ns.target == Target::Solana { + if !external_call + && res.accounts.is_none() + && !matches!( + ns.functions[context.function_no.unwrap()].visibility, + Visibility::External(_) + ) + && !ns.functions[context.function_no.unwrap()].is_constructor() + { + diagnostics.push(Diagnostic::error( + *loc, + "accounts are required for calling a contract. You can either provide the \ accounts with the {accounts: ...} call argument or change this function's \ visibility to external" - .to_string(), - )); - return Err(()); + .to_string(), + )); + return Err(()); + } + + if let Some(callee_contract_no) = callee_contract { + if res.program_id.is_none() && ns.contracts[callee_contract_no].program_id.is_none() { + diagnostics.push(Diagnostic::error( + *loc, + "a contract needs a program id to be called. Either a '@program_id' \ + must be declared above a contract or the {program_id: ...} call argument \ + must be present" + .to_string(), + )); + return Err(()); + } + } } Ok(res) @@ -2219,7 +2083,7 @@ pub fn named_call_expr( match ns.resolve_type( context.file_no, context.contract_no, - true, + ResolveTypeContext::Casting, ty, &mut nullsink, ) { @@ -2286,7 +2150,7 @@ pub fn call_expr( match ns.resolve_type( context.file_no, context.contract_no, - true, + ResolveTypeContext::Casting, ty, &mut nullsink, ) { @@ -2655,3 +2519,506 @@ fn resolve_internal_call( args: cast_args, }) } + +/// Resolve call to contract with named arguments +fn contract_call_named_args( + loc: &pt::Loc, + var_expr: Option, + func_name: &pt::Identifier, + args: &[pt::NamedArgument], + call_args: &[&pt::NamedArgument], + external_contract_no: usize, + context: &ExprContext, + symtable: &mut Symtable, + ns: &mut Namespace, + diagnostics: &mut Diagnostics, + resolve_to: ResolveTo, +) -> Result { + let mut arguments = HashMap::new(); + + // check if the arguments are not garbage + for arg in args { + if arguments.contains_key(arg.name.name.as_str()) { + diagnostics.push(Diagnostic::error( + arg.name.loc, + format!("duplicate argument with name '{}'", arg.name.name), + )); + + let _ = expression( + &arg.expr, + context, + ns, + symtable, + diagnostics, + ResolveTo::Unknown, + ); + + continue; + } + + arguments.insert(arg.name.name.as_str(), &arg.expr); + } + + let (call_args, name_matches) = match preprocess_contract_call( + loc, + call_args, + external_contract_no, + func_name, + args, + context, + ns, + symtable, + diagnostics, + ) { + PreProcessedCall::Success { + call_args, + name_matches, + } => (call_args, name_matches), + PreProcessedCall::DefaultConstructor(expr) => return Ok(expr), + PreProcessedCall::Error => return Err(()), + }; + + let mut errors = Diagnostics::default(); + + for function_no in &name_matches { + let func = &ns.functions[*function_no]; + + let unnamed_params = func.params.iter().filter(|p| p.id.is_none()).count(); + let params_len = func.params.len(); + + let mut matches = true; + + if unnamed_params > 0 { + errors.push(Diagnostic::cast_error_with_note( + *loc, + format!( + "function cannot be called with named arguments as {unnamed_params} of its parameters do not have names" + ), + func.loc, + format!("definition of {}", func.name), + )); + matches = false; + } else if params_len != args.len() { + errors.push(Diagnostic::cast_error( + *loc, + format!( + "function expects {} arguments, {} provided", + params_len, + args.len() + ), + )); + matches = false; + } + let mut cast_args = Vec::new(); + + for i in 0..params_len { + let param = ns.functions[*function_no].params[i].clone(); + if param.id.is_none() { + continue; + } + + let arg = match arguments.get(param.name_as_str()) { + Some(a) => a, + None => { + matches = false; + diagnostics.push(Diagnostic::cast_error( + *loc, + format!( + "missing argument '{}' to function '{}'", + param.name_as_str(), + func_name.name, + ), + )); + continue; + } + }; + + let arg = match expression( + arg, + context, + ns, + symtable, + &mut errors, + ResolveTo::Type(¶m.ty), + ) { + Ok(e) => e, + Err(()) => { + matches = false; + continue; + } + }; + + match arg.cast(&arg.loc(), ¶m.ty, true, ns, &mut errors) { + Ok(expr) => cast_args.push(expr), + Err(()) => { + matches = false; + break; + } + } + } + + if matches { + return contract_call_match( + loc, + func_name, + *function_no, + external_contract_no, + call_args, + cast_args, + var_expr.as_ref(), + ns, + diagnostics, + resolve_to, + ); + } else if name_matches.len() > 1 && diagnostics.extend_non_casting(&errors) { + return Err(()); + } + } + + match name_matches.len() { + 0 => { + diagnostics.push(Diagnostic::error( + *loc, + format!( + "contract '{}' does not have function '{}'", + ns.contracts[external_contract_no].name, func_name.name + ), + )); + } + 1 => diagnostics.extend(errors), + _ => { + diagnostics.push(Diagnostic::error( + *loc, + "cannot find overloaded function which matches signature".to_string(), + )); + } + } + Err(()) +} + +/// Resolve call to contract with positional arguments +fn contract_call_pos_args( + loc: &pt::Loc, + external_contract_no: usize, + func: &pt::Identifier, + var_expr: Option<&Expression>, + args: &[pt::Expression], + call_args: &[&pt::NamedArgument], + context: &ExprContext, + ns: &mut Namespace, + symtable: &mut Symtable, + diagnostics: &mut Diagnostics, + resolve_to: ResolveTo, +) -> Result, ()> { + let (call_args, name_matches) = match preprocess_contract_call( + loc, + call_args, + external_contract_no, + func, + args, + context, + ns, + symtable, + diagnostics, + ) { + PreProcessedCall::Success { + call_args, + name_matches, + } => (call_args, name_matches), + PreProcessedCall::DefaultConstructor(expr) => return Ok(Some(expr)), + PreProcessedCall::Error => return Err(()), + }; + + let mut errors = Diagnostics::default(); + + for function_no in &name_matches { + let params_len = ns.functions[*function_no].params.len(); + + if params_len != args.len() { + errors.push(Diagnostic::error( + *loc, + format!( + "function expects {} arguments, {} provided", + params_len, + args.len() + ), + )); + continue; + } + + let mut matches = true; + let mut cast_args = Vec::new(); + + // check if arguments can be implicitly casted + for (i, arg) in args.iter().enumerate() { + let ty = ns.functions[*function_no].params[i].ty.clone(); + + let arg = match expression( + arg, + context, + ns, + symtable, + &mut errors, + ResolveTo::Type(&ty), + ) { + Ok(e) => e, + Err(_) => { + matches = false; + continue; + } + }; + + match arg.cast(&arg.loc(), &ty, true, ns, &mut errors) { + Ok(expr) => cast_args.push(expr), + Err(()) => { + matches = false; + continue; + } + } + } + + if matches { + let resolved_call = contract_call_match( + loc, + func, + *function_no, + external_contract_no, + call_args, + cast_args, + var_expr, + ns, + diagnostics, + resolve_to, + )?; + return Ok(Some(resolved_call)); + } else if name_matches.len() > 1 && diagnostics.extend_non_casting(&errors) { + return Err(()); + } + } + + if let Some(var) = var_expr { + // what about call args + match using::try_resolve_using_call( + loc, + func, + var, + context, + args, + symtable, + diagnostics, + ns, + resolve_to, + ) { + Ok(Some(expr)) => { + return Ok(Some(expr)); + } + Ok(None) => (), + Err(_) => { + return Err(()); + } + } + } + + if name_matches.len() == 1 { + diagnostics.extend(errors); + } else if name_matches.len() != 1 { + diagnostics.push(Diagnostic::error( + *loc, + "cannot find overloaded function which matches signature".to_string(), + )); + } + + Err(()) +} + +/// Checks if an identifier path is an external call on Solana. +/// For instance, my_file.my_contract.my_func() may be a call to a contract. +fn is_solana_external_call( + list: &[(pt::Loc, usize)], + contract_no: Option, + call_args_loc: &Option, + ns: &Namespace, +) -> Option { + if ns.target == Target::Solana + && list.len() == 1 + && ns.functions[list[0].1].contract_no != contract_no + { + if let (Some(callee), Some(caller)) = (ns.functions[list[0].1].contract_no, contract_no) { + if is_base(callee, caller, ns) && call_args_loc.is_none() { + return None; + } + } + return ns.functions[list[0].1].contract_no; + } + + None +} + +/// Data structure to manage the returns of 'preprocess_contract_call' +enum PreProcessedCall { + Success { + call_args: CallArgs, + name_matches: Vec, + }, + DefaultConstructor(Expression), + Error, +} + +/// This functions preprocesses calls to contracts, i.e. it parses the call arguments, +/// find function name matches and identifies if we are calling a constructor on Solana. +fn preprocess_contract_call( + loc: &pt::Loc, + call_args: &[&pt::NamedArgument], + external_contract_no: usize, + func: &pt::Identifier, + args: &[T], + context: &ExprContext, + ns: &mut Namespace, + symtable: &mut Symtable, + diagnostics: &mut Diagnostics, +) -> PreProcessedCall { + let call_args = if let Ok(call_args) = parse_call_args( + loc, + call_args, + Some(external_contract_no), + func.name != "new", + context, + ns, + symtable, + diagnostics, + ) { + call_args + } else { + return PreProcessedCall::Error; + }; + + let mut name_matches: Vec = Vec::new(); + + for function_no in ns.contracts[external_contract_no].all_functions.keys() { + if func.name != ns.functions[*function_no].name + || ns.functions[*function_no].ty != pt::FunctionTy::Function + { + continue; + } + + name_matches.push(*function_no); + } + + if ns.target == Target::Solana && func.name == "new" { + solana_constructor_check( + loc, + external_contract_no, + diagnostics, + context, + &call_args, + ns, + ); + + let constructor_nos = ns.contracts[external_contract_no].constructors(ns); + if !constructor_nos.is_empty() { + // Solana contracts shall have only a single constructor + assert_eq!(constructor_nos.len(), 1); + name_matches.push(constructor_nos[0]); + } else if !args.is_empty() { + // Default constructor must not receive arguments + diagnostics.push(Diagnostic::error( + *loc, + format!( + "'{}' constructor takes no argument", + ns.contracts[external_contract_no].name + ), + )); + return PreProcessedCall::Error; + } else { + // Default constructor case + return PreProcessedCall::DefaultConstructor(Expression::Constructor { + loc: *loc, + contract_no: external_contract_no, + constructor_no: None, + args: vec![], + call_args, + }); + } + } + + PreProcessedCall::Success { + call_args, + name_matches, + } +} + +/// This function generates the final expression when a contract's function is matched with both +/// the provided name and arguments +fn contract_call_match( + loc: &pt::Loc, + func: &pt::Identifier, + function_no: usize, + external_contract_no: usize, + call_args: CallArgs, + cast_args: Vec, + var_expr: Option<&Expression>, + ns: &Namespace, + diagnostics: &mut Diagnostics, + resolve_to: ResolveTo, +) -> Result { + if !ns.functions[function_no].is_public() { + diagnostics.push(Diagnostic::error( + *loc, + format!("function '{}' is not 'public' or 'external'", func.name), + )); + return Err(()); + } else if let Some(value) = &call_args.value { + if !value.const_zero(ns) && !ns.functions[function_no].is_payable() { + diagnostics.push(Diagnostic::error( + *loc, + format!( + "sending value to function '{}' which is not payable", + func.name + ), + )); + return Err(()); + } + } + + let func = &ns.functions[function_no]; + let returns = function_returns(func, resolve_to); + let ty = function_type(func, true, resolve_to); + + let (address, implicit) = if let Some(program_id_var) = &call_args.program_id { + (*program_id_var.clone(), false) + } else if let Some(address_id) = &ns.contracts[external_contract_no].program_id { + ( + Expression::NumberLiteral { + loc: *loc, + ty: Type::Address(false), + value: BigInt::from_bytes_be(Sign::Plus, address_id), + }, + false, + ) + } else if let Some(var) = var_expr { + (var.clone(), true) + } else { + unreachable!("address not found") + }; + + Ok({ + Expression::ExternalFunctionCall { + loc: *loc, + returns, + function: Box::new(Expression::ExternalFunction { + loc: *loc, + ty, + function_no, + address: Box::new(address.cast( + &address.loc(), + &Type::Contract(func.contract_no.unwrap()), + implicit, + ns, + diagnostics, + )?), + }), + args: cast_args, + call_args, + } + }) +} diff --git a/src/sema/expression/member_access.rs b/src/sema/expression/member_access.rs index c5105a2d9..a761aaa99 100644 --- a/src/sema/expression/member_access.rs +++ b/src/sema/expression/member_access.rs @@ -10,6 +10,7 @@ use crate::sema::expression::function_call::function_type; use crate::sema::expression::integers::bigint_to_expression; use crate::sema::expression::resolve_expression::expression; use crate::sema::expression::{ExprContext, ResolveTo}; +use crate::sema::namespace::ResolveTypeContext; use crate::sema::solana_accounts::BuiltinAccounts; use crate::sema::symtable::Symtable; use crate::sema::unused_variable::{assigned_variable, used_variable}; @@ -648,7 +649,7 @@ fn type_name_expr( let ty = ns.resolve_type( context.file_no, context.contract_no, - false, + ResolveTypeContext::FunctionType, &args[0], diagnostics, )?; diff --git a/src/sema/functions.rs b/src/sema/functions.rs index f3a6a1271..c670873ac 100644 --- a/src/sema/functions.rs +++ b/src/sema/functions.rs @@ -10,6 +10,7 @@ use super::{ }; use crate::sema::ast::ParameterAnnotation; use crate::sema::function_annotation::unexpected_parameter_annotation; +use crate::sema::namespace::ResolveTypeContext; use crate::Target; use solang_parser::pt::FunctionTy; use solang_parser::{ @@ -881,7 +882,13 @@ pub fn resolve_params( let mut ty_loc = p.ty.loc(); - match ns.resolve_type(file_no, contract_no, false, &p.ty, diagnostics) { + match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &p.ty, + diagnostics, + ) { Ok(ty) => { if !is_internal { if ty.contains_internal_function(ns) { @@ -1002,7 +1009,13 @@ pub fn resolve_returns( let mut ty_loc = r.ty.loc(); - match ns.resolve_type(file_no, contract_no, false, &r.ty, diagnostics) { + match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &r.ty, + diagnostics, + ) { Ok(ty) => { if !is_internal { if ty.contains_internal_function(ns) { diff --git a/src/sema/namespace.rs b/src/sema/namespace.rs index 69504fc66..9a5f4b5c7 100644 --- a/src/sema/namespace.rs +++ b/src/sema/namespace.rs @@ -25,6 +25,14 @@ use solang_parser::{ }; use std::collections::HashMap; +/// Provides context information for the `resolve_type` function. +#[derive(PartialEq, Eq)] +pub(super) enum ResolveTypeContext { + None, + Casting, + FunctionType, +} + impl Namespace { /// Create a namespace and populate with the parameters for the target pub fn new(target: Target) -> Self { @@ -892,7 +900,7 @@ impl Namespace { &mut self, file_no: usize, contract_no: Option, - casting: bool, + resolve_context: ResolveTypeContext, id: &pt::Expression, diagnostics: &mut Diagnostics, ) -> Result { @@ -946,10 +954,20 @@ impl Namespace { value_name, .. } => { - let key_ty = - self.resolve_type(file_no, contract_no, false, key, diagnostics)?; - let value_ty = - self.resolve_type(file_no, contract_no, false, value, diagnostics)?; + let key_ty = self.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + key, + diagnostics, + )?; + let value_ty = self.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + value, + diagnostics, + )?; match key_ty { Type::Mapping(..) => { @@ -1162,7 +1180,7 @@ impl Namespace { } } pt::Type::Payable => { - if !casting { + if resolve_context != ResolveTypeContext::Casting { diagnostics.push(Diagnostic::decl_error( id.loc(), "'payable' cannot be used for type declarations, only casting. use 'address payable'" @@ -1211,11 +1229,25 @@ impl Namespace { Box::new(Type::Struct(*str_ty)), resolve_dimensions(&dimensions, diagnostics)?, )), - Some(Symbol::Contract(_, n)) if dimensions.is_empty() => Ok(Type::Contract(*n)), - Some(Symbol::Contract(_, n)) => Ok(Type::Array( - Box::new(Type::Contract(*n)), - resolve_dimensions(&dimensions, diagnostics)?, - )), + Some(Symbol::Contract(_, n)) => { + if self.target == Target::Solana + && resolve_context != ResolveTypeContext::FunctionType + { + diagnostics.push(Diagnostic::error( + id.loc, + "contracts are not allowed as types on Solana".to_string(), + )); + return Err(()); + } + if dimensions.is_empty() { + Ok(Type::Contract(*n)) + } else { + Ok(Type::Array( + Box::new(Type::Contract(*n)), + resolve_dimensions(&dimensions, diagnostics)?, + )) + } + } Some(Symbol::Event(_)) => { diagnostics.push(Diagnostic::decl_error( id.loc, diff --git a/src/sema/statements.rs b/src/sema/statements.rs index 91d2b0512..a35ed659c 100644 --- a/src/sema/statements.rs +++ b/src/sema/statements.rs @@ -18,6 +18,7 @@ use crate::sema::expression::function_call::{ use crate::sema::expression::resolve_expression::expression; use crate::sema::function_annotation::function_body_annotations; use crate::sema::function_annotation::{unexpected_parameter_annotation, UnresolvedAnnotation}; +use crate::sema::namespace::ResolveTypeContext; use crate::sema::symtable::{VariableInitializer, VariableUsage}; use crate::sema::unused_variable::{assigned_variable, check_function_call, used_variable}; use crate::sema::yul::resolve_inline_assembly; @@ -1838,8 +1839,13 @@ fn resolve_var_decl_ty( diagnostics: &mut Diagnostics, ) -> Result<(Type, pt::Loc), ()> { let mut loc_ty = ty.loc(); - let mut var_ty = - ns.resolve_type(context.file_no, context.contract_no, false, ty, diagnostics)?; + let mut var_ty = ns.resolve_type( + context.file_no, + context.contract_no, + ResolveTypeContext::None, + ty, + diagnostics, + )?; if let Some(storage) = storage { if !var_ty.can_have_data_location() { diff --git a/src/sema/tests/mod.rs b/src/sema/tests/mod.rs index 13e976b44..56c03719a 100644 --- a/src/sema/tests/mod.rs +++ b/src/sema/tests/mod.rs @@ -457,9 +457,8 @@ contract aborting { contract runner { function test() external pure { - aborting abort = new aborting(); - try abort.abort() returns (int32 a, bool b) { + try aborting.abort() returns (int32 a, bool b) { // call succeeded; return values are in a and b } catch Error(string x) { @@ -535,16 +534,15 @@ fn dynamic_account_metas() { import 'solana'; contract creator { - Child public c; function create_child_with_meta(address child, address payer) public { AccountMeta[] metas = new AccountMeta[](2); metas[0] = AccountMeta({pubkey: child, is_signer: false, is_writable: false}); metas[1] = AccountMeta({pubkey: payer, is_signer: true, is_writable: true}); - c = new Child{accounts: metas}(payer); + Child.new{accounts: metas}(payer); - c.say_hello(); + Child.say_hello(); } } @@ -580,19 +578,17 @@ fn no_address_and_no_metas() { import 'solana'; contract creator { - Child public c; - function create_child_with_meta(address child, address payer) public { - - c = new Child(payer); - - c.say_hello(); + function create_child_with_meta(address child) public { + Child.new(); + Child.say_hello(); } } +@program_id("Chi1d5XD6nTAp2EyaNGqMxZzUjh6NvhXRxbGHP3D1RaT") contract Child { @payer(payer) @space(511 + 7) - constructor(address payer) { + constructor() { print("In child constructor"); } diff --git a/src/sema/types.rs b/src/sema/types.rs index 2be049536..6b030a100 100644 --- a/src/sema/types.rs +++ b/src/sema/types.rs @@ -10,6 +10,7 @@ use super::{ diagnostics::Diagnostics, ContractDefinition, SOLANA_SPARSE_ARRAY_SIZE, }; +use crate::sema::namespace::ResolveTypeContext; use crate::Target; use base58::{FromBase58, FromBase58Error}; use indexmap::IndexMap; @@ -214,7 +215,13 @@ fn type_decl( ) { let mut diagnostics = Diagnostics::default(); - let mut ty = match ns.resolve_type(file_no, contract_no, false, &def.ty, &mut diagnostics) { + let mut ty = match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &def.ty, + &mut diagnostics, + ) { Ok(ty) => ty, Err(_) => { ns.diagnostics.extend(diagnostics); @@ -700,7 +707,13 @@ pub fn struct_decl( for field in &def.fields { let mut diagnostics = Diagnostics::default(); - let ty = match ns.resolve_type(file_no, contract_no, false, &field.ty, &mut diagnostics) { + let ty = match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &field.ty, + &mut diagnostics, + ) { Ok(s) => s, Err(()) => { ns.diagnostics.extend(diagnostics); @@ -795,8 +808,13 @@ fn event_decl( for field in &def.fields { let mut diagnostics = Diagnostics::default(); - let mut ty = match ns.resolve_type(file_no, contract_no, false, &field.ty, &mut diagnostics) - { + let mut ty = match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &field.ty, + &mut diagnostics, + ) { Ok(s) => s, Err(()) => { ns.diagnostics.extend(diagnostics); @@ -913,8 +931,13 @@ fn error_decl( for field in &def.fields { let mut diagnostics = Diagnostics::default(); - let mut ty = match ns.resolve_type(file_no, contract_no, false, &field.ty, &mut diagnostics) - { + let mut ty = match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &field.ty, + &mut diagnostics, + ) { Ok(s) => s, Err(()) => { ns.diagnostics.extend(diagnostics); diff --git a/src/sema/unused_variable.rs b/src/sema/unused_variable.rs index b39dc576d..d07fc2c10 100644 --- a/src/sema/unused_variable.rs +++ b/src/sema/unused_variable.rs @@ -281,6 +281,9 @@ fn check_call_args(ns: &mut Namespace, call_args: &CallArgs, symtable: &mut Symt if let Some(flags) = &call_args.flags { used_variable(ns, flags.as_ref(), symtable); } + if let Some(program_id) = &call_args.program_id { + used_variable(ns, program_id.as_ref(), symtable); + } } /// Marks as used variables that appear in an expression with right and left hand side. diff --git a/src/sema/using.rs b/src/sema/using.rs index f6117ac61..231f618ef 100644 --- a/src/sema/using.rs +++ b/src/sema/using.rs @@ -10,6 +10,7 @@ use super::{ }; use crate::sema::expression::function_call::{function_returns, function_type}; use crate::sema::expression::resolve_expression::expression; +use crate::sema::namespace::ResolveTypeContext; use solang_parser::pt::CodeLocation; use solang_parser::pt::{self}; use std::collections::HashSet; @@ -34,7 +35,13 @@ pub(crate) fn using_decl( } let ty = if let Some(expr) = &using.ty { - match ns.resolve_type(file_no, contract_no, false, expr, &mut diagnostics) { + match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + expr, + &mut diagnostics, + ) { Ok(Type::Contract(contract_no)) if ns.contracts[contract_no].is_library() => { ns.diagnostics.push(Diagnostic::error( expr.loc(), diff --git a/src/sema/variables.rs b/src/sema/variables.rs index d75c8bde3..3e66120b3 100644 --- a/src/sema/variables.rs +++ b/src/sema/variables.rs @@ -16,6 +16,7 @@ use super::{ }; use crate::sema::eval::check_term_for_constant_overflow; use crate::sema::expression::resolve_expression::expression; +use crate::sema::namespace::ResolveTypeContext; use crate::sema::Recurse; use solang_parser::{ doccomment::DocComment, @@ -120,7 +121,13 @@ pub fn variable_decl<'a>( let mut diagnostics = Diagnostics::default(); - let ty = match ns.resolve_type(file_no, contract_no, false, &ty, &mut diagnostics) { + let ty = match ns.resolve_type( + file_no, + contract_no, + ResolveTypeContext::None, + &ty, + &mut diagnostics, + ) { Ok(s) => s, Err(()) => { ns.diagnostics.extend(diagnostics); diff --git a/tests/codegen_testcases/import_test.sol b/tests/codegen_testcases/import_test.sol new file mode 100644 index 000000000..850692b8d --- /dev/null +++ b/tests/codegen_testcases/import_test.sol @@ -0,0 +1,6 @@ +@program_id("6qEm4QUJGFvqKNJGjTrAEiFhbVBY4ashpBjDHEFvEUmW") +contract Dog { + function barks(string what) public pure { + print(what); + } +} \ No newline at end of file diff --git a/tests/codegen_testcases/solidity/accounts_for_default_constructor.sol b/tests/codegen_testcases/solidity/accounts_for_default_constructor.sol new file mode 100644 index 000000000..00a0a0ac5 --- /dev/null +++ b/tests/codegen_testcases/solidity/accounts_for_default_constructor.sol @@ -0,0 +1,16 @@ +// RUN: --target solana --emit cfg +contract Foo { + uint b; + function get_b() public returns (uint) { + return b; + } +} + +contract Other { + // BEGIN-CHECK: Other::Other::function::call_foo__address + function call_foo(address id) external { + // The account must be properly indexed so that the call works. + // CHECK: constructor(no: ) salt: value: gas:uint64 0 address:(arg #0) seeds: Foo encoded buffer: %abi_encoded.temp.12 accounts: [1] [ struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 0]) field 0)), true, false } ] + Foo.new{program_id: id}(); + } +} \ No newline at end of file diff --git a/tests/codegen_testcases/solidity/borsh_decoding_simple_types.sol b/tests/codegen_testcases/solidity/borsh_decoding_simple_types.sol index 068aa1c60..cc196c3b0 100644 --- a/tests/codegen_testcases/solidity/borsh_decoding_simple_types.sol +++ b/tests/codegen_testcases/solidity/borsh_decoding_simple_types.sol @@ -1,19 +1,16 @@ // RUN: --target solana --emit cfg -contract Other { - -} contract Testing { // BEGIN-CHECK: Testing::Testing::function::addressContract__bytes - function addressContract(bytes memory buffer) public pure returns (address, Other) { - (address a, Other b) = abi.decode(buffer, (address, Other)); + function addressContract(bytes memory buffer) public pure returns (address, address) { + (address a, address b) = abi.decode(buffer, (address, address)); // CHECK: ty:bytes %buffer = (arg #0) - // CHECK: ty:uint32 %temp.64 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 64 <= %temp.64), block1, block2 + // CHECK: ty:uint32 %temp.60 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 64 <= %temp.60), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:address %temp.65 = (builtin ReadFromBuffer ((arg #0), uint32 0)) - // CHECK: ty:contract Other %temp.66 = (builtin ReadFromBuffer ((arg #0), uint32 32)) - // CHECK: branchcond (unsigned less uint32 64 < %temp.64), block3, block4 + // CHECK: ty:address %temp.61 = (builtin ReadFromBuffer ((arg #0), uint32 0)) + // CHECK: ty:address %temp.62 = (builtin ReadFromBuffer ((arg #0), uint32 32)) + // CHECK: branchcond (unsigned less uint32 64 < %temp.60), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure @@ -21,8 +18,8 @@ contract Testing { // CHECK: assert-failure // CHECK: block4: # buffer_read - // CHECK: ty:address %a = %temp.65 - // CHECK: ty:contract Other %b = %temp.66 + // CHECK: ty:address %a = %temp.61 + // CHECK: ty:address %b = %temp.62 return (a, b); } @@ -33,17 +30,17 @@ contract Testing { abi.decode(buffer, (uint8, uint16, uint32, uint64, uint128, uint256)); // CHECK: ty:bytes %buffer = (arg #0) - // CHECK: ty:uint32 %temp.67 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 63 <= %temp.67), block1, block2 + // CHECK: ty:uint32 %temp.63 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 63 <= %temp.63), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:uint8 %temp.68 = (builtin ReadFromBuffer ((arg #0), uint32 0)) - // CHECK: ty:uint16 %temp.69 = (builtin ReadFromBuffer ((arg #0), uint32 1)) - // CHECK: ty:uint32 %temp.70 = (builtin ReadFromBuffer ((arg #0), uint32 3)) - // CHECK: ty:uint64 %temp.71 = (builtin ReadFromBuffer ((arg #0), uint32 7)) - // CHECK: ty:uint128 %temp.72 = (builtin ReadFromBuffer ((arg #0), uint32 15)) - // CHECK: ty:uint256 %temp.73 = (builtin ReadFromBuffer ((arg #0), uint32 31)) - // CHECK: branchcond (unsigned less uint32 63 < %temp.67), block3, block4 + // CHECK: ty:uint8 %temp.64 = (builtin ReadFromBuffer ((arg #0), uint32 0)) + // CHECK: ty:uint16 %temp.65 = (builtin ReadFromBuffer ((arg #0), uint32 1)) + // CHECK: ty:uint32 %temp.66 = (builtin ReadFromBuffer ((arg #0), uint32 3)) + // CHECK: ty:uint64 %temp.67 = (builtin ReadFromBuffer ((arg #0), uint32 7)) + // CHECK: ty:uint128 %temp.68 = (builtin ReadFromBuffer ((arg #0), uint32 15)) + // CHECK: ty:uint256 %temp.69 = (builtin ReadFromBuffer ((arg #0), uint32 31)) + // CHECK: branchcond (unsigned less uint32 63 < %temp.63), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure @@ -52,12 +49,12 @@ contract Testing { // CHECK: assert-failure // CHECK: block4: # buffer_read - // CHECK: ty:uint8 %a = %temp.68 - // CHECK: ty:uint16 %b = %temp.69 - // CHECK: ty:uint32 %c = %temp.70 - // CHECK: ty:uint64 %d = %temp.71 - // CHECK: ty:uint128 %e = %temp.72 - // CHECK: ty:uint256 %f = %temp.73 + // CHECK: ty:uint8 %a = %temp.64 + // CHECK: ty:uint16 %b = %temp.65 + // CHECK: ty:uint32 %c = %temp.66 + // CHECK: ty:uint64 %d = %temp.67 + // CHECK: ty:uint128 %e = %temp.68 + // CHECK: ty:uint256 %f = %temp.69 return (a, b, c, d, e, f); } @@ -68,17 +65,17 @@ contract Testing { (int8 a, int16 b, int32 c, int64 d, int128 e, int256 f) = abi.decode(buffer, (int8, int16, int32, int64, int128, int256)); - // CHECK: ty:uint32 %temp.74 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 63 <= %temp.74), block1, block2 + // CHECK: ty:uint32 %temp.70 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 63 <= %temp.70), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:int8 %temp.75 = (builtin ReadFromBuffer ((arg #0), uint32 0)) - // CHECK: ty:int16 %temp.76 = (builtin ReadFromBuffer ((arg #0), uint32 1)) - // CHECK: ty:int32 %temp.77 = (builtin ReadFromBuffer ((arg #0), uint32 3)) - // CHECK: ty:int64 %temp.78 = (builtin ReadFromBuffer ((arg #0), uint32 7)) - // CHECK: ty:int128 %temp.79 = (builtin ReadFromBuffer ((arg #0), uint32 15)) - // CHECK: ty:int256 %temp.80 = (builtin ReadFromBuffer ((arg #0), uint32 31)) - // CHECK: branchcond (unsigned less uint32 63 < %temp.74), block3, block4 + // CHECK: ty:int8 %temp.71 = (builtin ReadFromBuffer ((arg #0), uint32 0)) + // CHECK: ty:int16 %temp.72 = (builtin ReadFromBuffer ((arg #0), uint32 1)) + // CHECK: ty:int32 %temp.73 = (builtin ReadFromBuffer ((arg #0), uint32 3)) + // CHECK: ty:int64 %temp.74 = (builtin ReadFromBuffer ((arg #0), uint32 7)) + // CHECK: ty:int128 %temp.75 = (builtin ReadFromBuffer ((arg #0), uint32 15)) + // CHECK: ty:int256 %temp.76 = (builtin ReadFromBuffer ((arg #0), uint32 31)) + // CHECK: branchcond (unsigned less uint32 63 < %temp.70), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure @@ -87,12 +84,12 @@ contract Testing { // CHECK: assert-failure // CHECK: block4: # buffer_read - // CHECK: ty:int8 %a = %temp.75 - // CHECK: ty:int16 %b = %temp.76 - // CHECK: ty:int32 %c = %temp.77 - // CHECK: ty:int64 %d = %temp.78 - // CHECK: ty:int128 %e = %temp.79 - // CHECK: ty:int256 %f = %temp.80 + // CHECK: ty:int8 %a = %temp.71 + // CHECK: ty:int16 %b = %temp.72 + // CHECK: ty:int32 %c = %temp.73 + // CHECK: ty:int64 %d = %temp.74 + // CHECK: ty:int128 %e = %temp.75 + // CHECK: ty:int256 %f = %temp.76 return (a, b, c, d, e, f); } @@ -101,15 +98,15 @@ contract Testing { function fixedBytes(bytes memory buffer) public pure returns (bytes1, bytes5, bytes20, bytes32) { (bytes1 a, bytes5 b, bytes20 c, bytes32 d) = abi.decode(buffer, (bytes1, bytes5, bytes20, bytes32)); - // CHECK: ty:uint32 %temp.81 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 58 <= %temp.81), block1, block2 + // CHECK: ty:uint32 %temp.77 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 58 <= %temp.77), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:bytes1 %temp.82 = (builtin ReadFromBuffer ((arg #0), uint32 0)) - // CHECK: ty:bytes5 %temp.83 = (builtin ReadFromBuffer ((arg #0), uint32 1)) - // CHECK: ty:bytes20 %temp.84 = (builtin ReadFromBuffer ((arg #0), uint32 6)) - // CHECK: ty:bytes32 %temp.85 = (builtin ReadFromBuffer ((arg #0), uint32 26)) - // CHECK: branchcond (unsigned less uint32 58 < %temp.81), block3, block4 + // CHECK: ty:bytes1 %temp.78 = (builtin ReadFromBuffer ((arg #0), uint32 0)) + // CHECK: ty:bytes5 %temp.79 = (builtin ReadFromBuffer ((arg #0), uint32 1)) + // CHECK: ty:bytes20 %temp.80 = (builtin ReadFromBuffer ((arg #0), uint32 6)) + // CHECK: ty:bytes32 %temp.81 = (builtin ReadFromBuffer ((arg #0), uint32 26)) + // CHECK: branchcond (unsigned less uint32 58 < %temp.77), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure @@ -118,10 +115,10 @@ contract Testing { // CHECK: assert-failure // CHECK: block4: # buffer_read - // CHECK: ty:bytes1 %a = %temp.82 - // CHECK: ty:bytes5 %b = %temp.83 - // CHECK: ty:bytes20 %c = %temp.84 - // CHECK: ty:bytes32 %d = %temp.85 + // CHECK: ty:bytes1 %a = %temp.78 + // CHECK: ty:bytes5 %b = %temp.79 + // CHECK: ty:bytes20 %c = %temp.80 + // CHECK: ty:bytes32 %d = %temp.81 return (a, b, c, d); } @@ -131,38 +128,38 @@ contract Testing { (bytes memory a, string memory b) = abi.decode(buffer, (bytes, string)); // CHECK: ty:bytes %buffer = (arg #0) - // CHECK: ty:uint32 %temp.86 = (builtin ArrayLength ((arg #0))) - // CHECK: ty:uint32 %temp.87 = (builtin ReadFromBuffer ((arg #0), uint32 0)) - // CHECK: branchcond (unsigned uint32 4 <= %temp.86), block1, block2 + // CHECK: ty:uint32 %temp.82 = (builtin ArrayLength ((arg #0))) + // CHECK: ty:uint32 %temp.83 = (builtin ReadFromBuffer ((arg #0), uint32 0)) + // CHECK: branchcond (unsigned uint32 4 <= %temp.82), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:uint32 %1.cse_temp = (uint32 0 + (%temp.87 + uint32 4)) - // CHECK: branchcond (unsigned %1.cse_temp <= %temp.86), block3, block4 + // CHECK: ty:uint32 %1.cse_temp = (uint32 0 + (%temp.83 + uint32 4)) + // CHECK: branchcond (unsigned %1.cse_temp <= %temp.82), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure // CHECK: block3: # inbounds - // CHECK: ty:bytes %temp.88 = (alloc bytes len %temp.87) - // CHECK: memcpy src: (advance ptr: %buffer, by: uint32 4), dest: %temp.88, bytes_len: %temp.87 - // CHECK: ty:uint32 %temp.89 = (builtin ReadFromBuffer ((arg #0), (uint32 0 + (%temp.87 + uint32 4)))) + // CHECK: ty:bytes %temp.84 = (alloc bytes len %temp.83) + // CHECK: memcpy src: (advance ptr: %buffer, by: uint32 4), dest: %temp.84, bytes_len: %temp.83 + // CHECK: ty:uint32 %temp.85 = (builtin ReadFromBuffer ((arg #0), (uint32 0 + (%temp.83 + uint32 4)))) // CHECK: ty:uint32 %2.cse_temp = (%1.cse_temp + uint32 4) - // CHECK: branchcond (unsigned %2.cse_temp <= %temp.86), block5, block6 + // CHECK: branchcond (unsigned %2.cse_temp <= %temp.82), block5, block6 // CHECK: block4: # out_of_bounds // CHECK: assert-failure // CHECK: block5: # inbounds - // CHECK: ty:uint32 %3.cse_temp = (%1.cse_temp + (%temp.89 + uint32 4)) - // CHECK: branchcond (unsigned %3.cse_temp <= %temp.86), block7, block8 + // CHECK: ty:uint32 %3.cse_temp = (%1.cse_temp + (%temp.85 + uint32 4)) + // CHECK: branchcond (unsigned %3.cse_temp <= %temp.82), block7, block8 // CHECK: block6: # out_of_bounds // CHECK: assert-failure // CHECK: block7: # inbounds - // CHECK: ty:string %temp.90 = (alloc string len %temp.89) - // CHECK: memcpy src: (advance ptr: %buffer, by: %2.cse_temp), dest: %temp.90, bytes_len: %temp.89 - // CHECK: branchcond (unsigned less %3.cse_temp < %temp.86), block9, block10 + // CHECK: ty:string %temp.86 = (alloc string len %temp.85) + // CHECK: memcpy src: (advance ptr: %buffer, by: %2.cse_temp), dest: %temp.86, bytes_len: %temp.85 + // CHECK: branchcond (unsigned less %3.cse_temp < %temp.82), block9, block10 // CHECK: block8: # out_of_bounds // CHECK: assert-failure @@ -171,8 +168,8 @@ contract Testing { // CHECK: assert-failure // CHECK: block10: # buffer_read - // CHECK: ty:bytes %a = %temp.88 - // CHECK: ty:string %b = %temp.90 + // CHECK: ty:bytes %a = %temp.84 + // CHECK: ty:string %b = %temp.86 return (a, b); } @@ -186,12 +183,12 @@ contract Testing { WeekDays a = abi.decode(buffer, (WeekDays)); // CHECK: ty:bytes %buffer = (arg #0) - // CHECK: ty:uint32 %temp.94 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 1 <= %temp.94), block1, block2 + // CHECK: ty:uint32 %temp.90 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 1 <= %temp.90), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:enum Testing.WeekDays %temp.95 = (builtin ReadFromBuffer ((arg #0), uint32 0)) - // CHECK: branchcond (unsigned less uint32 1 < %temp.94), block3, block4 + // CHECK: ty:enum Testing.WeekDays %temp.91 = (builtin ReadFromBuffer ((arg #0), uint32 0)) + // CHECK: branchcond (unsigned less uint32 1 < %temp.90), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure @@ -200,7 +197,7 @@ contract Testing { // CHECK: assert-failure // CHECK: block4: # buffer_read - // CHECK: ty:enum Testing.WeekDays %a = %temp.95 + // CHECK: ty:enum Testing.WeekDays %a = %temp.91 return a; } @@ -220,17 +217,17 @@ contract Testing { function decodeStruct(bytes memory buffer) public pure returns (noPadStruct memory, PaddedStruct memory) { (noPadStruct memory a, PaddedStruct memory b) = abi.decode(buffer, (noPadStruct, PaddedStruct)); - // CHECK: ty:uint32 %temp.96 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 57 <= %temp.96), block1, block2 + // CHECK: ty:uint32 %temp.92 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 57 <= %temp.92), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:struct Testing.noPadStruct %temp.97 = struct { } - // CHECK: memcpy src: %buffer, dest: %temp.97, bytes_len: uint32 8 - // CHECK: ty:uint128 %temp.98 = (builtin ReadFromBuffer ((arg #0), uint32 8)) - // CHECK: ty:uint8 %temp.99 = (builtin ReadFromBuffer ((arg #0), uint32 24)) - // CHECK: ty:bytes32 %temp.100 = (builtin ReadFromBuffer ((arg #0), uint32 25)) - // CHECK: ty:struct Testing.PaddedStruct %temp.101 = struct { %temp.98, %temp.99, %temp.100 } - // CHECK: branchcond (unsigned less uint32 57 < %temp.96), block3, block4 + // CHECK: ty:struct Testing.noPadStruct %temp.93 = struct { } + // CHECK: memcpy src: %buffer, dest: %temp.93, bytes_len: uint32 8 + // CHECK: ty:uint128 %temp.94 = (builtin ReadFromBuffer ((arg #0), uint32 8)) + // CHECK: ty:uint8 %temp.95 = (builtin ReadFromBuffer ((arg #0), uint32 24)) + // CHECK: ty:bytes32 %temp.96 = (builtin ReadFromBuffer ((arg #0), uint32 25)) + // CHECK: ty:struct Testing.PaddedStruct %temp.97 = struct { %temp.94, %temp.95, %temp.96 } + // CHECK: branchcond (unsigned less uint32 57 < %temp.92), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure @@ -239,8 +236,8 @@ contract Testing { // CHECK: assert-failure // CHECK: block4: # buffer_read - // CHECK: ty:struct Testing.noPadStruct %a = %temp.97 - // CHECK: ty:struct Testing.PaddedStruct %b = %temp.101 + // CHECK: ty:struct Testing.noPadStruct %a = %temp.93 + // CHECK: ty:struct Testing.PaddedStruct %b = %temp.97 return (a, b); } @@ -250,31 +247,31 @@ contract Testing { (uint32[4] memory a, noPadStruct[2] memory b, noPadStruct[] memory c) = abi.decode(buffer, (uint32[4], noPadStruct[2], noPadStruct[])); - // CHECK: ty:uint32 %temp.102 = (builtin ArrayLength ((arg #0))) - // CHECK: branchcond (unsigned uint32 32 <= %temp.102), block1, block2 + // CHECK: ty:uint32 %temp.98 = (builtin ArrayLength ((arg #0))) + // CHECK: branchcond (unsigned uint32 32 <= %temp.98), block1, block2 // CHECK: block1: # inbounds - // CHECK: ty:uint32[4] %temp.103 = [ ] - // CHECK: memcpy src: %buffer, dest: %temp.103, bytes_len: uint32 16 - // CHECK: ty:struct Testing.noPadStruct[2] %temp.104 = [ ] - // CHECK: memcpy src: (advance ptr: %buffer, by: uint32 16), dest: %temp.104, bytes_len: uint32 16 - // CHECK: ty:uint32 %temp.105 = (builtin ReadFromBuffer ((arg #0), uint32 32)) - // CHECK: branchcond (unsigned uint32 36 <= %temp.102), block3, block4 + // CHECK: ty:uint32[4] %temp.99 = [ ] + // CHECK: memcpy src: %buffer, dest: %temp.99, bytes_len: uint32 16 + // CHECK: ty:struct Testing.noPadStruct[2] %temp.100 = [ ] + // CHECK: memcpy src: (advance ptr: %buffer, by: uint32 16), dest: %temp.100, bytes_len: uint32 16 + // CHECK: ty:uint32 %temp.101 = (builtin ReadFromBuffer ((arg #0), uint32 32)) + // CHECK: branchcond (unsigned uint32 36 <= %temp.98), block3, block4 // CHECK: block2: # out_of_bounds // CHECK: assert-failure // CHECK: block3: # inbounds - // CHECK: ty:struct Testing.noPadStruct[] %temp.106 = (alloc struct Testing.noPadStruct[] len %temp.105) - // CHECK: ty:uint32 %1.cse_temp = (%temp.105 * uint32 8) - // CHECK: branchcond (unsigned (uint32 36 + %1.cse_temp) <= %temp.102), block5, block6 + // CHECK: ty:struct Testing.noPadStruct[] %temp.102 = (alloc struct Testing.noPadStruct[] len %temp.101) + // CHECK: ty:uint32 %1.cse_temp = (%temp.101 * uint32 8) + // CHECK: branchcond (unsigned (uint32 36 + %1.cse_temp) <= %temp.98), block5, block6 // CHECK: block4: # out_of_bounds // CHECK: assert-failure // CHECK: block5: # inbounds - // CHECK: memcpy src: (advance ptr: %buffer, by: uint32 36), dest: %temp.106, bytes_len: %1.cse_temp - // CHECK: branchcond (unsigned less (uint32 32 + (%1.cse_temp + uint32 4)) < %temp.102), block7, block8 + // CHECK: memcpy src: (advance ptr: %buffer, by: uint32 36), dest: %temp.102, bytes_len: %1.cse_temp + // CHECK: branchcond (unsigned less (uint32 32 + (%1.cse_temp + uint32 4)) < %temp.98), block7, block8 // CHECK: block6: # out_of_bounds // CHECK: assert-failure @@ -283,9 +280,9 @@ contract Testing { // CHECK: assert-failure // CHECK: block8: # buffer_read - // CHECK: ty:uint32[4] %a = %temp.103 - // CHECK: ty:struct Testing.noPadStruct[2] %b = %temp.104 - // CHECK: ty:struct Testing.noPadStruct[] %c = %temp.106 + // CHECK: ty:uint32[4] %a = %temp.99 + // CHECK: ty:struct Testing.noPadStruct[2] %b = %temp.100 + // CHECK: ty:struct Testing.noPadStruct[] %c = %temp.102 return (a, b, c); } diff --git a/tests/codegen_testcases/solidity/constructor_with_metas.sol b/tests/codegen_testcases/solidity/constructor_with_metas.sol index c3060d08a..cca315cf8 100644 --- a/tests/codegen_testcases/solidity/constructor_with_metas.sol +++ b/tests/codegen_testcases/solidity/constructor_with_metas.sol @@ -3,17 +3,16 @@ import 'solana'; contract creator { - Child public c; // BEGIN-CHECK: creator::creator::function::create_child_with_meta__address_address function create_child_with_meta(address child, address payer) external { AccountMeta[2] metas = [ AccountMeta({pubkey: child, is_signer: false, is_writable: false}), AccountMeta({pubkey: payer, is_signer: true, is_writable: true}) ]; - // CHECK: constructor(no: 4) salt: value: gas:uint64 0 address:address 0xadde28d6c5697771bb24a668136224c7aac8e8ba974c2881484973b2e762fb74 seeds: Child encoded buffer: %abi_encoded.temp.15 accounts: %metas - c = new Child{accounts: metas}(); + // CHECK: external call::regular address:address 0xadde28d6c5697771bb24a668136224c7aac8e8ba974c2881484973b2e762fb74 payload:%abi_encoded.temp.13 value:uint64 0 gas:uint64 0 accounts:%metas seeds: contract|function:(1, 3) flags: + Child.new{accounts: metas}(); - c.say_hello(); + Child.say_hello(); } } diff --git a/tests/codegen_testcases/solidity/import_ext_call.sol b/tests/codegen_testcases/solidity/import_ext_call.sol new file mode 100644 index 000000000..29c9c67a4 --- /dev/null +++ b/tests/codegen_testcases/solidity/import_ext_call.sol @@ -0,0 +1,25 @@ +// RUN: --target solana --emit cfg +import '../import_test.sol' as My; + +@program_id("6qEm4QUJGFvqKNJGjTrAEiFhbVBY4ashpBjDHEFvEUmW") +contract Foo { + // BEGIN-CHECK: Foo::Foo::function::get_b + function get_b(address id) public pure { + // External calls + // CHECK: external call::regular address:(arg #0) payload:%abi_encoded.temp.2 value:uint64 0 gas:uint64 0 accounts:[0] [ ] seeds: contract|function:(2, 2) flags: + My.Dog.barks{program_id: id}("woof"); + // CHECK: external call::regular address:(arg #0) payload:%abi_encoded.temp.4 value:uint64 0 gas:uint64 0 accounts:[0] [ ] seeds: contract|function:(2, 2) flags: + My.Dog.barks{program_id: id}({what: "meow"}); + } +} + +contract Cat is My.Dog { + // BEGIN-CHECK: Cat::Cat::function::try_cat + function try_cat() public pure { + // Internal calls + My.Dog.barks("woof"); + // CHECK: Cat::Dog::function::barks__string (alloc string uint32 4 "woof") + My.Dog.barks({what: "meow"}); + // CHECK: Cat::Dog::function::barks__string (alloc string uint32 4 "meow") + } +} \ No newline at end of file diff --git a/tests/codegen_testcases/solidity/solana_base_versus_external.sol b/tests/codegen_testcases/solidity/solana_base_versus_external.sol new file mode 100644 index 000000000..e0035b924 --- /dev/null +++ b/tests/codegen_testcases/solidity/solana_base_versus_external.sol @@ -0,0 +1,46 @@ +// RUN: --target solana --emit cfg + +import 'solana'; + +@program_id("6qEm4QUJGFvqKNJGjTrAEiFhbVBY4ashpBjDHEFvEUmW") +contract Foo { + uint b; + constructor(uint a) { + b = a; + } + + function get_b(address id) public returns (uint) { + return b; + } + + function get_b2(address id) public returns (uint) { + return b; + } +} + +@program_id("9VLAw4to9KsX9DvyzJUJwfUeQCouX79szENj78sZKiqA") +contract Other is Foo { + uint c; + constructor(uint d) Foo(d) { + c = d; + } + // BEGIN-CHECK: Other::Other::function::call_foo__address + function call_foo(address id) external { + // internal calls + Foo.get_b(id); + // CHECK: call Other::Foo::function::get_b__address (arg #0) + Foo.get_b2({id: id}); + // CHECK: call Other::Foo::function::get_b2__address (arg #0) + } + // BEGIN-CHECK: Other::Other::function::call_foo2__address_address + function call_foo2(address id, address acc) external { + AccountMeta[1] meta = [ + AccountMeta({pubkey: acc, is_writable: false, is_signer: false}) + ]; + // external calls + Foo.get_b{program_id: id, accounts: meta}(id); + // CHECK: external call::regular address:(arg #0) payload:%abi_encoded.temp.35 value:uint64 0 gas:uint64 0 accounts:%meta seeds: contract|function:(0, 3) flags: + Foo.get_b2{program_id: id, accounts: meta}(id); + // CHECK: external call::regular address:(arg #0) payload:%abi_encoded.temp.36 value:uint64 0 gas:uint64 0 accounts:%meta seeds: contract|function:(0, 4) flags: + } +} \ No newline at end of file diff --git a/tests/codegen_testcases/solidity/solana_payer_account.sol b/tests/codegen_testcases/solidity/solana_payer_account.sol index c4e59795c..5a1d647f0 100644 --- a/tests/codegen_testcases/solidity/solana_payer_account.sol +++ b/tests/codegen_testcases/solidity/solana_payer_account.sol @@ -2,15 +2,14 @@ @program_id("SoLDxXQ9GMoa15i4NavZc61XGkas2aom4aNiWT6KUER") contract Builder { - Built other; // BEGIN-CHECK: Builder::Builder::function::build_this function build_this() external { - // CHECK: constructor(no: 4) salt: value: gas:uint64 0 address:address 0x69be884fd55a2306354c305323cc6b7ce91768be33d32a021155ef608806bcb seeds: Built encoded buffer: %abi_encoded.temp.18 accounts: [3] [ struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 2]) field 0)), true, false }, struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 1]) field 0)), true, true }, struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 4]) field 0)), false, false } ] - other = new Built("my_seed"); + // CHECK: external call::regular address:address 0x69be884fd55a2306354c305323cc6b7ce91768be33d32a021155ef608806bcb payload:%abi_encoded.temp.17 value:uint64 0 gas:uint64 0 accounts:[3] [ struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 3]) field 0)), true, false }, struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 2]) field 0)), true, true }, struct { (load (struct (subscript struct AccountInfo[] (builtin Accounts ())[uint32 0]) field 0)), false, false } ] seeds: contract|function:(1, 4) flags: + Built.new("my_seed"); } function call_that() public view { - other.say_this("Hold up! I'm calling!"); + Built.say_this("Hold up! I'm calling!"); } } diff --git a/tests/contract_testcases/polkadot/contracts/program_id.sol b/tests/contract_testcases/polkadot/contracts/program_id.sol new file mode 100644 index 000000000..3592c8a71 --- /dev/null +++ b/tests/contract_testcases/polkadot/contracts/program_id.sol @@ -0,0 +1,14 @@ +contract Foo { + function tryFoo() public {} +} + +contract Bar { + Foo f; + function foobar(address id) public { + f = new Foo(); + f.tryFoo{program_id: id}(); + } +} + +// ---- Expect: diagnostics ---- +// error: 9:18-32: 'program_id' not permitted for external calls or constructors on Polkadot diff --git a/tests/contract_testcases/solana/abstract_interface.sol b/tests/contract_testcases/solana/abstract_interface.sol index 5658c649c..e09de85b4 100644 --- a/tests/contract_testcases/solana/abstract_interface.sol +++ b/tests/contract_testcases/solana/abstract_interface.sol @@ -2,8 +2,8 @@ abstract contract A { function v(int) public virtual; } contract C { - function t(A a) public { - a.v(1); + function t(address id) public { + A.v{program_id: id}(1); } } diff --git a/tests/contract_testcases/solana/accounts/constructor_in_loop.sol b/tests/contract_testcases/solana/accounts/constructor_in_loop.sol index de1c502e0..a162aec51 100644 --- a/tests/contract_testcases/solana/accounts/constructor_in_loop.sol +++ b/tests/contract_testcases/solana/accounts/constructor_in_loop.sol @@ -1,16 +1,14 @@ contract foo { - X a; - Y ay; function contruct() external { - a = new X({varia: 2, varb: 5}); + X.new({varia: 2, varb: 5}); } function test1() external returns (uint) { // This is allowed - uint j = a.vara(1, 2); + uint j = X.vara(1, 2); for (uint i = 0; i < 64; i++) { - j += a.vara(i, j); + j += X.vara(i, j); } return j; } @@ -18,7 +16,7 @@ contract foo { function test2() external returns (uint) { uint j = 3; for (uint i = 0; i < 64; i++) { - a = new X(i, j); + X.new(i, j); } return j; } @@ -29,19 +27,19 @@ contract foo { for (uint i = 0; i < 64; i++) { n += i; } - a = new X(n, j); + X.new(n, j); return j; } function test4(uint v1, string v2) external { - ay = new Y({b: v2, a: v1}); + Y.new({b: v2, a: v1}); } function test5() external returns (uint) { uint j = 3; uint i=0; while (i < 64) { - a = new X(i, j); + X.new(i, j); i++; } return j; @@ -53,7 +51,7 @@ contract foo { while (i < 64) { i++; } - a = new X(i, j); + X.new(i, j); return j; } @@ -61,7 +59,7 @@ contract foo { uint j = 3; uint i=0; do { - a = new X(i, j); + X.new(i, j); i++; } while (i < 64); return j; @@ -73,7 +71,7 @@ contract foo { do { i++; } while (i < 64); - a = new X(i, j); + X.new(i, j); return j; } @@ -85,7 +83,7 @@ contract foo { for(uint i=0; i<80; i++) { n +=i; } - a = new X(j, n); + X.new(j, n); } return j; @@ -116,8 +114,8 @@ contract Y { } // ---- Expect: diagnostics ---- -// error: 21:8-19: the {accounts: ..} call argument is needed since the constructor may be called multiple times -// error: 37:14-35: in order to instantiate contract 'Y', a @program_id is required on contract 'Y' -// error: 44:8-19: the {accounts: ..} call argument is needed since the constructor may be called multiple times -// error: 64:8-19: the {accounts: ..} call argument is needed since the constructor may be called multiple times -// error: 88:17-28: the {accounts: ..} call argument is needed since the constructor may be called multiple times +// error: 19:4-15: the {accounts: ..} call argument is needed since the constructor may be called multiple times +// error: 35:9-30: a contract needs a program id to be called. Either a '@program_id' must be declared above a contract or the {program_id: ...} call argument must be present +// error: 42:4-15: the {accounts: ..} call argument is needed since the constructor may be called multiple times +// error: 62:4-15: the {accounts: ..} call argument is needed since the constructor may be called multiple times +// error: 86:13-24: the {accounts: ..} call argument is needed since the constructor may be called multiple times diff --git a/tests/contract_testcases/solana/accounts/double_calls.sol b/tests/contract_testcases/solana/accounts/double_calls.sol index fb271704e..58716493a 100644 --- a/tests/contract_testcases/solana/accounts/double_calls.sol +++ b/tests/contract_testcases/solana/accounts/double_calls.sol @@ -1,22 +1,21 @@ contract adult { - hatchling hh; function test() external { - hatchling h1 = new hatchling("luna"); - hatchling h2 = new hatchling("sol"); + hatchling.new("luna"); + hatchling.new("sol"); } function create(string id) external { - hh = new hatchling(id); + hatchling.new(id); } function call2() external { - hh.call_me("id"); - hh.call_me("not_id"); + hatchling.call_me("id"); + hatchling.call_me("not_id"); } function create2(string id2) external { - hh = new hatchling(id2); - hh.call_me(id2); + hatchling.new(id2); + hatchling.call_me(id2); } } @@ -40,12 +39,10 @@ contract hatchling { } // ---- Expect: diagnostics ---- -// warning: 4:19-21: local variable 'h1' is unused -// warning: 5:19-21: local variable 'h2' is unused -// error: 5:24-44: contract 'hatchling' is called more than once in this function, so automatic account collection cannot happen. Please, provide the necessary accounts using the {accounts:..} call argument -// note 4:24-45: other call -// warning: 12:5-30: function can be declared 'view' -// error: 14:9-29: contract 'hatchling' is called more than once in this function, so automatic account collection cannot happen. Please, provide the necessary accounts using the {accounts:..} call argument -// note 13:9-25: other call -// error: 19:9-24: contract 'hatchling' is called more than once in this function, so automatic account collection cannot happen. Please, provide the necessary accounts using the {accounts:..} call argument -// note 18:14-32: other call +// error: 4:9-29: contract 'hatchling' is called more than once in this function, so automatic account collection cannot happen. Please, provide the necessary accounts using the {accounts:..} call argument +// note 3:9-30: other call +// warning: 11:5-30: function can be declared 'view' +// error: 13:9-36: contract 'hatchling' is called more than once in this function, so automatic account collection cannot happen. Please, provide the necessary accounts using the {accounts:..} call argument +// note 12:9-32: other call +// error: 18:9-31: contract 'hatchling' is called more than once in this function, so automatic account collection cannot happen. Please, provide the necessary accounts using the {accounts:..} call argument +// note 17:9-27: other call diff --git a/tests/contract_testcases/solana/annotations/account_name_collision.sol b/tests/contract_testcases/solana/annotations/account_name_collision.sol index 1405b0a2e..aeb54964f 100644 --- a/tests/contract_testcases/solana/annotations/account_name_collision.sol +++ b/tests/contract_testcases/solana/annotations/account_name_collision.sol @@ -1,10 +1,9 @@ @program_id("SoLDxXQ9GMoa15i4NavZc61XGkas2aom4aNiWT6KUER") contract Builder { - BeingBuilt other; @payer(payer_account) constructor() { - other = new BeingBuilt("abc"); + BeingBuilt.new("abc"); } } @@ -21,6 +20,5 @@ contract BeingBuilt { } // ---- Expect: diagnostics ---- -// warning: 3:5-21: storage variable 'other' has been assigned, but never read -// error: 5:5-26: account name collision encountered. Calling a function that requires an account whose name is also defined in the current function will create duplicate names in the IDL. Please, rename one of the accounts -// note 15:5-26: other declaration \ No newline at end of file +// error: 4:5-26: account name collision encountered. Calling a function that requires an account whose name is also defined in the current function will create duplicate names in the IDL. Please, rename one of the accounts +// note 14:5-26: other declaration \ No newline at end of file diff --git a/tests/contract_testcases/solana/annotations/constructor_external_function.sol b/tests/contract_testcases/solana/annotations/constructor_external_function.sol index 0f10ac5cd..ded075f49 100644 --- a/tests/contract_testcases/solana/annotations/constructor_external_function.sol +++ b/tests/contract_testcases/solana/annotations/constructor_external_function.sol @@ -6,27 +6,25 @@ contract Foo { } contract Bar { - Foo public foo; - function external_create_foo(address addr) external { // This not is allowed - foo = new Foo{address: addr}(); + Foo.new{address: addr}(); } function create_foo() public { // This is not allowed - foo = new Foo(); + Foo.new(); } function this_is_allowed() external { - foo = new Foo(); + Foo.new(); } function call_foo() public pure { - foo.say_hello(); + Foo.say_hello(); } } // ---- Expect: diagnostics ---- -// error: 13:23-36: 'address' not a valid call parameter -// error: 18:15-24: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external +// error: 11:17-30: 'address' not a valid call parameter +// error: 16:9-18: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external diff --git a/tests/contract_testcases/solana/call/call_args_three_ways.sol b/tests/contract_testcases/solana/call/call_args_three_ways.sol index 8e92be1af..22645e42f 100644 --- a/tests/contract_testcases/solana/call/call_args_three_ways.sol +++ b/tests/contract_testcases/solana/call/call_args_three_ways.sol @@ -1,15 +1,15 @@ contract C { function f() public { // Three different parse tree for callargs with new - D d = (new D{value: 1})(); - D dd = (new D){value: 1}(); - D ddd = new D{value: 1}(); + (D.new{value: 1})(); + (D.new){value: 1}(); + D.new{value: 1}(); } - function g(D d) public { + function g() public { // Three different parse tree for callargs - d.func{value: 1}(); - (d.func){value: 1}(); - (d.func{value: 1})(); + D.func{value: 1}(); + (D.func){value: 1}(); + (D.func{value: 1})(); } } @@ -20,8 +20,8 @@ contract D { } // ---- Expect: diagnostics ---- -// error: 4:9-28: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external -// error: 4:16-24: Solana Cross Program Invocation (CPI) cannot transfer native value. See https://solang.readthedocs.io/en/latest/language/functions.html#value_transfer +// error: 4:3-22: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external +// error: 4:10-18: Solana Cross Program Invocation (CPI) cannot transfer native value. See https://solang.readthedocs.io/en/latest/language/functions.html#value_transfer // error: 10:10-18: Solana Cross Program Invocation (CPI) cannot transfer native value. See https://solang.readthedocs.io/en/latest/language/functions.html#value_transfer // error: 11:12-20: Solana Cross Program Invocation (CPI) cannot transfer native value. See https://solang.readthedocs.io/en/latest/language/functions.html#value_transfer // error: 12:11-19: Solana Cross Program Invocation (CPI) cannot transfer native value. See https://solang.readthedocs.io/en/latest/language/functions.html#value_transfer diff --git a/tests/contract_testcases/solana/constant/not_constant.sol b/tests/contract_testcases/solana/constant/not_constant.sol index 2387df423..2544c6f59 100644 --- a/tests/contract_testcases/solana/constant/not_constant.sol +++ b/tests/contract_testcases/solana/constant/not_constant.sol @@ -11,5 +11,4 @@ } // ---- Expect: diagnostics ---- -// error: 8:26-27: 'C' is a contract -// error: 8:26-36: function calls via contract name are only valid for base contracts +// error: 8:26-36: a contract needs a program id to be called. Either a '@program_id' must be declared above a contract or the {program_id: ...} call argument must be present diff --git a/tests/contract_testcases/solana/contracts/abstract_constructor.sol b/tests/contract_testcases/solana/contracts/abstract_constructor.sol new file mode 100644 index 000000000..6ba82285a --- /dev/null +++ b/tests/contract_testcases/solana/contracts/abstract_constructor.sol @@ -0,0 +1,23 @@ +abstract contract Foo2 { + uint b; + constructor(uint a) { + b = a; + } +} + +contract Other2 { + uint c; + constructor(uint d) { + c = d; + } + function call_foo(address id) external { + Foo2.new{program_id: id}(5); + } + function call_foo2(address id) external { + Foo2.new{program_id: id}({a: 5}); + } +} + +// ---- Expect: diagnostics ---- +// error: 14:9-36: cannot construct 'Foo2' of type 'abstract contract' +// error: 17:9-41: cannot construct 'Foo2' of type 'abstract contract' diff --git a/tests/contract_testcases/solana/contracts/circular_reference.sol b/tests/contract_testcases/solana/contracts/circular_reference.sol new file mode 100644 index 000000000..7810a3343 --- /dev/null +++ b/tests/contract_testcases/solana/contracts/circular_reference.sol @@ -0,0 +1,48 @@ +contract Foo { + uint b; + function get_b(address id) external returns (uint) { + Other.new{program_id: id}(); + return b; + } +} + +contract Other { + function call_foo(address id) external { + Foo.new{program_id: id}(); + } +} + +contract Foo2 { + uint b; + constructor(uint a) { + b = a; + } + + function get_b(address id) external returns (uint) { + Other2.new{program_id: id}(2); + return b; + } + + function get_b2(address id) external returns (uint) { + Other2.new{program_id: id}({d: 2}); + return b; + } +} + +contract Other2 { + uint c; + constructor(uint d) { + c = d; + } + function call_foo(address id) external { + Foo2.new{program_id: id}(5); + } + function call_foo2(address id) external { + Foo2.new{program_id: id}({a: 5}); + } +} + +// ---- Expect: diagnostics ---- +// error: 11:9-34: circular reference creating contract 'Foo' +// error: 38:9-36: circular reference creating contract 'Foo2' +// error: 41:9-41: circular reference creating contract 'Foo2' diff --git a/tests/contract_testcases/solana/contracts/constructor_freestanding.sol b/tests/contract_testcases/solana/contracts/constructor_freestanding.sol new file mode 100644 index 000000000..63a71732d --- /dev/null +++ b/tests/contract_testcases/solana/contracts/constructor_freestanding.sol @@ -0,0 +1,29 @@ +import 'solana'; + +function standalone(address dataAccount) returns (address) { + AccountMeta[1] meta = [ + AccountMeta(dataAccount, false, false) + ]; + + hatchling.new{accounts: meta}("my_id", dataAccount); + return hatchling.root{accounts: meta}(); +} + +@program_id("5afzkvPkrshqu4onwBCsJccb1swrt4JdAjnpzK8N4BzZ") +contract hatchling { + string name; + address private origin; + + constructor(string id, address parent) { + require(id != "", "name must be provided"); + name = id; + origin = parent; + } + + function root() public returns (address) { + return origin; + } +} + +// ---- Expect: diagnostics ---- +// error: 8:5-56: constructors not allowed in free standing functions diff --git a/tests/contract_testcases/solana/contracts/contract_as_variables.sol b/tests/contract_testcases/solana/contracts/contract_as_variables.sol new file mode 100644 index 000000000..79869af1d --- /dev/null +++ b/tests/contract_testcases/solana/contracts/contract_as_variables.sol @@ -0,0 +1,51 @@ +import "solana"; + +contract B { + starter ss; + function declare_contract(address addr) external returns (bool) { + AccountMeta[1] meta = [ + AccountMeta({pubkey: addr, is_signer: true, is_writable: true}) + ]; + starter g = new starter{accounts: meta}(); + return g.get{accounts: meta}(); + } + + function receive_contract(starter g) public { + g.flip(); + } + + function return_contract() external returns (starter) { + starter c = new starter(); + return c; + } +} + + + +@program_id("CU8sqXecq7pxweQnJq6CARonEApGN2DXcv9ukRBRgnRf") +contract starter { + bool private value = true; + + modifier test_modifier() { + print("modifier"); + _; + } + + constructor() { + print("Hello, World!"); + } + + function flip() public test_modifier { + value = !value; + } + + function get() public view returns (bool) { + return value; + } +} + +// ---- Expect: diagnostics ---- +// error: 4:5-12: contracts are not allowed as types on Solana +// error: 9:9-16: contracts are not allowed as types on Solana +// error: 13:31-38: contracts are not allowed as types on Solana +// error: 17:50-57: contracts are not allowed as types on Solana diff --git a/tests/contract_testcases/solana/contracts/contract_call_freestanding.sol b/tests/contract_testcases/solana/contracts/contract_call_freestanding.sol new file mode 100644 index 000000000..adce2c393 --- /dev/null +++ b/tests/contract_testcases/solana/contracts/contract_call_freestanding.sol @@ -0,0 +1,28 @@ +import 'solana'; + +function standalone(address dataAccount) returns (address) { + AccountMeta[1] meta = [ + AccountMeta(dataAccount, false, false) + ]; + return hatchling.root{accounts: meta}(); +} + +@program_id("5afzkvPkrshqu4onwBCsJccb1swrt4JdAjnpzK8N4BzZ") +contract hatchling { + string name; + address private origin; + + constructor(string id, address parent) { + require(id != "", "name must be provided"); + name = id; + origin = parent; + } + + function root() public returns (address) { + return origin; + } +} + +// ---- Expect: diagnostics ---- +// warning: 12:5-16: storage variable 'name' has been assigned, but never read +// warning: 21:5-45: function can be declared 'view' diff --git a/tests/contract_testcases/solana/create_contract/syntax.sol b/tests/contract_testcases/solana/create_contract/syntax.sol index 6686f1088..b875569ae 100644 --- a/tests/contract_testcases/solana/create_contract/syntax.sol +++ b/tests/contract_testcases/solana/create_contract/syntax.sol @@ -1,10 +1,10 @@ contract y { function f() public { - x a = new x{gas: 102}(); + x.new{gas: 102}(); } } contract x {} // ---- Expect: diagnostics ---- -// error: 4:29-37: 'gas' not permitted for external calls or constructors on Solana +// error: 4:23-31: 'gas' not permitted for external calls or constructors on Solana diff --git a/tests/contract_testcases/solana/create_contract/syntax_01.sol b/tests/contract_testcases/solana/create_contract/syntax_01.sol index a42f00e81..982158b7b 100644 --- a/tests/contract_testcases/solana/create_contract/syntax_01.sol +++ b/tests/contract_testcases/solana/create_contract/syntax_01.sol @@ -1,10 +1,10 @@ contract y { function f() public { - x a = new x{salt: 102}(); + x.new{salt: 102}(); } } contract x {} // ---- Expect: diagnostics ---- -// error: 4:29-38: 'salt' not permitted for external calls or constructors on Solana +// error: 4:23-32: 'salt' not permitted for external calls or constructors on Solana diff --git a/tests/contract_testcases/solana/destructure_assign_struct_member_2.sol b/tests/contract_testcases/solana/destructure_assign_struct_member_2.sol index bc0fc6a24..3bf2f8cac 100644 --- a/tests/contract_testcases/solana/destructure_assign_struct_member_2.sol +++ b/tests/contract_testcases/solana/destructure_assign_struct_member_2.sol @@ -19,7 +19,7 @@ contract Contract { // get shares and eth required for each share Struct1[] memory struct_1 = new Struct1[](size); - (struct_1[0].a, struct_1[0].b,) = IUniswapV2Pair(_tokens[0]).getReserves(); + (struct_1[0].a, struct_1[0].b,) = IUniswapV2Pair.getReserves{program_id: _tokens[0]}(); } } diff --git a/tests/contract_testcases/solana/error.sol b/tests/contract_testcases/solana/error.sol index bc14c8d6e..0ac23bd2d 100644 --- a/tests/contract_testcases/solana/error.sol +++ b/tests/contract_testcases/solana/error.sol @@ -2,12 +2,11 @@ contract error { error X(); - function foo(error x) public { + function foo() public { } } // ---- Expect: diagnostics ---- // warning: 3:8-9: error 'X' has never been used -// warning: 5:2-30: function can be declared 'pure' -// warning: 5:21-22: function parameter 'x' is unused +// warning: 5:2-23: function can be declared 'pure' diff --git a/tests/contract_testcases/solana/expressions/contract_compare.sol b/tests/contract_testcases/solana/expressions/contract_compare.sol deleted file mode 100644 index 3d2fd7f84..000000000 --- a/tests/contract_testcases/solana/expressions/contract_compare.sol +++ /dev/null @@ -1,19 +0,0 @@ - -contract c { - function cmp(d left, d right) public returns (bool) { - return left < right; - } - - function cmp(d left, e right) public returns (bool) { - return left > right; - } - -} - -contract d {} -contract e {} - -// ---- Expect: diagnostics ---- -// error: 7:2-53: function 'cmp' overrides function in same contract -// note 3:2-53: previous definition of 'cmp' -// error: 8:10-14: expression of type contract d not allowed diff --git a/tests/contract_testcases/solana/expressions/contract_no_init.sol b/tests/contract_testcases/solana/expressions/contract_no_init.sol index ca162c075..7bb16586a 100644 --- a/tests/contract_testcases/solana/expressions/contract_no_init.sol +++ b/tests/contract_testcases/solana/expressions/contract_no_init.sol @@ -5,14 +5,13 @@ contract testing { function test(int x) public returns (int) { - other o; do { x--; - o = new other(); + other.new(); }while(x > 0); - return o.a(); + return other.a(); } } // ---- Expect: diagnostics ---- -// error: 11:25-36: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external +// error: 10:21-32: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external diff --git a/tests/contract_testcases/solana/expressions/selector_in_free_function_02.sol b/tests/contract_testcases/solana/expressions/selector_in_free_function_02.sol index 9dad5b1b9..19dc5b51c 100644 --- a/tests/contract_testcases/solana/expressions/selector_in_free_function_02.sol +++ b/tests/contract_testcases/solana/expressions/selector_in_free_function_02.sol @@ -4,11 +4,11 @@ } contract foo { - function f(I t) public returns (bytes8) { - return t.X.selector; + function f() public returns (bytes8) { + return I.X.selector; } } // ---- Expect: diagnostics ---- // warning: 3:13-34: function can be declared 'pure' -// warning: 7:13-52: function can be declared 'pure' +// warning: 7:13-49: function can be declared 'pure' diff --git a/tests/contract_testcases/solana/functions/external_functions.sol b/tests/contract_testcases/solana/functions/external_functions.sol index cd1936010..fe3adb0a6 100644 --- a/tests/contract_testcases/solana/functions/external_functions.sol +++ b/tests/contract_testcases/solana/functions/external_functions.sol @@ -1,7 +1,7 @@ -function doThis(bar1 bb) returns (int) { +function doThis(address id) returns (int) { // This is allwoed - return bb.this_is_external(1, 2); + return bar1.this_is_external{program_id: id}(1, 2); } contract bar1 { @@ -46,5 +46,6 @@ contract bar2 is bar1 { // note 19:5-61: declaration of function 'hello' // error: 30:16-35: functions declared external cannot be called via an internal function call // note 19:5-61: declaration of function 'hello' +// error: 35:16-43: a contract needs a program id to be called. Either a '@program_id' must be declared above a contract or the {program_id: ...} call argument must be present // error: 40:16-38: functions declared external cannot be called via an internal function call // note 10:5-72: declaration of function 'this_is_external' \ No newline at end of file diff --git a/tests/contract_testcases/solana/garbage_function_args.sol b/tests/contract_testcases/solana/garbage_function_args.sol index 0f8371157..17bf79ba4 100644 --- a/tests/contract_testcases/solana/garbage_function_args.sol +++ b/tests/contract_testcases/solana/garbage_function_args.sol @@ -1,3 +1,4 @@ +@program_id("A8A3VYtDN69E72gceahcfVjLbf7m3c1u2RDwnbWgfRAk") contract c { function g(address) public { require(rubbish, "rubbish n'existe pas!"); @@ -17,8 +18,8 @@ contract c { } // ---- Expect: diagnostics ---- -// error: 3:11-18: 'rubbish' not found -// error: 6:15-18: 'meh' not found -// error: 9:9-12: 'foo' not found -// error: 12:10-12: 'oo' not found -// error: 15:14-17: 'foo' not found +// error: 4:11-18: 'rubbish' not found +// error: 7:15-18: 'meh' not found +// error: 10:9-12: 'foo' not found +// error: 13:10-12: 'oo' not found +// error: 16:14-17: 'foo' not found diff --git a/tests/contract_testcases/solana/mapping_deletion.sol b/tests/contract_testcases/solana/mapping_deletion.sol index 327df92e1..4d61c9469 100644 --- a/tests/contract_testcases/solana/mapping_deletion.sol +++ b/tests/contract_testcases/solana/mapping_deletion.sol @@ -16,14 +16,14 @@ contract DeleteTest { } mapping(uint => data_struct) example; - mapping(uint => savedTest) example2; + mapping(uint => address) example2; - function addData() public { + function addData(address pid) public { data_struct dt = data_struct({addr1: address(this), addr2: tx.accounts.dataAccount.key}); uint id = 1; example[id] = dt; - savedTest tt = new savedTest(4); - example2[id] = tt; + savedTest.new{program_id: pid}(4); + example2[id] = pid; } function deltest() external { @@ -39,4 +39,4 @@ contract DeleteTest { } // ---- Expect: diagnostics ---- -// error: 25:24-40: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external +// error: 25:9-42: accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external diff --git a/tests/contract_testcases/solana/type_decl_import.sol b/tests/contract_testcases/solana/type_decl_import.sol index 7f1b15d0d..5d57915ac 100644 --- a/tests/contract_testcases/solana/type_decl_import.sol +++ b/tests/contract_testcases/solana/type_decl_import.sol @@ -1,11 +1,11 @@ import "./type_decl.sol" as IMP; contract d { - function f(IMP.x c) public { + function f(address pid) public { IMP.Addr a = IMP.Addr.wrap(payable(this)); IMP.x.Binary b = IMP.x.Binary.wrap(false); - c.f(a, b); + IMP.x.f{program_id: pid}(a, b); } } diff --git a/tests/optimization_testcases/calls/54e619457eea3be2790d0fa66cba06d2e9a36f17.json b/tests/optimization_testcases/calls/54e619457eea3be2790d0fa66cba06d2e9a36f17.json deleted file mode 100644 index 517c0ebd7..000000000 --- a/tests/optimization_testcases/calls/54e619457eea3be2790d0fa66cba06d2e9a36f17.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "constructor": [], - "function": [ - [ - "test", - [] - ] - ] -} diff --git a/tests/optimization_testcases/programs/54e619457eea3be2790d0fa66cba06d2e9a36f17.sol b/tests/optimization_testcases/programs/54e619457eea3be2790d0fa66cba06d2e9a36f17.sol deleted file mode 100644 index f13287bec..000000000 --- a/tests/optimization_testcases/programs/54e619457eea3be2790d0fa66cba06d2e9a36f17.sol +++ /dev/null @@ -1,21 +0,0 @@ -interface I { - function f(int) external; -} - -library L { - function F(I i, bool b, int n) public { - if (b) { - print("Hello"); - } - } -} - -contract C { - using L for I; - - function test() public { - I i = I(address(0)); - - i.F(true, 102); - } -} diff --git a/tests/solana_tests/abi_decode.rs b/tests/solana_tests/abi_decode.rs index 08c1270b8..b647b4b54 100644 --- a/tests/solana_tests/abi_decode.rs +++ b/tests/solana_tests/abi_decode.rs @@ -110,10 +110,10 @@ fn decode_address() { r#" contract Testing { function testAddress(bytes memory buffer) public view { - (address a, Testing b) = abi.decode(buffer, (address, Testing)); + (address a, address b) = abi.decode(buffer, (address, address)); assert(a == address(this)); - assert(b == this); + assert(b == address(this)); } } "#, diff --git a/tests/solana_tests/abi_encode.rs b/tests/solana_tests/abi_encode.rs index a7244efbe..f01e09e95 100644 --- a/tests/solana_tests/abi_encode.rs +++ b/tests/solana_tests/abi_encode.rs @@ -1004,8 +1004,8 @@ contract caller { return b + 3; } - function do_call() view public returns (int64, int32) { - return (this.doThis(5), this.doThat(3)); + function do_call(address pid) view public returns (int64, int32) { + return (this.doThis{program_id: pid}(5), this.doThat{program_id: pid}(3)); } }"#, ); @@ -1018,6 +1018,7 @@ contract caller { let caller_program_id = vm.stack[0].id; let returns = vm .function("do_call") + .arguments(&[BorshToken::Address(caller_program_id)]) .accounts(vec![ ("systemProgram", [0; 32]), ("caller_programId", caller_program_id), diff --git a/tests/solana_tests/accessor.rs b/tests/solana_tests/accessor.rs index 6efcbb39d..85d4a472f 100644 --- a/tests/solana_tests/accessor.rs +++ b/tests/solana_tests/accessor.rs @@ -262,17 +262,17 @@ fn struct_accessor() { m[1023413412] = S({f1: 414243, f2: true, f3: E("niff")}); } - function f() public view { + function f(address pid) public view { AccountMeta[1] meta = [ AccountMeta({pubkey: tx.accounts.dataAccount.key, is_writable: false, is_signer: false}) ]; - (int64 a1, bool b, E memory c) = this.a{accounts: meta}(); + (int64 a1, bool b, E memory c) = this.a{accounts: meta, program_id: pid}(); require(a1 == -63 && !b && c.b4 == "nuff", "a"); - (a1, b, c) = this.s{accounts: meta}(99); + (a1, b, c) = this.s{accounts: meta, program_id: pid}(99); require(a1 == 65535 && b && c.b4 == "naff", "b"); - (a1, b, c) = this.m{accounts: meta}(1023413412); + (a1, b, c) = this.m{accounts: meta, program_id: pid}(1023413412); require(a1 == 414243 && b && c.b4 == "niff", "c"); - c.b4 = this.e{accounts: meta}(); + c.b4 = this.e{accounts: meta, program_id: pid}(); require(a1 == 414243 && b && c.b4 == "cons", "E"); } }"#, @@ -283,10 +283,13 @@ fn struct_accessor() { .accounts(vec![("dataAccount", data_account)]) .call(); + let program_id = vm.stack[0].id; vm.function("f") + .arguments(&[BorshToken::Address(program_id)]) .accounts(vec![ ("dataAccount", data_account), ("systemProgram", [0; 32]), + ("C_programId", program_id), ]) .call(); } diff --git a/tests/solana_tests/call.rs b/tests/solana_tests/call.rs index 90c5a1499..2a2974dbc 100644 --- a/tests/solana_tests/call.rs +++ b/tests/solana_tests/call.rs @@ -4,7 +4,7 @@ use crate::{ build_solidity, create_program_address, AccountMeta, AccountState, BorshToken, Instruction, Pubkey, VirtualMachine, }; -use base58::{FromBase58, ToBase58}; +use base58::FromBase58; use num_bigint::BigInt; use num_traits::One; @@ -17,8 +17,8 @@ fn simple_external_call() { print("bar0 says: " + v); } - function test_other(bar1 x) public { - x.test_bar("cross contract call"); + function test_other(address x) public { + bar1.test_bar{program_id: x}("cross contract call"); } } @@ -86,8 +86,8 @@ fn external_call_with_returns() { let mut vm = build_solidity( r#" contract bar0 { - function test_other(bar1 x) public returns (int64) { - return x.test_bar(7) + 5; + function test_other(address x) public returns (int64) { + return bar1.test_bar{program_id: x}(7) + 5; } } @@ -166,7 +166,7 @@ fn external_raw_call_with_returns() { contract bar0 { bytes8 private constant SELECTOR = bytes8(sha256(bytes('global:test_bar'))); - function test_other(bar1 x) public returns (int64) { + function test_other(address x) public returns (int64) { bytes select = abi.encodeWithSelector(SELECTOR, int64(7)); bytes signature = abi.encodeWithSignature("global:test_bar", int64(7)); require(select == signature, "must be the same"); @@ -293,14 +293,14 @@ fn external_call_with_string_returns() { let mut vm = build_solidity( r#" contract bar0 { - function test_other(bar1 x) public returns (string) { - string y = x.test_bar(7); + function test_other(address x) public returns (string) { + string y = bar1.test_bar{program_id: x}(7); print(y); return y; } - function test_this(bar1 x) public { - address a = x.who_am_i(); + function test_this(address x) public { + address a = bar1.who_am_i{program_id: x}(); assert(a == address(x)); } } @@ -392,7 +392,7 @@ fn encode_call() { contract bar0 { bytes8 private constant SELECTOR = bytes8(sha256(bytes('global:test_bar'))); - function test_other(bar1 x) public returns (int64) { + function test_other(address x) public returns (int64) { bytes select = abi.encodeWithSelector(SELECTOR, int64(7)); bytes signature = abi.encodeCall(bar1.test_bar, 7); require(select == signature, "must be the same"); @@ -439,7 +439,6 @@ fn encode_call() { .accounts(vec![("dataAccount", bar0_account)]) .call(); - std::println!("bar_acc: {}", bar1_account.to_base58()); let res = vm .function("test_other") .arguments(&[BorshToken::Address(bar1_program_id)]) diff --git a/tests/solana_tests/create_contract.rs b/tests/solana_tests/create_contract.rs index 0a6a5dfaf..cf592ce67 100644 --- a/tests/solana_tests/create_contract.rs +++ b/tests/solana_tests/create_contract.rs @@ -11,14 +11,12 @@ fn simple_create_contract_no_seed() { let mut vm = build_solidity( r#" contract bar0 { - function test_other() external returns (bar1) { - bar1 x = new bar1("yo from bar0"); - - return x; + function test_other() external { + bar1.new("yo from bar0"); } - function call_bar1_at_address(bar1 a, string x) public { - a.say_hello(x); + function call_bar1_at_address(string x) public { + bar1.say_hello(x); } } @@ -63,8 +61,7 @@ fn simple_create_contract_no_seed() { }, ); - let bar1 = vm - .function("test_other") + vm.function("test_other") .accounts(vec![ ("bar1_dataAccount", acc), ("bar1_programId", program_id), @@ -76,8 +73,7 @@ fn simple_create_contract_no_seed() { is_writable: true, is_signer: true, }]) - .call() - .unwrap(); + .call(); assert_eq!(vm.logs, "bar1 says: yo from bar0"); @@ -86,7 +82,7 @@ fn simple_create_contract_no_seed() { vm.logs.truncate(0); vm.function("call_bar1_at_address") - .arguments(&[bar1, BorshToken::String(String::from("xywoleh"))]) + .arguments(&[BorshToken::String(String::from("xywoleh"))]) .accounts(vec![ ("bar1_programId", program_id), ("systemProgram", [0; 32]), @@ -101,14 +97,12 @@ fn simple_create_contract() { let mut vm = build_solidity( r#" contract bar0 { - function test_other() external returns (bar1) { - bar1 x = new bar1("yo from bar0"); - - return x; + function test_other() external { + bar1.new("yo from bar0"); } - function call_bar1_at_address(bar1 a, string x) public { - a.say_hello(x); + function call_bar1_at_address(string x) public { + bar1.say_hello(x); } } @@ -142,25 +136,21 @@ fn simple_create_contract() { vm.account_data.insert(payer, AccountState::default()); - let bar1 = vm - .function("test_other") + vm.function("test_other") .accounts(vec![ ("bar1_dataAccount", seed.0), ("bar1_programId", program_id), ("pay", payer), ("systemProgram", [0; 32]), ]) - .call() - .unwrap(); + .call(); assert_eq!(vm.logs, "bar1 says: yo from bar0"); vm.logs.truncate(0); - println!("next test, {bar1:?}"); - vm.function("call_bar1_at_address") - .arguments(&[bar1, BorshToken::String(String::from("xywoleh"))]) + .arguments(&[BorshToken::String(String::from("xywoleh"))]) .accounts(vec![ ("bar1_programId", program_id), ("systemProgram", [0; 32]), @@ -280,14 +270,12 @@ fn missing_contract() { let mut vm = build_solidity( r#" contract bar0 { - function test_other() external returns (bar1) { - bar1 x = new bar1("yo from bar0"); - - return x; + function test_other() external { + bar1.new("yo from bar0"); } - function call_bar1_at_address(bar1 a, string x) public { - a.say_hello(x); + function call_bar1_at_address(string x) public { + bar1.say_hello(x); } } @@ -337,7 +325,7 @@ fn two_contracts() { import 'solana'; contract bar0 { - function test_other(address a, address b, address payer) external returns (bar1) { + function test_other(address a, address b, address payer) external { AccountMeta[2] bar1_metas = [ AccountMeta({pubkey: a, is_writable: true, is_signer: true}), AccountMeta({pubkey: payer, is_writable: true, is_signer: true}) @@ -346,10 +334,8 @@ fn two_contracts() { AccountMeta({pubkey: b, is_writable: true, is_signer: true}), AccountMeta({pubkey: payer, is_writable: true, is_signer: true}) ]; - bar1 x = new bar1{accounts: bar1_metas}("yo from bar0"); - bar1 y = new bar1{accounts: bar2_metas}("hi from bar0"); - - return x; + bar1.new{accounts: bar1_metas}("yo from bar0"); + bar1.new{accounts: bar2_metas}("hi from bar0"); } } @@ -382,14 +368,16 @@ fn two_contracts() { vm.account_data.insert(seed2.0, AccountState::default()); vm.account_data.insert(payer, AccountState::default()); - let _bar1 = vm - .function("test_other") + vm.function("test_other") .arguments(&[ BorshToken::Address(seed1.0), BorshToken::Address(seed2.0), BorshToken::Address(payer), ]) - .accounts(vec![("systemProgram", [0; 32])]) + .accounts(vec![ + ("systemProgram", [0; 32]), + ("bar1_programId", program_id), + ]) .remaining_accounts(&[ AccountMeta { pubkey: Pubkey(seed1.0), @@ -401,11 +389,6 @@ fn two_contracts() { is_signer: true, is_writable: true, }, - AccountMeta { - pubkey: Pubkey(program_id), - is_writable: false, - is_signer: false, - }, AccountMeta { pubkey: Pubkey(payer), is_signer: true, @@ -629,12 +612,10 @@ fn create_child() { let mut vm = build_solidity( r#" contract creator { - Child public c; - function create_child() external { print("Going to create child"); - c = new Child(); - c.say_hello(); + Child.new(); + Child.say_hello(); } } @@ -675,7 +656,6 @@ fn create_child() { .accounts(vec![ ("Child_dataAccount", seed.0), ("Child_programId", child_program_id), - ("dataAccount", data_account), ("payer", payer), ("systemProgram", [0; 32]), ]) @@ -699,7 +679,6 @@ fn create_child_with_meta() { import 'solana'; contract creator { - Child public c; function create_child_with_meta(address child, address payer) public { print("Going to create child"); AccountMeta[2] metas = [ @@ -708,8 +687,8 @@ contract creator { // Passing the system account here crashes the VM, even if I add it to vm.account_data // AccountMeta({pubkey: address"11111111111111111111111111111111", is_writable: false, is_signer: false}) ]; - c = new Child{accounts: metas}(); - c.say_hello(); + Child.new{accounts: metas}(); + Child.say_hello(); } } @@ -750,7 +729,6 @@ contract Child { .arguments(&[BorshToken::Address(seed.0), BorshToken::Address(payer)]) .accounts(vec![ ("Child_programId", child_program_id), - ("dataAccount", data_account), ("systemProgram", [0; 32]), ]) .remaining_accounts(&[ diff --git a/tests/solana_tests/mappings.rs b/tests/solana_tests/mappings.rs index d83c282ef..8f6bf5a19 100644 --- a/tests/solana_tests/mappings.rs +++ b/tests/solana_tests/mappings.rs @@ -292,20 +292,18 @@ fn string_mapping() { fn contract_mapping() { let mut vm = build_solidity( r#" - interface I {} + contract foo { + mapping (address => string) public map; - contract foo { - mapping (I => string) public map; - - function set(I index, string s) public { + function set(address index, string s) public { map[index] = s; } - function get(I index) public returns (string) { + function get(address index) public returns (string) { return map[index]; } - function rm(I index) public { + function rm(address index) public { delete map[index]; } }"#, diff --git a/tests/solana_tests/runtime_errors.rs b/tests/solana_tests/runtime_errors.rs index afd106229..3b21e942c 100644 --- a/tests/solana_tests/runtime_errors.rs +++ b/tests/solana_tests/runtime_errors.rs @@ -10,9 +10,6 @@ fn runtime_errors() { contract RuntimeErrors { bytes b = hex"0000_00fa"; uint256[] arr; - child public c; - child public c2; - constructor() {} function print_test(int8 num) public returns (int8) { @@ -149,7 +146,7 @@ contract calle_contract { .must_fail(); assert_eq!( vm.logs, - "runtime_error: math overflow in test.sol:22:20-29,\n" + "runtime_error: math overflow in test.sol:19:20-29,\n" ); vm.logs.clear(); @@ -163,7 +160,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: sesa require condition failed in test.sol:28:27-33,\n" + "runtime_error: sesa require condition failed in test.sol:25:27-33,\n" ); vm.logs.clear(); @@ -175,7 +172,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: storage array index out of bounds in test.sol:48:19-23,\n" + "runtime_error: storage array index out of bounds in test.sol:45:19-23,\n" ); vm.logs.clear(); @@ -187,7 +184,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: storage index out of bounds in test.sol:41:11-12,\n" + "runtime_error: storage index out of bounds in test.sol:38:11-12,\n" ); vm.logs.clear(); @@ -201,7 +198,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: read integer out of bounds in test.sol:74:18-30,\n" + "runtime_error: read integer out of bounds in test.sol:71:18-30,\n" ); vm.logs.clear(); @@ -215,7 +212,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: truncated type overflows in test.sol:79:37-42,\n" + "runtime_error: truncated type overflows in test.sol:76:37-42,\n" ); vm.logs.clear(); @@ -223,7 +220,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: reached invalid instruction in test.sol:90:13-22,\n" + "runtime_error: reached invalid instruction in test.sol:87:13-22,\n" ); vm.logs.clear(); @@ -235,7 +232,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: pop from empty storage array in test.sol:54:9-12,\n" + "runtime_error: pop from empty storage array in test.sol:51:9-12,\n" ); vm.logs.clear(); @@ -250,7 +247,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: data does not fit into buffer in test.sol:69:18-28,\n" + "runtime_error: data does not fit into buffer in test.sol:66:18-28,\n" ); vm.logs.clear(); @@ -265,7 +262,7 @@ contract calle_contract { println!("{}", vm.logs); assert_eq!( vm.logs, - "runtime_error: assert failure in test.sol:34:16-24,\n" + "runtime_error: assert failure in test.sol:31:16-24,\n" ); vm.logs.clear(); @@ -279,7 +276,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: array index out of bounds in test.sol:85:16-21,\n" + "runtime_error: array index out of bounds in test.sol:82:16-21,\n" ); vm.logs.clear(); @@ -294,7 +291,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: integer too large to write in buffer in test.sol:63:18-31,\n" + "runtime_error: integer too large to write in buffer in test.sol:60:18-31,\n" ); vm.logs.clear(); @@ -309,7 +306,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: bytes cast error in test.sol:98:23-40,\n" + "runtime_error: bytes cast error in test.sol:95:23-40,\n" ); vm.logs.clear(); @@ -318,7 +315,7 @@ contract calle_contract { assert_eq!( vm.logs, - "runtime_error: unspecified revert encountered in test.sol:58:9-17,\n" + "runtime_error: unspecified revert encountered in test.sol:55:9-17,\n" ); vm.logs.clear(); @@ -326,7 +323,7 @@ contract calle_contract { _res = vm.function("revert_with_message").must_fail(); assert_eq!( vm.logs, - "runtime_error: I reverted! revert encountered in test.sol:103:9-30,\n" + "runtime_error: I reverted! revert encountered in test.sol:100:9-30,\n" ); assert!(vm.return_data.is_none()); } diff --git a/tests/solana_tests/using.rs b/tests/solana_tests/using.rs index 7cd651406..cf93da594 100644 --- a/tests/solana_tests/using.rs +++ b/tests/solana_tests/using.rs @@ -1,98 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 +use crate::borsh_encoding::BorshToken; use crate::build_solidity; - -#[test] -fn using_for_contracts() { - let mut runtime = build_solidity( - r#" - interface I { - function f(int) external; - } - - library L { - function F(I i, bool b, int n) public { - if (b) { - print("Hello"); - } - } - } - - contract C { - using L for I; - - function test() public { - I i = I(address(0)); - - i.F(true, 102); - } - }"#, - ); - - let data_account = runtime.initialize_data_account(); - runtime - .function("new") - .accounts(vec![("dataAccount", data_account)]) - .call(); - runtime.function("test").call(); - - assert_eq!(runtime.logs, "Hello"); - - let mut runtime = build_solidity( - r#" - interface I { - function f1(int) external; - function X(int) external; - } - - library L { - function f1_2(I i) external { - i.f1(2); - } - - function X(I i) external { - print("X lib"); - } - } - - contract foo is I { - using L for I; - - function test() public { - I i = I(address(this)); - - i.X(); - i.X(2); - i.f1_2(); - } - - function f1(int x) public { - print("x:{}".format(x)); - } - - function X(int) public { - print("X contract"); - } - }"#, - ); - - let data_account = runtime.initialize_data_account(); - runtime - .function("new") - .accounts(vec![("dataAccount", data_account)]) - .call(); - - let program_id = runtime.stack[0].id; - runtime - .function("test") - .accounts(vec![ - ("I_programId", program_id), - ("systemProgram", [0; 32]), - ]) - .call(); - - assert_eq!(runtime.logs, "X libX contractx:2"); -} +use num_bigint::BigInt; #[test] fn user_defined_oper() { @@ -228,3 +138,120 @@ fn user_defined_oper() { runtime.function("test_arith").call(); runtime.function("test_bit").call(); } + +#[test] +fn using_for_struct() { + let mut vm = build_solidity( + r#" +struct Pet { + string name; + uint8 age; +} + +library Info { + function isCat(Pet memory myPet) public pure returns (bool) { + return myPet.name == "cat"; + } + + function setAge(Pet memory myPet, uint8 age) pure public { + myPet.age = age; + } +} + +contract C { + using Info for Pet; + + function testPet(string memory name, uint8 age) pure public returns (bool) { + Pet memory my_pet = Pet(name, age); + return my_pet.isCat(); + } + + function changeAge(Pet memory myPet) public pure returns (Pet memory) { + myPet.setAge(5); + return myPet; + } + +} + "#, + ); + + let data_account = vm.initialize_data_account(); + + vm.function("new") + .accounts(vec![("dataAccount", data_account)]) + .call(); + + let res = vm + .function("testPet") + .arguments(&[ + BorshToken::String("cat".to_string()), + BorshToken::Uint { + width: 8, + value: BigInt::from(2u8), + }, + ]) + .call() + .unwrap(); + + assert_eq!(res, BorshToken::Bool(true)); + + let res = vm + .function("changeAge") + .arguments(&[BorshToken::Tuple(vec![ + BorshToken::String("cat".to_string()), + BorshToken::Uint { + width: 8, + value: BigInt::from(2u8), + }, + ])]) + .call() + .unwrap(); + + assert_eq!( + res, + BorshToken::Tuple(vec![ + BorshToken::String("cat".to_string()), + BorshToken::Uint { + width: 8, + value: BigInt::from(5u8), + } + ]) + ); +} + +#[test] +fn using_overload() { + let mut vm = build_solidity( + r#" + library MyBytes { + function push(bytes memory b, uint8[] memory a) pure public returns (bool) { + return b[0] == bytes1(a[0]) && b[1] == bytes1(a[1]); + } +} + +contract C { + using MyBytes for bytes; + + function check() public pure returns (bool) { + bytes memory b; + b.push(1); + b.push(2); + uint8[] memory vec = new uint8[](2); + vec[0] = 1; + vec[1] = 2; + return b.push(vec); + } +} + + "#, + ); + + let data_account = vm.initialize_data_account(); + vm.function("new") + .accounts(vec![("dataAccount", data_account)]) + .call(); + + let res = vm.function("check").call().unwrap(); + + assert_eq!(res, BorshToken::Bool(true)); +} From 61aa1ab7a1221a0b5aba9c1cf11f1658e836326e Mon Sep 17 00:00:00 2001 From: Lucas Steuernagel <38472950+LucasSte@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:48:39 -0300 Subject: [PATCH 5/9] Fix mutability check (#1544) Fixes #1493 Signed-off-by: Lucas Steuernagel --- src/sema/mutability.rs | 9 +++------ .../polkadot/functions/global_functions_07.sol | 2 +- .../polkadot/functions/mutability.sol | 2 +- .../polkadot/functions/mutability_02.sol | 2 +- .../solana/functions/mutability.sol | 17 +++++++++++++++++ tests/evm.rs | 2 +- 6 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 tests/contract_testcases/solana/functions/mutability.sol diff --git a/src/sema/mutability.rs b/src/sema/mutability.rs index 78f4ec065..88138877e 100644 --- a/src/sema/mutability.rs +++ b/src/sema/mutability.rs @@ -367,7 +367,7 @@ fn read_expression(expr: &Expression, state: &mut StateCheck) -> bool { left.recurse(state, write_expression); return false; } - Expression::StorageArrayLength { loc, .. } | Expression::StorageLoad { loc, .. } => { + Expression::StorageArrayLength { loc, .. } => { state.data_account |= DataAccountUsage::READ; state.read(loc); return false; @@ -375,12 +375,9 @@ fn read_expression(expr: &Expression, state: &mut StateCheck) -> bool { Expression::Subscript { loc, array_ty, .. } if array_ty.is_contract_storage() => { state.data_account |= DataAccountUsage::READ; state.read(loc); - return false; } - Expression::Variable { ty, .. } => { - if ty.is_contract_storage() { - state.data_account |= DataAccountUsage::READ; - } + Expression::Variable { ty, .. } if ty.is_contract_storage() => { + state.data_account |= DataAccountUsage::READ; } Expression::StorageVariable { loc, .. } => { state.data_account |= DataAccountUsage::READ; diff --git a/tests/contract_testcases/polkadot/functions/global_functions_07.sol b/tests/contract_testcases/polkadot/functions/global_functions_07.sol index 88aa6ab26..3d2eeb769 100644 --- a/tests/contract_testcases/polkadot/functions/global_functions_07.sol +++ b/tests/contract_testcases/polkadot/functions/global_functions_07.sol @@ -4,4 +4,4 @@ // ---- Expect: diagnostics ---- // warning: 2:34-35: declaration of 'x' shadows function // note 2:18-19: previous declaration of function -// error: 2:58-69: function declared 'pure' but this expression reads from state +// error: 2:65-69: function declared 'pure' but this expression reads from state diff --git a/tests/contract_testcases/polkadot/functions/mutability.sol b/tests/contract_testcases/polkadot/functions/mutability.sol index 90b2ce3f2..ff65f3fcc 100644 --- a/tests/contract_testcases/polkadot/functions/mutability.sol +++ b/tests/contract_testcases/polkadot/functions/mutability.sol @@ -6,4 +6,4 @@ contract test { } } // ---- Expect: diagnostics ---- -// error: 5:17-27: function declared 'pure' but this expression reads from state +// error: 5:24-27: function declared 'pure' but this expression reads from state diff --git a/tests/contract_testcases/polkadot/functions/mutability_02.sol b/tests/contract_testcases/polkadot/functions/mutability_02.sol index ef0436ee5..364ed07c4 100644 --- a/tests/contract_testcases/polkadot/functions/mutability_02.sol +++ b/tests/contract_testcases/polkadot/functions/mutability_02.sol @@ -4,4 +4,4 @@ abstract contract test { } } // ---- Expect: diagnostics ---- -// error: 3:17-30: function declared 'pure' but this expression reads from state +// error: 3:24-30: function declared 'pure' but this expression reads from state diff --git a/tests/contract_testcases/solana/functions/mutability.sol b/tests/contract_testcases/solana/functions/mutability.sol new file mode 100644 index 000000000..5bc400a1a --- /dev/null +++ b/tests/contract_testcases/solana/functions/mutability.sol @@ -0,0 +1,17 @@ +contract C { + bool x; + int[4] a; + + function test() public view returns (int) { + return foo()[1]; + } + + function foo() internal returns (int[4] storage) { + x = true; + return a; + } + +} + +// ---- Expect: diagnostics ---- +// error: 6:10-15: function declared 'view' but this expression writes to state \ No newline at end of file diff --git a/tests/evm.rs b/tests/evm.rs index 8f5f30187..21ae43539 100644 --- a/tests/evm.rs +++ b/tests/evm.rs @@ -249,7 +249,7 @@ fn ethereum_solidity_tests() { }) .sum(); - assert_eq!(errors, 1030); + assert_eq!(errors, 1029); } fn set_file_contents(source: &str, path: &Path) -> (FileResolver, Vec) { From 95f4ad0843b342f48260dd037a92c14aced749d9 Mon Sep 17 00:00:00 2001 From: Sean Young Date: Wed, 20 Sep 2023 09:10:04 +0100 Subject: [PATCH 6/9] solc allows @param tag for return values (#1539) @return should be used, warn about this. https://github.com/hyperledger/solang/issues/1533 Signed-off-by: Sean Young --- src/sema/tags.rs | 28 +++++++++++++-- .../polkadot/tags/event_tag_01.sol | 2 +- .../polkadot/tags/functions_01.sol | 2 +- .../polkadot/tags/struct_tag_01.sol | 2 +- tests/contract_testcases/solana/tags.sol | 6 ++-- tests/evm.rs | 2 +- tests/solana_tests/tags.rs | 34 +++++++++++++++++++ 7 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/sema/tags.rs b/src/sema/tags.rs index 8c86f76c9..de3ccdc0b 100644 --- a/src/sema/tags.rs +++ b/src/sema/tags.rs @@ -57,10 +57,34 @@ pub fn resolve_tags( value, }); } + } else if let Some(Some(no)) = + returns.map(|params| params.iter().position(|p| p.name_as_str() == name)) + { + if let Some(other) = res.iter().find(|e| e.tag == "return" && e.no == no) { + // Note: solc does not detect this problem + ns.diagnostics.push(Diagnostic::warning_with_note( + loc, + format!("duplicate tag '@param' for '{name}'"), + other.loc, + format!("previous tag '@param' for '{name}'"), + )); + } else { + ns.diagnostics.push(Diagnostic::warning( + loc, + format!("'@param' used in stead of '@return' for '{name}'"), + )); + + res.push(Tag { + loc, + tag: String::from("return"), + no, + value, + }); + } } else { ns.diagnostics.push(Diagnostic::error( value_loc, - format!("tag '@param' no field '{name}'"), + format!("function parameter named '{name}' not found"), )); } } @@ -128,7 +152,7 @@ pub fn resolve_tags( } else { ns.diagnostics.push(Diagnostic::error( pt::Loc::File(file_no, c.value_offset, c.value_offset + c.value.len()), - format!("tag '@return' no matching return value '{}'", c.value), + format!("function return value named '{name}' not found"), )); } } diff --git a/tests/contract_testcases/polkadot/tags/event_tag_01.sol b/tests/contract_testcases/polkadot/tags/event_tag_01.sol index ef00f0ef9..c7487fa83 100644 --- a/tests/contract_testcases/polkadot/tags/event_tag_01.sol +++ b/tests/contract_testcases/polkadot/tags/event_tag_01.sol @@ -4,4 +4,4 @@ uint32 f ); // ---- Expect: diagnostics ---- -// error: 2:20-21: tag '@param' no field 'g' +// error: 2:20-21: function parameter named 'g' not found diff --git a/tests/contract_testcases/polkadot/tags/functions_01.sol b/tests/contract_testcases/polkadot/tags/functions_01.sol index 117227c94..c0d2d1718 100644 --- a/tests/contract_testcases/polkadot/tags/functions_01.sol +++ b/tests/contract_testcases/polkadot/tags/functions_01.sol @@ -5,4 +5,4 @@ function foo(int f) public {} } // ---- Expect: diagnostics ---- -// error: 4:24-25: tag '@param' no field 'g' +// error: 4:24-25: function parameter named 'g' not found diff --git a/tests/contract_testcases/polkadot/tags/struct_tag_01.sol b/tests/contract_testcases/polkadot/tags/struct_tag_01.sol index 278ae05fa..1726ff396 100644 --- a/tests/contract_testcases/polkadot/tags/struct_tag_01.sol +++ b/tests/contract_testcases/polkadot/tags/struct_tag_01.sol @@ -4,4 +4,4 @@ uint32 f; } // ---- Expect: diagnostics ---- -// error: 2:20-21: tag '@param' no field 'g' +// error: 2:20-21: function parameter named 'g' not found diff --git a/tests/contract_testcases/solana/tags.sol b/tests/contract_testcases/solana/tags.sol index ccc090542..fbefaa96f 100644 --- a/tests/contract_testcases/solana/tags.sol +++ b/tests/contract_testcases/solana/tags.sol @@ -31,6 +31,6 @@ contract C { } // ---- Expect: diagnostics ---- -// error: 21:15-18: tag '@return' no matching return value 'feh' -// error: 22:15-18: tag '@return' no matching return value 'foo' -// error: 28:15-18: tag '@return' no matching return value 'foo' +// error: 21:15-18: function return value named 'feh' not found +// error: 22:15-18: function return value named 'foo' not found +// error: 28:15-18: function return value named 'foo' not found diff --git a/tests/evm.rs b/tests/evm.rs index 21ae43539..c3e114c95 100644 --- a/tests/evm.rs +++ b/tests/evm.rs @@ -249,7 +249,7 @@ fn ethereum_solidity_tests() { }) .sum(); - assert_eq!(errors, 1029); + assert_eq!(errors, 1024); } fn set_file_contents(source: &str, path: &Path) -> (FileResolver, Vec) { diff --git a/tests/solana_tests/tags.rs b/tests/solana_tests/tags.rs index 18d7f33ee..453783f62 100644 --- a/tests/solana_tests/tags.rs +++ b/tests/solana_tests/tags.rs @@ -211,6 +211,40 @@ fn functions() { assert_eq!(func.tags[2].tag, "inheritdoc"); assert_eq!(func.tags[2].value, "b"); assert_eq!(func.tags[2].no, 0); + + let ns = parse_and_resolve( + r#" + contract c is b { + /// @return x sadad + /// @param k is a boolean + /// @custom:smtchecker abstract-function-nondet + function foo(int x) public pure returns (int a, bool k) {} + } + + contract b {}"#, + Target::Solana, + ); + + assert_eq!(ns.diagnostics.len(), 4); + + assert_eq!( + ns.diagnostics.first_error(), + "function return value named 'x' not found" + ); + + assert_eq!( + ns.diagnostics.first_warning().message, + "'@param' used in stead of '@return' for 'k'" + ); + + let func = ns.functions.iter().find(|func| func.name == "foo").unwrap(); + + assert_eq!(func.tags[0].tag, "return"); + assert_eq!(func.tags[0].value, "is a boolean"); + assert_eq!(func.tags[0].no, 1); + assert_eq!(func.tags[1].tag, "custom:smtchecker"); + assert_eq!(func.tags[1].value, "abstract-function-nondet"); + assert_eq!(func.tags[1].no, 0); } #[test] From c2d0fd8d3bedf15cb8e293d6c5b2822377e4d080 Mon Sep 17 00:00:00 2001 From: Lucas Steuernagel <38472950+LucasSte@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:05:02 -0300 Subject: [PATCH 7/9] Fix recursive struct issue (#1522) --- src/sema/types.rs | 54 ++++++++++++++----- .../solana/structs/parse_structs.sol | 29 ++++++++++ 2 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 tests/contract_testcases/solana/structs/parse_structs.sol diff --git a/src/sema/types.rs b/src/sema/types.rs index 6b030a100..bac601f90 100644 --- a/src/sema/types.rs +++ b/src/sema/types.rs @@ -20,9 +20,11 @@ use num_traits::{One, Zero}; use petgraph::algo::{all_simple_paths, tarjan_scc}; use petgraph::stable_graph::IndexType; use petgraph::Directed; +use phf::{phf_set, Set}; use solang_parser::diagnostics::Note; use solang_parser::{doccomment::DocComment, pt, pt::CodeLocation}; use std::collections::HashSet; +use std::ops::MulAssign; use std::{fmt::Write, ops::Mul}; type Graph = petgraph::Graph<(), usize, Directed, usize>; @@ -288,7 +290,8 @@ fn check_infinite_struct_size(graph: &Graph, nodes: Vec, ns: &mut Namespa let mut infinite_edge = false; for edge in graph.edges_connecting(a.into(), b.into()) { match &ns.structs[a].fields[*edge.weight()].ty { - Type::Array(_, dims) if dims.first() != Some(&ArrayLength::Dynamic) => {} + Type::Array(_, dims) if dims.contains(&ArrayLength::Dynamic) => continue, + Type::Array(_, _) => {} Type::Struct(StructType::UserDefined(_)) => {} _ => continue, } @@ -367,6 +370,7 @@ fn find_struct_recursion(ns: &mut Namespace) { check_recursive_struct_field(n.index(), &graph, ns); } } + for n in 0..ns.structs.len() { let mut notes = vec![]; for field in ns.structs[n].fields.iter().filter(|f| f.infinite_size) { @@ -1579,7 +1583,11 @@ impl Type { /// which is not accounted for. pub fn solana_storage_size(&self, ns: &Namespace) -> BigInt { match self { - Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => 4.into(), + Type::Array(_, dims) if dims.contains(&ArrayLength::Dynamic) => { + let size = dynamic_array_size(dims); + // A pointer is four bytes on Solana + size * 4 + } Type::Array(ty, dims) => ty.solana_storage_size(ns).mul( dims.iter() .map(|d| match d { @@ -1707,8 +1715,10 @@ impl Type { Type::Value => BigInt::from(ns.value_length), Type::Uint(n) | Type::Int(n) => BigInt::from(n / 8), Type::Rational => unreachable!(), - Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => { - BigInt::from(4) + Type::Array(_, dims) if dims.contains(&ArrayLength::Dynamic) => { + let size = dynamic_array_size(dims); + // A pointer is four bytes on Solana + size * 4 } Type::Array(ty, dims) => { let pointer_size = BigInt::from(4); @@ -1756,7 +1766,9 @@ impl Type { .filter(|f| !f.infinite_size) .map(|f| f.ty.storage_slots(ns)) .sum(), - Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => 1.into(), + Type::Array(_, dims) if dims.contains(&ArrayLength::Dynamic) => { + dynamic_array_size(dims) + } Type::Array(ty, dims) => { let one = 1.into(); ty.storage_slots(ns) @@ -1787,9 +1799,7 @@ impl Type { Type::Value => BigInt::from(ns.value_length), Type::Uint(n) | Type::Int(n) => BigInt::from(n / 8), Type::Rational => unreachable!(), - Type::Array(_, dims) if dims.first() == Some(&ArrayLength::Dynamic) => { - BigInt::from(4) - } + Type::Array(_, dims) if dims.contains(&ArrayLength::Dynamic) => BigInt::from(4), Type::Array(ty, _) => { if self.is_sparse_solana(ns) { BigInt::from(4) @@ -2174,10 +2184,28 @@ impl Type { /// These names cannot be used on Windows, even with an extension. /// shamelessly stolen from cargo +static WINDOWS_RESERVED: Set<&'static str> = phf_set! { + "con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", + "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", +}; fn is_windows_reserved(name: &str) -> bool { - [ - "con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", - "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", - ] - .contains(&name.to_ascii_lowercase().as_str()) + WINDOWS_RESERVED.contains(name.to_ascii_lowercase().as_str()) +} + +/// This function calculates the size of a dynamic array. +/// The reasoning is the following: +/// An array `uint [2][][3][1]` is a `void * foo[3][1]`-like in C, so its size +/// in storage is 3*1*ptr_size. Each pointer points to a `uint[2]` so whatever comes before the +/// ultimate empty square bracket does not matter. +fn dynamic_array_size(dims: &[ArrayLength]) -> BigInt { + let mut result = BigInt::one(); + for dim in dims.iter().rev() { + match dim { + ArrayLength::Fixed(num) => result.mul_assign(num), + ArrayLength::Dynamic => break, + ArrayLength::AnyFixed => unreachable!("unknown dimension"), + } + } + + result } diff --git a/tests/contract_testcases/solana/structs/parse_structs.sol b/tests/contract_testcases/solana/structs/parse_structs.sol new file mode 100644 index 000000000..53caf131a --- /dev/null +++ b/tests/contract_testcases/solana/structs/parse_structs.sol @@ -0,0 +1,29 @@ +contract hatchling { + struct A { + A [][2][1] b; + } + struct B { + B [2][][1] b; + } + struct C { + C [2][1][] b; + } + + A private n1; + B private n2; + C private n3; + + constructor() {} + + function foo(uint a, uint b) public { + + } +} + +// ---- Expect: diagnostics ---- +// warning: 12:5-17: storage variable 'n1' has never been used +// warning: 13:5-17: storage variable 'n2' has never been used +// warning: 14:5-17: storage variable 'n3' has never been used +// warning: 18:5-40: function can be declared 'pure' +// warning: 18:23-24: function parameter 'a' is unused +// warning: 18:31-32: function parameter 'b' is unused \ No newline at end of file From a74ab4db8a0de0e5020bbcd334be02409d4cdc2a Mon Sep 17 00:00:00 2001 From: Sean Young Date: Thu, 21 Sep 2023 21:05:30 +0100 Subject: [PATCH 8/9] Allow using {func} for type to use library functions (#1528) Fixes https://github.com/hyperledger/solang/issues/1525 Signed-off-by: Sean Young --- src/sema/contracts.rs | 5 +- src/sema/expression/function_call.rs | 7 +- src/sema/namespace.rs | 14 +- src/sema/using.rs | 32 ++++- .../solana/using_functions.sol | 31 +++++ tests/evm.rs | 2 +- tests/polkadot_tests/libraries.rs | 2 +- tests/solana_tests/using.rs | 127 ++++++++++++++++++ 8 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 tests/contract_testcases/solana/using_functions.sol diff --git a/src/sema/contracts.rs b/src/sema/contracts.rs index 61fdb5e0c..d7ae42f63 100644 --- a/src/sema/contracts.rs +++ b/src/sema/contracts.rs @@ -63,8 +63,6 @@ impl ast::Contract { /// Resolve the following contract pub fn resolve(contracts: &[ContractDefinition], file_no: usize, ns: &mut ast::Namespace) { - resolve_using(contracts, file_no, ns); - // we need to resolve declarations first, so we call functions/constructors of // contracts before they are declared let mut delayed: ResolveLater = Default::default(); @@ -73,6 +71,9 @@ pub fn resolve(contracts: &[ContractDefinition], file_no: usize, ns: &mut ast::N resolve_declarations(def, file_no, ns, &mut delayed); } + // using may use functions declared in contracts + resolve_using(contracts, file_no, ns); + // Resolve base contract constructor arguments on contract definition (not constructor definitions) resolve_base_args(contracts, file_no, ns); diff --git a/src/sema/expression/function_call.rs b/src/sema/expression/function_call.rs index 2cf923595..3eea2bae7 100644 --- a/src/sema/expression/function_call.rs +++ b/src/sema/expression/function_call.rs @@ -1324,7 +1324,6 @@ pub(super) fn method_call_pos_args( resolve_to, )? { return Ok(resolved_call); - } else { } if let Some(resolved_call) = try_user_type( @@ -1352,8 +1351,9 @@ pub(super) fn method_call_pos_args( if let Some(mut path) = ns.expr_to_identifier_path(var) { path.identifiers.push(func.clone()); - if let Ok(list) = ns.resolve_free_function_with_namespace( + if let Ok(list) = ns.resolve_function_with_namespace( context.file_no, + None, &path, &mut Diagnostics::default(), ) { @@ -1640,8 +1640,9 @@ pub(super) fn method_call_named_args( if let Some(mut path) = ns.expr_to_identifier_path(var) { path.identifiers.push(func_name.clone()); - if let Ok(list) = ns.resolve_free_function_with_namespace( + if let Ok(list) = ns.resolve_function_with_namespace( context.file_no, + None, &path, &mut Diagnostics::default(), ) { diff --git a/src/sema/namespace.rs b/src/sema/namespace.rs index 9a5f4b5c7..aa9d360ac 100644 --- a/src/sema/namespace.rs +++ b/src/sema/namespace.rs @@ -364,9 +364,10 @@ impl Namespace { } /// Resolve a free function name with namespace - pub(super) fn resolve_free_function_with_namespace( + pub(super) fn resolve_function_with_namespace( &mut self, file_no: usize, + contract_no: Option, name: &pt::IdentifierPath, diagnostics: &mut Diagnostics, ) -> Result, ()> { @@ -376,12 +377,12 @@ impl Namespace { .map(|(id, namespace)| (id, namespace.iter().collect())) .unwrap(); - let s = self.resolve_namespace(namespace, file_no, None, id, diagnostics)?; + let symbol = self.resolve_namespace(namespace, file_no, contract_no, id, diagnostics)?; - if let Some(Symbol::Function(list)) = s { + if let Some(Symbol::Function(list)) = symbol { Ok(list.clone()) } else { - let error = Namespace::wrong_symbol(s, id); + let error = Namespace::wrong_symbol(symbol, id); diagnostics.push(error); @@ -1335,6 +1336,7 @@ impl Namespace { )); return Err(()); }; + namespace.clear(); Some(*n) } Some(Symbol::Function(_)) => { @@ -1390,6 +1392,10 @@ impl Namespace { }; } + if !namespace.is_empty() { + return Ok(None); + } + let mut s = self .variable_symbols .get(&(import_file_no, contract_no, id.name.to_owned())) diff --git a/src/sema/using.rs b/src/sema/using.rs index 231f618ef..c63dd77cd 100644 --- a/src/sema/using.rs +++ b/src/sema/using.rs @@ -94,8 +94,9 @@ pub(crate) fn using_decl( for using_function in functions { let function_name = &using_function.path; - if let Ok(list) = ns.resolve_free_function_with_namespace( + if let Ok(list) = ns.resolve_function_with_namespace( file_no, + contract_no, &using_function.path, &mut diagnostics, ) { @@ -120,6 +121,18 @@ pub(crate) fn using_decl( let func = &ns.functions[func_no]; + if let Some(contract_no) = func.contract_no { + if !ns.contracts[contract_no].is_library() { + diagnostics.push(Diagnostic::error_with_note( + function_name.loc, + format!("'{function_name}' is not a library function"), + func.loc, + format!("definition of {}", using_function.path), + )); + continue; + } + } + if func.params.is_empty() { diagnostics.push(Diagnostic::error_with_note( function_name.loc, @@ -251,7 +264,22 @@ pub(crate) fn using_decl( Some(oper) } else { if let Some(ty) = &ty { - if *ty != func.params[0].ty { + let dummy = Expression::Variable { + loc, + ty: ty.clone(), + var_no: 0, + }; + + if dummy + .cast( + &loc, + &func.params[0].ty, + true, + ns, + &mut Diagnostics::default(), + ) + .is_err() + { diagnostics.push(Diagnostic::error_with_note( function_name.loc, format!("function cannot be used since first argument is '{}' rather than the required '{}'", func.params[0].ty.to_string(ns), ty.to_string(ns)), diff --git a/tests/contract_testcases/solana/using_functions.sol b/tests/contract_testcases/solana/using_functions.sol new file mode 100644 index 000000000..814c48fe5 --- /dev/null +++ b/tests/contract_testcases/solana/using_functions.sol @@ -0,0 +1,31 @@ +contract C { + function foo(int256 a) internal pure returns (int256) { + return a; + } +} + +library L { + function bar(int256 a) internal pure returns (int256) { + return a; + } +} + +library Lib { + function baz(int256 a, bool b) internal pure returns (int256) { + if (b) { + return 1; + } else { + return a; + } + } + using {L.bar, baz} for int256; +} + +library Lib2 { + using {L.foo.bar, C.foo} for int256; +} + +// ---- Expect: diagnostics ---- +// error: 25:15-18: 'foo' not found +// error: 25:20-25: 'C.foo' is not a library function +// note 2:2-55: definition of C.foo diff --git a/tests/evm.rs b/tests/evm.rs index c3e114c95..6027ac301 100644 --- a/tests/evm.rs +++ b/tests/evm.rs @@ -249,7 +249,7 @@ fn ethereum_solidity_tests() { }) .sum(); - assert_eq!(errors, 1024); + assert_eq!(errors, 1018); } fn set_file_contents(source: &str, path: &Path) -> (FileResolver, Vec) { diff --git a/tests/polkadot_tests/libraries.rs b/tests/polkadot_tests/libraries.rs index fabff28f2..282283084 100644 --- a/tests/polkadot_tests/libraries.rs +++ b/tests/polkadot_tests/libraries.rs @@ -74,7 +74,7 @@ fn using() { let mut runtime = build_solidity( r##" contract test { - using ints for uint32; + using {ints.max} for uint32; function foo(uint32 x) public pure returns (uint64) { // x is 32 bit but the max function takes 64 bit uint return x.max(65536); diff --git a/tests/solana_tests/using.rs b/tests/solana_tests/using.rs index cf93da594..00308d73a 100644 --- a/tests/solana_tests/using.rs +++ b/tests/solana_tests/using.rs @@ -255,3 +255,130 @@ contract C { assert_eq!(res, BorshToken::Bool(true)); } + +#[test] +fn using_function_for_struct() { + let mut vm = build_solidity( + r#" +struct Pet { + string name; + uint8 age; +} + +library Info { + function isCat(Pet memory myPet) public pure returns (bool) { + return myPet.name == "cat"; + } + + function setAge(Pet memory myPet, uint8 age) pure public { + myPet.age = age; + } +} + +contract C { + using {Info.isCat, Info.setAge} for Pet; + + function testPet(string memory name, uint8 age) pure public returns (bool) { + Pet memory my_pet = Pet(name, age); + return my_pet.isCat(); + } + + function changeAge(Pet memory myPet) public pure returns (Pet memory) { + myPet.setAge(5); + return myPet; + } + +} + "#, + ); + + let data_account = vm.initialize_data_account(); + + vm.function("new") + .accounts(vec![("dataAccount", data_account)]) + .call(); + + let res = vm + .function("testPet") + .arguments(&[ + BorshToken::String("cat".to_string()), + BorshToken::Uint { + width: 8, + value: BigInt::from(2u8), + }, + ]) + .call() + .unwrap(); + + assert_eq!(res, BorshToken::Bool(true)); + + let res = vm + .function("changeAge") + .arguments(&[BorshToken::Tuple(vec![ + BorshToken::String("cat".to_string()), + BorshToken::Uint { + width: 8, + value: BigInt::from(2u8), + }, + ])]) + .call() + .unwrap(); + + assert_eq!( + res, + BorshToken::Tuple(vec![ + BorshToken::String("cat".to_string()), + BorshToken::Uint { + width: 8, + value: BigInt::from(5u8), + } + ]) + ); +} + +#[test] +fn using_function_overload() { + let mut vm = build_solidity( + r#" + library LibInLib { + function get0(bytes x) public pure returns (bytes1) { + return x[0]; + } + + function get1(bytes x) public pure returns (bytes1) { + return x[1]; + } + } + + library MyBytes { + using {LibInLib.get0, LibInLib.get1} for bytes; + + function push(bytes memory b, uint8[] memory a) pure public returns (bool) { + return b.get0() == a[0] && b.get1()== a[1]; + } + } + + contract C { + using {MyBytes.push} for bytes; + + function check() public pure returns (bool) { + bytes memory b; + b.push(1); + b.push(2); + uint8[] memory vec = new uint8[](2); + vec[0] = 1; + vec[1] = 2; + return b.push(vec); + } + }"#, + ); + + let data_account = vm.initialize_data_account(); + vm.function("new") + .accounts(vec![("dataAccount", data_account)]) + .call(); + + let res = vm.function("check").call().unwrap(); + + assert_eq!(res, BorshToken::Bool(true)); +} From c53d8d998e0578417b281696fa31b8662e3856b1 Mon Sep 17 00:00:00 2001 From: Govardhan G D <117805210+chioni16@users.noreply.github.com> Date: Fri, 22 Sep 2023 02:37:26 +0530 Subject: [PATCH 9/9] Add Rename functionality (#1514) This feature depends on having the right span information for identifiers in the parse tree. Currently, some changes are needed to either add the missing span information in some cases, or improve the already present spans (look for one-off errors). The rest of the "goto-*" functions also depend on the availability of proper spans. But in their case, as no modification is made to the source code, lack of accurate spans is less of an issue. But `rename` does modify the source code and hence having incorrect spans can result in unintentionally overwriting pieces of source code, which is highly undesirable. The changes made as part of this PR provide the necessary code for `rename` to work. Correcting the parse tree is expected to be done as part of future PRs A non-exhaustive list of changes to be made to parse tree: (https://github.com/hyperledger/solang/pull/1411#issuecomment-1648031602) 1. No loc for contract variables type - array_type_dynamic_push.sol L6 2. No loc for constructor base call - abstract_contract_inheritance.sol L14 3. No loc for function modifier arguments - function_modifier_arguments.sol L8 4. No loc for function override base contracts - virtual_functions_override.sol L2 5. Struct literal struct loc and Constructor contract loc not available - inline_assembly.sol L12 6. Internal Function Location wrong when preceded by a contract name. - abi_encode_call.sol L3 7. array with size length function (stored as NumLiteral) - array_type_fixed_length.sol L7 8. struct literals fields point to the struct and not fields, function calls with struct literals => wrong loc - function_arguments.sol L12 9. function calls with contract names : contract defintions absent - - base_contract_function_call.sol L25 10. enum variants vs enum name - struct_type.sol L29 11. using - using.sol L10 --------- Signed-off-by: Govardhan G D --- src/bin/languageserver/mod.rs | 73 ++++++++++++++++++++++--- vscode/src/test/suite/extension.test.ts | 66 +++++++++++++++++++--- vscode/src/testFixture/rename.sol | 12 ++++ 3 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 vscode/src/testFixture/rename.sol diff --git a/src/bin/languageserver/mod.rs b/src/bin/languageserver/mod.rs index 34de2e3e8..024e915f6 100644 --- a/src/bin/languageserver/mod.rs +++ b/src/bin/languageserver/mod.rs @@ -30,10 +30,11 @@ use tower_lsp::{ ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, ImplementationProviderCapability, InitializeParams, InitializeResult, InitializedParams, - Location, MarkedString, MessageType, OneOf, Position, Range, ReferenceParams, + Location, MarkedString, MessageType, OneOf, Position, Range, ReferenceParams, RenameParams, ServerCapabilities, SignatureHelpOptions, TextDocumentContentChangeEvent, - TextDocumentSyncCapability, TextDocumentSyncKind, TypeDefinitionProviderCapability, Url, - WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, + TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, + TypeDefinitionProviderCapability, Url, WorkspaceEdit, WorkspaceFoldersServerCapabilities, + WorkspaceServerCapabilities, }, Client, LanguageServer, LspService, Server, }; @@ -1651,7 +1652,10 @@ impl<'a> Builder<'a> { ReferenceEntry { start: file .get_offset(range.start.line as usize, range.start.character as usize), - stop: file.get_offset(range.end.line as usize, range.end.character as usize), + // 1 is added to account for the fact that `Lapper` expects half open ranges of the type: [`start`, `stop`) + // i.e, `start` included but `stop` excluded. + stop: file.get_offset(range.end.line as usize, range.end.character as usize) + + 1, val: di.clone(), }, )); @@ -1776,6 +1780,7 @@ impl LanguageServer for SolangServer { type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), implementation_provider: Some(ImplementationProviderCapability::Simple(true)), references_provider: Some(OneOf::Left(true)), + rename_provider: Some(OneOf::Left(true)), ..ServerCapabilities::default() }, }) @@ -1912,8 +1917,7 @@ impl LanguageServer for SolangServer { .find(offset, offset + 1) .min_by(|a, b| (a.stop - a.start).cmp(&(b.stop - b.start))) { - let loc = pt::Loc::File(0, hover.start, hover.stop); - let range = loc_to_range(&loc, &cache.file); + let range = get_range_exclusive(hover.start, hover.stop, &cache.file); return Ok(Some(Hover { contents: HoverContents::Scalar(MarkedString::from_markdown( @@ -2109,7 +2113,7 @@ impl LanguageServer for SolangServer { .filter(|r| r.val == reference) .map(move |r| Location { uri: uri.clone(), - range: get_range(r.start, r.stop, &cache.file), + range: get_range_exclusive(r.start, r.stop, &cache.file), }) }) .collect(); @@ -2133,6 +2137,55 @@ impl LanguageServer for SolangServer { Ok(locations) } + + /// Called when "Rename Symbol" is called by the user on the client side. + /// + /// Expected to return a list of changes to be made in user code so that every occurrence of the code object is renamed. + /// + /// ### Arguments + /// * `RenameParams` + /// * provides the source code location (filename, line number, column number) of the code object for which the request was made. + /// * the new symbol that the code object should go by. + /// + /// ### Edge cases + /// * Returns `Err` when an invalid file path is received. + /// * Returns `Ok(None)` when the definition of code object is not found in user code. + async fn rename(&self, params: RenameParams) -> Result> { + // fetch the `DefinitionIndex` of the code object in question + let def_params: GotoDefinitionParams = GotoDefinitionParams { + text_document_position_params: params.text_document_position, + work_done_progress_params: params.work_done_progress_params, + partial_result_params: Default::default(), + }; + let Some(reference) = self.get_reference_from_params(def_params).await? else { + return Ok(None); + }; + + // the new name of the code object + let new_text = params.new_name; + + // create `TextEdit` instances that represent the changes to be made for every occurrence of the old symbol + // these `TextEdit` objects are then grouped into separate list per source file to which they bolong + let caches = &self.files.lock().await.caches; + let ws = caches + .iter() + .map(|(p, cache)| { + let uri = Url::from_file_path(p).unwrap(); + let text_edits: Vec<_> = cache + .references + .iter() + .filter(|r| r.val == reference) + .map(|r| TextEdit { + range: get_range_exclusive(r.start, r.stop, &cache.file), + new_text: new_text.clone(), + }) + .collect(); + (uri, text_edits) + }) + .collect::>(); + + Ok(Some(WorkspaceEdit::new(ws))) + } } /// Calculate the line and column from the Loc offset received from the parser @@ -2149,6 +2202,12 @@ fn get_range(start: usize, end: usize, file: &ast::File) -> Range { Range::new(start, end) } +// Get `Range` when the parameters passed represent a half open range of type [start, stop) +// Used when `Range` is to be extracted from `Interval` from the `rust_lapper` crate. +fn get_range_exclusive(start: usize, end: usize, file: &ast::File) -> Range { + get_range(start, end - 1, file) +} + fn get_type_definition(ty: &Type) -> Option { match ty { Type::Enum(id) => Some(DefinitionType::Enum(*id)), diff --git a/vscode/src/test/suite/extension.test.ts b/vscode/src/test/suite/extension.test.ts index 1aee1d117..265740cbe 100644 --- a/vscode/src/test/suite/extension.test.ts +++ b/vscode/src/test/suite/extension.test.ts @@ -97,6 +97,12 @@ suite('Extension Test Suite', function () { await testrefs(refsdoc1); }); + // Tests for rename + this.timeout(20000); + const renamedoc1 = getDocUri('rename.sol'); + test('Testing for Rename', async () => { + await testrename(renamedoc1); + }); }); function toRange(lineno1: number, charno1: number, lineno2: number, charno2: number) { @@ -290,25 +296,25 @@ async function testrefs(docUri: vscode.Uri) { assert.strictEqual(loc01.range.start.line, 30); assert.strictEqual(loc01.range.start.character, 16); assert.strictEqual(loc01.range.end.line, 30); - assert.strictEqual(loc01.range.end.character, 22); + assert.strictEqual(loc01.range.end.character, 21); assert.strictEqual(loc01.uri.path, docUri.path); const loc02 = actualref0[2] as vscode.Location; assert.strictEqual(loc02.range.start.line, 33); assert.strictEqual(loc02.range.start.character, 16); assert.strictEqual(loc02.range.end.line, 33); - assert.strictEqual(loc02.range.end.character, 22); + assert.strictEqual(loc02.range.end.character, 21); assert.strictEqual(loc02.uri.path, docUri.path); const loc03 = actualref0[3] as vscode.Location; assert.strictEqual(loc03.range.start.line, 36); assert.strictEqual(loc03.range.start.character, 16); assert.strictEqual(loc03.range.end.line, 36); - assert.strictEqual(loc03.range.end.character, 22); + assert.strictEqual(loc03.range.end.character, 21); assert.strictEqual(loc03.uri.path, docUri.path); const loc04 = actualref0[4] as vscode.Location; assert.strictEqual(loc04.range.start.line, 39); assert.strictEqual(loc04.range.start.character, 16); assert.strictEqual(loc04.range.end.line, 39); - assert.strictEqual(loc04.range.end.character, 22); + assert.strictEqual(loc04.range.end.character, 21); assert.strictEqual(loc04.uri.path, docUri.path); const pos1 = new vscode.Position(28, 12); @@ -328,34 +334,76 @@ async function testrefs(docUri: vscode.Uri) { assert.strictEqual(loc11.range.start.line, 28); assert.strictEqual(loc11.range.start.character, 12); assert.strictEqual(loc11.range.end.line, 28); - assert.strictEqual(loc11.range.end.character, 14); + assert.strictEqual(loc11.range.end.character, 13); assert.strictEqual(loc11.uri.path, docUri.path); const loc12 = actualref1[2] as vscode.Location; assert.strictEqual(loc12.range.start.line, 29); assert.strictEqual(loc12.range.start.character, 16); assert.strictEqual(loc12.range.end.line, 29); - assert.strictEqual(loc12.range.end.character, 18); + assert.strictEqual(loc12.range.end.character, 17); assert.strictEqual(loc12.uri.path, docUri.path); const loc13 = actualref1[3] as vscode.Location; assert.strictEqual(loc13.range.start.line, 32); assert.strictEqual(loc13.range.start.character, 16); assert.strictEqual(loc13.range.end.line, 32); - assert.strictEqual(loc13.range.end.character, 18); + assert.strictEqual(loc13.range.end.character, 17); assert.strictEqual(loc13.uri.path, docUri.path); const loc14 = actualref1[4] as vscode.Location; assert.strictEqual(loc14.range.start.line, 35); assert.strictEqual(loc14.range.start.character, 16); assert.strictEqual(loc14.range.end.line, 35); - assert.strictEqual(loc14.range.end.character, 18); + assert.strictEqual(loc14.range.end.character, 17); assert.strictEqual(loc14.uri.path, docUri.path); const loc15 = actualref1[5] as vscode.Location; assert.strictEqual(loc15.range.start.line, 38); assert.strictEqual(loc15.range.start.character, 16); assert.strictEqual(loc15.range.end.line, 38); - assert.strictEqual(loc15.range.end.character, 18); + assert.strictEqual(loc15.range.end.character, 17); assert.strictEqual(loc15.uri.path, docUri.path); } +async function testrename(docUri: vscode.Uri) { + await activate(docUri); + + const pos0 = new vscode.Position(9, 8); + const newname0 = "changed"; + const rename0 = (await vscode.commands.executeCommand( + 'vscode.executeDocumentRenameProvider', + docUri, + pos0, + newname0, + )) as vscode.WorkspaceEdit; + + assert(rename0.has(docUri)); + + const loc0 = rename0.get(docUri); + + const loc00 = loc0[0] as vscode.TextEdit; + assert.strictEqual(loc00.range.start.line, 0); + assert.strictEqual(loc00.range.start.character, 41); + assert.strictEqual(loc00.range.end.line, 0); + assert.strictEqual(loc00.range.end.character, 42); + assert.strictEqual(loc00.newText, newname0); + const loc01 = loc0[1] as vscode.TextEdit; + assert.strictEqual(loc01.range.start.line, 1); + assert.strictEqual(loc01.range.start.character, 4); + assert.strictEqual(loc01.range.end.line, 1); + assert.strictEqual(loc01.range.end.character, 5); + assert.strictEqual(loc01.newText, newname0); + const loc02 = loc0[2] as vscode.TextEdit; + assert.strictEqual(loc02.range.start.line, 9); + assert.strictEqual(loc02.range.start.character, 8); + assert.strictEqual(loc02.range.end.line, 9); + assert.strictEqual(loc02.range.end.character, 9); + assert.strictEqual(loc02.newText, newname0); + const loc03 = loc0[3] as vscode.TextEdit; + assert.strictEqual(loc03.range.start.line, 9); + assert.strictEqual(loc03.range.start.character, 12); + assert.strictEqual(loc03.range.end.line, 9); + assert.strictEqual(loc03.range.end.character, 13); + assert.strictEqual(loc03.newText, newname0); +} + async function testhover(docUri: vscode.Uri) { await activate(docUri); diff --git a/vscode/src/testFixture/rename.sol b/vscode/src/testFixture/rename.sol new file mode 100644 index 000000000..a41cb50bd --- /dev/null +++ b/vscode/src/testFixture/rename.sol @@ -0,0 +1,12 @@ +function foo(uint256 n) returns (uint256 d) { + d = 2; + for (;;) { + if (n == 0) { + break; + } + + n = n - 1; + + d = d + 2; + } +}