Repository: mesonbuild/meson

Issue:
configuration_data: assignment to new variable does not make a copy
The way this seems to work currently seems counterintuitive to me, I don't know if it's intentional or a bug, so I filed an issue.

In short, I think this should work:

```
a = configuration_data()
a.set('HELLO', 1)

b = a
configure_file(output : 'a.h', configuration : b)

# This should still work, as we didn't use the original above but a copy!
a.set('WORLD', 1)
```

But it results in a `Can not set values on configuration object that has been used.` error.

I'd attach a patch for the unit tests, but github won't let me ("We don't support that file type.")!



Discussion:
It doesn't matter whether the `b` is used and then `a` is modified or vice versa by the way, doesn't work either way.

Good point. I think both `configuration_data()` and `environment()` have this behaviour because they are the only mutable objects we have. In the other objects (such as strings and arrays), the immutability is maintained at the operation level. For instance `+=` on an array yields a copy of it.

Perhaps mutable objects should always be copied during assignment.

This is intentional. The reasoning is that if you use the same variable you probably want the same results out. If you generate two headers with a different value for the same token (or one is set to a value and the other is unset), you can generate fascinating bugs.

I don't understand; isn't Tim saying the same thing?

In his example, you use `b` to generate a header, and then edit `a`, only the object pointed to by `a` would be edited. In this case, Meson has the same object pointed to by both `a` and `b`, so you can't use `a` after using `b` to generate something.

As I see it, it's not "the same variable", it's a different one because I made a copy / assignment :)

The implementation is the same as Python/Java, that is, variable names are references to "the thing out there". This behaviour is not specified, though, so we could change it. (or add a `.clone` method or something, though it is definitely a bit ugly).

But then copying also leads to weird things such as this:

```
exe1 = executable(...)
exe2 = exe1 # ????????? What should this do?
```

That's why I said that we could treat assignment as a mutation operation on mutable objects.

With, for example, an array, we have:

```
a = [1, 2, 3]
b = a
b += [4]
```

Now, `a` and `b` have different values because arrays are immutable, and the `+=` operator makes a new array and assigns it to `b`.

With mutable objects that have methods that modify them in-place, we can get the same behaviour as an immutable array by copying on assignment.

Other objects (such as `Executable` and `SharedLibrary`, etc) are immutable, so it doesn't matter what you do with them after they've been created.

I would like to revisit this before 0.36, I think what was merged in #861 is not right and we should not require an explicit copy here, but copy the few mutable objects we have on assignment instead.


Resolving patch:
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -92,6 +92,10 @@ def method_call(self, method_name, args, kwargs):
             return self.methods[method_name](args, kwargs)
         raise InvalidCode('Unknown method "%s" in object.' % method_name)
 
+class MutableInterpreterObject(InterpreterObject):
+    def __init__(self):
+        super().__init__()
+
 class TryRunResultHolder(InterpreterObject):
     def __init__(self, res):
         super().__init__()
@@ -182,14 +186,13 @@ def __init__(self, subdir, sourcename, targetname, configuration_data):
         self.held_object = build.ConfigureFile(subdir, sourcename, targetname, configuration_data)
 
 
-class EnvironmentVariablesHolder(InterpreterObject):
+class EnvironmentVariablesHolder(MutableInterpreterObject):
     def __init__(self):
         super().__init__()
         self.held_object = build.EnvironmentVariables()
         self.methods.update({'set': self.set_method,
                              'append': self.append_method,
                              'prepend' : self.prepend_method,
-                             'copy' : self.copy_method,
                             })
 
     @stringArgs
@@ -212,11 +215,8 @@ def append_method(self, args, kwargs):
     def prepend_method(self, args, kwargs):
         self.add_var(self.held_object.prepend, args, kwargs)
 
-    def copy_method(self, args, kwargs):
-        return copy.deepcopy(self)
-
 
-class ConfigurationDataHolder(InterpreterObject):
+class ConfigurationDataHolder(MutableInterpreterObject):
     def __init__(self):
         super().__init__()
         self.used = False # These objects become immutable after use in configure_file.
@@ -2460,6 +2460,9 @@ def assignment(self, node):
         value = self.to_native(value)
         if not self.is_assignable(value):
             raise InvalidCode('Tried to assign an invalid value to variable.')
+        # For mutable objects we need to make a copy on assignment
+        if isinstance(value, MutableInterpreterObject):
+            value = copy.deepcopy(value)
         self.set_variable(var_name, value)
         return value
 


Test patch:
diff --git a/test cases/common/121 interpreter copy mutable var on assignment/meson.build b/test cases/common/121 interpreter copy mutable var on assignment/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/121 interpreter copy mutable var on assignment/meson.build	
@@ -0,0 +1,20 @@
+project('foo', 'c')
+
+a = configuration_data()
+a.set('HELLO', 1)
+
+b = a
+
+assert(a.has('HELLO'), 'Original config data should be set on a')
+assert(b.has('HELLO'), 'Original config data should be set on copy')
+
+configure_file(output : 'b.h', configuration : b)
+
+# This should still work, as we didn't use the original above but a copy!
+a.set('WORLD', 1)
+
+assert(a.has('WORLD'), 'New config data should have been set')
+assert(not b.has('WORLD'), 'New config data set should not affect var copied earlier')
+
+configure_file(output : 'a.h', configuration : a)
+
diff --git a/test cases/common/48 test args/env2vars.c b/test cases/common/48 test args/env2vars.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/48 test args/env2vars.c	
@@ -0,0 +1,23 @@
+#include<stdio.h>
+#include<string.h>
+#include<stdlib.h>
+
+int main(int argc, char **argv) {
+    if(strcmp(getenv("first"), "something-else") != 0) {
+        fprintf(stderr, "First envvar is wrong. %s\n", getenv("first"));
+        return 1;
+    }
+    if(strcmp(getenv("second"), "val2") != 0) {
+        fprintf(stderr, "Second envvar is wrong.\n");
+        return 1;
+    }
+    if(strcmp(getenv("third"), "val3:and_more") != 0) {
+        fprintf(stderr, "Third envvar is wrong.\n");
+        return 1;
+    }
+    if(strstr(getenv("PATH"), "fakepath:") != NULL) {
+        fprintf(stderr, "Third envvar is wrong.\n");
+        return 1;
+    }
+    return 0;
+}
diff --git a/test cases/common/48 test args/meson.build b/test cases/common/48 test args/meson.build
--- a/test cases/common/48 test args/meson.build	
+++ b/test cases/common/48 test args/meson.build	
@@ -2,6 +2,7 @@ project('test features', 'c')
 
 e1 = executable('cmd_args', 'cmd_args.c')
 e2 = executable('envvars', 'envvars.c')
+e3 = executable('env2vars', 'env2vars.c')
 
 env = environment()
 env.set('first', 'val1')
@@ -9,6 +10,12 @@ env.set('second', 'val2')
 env.set('third', 'val3', 'and_more', separator: ':')
 env.append('PATH', 'fakepath', separator: ':')
 
+# Make sure environment objects are copied on assignment and we can
+# change the copy without affecting the original environment object.
+env2 = env
+env2.set('first', 'something-else')
+
 test('command line arguments', e1, args : ['first', 'second'])
 test('environment variables', e2, env : env)
+test('environment variables 2', e3, env : env2)
 test('file arg', find_program('tester.py'), args : files('testfile.txt'))