Repository: vyperlang/vyper

Issue:
chore[ci]: github artifact actions to be deprecated on jan 30
see https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/

we should update to v4, but there are some breaking changes so the actions need to be tested.
https://github.com/actions/upload-artifact?tab=readme-ov-file#v4---whats-new

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/vyperlang/vyper/pull/4445?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vyperlang) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.09%. Comparing base [(`9db1546`)](https://app.codecov.io/gh/vyperlang/vyper/commit/9db1546272660bde375409147856ddeada1979b9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vyperlang) to head [(`5b93c81`)](https://app.codecov.io/gh/vyperlang/vyper/commit/5b93c8140b77773ec8eaf29ab04591c3eb2a65c8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vyperlang).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4445      +/-   ##
==========================================
- Coverage   91.96%   90.09%   -1.87%     
==========================================
  Files         119      119              
  Lines       16686    16686              
  Branches     2808     2808              
==========================================
- Hits        15346    15034     -312     
- Misses        921     1175     +254     
- Partials      419      477      +58     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/vyperlang/vyper/pull/4445?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vyperlang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=vyperlang).

tserg: How can I test this?

https://github.com/vyperlang/vyper/blob/9db1546272660bde375409147856ddeada1979b9/.github/workflows/build.yml#L101
charles-cooper: > How can I test this?
> 
> https://github.com/vyperlang/vyper/blob/9db1546272660bde375409147856ddeada1979b9/.github/workflows/build.yml#L101-L115

check the build artifacts actions -- ex. https://github.com/vyperlang/vyper/actions/runs/12720146923/job/35461412257?pr=4445
tserg: I have made some changes and verified it is [working](https://github.com/tserg/vyper/actions/runs/12725533955).

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 515be87c..5dd98413 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -49,8 +49,9 @@ jobs:
 
 
       - name: Upload Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
+          name: vyper-${{ runner.os }}
           path: dist/vyper.*
 
   windows-build:
@@ -81,8 +82,9 @@ jobs:
           ./make.cmd freeze
 
       - name: Upload Artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
+          name: vyper-${{ runner.os }}
           path: dist/vyper.*
 
   publish-release-assets:
@@ -92,14 +94,13 @@ jobs:
 
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           path: artifacts/
+          merge-multiple: true
 
       - name: Upload assets
-        # fun - artifacts are downloaded into "artifact/".
-        # TODO: this needs to be tested when upgrading to upload-artifact v4
-        working-directory: artifacts/artifact
+        working-directory: artifacts
         run: |
           set -Eeuxo pipefail
           for BIN_NAME in $(ls)
diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py
index ccc80947..24a0f9ad 100644
--- a/vyper/ast/nodes.py
+++ b/vyper/ast/nodes.py
@@ -331,26 +331,10 @@ class VyperNode:
         slot_fields = [x for i in cls.__mro__ for x in getattr(i, "__slots__", [])]
         return set(i for i in slot_fields if not i.startswith("_"))
 
-    def __hash__(self):
-        values = [getattr(self, i, None) for i in VyperNode._public_slots]
-        return hash(tuple(values))
-
     def __deepcopy__(self, memo):
         # default implementation of deepcopy is a hotspot
         return pickle.loads(pickle.dumps(self))
 
-    def __eq__(self, other):
-        # CMC 2024-03-03 I'm not sure it makes much sense to compare AST
-        # nodes, especially if they come from other modules
-        if not isinstance(other, type(self)):
-            return False
-        if getattr(other, "node_id", None) != getattr(self, "node_id", None):
-            return False
-        for field_name in (i for i in self.get_fields() if i not in VyperNode.__slots__):
-            if getattr(self, field_name, None) != getattr(other, field_name, None):
-                return False
-        return True
-
     def __repr__(self):
         cls = type(self)
         class_repr = f"{cls.__module__}.{cls.__qualname__}"
@@ -889,7 +873,10 @@ class Str(Constant):
 
     def validate(self):
         for c in self.value:
-            if ord(c) >= 256:
+            # in utf-8, bytes in the 128 and up range deviate from latin1 and
+            # can be control bytes, allowing multi-byte characters.
+            # reject them here.
+            if ord(c) >= 128:
                 raise InvalidLiteral(f"'{c}' is not an allowed string literal character", self)
 
 
diff --git a/vyper/codegen/expr.py b/vyper/codegen/expr.py
index 3a09bbe6..d3059e42 100644
--- a/vyper/codegen/expr.py
+++ b/vyper/codegen/expr.py
@@ -61,13 +61,7 @@ from vyper.semantics.types import (
 from vyper.semantics.types.bytestrings import _BytestringT
 from vyper.semantics.types.function import ContractFunctionT, MemberFunctionT
 from vyper.semantics.types.shortcuts import BYTES32_T, UINT256_T
-from vyper.utils import (
-    DECIMAL_DIVISOR,
-    bytes_to_int,
-    is_checksum_encoded,
-    string_to_bytes,
-    vyper_warn,
-)
+from vyper.utils import DECIMAL_DIVISOR, bytes_to_int, is_checksum_encoded, vyper_warn
 
 ENVIRONMENT_VARIABLES = {"block", "msg", "tx", "chain"}
 
@@ -135,24 +129,21 @@ class Expr:
 
     # String literals
     def parse_Str(self):
-        bytez, bytez_length = string_to_bytes(self.expr.value)
-        typ = StringT(bytez_length)
-        return self._make_bytelike(typ, bytez, bytez_length)
+        bytez = self.expr.value.encode("utf-8")
+        return self._make_bytelike(StringT, bytez)
 
     # Byte literals
     def parse_Bytes(self):
-        return self._parse_bytes()
+        return self._make_bytelike(BytesT, self.expr.value)
 
     def parse_HexBytes(self):
-        return self._parse_bytes()
-
-    def _parse_bytes(self):
-        bytez = self.expr.value
-        bytez_length = len(self.expr.value)
-        typ = BytesT(bytez_length)
-        return self._make_bytelike(typ, bytez, bytez_length)
+        # HexBytes already has value as bytes
+        assert isinstance(self.expr.value, bytes)
+        return self._make_bytelike(BytesT, self.expr.value)
 
-    def _make_bytelike(self, btype, bytez, bytez_length):
+    def _make_bytelike(self, typeclass, bytez):
+        bytez_length = len(bytez)
+        btype = typeclass(bytez_length)
         placeholder = self.context.new_internal_variable(btype)
         seq = []
         seq.append(["mstore", placeholder, bytez_length])
diff --git a/vyper/semantics/analysis/imports.py b/vyper/semantics/analysis/imports.py
index 148205f5..7f02bce7 100644
--- a/vyper/semantics/analysis/imports.py
+++ b/vyper/semantics/analysis/imports.py
@@ -59,8 +59,7 @@ class _ImportGraph:
 
     def pop_path(self, expected: vy_ast.Module) -> None:
         popped = self._path.pop()
-        if expected != popped:
-            raise CompilerPanic("unreachable")
+        assert expected is popped, "unreachable"
         self._imports.pop()
 
     @contextlib.contextmanager
@@ -78,7 +77,7 @@ class ImportAnalyzer:
         self.graph = graph
         self._ast_of: dict[int, vy_ast.Module] = {}
 
-        self.seen: set[int] = set()
+        self.seen: set[vy_ast.Module] = set()
 
         self._integrity_sum = None
 
@@ -103,7 +102,7 @@ class ImportAnalyzer:
         return sha256sum("".join(acc))
 
     def _resolve_imports_r(self, module_ast: vy_ast.Module):
-        if id(module_ast) in self.seen:
+        if module_ast in self.seen:
             return
         with self.graph.enter_path(module_ast):
             for node in module_ast.body:
@@ -112,7 +111,8 @@ class ImportAnalyzer:
                         self._handle_Import(node)
                     elif isinstance(node, vy_ast.ImportFrom):
                         self._handle_ImportFrom(node)
-        self.seen.add(id(module_ast))
+
+        self.seen.add(module_ast)
 
     def _handle_Import(self, node: vy_ast.Import):
         # import x.y[name] as y[alias]
diff --git a/vyper/utils.py b/vyper/utils.py
index db506267..999e211a 100644
--- a/vyper/utils.py
+++ b/vyper/utils.py
@@ -11,7 +11,7 @@ import traceback
 import warnings
 from typing import Generic, List, TypeVar, Union
 
-from vyper.exceptions import CompilerPanic, DecimalOverrideException, InvalidLiteral, VyperException
+from vyper.exceptions import CompilerPanic, DecimalOverrideException, VyperException
 
 _T = TypeVar("_T")
 
@@ -310,17 +310,6 @@ def round_towards_zero(d: decimal.Decimal) -> int:
     return int(d.to_integral_exact(decimal.ROUND_DOWN))
 
 
-# Converts string to bytes
-def string_to_bytes(str):
-    bytez = b""
-    for c in str:
-        if ord(c) >= 256:
-            raise InvalidLiteral(f"Cannot insert special character {c} into byte array")
-        bytez += bytes([ord(c)])
-    bytez_length = len(bytez)
-    return bytez, bytez_length
-
-
 # Converts a provided hex string to an integer
 def hex_to_int(inp):
     if inp[:2] == "0x":


Test patch:
diff --git a/tests/ast_utils.py b/tests/ast_utils.py
new file mode 100644
index 00000000..e4be35ad
--- /dev/null
+++ b/tests/ast_utils.py
@@ -0,0 +1,25 @@
+from vyper.ast.nodes import VyperNode
+
+
+def deepequals(node: VyperNode, other: VyperNode):
+    # checks two nodes are recursively equal, ignoring metadata
+    # like line info.
+    if not isinstance(other, type(node)):
+        return False
+
+    if isinstance(node, list):
+        if len(node) != len(other):
+            return False
+        return all(deepequals(a, b) for a, b in zip(node, other))
+
+    if not isinstance(node, VyperNode):
+        return node == other
+
+    if getattr(node, "node_id", None) != getattr(other, "node_id", None):
+        return False
+    for field_name in (i for i in node.get_fields() if i not in VyperNode.__slots__):
+        lhs = getattr(node, field_name, None)
+        rhs = getattr(other, field_name, None)
+        if not deepequals(lhs, rhs):
+            return False
+    return True
diff --git a/tests/functional/syntax/test_string.py b/tests/functional/syntax/test_string.py
index 77cb7eae..1dc354f7 100644
--- a/tests/functional/syntax/test_string.py
+++ b/tests/functional/syntax/test_string.py
@@ -1,7 +1,7 @@
 import pytest
 
 from vyper import compiler
-from vyper.exceptions import StructureException
+from vyper.exceptions import InvalidLiteral, StructureException
 
 valid_list = [
     """
@@ -11,25 +11,13 @@ def foo() -> String[10]:
     """,
     """
 @external
-def foo():
-    x: String[11] = "¡très bien!"
-    """,
-    """
-@external
 def foo() -> bool:
-    x: String[15] = "¡très bien!"
+    x: String[15] = "tres bien!"
     y: String[15] = "test"
     return x != y
     """,
     """
 @external
-def foo() -> bool:
-    x: String[15] = "¡très bien!"
-    y: String[12] = "test"
-    return x != y
-    """,
-    """
-@external
 def test() -> String[100]:
     return "hello world!"
     """,
@@ -46,13 +34,36 @@ invalid_list = [
         """
 @external
 def foo():
+    # invalid type annotation - should be String[N]
     a: String = "abc"
     """,
         StructureException,
-    )
+    ),
+    (
+        """
+@external
+@view
+def compile_hash() -> bytes32:
+    # GH issue #3088 - ord("è") == 232
+    return keccak256("è")
+    """,
+        InvalidLiteral,
+    ),
+    (
+        """
+@external
+def foo() -> bool:
+    # ord("¡") == 161
+    x: String[15] = "¡très bien!"
+    y: String[12] = "test"
+    return x != y
+    """,
+        InvalidLiteral,
+    ),
 ]
 
 
 @pytest.mark.parametrize("bad_code,exc", invalid_list)
-def test_string_fail(assert_compile_failed, get_contract, bad_code, exc):
-    assert_compile_failed(lambda: get_contract(bad_code), exc)
+def test_string_fail(get_contract, bad_code, exc):
+    with pytest.raises(exc):
+        compiler.compile_code(bad_code)
diff --git a/tests/unit/ast/nodes/test_binary.py b/tests/unit/ast/nodes/test_binary.py
index d7662bc4..4bebe0ab 100644
--- a/tests/unit/ast/nodes/test_binary.py
+++ b/tests/unit/ast/nodes/test_binary.py
@@ -1,5 +1,6 @@
 import pytest
 
+from tests.ast_utils import deepequals
 from vyper import ast as vy_ast
 from vyper.exceptions import SyntaxException
 
@@ -18,7 +19,7 @@ def x():
     """
     )
 
-    assert expected == mutated
+    assert deepequals(expected, mutated)
 
 
 def test_binary_length():
diff --git a/tests/unit/ast/nodes/test_compare_nodes.py b/tests/unit/ast/nodes/test_compare_nodes.py
index 164cd3d3..d228e40b 100644
--- a/tests/unit/ast/nodes/test_compare_nodes.py
+++ b/tests/unit/ast/nodes/test_compare_nodes.py
@@ -1,3 +1,4 @@
+from tests.ast_utils import deepequals
 from vyper import ast as vy_ast
 
 
@@ -6,21 +7,21 @@ def test_compare_different_node_clases():
     left = vyper_ast.body[0].target
     right = vyper_ast.body[0].value
 
-    assert left != right
+    assert not deepequals(left, right)
 
 
 def test_compare_different_nodes_same_class():
     vyper_ast = vy_ast.parse_to_ast("[1, 2]")
     left, right = vyper_ast.body[0].value.elements
 
-    assert left != right
+    assert not deepequals(left, right)
 
 
 def test_compare_different_nodes_same_value():
     vyper_ast = vy_ast.parse_to_ast("[1, 1]")
     left, right = vyper_ast.body[0].value.elements
 
-    assert left != right
+    assert not deepequals(left, right)
 
 
 def test_compare_similar_node():
@@ -28,11 +29,11 @@ def test_compare_similar_node():
     left = vy_ast.Int(value=1)
     right = vy_ast.Int(value=1)
 
-    assert left == right
+    assert deepequals(left, right)
 
 
 def test_compare_same_node():
     vyper_ast = vy_ast.parse_to_ast("42")
     node = vyper_ast.body[0].value
 
-    assert node == node
+    assert deepequals(node, node)
diff --git a/tests/unit/ast/test_ast_dict.py b/tests/unit/ast/test_ast_dict.py
index 196b1e24..cfad0795 100644
--- a/tests/unit/ast/test_ast_dict.py
+++ b/tests/unit/ast/test_ast_dict.py
@@ -1,6 +1,7 @@
 import copy
 import json
 
+from tests.ast_utils import deepequals
 from vyper import compiler
 from vyper.ast.nodes import NODE_SRC_ATTRIBUTES
 from vyper.ast.parse import parse_to_ast
@@ -138,7 +139,7 @@ def test() -> int128:
     new_dict = json.loads(out_json)
     new_ast = dict_to_ast(new_dict)
 
-    assert new_ast == original_ast
+    assert deepequals(new_ast, original_ast)
 
 
 # strip source annotations like lineno, we don't care for inspecting
diff --git a/tests/unit/ast/test_parser.py b/tests/unit/ast/test_parser.py
index e0bfcbc2..96df6cf2 100644
--- a/tests/unit/ast/test_parser.py
+++ b/tests/unit/ast/test_parser.py
@@ -1,3 +1,4 @@
+from tests.ast_utils import deepequals
 from vyper.ast.parse import parse_to_ast
 
 
@@ -12,7 +13,7 @@ def test() -> int128:
     ast1 = parse_to_ast(code)
     ast2 = parse_to_ast("\n   \n" + code + "\n\n")
 
-    assert ast1 == ast2
+    assert deepequals(ast1, ast2)
 
 
 def test_ast_unequal():
@@ -32,4 +33,4 @@ def test() -> int128:
     ast1 = parse_to_ast(code1)
     ast2 = parse_to_ast(code2)
 
-    assert ast1 != ast2
+    assert not deepequals(ast1, ast2)