osiris-lang 0.2.4__py3-none-win_amd64.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.
osiris/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from importlib.metadata import PackageNotFoundError, version as distribution_version
2
+
3
+ __all__ = ["version"]
4
+
5
+ try:
6
+ __version__ = distribution_version("osiris-lang")
7
+ except PackageNotFoundError:
8
+ __version__ = "0+unknown"
9
+
10
+
11
+ def version() -> str:
12
+ return __version__
@@ -0,0 +1,8 @@
1
+ """Runtime ABI imported by Python emitted from Osiris programs."""
2
+
3
+ from . import control as _control
4
+ from . import sequences as _sequences
5
+ from .control import *
6
+ from .sequences import *
7
+
8
+ __all__ = [*_control.__all__, *_sequences.__all__]
@@ -0,0 +1,77 @@
1
+ """Sequence realization and predicate consumers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins as _builtins
6
+ from collections.abc import Callable
7
+
8
+ from ._sequence_core import _MISSING, _iter_or_empty
9
+ from .control import truthy
10
+
11
+ def run_bang(function: Callable[[object], object], collection: object) -> None:
12
+ """Invoke ``function`` for each item and discard all results."""
13
+
14
+ for value in _iter_or_empty(collection):
15
+ function(value)
16
+ return None
17
+
18
+
19
+ def _realize_prefix(arguments: tuple[object, ...]) -> object:
20
+ if len(arguments) == 1:
21
+ limit = None
22
+ collection = arguments[0]
23
+ elif len(arguments) == 2:
24
+ limit = arguments[0]
25
+ collection = arguments[1]
26
+ if not isinstance(limit, int) or isinstance(limit, bool):
27
+ raise TypeError("realization count must be an integer")
28
+ else:
29
+ raise TypeError("expected collection or count and collection")
30
+ iterator = _iter_or_empty(collection)
31
+ if limit is None:
32
+ for _ in iterator:
33
+ pass
34
+ else:
35
+ for _ in range(max(0, limit)):
36
+ if _builtins.next(iterator, _MISSING) is _MISSING:
37
+ break
38
+ return collection
39
+
40
+
41
+ def doall(*arguments: object) -> object:
42
+ """Realize all, or a bounded prefix of, an iterable and return it."""
43
+
44
+ return _realize_prefix(arguments)
45
+
46
+
47
+ def dorun(*arguments: object) -> None:
48
+ """Realize all, or a bounded prefix of, an iterable and return nil."""
49
+
50
+ _realize_prefix(arguments)
51
+ return None
52
+
53
+
54
+ def some(predicate: Callable[[object], object], collection: object) -> object:
55
+ """Return the first Clojure-truthy predicate result, or nil."""
56
+
57
+ for value in _iter_or_empty(collection):
58
+ result = predicate(value)
59
+ if truthy(result):
60
+ return result
61
+ return None
62
+
63
+
64
+ def every_q(predicate: Callable[[object], object], collection: object) -> bool:
65
+ """Clojure-truthy all predicate check."""
66
+
67
+ return all(truthy(predicate(value)) for value in _iter_or_empty(collection))
68
+
69
+
70
+ def not_every_q(predicate: Callable[[object], object], collection: object) -> bool:
71
+ return not every_q(predicate, collection)
72
+
73
+
74
+ def not_any_q(predicate: Callable[[object], object], collection: object) -> bool:
75
+ # ``some`` returns the predicate's value, and Python's ``not`` would
76
+ # incorrectly treat Clojure-truthy values such as ``0``/``""`` as false.
77
+ return not truthy(some(predicate, collection))
@@ -0,0 +1,259 @@
1
+ """Memoized lazy-sequence representation and core sequence operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins as _builtins
6
+ import threading as _threading
7
+ from collections.abc import Callable, Iterable
8
+ from typing import Iterator, Optional
9
+
10
+ _MISSING = object()
11
+
12
+ class _LazySeq:
13
+ """Memoized lazy sequence that realizes elements one at a time.
14
+
15
+ The source iterator and already produced values are retained so a lazy
16
+ sequence can be traversed repeatedly without re-running its thunk or
17
+ losing values from a one-shot generator.
18
+ """
19
+
20
+ __slots__ = ("_thunk", "_source", "_cache", "_done", "_error", "_lock")
21
+
22
+ def __init__(self, thunk: Callable[[], Iterable[object]]) -> None:
23
+ self._thunk = thunk
24
+ self._source: Optional[Iterator[object]] = None
25
+ self._cache: list[object] = []
26
+ self._done = False
27
+ self._error: Optional[BaseException] = None
28
+ self._lock = _threading.RLock()
29
+
30
+ def _ensure_source_locked(self) -> Iterator[object]:
31
+ """Initialize the source while ``_lock`` is held."""
32
+
33
+ if self._error is not None:
34
+ raise self._error
35
+ if self._source is None:
36
+ try:
37
+ value = self._thunk()
38
+ self._source = iter(()) if value is None else iter(value)
39
+ except BaseException as error:
40
+ # A lazy source is a memoized computation. Cache failures so
41
+ # a second consumer cannot rerun an effectful or partial thunk.
42
+ self._error = error
43
+ self._done = True
44
+ raise
45
+ return self._source
46
+
47
+ def _iter_from(self, index: int) -> Iterator[object]:
48
+ """Iterate from a cached position without replaying earlier values."""
49
+
50
+ while True:
51
+ with self._lock:
52
+ if self._error is not None:
53
+ raise self._error
54
+ if index < len(self._cache):
55
+ value = self._cache[index]
56
+ index += 1
57
+ elif self._done:
58
+ return
59
+ else:
60
+ try:
61
+ value = _builtins.next(self._ensure_source_locked())
62
+ except StopIteration:
63
+ self._done = True
64
+ return
65
+ except BaseException as error:
66
+ self._error = error
67
+ self._done = True
68
+ raise
69
+ self._cache.append(value)
70
+ index += 1
71
+ yield value
72
+
73
+ def __iter__(self) -> Iterator[object]:
74
+ return self._iter_from(0)
75
+
76
+ def __bool__(self) -> bool:
77
+ """Match sequence truthiness without realizing more than one item."""
78
+
79
+ try:
80
+ _builtins.next(iter(self))
81
+ except StopIteration:
82
+ return False
83
+ return True
84
+
85
+
86
+ def lazy_seq(thunk: Callable[[], Iterable[object]]) -> _LazySeq:
87
+ """Construct a memoized, iterable lazy sequence from ``thunk``."""
88
+
89
+ return _LazySeq(thunk)
90
+
91
+
92
+ def _iter_or_empty(collection: object) -> Iterator[object]:
93
+ """Treat Clojure nil as an empty sequence at sequence boundaries."""
94
+
95
+ if collection is None:
96
+ return iter(())
97
+ return iter(collection) # type: ignore[arg-type]
98
+
99
+
100
+ def cons(value: object, collection: object) -> _LazySeq:
101
+ """Return a lazy sequence with ``value`` before ``collection``."""
102
+
103
+ def produce() -> Iterator[object]:
104
+ yield value
105
+ yield from _iter_or_empty(collection)
106
+
107
+ return lazy_seq(produce)
108
+
109
+
110
+ def concat(*collections: object) -> _LazySeq:
111
+ """Concatenate sequences lazily, skipping nil collections."""
112
+
113
+ def produce() -> Iterator[object]:
114
+ for collection in collections:
115
+ yield from _iter_or_empty(collection)
116
+
117
+ return lazy_seq(produce)
118
+
119
+
120
+ def count(collection: object) -> int:
121
+ """Count a collection without applying Python truthiness to it."""
122
+
123
+ if collection is None:
124
+ return 0
125
+ try:
126
+ return len(collection) # type: ignore[arg-type]
127
+ except TypeError:
128
+ return sum(1 for _ in collection) # type: ignore[operator]
129
+
130
+
131
+ _EMPTY_PROBE = object()
132
+
133
+
134
+ def empty_q(collection: object) -> bool:
135
+ """Return whether ``collection`` has no values.
136
+
137
+ Sized containers are checked without iteration. For a one-shot iterable
138
+ the probe necessarily advances its iterator by at most one item; Osiris
139
+ lazy sequences memoize that item, so repeated checks remain non-destructive
140
+ at the public sequence boundary. ``None`` follows Clojure's empty-seq
141
+ convention, while scalar non-collections raise ``TypeError``.
142
+ """
143
+
144
+ if collection is None:
145
+ return True
146
+ try:
147
+ return len(collection) == 0 # type: ignore[arg-type]
148
+ except TypeError:
149
+ try:
150
+ iterator = iter(collection) # type: ignore[arg-type]
151
+ except TypeError as error:
152
+ raise TypeError("empty? expects a collection") from error
153
+ return _builtins.next(iterator, _EMPTY_PROBE) is _EMPTY_PROBE
154
+
155
+
156
+ def seq_q(value: object) -> bool:
157
+ """Return whether ``value`` is an Osiris seq/list value.
158
+
159
+ Raw Python iterators are deliberately not treated as seqs; callers can
160
+ opt into the replayable seq protocol with :func:`sequence`.
161
+ """
162
+
163
+ return isinstance(value, (list, _LazySeq))
164
+
165
+
166
+ def coll_q(value: object) -> bool:
167
+ """Return whether ``value`` is an Osiris collection value."""
168
+
169
+ return isinstance(value, (list, tuple, dict, set, _LazySeq))
170
+
171
+
172
+ def sequential_q(value: object) -> bool:
173
+ """Return whether ``value`` is an ordered sequential collection."""
174
+
175
+ return isinstance(value, (list, tuple, _LazySeq))
176
+
177
+
178
+ def first(collection: object) -> object:
179
+ """Return the first item or nil for an empty sequence."""
180
+
181
+ return _builtins.next(_iter_or_empty(collection), None)
182
+
183
+
184
+ def rest(collection: object) -> list[object]:
185
+ """Return all but the first item as a materialized list."""
186
+
187
+ iterator = _iter_or_empty(collection)
188
+ _builtins.next(iterator, None)
189
+ return list(iterator)
190
+
191
+
192
+ def next(collection: object) -> Optional[_LazySeq]:
193
+ """Return the lazy tail, or nil when no tail exists."""
194
+
195
+ iterator = _iter_or_empty(collection)
196
+ try:
197
+ _builtins.next(iterator)
198
+ except StopIteration:
199
+ return None
200
+ try:
201
+ first_tail = _builtins.next(iterator)
202
+ except StopIteration:
203
+ return None
204
+
205
+ def produce() -> Iterator[object]:
206
+ yield first_tail
207
+ yield from iterator
208
+
209
+ return lazy_seq(produce)
210
+
211
+
212
+ def nth(collection: object, index: int, not_found: object = _MISSING) -> object:
213
+ """Return an indexed item, or use the explicitly supplied fallback.
214
+
215
+ Clojure distinguishes ``(nth coll index)`` from
216
+ ``(nth coll index not-found)`` even when ``not-found`` is nil. Keep that
217
+ distinction at the Python boundary instead of silently turning an invalid
218
+ two-argument lookup into ``None``.
219
+ """
220
+
221
+ if not isinstance(index, int) or isinstance(index, bool):
222
+ raise TypeError("nth index must be an integer")
223
+ if index < 0:
224
+ if not_found is _MISSING:
225
+ raise IndexError("nth index is out of range")
226
+ return not_found
227
+ for position, value in enumerate(_iter_or_empty(collection)):
228
+ if position == index:
229
+ return value
230
+ if not_found is _MISSING:
231
+ raise IndexError("nth index is out of range")
232
+ return not_found
233
+
234
+
235
+ def seq(collection: object) -> Optional[object]:
236
+ """Return a non-empty sequence or nil."""
237
+
238
+ iterator = _iter_or_empty(collection)
239
+ try:
240
+ first_value = _builtins.next(iterator)
241
+ except StopIteration:
242
+ return None
243
+ return cons(first_value, iterator)
244
+
245
+
246
+ def empty(collection: object) -> object:
247
+ """Return an empty value with the broad shape of ``collection``."""
248
+
249
+ if isinstance(collection, tuple):
250
+ return ()
251
+ if isinstance(collection, set):
252
+ return set()
253
+ if isinstance(collection, dict):
254
+ return {}
255
+ if isinstance(collection, str):
256
+ return ""
257
+ if isinstance(collection, bytes):
258
+ return b""
259
+ return []
@@ -0,0 +1,255 @@
1
+ """Eager collection, reduction, loop, and trampoline operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins as _builtins
6
+ from collections.abc import Callable, Iterable
7
+ from typing import Generic, Iterator, TypeVar, Union
8
+
9
+ from ._sequence_core import _LazySeq, _iter_or_empty, empty_q, lazy_seq
10
+ from .control import truthy
11
+
12
+ _Result = TypeVar("_Result")
13
+
14
+ def nonempty(collection: object) -> bool:
15
+ """Test a collection without consuming memoized lazy values."""
16
+
17
+ return not empty_q(collection)
18
+
19
+
20
+ class _ForStop:
21
+ """Private nearest-collection stop token used by ``for :while``."""
22
+
23
+ __slots__ = ()
24
+
25
+
26
+ _FOR_STOP = _ForStop()
27
+
28
+
29
+ def for_stop() -> _ForStop:
30
+ """Return the private stop token consumed by collection control helpers."""
31
+
32
+ return _FOR_STOP
33
+
34
+
35
+ def doseq(
36
+ function: Callable[[object], object], collection: Iterable[object]
37
+ ) -> None:
38
+ """Invoke ``function`` in order without materializing callback results."""
39
+
40
+ for value in _iter_or_empty(collection):
41
+ if function(value) is _FOR_STOP:
42
+ break
43
+
44
+
45
+ def _map_values(
46
+ function: Callable[..., _Result], collections: tuple[Iterable[object], ...]
47
+ ) -> Iterator[_Result]:
48
+ return _builtins.map(
49
+ function, *(_iter_or_empty(collection) for collection in collections)
50
+ )
51
+
52
+
53
+ def mapv(
54
+ function: Callable[..., _Result], *collections: Iterable[object]
55
+ ) -> tuple[_Result, ...]:
56
+ """Apply ``function`` lazily through ``map`` and materialize a vector."""
57
+
58
+ return tuple(_map_values(function, collections))
59
+
60
+
61
+ def map(
62
+ function: Callable[..., _Result], *collections: Iterable[object]
63
+ ) -> list[_Result]:
64
+ """Apply ``function`` in order and materialize a Lisp list.
65
+
66
+ ``mapv`` is the explicit tuple/vector variant. Keeping this helper
67
+ materialized gives the typed core a deterministic ``List`` result while
68
+ ``lazy_seq`` remains available when a caller needs deferred production.
69
+ """
70
+
71
+ return list(_map_values(function, collections))
72
+
73
+
74
+ def mapcatv(
75
+ function: Callable[..., Iterable[object]], *collections: Iterable[object]
76
+ ) -> tuple[object, ...]:
77
+ """Map one or more collections and concatenate each returned sequence.
78
+
79
+ Like Clojure's ``mapcat``, traversal stops at the shortest input and the
80
+ callback receives one value from each collection.
81
+ """
82
+
83
+ result: list[object] = []
84
+ iterators = tuple(_iter_or_empty(collection) for collection in collections)
85
+ for values in zip(*iterators):
86
+ mapped = function(*values)
87
+ if mapped is _FOR_STOP:
88
+ break
89
+ result.extend(_iter_or_empty(mapped))
90
+ return tuple(result)
91
+
92
+
93
+ def mapcat(
94
+ function: Callable[..., Iterable[object]], *collections: Iterable[object]
95
+ ) -> list[object]:
96
+ """List variant of :func:`mapcatv`."""
97
+
98
+ return list(mapcatv(function, *collections))
99
+
100
+
101
+ def _filter_values(
102
+ predicate: Callable[[object], object], collection: Iterable[object]
103
+ ) -> Iterator[object]:
104
+ return (
105
+ value
106
+ for value in _iter_or_empty(collection)
107
+ if truthy(predicate(value))
108
+ )
109
+
110
+
111
+ def filterv(
112
+ predicate: Callable[[object], bool], collection: Iterable[object]
113
+ ) -> tuple[object, ...]:
114
+ """Return the values satisfying ``predicate`` as a vector."""
115
+
116
+ return tuple(_filter_values(predicate, collection))
117
+
118
+
119
+ def filter(
120
+ predicate: Callable[[object], bool], collection: Iterable[object]
121
+ ) -> list[object]:
122
+ """Return the values satisfying ``predicate`` as a Lisp list."""
123
+
124
+ return list(_filter_values(predicate, collection))
125
+
126
+
127
+ def removev(
128
+ predicate: Callable[[object], object], collection: object
129
+ ) -> tuple[object, ...]:
130
+ """Vector form of ``remove`` using Clojure truthiness."""
131
+
132
+ return tuple(value for value in _iter_or_empty(collection) if not truthy(predicate(value)))
133
+
134
+
135
+ def remove(
136
+ predicate: Callable[[object], object], collection: object
137
+ ) -> _LazySeq:
138
+ """Lazily retain values for which ``predicate`` is false/nil."""
139
+
140
+ def produce() -> Iterator[object]:
141
+ for value in _iter_or_empty(collection):
142
+ if not truthy(predicate(value)):
143
+ yield value
144
+
145
+ return lazy_seq(produce)
146
+
147
+
148
+ class Reduced(Generic[_Result]):
149
+ """Value marker returned by a reducing callback to stop traversal."""
150
+
151
+ __slots__ = ("value",)
152
+
153
+ def __init__(self, value: _Result) -> None:
154
+ self.value = value
155
+
156
+
157
+ def reduced(value: _Result) -> Reduced[_Result]:
158
+ """Wrap ``value`` as an early result for ``reduce`` or ``fold``."""
159
+
160
+ return Reduced(value)
161
+
162
+
163
+ def reduced_p(value: object) -> bool:
164
+ """Return whether ``value`` carries the reduction stop marker."""
165
+
166
+ return isinstance(value, Reduced)
167
+
168
+
169
+ def unreduced(value: Union[_Result, Reduced[_Result]]) -> _Result:
170
+ """Remove one reduction marker, leaving ordinary values unchanged."""
171
+
172
+ if isinstance(value, Reduced):
173
+ return value.value
174
+ return value
175
+
176
+
177
+ def _reduce_with_initial(
178
+ function: Callable[[object, object], object],
179
+ initial: object,
180
+ collection: Iterable[object],
181
+ ) -> object:
182
+ accumulator = initial
183
+ for item in collection:
184
+ accumulator = function(accumulator, item)
185
+ if isinstance(accumulator, Reduced):
186
+ return accumulator.value
187
+ return accumulator
188
+
189
+
190
+ def reduce(function: Callable[[object, object], object], *arguments: object) -> object:
191
+ """Reduce ``collection`` with an optional initial accumulator."""
192
+
193
+ if len(arguments) == 1:
194
+ iterator = _iter_or_empty(arguments[0])
195
+ try:
196
+ initial = _builtins.next(iterator)
197
+ except StopIteration:
198
+ raise TypeError("reduce() of empty iterable with no initial value") from None
199
+ return _reduce_with_initial(function, initial, iterator)
200
+ if len(arguments) == 2:
201
+ return _reduce_with_initial(
202
+ function,
203
+ arguments[0],
204
+ _iter_or_empty(arguments[1]),
205
+ )
206
+ raise TypeError("reduce expects a function, collection, and optional initial value")
207
+
208
+
209
+ def fold(
210
+ function: Callable[[object, object], object],
211
+ initial: object,
212
+ collection: Iterable[object],
213
+ ) -> object:
214
+ """Named initial-value form of ``reduce``."""
215
+
216
+ return _reduce_with_initial(function, initial, collection)
217
+
218
+
219
+ class _RecurSignal:
220
+ """Private value used by ``loop`` to carry the next state tuple."""
221
+
222
+ __slots__ = ("values",)
223
+
224
+ def __init__(self, values: tuple[object, ...]) -> None:
225
+ self.values = values
226
+
227
+
228
+ def recur(*values: object) -> _RecurSignal:
229
+ """Return a control token consumed by :func:`loop`."""
230
+
231
+ return _RecurSignal(tuple(values))
232
+
233
+
234
+ def loop(function: Callable[..., _Result], *initial: object) -> _Result:
235
+ """Run a Clojure-style state loop without growing the Python stack."""
236
+
237
+ state = tuple(initial)
238
+ while True:
239
+ result = function(*state)
240
+ if not isinstance(result, _RecurSignal):
241
+ return result
242
+ if len(result.values) != len(state):
243
+ raise TypeError(
244
+ "recur must supply the same number of values as loop bindings"
245
+ )
246
+ state = result.values
247
+
248
+
249
+ def trampoline(function: Callable[..., object], *arguments: object) -> object:
250
+ """Invoke a function, then bounce through zero-argument callables."""
251
+
252
+ result = function(*arguments)
253
+ while callable(result):
254
+ result = result()
255
+ return result