Repository navigation
feat: bind number and true/false loop variables in generated Python operators - #9359
aglinxinyuan wants to merge 70 commits into
Conversation
Inside a control block the iteration's loop variables reach every operator as a state message. The Python worker now stores that message on `Operator.loop_state` right before `process_state` runs -- in DataProcessor.process_state and in the LoopEnd's deferred consume, which calls the operator directly -- so the callback and every call after it can consult the current iteration's variables. The default callback is unchanged and still forwards the state.
A property of an operator inside a loop block (LoopStart ... LoopEnd) may name a loop variable as `$K`. The frontend keeps sending the literal string whatever the property's type; the backend binds it in three steps. Parse. LogicalOp is now a StateReferencing descriptor, with a `stateReferences` sidecar (JSON pointer -> variable name) that the property panel never shows. StateReferenceModule, registered on JSONUtils.objectMapper and on the workflow-compiling-service mapper, adds a Jackson problem handler: when a whole-string `$name` cannot be converted to an integral, floating-point or boolean property, it returns the placeholder 0 / 0.0 / false and records the value's pointer, relative to the operator, in the operator's sidecar. A thin wrapper around each concrete StateReferencing deserializer opens that recording frame. An object that a polymorphic type deserializer hands over mid-way is first copied into a buffer of its own, because Jackson replays the fields before a late type id from a buffer whose parse context copies the first field's value, not the object. String properties keep the literal, and any other target (an enum, ...) keeps Jackson's ordinary error. Compile. WorkflowCompiler works out which operators lie inside a loop block (LoopBlockMembership, the frontend's block walk) and makes the sidecar final before the physical plan serializes it into the executor's descString. Inside a block it adds every whole-string `$name` of the operator's JSON. Outside every block a string such as "$AAPL" stays the literal it is on main, and a typed placeholder is a compile error naming the property and the variable. Inside a block, an operator whose executor runs generated code, or with an input fed from outside the block, is a compile error too. Worker. ExecFactory hands out a LateBoundExecutor when the descString's sidecar is not empty. It keeps and forwards each state message, and binds when the real executor is first needed, at the first tuple or finishing port: the channel has delivered its state messages by then, so their values are merged with the last one winning, and an inner loop's variable shadows an outer one of the same name. It patches the placeholders, coerced to their JSON type (a string property takes only a string, a number or a boolean), builds the real executor through the normal factory path, opens it and replays the kept state messages into it in order, so an executor that acts on them (If) behaves as without references. Every later call goes to that executor, which is never rebound. A variable still missing then is an error naming it. Closes apache#8635
The attribute the runtime registers on every operator right before process_state is now `state` instead of `loop_state`. LoopStartOperator and LoopEndOperator already used `self.state` for their own loop variables, so registering the incoming message there would have merged it into itself (LoopStart) and made the back-edge write the message instead of the updated variables (LoopEnd). Their own field is renamed to `variables`, and the back-edge in MainLoop._jump_to_loop_start now writes `executor.variables`. Loop operator behavior is unchanged. Two new tests pin the split: a LoopStart run through DataProcessor.process_state keeps its variables apart from the registered state, and the LoopEnd back-edge writes the updated variables rather than the registered state.
The attribute the runtime registers before process_state is now `state` (apache#8642), so the method that registers it follows: Operator.register_loop_state is now Operator.register_state. DataProcessor.process_state and the deferred consume in MainLoop call it under the new name, and its docstring and the tests say `state` / `register_state` throughout. The loop-variable reads, loop_variable_text and loop_variable_value, and their _loop_variables are unchanged.
…nding gaps SplitOpExec.open() resets its generator again, as before the simplification, so a reopened executor repeats its seeded split; the reseed test is back. New tests, each checked against a mutant that the old suites let through: - a reference on an operator fed by another body operator compiles (LoopStart -> Projection -> Limit($n) -> LoopEnd); - the compiling service's Dropwizard mapper parses a typed "$n"; - a control operator is not inside its own block when an outer block is left open, in both directions; - ExecFactory finds a setting declared only in a superclass, and tells two equal settings apart by identity; - a sidecar pointer that is not a JSON pointer is reported at binding. The Filter($i) test runs the compiled executor again.
…ock around it The outside-input check counted any operator inside some block as carrying the state, so an inner block's operator could read first from the outer body or a sibling block, whose tuples come before the inner loop's state. LogicalPlan.enclosingLoopStarts gives the LoopStarts around each operator, checked against the panel's walk, and a link carries the state only when its source is inside all of the operator's blocks or opens the innermost. Also pins an input with one link from inside the block and one from outside.
- LogicalPlan: derive the operators inside loop blocks from enclosingLoopStarts, so the block walk runs once and is cached. - WorkflowCompiler: normalizeStateReferences reads membership from the plan instead of a separate parameter. - OperatorExecutor: two weak maps (states, bindings) replace the Registration holder; StateReferenceBinding.bind() replaces `unbound`. - Shorter docs on ExecFactory and JSONUtils.registerWorkflowModules. - Tests: set-only port of the panel walk, merged overlapping compiler, binding and mapper cases, EndChannelHandlerSpec reuses the DataProcessorSpec fixture, and the single-loop Filter($i) run is dropped (the branching run covers it). Pin a variable written into every property that names it, and a refused message writing and registering nothing.
…-loop-refs # Conflicts: # amber/src/test/integration/org/apache/texera/amber/engine/e2e/LoopIntegrationSpec.scala
…fs-backend # Conflicts: # amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/DataProcessorSpec.scala
…perators Inside a control block, a number or true/false property of an operator whose Scala descriptor generates Python, such as Radar Chart's Fill Opacity or Radar Plot's Show Legend, is written into the code before the loop runs, so the compiler rejects a reference such as $o there today. These tests pin how such a reference is read when the code runs: - PythonOperatorDescriptorSpec and WorkflowCompilerSpec: the code is generated from probe values in the reference's place, and where the value lands as it is, the code calls self.loop_variable_value(name, kind) instead: 'integer' for an int property, 'long' for a long one. A value the descriptor checks or adjusts (Histogram2D's bins, Hugging Face's limits, Figure Factory Table's Row Height and Font Size), one inside a text, a comment or a name, one the code computes with, never holds or reads when the operator starts, and one that decides the output schema stay compile errors that name the property and why, never a probe value the user did not type. The compile probes the code once and decides every reason from that binding: the code's reason before the schema's, and a schema reason only once every input has a schema. - test_operator.py: loop_variable_value reads the values bound at the operator's first tuple and converts them as StateReferenceBinding does: trimmed as String.trim, then new BigDecimal(text) .longValueExact() (within 32 bits for an int property, as Jackson writes it), Double.parseDouble, or true/false by equalsIgnoreCase. The parity tables are generated on JDK 17 through the compiled State.fromJson and StateReferenceBinding.coerce, and through a Limit's int property for the 32-bit bound. Binding fails for a required value its property cannot take, as the JVM's binding does. - test_executor_manager.py: binding requires the values the generated code reads, by name and kind. - LoopIntegrationSpec: Radar Chart's opacity and Radar Plot's legend follow the loop variable on every iteration; each legend is matched to its iteration by the plot's trace count. Before the next commit the Scala specs do not compile (there is no loopVariableBinding), the new Python tests fail with AttributeError or on the values they read, and the two new LoopIntegrationSpec cases fail to compile their workflow.
…perators A number or true/false property of an operator whose Scala descriptor generates Python can now refer to a loop variable inside a control block. The code is generated before the loop runs, so PythonOperatorDescriptor.loopVariableBinding generates it from copies of the descriptor with probe values in the reference's place: 3 and 5, then 70 and 90 for an integer; 0.25 and 0.75, then 2.5 and 7.5 for a number; True and False. Where the two codes differ by exactly the two values, and nowhere else, the code calls self.loop_variable_value(name, kind) there instead: 'integer' for an int property, 'long' for one that takes a whole number past 32 bits. The value must also land as it is at 0, 1, -1, 1000000 and the other probes. When no probe pair generates the code, the properties' defaults and the values next to them are tried, so a descriptor's own check never reports a probe value. A value the descriptor checks or adjusts, one that lands in a text, a comment or a name, one the code computes with, never holds or reads when the operator starts (in open or __init__, before any state message), and one that decides the output schema leave the reference a compile error that names the property and why. WorkflowCompiler probes the code once per compile: the physical operator runs that binding's code, and unbindableStateReferences decides every reason from the same binding, the code's before the schema's, and the schema one only once every input has a schema, as the schema propagation does. Operator.loop_variable_value reads the variable's text as loop_variable_text spells it, from the values bound at the operator's first tuple, and converts it as a JVM operator's property binds it (StateReferenceBinding.coerce): trimmed as String.trim, an integer as new BigDecimal(text).longValueExact() reads it, and within 32 bits for an int property, as Jackson writes it there; a number as Double.parseDouble does (2.5d and 0x1p3, but not inf, nan, 1_000 or fullwidth digits), and a boolean as true or false in any case. A whole double past 10^16 reads as the whole number its fewest digits spell, as JDK 19 and later bind it; JDK 17 spells it with other digits (JDK-4511638). An integer past 64 bits is refused, as loop_variable_text refuses it. The executor manager also hands each Python operator the values its code reads through loop_variable_value, so binding fails, naming each, for a required value no state message carried or one its property cannot take, as the JVM's binding does, whether or not the code reads the value then.
Automated Reviewer SuggestionsBased on the
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9359 +/- ##
============================================
- Coverage 91.75% 91.63% -0.12%
- Complexity 5022 5043 +21
============================================
Files 1281 1282 +1
Lines 56777 57208 +431
Branches 7454 7543 +89
============================================
+ Hits 52096 52423 +327
- Misses 2802 2874 +72
- Partials 1879 1911 +32
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🟢 | bs=10 sw=10 sl=64 | 532 | 0.325 | 18,578/26,789/26,789 us | 🟢 -8.6% / 🔴 +84.4% |
| 🔴 | bs=100 sw=10 sl=64 | 1,155 | 0.705 | 83,188/115,473/115,473 us | 🔴 -6.0% / 🔴 +20.0% |
| ⚪ | bs=1000 sw=10 sl=64 | 1,464 | 0.893 | 677,213/733,822/733,822 us | ⚪ within ±5% / 🟢 -22.6% |
Baseline details
Latest main 00940a3 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 532 tuples/sec | 493 tuples/sec | 901.09 tuples/sec | +7.9% | -41.0% |
| bs=10 sw=10 sl=64 | MB/s | 0.325 MB/s | 0.301 MB/s | 0.55 MB/s | +8.0% | -40.9% |
| bs=10 sw=10 sl=64 | p50 | 18,578 us | 20,335 us | 11,350 us | -8.6% | +63.7% |
| bs=10 sw=10 sl=64 | p95 | 26,789 us | 27,671 us | 14,525 us | -3.2% | +84.4% |
| bs=10 sw=10 sl=64 | p99 | 26,789 us | 27,671 us | 17,637 us | -3.2% | +51.9% |
| bs=100 sw=10 sl=64 | throughput | 1,155 tuples/sec | 1,228 tuples/sec | 1,205 tuples/sec | -5.9% | -4.1% |
| bs=100 sw=10 sl=64 | MB/s | 0.705 MB/s | 0.75 MB/s | 0.735 MB/s | -6.0% | -4.1% |
| bs=100 sw=10 sl=64 | p50 | 83,188 us | 81,673 us | 88,882 us | +1.9% | -6.4% |
| bs=100 sw=10 sl=64 | p95 | 115,473 us | 114,192 us | 96,205 us | +1.1% | +20.0% |
| bs=100 sw=10 sl=64 | p99 | 115,473 us | 114,192 us | 120,754 us | +1.1% | -4.4% |
| bs=1000 sw=10 sl=64 | throughput | 1,464 tuples/sec | 1,471 tuples/sec | 1,245 tuples/sec | -0.5% | +17.6% |
| bs=1000 sw=10 sl=64 | MB/s | 0.893 MB/s | 0.898 MB/s | 0.76 MB/s | -0.6% | +17.6% |
| bs=1000 sw=10 sl=64 | p50 | 677,213 us | 675,969 us | 862,870 us | +0.2% | -21.5% |
| bs=1000 sw=10 sl=64 | p95 | 733,822 us | 755,174 us | 905,485 us | -2.8% | -19.0% |
| bs=1000 sw=10 sl=64 | p99 | 733,822 us | 755,174 us | 948,588 us | -2.8% | -22.6% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,375.61,200,128000,532,0.325,18578.01,26788.83,26788.83
1,100,10,64,20,1731.07,2000,1280000,1155,0.705,83188.06,115473.06,115473.06
2,1000,10,64,20,13663.27,20000,12800000,1464,0.893,677213.46,733822.47,733822.47…o values Since main's apache#8642, reading a name that got two different values from one depth fails. bind_loop_variables copies loop_variables with dict(), which reads every name, so a generated operator fails at its first tuple, or when an input ends, as soon as any name has two values, even one no property refers to. A JVM operator fails only for a name its setting refers to. main's test_main_loop_thread_can_process_state (four values of one name, then EndChannel) fails the same way. Pin that binding fails only for a required name, listed with the other problems, and that the bound values keep the two values: reading that name fails, the other names read, and a deeper value before binding replaces the two.
… values bind_loop_variables now copies the values and the names that got two different values, as loop_variables holds them, instead of reading every name through dict(). Binding fails for a required name with two values, listed with the other problems, and reading such a name from the bound values fails with the same error, while the other names read.
loop_variable_value reads a variable's text through _loop_variable_text, and binding checks each required value the same way, so since feat/generated-op-loop-refs merged in, both follow its rule for a name that got two different values from one depth: binding fails only for a required name, listed with the other problems in order, and the bound values keep the two values. Pin it for numeric and boolean properties: an operator whose code reads only n binds and converts n while i has two values; a required i with two values fails binding with StateReferenceBinding's message whether or not the values fit the property's kind, as a JVM operator fails before it converts either; reading i as any kind fails before and after binding, and still after a deeper i arrives; the other names convert as before; a deeper value before binding replaces the two. The merge commit already passes these tests, so no source change follows.
Binding checks every read of a loop variable the generated code makes: once as a text, and once for each kind it is read as. A name that got two different values from one depth fails each of those reads with the same error, so code that reads it as an integer, a number and a text lists StateReferenceBinding's message three times. A JVM operator throws that message once for the name (StateReferenceBinding.write goes over each referenced name once). Pin that binding names the two values once, where the name is first read: for code that reads n three ways, for a text and several kinds, and beside a missing and a refused value, which keep their place. A value that two kinds refuse is still named once for each kind.
bind_loop_variables now names a required loop variable that got two different values once, where the code first reads it: a value read of that name adds StateReferenceBinding's message only when no text or earlier kind has added it. Every read is still checked, and missing and refused values are named as before.
… the loop runs Inside a loop block, SVM Regressor took a Ground Truth Attribute Column "$y" beside the Selected Feature "Unit Price". Its check that the ground truth is no feature runs in getOutputSchemas, before the loop runs, so it saw "$y", and when y was "Unit Price" the run fitted Unit Price on itself, which the operator refuses outside a loop. Sklearn Prediction failed a ground truth "$g" with "Ground Truth column '$g' is not in the input table", and Machine Learning Scorer an Actual Value "$a" the same way, neither saying that a loop variable cannot go there. These cases pin the reason "it checks the text before the loop runs" for the three, a feature "$f", a classic trainer's target with Count Vectorizer on and both of the Scorer's columns included, and "the text decides its output schema" for a test-only operator that types its output column like the input column its text names. The same operator pins a check in its code generation against another of its texts, one naming an input column and one naming none; a check against the empty text, which no copy tries, so the text stays bound; and a null setting, which the copies keep rather than take the property's default. As the schema propagation, the check waits until every input has a schema.
…the loop runs PythonOperatorDescriptor.textsReadBeforeTheLoop tells which of a generated operator's text references it checks, or computes its output schemas from, before the loop runs. It builds copies of the descriptor that hold another text in the reference's place: the first column of each type on each input, and each other non-empty text among its settings, which a check commonly compares the text to. Where only one of a copy and the copy it is compared with computes its output schemas, or the copy fails to generate its code where the other does not, the operator checks the text; where both compute them and they differ, the text decides them. The copy compared with holds every reference's "$name", unless its schemas fail; then it is the first copy that computes them with such texts in the references' places, so that one reference that fails a check cannot hide another's. WorkflowCompiler asks it once every input has a schema, for the texts the generated code would otherwise bind, and refuses them with "it checks the text before the loop runs" or "the text decides its output schema", much as apache#9359 refuses a number it checks or computes its schema from. Inside a loop this refuses the ground truth and Selected Features of the KNN Classifier, KNN Regressor, SVC and SVR trainers, and the Target and Text Attribute of the classic sklearn trainers with Count Vectorizer on, which each check against the other. Sklearn Prediction's ground truth and the Machine Learning Scorer's columns, which failed before with "... is not in the input table", now get this reason. Sweeping every generated operator, one text property at a time, finds no other text it refuses.
On this branch a generated operator's code and output schemas come from the binding's baseline copy, which holds a probe value for each numeric or boolean reference, while the descriptor holds its placeholder (0, 0.0 or false). apache#8671's check of the texts runs on the descriptor, so a placeholder that fails the operator's schema makes every copy fail alike and hides the check of a text. This case pins it with a test-only operator whose schema requires its ratio, "$r", to be positive and its column, "$c", not to be "total": "$c" is refused, and with nothing to avoid both references are read from the loop state.
LoopVariableBinding now holds the copy its code and output schemas come from, the descriptor itself or the probing's baseline, in place of that copy's schema function, and asks that copy which text references it checks or computes its schemas from (textsReadBeforeTheLoop). The compiler asks the binding rather than the descriptor, whose placeholders for the numeric and boolean references may fail a check whatever the text is.
Xiao-zhen-Liu
left a comment
There was a problem hiding this comment.
I ran the PR's Python tests (495 passed) and set every top-level number and true/false property of the generated operators I could configure to $v inside a loop: Time Series, Bar Chart, Word Cloud, Scatter Plot opacity and Dumbbell bind, and the others are refused at compile time. One structural ask: the probing grows the base that every generated operator's Scala descriptor extends from 312 to 773 lines. Could it live in its own file, with the base only calling it?
…ding Move the probing and the judging of loop-variable references out of PythonOperatorDescriptor (the base trait of generated operators) and its companion object, and the text reasons that WorkflowCompiler worked out, into their own file, LoopVariableBinding.scala. The class LoopVariableBinding, which was nested in PythonOperatorDescriptor's companion and is now top-level, and its new companion object hold the probing that was in PythonOperatorDescriptor (the probe pairs, edge values, baseline and variant copies, finding where a value lands, the probing helpers and the JSON copy helpers), the check of whether code runs when the operator starts (runsAtStartup), the text check that was the trait's textsReadBeforeTheLoop, and the reasons the compiler used to work out for text references (the code embeds the text, does not read it, reads it when the operator starts, or the text names an output column), with their gating and order. PythonOperatorDescriptor now only calls LoopVariableBinding.of(this) and builds its physical operator from the binding; its companion keeps only loopVariableLookup and loopVariableValueLookup, the calls through which generated code reads a loop variable. The binding now decides the number, true/false and text reasons itself: the number and true/false reasons other than the schema one while it generates the code (LoopVariableBinding.of), and the schema and text reasons in reasons, given the input schemas when every input has one, which returns them all in the order the error prints them. The columns an operator adds come from the baseline's output schemas, computed once in reasons for both the schema check and the column check. The physical operator propagates its schemas with outputSchemas, which throws the schema reason too, from the same schema check, decidedBy. One helper, readsAtStartup, checks whether code reads a value or a text when the operator starts, and each pair of messages that differ only by "text" or "value" comes from a builder that takes the noun (unread, readAtStartup, decidesOutputSchema). For a generated operator, WorkflowCompiler.unbindableStateReferences only reports the binding's reasons; any other operator that runs code still gets the fixed-code reason there. getPhysicalPlan and getPhysicalOp from a binding are now final, so the physical operator always runs the code the binding judged. LoopVariableBinding.of and outputSchemas, which only the operator package calls, are private[operator]; unboundBySchema, textsReadBeforeTheLoop, runsAtStartup and the reason strings are private. WorkflowCompilerSpec's note on its test-only operators now names LoopVariableBinding as what parses their copies. Every compile error message is unchanged, and so is the order of its parts. Line counts: PythonOperatorDescriptor.scala 773 -> 134, WorkflowCompiler.scala 549 -> 459, new LoopVariableBinding.scala 838.
What changes were proposed in this PR?
Inside a control block (
Loop Start ... Loop End), a number or true/false property of an operator whose Scala descriptor generates Python, such as Radar Chart's Fill Opacity or Radar Plot's Show Legend, can now hold a loop-variable reference such as$o. The descriptor writes such a value into the code when the workflow is compiled, before the loop runs. To find where it lands, the code is generated from copies of the descriptor with probe values in the reference's place. Where the two codes differ by exactly the value, and nowhere else, the code reads it from the iteration's state instead:TrueandFalse. When the descriptor rejects every pair, as Figure Factory Table does below a Row Height of 30, the code is generated from the property's default and the value next to it.openor__init__, before any state message can arrive), and one that decides the output schema. A value that fails a check and also decides the schema gets the check's reason. No error quotes the descriptor's own message about a probe value the user never typed.LoopVariableBindingat most once, probing its number and true/false references then, and the physical operator runs the binding's code. For a generated operator,WorkflowCompiler.unbindableStateReferencesonly reports the reasons that binding gives, for number, true/false and text references alike. The schema reason waits until every input has a schema, as the schema propagation does.Operator.loop_variable_value(name, kind)reads the variable's text asloop_variable_textspells it, from the values the operator fixed when its first tuple arrived, and converts it as a JVM operator's property takes it (StateReferenceBinding.coerce, then Jackson). The text is trimmed asString.trimtrims it. An integer is read asnew BigDecimal(text).longValueExact()reads it: within 32 bits for anintorIntegerproperty ('integer'), within 64 for alongone ('long'). A number is read asDouble.parseDoublereads it (2.5dand0x1p3, but notinf,nan,1_000or fullwidth digits), and a boolean astrueorfalsein any case, never1or0.Outside every block nothing changes. The
stateReferencessidecar from #8639, which maps each property that holds a reference to its variable, is empty there in any workflow that compiles, so the code and the schema are the same as onmain.This builds on #8671; #8642 is on
main.Any related issues, documentation, discussions?
Closes #9358.
How was this PR tested?
The test commit, 959939d, fails without the feat commit, 9e841f2: the Scala specs do not compile, the new Python tests fail with
AttributeErroror on the values they read, and the two new end-to-end cases fail to compile their workflow.PythonOperatorDescriptorSpec: Radar Chart's opacity read as a number, next to a text reference; Histogram2D's checked bins and Hugging Face's clamped values stay compile errors, and so does a sklearn boolean that picks the code's shape; Figure Factory Table's Row Height, which rejects every probe, and Font Size reported as checked, with no probe value in the code; a generation that the descriptor's own values fail embeds that failure; nulls the descriptor holds survive the copies; every descriptor the frontend can send, with each top-level number and true/false property referring to a loop variable, embeds no failure but its own.WorkflowCompilerSpec, with test-only generated operators: an integer, a number and a Python boolean read from the loop state; anintproperty's reference read as'integer'and alongone's as'long'; a value the descriptor rejects at a probe, at an edge value or at every probe, adjusts, or computes with; a value in a text, a comment or next to a name; a value the code does not hold, or reads inopen; two values that land in one place; the next probe pair; the schema from a probe, and a value that decides it, reported in one error with the other references, only once every input has a schema, and after the check's reason when the value also fails a check; a text the operator checks, judged on the probing's copy where a number's placeholder fails the schema; the descriptor probed once per compile.test_operator.py,test_executor_manager.py:loop_variable_valuefor every kind and scalar type, against tables generated on JDK 17 through the compiledState.fromJsonandStateReferenceBinding.coerce, and through a Limit'sintproperty for the 32-bit bound. The tables cover trimming, signs, exponents of any length, hexadecimal numerals with letter digits,+exponents and suffixes, NaN and Infinity spellings, other scripts' digits, numerals that start or end with a point, the 32-bit and 64-bit limits, andtrue/falsein any case. Also: the values fixed at the first tuple, kept when a later state arrives; the failure for a required value no state carried, one with two different values from the same loop (named once, however many properties read it, and before any kind check), or one its property cannot take, the texts named first; a variable with two values that the code does not read, which binds and fails only when read; errors before any state, for a missing name, for a non-scalar and for an unknown kind; the values the executor manager notes from the generated code; snippets shaped like the generated code, run against a real operator.LoopIntegrationSpec, end to end:sales CSV -> Loop Start (o = 0.25, table) -> Radar Chart (fill opacity = $o) -> Loop End (o += 0.25 while o <= 0.75)draws three charts whose traces have opacity 0.25, 0.5 and 0.75. Radar Plot with Show Legend =$legend(True, thenFalse), over the firsti + 1rows, shows the legend on the first iteration's plot only; each plot is matched to its iteration by its trace count.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5.5)
🤖 Generated with Claude Code