sonolus.py 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

sonolus/script/debug.py CHANGED
@@ -1,15 +1,25 @@
1
1
  from collections.abc import Callable, Sequence
2
2
  from contextvars import ContextVar
3
- from typing import Any, Never
3
+ from typing import Any, Literal, Never
4
4
 
5
5
  from sonolus.backend.mode import Mode
6
6
  from sonolus.backend.ops import Op
7
+ from sonolus.backend.optimize.constant_evaluation import SparseConditionalConstantPropagation
8
+ from sonolus.backend.optimize.copy_coalesce import CopyCoalesce
9
+ from sonolus.backend.optimize.dead_code import (
10
+ AdvancedDeadCodeElimination,
11
+ DeadCodeElimination,
12
+ UnreachableCodeElimination,
13
+ )
7
14
  from sonolus.backend.optimize.flow import cfg_to_mermaid
15
+ from sonolus.backend.optimize.inlining import InlineVars
8
16
  from sonolus.backend.optimize.passes import CompilerPass, run_passes
9
- from sonolus.backend.optimize.simplify import CoalesceFlow
10
- from sonolus.script.internal.context import GlobalContextState, ctx, set_ctx
17
+ from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
18
+ from sonolus.backend.optimize.ssa import FromSSA, ToSSA
19
+ from sonolus.script.internal.context import GlobalContextState, ReadOnlyMemory, ctx, set_ctx
11
20
  from sonolus.script.internal.impl import meta_fn, validate_value
12
21
  from sonolus.script.internal.native import native_function
22
+ from sonolus.script.internal.simulation_context import SimulationContext
13
23
  from sonolus.script.num import Num
14
24
 
15
25
  debug_log_callback = ContextVar[Callable[[Num], None]]("debug_log_callback")
@@ -93,12 +103,60 @@ def terminate():
93
103
  raise RuntimeError("Terminated")
94
104
 
95
105
 
96
- def visualize_cfg(fn: Callable[[], Any], passes: Sequence[CompilerPass] | None = None) -> str:
106
+ def visualize_cfg(
107
+ fn: Callable[[], Any] | Callable[[type], Any],
108
+ /,
109
+ *,
110
+ mode: Mode = Mode.PLAY,
111
+ archetype: type | None = None,
112
+ archetypes: list[type] | None,
113
+ passes: Sequence[CompilerPass] | Literal["minimal", "basic", "standard"] = "basic",
114
+ ) -> str:
97
115
  from sonolus.build.compile import callback_to_cfg
98
116
 
99
- if passes is None:
100
- passes = [CoalesceFlow()]
101
-
102
- cfg = callback_to_cfg(GlobalContextState(Mode.PLAY), fn, "")
117
+ match passes:
118
+ case "minimal":
119
+ passes = [
120
+ CoalesceFlow(),
121
+ ]
122
+ case "basic":
123
+ passes = [
124
+ CoalesceFlow(),
125
+ UnreachableCodeElimination(),
126
+ AdvancedDeadCodeElimination(),
127
+ CoalesceFlow(),
128
+ ]
129
+ case "standard":
130
+ passes = [
131
+ CoalesceFlow(),
132
+ UnreachableCodeElimination(),
133
+ DeadCodeElimination(),
134
+ ToSSA(),
135
+ SparseConditionalConstantPropagation(),
136
+ UnreachableCodeElimination(),
137
+ DeadCodeElimination(),
138
+ CoalesceFlow(),
139
+ InlineVars(),
140
+ DeadCodeElimination(),
141
+ RewriteToSwitch(),
142
+ FromSSA(),
143
+ CoalesceFlow(),
144
+ CopyCoalesce(),
145
+ AdvancedDeadCodeElimination(),
146
+ CoalesceFlow(),
147
+ NormalizeSwitch(),
148
+ ]
149
+
150
+ global_state = GlobalContextState(
151
+ mode,
152
+ {a: i for i, a in enumerate(archetypes)} if archetypes is not None else None,
153
+ ReadOnlyMemory(),
154
+ )
155
+
156
+ cfg = callback_to_cfg(global_state, fn, "", archetype=archetype)
103
157
  cfg = run_passes(cfg, passes)
104
158
  return cfg_to_mermaid(cfg)
159
+
160
+
161
+ def simulation_context() -> SimulationContext:
162
+ return SimulationContext()
sonolus/script/globals.py CHANGED
@@ -5,6 +5,7 @@ from sonolus.backend.blocks import Block, PlayBlock, PreviewBlock, TutorialBlock
5
5
  from sonolus.backend.mode import Mode
6
6
  from sonolus.script.internal.descriptor import SonolusDescriptor
7
7
  from sonolus.script.internal.generic import validate_concrete_type
8
+ from sonolus.script.internal.simulation_context import sim_ctx
8
9
  from sonolus.script.internal.value import Value
9
10
 
10
11
 
@@ -27,6 +28,11 @@ class _GlobalField(SonolusDescriptor):
27
28
  if instance is None:
28
29
  return self
29
30
 
31
+ if sim_ctx():
32
+ from sonolus.script.values import zeros
33
+
34
+ return sim_ctx().get_or_put_value((instance, self), lambda: zeros(self.type))
35
+
30
36
  from sonolus.script.internal.context import ctx
31
37
 
32
38
  info = owner._global_info_
@@ -38,6 +44,12 @@ class _GlobalField(SonolusDescriptor):
38
44
  def __set__(self, instance, value):
39
45
  from sonolus.script.internal.context import ctx
40
46
 
47
+ if sim_ctx():
48
+ from sonolus.script.values import zeros
49
+
50
+ sim_ctx().set_or_put_value((instance, self), lambda: zeros(self.type), value)
51
+ return
52
+
41
53
  info = instance._global_info_
42
54
  if not ctx():
43
55
  raise RuntimeError("Global field access outside of compilation")
@@ -64,6 +76,11 @@ class _GlobalPlaceholder:
64
76
  base = ctx().get_global_base(self)
65
77
  return self.type._from_place_(base)
66
78
 
79
+ def _get_sim_replacement_(self):
80
+ from sonolus.script.values import zeros
81
+
82
+ return sim_ctx().get_or_put_value(self, lambda: zeros(self.type))
83
+
67
84
 
68
85
  def _create_global(cls: type, blocks: dict[Mode, Block], offset: int | None):
69
86
  if issubclass(cls, Value):
@@ -1,4 +1,3 @@
1
- from abc import ABC
2
1
  from collections.abc import Iterable
3
2
  from typing import overload
4
3
 
@@ -32,7 +31,7 @@ def _isinstance(value, type_):
32
31
  return isinstance(value, TupleImpl)
33
32
  if type_ in {_int, _float, _bool}:
34
33
  raise TypeError("Instance check against int, float, or bool is not supported, use Num instead")
35
- if not (isinstance(type_, type) and issubclass(type_, Value | ABC)):
34
+ if not (isinstance(type_, type) and (issubclass(type_, Value) or getattr(type_, "_allow_instance_check_", False))):
36
35
  raise TypeError(f"Unsupported type: {type_} for isinstance")
37
36
  return validate_value(isinstance(value, type_))
38
37
 
@@ -87,7 +86,7 @@ def _zip(*iterables):
87
86
  if any(isinstance(iterable, TupleImpl) for iterable in iterables):
88
87
  if not all(isinstance(iterable, TupleImpl) for iterable in iterables):
89
88
  raise TypeError("Cannot mix tuples with other types in zip")
90
- return TupleImpl._accept_(tuple(zip(*(iterable._as_py_() for iterable in iterables), strict=False)))
89
+ return TupleImpl._accept_(tuple(zip(*(iterable.value for iterable in iterables), strict=False)))
91
90
  iterators = [compile_and_call(iterable.__iter__) for iterable in iterables]
92
91
  if not all(isinstance(iterator, SonolusIterator) for iterator in iterators):
93
92
  raise TypeError("Only subclasses of SonolusIterator are supported as iterators")
@@ -19,6 +19,25 @@ _compiler_internal_ = True
19
19
  context_var = ContextVar("context_var", default=None)
20
20
 
21
21
 
22
+ @dataclass(frozen=True)
23
+ class DebugConfig:
24
+ unchecked_reads: bool
25
+ unchecked_writes: bool
26
+
27
+
28
+ _full_debug_config = DebugConfig(
29
+ unchecked_reads=True,
30
+ unchecked_writes=True,
31
+ )
32
+
33
+ _disabled_debug_config = DebugConfig(
34
+ unchecked_reads=False,
35
+ unchecked_writes=False,
36
+ )
37
+
38
+ debug_var = ContextVar("debug_var", default=_disabled_debug_config)
39
+
40
+
22
41
  class GlobalContextState:
23
42
  archetypes: dict[type, int]
24
43
  rom: ReadOnlyMemory
@@ -92,12 +111,16 @@ class Context:
92
111
  return self.callback_state.used_names
93
112
 
94
113
  def check_readable(self, place: BlockPlace):
114
+ if debug_config().unchecked_reads:
115
+ return
95
116
  if not self.callback:
96
117
  return
97
118
  if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).readable:
98
119
  raise RuntimeError(f"Block {place.block} is not readable in {self.callback}")
99
120
 
100
121
  def check_writable(self, place: BlockPlace):
122
+ if debug_config().unchecked_writes:
123
+ return
101
124
  if not self.callback:
102
125
  return
103
126
  if isinstance(place.block, BlockData) and self.callback not in self.blocks(place.block).writable:
@@ -238,6 +261,12 @@ class Context:
238
261
  context.outgoing[None] = target
239
262
  return target
240
263
 
264
+ def register_archetype(self, type_: type) -> int:
265
+ with self.global_state.lock:
266
+ if type_ not in self.global_state.archetypes:
267
+ self.global_state.archetypes[type_] = len(self.global_state.archetypes)
268
+ return self.global_state.archetypes[type_]
269
+
241
270
 
242
271
  def ctx() -> Context | None:
243
272
  return context_var.get()
@@ -283,6 +312,27 @@ class ReadOnlyMemory:
283
312
  else:
284
313
  return PlayBlock.EngineRom
285
314
 
315
+ def get_value(self, index: int) -> float:
316
+ with self._lock:
317
+ if index < 0 or index >= len(self.values):
318
+ raise IndexError(f"Index {index} out of bounds for ReadOnlyMemory")
319
+ return self.values[index]
320
+
321
+
322
+ @contextmanager
323
+ def enable_debug(config: DebugConfig | None = None):
324
+ if config is None:
325
+ config = _full_debug_config
326
+ token = debug_var.set(config)
327
+ try:
328
+ yield
329
+ finally:
330
+ debug_var.reset(token)
331
+
332
+
333
+ def debug_config() -> DebugConfig:
334
+ return debug_var.get()
335
+
286
336
 
287
337
  @dataclass
288
338
  class ValueBinding:
@@ -0,0 +1,131 @@
1
+ import builtins
2
+ import sys
3
+ from collections.abc import Callable
4
+ from types import MethodType, ModuleType
5
+ from typing import Any, ClassVar, Self
6
+
7
+ from sonolus.script.internal.impl import validate_value
8
+
9
+ _MISSING = object()
10
+
11
+
12
+ class SimulationContext:
13
+ """Context manager to simulate additional Sonolus runtime features like level memory."""
14
+
15
+ _active_context: ClassVar[Self | None] = None
16
+ additional_replacements: dict[Any, Any]
17
+ values: dict[Any, Any]
18
+ _original_values: dict[str, dict[str, Any]] # module_name: {var_name: original_value}
19
+ _original_import: Callable[[str, Any, Any, Any, Any], Any] | None
20
+
21
+ def __init__(self, *, additional_replacements: dict[Any, Any] | None = None):
22
+ if SimulationContext._active_context is not None:
23
+ raise RuntimeError("SimulationContext is already active")
24
+ SimulationContext._active_context = self
25
+ self.additional_replacements = additional_replacements or {}
26
+ self.values = {}
27
+ self._original_values = {}
28
+ self._original_import = None
29
+
30
+ def get_or_put_value(self, key: Any, factory: Callable[[], Any]) -> Any:
31
+ if key not in self.values:
32
+ self.values[key] = validate_value(factory())
33
+ return self.values[key]._get_()._as_py_()
34
+
35
+ def set_or_put_value(self, key: Any, factory: Callable[[], Any], value: Any):
36
+ if key not in self.values:
37
+ self.values[key] = validate_value(factory())
38
+ existing_value = self.values[key]
39
+ existing_type = type(existing_value)
40
+ value = existing_type._accept_(validate_value(value))
41
+ if existing_type._is_value_type_():
42
+ existing_value._set_(value)
43
+ else:
44
+ existing_value._copy_from_(value)
45
+
46
+ def _get_replacement(self, value: Any) -> Any:
47
+ try:
48
+ if value in self.additional_replacements:
49
+ return self.additional_replacements[value]
50
+ except TypeError:
51
+ pass
52
+ if hasattr(value, "_get_sim_replacement_") and isinstance(value._get_sim_replacement_, MethodType):
53
+ return value._get_sim_replacement_()
54
+ return _MISSING
55
+
56
+ def _substitute_module_variables(self, module) -> None:
57
+ if not isinstance(module, ModuleType):
58
+ return
59
+
60
+ module_name = module.__name__
61
+
62
+ if module_name in self._original_values:
63
+ return
64
+
65
+ original_values = {}
66
+
67
+ for var_name, var_value in list(module.__dict__.items()):
68
+ replacement = self._get_replacement(var_value)
69
+ if replacement is not _MISSING:
70
+ original_values[var_name] = var_value
71
+ setattr(module, var_name, replacement)
72
+
73
+ if original_values:
74
+ self._original_values[module_name] = original_values
75
+
76
+ def _update_loaded_modules(self) -> None:
77
+ loaded_modules = list(sys.modules.values())
78
+
79
+ for module in loaded_modules:
80
+ if module is not None:
81
+ self._substitute_module_variables(module)
82
+
83
+ def _create_import_hook(self):
84
+ original_import = builtins.__import__
85
+
86
+ def hooked_import(name, globals=None, locals=None, fromlist=(), level=0): # noqa: A002
87
+ module = original_import(name, globals, locals, fromlist, level)
88
+ if isinstance(module, ModuleType):
89
+ self._substitute_module_variables(module)
90
+
91
+ if fromlist:
92
+ for item in fromlist:
93
+ if hasattr(module, item):
94
+ attr = getattr(module, item)
95
+ if isinstance(attr, ModuleType):
96
+ self._substitute_module_variables(attr)
97
+
98
+ return module
99
+
100
+ return hooked_import
101
+
102
+ def __enter__(self) -> Self:
103
+ self._update_loaded_modules()
104
+
105
+ self._original_import = builtins.__import__
106
+ builtins.__import__ = self._create_import_hook()
107
+
108
+ return self
109
+
110
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any):
111
+ try:
112
+ if self._original_import is not None:
113
+ builtins.__import__ = self._original_import
114
+ self._original_import = None
115
+
116
+ for module_name, original_values in self._original_values.items():
117
+ if module_name in sys.modules:
118
+ module = sys.modules[module_name]
119
+ if isinstance(module, ModuleType):
120
+ for var_name, original_value in original_values.items():
121
+ setattr(module, var_name, original_value)
122
+
123
+ self._original_values.clear()
124
+
125
+ finally:
126
+ SimulationContext._active_context = None
127
+
128
+
129
+ def sim_ctx() -> SimulationContext | None:
130
+ """Get the current simulation context, or None if not active."""
131
+ return SimulationContext._active_context
@@ -30,7 +30,7 @@ class TupleImpl(TransientValue):
30
30
  return len(self.value)
31
31
 
32
32
  def __eq__(self, other):
33
- if not isinstance(other, tuple):
33
+ if not self._is_tuple_impl(other):
34
34
  return False
35
35
  if len(self) != len(other):
36
36
  return False
@@ -40,7 +40,7 @@ class TupleImpl(TransientValue):
40
40
  return True
41
41
 
42
42
  def __ne__(self, other):
43
- if not isinstance(other, tuple):
43
+ if not self._is_tuple_impl(other):
44
44
  return True
45
45
  if len(self) != len(other):
46
46
  return True
@@ -50,33 +50,33 @@ class TupleImpl(TransientValue):
50
50
  return False
51
51
 
52
52
  def __lt__(self, other):
53
- if not isinstance(other, tuple):
53
+ if not self._is_tuple_impl(other):
54
54
  return NotImplemented
55
- for a, b in zip(self.value, other.value):
55
+ for a, b in zip(self, other):
56
56
  if a != b:
57
57
  return a < b
58
58
  return len(self.value) < len(other.value)
59
59
 
60
60
  def __le__(self, other):
61
- if not isinstance(other, tuple):
61
+ if not self._is_tuple_impl(other):
62
62
  return NotImplemented
63
- for a, b in zip(self.value, other.value):
63
+ for a, b in zip(self, other):
64
64
  if a != b:
65
65
  return a < b
66
66
  return len(self.value) <= len(other.value)
67
67
 
68
68
  def __gt__(self, other):
69
- if not isinstance(other, tuple):
69
+ if not self._is_tuple_impl(other):
70
70
  return NotImplemented
71
- for a, b in zip(self.value, other.value):
71
+ for a, b in zip(self, other):
72
72
  if a != b:
73
73
  return a > b
74
74
  return len(self.value) > len(other.value)
75
75
 
76
76
  def __ge__(self, other):
77
- if not isinstance(other, tuple):
77
+ if not self._is_tuple_impl(other):
78
78
  return NotImplemented
79
- for a, b in zip(self.value, other.value):
79
+ for a, b in zip(self, other):
80
80
  if a != b:
81
81
  return a > b
82
82
  return len(self.value) >= len(other.value)
@@ -89,6 +89,11 @@ class TupleImpl(TransientValue):
89
89
  other = TupleImpl._accept_(other)
90
90
  return TupleImpl._accept_(self.value + other.value)
91
91
 
92
+ @staticmethod
93
+ @meta_fn
94
+ def _is_tuple_impl(value: Any) -> bool:
95
+ return isinstance(value, TupleImpl)
96
+
92
97
  @classmethod
93
98
  def _accepts_(cls, value: Any) -> bool:
94
99
  return isinstance(value, cls | tuple)
@@ -1,14 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
- from collections.abc import Iterator
5
4
  from typing import Any
6
5
 
7
6
  from sonolus.script.internal.impl import meta_fn
8
7
  from sonolus.script.record import Record
9
8
 
10
9
 
11
- class SonolusIterator[T](Iterator[T]):
10
+ class SonolusIterator[T]:
12
11
  """Base class for Sonolus iterators.
13
12
 
14
13
  This class is used to define custom iterators that can be used in Sonolus.py.
@@ -30,6 +29,8 @@ class SonolusIterator[T](Iterator[T]):
30
29
  ```
31
30
  """
32
31
 
32
+ _allow_instance_check_ = True
33
+
33
34
  def next(self) -> T:
34
35
  result = self.get()
35
36
  self.advance()
sonolus/script/num.py CHANGED
@@ -1,4 +1,3 @@
1
- # ruff: noqa: N801
2
1
  from __future__ import annotations
3
2
 
4
3
  import operator
@@ -81,12 +80,22 @@ class _Num(Value, metaclass=_NumMeta):
81
80
  return value
82
81
  return cls(value)
83
82
 
83
+ def _is_rom_constant(self) -> bool:
84
+ return (
85
+ ctx()
86
+ and isinstance(self.data, BlockPlace)
87
+ and self.data.block == ctx().blocks.EngineRom
88
+ and isinstance(self.data.index, int)
89
+ )
90
+
84
91
  def _is_py_(self) -> bool:
85
- return isinstance(self.data, float | int | bool)
92
+ return isinstance(self.data, float | int | bool) or self._is_rom_constant()
86
93
 
87
94
  def _as_py_(self) -> Any:
88
95
  if not self._is_py_():
89
96
  raise ValueError("Not a compile time constant Num")
97
+ if self._is_rom_constant():
98
+ return ctx().rom.get_value(self.data.index + self.data.offset)
90
99
  if self.data.is_integer():
91
100
  return int(self.data)
92
101
  return self.data
sonolus/script/options.py CHANGED
@@ -5,11 +5,13 @@ from typing import Annotated, Any, NewType, dataclass_transform, get_origin
5
5
  from sonolus.backend.mode import Mode
6
6
  from sonolus.backend.place import BlockPlace
7
7
  from sonolus.script.debug import assert_unreachable
8
- from sonolus.script.internal.context import ctx
8
+ from sonolus.script.internal.context import ctx, debug_config
9
9
  from sonolus.script.internal.descriptor import SonolusDescriptor
10
10
  from sonolus.script.internal.generic import validate_concrete_type
11
11
  from sonolus.script.internal.introspection import get_field_specifiers
12
+ from sonolus.script.internal.simulation_context import sim_ctx
12
13
  from sonolus.script.num import Num
14
+ from sonolus.script.values import copy
13
15
 
14
16
 
15
17
  @dataclass
@@ -186,6 +188,8 @@ class _OptionField(SonolusDescriptor):
186
188
  self.index = index
187
189
 
188
190
  def __get__(self, instance, owner):
191
+ if sim_ctx():
192
+ return sim_ctx().get_or_put_value((instance, self), lambda: copy(self.info.default))
189
193
  if ctx():
190
194
  match ctx().global_state.mode:
191
195
  case Mode.PLAY:
@@ -202,8 +206,27 @@ class _OptionField(SonolusDescriptor):
202
206
  return Num._from_place_(BlockPlace(block, self.index))
203
207
  else:
204
208
  return Num._accept_(self.info.default)
209
+ raise RuntimeError("Options can only be accessed in a context")
205
210
 
206
211
  def __set__(self, instance, value):
212
+ if sim_ctx():
213
+ return sim_ctx().set_or_put_value((instance, self), lambda: copy(self.info.default), value)
214
+ if ctx() and debug_config().unchecked_writes:
215
+ match ctx().global_state.mode:
216
+ case Mode.PLAY:
217
+ block = ctx().blocks.LevelOption
218
+ case Mode.WATCH:
219
+ block = ctx().blocks.LevelOption
220
+ case Mode.PREVIEW:
221
+ block = ctx().blocks.PreviewOption
222
+ case Mode.TUTORIAL:
223
+ block = None
224
+ case _:
225
+ assert_unreachable()
226
+ if block is not None:
227
+ Num._from_place_(BlockPlace(block, self.index))._set_(Num._accept_(value))
228
+ else:
229
+ raise RuntimeError("Options in the current mode cannot be set and use the default value")
207
230
  raise AttributeError("Options are read-only")
208
231
 
209
232
 
sonolus/script/quad.py CHANGED
@@ -147,6 +147,8 @@ class Quad(Record):
147
147
  def contains_point(self, point: Vec2, /) -> bool:
148
148
  """Check if the quad contains the given point.
149
149
 
150
+ It is not guaranteed whether points on the edges of the quad are considered inside or outside.
151
+
150
152
  Args:
151
153
  point: The point to check.
152
154
 
sonolus/script/record.py CHANGED
@@ -20,8 +20,14 @@ from sonolus.script.internal.value import BackingSource, DataValue, Value
20
20
  from sonolus.script.num import Num
21
21
 
22
22
 
23
+ class RecordMeta(type):
24
+ @meta_fn
25
+ def __pos__[T](cls: type[T]) -> T:
26
+ return cls._zero_()
27
+
28
+
23
29
  @dataclass_transform(eq_default=True)
24
- class Record(GenericValue):
30
+ class Record(GenericValue, metaclass=RecordMeta):
25
31
  """Base class for user-defined data structures.
26
32
 
27
33
  Usage:
@@ -38,6 +44,15 @@ class Record(GenericValue):
38
44
  field1: T
39
45
  field2: U
40
46
  ```
47
+
48
+ Creating an instance:
49
+ ```python
50
+ record = MyRecord(field1=42, field2=True)
51
+ record_2 = MyGenericRecord[int, int](field1=42, field2=100)
52
+ record_3 = MyGenericRecord(field1=42, field2=100) # Type arguments can be inferred
53
+ record_4 = +MyRecord # Create a zero-initialized record
54
+ record_5 = +MyGenericRecord[int, int]
55
+ ```
41
56
  """
42
57
 
43
58
  _value: dict[str, Value]
@@ -239,12 +254,12 @@ class Record(GenericValue):
239
254
 
240
255
  def __str__(self):
241
256
  return (
242
- f"{self.__class__.__name__}({", ".join(f"{field.name}={field.__get__(self)}" for field in self._fields)})"
257
+ f"{self.__class__.__name__}({', '.join(f'{field.name}={field.__get__(self)}' for field in self._fields)})"
243
258
  )
244
259
 
245
260
  def __repr__(self):
246
261
  return (
247
- f"{self.__class__.__name__}({", ".join(f"{field.name}={field.__get__(self)!r}" for field in self._fields)})"
262
+ f"{self.__class__.__name__}({', '.join(f'{field.name}={field.__get__(self)!r}' for field in self._fields)})"
248
263
  )
249
264
 
250
265
  @meta_fn
@@ -268,6 +283,10 @@ class Record(GenericValue):
268
283
  def __hash__(self):
269
284
  return hash(tuple(field.__get__(self) for field in self._fields))
270
285
 
286
+ def __pos__(self) -> Self:
287
+ """Return a copy of the record."""
288
+ return self._copy_()
289
+
271
290
  @classmethod
272
291
  @meta_fn
273
292
  def type_var_value(cls, var: TypeVar, /) -> Any: