holoso 0.1.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 (148) hide show
  1. holoso-0.1.0/PKG-INFO +16 -0
  2. holoso-0.1.0/README.md +107 -0
  3. holoso-0.1.0/holoso/__init__.py +48 -0
  4. holoso-0.1.0/holoso/_api.py +163 -0
  5. holoso-0.1.0/holoso/_backend/__init__.py +4 -0
  6. holoso-0.1.0/holoso/_backend/cocotb.py +195 -0
  7. holoso-0.1.0/holoso/_backend/html/__init__.py +3 -0
  8. holoso-0.1.0/holoso/_backend/html/_html.py +168 -0
  9. holoso-0.1.0/holoso/_backend/html/_schedule.py +851 -0
  10. holoso-0.1.0/holoso/_backend/html/html.css +121 -0
  11. holoso-0.1.0/holoso/_backend/html/html.js +293 -0
  12. holoso-0.1.0/holoso/_backend/numerical.py +366 -0
  13. holoso-0.1.0/holoso/_backend/verilog/__init__.py +3 -0
  14. holoso-0.1.0/holoso/_backend/verilog/_emit.py +905 -0
  15. holoso-0.1.0/holoso/_backend/verilog/_microcode.py +368 -0
  16. holoso-0.1.0/holoso/_backend/verilog/_support.py +79 -0
  17. holoso-0.1.0/holoso/_backend/verilog/rtl/README.md +7 -0
  18. holoso-0.1.0/holoso/_backend/verilog/rtl/holoso_support.vh +26 -0
  19. holoso-0.1.0/holoso/_backend/verilog/rtl/holoso_support_template.v +247 -0
  20. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/README.md +32 -0
  21. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m11.v +103 -0
  22. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m16.v +105 -0
  23. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m18.v +106 -0
  24. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m24.v +109 -0
  25. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m27.v +111 -0
  26. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m32.v +113 -0
  27. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m36.v +115 -0
  28. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m48.v +121 -0
  29. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_cordic_m53.v +124 -0
  30. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m16.v +161 -0
  31. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m18.v +161 -0
  32. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m24.v +353 -0
  33. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m27.v +161 -0
  34. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m32.v +225 -0
  35. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m36.v +353 -0
  36. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m48.v +161 -0
  37. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_exp2_m53.v +225 -0
  38. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m16.v +216 -0
  39. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m18.v +216 -0
  40. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m24.v +487 -0
  41. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m27.v +216 -0
  42. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m32.v +487 -0
  43. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m36.v +849 -0
  44. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m48.v +306 -0
  45. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_tables/_zkf_log2_m53.v +487 -0
  46. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_cordic.v +313 -0
  47. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_div_core.v +276 -0
  48. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_fixed_to_float.v +164 -0
  49. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_horner.v +141 -0
  50. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_normshift.v +281 -0
  51. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_pack.v +226 -0
  52. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_pmul.v +511 -0
  53. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_rshift_sticky.v +197 -0
  54. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/_zkf_to_fixpoint.v +336 -0
  55. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_abs.v +12 -0
  56. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_add.v +520 -0
  57. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_addsub.v +60 -0
  58. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_atan2.v +897 -0
  59. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_cmp.v +71 -0
  60. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_cmp_comb.v +63 -0
  61. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_div.v +123 -0
  62. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_exp2.v +305 -0
  63. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_fma.v +556 -0
  64. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_from_int.v +127 -0
  65. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_is_finite.v +11 -0
  66. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_log2.v +438 -0
  67. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_mul.v +156 -0
  68. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_mul_ilog2_const.v +203 -0
  69. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_neg.v +14 -0
  70. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_pipe.v +54 -0
  71. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_resize.v +195 -0
  72. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_round.v +239 -0
  73. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_saturate.v +19 -0
  74. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_sincos.v +673 -0
  75. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_sort.v +69 -0
  76. holoso-0.1.0/holoso/_backend/verilog/rtl/kulibin/zkf_to_int.v +126 -0
  77. holoso-0.1.0/holoso/_errors.py +44 -0
  78. holoso-0.1.0/holoso/_frontend/__init__.py +3 -0
  79. holoso-0.1.0/holoso/_frontend/_aggregate.py +87 -0
  80. holoso-0.1.0/holoso/_frontend/_ast_support.py +106 -0
  81. holoso-0.1.0/holoso/_frontend/_lower.py +2045 -0
  82. holoso-0.1.0/holoso/_frontend/_scope.py +87 -0
  83. holoso-0.1.0/holoso/_hir/__init__.py +40 -0
  84. holoso-0.1.0/holoso/_hir/_const.py +38 -0
  85. holoso-0.1.0/holoso/_hir/_const_fold.py +62 -0
  86. holoso-0.1.0/holoso/_hir/_copy.py +163 -0
  87. holoso-0.1.0/holoso/_hir/_dce.py +41 -0
  88. holoso-0.1.0/holoso/_hir/_if_convert.py +128 -0
  89. holoso-0.1.0/holoso/_hir/_ir.py +396 -0
  90. holoso-0.1.0/holoso/_hir/_operators.py +329 -0
  91. holoso-0.1.0/holoso/_hir/_optimize.py +23 -0
  92. holoso-0.1.0/holoso/_hir/_strength_reduce.py +88 -0
  93. holoso-0.1.0/holoso/_hir/_thread_merges.py +144 -0
  94. holoso-0.1.0/holoso/_hir/_types.py +31 -0
  95. holoso-0.1.0/holoso/_lir/__init__.py +52 -0
  96. holoso-0.1.0/holoso/_lir/_bankalloc.py +715 -0
  97. holoso-0.1.0/holoso/_lir/_build.py +314 -0
  98. holoso-0.1.0/holoso/_lir/_build_base.py +84 -0
  99. holoso-0.1.0/holoso/_lir/_coalesce.py +244 -0
  100. holoso-0.1.0/holoso/_lir/_construct.py +284 -0
  101. holoso-0.1.0/holoso/_lir/_ir.py +1276 -0
  102. holoso-0.1.0/holoso/_lir/_layout.py +287 -0
  103. holoso-0.1.0/holoso/_lir/_liveness.py +188 -0
  104. holoso-0.1.0/holoso/_lir/_mir_facts.py +134 -0
  105. holoso-0.1.0/holoso/_lir/_portassign.py +202 -0
  106. holoso-0.1.0/holoso/_lir/_ports.py +77 -0
  107. holoso-0.1.0/holoso/_lir/_regalloc.py +295 -0
  108. holoso-0.1.0/holoso/_lir/_schedule.py +279 -0
  109. holoso-0.1.0/holoso/_mir/__init__.py +33 -0
  110. holoso-0.1.0/holoso/_mir/_interpret.py +233 -0
  111. holoso-0.1.0/holoso/_mir/_ir.py +698 -0
  112. holoso-0.1.0/holoso/_mir/_lower.py +428 -0
  113. holoso-0.1.0/holoso/_operators.py +713 -0
  114. holoso-0.1.0/holoso/_type.py +236 -0
  115. holoso-0.1.0/holoso/_util.py +42 -0
  116. holoso-0.1.0/holoso/_value.py +291 -0
  117. holoso-0.1.0/holoso/py.typed +0 -0
  118. holoso-0.1.0/holoso.egg-info/PKG-INFO +16 -0
  119. holoso-0.1.0/holoso.egg-info/SOURCES.txt +146 -0
  120. holoso-0.1.0/holoso.egg-info/dependency_links.txt +1 -0
  121. holoso-0.1.0/holoso.egg-info/requires.txt +14 -0
  122. holoso-0.1.0/holoso.egg-info/top_level.txt +1 -0
  123. holoso-0.1.0/pyproject.toml +59 -0
  124. holoso-0.1.0/setup.cfg +4 -0
  125. holoso-0.1.0/tests/test_arithmetic_behavior.py +553 -0
  126. holoso-0.1.0/tests/test_backend.py +439 -0
  127. holoso-0.1.0/tests/test_const_install.py +45 -0
  128. holoso-0.1.0/tests/test_cosim.py +592 -0
  129. holoso-0.1.0/tests/test_cosim_examples.py +53 -0
  130. holoso-0.1.0/tests/test_cycle_model.py +186 -0
  131. holoso-0.1.0/tests/test_determinism.py +132 -0
  132. holoso-0.1.0/tests/test_example_reference.py +89 -0
  133. holoso-0.1.0/tests/test_frontend.py +2211 -0
  134. holoso-0.1.0/tests/test_fuzz.py +149 -0
  135. holoso-0.1.0/tests/test_fuzz_regressions.py +93 -0
  136. holoso-0.1.0/tests/test_install_landing.py +110 -0
  137. holoso-0.1.0/tests/test_interpret.py +171 -0
  138. holoso-0.1.0/tests/test_language_features.py +747 -0
  139. holoso-0.1.0/tests/test_latency_freeze.py +140 -0
  140. holoso-0.1.0/tests/test_metrics.py +259 -0
  141. holoso-0.1.0/tests/test_overlap_behavior.py +506 -0
  142. holoso-0.1.0/tests/test_passes.py +628 -0
  143. holoso-0.1.0/tests/test_public_api_behavior.py +559 -0
  144. holoso-0.1.0/tests/test_report.py +120 -0
  145. holoso-0.1.0/tests/test_schedule.py +2434 -0
  146. holoso-0.1.0/tests/test_synth.py +159 -0
  147. holoso-0.1.0/tests/test_timing_equivalence_behavior.py +224 -0
  148. holoso-0.1.0/tests/test_verify.py +1657 -0
holoso-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: holoso
3
+ Version: 0.1.0
4
+ Summary: A simple Python-to-Verilog synthesizer for numeric kernels, control systems, and DSP.
5
+ Requires-Python: >=3.14
6
+ Requires-Dist: numpy~=2.4
7
+ Requires-Dist: scipy~=1.17
8
+ Requires-Dist: sympy~=1.14
9
+ Provides-Extra: test
10
+ Requires-Dist: cocotb~=2.0; extra == "test"
11
+ Requires-Dist: jaxtyping~=0.3; extra == "test"
12
+ Requires-Dist: pytest~=9.0; extra == "test"
13
+ Provides-Extra: format
14
+ Requires-Dist: black~=26.5; extra == "format"
15
+ Provides-Extra: typecheck
16
+ Requires-Dist: mypy~=2.1; extra == "typecheck"
holoso-0.1.0/README.md ADDED
@@ -0,0 +1,107 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/holoso-logo-128.png" width="60px">
4
+
5
+ <h1>Holoso</h1>
6
+
7
+ _Simple high-level synthesis of portable Verilog from idiomatic Python_
8
+
9
+ [![Try online](https://img.shields.io/badge/try_online-holoso.digital-black?color=ff0000)](https://holoso.digital/)
10
+ [![Forum](https://img.shields.io/discourse/https/forum.zubax.com/users.svg?logo=discourse&color=ff0000)](https://forum.zubax.com)
11
+
12
+ </div>
13
+
14
+ -----
15
+
16
+ Holoso converts a subset of idiomatic Python into synthesizable and verifiable Verilog.
17
+ It is primarily designed for heavy numerical code which is abundant in control systems and DSP
18
+ where manual RTL coding is inefficient and error-prone.
19
+
20
+ Holoso focuses on Python because this is a popular language in modeling, system design, and verification domains;
21
+ ability to generate production HDL directly from the original model allows the designer to work with much simpler
22
+ harnesses and iterate faster.
23
+
24
+ See [PRIOR_ART.md](PRIOR_ART.md) for a detailed review of existing alternatives,
25
+ and why none are good enough for practical use.
26
+
27
+ Holoso is under active development and as such it has no burden of backward compatibility.
28
+ Breaking changes will occur regularly without notice until v1.0 is out.
29
+ Many critical features are missing which may limit applicability beyond applications that we are immediately involved with.
30
+ Contributions of any kind are emphatically welcome!
31
+
32
+ <img src="docs/hero.png" width="900px">
33
+
34
+ ## Design
35
+
36
+ Holoso implements essentially a separate programming language whose syntax is a strict subset of Python,
37
+ and whose semantics is largely equivalent to Python with minor deviations that make sense in chip design context.
38
+ Save for the minor differences in semantics, Holoso ensures that one can execute the original Python code
39
+ and run the generated circuit (RTL) side by side, and obtain equivalent results (bit-exact unless floating points
40
+ are used, in which case small errors may creep up, due to the inherent limitations of floating points).
41
+
42
+ Unlike most (all known to us) HLS engines out there, Holoso does not generate a straight-line II=1 pipeline
43
+ because this is rarely what you actually need in practice; instead, it designs a narrowly specialized
44
+ computing core (a zero-instruction-set processor) with custom microcode, and statically schedules a program for the
45
+ designed core. Being in control of both the core synthesis and the program compilation, Holoso tends to generate
46
+ extremely efficient designs in terms of cycle latency and chip area utilization compared to the state of the art.
47
+
48
+ Holoso outputs a purely portable and vendor-agnostic Verilog that can be fed into thid-party synthesis tools as-is,
49
+ along with its support library implementing various arithmetic operators. So far it has been tested at least with
50
+ Yosys (ECP5), Diamond (ECP5), and Vivado (Artix-7).
51
+
52
+ By default, Holoso is tuned for the minimum cycle latency and minimum $f_\max$.
53
+ If timing closure fails, one needs to locate the critical path and enable the staging knob that inserts a
54
+ register stage into the offending path; then re-synthesize and repeat until timings close.
55
+
56
+ Holoso has its own efficient floating point engine that is a subset of IEEE-754, omitting support for subnormals and NaN.
57
+ Arbitrary exponent and significand bit widths are supported (the IEEE-754 defaults map poorly onto FPGA DSP tiles).
58
+
59
+ Along with the synthesized Verilog, Holoso produces a Cocotb co-simulation testbench and a detailed and beautiful HTML
60
+ report that provides a human-friendly view of the processor and the microcode sequence constructed by the synthesizer.
61
+
62
+ >*You can SEE the pipeline — every cycle, every landing, every spill. It's gorgeous. People love it.*
63
+ >*They come up to me with tears in their eyes, they say sir, that schedule report, it's the most beautiful report we have ever seen.*
64
+
65
+ For a detailed review of the design and trade-offs, please refer to `DESIGN.md`.
66
+
67
+ ## Usage
68
+
69
+ Unlike most tools in this domain, Holoso is trivial to set up and get started with; it is not a framework.
70
+
71
+ Install: `pip install holoso`.
72
+
73
+ ```python
74
+ # Select the floating-point format you wish to use.
75
+ # Ideally, wman (mantissa width) should be a multiple of DSP tile operand width.
76
+ float_format = holoso.FloatFormat(wexp=6, wman=18)
77
+
78
+ # Define the numerical operators. This is where you can configure additional stages to close timings.
79
+ ops = holoso.OpConfig(
80
+ holoso.FAddOperator(float_format),
81
+ holoso.FMulOperator(float_format),
82
+ holoso.FDivOperator(float_format),
83
+ holoso.FMulILog2OperatorFamily(float_format),
84
+ holoso.FCmpOperator(float_format),
85
+ )
86
+
87
+ # Run Holoso -- construct the processor and the microcode.
88
+ # The results are returned in-memory; you can write them to disk where you want.
89
+ # They include the generated Verilog module, the fixed holoso_support.v/.vh, testbench, and the reports.
90
+ result = holoso.synthesize(your_function_or_method_here)
91
+
92
+ # Write the files -- this is usually what you want.
93
+ out = result.write(Path(__file__).resolve().parent)
94
+
95
+ # Show what's been written.
96
+ for filename, path in out.items():
97
+ print(f"{filename}: {path}")
98
+ ```
99
+
100
+ See the `examples/` directory for self-contained usage examples.
101
+
102
+ ## Verification
103
+
104
+ Just say `nox`. Read the `noxfile.py` and `DESIGN.md` for details.
105
+
106
+ You may find the [zubax-fpga-toolchain](https://github.com/Zubax/fpga-toolchain-docker/pkgs/container/zubax-fpga-toolchain)
107
+ container useful as it comes with all of the required tools out of the box.
@@ -0,0 +1,48 @@
1
+ """Holoso: a narrow Python-to-Verilog synthesizer for numeric kernels."""
2
+
3
+ from ._api import synthesize as synthesize, SynthesisResult as SynthesisResult
4
+ from ._lir import (
5
+ ControlInputPort as ControlInputPort,
6
+ ControlOutputPort as ControlOutputPort,
7
+ ControlPort as ControlPort,
8
+ DataInputPort as DataInputPort,
9
+ DataOutputPort as DataOutputPort,
10
+ DataPort as DataPort,
11
+ Direction as Direction,
12
+ Port as Port,
13
+ )
14
+ from ._type import (
15
+ BoolType as BoolType,
16
+ FloatFormat as FloatFormat,
17
+ FloatType as FloatType,
18
+ LogicalPort as LogicalPort,
19
+ ScalarType as ScalarType,
20
+ )
21
+ from ._value import FloatValue as FloatValue
22
+ from ._errors import (
23
+ HolosoError as HolosoError,
24
+ MissingIntrinsic as MissingIntrinsic,
25
+ SourceUnavailable as SourceUnavailable,
26
+ SynthesisError as SynthesisError,
27
+ UnsupportedConstruct as UnsupportedConstruct,
28
+ )
29
+
30
+ from ._backend.cocotb import CocotbOutput as CocotbOutput
31
+ from ._backend.html import HtmlOutput as HtmlOutput
32
+ from ._backend.numerical import (
33
+ NumericalModel as NumericalModel,
34
+ NumericalSimulator as NumericalSimulator,
35
+ )
36
+ from ._backend.verilog import VerilogOutput as VerilogOutput
37
+
38
+ from ._operators import (
39
+ FAddOperator as FAddOperator,
40
+ FDivOperator as FDivOperator,
41
+ FMulILog2OperatorFamily as FMulILog2OperatorFamily,
42
+ FMulOperator as FMulOperator,
43
+ FCmpOperator as FCmpOperator,
44
+ OpConfig as OpConfig,
45
+ )
46
+
47
+ __version__ = "0.1.0"
48
+ __url__ = "https://github.com/Zubax/holoso"
@@ -0,0 +1,163 @@
1
+ """The public synthesis entry point."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+ from dataclasses import dataclass, fields
6
+ from pathlib import Path
7
+ import inspect
8
+ import logging
9
+ import re
10
+
11
+ from ._backend.cocotb import generate as generate_testbench, CocotbOutput
12
+ from ._backend.html import generate as generate_html, HtmlOutput
13
+ from ._backend.numerical import generate as generate_model, NumericalModel
14
+ from ._backend.verilog import generate as generate_verilog, VerilogOutput
15
+
16
+ from ._frontend import lower as lower_frontend
17
+ from ._hir import optimize
18
+ from ._lir import ControlPort, DataInputPort, DataOutputPort, Port, build
19
+ from ._mir import lower as lower_to_mir
20
+ from ._operators import OpConfig
21
+
22
+ type Target = Callable[..., Any]
23
+ """
24
+ Currently supported targets are:
25
+ - A plain stateless function or lambda.
26
+ - A bound method of a class instance -- stateful. Public attributes become additional output ports.
27
+ - Later on we may potentially add support for multiple methods per class, where the generated module will provide
28
+ a selector port to choose which method to execute, all sharing the same state. In this case we would accept
29
+ a tuple containing the class type and a list of its unbound methods. This remains to be seen.
30
+ """
31
+
32
+ _logger = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class SynthesisResult:
37
+ """Everything produced by a synthesis run, held in memory. Nothing is written to disk unless requested."""
38
+
39
+ module_name: str
40
+
41
+ ports: list[Port]
42
+ input_ports: list[DataInputPort]
43
+ output_ports: list[DataOutputPort]
44
+ control_ports: list[ControlPort]
45
+
46
+ initiation_interval: tuple[int, int | None] # (min II, max II or None if data-dependent)
47
+ verilog_output: VerilogOutput
48
+ numerical_model: NumericalModel
49
+ cocotb_output: CocotbOutput
50
+ html_output: HtmlOutput
51
+
52
+ def write(self, out_dir: Path | str) -> dict[str, Path]:
53
+ """
54
+ Write every artifact to ``out_dir`` and return the written paths keyed by filename.
55
+ This is the only Holoso operation that touches the filesystem.
56
+ """
57
+ directory = Path(out_dir)
58
+ directory.mkdir(parents=True, exist_ok=True)
59
+ files: dict[str, str] = {
60
+ f"{self.module_name}.v": self.verilog_output.verilog,
61
+ **self.verilog_output.support_files,
62
+ f"test_{self.module_name}.py": self.cocotb_output.testbench,
63
+ f"{self.module_name}.html": self.html_output.html,
64
+ }
65
+ written: dict[str, Path] = {}
66
+ for filename, content in files.items():
67
+ path = directory / filename
68
+ path.write_text(content, encoding="utf-8")
69
+ written[filename] = path
70
+ return written
71
+
72
+
73
+ def synthesize(target: Target, /, ops: OpConfig, *, name: str | None = None) -> SynthesisResult:
74
+ """
75
+ Synthesize ``target`` (a plain function or a bound method of a constructed instance) into RTL.
76
+ ``ops`` is the operator configuration, constructed explicitly by the caller: each field fixes one operator's
77
+ float format and parameters, including any pipeline-stage knobs that lengthen its latency to ease timing closure.
78
+ ``name`` overrides the generated module name (inferred from target by default).
79
+ """
80
+ logging.basicConfig(level=logging.INFO, format="%(levelname)-5.5s %(name)s: %(message)s") # no-op if already setup
81
+ module_name: str = name or _default_module_name(target)
82
+ _validate_module_name(module_name)
83
+ _logger.info("Synthesis start: module=%r target=%r", module_name, target)
84
+ _logger.info("Configured operators:")
85
+ for field in fields(ops):
86
+ _logger.info("\t%s: %s", field.name, getattr(ops, field.name))
87
+
88
+ hir = optimize(lower_frontend(target))
89
+ _logger.info("HIR:\n\tinputs=%s\n\toutputs=%s\n\thir_nodes=%d", hir.input_ids, hir.outputs, len(hir.nodes))
90
+
91
+ mir = lower_to_mir(hir, ops)
92
+
93
+ lir = build(mir, module_name)
94
+ _logger.info("LIR ports:\n\t%s", "\n\t".join(f"{port}" for port in lir.ports))
95
+
96
+ verilog_output = generate_verilog(lir)
97
+ html_output = generate_html(lir, verilog_output)
98
+ model = generate_model(lir)
99
+ cocotb_output = generate_testbench(model)
100
+
101
+ latency_is_exact = len(lir.blocks) == 1 # a straight-line kernel has one fixed path; branches/loops vary by data
102
+ ii = (lir.min_initiation_interval, lir.min_initiation_interval if latency_is_exact else None)
103
+ _logger.info("Generated Verilog: %s; II [min,max]: %s cycles", verilog_output, ii)
104
+ return SynthesisResult(
105
+ module_name=module_name,
106
+ ports=lir.ports,
107
+ input_ports=lir.input_ports,
108
+ output_ports=lir.output_ports,
109
+ control_ports=lir.control_ports,
110
+ initiation_interval=ii,
111
+ verilog_output=verilog_output,
112
+ numerical_model=model,
113
+ cocotb_output=cocotb_output,
114
+ html_output=html_output,
115
+ )
116
+
117
+
118
+ def _default_module_name(target: Target) -> str:
119
+ if inspect.ismethod(target):
120
+ n = type(target.__self__).__name__
121
+ if "__" not in target.__name__:
122
+ n += f"_{target.__name__}"
123
+ return n
124
+ return str(getattr(target, "__name__", "kernel"))
125
+
126
+
127
+ def _validate_module_name(name: str) -> None:
128
+ if _MODULE_NAME.fullmatch(name) is None:
129
+ raise ValueError(f"module name {name!r} is not a valid identifier; expected [A-Za-z_][A-Za-z0-9_]*")
130
+ if name in _BLACKLIST:
131
+ raise ValueError(f"module name {name!r} is a reserved keyword; choose another name")
132
+ if name.lower().startswith("holoso"):
133
+ raise ValueError(f"module name {name!r} uses the reserved 'holoso' prefix; choose another name")
134
+
135
+
136
+ _MODULE_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
137
+
138
+ # Keywords from supported HDLs etc. that are not valid module names. Includes Verilog and VHDL keywords.
139
+ _BLACKLIST = frozenset("""
140
+ always and assign automatic begin buf bufif0 bufif1 case casex casez cell cmos config deassign default defparam
141
+ design disable edge else end endcase endconfig endfunction endgenerate endmodule endprimitive endspecify endtable
142
+ endtask event for force forever fork function generate genvar highz0 highz1 if ifnone incdir include initial inout
143
+ input instance integer join large liblist library localparam macromodule medium module nand negedge nmos nor
144
+ noshowcancelled not notif0 notif1 or output parameter pmos posedge primitive pull0 pull1 pulldown pullup
145
+ pulsestyle_onevent pulsestyle_ondetect rcmos real realtime reg release repeat rnmos rpmos rtran rtranif0 rtranif1
146
+ scalared showcancelled signed small specify specparam strong0 strong1 supply0 supply1 table task time tran tranif0
147
+ tranif1 tri tri0 tri1 triand trior trireg unsigned use uwire vectored wait wand weak0 weak1 while wire wor xnor xor
148
+ abs access after alias all architecture array assert attribute block body buffer bus component configuration constant
149
+ context disconnect downto elsif entity exit file generic group guarded impure in inertial is label linkage literal
150
+ loop map mod new next null of on open others out package port postponed procedure process protected pure
151
+ range record register reject rem report return rol ror select severity signal shared sla sll sra srl subtype
152
+ then to transport type unaffected units until variable when with
153
+ accept_on always_comb always_ff always_latch assume before bind bins binsof bit break byte chandle checker
154
+ class clocking const constraint continue cover covergroup coverpoint cross dist do endchecker endclass
155
+ endclocking endgroup endinterface endpackage endprogram endproperty endsequence enum expect export extends extern
156
+ final first_match foreach forkjoin global iff ignore_bins illegal_bins implements implies import inside int interface
157
+ intersect join_any join_none let logic longint matches modport nettype packed priority program property rand randc
158
+ randcase randsequence ref reject_on restrict s_always s_eventually s_nexttime s_until s_until_with sequence shortint
159
+ shortreal soft solve static string strong struct super sync_accept_on sync_reject_on tagged this throughout
160
+ timeprecision timeunit typedef union unique unique0 var virtual void wait_order weak wildcard within
161
+ assume_guarantee eventually fairness interconnect local nexttime restrict_guarantee untyped until_with vmode vprop vunit
162
+ false none true as async await def del elif except finally from lambda nonlocal pass raise try yield
163
+ """.split())
@@ -0,0 +1,4 @@
1
+ """
2
+ Output backends: each ``holoso/_backend/<name>`` exposes a stateless ``generate(...) -> <Name>Output``.
3
+ Backends are imported as needed.
4
+ """
@@ -0,0 +1,195 @@
1
+ """
2
+ The cocotb backend: a self-contained, self-checking testbench for a generated module.
3
+
4
+ ``generate`` embeds the module's :class:`NumericalModel` handle into a standalone cocotb test. The bench unpickles the
5
+ handle, elaborates a :class:`NumericalSimulator`, and ticks it in cycle-by-cycle lockstep with the DUT: each clock it
6
+ asserts that ``out_valid``/``in_ready`` agree (so the model reaches ``out_valid`` on exactly the DUT's cycle -- the
7
+ data-dependent latency check) and that the output bits match when valid, back-pressure included. The model is bit-exact
8
+ to the RTL, so the output check needs no tolerance.
9
+
10
+ The input sequence is either an explicit list of vectors supplied by the caller (replayed verbatim) or, when none is
11
+ given, a default random sweep drawn from a fixed seed. The seed also drives the back-pressure stalls, so every generated
12
+ bench is fully reproducible.
13
+ """
14
+
15
+ import base64
16
+ import pickle
17
+ import zlib
18
+ from collections.abc import Mapping
19
+ from dataclasses import dataclass
20
+
21
+ from .numerical import NumericalModel
22
+
23
+ # language=python
24
+ _TEMPLATE = '''\
25
+ """
26
+ Auto-generated by Holoso: bit-exact cocotb testbench for module @@MODULE@@.
27
+ """
28
+
29
+ import base64
30
+ import pickle
31
+ import random
32
+ import zlib
33
+
34
+ import cocotb
35
+ from cocotb.clock import Clock
36
+ from cocotb.triggers import FallingEdge, RisingEdge, Timer
37
+
38
+ # TODO: make it self-contained by removing the holoso dependency: https://github.com/Zubax/holoso/issues/12
39
+ # Currently it is needed for unpickling.
40
+ import holoso
41
+
42
+ # Unpickle the opaque model handle and elaborate the runnable simulator the bench drives in lockstep with the DUT.
43
+ _SIM = pickle.loads(zlib.decompress(base64.b64decode("@@BLOB@@"))).elaborate() # TODO issues/12
44
+ _IN_PORTS = _SIM.inputs # logical input ports (name + scalar type), in module-port order
45
+ _OUT_PORTS = _SIM.outputs
46
+
47
+ # An explicit input sequence -- rows of per-input ZKF bits ordered as the input ports -- or None to draw the default
48
+ # random sweep below. The fixed seed makes the default sweep and the back-pressure stalls reproducible.
49
+ _VECTORS = @@VECTORS@@
50
+ _SEED = 0x9E3779B97F4A7C15 # TODO: allow overriding the seed, count, and range via plusargs.
51
+ _DEFAULT_COUNT = 64
52
+ _DEFAULT_RANGE = (-4.0, +4.0) # small bounded range keeps multi-operation kernels from overflowing into infinities
53
+ _MAX_TRANSACTION_CYCLES = 1 << 20 # out_valid ceiling: well above any real transaction, tripped only by a runaway loop
54
+
55
+
56
+ def _output_bits(value):
57
+ return int(value) if isinstance(value, bool) else value.bits
58
+
59
+
60
+ def _input_bits(port, rng, lo, hi):
61
+ # Encode one random input per its scalar type: a boolean draws a single bit, a float a ZKF-encoded sample.
62
+ match port.scalar_type:
63
+ case holoso.BoolType():
64
+ return rng.randint(0, 1)
65
+ case holoso.FloatType(fmt=fmt):
66
+ return fmt.encode(rng.uniform(lo, hi))
67
+ case other:
68
+ raise TypeError("unsupported input scalar type: %r" % (other,))
69
+
70
+
71
+ def _input_value(port, bits):
72
+ match port.scalar_type:
73
+ case holoso.BoolType():
74
+ return bool(bits)
75
+ case holoso.FloatType(fmt=fmt):
76
+ return holoso.FloatValue.from_bits(fmt, bits)
77
+ case other:
78
+ raise TypeError("unsupported input scalar type: %r" % (other,))
79
+
80
+
81
+ @cocotb.test()
82
+ async def cosim(dut):
83
+ rng = random.Random(_SEED)
84
+ if _VECTORS is not None:
85
+ sequence = _VECTORS
86
+ else:
87
+ lo, hi = _DEFAULT_RANGE
88
+ sequence = [[_input_bits(p, rng, lo, hi) for p in _IN_PORTS] for _ in range(_DEFAULT_COUNT)]
89
+
90
+ cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
91
+ await FallingEdge(dut.clk)
92
+ dut.rst.value = 1
93
+ dut.in_valid.value = 0
94
+ dut.out_ready.value = 0
95
+ for _ in range(4):
96
+ await RisingEdge(dut.clk)
97
+ dut.rst.value = 0
98
+ await FallingEdge(dut.clk)
99
+ _SIM.reset() # the simulator and the DUT are now both idle at pc==0 (in_ready), aligned cycle for cycle from here
100
+
101
+ async def lockstep(in_valid, out_ready):
102
+ # Advance ONE clock on the DUT and the simulator with the identical handshake, then assert they still agree. The
103
+ # simulator is the cycle-accurate reference, so out_valid asserting on the same cycle is the latency check, and
104
+ # in_ready agreeing keeps the accept handshake aligned.
105
+ dut.in_valid.value = 1 if in_valid else 0
106
+ dut.out_ready.value = 1 if out_ready else 0
107
+ await RisingEdge(dut.clk)
108
+ await Timer(1, unit="ns")
109
+ _SIM.tick(in_valid, out_ready)
110
+ assert _SIM.out_valid == bool(int(dut.out_valid.value)), "out_valid diverged: model %d dut %d" % (
111
+ int(_SIM.out_valid),
112
+ int(dut.out_valid.value),
113
+ )
114
+ assert _SIM.in_ready == bool(int(dut.in_ready.value)), "in_ready diverged: model %d dut %d" % (
115
+ int(_SIM.in_ready),
116
+ int(dut.in_ready.value),
117
+ )
118
+
119
+ def check_outputs(index):
120
+ for port, value in zip(_OUT_PORTS, _SIM.output_values):
121
+ want = _output_bits(value)
122
+ got = int(getattr(dut, port.name).value)
123
+ assert got == want, "vector %d port %s: got 0x%x expected 0x%x" % (index, port.name, got, want)
124
+
125
+ for index, in_bits in enumerate(sequence):
126
+ waited = 0
127
+ while not _SIM.in_ready: # idle until both can accept; they advance together so they reach it together
128
+ await lockstep(False, False)
129
+ waited += 1
130
+ assert waited < _MAX_TRANSACTION_CYCLES, "vector %d: in_ready not reached" % index
131
+ # Accept: present the same input bits to the DUT and the simulator, then pulse in_valid for one cycle.
132
+ for port, bits in zip(_IN_PORTS, in_bits):
133
+ getattr(dut, "in_" + port.name).value = int(bits)
134
+ _SIM.set_inputs(*[_input_value(p, bits) for p, bits in zip(_IN_PORTS, in_bits)])
135
+ await lockstep(True, False) # accept edge: pc 0 -> 1
136
+ # Run to out_valid in lockstep. The simulator reaches it on the same cycle as the DUT (the lockstep asserts so
137
+ # each cycle), which is the cycle-accurate, data-dependent latency check -- no separate prediction needed.
138
+ waited = 0
139
+ while not _SIM.out_valid:
140
+ await lockstep(False, False)
141
+ waited += 1
142
+ assert waited < _MAX_TRANSACTION_CYCLES, "vector %d: out_valid not reached within %d cycles" % (
143
+ index,
144
+ _MAX_TRANSACTION_CYCLES,
145
+ )
146
+ assert int(dut.err_pc.value) == 0, "vector %d: unexpected error at cycle %d" % (index, int(dut.err_pc.value))
147
+ check_outputs(index)
148
+ # Random back-pressure: hold out_ready low. out_valid and the outputs must stay stable on both, and neither
149
+ # advances its persistent state until the transaction is accepted (out_valid && out_ready).
150
+ for _ in range(rng.randint(0, 3)):
151
+ assert _SIM.out_valid, "vector %d: simulator dropped out_valid under back-pressure" % index
152
+ check_outputs(index)
153
+ await lockstep(False, False)
154
+ check_outputs(index)
155
+ await lockstep(False, True) # accept the output: both advance the persistent state, pc -> 0
156
+ '''
157
+
158
+
159
+ @dataclass(frozen=True, slots=True)
160
+ class CocotbOutput:
161
+ """The cocotb backend's output: one self-contained, self-checking cocotb testbench module."""
162
+
163
+ testbench: str
164
+
165
+ def __str__(self) -> str:
166
+ return f"{type(self).__name__}(testbench_bytes={len(self.testbench.encode())})"
167
+
168
+
169
+ def _embed_vectors(input_names: list[str], vectors: list[Mapping[str, int]] | None) -> str:
170
+ """Render the explicit input sequence as a Python literal of input-name-ordered ZKF-bit rows, or ``None``."""
171
+ if vectors is None:
172
+ return "None"
173
+ rows: list[list[int]] = []
174
+ for index, vector in enumerate(vectors):
175
+ if missing := [name for name in input_names if name not in vector]:
176
+ raise ValueError(f"cosim vector {index} is missing inputs {missing}")
177
+ rows.append([int(vector[name]) for name in input_names])
178
+ return repr(rows)
179
+
180
+
181
+ def generate(model: NumericalModel, vectors: list[Mapping[str, int]] | None = None) -> CocotbOutput:
182
+ """
183
+ Build a self-contained cocotb testbench that checks the DUT against the embedded bit-exact model.
184
+
185
+ When ``vectors`` is given, each maps an input-port name to its ZKF bits and the bench replays the sequence verbatim
186
+ (one accepted transaction per vector, state carried across for stateful modules). When it is ``None``, the bench
187
+ draws a fixed-seed random sweep over a small bounded range instead. Either way the run is fully reproducible.
188
+ """
189
+ blob = base64.b64encode(zlib.compress(pickle.dumps(model, pickle.HIGHEST_PROTOCOL))).decode("ascii")
190
+ testbench = (
191
+ _TEMPLATE.replace("@@MODULE@@", model.module_name)
192
+ .replace("@@VECTORS@@", _embed_vectors([port.name for port in model.inputs], vectors))
193
+ .replace("@@BLOB@@", blob)
194
+ )
195
+ return CocotbOutput(testbench=testbench)
@@ -0,0 +1,3 @@
1
+ """The HTML backend: render a self-contained single-page report for a synthesized module."""
2
+
3
+ from ._html import HtmlOutput as HtmlOutput, generate as generate