Fix write() quoting of symbols in proper sexps#490
Fix write() quoting of symbols in proper sexps#490DimitriosDalaklidhs wants to merge 5 commits intoion-fusion:mainfrom
Conversation
ImmutablePair.write() was incorrectly quoting operator symbols like +
as '+' when inside a sexp. This happened because the manual write path
had no context about being inside a sexp, unlike ionize() which
delegates to IonWriter and gets correct quoting automatically.
Fix: for proper sexps, delegate write() entirely to ionize() via a
temporary IonWriter. Improper sexps fall back to manual rendering
with Fusion's {.} dotted-pair notation as before.
Fixes the discrepancy between:
(write (quote (+ 1 2))) => ('+' 1 2) [wrong]
(ionize (quote (+ 1 2))) => (+ 1 2) [correct]
SharkBaitDLS
left a comment
There was a problem hiding this comment.
It would be good to add tests that assert this behavior is working correctly. runtime/src/test/fusion/scripts/io.test.fusion is an appropriate place to add tests that cover this. There also is one existing failing test according to the CI build.
toddjonker
left a comment
There was a problem hiding this comment.
Hi @DimitriosDalaklidhs, thanks for the contribution! You found the correct centrum of the change, but unfortunately the minimal solution has a bit more inherent complexity.
As @SharkBaitDLS notes, we need some test cases as well. The best place for those is in io.test.fusion. While the tests for ionize use ionize_to_string, theres currently no write_to_string so you'll need to use with_output_to_string down at the bottom of that file.
(I needed write_to_string just this weekend, so I just cut #491 and will see if I can add that in the next day or two.)
| // quoting rules, so operators like + are not wrongly quoted. | ||
| // ionize also handles annotations via setTypeAnnotations. | ||
| IonWriter iw = WRITER_BUILDER.build(out); | ||
| ionize(eval, iw); |
There was a problem hiding this comment.
I think there's a misconception at play: the value being a "proper sexp" doesn't imply that everything inside the sexp, recursively, is ionizable.
So I think this call is incorrect, in that it changes the output style for the entire sexp content, recursively.
It looks like this use:
(write (sexp (quote +) (void)))
...will raise an exception because the void value is not ionizable. But that's exactly the difference between ionize and write, the latter is more lenient.
Instead, that expression should output (+ {{{void}}}).
… sexp writing behavior.
toddjonker
left a comment
There was a problem hiding this comment.
Oof, this might turn out to be more complicated than I expected, do to limitations in the ion-java library.
When writing a sexp, operator symbols such as + and = were being quoted (e.g. '+' '=') rather than emitted bare (e.g. + =). This was caused by dispatchWrite delegating to BaseValue.write() which has no awareness of the parent context, causing ActualSymbol to always call IonTextUtils.printSymbol() which quotes operators. Fix by introducing a quoteOperators context flag threaded through new overloads of FusionIo.dispatchWrite() and BaseValue.write(). ImmutablePair.write() now passes quoteOperators=false to its direct children for both proper and improper sexps. ActualSymbol overrides the new write() overload and uses IonTextUtils.symbolVariant() to suppress quoting only for OPERATOR-variant symbols, leaving all other symbols (identifiers, symbols requiring quotes, etc.) unaffected. AnnotatedSymbol forwards the flag to its inner value while always quoting annotations. The default BaseValue.write(eval, out, quoteOperators) falls through to write(eval, out), so all other value types are unaffected with no call-site changes required. Also fixes a duplicate render bug in ImmutablePair.write() where the improper sexp traversal loop was emitted twice. Add tests covering: operator inside sexp, operator outside sexp, quotes-required symbol inside sexp, operator in improper sexp tail, and quotes-required symbol in improper sexp tail.
toddjonker
left a comment
There was a problem hiding this comment.
This is looking great! My only substantial request is the addition of a few test cases to verify context changes in relevant container combinations.
Plus correction of some spurious whitespace changes affecting indentation.
Thanks so much for following through with this!
| throws IOException | ||
| { | ||
| if (!quoteOperators | ||
| && IonTextUtils.symbolVariant(myContent) == IonTextUtils.SymbolVariant.OPERATOR) |
There was a problem hiding this comment.
[nitpick] Our general style would use static imports for symbolVariant and OPERATOR to avoid the qualified uses.
…stead of pretty in a couple places.
…stead of pretty in a couple places.
There was a problem hiding this comment.
This file got a ton of superfluous formatting changes. Please revert all except for the functional changes relating to the PR intent.
Looks like the whole thing got indented several spaces?
| { | ||
| myCurrentOutputPort = (DynamicParameter) currentOutputPort; | ||
| myBuilder = IonTextWriterBuilder.pretty().immutable(); | ||
| myBuilder = IonTextWriterBuilder.minimal().immutable(); |
There was a problem hiding this comment.
Ditto.
Eventually this will be controlled by a dynamic parameter or something, but for now the human-oriented pretty printing is best.
Also, I don't think that this would have any impact on the write paths we're concerned with here. Did you see otherwise?
| throws IOException | ||
| { | ||
| IonTextWriterBuilder b = IonTextWriterBuilder.pretty(); | ||
| IonTextWriterBuilder b = IonTextWriterBuilder.minimal(); |
There was a problem hiding this comment.
Please revert this; it will change the CLI output and I'd like to keep that in the more readable pretty-printed format.
| // List context: nested sexp correctly unquotes its direct children but contains void. | ||
| (check === "['+', ['+'], {'+':'+'}, (+ {{{void}}})]" | ||
| (with_output_to_string | ||
| (write (quote ['+', ['+'], {'+':'+'}, (+ (void))])))) |
There was a problem hiding this comment.
This input quote won't produce the desired output, since the (void) is treated literally, not evaluated to produce the void-value.
There was a problem hiding this comment.
Please be sure to run ./gradlew build from the repo root to make sure all the tests pass. On the latest commit two test files fail: https://github.com/ion-fusion/fusion-java/actions/runs/23517778238/job/68456272579?pr=490#step:5:83
| // Voids verify that write mode is maintained and does not escalate to ionize. | ||
|
|
||
| // List context: nested sexp correctly unquotes its direct children but contains void. | ||
| (check === "['+', ['+'], {'+':'+'}, (+ {{{void}}})]" |
There was a problem hiding this comment.
OK, I see why you did the formatting change. Theses cases would be quite annoying to write without that.
And the struct one is just grrrrrrrr. You came up with a clever solution though!
It's not worth holding up the whole PR on this, though, so you can drop a big (when false around this whole section, and I can come back to it later. (And probably build some features to make it easier to test.)
Fix write() quoting of symbols in proper sexps
ImmutablePair.write() was incorrectly quoting operator symbols like + as '+' when inside a sexp. This happened because the manual write path had no context about being inside a sexp, unlike ionize() which delegates to IonWriter and gets correct quoting automatically.
Fix: for proper sexps, delegate write() entirely to ionize() via a temporary IonWriter. Improper sexps fall back to manual rendering with Fusion's {.} dotted-pair notation as before.
Fixes the discrepancy between:
(write (quote (+ 1 2))) => ('+' 1 2) [wrong]
(ionize (quote (+ 1 2))) => (+ 1 2) [correct]