doeff-hy 0.5.0__tar.gz

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.
Files changed (166) hide show
  1. doeff_hy-0.5.0/.gitignore +142 -0
  2. doeff_hy-0.5.0/LICENSE +21 -0
  3. doeff_hy-0.5.0/PKG-INFO +395 -0
  4. doeff_hy-0.5.0/README.md +378 -0
  5. doeff_hy-0.5.0/architecture.hy +55 -0
  6. doeff_hy-0.5.0/docs/hy-upstream-rewriting-unparse.md +69 -0
  7. doeff_hy-0.5.0/docs/static-check.md +142 -0
  8. doeff_hy-0.5.0/pyproject.toml +56 -0
  9. doeff_hy-0.5.0/src/doeff_hy/__init__.py +46 -0
  10. doeff_hy-0.5.0/src/doeff_hy/ast_unparse.py +85 -0
  11. doeff_hy-0.5.0/src/doeff_hy/binding_forms.py +1369 -0
  12. doeff_hy-0.5.0/src/doeff_hy/check_slot.py +190 -0
  13. doeff_hy-0.5.0/src/doeff_hy/clause_endings.hy +230 -0
  14. doeff_hy-0.5.0/src/doeff_hy/clause_ops.hy +172 -0
  15. doeff_hy-0.5.0/src/doeff_hy/conductor.hy +166 -0
  16. doeff_hy-0.5.0/src/doeff_hy/declarations.hy +450 -0
  17. doeff_hy-0.5.0/src/doeff_hy/declarations.pyi +58 -0
  18. doeff_hy-0.5.0/src/doeff_hy/env_places.py +78 -0
  19. doeff_hy-0.5.0/src/doeff_hy/expansion_names.py +164 -0
  20. doeff_hy-0.5.0/src/doeff_hy/frozen.hy +202 -0
  21. doeff_hy-0.5.0/src/doeff_hy/frozen.pyi +62 -0
  22. doeff_hy-0.5.0/src/doeff_hy/handle.hy +1042 -0
  23. doeff_hy-0.5.0/src/doeff_hy/http.hy +99 -0
  24. doeff_hy-0.5.0/src/doeff_hy/http.pyi +57 -0
  25. doeff_hy-0.5.0/src/doeff_hy/json_value.py +167 -0
  26. doeff_hy-0.5.0/src/doeff_hy/lazy.py +139 -0
  27. doeff_hy-0.5.0/src/doeff_hy/light_stack.py +41 -0
  28. doeff_hy-0.5.0/src/doeff_hy/macros.hy +2826 -0
  29. doeff_hy-0.5.0/src/doeff_hy/macros.pyi +57 -0
  30. doeff_hy-0.5.0/src/doeff_hy/match_fields.py +155 -0
  31. doeff_hy-0.5.0/src/doeff_hy/outcome_forms.hy +224 -0
  32. doeff_hy-0.5.0/src/doeff_hy/positions.hy +80 -0
  33. doeff_hy-0.5.0/src/doeff_hy/py.typed +0 -0
  34. doeff_hy-0.5.0/src/doeff_hy/pytest_items.py +386 -0
  35. doeff_hy-0.5.0/src/doeff_hy/pytest_plugin.py +11 -0
  36. doeff_hy-0.5.0/src/doeff_hy/quoted_forms.py +65 -0
  37. doeff_hy-0.5.0/src/doeff_hy/record.hy +593 -0
  38. doeff_hy-0.5.0/src/doeff_hy/record.pyi +18 -0
  39. doeff_hy-0.5.0/src/doeff_hy/record_runtime.py +89 -0
  40. doeff_hy-0.5.0/src/doeff_hy/session.py +21 -0
  41. doeff_hy-0.5.0/src/doeff_hy/session.pyi +7 -0
  42. doeff_hy-0.5.0/src/doeff_hy/sexpr.hy +563 -0
  43. doeff_hy-0.5.0/src/doeff_hy/static_baseline.py +164 -0
  44. doeff_hy-0.5.0/src/doeff_hy/static_cache.py +175 -0
  45. doeff_hy-0.5.0/src/doeff_hy/static_check.py +1385 -0
  46. doeff_hy-0.5.0/src/doeff_hy/static_stub.py +868 -0
  47. doeff_hy-0.5.0/src/doeff_hy/static_types.py +1 -0
  48. doeff_hy-0.5.0/src/doeff_hy/static_types.pyi +131 -0
  49. doeff_hy-0.5.0/src/doeff_hy/static_view.py +222 -0
  50. doeff_hy-0.5.0/src/doeff_hy/system_form.hy +292 -0
  51. doeff_hy-0.5.0/src/doeff_hy/table.hy +284 -0
  52. doeff_hy-0.5.0/src/doeff_hy/table.pyi +53 -0
  53. doeff_hy-0.5.0/src/doeff_hy/type_forms.py +109 -0
  54. doeff_hy-0.5.0/src/doeff_hy/typecheck.hy +64 -0
  55. doeff_hy-0.5.0/src/doeff_hy/typecheck_handlers.hy +45 -0
  56. doeff_hy-0.5.0/src/doeff_hy/typecheck_handlers.pyi +11 -0
  57. doeff_hy-0.5.0/src/doeff_hy/wire.hy +169 -0
  58. doeff_hy-0.5.0/src/doeff_hy/wire.pyi +62 -0
  59. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/__init__.py +64 -0
  60. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/bound_source.py +962 -0
  61. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/called_code.py +125 -0
  62. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/code_store.py +312 -0
  63. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/dataclass_code.py +79 -0
  64. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/expansion.py +16 -0
  65. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/import_gc.py +98 -0
  66. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/import_scope.py +53 -0
  67. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/loader_hooks.py +759 -0
  68. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/macro_use.py +2296 -0
  69. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard/records.py +540 -0
  70. doeff_hy-0.5.0/src/doeff_hy_bytecode_guard.pth +1 -0
  71. doeff_hy-0.5.0/src/hy-stubs/__init__.pyi +79 -0
  72. doeff_hy-0.5.0/src/hy-stubs/macros.pyi +29 -0
  73. doeff_hy-0.5.0/src/hy-stubs/models.pyi +134 -0
  74. doeff_hy-0.5.0/src/hy-stubs/py.typed +1 -0
  75. doeff_hy-0.5.0/tests/conductor_dsl_macro_script.hy +35 -0
  76. doeff_hy-0.5.0/tests/data/record_field_cases.json +14 -0
  77. doeff_hy-0.5.0/tests/defeffect_shape_deftests.hy +207 -0
  78. doeff_hy-0.5.0/tests/defrecord_check_deftests.hy +215 -0
  79. doeff_hy-0.5.0/tests/defrecord_shape_deftests.hy +183 -0
  80. doeff_hy-0.5.0/tests/defwire_deftests.hy +318 -0
  81. doeff_hy-0.5.0/tests/match_field_names_deftests.hy +125 -0
  82. doeff_hy-0.5.0/tests/multimod/__init__.py +0 -0
  83. doeff_hy-0.5.0/tests/multimod/data_fetch.hy +20 -0
  84. doeff_hy-0.5.0/tests/multimod/effects.hy +15 -0
  85. doeff_hy-0.5.0/tests/multimod/python_layer.py +16 -0
  86. doeff_hy-0.5.0/tests/multimod/signal.hy +17 -0
  87. doeff_hy-0.5.0/tests/test_aliased_generic_contract.py +110 -0
  88. doeff_hy-0.5.0/tests/test_ast_unparse_on_py314.py +124 -0
  89. doeff_hy-0.5.0/tests/test_bound_source_store.py +119 -0
  90. doeff_hy-0.5.0/tests/test_bytecode_guard.py +1133 -0
  91. doeff_hy-0.5.0/tests/test_check_slot_opacity.py +473 -0
  92. doeff_hy-0.5.0/tests/test_code_store_entries.py +204 -0
  93. doeff_hy-0.5.0/tests/test_conductor_dsl.py +17 -0
  94. doeff_hy-0.5.0/tests/test_contract_alias_static_types.py +80 -0
  95. doeff_hy-0.5.0/tests/test_contract_type_params.py +136 -0
  96. doeff_hy-0.5.0/tests/test_dataclass_code_store.py +93 -0
  97. doeff_hy-0.5.0/tests/test_declarations.py +314 -0
  98. doeff_hy-0.5.0/tests/test_declarations_static_types.py +109 -0
  99. doeff_hy-0.5.0/tests/test_defeffect_answer_static_types.py +142 -0
  100. doeff_hy-0.5.0/tests/test_defeffect_shape.py +41 -0
  101. doeff_hy-0.5.0/tests/test_defeffect_type_params.py +153 -0
  102. doeff_hy-0.5.0/tests/test_defenum.py +185 -0
  103. doeff_hy-0.5.0/tests/test_defhandler_effect_type_filter.py +132 -0
  104. doeff_hy-0.5.0/tests/test_defhandler_passed_effects.py +119 -0
  105. doeff_hy-0.5.0/tests/test_defpipeline.hy +106 -0
  106. doeff_hy-0.5.0/tests/test_defrecord_check.py +41 -0
  107. doeff_hy-0.5.0/tests/test_defrecord_shape.py +41 -0
  108. doeff_hy-0.5.0/tests/test_deftest_scans.py +77 -0
  109. doeff_hy-0.5.0/tests/test_defwire.py +39 -0
  110. doeff_hy-0.5.0/tests/test_duplicate_stages.hy +61 -0
  111. doeff_hy-0.5.0/tests/test_env_places_read.py +57 -0
  112. doeff_hy-0.5.0/tests/test_frozen_json_contract.py +92 -0
  113. doeff_hy-0.5.0/tests/test_frozen_refreeze.py +311 -0
  114. doeff_hy-0.5.0/tests/test_frozen_static_types.py +174 -0
  115. doeff_hy-0.5.0/tests/test_generic_type_contract.py +164 -0
  116. doeff_hy-0.5.0/tests/test_gensym_canonical.py +492 -0
  117. doeff_hy-0.5.0/tests/test_helper_imports_table.py +207 -0
  118. doeff_hy-0.5.0/tests/test_hy_internal_names_have_one_home.py +186 -0
  119. doeff_hy-0.5.0/tests/test_hy_stubs.py +115 -0
  120. doeff_hy-0.5.0/tests/test_import_gc.py +169 -0
  121. doeff_hy-0.5.0/tests/test_json_value.py +234 -0
  122. doeff_hy-0.5.0/tests/test_lazy_and_threading.py +678 -0
  123. doeff_hy-0.5.0/tests/test_light_stack.py +124 -0
  124. doeff_hy-0.5.0/tests/test_macro_file_ext_without_stack_walk.py +39 -0
  125. doeff_hy-0.5.0/tests/test_macro_use_called.py +239 -0
  126. doeff_hy-0.5.0/tests/test_macro_use_record.py +1901 -0
  127. doeff_hy-0.5.0/tests/test_match_field_names.py +38 -0
  128. doeff_hy-0.5.0/tests/test_multimod.hy +167 -0
  129. doeff_hy-0.5.0/tests/test_none_type_contract.py +142 -0
  130. doeff_hy-0.5.0/tests/test_pre_check_static_types.py +111 -0
  131. doeff_hy-0.5.0/tests/test_pytest_item_types.py +63 -0
  132. doeff_hy-0.5.0/tests/test_pytest_items.py +137 -0
  133. doeff_hy-0.5.0/tests/test_python_interop.hy +45 -0
  134. doeff_hy-0.5.0/tests/test_quoted_forms.py +76 -0
  135. doeff_hy-0.5.0/tests/test_record_static_types.py +143 -0
  136. doeff_hy-0.5.0/tests/test_runtime_do_spelling.py +56 -0
  137. doeff_hy-0.5.0/tests/test_sexpr.hy +75 -0
  138. doeff_hy-0.5.0/tests/test_source_positions.py +177 -0
  139. doeff_hy-0.5.0/tests/test_stage_of.hy +112 -0
  140. doeff_hy-0.5.0/tests/test_static_cache.py +585 -0
  141. doeff_hy-0.5.0/tests/test_static_check.py +518 -0
  142. doeff_hy-0.5.0/tests/test_static_check_baseline.py +190 -0
  143. doeff_hy-0.5.0/tests/test_static_check_defhandler.py +591 -0
  144. doeff_hy-0.5.0/tests/test_static_check_deftest.py +171 -0
  145. doeff_hy-0.5.0/tests/test_static_check_effects_reads.py +97 -0
  146. doeff_hy-0.5.0/tests/test_static_check_for_do.py +182 -0
  147. doeff_hy-0.5.0/tests/test_static_check_locate.py +125 -0
  148. doeff_hy-0.5.0/tests/test_static_check_named_paths.py +74 -0
  149. doeff_hy-0.5.0/tests/test_static_check_of_contract.py +92 -0
  150. doeff_hy-0.5.0/tests/test_static_check_parallel.py +196 -0
  151. doeff_hy-0.5.0/tests/test_static_check_post_early_return.py +230 -0
  152. doeff_hy-0.5.0/tests/test_static_check_pyright_python.py +46 -0
  153. doeff_hy-0.5.0/tests/test_static_check_pyright_threads.py +143 -0
  154. doeff_hy-0.5.0/tests/test_static_check_recursive_defk.py +142 -0
  155. doeff_hy-0.5.0/tests/test_static_check_session.py +151 -0
  156. doeff_hy-0.5.0/tests/test_static_check_sibling_imports.py +113 -0
  157. doeff_hy-0.5.0/tests/test_static_check_slot.py +452 -0
  158. doeff_hy-0.5.0/tests/test_static_stub.py +422 -0
  159. doeff_hy-0.5.0/tests/test_system_static_types.py +172 -0
  160. doeff_hy-0.5.0/tests/test_table.py +583 -0
  161. doeff_hy-0.5.0/tests/test_table_static_types.py +103 -0
  162. doeff_hy-0.5.0/tests/test_tail_resume_tco.py +230 -0
  163. doeff_hy-0.5.0/tests/test_type_forms.py +62 -0
  164. doeff_hy-0.5.0/tests/test_val_var_lazy.py +752 -0
  165. doeff_hy-0.5.0/tests/test_wire_static_types.py +193 -0
  166. doeff_hy-0.5.0/tests/test_with_handler_macro.py +49 -0
@@ -0,0 +1,142 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ # macOS debug symbols for the compiled extension above: build output that
7
+ # embeds the builder's absolute paths, so it must not be distributed either.
8
+ *.dSYM/
9
+ .Python
10
+ env/
11
+ venv/
12
+ .venv/
13
+ ENV/
14
+ env.bak/
15
+ venv.bak/
16
+ .env
17
+ .env.*
18
+
19
+ # Distribution / packaging
20
+ .Python
21
+ build/
22
+ develop-eggs/
23
+ dist/
24
+ downloads/
25
+ eggs/
26
+ .eggs/
27
+ lib/
28
+ lib64/
29
+ parts/
30
+ sdist/
31
+ var/
32
+ wheels/
33
+ *.egg-info/
34
+ .installed.cfg
35
+ *.egg
36
+ MANIFEST
37
+
38
+ # Testing
39
+ .pytest_cache/
40
+ .coverage
41
+ .coverage.*
42
+ coverage.xml
43
+ *.cover
44
+ .hypothesis/
45
+ htmlcov/
46
+ .tox/
47
+
48
+ # IDE
49
+ # Generated by Eclipse buildship and machine-specific: embeds the local Java home
50
+ # and toolchain paths. The gradle build does not read it.
51
+ ide-plugins/pycharm/.settings/org.eclipse.buildship.core.prefs
52
+ .idea/
53
+ .vscode/
54
+ *.swp
55
+ *.swo
56
+ *~
57
+ .DS_Store
58
+
59
+ # UV
60
+ !uv.lock
61
+ .venv/
62
+
63
+ # Ruff
64
+ .ruff_cache/
65
+
66
+ # MyPy
67
+ .mypy_cache/
68
+ .dmypy.json
69
+ dmypy.json
70
+
71
+ # Generated logs, traces, and local databases
72
+ .doeff-lint.jsonl
73
+ .doeff-flow/
74
+ *.jsonl
75
+ # 旧い版の coordinator の置き場の log(版上げの下見の検 test_deployed_state_upgrade.hy が読む fixture — 生成された log ではない)
76
+ !packages/doeff-cluster/tests/fixtures/coordinator_state_*/wal/wal.jsonl
77
+ # codex の app-server の実物の行を録った物(行の分類の検 packages/doeff-codex/tests/test_lines.hy が読む fixture — 生成された log ではない)
78
+ !packages/doeff-codex/tests/recorded/codex-*/*.jsonl
79
+ *.db
80
+ *.db-journal
81
+ *.db-shm
82
+ *.db-wal
83
+ *.sqlite
84
+ *.sqlite-journal
85
+ *.sqlite-shm
86
+ *.sqlite-wal
87
+ *.sqlite3
88
+ *.sqlite3-journal
89
+ *.sqlite3-shm
90
+ *.sqlite3-wal
91
+ None
92
+ Untitled
93
+
94
+ # Local agent and browser automation state
95
+ .agent-home/
96
+ .playwright-mcp/
97
+ .claude/settings.local.json
98
+ .claude/skills/install-vscode-plugin/
99
+ .agents/skills/install-vscode-plugin/
100
+
101
+ # Accidental desktop helper bundle captured at repository root
102
+ /server.js
103
+
104
+ # Rust build artifacts
105
+ rust/**/target/
106
+ packages/**/target/
107
+
108
+ # Local deploy keys
109
+ proboscis_doeff_deploy
110
+ proboscis_doeff_deploy.pub
111
+
112
+ # Worktrees and orchestration
113
+ .git-worktrees/
114
+ .orch/
115
+ .sisyphus/
116
+ VAULT
117
+
118
+ # Override global gitignore for examples
119
+ !examples/
120
+ ORCH_PROMPT.md
121
+ .worktrees/
122
+
123
+ # Local workspace artifacts
124
+ # Repo-root scratch directory only. A `python/` directory inside a package
125
+ # (e.g. packages/doeff-indexer/python/) is NOT covered by this rule.
126
+ /python/
127
+
128
+
129
+ # 設計記録は repo に入れる — global の除外(`artifacts` の dir・`*.html`・`*.svg`)を打ち消す
130
+ # (打ち消しは dir を先に戻さないと中身に効かない)
131
+ !docs/design/*/artifacts/
132
+ !docs/design/**
133
+ # 書き込みロックだけは入れない(道具が作る一時 file — 打ち消しの後に置く)
134
+ docs/design/*/.records.lock
135
+
136
+ # Repository-owned Zenn article visuals (override global image ignores)
137
+ !/images/zenn-use-cases-v0/*.png
138
+ # 記事が参照する採用画像は generated/ の下に在る(上の行は直下だけに効き、2026-09-16 の 47 枚は入らなかった)
139
+ !/images/zenn-use-cases-v0/generated/
140
+ !/images/zenn-use-cases-v0/generated/*.png
141
+ # 手で書いた図の正本(global の `*.svg` の除外を打ち消す)
142
+ !/publications/zenn-use-cases-v0/visuals/*.svg
doeff_hy-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 proboscis contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,395 @@
1
+ Metadata-Version: 2.5
2
+ Name: doeff-hy
3
+ Version: 0.5.0
4
+ Summary: Hy macros for writing doeff programs, effects, handlers and tests
5
+ Project-URL: Homepage, https://github.com/proboscis/doeff/tree/main/packages/doeff-hy
6
+ Project-URL: Repository, https://github.com/proboscis/doeff
7
+ Project-URL: Issues, https://github.com/proboscis/doeff/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: doeff-core-effects<0.6,>=0.5.0
12
+ Requires-Dist: doeff-hy-pytest<0.6,>=0.5.0
13
+ Requires-Dist: doeff<0.6,>=0.5.0
14
+ Requires-Dist: hy<=1.3.1,>=1.3.0
15
+ Requires-Dist: pydantic>=2.11
16
+ Description-Content-Type: text/markdown
17
+
18
+ # doeff-hy
19
+
20
+ Macros for writing [doeff](https://github.com/proboscis/doeff) programs, effects,
21
+ handlers and tests in [Hy](https://hylang.org/), a Lisp that compiles to Python.
22
+
23
+ **This README is for** developers who want to write doeff code in Hy, or who read Hy
24
+ code that uses doeff. After reading it you can install doeff-hy, define an effect, a
25
+ function that performs it and a handler for it, call them from Python, test them with
26
+ pytest, and look up each macro.
27
+
28
+ ## What it does
29
+
30
+ doeff-hy gives you `defk`, `defeffect`, `defhandler`, `deftest` and related macros.
31
+ They expand to ordinary doeff code (`@do` generators, `EffectBase` dataclasses,
32
+ handler installers, pytest functions) with runtime contracts (`:pre` / `:post`) on
33
+ arguments and results.
34
+
35
+ Parts of [doeff-core-effects](https://github.com/proboscis/doeff/tree/main/packages/doeff-core-effects)
36
+ are written with these macros, so doeff-hy is installed whenever doeff is.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install doeff # installs doeff-hy
42
+ pip install doeff-hy # the same set of packages
43
+ ```
44
+
45
+ Requires Python 3.11 or newer and Hy 1.3 (doeff-hy pins `hy>=1.3.0,<=1.3.1`, the
46
+ versions its tests cover).
47
+
48
+ ## Quick start
49
+
50
+ `greeting.hy` defines an effect, a function that performs it, and a handler:
51
+
52
+ ```hy file=greeting.hy
53
+ (require doeff-hy.macros [defeffect defk defhandler <-])
54
+
55
+ (defeffect Greeting
56
+ "Ask which word to greet with."
57
+ {:fields [(: lang str)]
58
+ :answer str
59
+ :tags {:context "example" :role "intent"}})
60
+
61
+ (defk greet [name]
62
+ {:pre [(: name str)] :post [(: % str)]}
63
+ (<- word (Greeting :lang "en"))
64
+ (+ word ", " name))
65
+
66
+ (defhandler english
67
+ (Greeting [lang]
68
+ (resume "hello")))
69
+ ```
70
+
71
+ `main.py` imports it from Python and runs it:
72
+
73
+ ```python file=main.py
74
+ import doeff_hy # noqa: F401 - lets Python import .hy / .hyk / .hyp modules
75
+ from doeff import run
76
+ from doeff_core_effects.scheduler import scheduled
77
+
78
+ from greeting import english, greet
79
+
80
+ print(run(scheduled(english(greet("doeff"))))) # hello, doeff
81
+ ```
82
+
83
+ - `(<- word effect)` performs the effect and binds its answer to `word`.
84
+ - `{:pre [(: name str)] :post [(: % str)]}` checks the argument and the result types
85
+ at run time (`%` is the result).
86
+ - `english` is a handler installer: it wraps a program and answers every `Greeting`
87
+ performed inside it with `(resume "hello")`.
88
+
89
+ ## Testing with deftest
90
+
91
+ `deftest` defines a pytest test whose body performs effects. Put the tests in
92
+ `test_greeting.hy`:
93
+
94
+ ```hy file=test_greeting.hy
95
+ (require doeff-hy.macros [deftest <-])
96
+ (import doeff_core_effects [Ask])
97
+ (import greeting [english greet])
98
+
99
+ (deftest test-greet
100
+ (<- text (english (greet "doeff")))
101
+ (assert (= text "hello, doeff")))
102
+
103
+ (deftest test-greet-from-env
104
+ {:env {"name" "world"}}
105
+ (<- name (Ask "name"))
106
+ (<- text (english (greet name)))
107
+ (assert (= text "hello, world")))
108
+ ```
109
+
110
+ A `deftest` function asks pytest for a fixture named `doeff_interpreter` and calls it
111
+ with the test's program (and with `env=` when the test sets `:env`). You decide which
112
+ handlers the tests run under. pytest also needs to be told to collect `test_*.hy`
113
+ files. Both go in `conftest.py`:
114
+
115
+ ```python file=conftest.py
116
+ import doeff_hy # noqa: F401 - lets Python import .hy / .hyk / .hyp modules
117
+ import pytest
118
+ from doeff import run
119
+ from doeff_core_effects.handlers import reader
120
+ from doeff_core_effects.scheduler import scheduled
121
+
122
+
123
+ @pytest.fixture
124
+ def doeff_interpreter():
125
+ """How a deftest body runs: deftest calls this with the test's program."""
126
+
127
+ def interpret(program, env=None):
128
+ return run(scheduled(reader(env=env or {})(program)))
129
+
130
+ return interpret
131
+
132
+
133
+ def pytest_collect_file(file_path, parent):
134
+ """Collect deftest functions from test_*.hy files."""
135
+ if file_path.suffix == ".hy" and file_path.name.startswith("test_"):
136
+ return pytest.Module.from_parent(parent, path=file_path)
137
+ return None
138
+ ```
139
+
140
+ ```bash run
141
+ pytest # 2 passed
142
+ ```
143
+
144
+ [doeff-hy-pytest](https://github.com/proboscis/doeff/tree/main/packages/doeff-hy-pytest),
145
+ installed with doeff-hy, is a pytest plugin that pytest loads by itself. It fixes a
146
+ Hy problem on Python 3.14 and offers optional time limits for these tests.
147
+
148
+ ### deftest options
149
+
150
+ The optional map after the test name (or after its fixture list) takes these keys:
151
+
152
+ | Key | What it does | Example |
153
+ | --- | --- | --- |
154
+ | `:interpreters` | Runs the test once per name; the names are passed to a `doeff_interpreter_name` fixture | `{:interpreters ["fast" "traced"]}` |
155
+ | `:env` | Passed to `doeff_interpreter` as `env=` | `{:env {"name" "world"}}` |
156
+ | `:params` | `pytest.mark.parametrize` for each fixture parameter | `{:params {"day" ["2026-01-01" "2026-02-01"]}}` |
157
+ | `:marks` | Adds pytest marks (`pytest -m "not slow"`) | `{:marks ["slow"]}` |
158
+ | `:skip-if` / `:skip-reason` | Skips when the expression is true; it is evaluated when the module is imported | `{:skip-if (not (shutil.which "git")) :skip-reason "git not installed"}` |
159
+
160
+ With `:interpreters`, resolve the name in `conftest.py`:
161
+
162
+ ```python excerpt
163
+ INTERPRETERS = {
164
+ "fast": fast_interpreter,
165
+ "traced": traced_interpreter,
166
+ }
167
+
168
+
169
+ @pytest.fixture
170
+ def doeff_interpreter(doeff_interpreter_name):
171
+ return INTERPRETERS[doeff_interpreter_name]
172
+ ```
173
+
174
+ Fixture parameters go in a list after the name:
175
+
176
+ ```hy excerpt
177
+ (deftest test-report-for-day [day]
178
+ {:params {"day" ["2026-01-01" "2026-02-01"]}}
179
+ (<- report (daily-report day))
180
+ (assert (> (len report.rows) 0)))
181
+ ```
182
+
183
+ ## File extensions
184
+
185
+ | Extension | Purpose | Allowed macros |
186
+ | --- | --- | --- |
187
+ | `.hy` | General Hy source (handlers, effects, utilities) | All |
188
+ | `.hyk` | Kleisli modules (reusable effectful functions) | `defk`, `deff`, `defhandler`, `deftest` |
189
+ | `.hyp` | Program modules (entry points) | `defp`, `defpp`, `deftest` |
190
+
191
+ `import doeff_hy` registers `.hyk` and `.hyp` as Hy source extensions.
192
+
193
+ ## Macros
194
+
195
+ Import macros with `(require doeff-hy.macros [<names>])`.
196
+
197
+ | Macro | Purpose |
198
+ | --- | --- |
199
+ | `defeffect` | Define an effect type: a frozen dataclass inheriting `EffectBase`, with its fields, answer type and tags |
200
+ | `defk` | Define an effectful function (`@do` + `:pre` / `:post` contracts) |
201
+ | `fnk` | Anonymous effectful function |
202
+ | `deff` | Define a pure function with `:pre` / `:post` contracts |
203
+ | `defhandler` | Define a handler installer from one clause per effect type |
204
+ | `defp` | Define a program constant (`:post` required) |
205
+ | `defpp` | Define a `Program[Program[T]]` constant |
206
+ | `deftest` | Define an effectful pytest test |
207
+ | `do!` | Inline do block: perform effects in sequence |
208
+ | `<-` | Perform an effect and bind its answer: `(<- name effect)`; `(<- name Type effect)` also checks `isinstance` |
209
+ | `!` | Perform an effect in argument position: `(f (! (Ask "x")))` |
210
+ | `on-raise` | Map matching `Raise` reasons to answers: `(on-raise body (Conflict d) answer …)` |
211
+ | `absent-as` | Fold `Absent` into a default: `(absent-as 0 body)` |
212
+ | `defpipeline` | Named-stage pipeline composition |
213
+ | `traverse` | Applicative traverse over collections |
214
+ | `for/do` | Comprehension whose body performs effects |
215
+
216
+ ### Handlers
217
+
218
+ A `defhandler` clause names an effect type and its fields, and must end every path
219
+ with one of `resume`, `transfer`, `finish`, `reperform` or `raise`. `(finish value)`
220
+ ends the handler's scope without resuming; finishing an ordinary effect needs
221
+ `:finish-reason "…"`. Clauses for `Raise` and `Absent` never resume.
222
+
223
+ ### Typed binding
224
+
225
+ `(<- name Type effect)` expands to the bind plus `(assert (isinstance name Type) …)`.
226
+ The expansion has one definition point (`_bind-yield` in `macros.hy`), shared by the
227
+ `<-` macro and by every body that accepts `<-` (`do!`, `defp`, `deftest`, `for/do`,
228
+ `traverse`, `defhandler` clauses), so the type contract holds wherever the bind is
229
+ written. The 2- and 3-element forms add no check.
230
+
231
+ `None` is a type in these positions, as in a Python annotation: `(: % None)`,
232
+ `(<- x None effect)` and `#(int None)` check against `type(None)` at run time
233
+ (`runtime_type_form` in `doeff_hy/type_forms.py` is the one place that maps it).
234
+ `(| int None)` needs no mapping.
235
+
236
+ ### Absence and failure
237
+
238
+ Expected absence and failure are two effects in `doeff_core_effects.effects`:
239
+ `Absent(why)` (the Maybe side) and `Raise(reason)` (the Result side). Code writes only
240
+ the success path; the places that want to *do* something about absence or failure
241
+ fold them into values: `maybe` (`Absent` → `Nothing`) and `result` (`Raise` → `Err`)
242
+ from `doeff_core_effects.outcomes`, and the `on-raise` / `absent-as` macros. The
243
+ nesting order picks the shape: `(result (maybe b))` is `Ok(Some v)` / `Ok(Nothing)` /
244
+ `Err(e)`.
245
+
246
+ `defeffect` may split its answer by meaning — `:absent [Missing] :failure [Unreachable]
247
+ :value [Conflict]`; the rest is success. For such an effect `(<- x (ReadRow …))` binds
248
+ only the success and performs `Absent` / `Raise(answer)` in the caller's scope
249
+ instead; `:absent F` at the end of a bind turns an absence inside that bind into
250
+ `Raise(F)`. `<-` also opens `Ok` / `Err` / `Some` / `Nothing` values. An effect
251
+ without the declaration binds exactly as before (the bind yields the same object).
252
+
253
+ ### Where `!` may appear
254
+
255
+ `(! effect)` is replaced with a `yield` expression at the position where it is
256
+ written. Branches, `and` / `or` short-circuiting, argument order, `try` suites, and
257
+ literal containers therefore keep normal Python evaluation order.
258
+
259
+ Python-native `lfor` / `gfor` / `sfor` / `dfor` comprehensions and plain nested `fn` /
260
+ `defn` bodies cannot host a doeff `!` safely. Macro expansion raises an
261
+ `ADR-DOE-HY-003` `SyntaxError` with a rewrite: use `for/do` plus `<-` for effectful
262
+ iteration, and `fnk` or `defk` for an effectful function.
263
+
264
+ ### Names: val, var, lazy val, lazy var, session val, session var
265
+
266
+ In the bodies of `defk`, `deftest` and `defhandler` clauses, bind names with these
267
+ forms (`doeff-hy-check` warns on `setv` and asks for `val` or `var`):
268
+
269
+ | Form | Meaning |
270
+ | --- | --- |
271
+ | `(val x expr)` | Bind once. Binding the same name again is an error at expansion time |
272
+ | `(var x expr)` | Rebindable. Rebind with `(:= x new-value)` |
273
+ | `(lazy val x expr)` | Evaluated the first time `x` is used in that call, then remembered. Never evaluated if unused. If the first evaluation raises, the next use evaluates again |
274
+ | `(lazy var x expr)` | Like `lazy val`, and rebindable with `:=`. If rebound before first use, `expr` is never evaluated |
275
+ | `(session val x expr)` | Directly under `defhandler` only. Created once, on first use, and shared for the handler's session |
276
+ | `(session var x expr)` | Directly under `defhandler` only. Rebindable state that lives for the handler's session. Rebind with `(:= x v)` |
277
+
278
+ In `prices.hy`:
279
+
280
+ ```hy file=prices.hy
281
+ (require doeff-hy.macros [defeffect defk defhandler <-])
282
+
283
+ (defeffect LoadPrices
284
+ "Load the whole price table."
285
+ {:answer dict
286
+ :tags {:context "example" :role "intent"}})
287
+
288
+ (defk price-of [ticker]
289
+ {:pre [(: ticker str)] :post [(: % float)]}
290
+ (lazy val table (! (LoadPrices))) ; performed only if `table` is used
291
+ (var price 0.0)
292
+ (when (in ticker table)
293
+ (:= price (get table ticker)))
294
+ price)
295
+
296
+ (defhandler price-table
297
+ (session var loads 0) ; counts loads for the whole session
298
+ (LoadPrices []
299
+ (:= loads (+ loads 1))
300
+ (resume {"ACME" 12.5})))
301
+ ```
302
+
303
+ Run from Python:
304
+
305
+ ```python
306
+ import doeff_hy # noqa: F401 - lets Python import .hy / .hyk / .hyp modules
307
+ from doeff import run
308
+ from doeff_core_effects.handlers import state
309
+ from doeff_core_effects.scheduler import scheduled
310
+
311
+ from prices import price_of, price_table
312
+
313
+ print(run(scheduled(state()(price_table(price_of("ACME")))))) # 12.5
314
+ ```
315
+
316
+ - A value expression may be written `!(effect)`. Hy's reader reads `!(f)` as `!` and
317
+ `(f)`; in declarations and `:=` both spellings mean `(! (f))`.
318
+ - `val` forbids rebinding the name only. Changing the value's contents
319
+ (`(setv (get x k) v)`, `(.append x v)`) is allowed. `(<- x effect)` is another way
320
+ to write `val`.
321
+ - A lazy name is referenced by its bare name. It cannot be referenced inside `fn`,
322
+ comprehensions (`lfor` and friends) or `handle` / `defhandler` clauses (they cannot
323
+ perform effects); take it out first with `(val v x)`.
324
+ - Session values are read and written through the state effects (`Get` / `Put`), so
325
+ **install `state()` outside the handler**. The key is
326
+ `doeff_hy.session.session_key(module, handler, name)` =
327
+ `"<module __name__>/<handler name>/<variable name>"`. An outer handler that answers
328
+ `Get` for this key replaces the value without running the initial expression, and
329
+ one that receives `Put` observes the writes.
330
+ - At module top level, `(require doeff-hy.macros [val var lazy])` lets you write
331
+ `(val x expr)`, `(var x expr)` and `(lazy val x expr)`. A top-level `lazy val` takes
332
+ only expressions that perform no effects (a module has no handler; put resources
333
+ built with effects in a `defhandler`'s `session val`). Top-level `(:= …)` does not
334
+ go through a macro in Hy, so rebind a module `var` with a top-level `setv`.
335
+ - The older forms `(lazy …)`, `(lazy-val …)`, `(lazy-var …)` and `(set! …)` keep
336
+ their behaviour in `defhandler` and emit a `DeprecationWarning` at expansion time
337
+ pointing to `session val` / `session var` / `:=` (the keys are the same, so values
338
+ carry over). In `defk` and `deftest` they are expansion-time errors.
339
+ - Rebinding the same name twice with the older forms (two `setv`s, for example) is a
340
+ `doeff-hy-rebind` error from `doeff-hy-check`. Loop variables and mutually
341
+ exclusive `if` branches are not counted.
342
+
343
+ ## Static type checking
344
+
345
+ `doeff-hy-check PATH... --root <repo>` expands Hy sources with the real macros and runs
346
+ [pyright](https://github.com/microsoft/pyright) on the result, reporting errors at
347
+ `.hy` lines. It needs `pyright` on `PATH` (or `--pyright <command>`). Contract types
348
+ (`(: x T)`) become parameter and result annotations, so argument types, missing
349
+ arguments, result types, a forgotten `<-`, and effect constructor arguments are
350
+ checked. What it does not catch yet (effect result types, env / effect sets) and why:
351
+ [docs/static-check.md](https://github.com/proboscis/doeff/blob/main/packages/doeff-hy/docs/static-check.md).
352
+
353
+ ## What installing doeff-hy changes in your environment
354
+
355
+ - **A startup hook.** doeff-hy installs `doeff_hy_bytecode_guard.pth`, which runs at
356
+ every Python start in the environment. It wraps the standard source loader so that a
357
+ Hy module is recompiled when a macro it used has changed (Python's `.pyc` check looks
358
+ only at the source file, not at the macros that expanded it). `import doeff_hy`
359
+ installs the same hook when `.pth` files are skipped (`python -S`).
360
+ - **A compiled-code cache.** Compiled Hy code is also kept in a shared directory:
361
+ `$DOEFF_HY_CODE_STORE`, by default `$XDG_CACHE_HOME/doeff-hy/code-store`
362
+ (`~/.cache/doeff-hy/code-store`). Set `DOEFF_HY_CODE_STORE=off` to turn it off.
363
+ - **Partial Hy type stubs** (`hy-stubs`), so that pyright in strict mode accepts the
364
+ `import hy` lines that Hy's expansion always emits.
365
+
366
+ ## How it fits with the other doeff packages
367
+
368
+ doeff is published as five core packages. `pip install doeff` installs all of them.
369
+
370
+ | Package | Role |
371
+ | --- | --- |
372
+ | [doeff](https://github.com/proboscis/doeff) | The Python API: `@do`, handler installers, `run`, the `doeff` CLI |
373
+ | [doeff-vm](https://github.com/proboscis/doeff/tree/main/packages/doeff-vm) | The Rust VM that executes programs |
374
+ | [doeff-core-effects](https://github.com/proboscis/doeff/tree/main/packages/doeff-core-effects) | The standard effects and their handlers |
375
+ | **doeff-hy** | This package: macros for writing doeff code in Hy |
376
+ | [doeff-hy-pytest](https://github.com/proboscis/doeff/tree/main/packages/doeff-hy-pytest) | pytest plugin for tests written with doeff-hy |
377
+
378
+ Arrows point from a package to a package it depends on:
379
+
380
+ ```text
381
+ doeff ──► doeff-vm
382
+ │
383
+ └─────► doeff-core-effects ──► doeff-hy ──► doeff-hy-pytest
384
+ ```
385
+
386
+ doeff-hy also depends on doeff and doeff-core-effects, so the five packages are
387
+ released together with matching versions.
388
+
389
+ ## License
390
+
391
+ MIT. See [LICENSE](https://github.com/proboscis/doeff/blob/main/LICENSE).
392
+
393
+ Its own dependencies — Hy (with funcparserlib) and pydantic — are MIT-licensed.
394
+ Through doeff-core-effects it also installs frozendict, which is LGPL-3.0; see
395
+ [the doeff-core-effects README](https://github.com/proboscis/doeff/tree/main/packages/doeff-core-effects#licenses-of-dependencies).