Dry run
A dry run should be a normal step runner execution except the
leaf commands are not exec'ed. Required outputs should be
synthesized. E.g. if an output is a list then an empty list will be
recorded as the dry run output. Likewise a struct, bool, number
and string will be synthesized as {}, false, 0.0 and ""
respectively.
Expressions can be evaluated during a dry run as well. This
allows "type checking" of references to outputs. E.g. an
expression ${{ step.foo.output.bar(0) }} requires the output
bar from step foo be a list. A dry run cannot check types
deeper than the first reference because the output contents are
not yet known.
When encountering a situation where the output contents are referenced but the type is not known, a warning should be printed but evaluation should continue. In any case, the result of a reference to any output should be the empty string.
When the actual step reference value includes an expression, a error should be returned that the concrete step is not known. A dry run is incompatible with interpolated step references.
When dry running a step with input type steps the resulting
steps should be included in the dry run. And dry run will look for
an output with type step_results to determine the key under
which to store the result tree and synthesized outputs.
Outputs of type step_results should not be stored alongside
other outputs. They should be removed when reified. Otherwise
expressions could see past the first level of steps and reference
outputs from sub steps, breaking the invariant that steps should
not couple with implementation.