Repository: vyperlang/vyper (Python)

Issue:
VIP: Convert Struct construction to use kwargs
## Simple Summary
Add less overhead and improve type safety of creating a struct

## Motivation
Currently, to create a struct it takes a lot of overhead because of the use of modified dict syntax:
```python
struct MyStruct:
    a: address
    b: bytes32
    c: bool

...

s: MyStruct = MyStruct({a: a, b: b, c: True})
```

This might be a little confusing to newcomers because of how we expected to write strings for dict args (most of the time), plus you don't get the benefit of additional features like struct member name matching or default values for struct members.

## Specification
This proposal would modify the structure syntax to the following:

```python
s: MyStruct = MyStruct(a, b, c=True)
```

The trick is to interpret this as using them like keyword args, where all struct members have a default value (e.g. whatever their "empty" value would be if the struct member didn't appear). We would gain additional type safety here via the `member_name=value` syntax, which can be bypassed *if and only if* the name of the variable matches the name of the struct member (e.g. `a` and `b` here). This would be typical for input arg names to functions, making it very easy to create a new struct from call args.

## Backwards Compatibility
This would be a backwards-incompatible change to the struct creation syntax

## Dependencies
No dependnecies

## References
No references

## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)



Discussion: Note: we should do the same with Event logs
I like the idea of using the kwargs syntax, not so excited about default values and positional args (both increase the risk of misleading code IMO).
> I like the idea of using the kwargs syntax, not so excited about default values and positional args (both increase the risk of misleading code IMO).

To be clear, this proposal would not have positional args in the traditional sense, all args would be kwargs (meaning they can be in any position and still work as kwargs because we will use the actual name to determine positioning and disallow out-of-order positioning). This is more akin to how Rust's syntax for structs are, or more like how `foo(**kwargs)` applies names without using the positioning

Default values are nice for code deduplication. It's a little more opaque, but still acceptable because any good reviewer should know what the "default" value should be

---

Edit: let's talk about this more, your concern is valid, but the implementation of this would be slightly different than how Python works w/ kwargs
Yeah let's talk about it more. My thought right now is I think we should split out the default kwargs for its own VIP. I feel like that's a feature that's easy to add and hard to take out.

Also, defaults for kwargs is a semantic change. If we only discuss the syntax here then we can be sure the VIP deals strictly with a syntactic change to the language, that makes it simpler to reason about, any proposed semantic changes can be dealt with separately.
EDIT: removed my comment because I proposed something @fubuloubu proposed to begin with (because I am a derp)
> Here's an idea though, what if we allow "positional" args but only if the variable name is exactly the same as the parameter name.

Yes! This is exactly what I was trying to say above, very much like how Rust handles structs.


> Yes! This is exactly what I was trying to say above, very much like how Rust handles structs.

Yes I see that, lol! My bad
Meeting: only do kwargs



Fix (patch):
diff --git a/examples/auctions/blind_auction.vy b/examples/auctions/blind_auction.vy
index 966565138f..143206ccb4 100644
--- a/examples/auctions/blind_auction.vy
+++ b/examples/auctions/blind_auction.vy
@@ -69,10 +69,10 @@ def bid(_blindedBid: bytes32):
     assert numBids < MAX_BIDS
 
     # Add bid to mapping of all bids
-    self.bids[msg.sender][numBids] = Bid({
-        blindedBid: _blindedBid,
-        deposit: msg.value
-        })
+    self.bids[msg.sender][numBids] = Bid(
+        blindedBid=_blindedBid,
+        deposit=msg.value
+        )
     self.bidCounts[msg.sender] += 1
 
 
diff --git a/examples/voting/ballot.vy b/examples/voting/ballot.vy
index daaf712e0f..9016ae38c6 100644
--- a/examples/voting/ballot.vy
+++ b/examples/voting/ballot.vy
@@ -57,10 +57,10 @@ def __init__(_proposalNames: bytes32[2]):
     self.chairperson = msg.sender
     self.voterCount = 0
     for i: int128 in range(2):
-        self.proposals[i] = Proposal({
-            name: _proposalNames[i],
-            voteCount: 0
-        })
+        self.proposals[i] = Proposal(
+            name=_proposalNames[i],
+            voteCount=0
+        )
         self.int128Proposals += 1
 
 # Give a `voter` the right to vote on this ballot.
diff --git a/vyper/ast/parse.py b/vyper/ast/parse.py
index a10a840da0..4f162ac18f 100644
--- a/vyper/ast/parse.py
+++ b/vyper/ast/parse.py
@@ -1,5 +1,6 @@
 import ast as python_ast
 import tokenize
+import warnings
 from decimal import Decimal
 from typing import Any, Dict, List, Optional, Union, cast
 
@@ -341,6 +342,29 @@ def visit_Expr(self, node):
 
         return node
 
+    def visit_Call(self, node):
+        # Convert structs declared as `Dict` node for vyper < 0.4.0 to kwargs
+        if len(node.args) == 1 and isinstance(node.args[0], python_ast.Dict):
+            msg = "Instantiating a struct using a dictionary is deprecated "
+            msg += "as of v0.4.0 and will be disallowed in a future release. "
+            msg += "Use kwargs instead e.g. Foo(a=1, b=2)"
+            warnings.warn(msg, stacklevel=2)
+
+            dict_ = node.args[0]
+            kw_list = []
+
+            assert len(dict_.keys) == len(dict_.values)
+            for key, value in zip(dict_.keys, dict_.values):
+                replacement_kw_node = python_ast.keyword(key.id, value)
+                kw_list.append(replacement_kw_node)
+
+            node.args = []
+            node.keywords = kw_list
+
+        self.generic_visit(node)
+
+        return node
+
     def visit_Constant(self, node):
         """
         Handle `Constant` when using Python >=3.8
diff --git a/vyper/codegen/expr.py b/vyper/codegen/expr.py
index 9c7f11dcb3..13e1309e6d 100644
--- a/vyper/codegen/expr.py
+++ b/vyper/codegen/expr.py
@@ -683,9 +683,7 @@ def parse_Call(self):
 
         # Struct constructor
         if is_type_t(func_type, StructT):
-            args = self.expr.args
-            if len(args) == 1 and isinstance(args[0], vy_ast.Dict):
-                return Expr.struct_literals(args[0], self.context, self.expr._metadata["type"])
+            return self.handle_struct_literal()
 
         # Interface constructor. Bar(<address>).
         if is_type_t(func_type, InterfaceT):
@@ -750,17 +748,15 @@ def parse_IfExp(self):
         location = body.location
         return IRnode.from_list(["if", test, body, orelse], typ=typ, location=location)
 
-    @staticmethod
-    def struct_literals(expr, context, typ):
+    def handle_struct_literal(self):
+        expr = self.expr
+        typ = expr._metadata["type"]
         member_subs = {}
-        member_typs = {}
-        for key, value in zip(expr.keys, expr.values):
-            assert isinstance(key, vy_ast.Name)
-            assert key.id not in member_subs
-
-            sub = Expr(value, context).ir_node
-            member_subs[key.id] = sub
-            member_typs[key.id] = sub.typ
+        for kwarg in expr.keywords:
+            assert kwarg.arg not in member_subs
+
+            sub = Expr(kwarg.value, self.context).ir_node
+            member_subs[kwarg.arg] = sub
 
         return IRnode.from_list(
             ["multi"] + [member_subs[key] for key in member_subs.keys()], typ=typ
diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py
index 39a1c59290..cc08b3b95a 100644
--- a/vyper/semantics/analysis/local.py
+++ b/vyper/semantics/analysis/local.py
@@ -731,8 +731,8 @@ def visit_Call(self, node: vy_ast.Call, typ: VyperType) -> None:
             # struct ctors
             # ctors have no kwargs
             expected_types = func_type.typedef.members.values()  # type: ignore
-            for value, arg_type in zip(node.args[0].values, expected_types):
-                self.visit(value, arg_type)
+            for kwarg, arg_type in zip(node.keywords, expected_types):
+                self.visit(kwarg.value, arg_type)
         elif isinstance(func_type, MemberFunctionT):
             if func_type.is_modifying and self.function_analyzer is not None:
                 # TODO refactor this
diff --git a/vyper/semantics/types/user.py b/vyper/semantics/types/user.py
index 0c9b5d70da..f09a97d0b9 100644
--- a/vyper/semantics/types/user.py
+++ b/vyper/semantics/types/user.py
@@ -381,38 +381,36 @@ def to_abi_arg(self, name: str = "") -> dict:
         components = [t.to_abi_arg(name=k) for k, t in self.member_types.items()]
         return {"name": name, "type": "tuple", "components": components}
 
-    # TODO breaking change: use kwargs instead of dict
-    # when using the type itself (not an instance) in the call position
-    # maybe rename to _ctor_call_return
     def _ctor_call_return(self, node: vy_ast.Call) -> "StructT":
-        validate_call_args(node, 1)
-        if not isinstance(node.args[0], vy_ast.Dict):
+        if len(node.args) > 0:
             raise VariableDeclarationException(
-                "Struct values must be declared via dictionary", node.args[0]
+                "Struct values must be declared as kwargs e.g. Foo(a=1, b=2)", node.args[0]
             )
         if next((i for i in self.member_types.values() if isinstance(i, HashMapT)), False):
             raise VariableDeclarationException(
                 "Struct contains a mapping and so cannot be declared as a literal", node
             )
 
+        # manually validate kwargs for better error messages instead of
+        # relying on `validate_call_args`
         members = self.member_types.copy()
         keys = list(self.member_types.keys())
-        for i, (key, value) in enumerate(zip(node.args[0].keys, node.args[0].values)):
-            if key is None or key.get("id") not in members:
-                hint = get_levenshtein_error_suggestions(key.get("id"), members, 1.0)
-                raise UnknownAttribute(
-                    "Unknown or duplicate struct member.", key or value, hint=hint
-                )
-            expected_key = keys[i]
-            if key.id != expected_key:
+        for i, kwarg in enumerate(node.keywords):
+            # x=5 => kwarg(arg="x", value=Int(5))
+            argname = kwarg.arg
+            if argname not in members:
+                hint = get_levenshtein_error_suggestions(argname, members, 1.0)
+                raise UnknownAttribute("Unknown or duplicate struct member.", kwarg, hint=hint)
+            expected = keys[i]
+            if argname != expected:
                 raise InvalidAttribute(
                     "Struct keys are required to be in order, but got "
-                    f"`{key.id}` instead of `{expected_key}`. (Reminder: the "
+                    f"`{argname}` instead of `{expected}`. (Reminder: the "
                     f"keys in this struct are {list(self.member_types.items())})",
-                    key,
+                    kwarg,
                 )
-
-            validate_expected_type(value, members.pop(key.id))
+            expected_type = members.pop(argname)
+            validate_expected_type(kwarg.value, expected_type)
 
         if members:
             raise VariableDeclarationException(
@@ -422,4 +420,4 @@ def _ctor_call_return(self, node: vy_ast.Call) -> "StructT":
         return self
 
     def _ctor_modifiability_for_call(self, node: vy_ast.Call, modifiability: Modifiability) -> bool:
-        return all(check_modifiability(v, modifiability) for v in node.args[0].values)
+        return all(check_modifiability(k.value, modifiability) for k in node.keywords)