# Compile-time constants ###### tags: `functional cycle 11` - Appetite: half cycle - Developer: Peter - Shaped by: Enrique, Till, Peter ## Goal The GT4Py field operators may use several mathematical, physical and configuration constants. Currently, the only way to use such constants is by copy-pasting them as a literal into the field operator, which is not scalable. The goal of this project is to add support for using constants defined at file scope or imported from another module within the body of field operators. ## Possible implementations An implementation must meet the following requirements: - Allow defining constants at the module scope in current module - Allow importing constants defined at module scope from another module - [Optional] Allow defining constants at the nonlocal scope - [Optional] Prevent accidental change to the constant value The constant's value is baked into the binaries produced for the field operator, and the constant's value will be as it was determined at compilation time. If, further down in the Python code, the user changes the value of the constant, it will not be reflected in the binaries and may lead to confusing errors. Therefore, it's important to ensure that a captured constant's value never actually changes. We examined several options that have different safety, performance and syntactical characteristics: ### Globals and nonlocals marked `Final` ```python PI: Final = 3.141592653589793238 @field_operator def func(): var = PI PI = 2.71828 # mypy error ``` This solution requires that global, nonlocal, and imported symbols are marked `Final` or else they will be rejected when compiling the field operator. While the syntax of this implementation is ideal, proving that a nonlocal or foreign module global is marked `Final` is complicated. For nonlocals, there is no way around examining the Python AST. For globals, you must find out the module from which they came from, which again requires examining the AST's `import` statements, possibly recursively through multiple modules. ### Globals and nonlocal with runtime checks ```python PI = 3.14159 @field_operator def func(): var = PI PI = 2.71828 # OK func() # runtime exception ``` This solution allows capturing any global variables, however, for each call of the field operator, it checks for every variable if its value has changed. Syntactially, this is quite nice, especially when combined with the use of `Final`, and it's very robust against misuse. The runtime check however may be too expensive as Python is slow and we want to minimize the overhead on field operator calls. ### Constant namespaces ```python constants = ConstantNamespace() constants.PI = 3.14159 @field_operator def func(): var = constants.PI constants.PI = 2.71828 # runtime exception ``` Constant namespaces are robust against accidental errors, they make implementation easy, and they incur no performance hit. Compared to the other two solutions, however, they have a slightly more verbose and restrictive syntax. ## Steps for implementation After a bit of discussion, we decided to go with constant namespaces. Although not ideal, it seems to be the best compromise when it comes to features and ease of implementation. Steps: - [ ] Create a `ConstantNamespace` class. (The `FrozenNamespace` class can be used as either inspiration or basis. There is also the `pconst` Python library for reference.) - [ ] Add unit tests for `ConstantNamespace` - [ ] Extend frontend structures to handle closure variables uniformly. (Currently, only GT4Py callables are stored as closure variables.) - [ ] Implement lowering of closure variables to ITIR. - [ ] Add integration tests for field operators capturing constants ## Dependencies Constant namespaces require support for attributes (i.e. `namespace.member` accesses) in the field operator AST. Before implementation on constant namespaces can begin, this feature has to be implemented.