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.