fusion-engineering / inline-python

Inline Python code directly in your Rust code
https://docs.rs/inline-python
BSD 2-Clause "Simplified" License
1.15k stars 37 forks source link

Cause syntax error when assigning to 'var #39

Open wonkodv opened 3 years ago

wonkodv commented 3 years ago

You can cause a Syntaxerror for assignments to rust variables by expanding them to ((lambda:_RUST_var)())

Since the SyntaxError has a very confusing message, the likely cause is appended:

error: python: cannot assign to function call. LIKELY CAUSE: you cannot assign to RUST-Variables, see Context::get_global() instead
 --> examples/readonly.rs:6:9
  |
6 |         'x = 42;
  |         ^^^^^^^^

error: aborting due to previous error

The Error message could be improved by inspecting the code it is raised for, but I don't know how to do it reliably.

de-vri-es commented 3 years ago

Hey, thanks for the PR. It's an interesting idea.

I think it would be nicer to make the Rust globals an object with read-only properties. One downside for that is that it gives a runtime error when trying to assign, rather than syntax errors. But it's python, so I think people will forgive that. The advantage is that the error message will make more sense, and there's no need to intercept and adjust error messages then.

wonkodv commented 3 years ago

My first Idea was to use a new type as locals argument to exec, it does not have to be a dict. Globals have to be a dict. This is how it could look in python:

class ReadOnlyRustVariables:
    def __init__(self, dict):
        self.dict = dict
    def __getitem__(self, key):
        return self.dict[key]

    def __setitem__(self, key, val):
        if key.startswith("__rust_var_"):
            k = key[len("__rust_var_"):]
            raise TypeError(f"You can not modify the rust-variable '{k} use Context::get_global()")
        self.dict[key] = val

locals_ = ReadOnlyRustVariables({"__rust_var_x":42})
globals_ = vars(__import__("builtins"))

exec("y = __rust_var_x * 2", globals_, locals_)
assert locals_["y"] == 84, locals_

exec("__rust_var_x = __rust_var_x * 2", globals_, locals_)
# TypeError: You can not modify the rust-variable 'x use Context::get_global()

I didn't try this one.

another Idea was to replace 'var with _RUST_IMMUT_VARS['var'] (and maybe #var with _RUST_MUT_VARS['var']). The two objects could be created in Context::try_new() using inline_python::python! to initialize the python environment like so:

class RustVars:                                                                                      
    __slots__ = ("_dict", "_mutable")                                                                
    def __init__(self, mutable:bool):                                                                
        self._dict = dict()                                                                          
        self._mutable = mutable                                                                      

    def __setitem__(self, key, value):                                                               
        if self._mutable:                                                                            
            self._dict[key] = value                                                                  
        else:                                                                                        
            raise TypeError("You are trying to write to "+'\''+key+" which is read-only. Try #"+key) 

    def __getitem__(self, key):                                                                      
        return self._dict[key]                                                                       

_RUST_MUT_VARS = RustVars(True)                                                                      
_RUST_IMMUT_VARS = RustVars(False)                                                                   

del RustVars
from builtins import *

I tried implementing the second in rust and failed to run python code when initializing the context. It's a mess: https://github.com/wonkodv/inline-python/commit/1a6a51f28a4324dae417eee65e265ffb38ca90fb

When I thought of the lambda trick and found it easy to implement I liked the compile time errors better than runtime errors, even if the Error Message is not perfect.