Skip to content

Fix write() quoting of symbols in proper sexps#490

Open
DimitriosDalaklidhs wants to merge 5 commits intoion-fusion:mainfrom
DimitriosDalaklidhs:main
Open

Fix write() quoting of symbols in proper sexps#490
DimitriosDalaklidhs wants to merge 5 commits intoion-fusion:mainfrom
DimitriosDalaklidhs:main

Conversation

@DimitriosDalaklidhs
Copy link
Copy Markdown

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]

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]
Copy link
Copy Markdown
Contributor

@SharkBaitDLS SharkBaitDLS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

@toddjonker toddjonker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}}}).

Copy link
Copy Markdown
Member

@toddjonker toddjonker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Member

@toddjonker toddjonker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Our general style would use static imports for symbolVariant and OPERATOR to avoid the qualified uses.

ddalaklidhs added 2 commits March 25, 2026 01:38
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))]))))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This input quote won't produce the desired output, since the (void) is treated literally, not evaluated to produce the void-value.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}}})]"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants