Repository: vyperlang/vyper

Issue:
Assigning the field of a constant/immutable struct leads to compiler panic
### Version Information

* vyper Version (output of `vyper --version`): 0.3.8+commit.825fc699
* OS: OSX
* Python Version (output of `python --version`): 3.8.0

### What's your issue about?

When trying to:

- Re-assign a value to the field of an immutable struct (outside of the constructor)
- Assign a value to the field of a constant struct

The compiler fails to detect that it is not allowed and panic later.

#### Immutable

```Vyper
struct Foo:
    a : uint256

x: immutable(Foo)

@external
def __init__():
    x = Foo({a:1})

@external
def hello() :
    x.a =  2
```
This contract, when compiled with the command `vyper --verbose -f  abi,bytecode file.vy` outputs the following:
```
Error compiling: tests/customs/issue_immutable_struct.vy
Traceback (most recent call last):
  File "/Users/trocher/Documents/thesis/vyper/venv/bin/vyper", line 8, in <module>
    sys.exit(_parse_cli_args())
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 57, in _parse_cli_args
    return _parse_args(sys.argv[1:])
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 154, in _parse_args
    compiled = compile_files(
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 294, in compile_files
    compiler_data = vyper.compile_codes(
  File "/Users/trocher/Documents/thesis/vyper/vyper/evm/opcodes.py", line 226, in _wrapper
    return fn(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/__init__.py", line 141, in compile_codes
    exc_handler(contract_name, exc)
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 189, in exc_handler
    raise exception
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/__init__.py", line 138, in compile_codes
    out[contract_name][output_format] = OUTPUT_FORMATS[output_format](compiler_data)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/output.py", line 245, in build_bytecode_output
    return f"0x{compiler_data.bytecode.hex()}"
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 150, in bytecode
    self.assembly, is_runtime=False, no_bytecode_metadata=self.no_bytecode_metadata
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 141, in assembly
    return generate_assembly(self.ir_nodes, self.no_optimize)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 126, in ir_nodes
    ir, ir_runtime, sigs = self._ir_output
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 122, in _ir_output
    return generate_ir_nodes(self.global_ctx, self.no_optimize)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 279, in generate_ir_nodes
    ir_nodes, ir_runtime, function_sigs = module.generate_ir_for_module(global_ctx)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/module.py", line 209, in generate_ir_for_module
    runtime, internal_functions = _runtime_ir(runtime_functions, all_sigs, global_ctx)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/module.py", line 145, in _runtime_ir
    func_ir = generate_ir_for_function(func_ast, all_sigs, global_ctx, skip_nonpayable_check)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/function_definitions/common.py", line 62, in generate_ir_for_function
    o = generate_ir_for_external_function(code, sig, context, skip_nonpayable_check)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/function_definitions/external_function.py", line 182, in generate_ir_for_external_function
    body += [parse_body(code.body, context, ensure_terminated=True)]
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 424, in parse_body
    ir = parse_stmt(stmt, context)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 398, in parse_stmt
    return Stmt(stmt, context).ir_node
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 40, in __init__
    self.ir_node = fn()
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 93, in parse_Assign
    ir_node = make_setter(target, sub)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/core.py", line 767, in make_setter
    return STORE(left, right)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/core.py", line 545, in STORE
    raise CompilerPanic(f"unreachable {ptr.location}")  # pragma: notest
vyper.exceptions.CompilerPanic: unreachable AddrSpace(name='data', word_scale=32, load_op='dload', store_op=None)

This is an unhandled internal compiler error. Please create an issue on Github to notify the developers.
https://github.com/vyperlang/vyper/issues/new?template=bug.md
```
#### Constant

```Vyper
struct Foo:
    a : uint256

x: constant(Foo)= Foo({a:21})

@external
def hello() :
    x.a = 42
```
This contract, when compiled with the command `vyper --verbose -f  abi,bytecode file.vy` outputs the following:
```
Error compiling: tests/customs/issue_constant_struct.vy
Traceback (most recent call last):
  File "/Users/trocher/Documents/thesis/vyper/venv/bin/vyper", line 8, in <module>
    sys.exit(_parse_cli_args())
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 57, in _parse_cli_args
    return _parse_args(sys.argv[1:])
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 154, in _parse_args
    compiled = compile_files(
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 294, in compile_files
    compiler_data = vyper.compile_codes(
  File "/Users/trocher/Documents/thesis/vyper/vyper/evm/opcodes.py", line 226, in _wrapper
    return fn(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/__init__.py", line 141, in compile_codes
    exc_handler(contract_name, exc)
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 189, in exc_handler
    raise exception
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/__init__.py", line 138, in compile_codes
    out[contract_name][output_format] = OUTPUT_FORMATS[output_format](compiler_data)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/output.py", line 245, in build_bytecode_output
    return f"0x{compiler_data.bytecode.hex()}"
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 150, in bytecode
    self.assembly, is_runtime=False, no_bytecode_metadata=self.no_bytecode_metadata
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 141, in assembly
    return generate_assembly(self.ir_nodes, self.no_optimize)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 126, in ir_nodes
    ir, ir_runtime, sigs = self._ir_output
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 122, in _ir_output
    return generate_ir_nodes(self.global_ctx, self.no_optimize)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 279, in generate_ir_nodes
    ir_nodes, ir_runtime, function_sigs = module.generate_ir_for_module(global_ctx)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/module.py", line 209, in generate_ir_for_module
    runtime, internal_functions = _runtime_ir(runtime_functions, all_sigs, global_ctx)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/module.py", line 145, in _runtime_ir
    func_ir = generate_ir_for_function(func_ast, all_sigs, global_ctx, skip_nonpayable_check)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/function_definitions/common.py", line 62, in generate_ir_for_function
    o = generate_ir_for_external_function(code, sig, context, skip_nonpayable_check)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/function_definitions/external_function.py", line 182, in generate_ir_for_external_function
    body += [parse_body(code.body, context, ensure_terminated=True)]
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 424, in parse_body
    ir = parse_stmt(stmt, context)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 398, in parse_stmt
    return Stmt(stmt, context).ir_node
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 40, in __init__
    self.ir_node = fn()
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 91, in parse_Assign
    target = self._get_target(self.stmt.target)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/stmt.py", line 390, in _get_target
    target = Expr.parse_pointer_expr(target, self.context)
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/expr.py", line 726, in parse_pointer_expr
    o = cls(expr, context).ir_node
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/expr.py", line 75, in __init__
    self.ir_node = fn()
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/expr.py", line 326, in parse_Attribute
    sub = Expr(self.expr.value, self.context).ir_node
  File "/Users/trocher/Documents/thesis/vyper/vyper/codegen/expr.py", line 77, in __init__
    raise TypeCheckFailure(f"{type(node).__name__} node did not produce IR. {self.expr}")
vyper.exceptions.TypeCheckFailure: Name node did not produce IR. vyper.ast.nodes.Name:
     8 def hello() :
---> 9     x.a = 42
-----------^
    10

This is an unhandled internal compiler error. Please create an issue on Github to notify the developers.
https://github.com/vyperlang/vyper/issues/new?template=bug.md
```

### How can it be fixed?

I don't have any fix, but from browsing the code, it seems that during the validation of the bogus assignment, `validate_modification` do not raise an `ImmutableViolation` as the `BaseTypeDefinition` is not marked as `immutable` (resp. `constant`)


Discussion:

Resolving patch:
diff --git a/vyper/semantics/types/user/struct.py b/vyper/semantics/types/user/struct.py
--- a/vyper/semantics/types/user/struct.py
+++ b/vyper/semantics/types/user/struct.py
@@ -80,6 +80,14 @@ def from_annotation(
     ) -> StructDefinition:
         if not isinstance(node, vy_ast.Name):
             raise StructureException("Invalid type assignment", node)
+
+        for _, member_type in self.members.items():
+            # Propagate `is_constant` and `is_immutable` to struct members
+            # `is_public` can be skipped because public getter does not need to be
+            # generated for struct member
+            member_type.is_constant = is_constant
+            member_type.is_immutable = is_immutable
+
         return StructDefinition(
             self._id, self.members, location, is_constant, is_public, is_immutable
         )


Test patch:
diff --git a/tests/parser/syntax/test_constants.py b/tests/parser/syntax/test_constants.py
--- a/tests/parser/syntax/test_constants.py
+++ b/tests/parser/syntax/test_constants.py
@@ -4,6 +4,7 @@
 from vyper import compiler
 from vyper.exceptions import (
     ArgumentException,
+    ImmutableViolation,
     InvalidType,
     NamespaceCollision,
     StateAccessViolation,
@@ -116,6 +117,20 @@ def foo() -> uint256:
     """,
         SyntaxException,
     ),
+    # cannot re-assign constant value
+    (
+        """
+struct Foo:
+    a : uint256
+
+x: constant(Foo) = Foo({a: 1})
+
+@external
+def hello() :
+    x.a =  2
+    """,
+        ImmutableViolation,
+    ),
 ]
 
 
diff --git a/tests/parser/syntax/test_immutables.py b/tests/parser/syntax/test_immutables.py
--- a/tests/parser/syntax/test_immutables.py
+++ b/tests/parser/syntax/test_immutables.py
@@ -156,6 +156,23 @@ def report():
     """,
         "'imm' is not a storage variable, it should not be prepended with self",
     ),
+    (
+        """
+struct Foo:
+    a : uint256
+
+x: immutable(Foo)
+
+@external
+def __init__():
+    x = Foo({a:1})
+
+@external
+def hello() :
+    x.a =  2
+    """,
+        "Immutable value cannot be written to",
+    ),
 ]