effecton 0.1.0__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.
- effecton/__init__.py +91 -0
- effecton/attempt.py +24 -0
- effecton/effect.py +155 -0
- effecton/exit.py +21 -0
- effecton/gen.py +47 -0
- effecton/implicit_requirement.py +48 -0
- effecton/provide.py +60 -0
- effecton/py.typed +0 -0
- effecton/run_sync.py +161 -0
- effecton/std/__init__.py +1 -0
- effecton/std/logger.py +214 -0
- effecton/std/pretty_logger.py +119 -0
- effecton/std/scope.py +58 -0
- effecton/suspend.py +49 -0
- effecton-0.1.0.dist-info/METADATA +307 -0
- effecton-0.1.0.dist-info/RECORD +18 -0
- effecton-0.1.0.dist-info/WHEEL +4 -0
- effecton-0.1.0.dist-info/licenses/LICENSE +9 -0
effecton/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from effecton.attempt import attempt
|
|
2
|
+
from effecton.effect import (
|
|
3
|
+
Cause,
|
|
4
|
+
Die,
|
|
5
|
+
Effect,
|
|
6
|
+
EffectonError,
|
|
7
|
+
Fail,
|
|
8
|
+
die,
|
|
9
|
+
fail,
|
|
10
|
+
require,
|
|
11
|
+
success,
|
|
12
|
+
sync,
|
|
13
|
+
)
|
|
14
|
+
from effecton.exit import Exit, Failure, Succeeded
|
|
15
|
+
from effecton.gen import EffectGen, gen
|
|
16
|
+
from effecton.implicit_requirement import (
|
|
17
|
+
ImplicitRequirement,
|
|
18
|
+
provide_implicit,
|
|
19
|
+
require_implicit,
|
|
20
|
+
)
|
|
21
|
+
from effecton.provide import RequirementProvider
|
|
22
|
+
from effecton.run_sync import MissingRequirement, run_sync
|
|
23
|
+
from effecton.std.logger import (
|
|
24
|
+
CurrentLogAnnotations,
|
|
25
|
+
CurrentLoggers,
|
|
26
|
+
CurrentLogLevel,
|
|
27
|
+
EffectonLogger,
|
|
28
|
+
LogData,
|
|
29
|
+
LogLevel,
|
|
30
|
+
MinimumLogLevel,
|
|
31
|
+
Severity,
|
|
32
|
+
annotate_logs,
|
|
33
|
+
log,
|
|
34
|
+
log_debug,
|
|
35
|
+
log_error,
|
|
36
|
+
log_fatal,
|
|
37
|
+
log_info,
|
|
38
|
+
log_trace,
|
|
39
|
+
log_warning,
|
|
40
|
+
)
|
|
41
|
+
from effecton.std.pretty_logger import PrettyFormatter, pretty_logger
|
|
42
|
+
from effecton.std.scope import Scope, acquire_and_release, add_finalizer, scoped
|
|
43
|
+
from effecton.suspend import suspend
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Cause",
|
|
47
|
+
"CurrentLogAnnotations",
|
|
48
|
+
"CurrentLogLevel",
|
|
49
|
+
"CurrentLoggers",
|
|
50
|
+
"Die",
|
|
51
|
+
"Effect",
|
|
52
|
+
"EffectGen",
|
|
53
|
+
"EffectonError",
|
|
54
|
+
"EffectonLogger",
|
|
55
|
+
"Exit",
|
|
56
|
+
"Fail",
|
|
57
|
+
"Failure",
|
|
58
|
+
"ImplicitRequirement",
|
|
59
|
+
"LogData",
|
|
60
|
+
"LogLevel",
|
|
61
|
+
"MinimumLogLevel",
|
|
62
|
+
"MissingRequirement",
|
|
63
|
+
"PrettyFormatter",
|
|
64
|
+
"RequirementProvider",
|
|
65
|
+
"Scope",
|
|
66
|
+
"Severity",
|
|
67
|
+
"Succeeded",
|
|
68
|
+
"acquire_and_release",
|
|
69
|
+
"add_finalizer",
|
|
70
|
+
"annotate_logs",
|
|
71
|
+
"attempt",
|
|
72
|
+
"die",
|
|
73
|
+
"fail",
|
|
74
|
+
"gen",
|
|
75
|
+
"log",
|
|
76
|
+
"log_debug",
|
|
77
|
+
"log_error",
|
|
78
|
+
"log_fatal",
|
|
79
|
+
"log_info",
|
|
80
|
+
"log_trace",
|
|
81
|
+
"log_warning",
|
|
82
|
+
"pretty_logger",
|
|
83
|
+
"provide_implicit",
|
|
84
|
+
"require",
|
|
85
|
+
"require_implicit",
|
|
86
|
+
"run_sync",
|
|
87
|
+
"scoped",
|
|
88
|
+
"success",
|
|
89
|
+
"suspend",
|
|
90
|
+
"sync",
|
|
91
|
+
]
|
effecton/attempt.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from effecton.effect import Effect, EffectonError, fail, success
|
|
4
|
+
from effecton.suspend import suspend
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def attempt[A, E: EffectonError](
|
|
8
|
+
thunk: Callable[[], A], on_error: Callable[[Exception], E]
|
|
9
|
+
) -> Effect[A, E]:
|
|
10
|
+
"""Run an exception-throwing thunk lazily, with typed failures.
|
|
11
|
+
|
|
12
|
+
The thunk runs once per run of the effect, like sync. When it raises,
|
|
13
|
+
on_error maps the exception into the typed error channel — under sync,
|
|
14
|
+
every exception becomes an uncatchable defect. To keep an unexpected
|
|
15
|
+
exception a defect, re-raise it from on_error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def go() -> Effect[A, E]:
|
|
19
|
+
try:
|
|
20
|
+
return success(thunk())
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return fail(on_error(e))
|
|
23
|
+
|
|
24
|
+
return suspend(go)
|
effecton/effect.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from collections.abc import Callable, Generator
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Generic, Literal, Never, TypeVar, final
|
|
4
|
+
|
|
5
|
+
from typing_extensions import TypeForm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class EffectonError(Exception):
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
# The dataclass __init__ never fills Exception.args, so the
|
|
12
|
+
# inherited __str__ renders every error as an empty string.
|
|
13
|
+
return repr(self)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@final
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Fail[E: EffectonError]:
|
|
19
|
+
error: E
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@final
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Die:
|
|
25
|
+
defect: Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
type Cause[E] = Fail[E] | Die
|
|
29
|
+
|
|
30
|
+
# Variance is declared through old-style TypeVars: mypy's PEP 695
|
|
31
|
+
# inference wrongly makes A, E, and R invariant once the class has a
|
|
32
|
+
# third type parameter. This might be unnecessary once we switch to ty.
|
|
33
|
+
A = TypeVar("A", covariant=True)
|
|
34
|
+
E = TypeVar("E", bound=EffectonError, covariant=True, default=Never)
|
|
35
|
+
R = TypeVar("R", covariant=True, default=Never)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Effect(Generic[A, E, R]):
|
|
39
|
+
def flat_map[B, E2: EffectonError, R2](
|
|
40
|
+
self, f: Callable[[A], Effect[B, E2, R2]]
|
|
41
|
+
) -> Effect[B, E | E2, R | R2]:
|
|
42
|
+
return FlatMap(self, f)
|
|
43
|
+
|
|
44
|
+
def map[B](self, f: Callable[[A], B]) -> Effect[B, E, R]:
|
|
45
|
+
return self.flat_map(lambda a: Success(f(a))) # type: ignore[misc]
|
|
46
|
+
|
|
47
|
+
def catch_all[B, E2: EffectonError, R2](
|
|
48
|
+
self, f: Callable[[E], Effect[B, E2, R2]]
|
|
49
|
+
) -> Effect[A | B, E2, R | R2]:
|
|
50
|
+
return OnFailure(self, f)
|
|
51
|
+
|
|
52
|
+
def on_exit[R2](self, finalizer: Effect[Any, Never, R2]) -> Effect[A, E, R | R2]:
|
|
53
|
+
return OnExit(self, finalizer)
|
|
54
|
+
|
|
55
|
+
def __iter__(self) -> Generator[Effect[A, E, R], A, A]:
|
|
56
|
+
"""Make ``x = yield from effect`` infer ``x`` as A inside @gen.
|
|
57
|
+
|
|
58
|
+
A bare ``yield`` types as the generator's single send type (Any).
|
|
59
|
+
``yield from`` takes this method's return type parameter instead,
|
|
60
|
+
so the value the interpreter sends back is typed per expression.
|
|
61
|
+
"""
|
|
62
|
+
return (yield self)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@final
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Success[A](Effect[A]):
|
|
68
|
+
value: A
|
|
69
|
+
kind: Literal["success"] = "success"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@final
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class Sync[A](Effect[A]):
|
|
75
|
+
fn: Callable[[], A]
|
|
76
|
+
kind: Literal["sync"] = "sync"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@final
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class FailCause[E: EffectonError](Effect[Never, E]):
|
|
82
|
+
cause: Cause[E]
|
|
83
|
+
kind: Literal["fail"] = "fail"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@final
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class FlatMap[B, E: EffectonError, R](Effect[B, E, R]):
|
|
89
|
+
first: Effect[Any, Any, Any]
|
|
90
|
+
and_then: Callable[[Any], Effect[B, E, R]]
|
|
91
|
+
kind: Literal["flat_map"] = "flat_map"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@final
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class OnFailure[A, E: EffectonError, R](Effect[A, E, R]):
|
|
97
|
+
first: Effect[A, Any, Any]
|
|
98
|
+
handler: Callable[[Any], Effect[A, E, R]]
|
|
99
|
+
kind: Literal["on_failure"] = "on_failure"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@final
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class Require[R](Effect[R, Never, R]):
|
|
105
|
+
requirement_type: TypeForm[R]
|
|
106
|
+
kind: Literal["require"] = "require"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@final
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class ProvideRequirement[A, E: EffectonError, R](Effect[A, E, R]):
|
|
112
|
+
first: Effect[A, E, Any]
|
|
113
|
+
requirement_type: TypeForm[Any]
|
|
114
|
+
requirement_impl: Any
|
|
115
|
+
kind: Literal["provide_requirement"] = "provide_requirement"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@final
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class OnExit[A, E: EffectonError, R](Effect[A, E, R]):
|
|
121
|
+
first: Effect[Any, Any, Any]
|
|
122
|
+
finalizer: Effect[Any, Any, Any]
|
|
123
|
+
kind: Literal["on_exit"] = "on_exit"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
Node = (
|
|
127
|
+
Success[Any]
|
|
128
|
+
| Sync[Any]
|
|
129
|
+
| FailCause[Any]
|
|
130
|
+
| FlatMap[Any, Any, Any]
|
|
131
|
+
| OnFailure[Any, Any, Any]
|
|
132
|
+
| Require[Any]
|
|
133
|
+
| ProvideRequirement[Any, Any, Any]
|
|
134
|
+
| OnExit[Any, Any, Any]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def success[A](value: A) -> Effect[A]:
|
|
139
|
+
return Success(value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def sync[A](fn: Callable[[], A]) -> Effect[A]:
|
|
143
|
+
return Sync(fn)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def fail[E: EffectonError](error: E) -> Effect[Never, E]:
|
|
147
|
+
return FailCause(cause=Fail(error=error))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def die(defect: Any) -> Effect[Never]: # noqa: ANN401
|
|
151
|
+
return FailCause(cause=Die(defect=defect))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def require[R](requirement_type: TypeForm[R]) -> Effect[R, Never, R]:
|
|
155
|
+
return Require(requirement_type=requirement_type)
|
effecton/exit.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal, Never, final
|
|
3
|
+
|
|
4
|
+
from effecton.effect import Cause, EffectonError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@final
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Succeeded[A]:
|
|
10
|
+
value: A
|
|
11
|
+
kind: Literal["succeeded"] = "succeeded"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@final
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Failure[E: EffectonError = Never]:
|
|
17
|
+
cause: Cause[E]
|
|
18
|
+
kind: Literal["failure"] = "failure"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
type Exit[A, E: EffectonError = Never] = Succeeded[A] | Failure[E]
|
effecton/gen.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections.abc import Callable, Generator
|
|
3
|
+
from typing import Any, Never, cast
|
|
4
|
+
|
|
5
|
+
from effecton.effect import Effect, EffectonError, success
|
|
6
|
+
from effecton.suspend import suspend
|
|
7
|
+
|
|
8
|
+
type EffectGen[A, E: EffectonError = Never, R = Never] = Generator[
|
|
9
|
+
Effect[Any, E, R], Any, A
|
|
10
|
+
]
|
|
11
|
+
"""Return type for @gen generator functions: yields effects, returns A."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def gen[**P, A2, E2: EffectonError, R2](
|
|
15
|
+
f: Callable[P, EffectGen[A2, E2, R2]],
|
|
16
|
+
) -> Callable[P, Effect[A2, E2, R2]]:
|
|
17
|
+
"""Turn a generator function into a factory of effects.
|
|
18
|
+
|
|
19
|
+
The interpreter runs each yielded effect and sends its result back;
|
|
20
|
+
the generator's return value becomes the effect's success value.
|
|
21
|
+
Use ``x = yield from effect`` instead of ``x = yield effect`` so the
|
|
22
|
+
type of ``x`` is inferred correctly.
|
|
23
|
+
The error and requirement channels flow from the annotated yield type
|
|
24
|
+
either way.
|
|
25
|
+
|
|
26
|
+
Each run creates a fresh generator, so the returned effect is a
|
|
27
|
+
reusable value. A failing yielded effect abandons the generator:
|
|
28
|
+
``try/except`` around a ``yield`` never observes effect failures (use
|
|
29
|
+
catch_all), and ``finally`` blocks run only when the abandoned
|
|
30
|
+
generator is garbage collected.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@functools.wraps(f)
|
|
34
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Effect[A2, E2, R2]:
|
|
35
|
+
return suspend(lambda: _step(f(*args, **kwargs), None))
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _step[A2, E2: EffectonError, R2](
|
|
41
|
+
g: EffectGen[A2, E2, R2], to_send: object
|
|
42
|
+
) -> Effect[A2, E2, R2]:
|
|
43
|
+
try:
|
|
44
|
+
yielded = g.send(to_send)
|
|
45
|
+
except StopIteration as e:
|
|
46
|
+
return cast("Effect[A2, E2, R2]", success(e.value))
|
|
47
|
+
return yielded.flat_map(lambda v: _step(g, v))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Protocol, Self, cast, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from effecton.effect import Effect, EffectonError, ProvideRequirement, Require
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class ImplicitRequirement(Protocol):
|
|
8
|
+
"""A requirement that carries its own default value.
|
|
9
|
+
|
|
10
|
+
Extend it explicitly — ``class Foo(ImplicitRequirement)`` — so mypy
|
|
11
|
+
rejects the definition site when ``default()`` is missing or
|
|
12
|
+
mis-typed. As a Protocol, any class with a matching ``default()``
|
|
13
|
+
classmethod also qualifies structurally, but explicit inheritance is
|
|
14
|
+
the convention. Requiring one never enters the R channel: the
|
|
15
|
+
interpreter falls back to the memoized default on a miss, and
|
|
16
|
+
ordinary provision still overrides it.
|
|
17
|
+
|
|
18
|
+
The default is computed once per process and shared by every later
|
|
19
|
+
interpretation, so it must be an immutable value.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def default(cls) -> Self: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Implicits are lazily initialized. Once initialized, the default is
|
|
27
|
+
# shared across all run_sync executions.
|
|
28
|
+
_implicit_defaults: dict[type[ImplicitRequirement], Any] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_default[S: ImplicitRequirement](requirement_type: type[S]) -> S:
|
|
32
|
+
if requirement_type not in _implicit_defaults:
|
|
33
|
+
_implicit_defaults[requirement_type] = requirement_type.default()
|
|
34
|
+
return cast("S", _implicit_defaults[requirement_type])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def require_implicit[S: ImplicitRequirement](
|
|
38
|
+
requirement_type: type[S],
|
|
39
|
+
) -> Effect[S]:
|
|
40
|
+
return cast("Effect[S]", Require(requirement_type=requirement_type))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def provide_implicit[S: ImplicitRequirement, A, E: EffectonError, R](
|
|
44
|
+
effect: Effect[A, E, R], value: S
|
|
45
|
+
) -> Effect[A, E, R]:
|
|
46
|
+
return ProvideRequirement(
|
|
47
|
+
first=effect, requirement_type=type(value), requirement_impl=value
|
|
48
|
+
)
|
effecton/provide.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Never, cast, final
|
|
3
|
+
|
|
4
|
+
from typing_extensions import TypeForm
|
|
5
|
+
|
|
6
|
+
from effecton.effect import Effect, EffectonError, ProvideRequirement
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from effecton.std.scope import Scope
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@final
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class RequirementProvider[R = Never]:
|
|
15
|
+
"""Accumulates provided requirements, then discharges them in one apply.
|
|
16
|
+
|
|
17
|
+
mypy limitations dictate this design: all requirements must be
|
|
18
|
+
provided at once; otherwise the remaining requirements are not
|
|
19
|
+
subtracted correctly and collapse to object.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_links: tuple[tuple[TypeForm[Any], Any], ...] = ()
|
|
23
|
+
|
|
24
|
+
def and_provide[R2](
|
|
25
|
+
self, requirement_type: TypeForm[R2]
|
|
26
|
+
) -> ChainedRequirementBinder[R, R2]:
|
|
27
|
+
return ChainedRequirementBinder(requirement_type=requirement_type, _rest=self)
|
|
28
|
+
|
|
29
|
+
def and_scoped[A, E: EffectonError](
|
|
30
|
+
self, effect: Effect[A, E, Scope | R]
|
|
31
|
+
) -> Effect[A, E]:
|
|
32
|
+
"""Discharge the chain plus a Scope created fresh per interpretation."""
|
|
33
|
+
from effecton.std.scope import scoped
|
|
34
|
+
|
|
35
|
+
return self.apply(scoped(effect))
|
|
36
|
+
|
|
37
|
+
def apply[A, E: EffectonError](self, effect: Effect[A, E, R]) -> Effect[A, E]:
|
|
38
|
+
result: Effect[A, E, Any] = effect
|
|
39
|
+
for requirement_type, requirement_impl in self._links:
|
|
40
|
+
result = ProvideRequirement(
|
|
41
|
+
first=result,
|
|
42
|
+
requirement_type=requirement_type,
|
|
43
|
+
requirement_impl=requirement_impl,
|
|
44
|
+
)
|
|
45
|
+
return cast("Effect[A, E]", result)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@final
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ChainedRequirementBinder[R, R2]:
|
|
51
|
+
requirement_type: TypeForm[R2]
|
|
52
|
+
_rest: RequirementProvider[Any]
|
|
53
|
+
|
|
54
|
+
def __call__(self, requirement_impl: R2) -> RequirementProvider[R | R2]:
|
|
55
|
+
return cast(
|
|
56
|
+
"RequirementProvider[R | R2]",
|
|
57
|
+
RequirementProvider(
|
|
58
|
+
_links=(*self._rest._links, (self.requirement_type, requirement_impl))
|
|
59
|
+
),
|
|
60
|
+
)
|
effecton/py.typed
ADDED
|
File without changes
|
effecton/run_sync.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, assert_never, final
|
|
4
|
+
|
|
5
|
+
from typing_extensions import TypeForm
|
|
6
|
+
|
|
7
|
+
from effecton.effect import (
|
|
8
|
+
Die,
|
|
9
|
+
Effect,
|
|
10
|
+
EffectonError,
|
|
11
|
+
FailCause,
|
|
12
|
+
FlatMap,
|
|
13
|
+
Node,
|
|
14
|
+
OnExit,
|
|
15
|
+
OnFailure,
|
|
16
|
+
ProvideRequirement,
|
|
17
|
+
Require,
|
|
18
|
+
Success,
|
|
19
|
+
Sync,
|
|
20
|
+
)
|
|
21
|
+
from effecton.exit import Exit, Failure, Succeeded
|
|
22
|
+
from effecton.implicit_requirement import ImplicitRequirement, resolve_default
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@final
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class MissingRequirement:
|
|
28
|
+
"""Defect raised when an unprovided requirement is requested at runtime.
|
|
29
|
+
|
|
30
|
+
Unreachable through fully typed code.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
requirement_type: TypeForm[Any]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@final
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class RestoreEnv:
|
|
39
|
+
"""Interpreter stack frame delimiting a ProvideRequirement scope."""
|
|
40
|
+
|
|
41
|
+
env: dict[TypeForm[Any], Any]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@final
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class OnExitFrame:
|
|
47
|
+
finalizer: Effect[Any, Any, Any]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Frame = FlatMap[Any, Any, Any] | OnFailure[Any, Any, Any] | RestoreEnv | OnExitFrame
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_sync[A, E: EffectonError](effect: Effect[A, E]) -> Exit[A, E]:
|
|
54
|
+
stack: list[Frame] = []
|
|
55
|
+
env: dict[TypeForm[Any], Any] = {}
|
|
56
|
+
current: Node = effect # type: ignore[assignment]
|
|
57
|
+
|
|
58
|
+
while True:
|
|
59
|
+
match current:
|
|
60
|
+
case Success(value):
|
|
61
|
+
while stack:
|
|
62
|
+
item = stack.pop()
|
|
63
|
+
|
|
64
|
+
match item:
|
|
65
|
+
case RestoreEnv():
|
|
66
|
+
env = item.env
|
|
67
|
+
case OnExitFrame(finalizer):
|
|
68
|
+
current = finalizer.flat_map(_resume(current)) # type: ignore[assignment]
|
|
69
|
+
break
|
|
70
|
+
case FlatMap():
|
|
71
|
+
current = _run_fn_or_die(item.and_then, value)
|
|
72
|
+
break
|
|
73
|
+
case OnFailure():
|
|
74
|
+
continue
|
|
75
|
+
case _:
|
|
76
|
+
assert_never(item)
|
|
77
|
+
else:
|
|
78
|
+
return Succeeded(value=value)
|
|
79
|
+
|
|
80
|
+
case FailCause(cause):
|
|
81
|
+
while stack:
|
|
82
|
+
item = stack.pop()
|
|
83
|
+
|
|
84
|
+
match item:
|
|
85
|
+
case RestoreEnv():
|
|
86
|
+
env = item.env
|
|
87
|
+
case OnExitFrame(finalizer):
|
|
88
|
+
current = finalizer.flat_map(_resume(current)) # type: ignore[assignment]
|
|
89
|
+
break
|
|
90
|
+
case FlatMap():
|
|
91
|
+
continue
|
|
92
|
+
case OnFailure():
|
|
93
|
+
if not isinstance(cause, Die):
|
|
94
|
+
current = _run_fn_or_die(item.handler, cause.error)
|
|
95
|
+
break
|
|
96
|
+
case _:
|
|
97
|
+
assert_never(item)
|
|
98
|
+
else:
|
|
99
|
+
return Failure(cause=cause)
|
|
100
|
+
|
|
101
|
+
case FlatMap(first):
|
|
102
|
+
stack.append(current)
|
|
103
|
+
current = first # type: ignore[assignment]
|
|
104
|
+
|
|
105
|
+
case OnFailure(first):
|
|
106
|
+
stack.append(current)
|
|
107
|
+
current = first # type: ignore[assignment]
|
|
108
|
+
|
|
109
|
+
case Sync(fn):
|
|
110
|
+
try:
|
|
111
|
+
current = Success(fn())
|
|
112
|
+
except Exception as e:
|
|
113
|
+
current = FailCause(cause=Die(defect=e))
|
|
114
|
+
|
|
115
|
+
case Require(requirement_type):
|
|
116
|
+
if requirement_type in env:
|
|
117
|
+
current = Success(env[requirement_type])
|
|
118
|
+
else:
|
|
119
|
+
current = _default_or_die(requirement_type)
|
|
120
|
+
|
|
121
|
+
case ProvideRequirement(first, requirement_type, requirement_impl):
|
|
122
|
+
stack.append(RestoreEnv(env))
|
|
123
|
+
env = {**env, requirement_type: requirement_impl}
|
|
124
|
+
current = first # type: ignore[assignment]
|
|
125
|
+
|
|
126
|
+
case OnExit(first, finalizer):
|
|
127
|
+
stack.append(OnExitFrame(finalizer))
|
|
128
|
+
current = first # type: ignore[assignment]
|
|
129
|
+
|
|
130
|
+
case _:
|
|
131
|
+
assert_never(current)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _default_or_die(requirement_type: TypeForm[Any]) -> Node:
|
|
135
|
+
if (
|
|
136
|
+
isinstance(requirement_type, type)
|
|
137
|
+
and issubclass(requirement_type, ImplicitRequirement)
|
|
138
|
+
# Exclude the protocol class itself
|
|
139
|
+
and not getattr(requirement_type, "_is_protocol", False)
|
|
140
|
+
):
|
|
141
|
+
try:
|
|
142
|
+
return Success(resolve_default(requirement_type))
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return FailCause(cause=Die(defect=e))
|
|
145
|
+
|
|
146
|
+
return FailCause(cause=Die(defect=MissingRequirement(requirement_type)))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _run_fn_or_die(f: Callable[[Any], Effect[Any, Any]], value: object) -> Node:
|
|
150
|
+
try:
|
|
151
|
+
return f(value) # type: ignore[return-value]
|
|
152
|
+
except Exception as e:
|
|
153
|
+
return FailCause(cause=Die(defect=e))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Captures the current outcome by closure.
|
|
157
|
+
def _resume(outcome: Node) -> Callable[[Any], Effect[Any, Any, Any]]:
|
|
158
|
+
def resume(_: object) -> Effect[Any, Any, Any]:
|
|
159
|
+
return outcome
|
|
160
|
+
|
|
161
|
+
return resume
|
effecton/std/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Standard-library modules built on the effecton kernel."""
|