Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

more correct emulation of \box and \copy primitives #2480

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

dginev
Copy link
Collaborator

@dginev dginev commented Jan 8, 2025

Fixes #2412 .

Two key upgrades:

  • deeply clone the box returned during \copy
  • for \box, add a new removeValue method in State, which (idempotently) unbinds the nearest binding of a value in the undo table.

This gets the tests discussed in the issue to pass, and matches the pgf rendering in the PDF build (really great diagnostic by @xworld21 !)

I would need a second opinion by @brucemiller on whether this kind of "removeValue" method makes sense for latexml's State. Its code is inspired by the global assignment mode of assign_internal, which will unbind all values in all frames, and then assign a value on the cleaned up 1-element stack.

I am wondering if many of the current uses of AssignValue($key, undef) are meant as this RemoveValue behavior, which could motivate changing local uses of assign_internal with undef to also unbind from the nearest assigned frame -- without creating a new method.

Edit: Another direction for refining the code is to add a 'local' vs 'global' argument for removeValue, to indicate whether one or all bindings should be deleted.

But I am feeling confident I am following the \box unbind semantics accurately now, as explained in the TeXbook. What was missing was unbinding the actual assignment - we were adding a local undef assignment, while also keeping the original assignment in place, when it happened in a higher frame.

Thoughts?

@dginev dginev requested a review from brucemiller January 8, 2025 16:15
@@ -227,6 +227,23 @@ sub shiftValue {
assign_internal($self, 'value', $key, [], 'global') unless $$vtable{$key}[0];
return shift(@{ $$vtable{$key}[0] }); }

sub removeValue {
Copy link
Contributor

Choose a reason for hiding this comment

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

You could use this code to implement scope => 'samelevel' assignments (with a better name, if you can find it), which would be equivalent to removeValue when assigning \undef. Should you one day discover another assignment 'in place'.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's also a possibility. My main worry is that assign_internal is already somewhat entangled, so adding more to it just makes life harder... Will wait for feedback from Bruce.

I enjoy the idea of having a dedicated removal method, for what that's worth. But since the removal isn't global, the name is a bit misleading. It's really UnbindLastValueAssignment, but this is not a Java project...

Copy link
Contributor

Choose a reason for hiding this comment

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

That's also a possibility. My main worry is that assign_internal is already somewhat entangled, so adding more to it just makes life harder... Will wait for feedback from Bruce.

I was thinking the opposite: this code is very similar to assign_internal and would be better placed there for ease of maintainance... ?

I am also wondering if removing values has other side effects, e.g. will this make \ifcsname false? That's not really possible in TeX, is it? (This has no real consequence of course since removeValue is only used for the box registers.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this code is very similar to assign_internal and would be better placed there for ease of maintainance... ?

Only if it can be DRY ("do not repeat yourself"). Otherwise just growing the body of the function will make it more impenetrable.

my @frames = @{ $$self{undo} };
while (@frames) {
$frame = shift(@frames);
if (my $n = $$frame{value}{$key}) { # Undo the bindings, if $key was bound in this frame
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this idempotent? I.e. if applied twice, will it remove the two previous bindings? That would be too much for \box.

Copy link
Collaborator Author

@dginev dginev Jan 9, 2025

Choose a reason for hiding this comment

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

Curious idea - we don't yet have a test for that. Something like:

\newbox\myboxD
\setbox\myboxD\vbox{D won't appear outer.}
\begingroup
  \setbox\myboxD\vbox{D will appear inner, once.}
  \box\myboxD
  \box\myboxD
\endgroup
\bye

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a test. It appears to work as expected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Scratch that, the test wasn't precise enough -- there is indeed a bug where unbinding twice will pop the outer frame, nice catch @xworld21

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, using exists and a 0 value for the undo counter becomes idempotent. The new test now has correct timing and matches the PDF.

But the code got extra scary. Maybe good, maybe not?

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.

pgf layers do not get cleared
2 participants