Generator composition
Closed this issue · 0 comments
While single-shot generators solved problems with generators arbitrarily appending data to a block (and in a way that doesn't compose easily, like making redundant links that need to be merged), maybe it's a bit too restrictive and limits composability of generators.
Example use cases
Part table
It may make sense to have these base classes for a table-based resistor part:
AbstractResistor
- definesresistance
(spec parameter)PartTablePart
- base class that selects parts from a part table- Defines abstract
part_matches(candidate_part) -> bool
, that defines whether some candidate part matches the current spec - Defines abstract
get_table()
that returns the table - Defines abstract
selected_part(part) -> None
that is called on the selected part table part
- Defines abstract
PartTableResistor
- extendsAbstractResistor
andPartTablePart
- Implements
part_matches(...)
based on the resistance spec. Generator dependency onresistance
.
- Implements
SmtStandardPart
- base class that defines parts that can generate into 0402/0603/0805/... series and provides aminimum_package
parameter (without an implementation)MyPartTableResistor
- extendsPartTableResistor
andSmtStandardPart
- But this needs to change the
part_matches
behavior and now also depends onminimum_package
- which changes the generator signature previously defined inPartTableResistor
. - Implements
get_table()
with the relevant table andselected_part(...)
to generate the part
- But this needs to change the
Somewhat abstractly, this use case is composing base classes which define generator snippets that need different pieces of data. So the current structure of a single generator function makes it monolithic and ends up preventing modularity and re-use.
Mixins: microcontrollers
In some subcircuits, parts may be re-usable. For example, a microcontroller block might automatically generate the programming port, for example HasSwdPort
might be a mixin to a Microcontroller. This would define a SWD port externally, and if it's not connected, generate a programming header internally. Though note, there would need to be an internal node for the interface, not sure how that would be implemented yet
But if the top level microcontroller is also a generator, then this would need two generators, or HasSwdPort
would have a utility function called by a top-level monolithic generator and components. This structure isn't unworkable, but all parameters must be plumbed through the top generator, and that function can get unwieldly quickly.
Ideas
Declaring generator parameters
Maybe it makes sense to replace ArgParam with GeneratorParam, and this is used to register which parameters a generator depends on. Crucially, this could be composed additively (superclasses can declare GeneratorParams, we don't need all of them declared at once like the current self.generator(fn, *params)
).
The generator fn would then be replaced with generator()
, much like contents()
. It takes no arguments, but within this function, GeneratorParam values can be accessed with .getValue
, that returns the corresponding solved Python value. Overall this makes generators much more consistent stylistically with non-generators. GeneratorParam can also define a default value that doesn't need to be tied to init default args.
This might not even need a compiler core change, it could be done in the frontend only since the core generator semantics don't change (call some Python function once inputs are available, and that fills in a Block's implementation).
Generator directed assignments
Full generators may be overkill for applications where the circuit doesn't need to be generated - these are cases where only a value needs to be generated because the logic is outside the scope of the mini expression language, and we need to fall back to Python. An option might be to have an expression node that is a Python expression, while the rest of the Block is defined as normal.
Potential benefits:
- no longer limited to generator semantics where it can't depend on values of inner blocks - for example, it can do arbitrary calculations based on post-generate values of child blocks
- currently, that behavior could be emulated by putting it into another child block, which runs its generate after the value is available - this is a bit of a mess
- this is also the reason behind a separate BuckConverterPowerPath, since the output voltage may be needed to calculate the output current which is needed to generate, but the output voltage is defined by an internal block
- holes in the design graph are tighter - the whole thing doesn't need to be a generator just become one of the calculations needs a specialty function
Potential issues:
- currently, memoizing generator outputs probably provides a significant speedup (so every single 0.1uF cap doesn't need to re-check the parts table), something similar would be needed at this level.
- how to deal with multiple-input, multiple-output functions syntactically?
- yet another concept to deal with and learn for library builders
Other problems
- Currently, the only support for default values is through the init param system which is pretty hacky and requires plumbing all params to init, even ones that aren't meant to be visible to block instantiators. But this is needed since generator contents aren't defined pre-generator, which includes internal assigns.
- Alternatives: a more general infrastructure to improve default parameters? Maybe ArgParam can take (init param, optional default)? Maybe defaults should be assigned by infrastructure instead of in the containing block (eg, containing block can define param values, but if there is none assigned to an arg-param specifically, the compiler inserts the default fetched from the internal block's definition using the current defaults-for-subclasses infrastructure)?