Skip to content

Visualize the actual proofs of the current problem with the TableauSVG component#79

Merged
jgonggrijp merged 31 commits intodevelopfrom
feature/actual-proof-svg
Mar 25, 2026
Merged

Visualize the actual proofs of the current problem with the TableauSVG component#79
jgonggrijp merged 31 commits intodevelopfrom
feature/actual-proof-svg

Conversation

@jgonggrijp
Copy link
Copy Markdown
Contributor

Together with #78 and #77, I think this finally completes the work that started with #51.

The pre-existing "Tableau / Proof" tab is split into a "Proof / Entailment" and a "Proof / Contradiction" tab, which visualize the respective proofs for the current problem. Please read the commit messages for a quick summary of my approach.

The tests pass and it generally seems to work well. That being said, at the time of writing, there are still a few known problems with the code on this branch. More on this below.

For now, I suggest trying the branch as follows.

  1. Enter and save two or more small problems. If you don't already have a few, I suggest the following.

The default example from the old demo

Every man is working
Everybody who is working has an expensive car
---
Every man owns a car

A similar example that I just came up with

Every child likes to play
Everyone who likes to play is creative
---
Every child is creative
  1. Navigate to one of these problems and make sure the "CCG / LLF" tab is selected.
  2. Press the "parse and prove" button. Wait for the contents of the "CCG / LLF" tab to disappear.
  3. Open the "Proof / Entailment" tab and observe a proof tableau.
  4. Open the "Proof / Contradiction" tab and observe a different proof tableau.
  5. Repeat from 2 with another small problem and observe two yet different proof tableaux.

It works for large problems, too, but in most cases, the overflow on the tab content element is preventing the proof from being visible. For example, if you try the first SICK problem, the Entailment tab will look empty, but if you inspect the DOM, you will find that the tableau is actually there. There is still a visible visualization in the Contradiction tab, but it is truncated on the right. I have held off from fixing this because I already took a quick peek at #78 and noticed that @XanderVertegaal fixed an overflow problem on that brach, which is probably the same issue.

Other known issues that I think are also best solved in coordination with #78:

  • As mentioned above, the "CCG / LLF" tab must be selected when the "Parse and prove" button is pressed, in order for the tableaux to be rendered. This is because AnnotationMenuComponent, which passes the proof trees to TableauSVG, is not (yet) subscribed to the AnnotationService#proofs$ observable. The AnnotationsParseResultsComponent is subscribed to the underlying #parse$ observable, however, and is therefore used (for now) to trigger the request to the backend. We could subscribe in two places, but it's probably more elegant (and perhaps also more efficient?) to coordinate a single place where this is done.
  • The types are just any all over the place. Without having seen the code, I suspect @XanderVertegaal already (partially) addressed this in Feature/langpro parse results #78.

Other other known issues:

  • The nltk2tableau conversion function deserves some solid documentation. I still want to add this before merging.
  • The TableauNodeComponent does not take the rule into account when calculating the width of leaf nodes (or of non-leaf nodes with very slim terms, for that matter). Also something I still want to fix before merging.

Without the still to be added comments, I expect the conversion logic to be difficult to follow if not outright mysterious. That being said, you are welcome to review it anyway and let me know what you find most opaque/offensive. 😉

@jgonggrijp jgonggrijp added the enhancement New feature or request label Mar 17, 2026
@XanderVertegaal
Copy link
Copy Markdown
Contributor

The types are just any all over the place. Without having seen the code, I suspect @XanderVertegaal already (partially) addressed this in #78.

I hate to disappoint, but I merely replaced any with unknown when it comes to the proofs. 😂

export type ParseResponseData = {
    ccg_parses: CCGParse[];
    proofs: unknown[];
};

If you need "any" help (see what I did there?) getting rid of the anys, let me know. I actually enjoy type wrangling.

Copy link
Copy Markdown
Contributor

@XanderVertegaal XanderVertegaal left a comment

Choose a reason for hiding this comment

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

Good work so far! I'm glad you added the comment about the proof being there (but hidden out of screen) for some of the longer problems. I was very confused. 😅 My solution for wide output was simply to add overflow: scroll to a couple of containers, so there is space to scroll horizontally. Still, I wonder if this is the neatest and most 'overzichtelijke' solution. Perhaps adding an external library with a zoom and drag interface would be a good addition at some point for both the tableaus and the (often very wide) syntactic parse result tables.

One note: I could not get this branch to work without commenting out this.parseResults = response.data.ccg_trees; in the AnnotationParseResults component. (This should be fixed once both of our branches are merged.)

As indicated in one of the comments. Let me know if you would like to delegate the type wrangling to me. I will continue my review after you have had time to add documentation to the tree2Tableau magic! 🧙‍♂️

<button ngbNavLink>
<fa-icon [icon]="faTree" class="me-2" aria-hidden="true" />
<span i18n>Tableau / Proof</span>
<span i18n>Proof / Entailment</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Much better! 'Tableau' is a representation but does not give information as to the actual content. (I don't call the syntactic parses "Table" either :P)

<ng-template ngbNavContent>
<la-annotation-tableau />
<section>
@if (proofs) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps for later, but maybe you could add an @else block with something like "No entailment data available yet."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe we should factor that out from the AnnotationParseResults component in #78 and reuse it across tabs to make them look consistent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

#82

</li>
<li [ngbNavItem]="3">
<button ngbNavLink>
<fa-icon [icon]="faTree" class="me-2" aria-hidden="true" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I tried looking for another icon we could use to differentiate the entailment tree from the contradiction tree, but no obvious candidates present themselves. Maybe link and linkSlash for entailment and contradiction, respectively?

(This is obviously not a high-priority item. 😅)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are symbols in Unicode that exactly convey the intended meaning: ⊤ and ⊥. I went with those.

it('does not cause a stack overflow', () => {
let nltk: any = {node: 'Model'};
const tableau: any = {nodes: []};
for (let i = 0; i < 100000; ++i) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

TIL about post-increments and pre-increments! Neat!

selector: "[tableau-tree]",
standalone: true,
imports: [TableauTerm],
imports: [TableauNode],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My IDE says that this import is unused. Is that true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The template of the tree component inserts instances of the node component. Maybe your IDE is failing to recognize that because the template is SVG instead of HTML?

Comment thread frontend/src/app/services/parse.service.ts Outdated
};
}

const emptyTableau = (): any => ({nodes: []});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Out of curiosity: any reason why you opt for an arrow function instead of a traditional one here? Brevity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, if a function is just a single expression that fits on one line, I like to write it as an arrow.

public proofs$ = this.parse$.pipe(map(extractProofs));
}

function extractProofs(response: ParseResponse) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a word of caution: catchError on line 21 returns null, so destructuring on line 32 will fail if that is the case. (This will undoubtedly be caught once we add proper typing.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing that out to me! I just assumed that it would somehow magically prevent errors from being problematic in any way.

return tree;
}

function nltkNode2tableauNode(nltk: any, tree: any) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I await the documentation for this with bated breath! It feels very magical but at the same time there is not a lot of code, which is good.

jgonggrijp added a commit that referenced this pull request Mar 17, 2026
@jgonggrijp
Copy link
Copy Markdown
Contributor Author

@XanderVertegaal I responded to most of your comments so far and added comments for the nltk2tableau jugglefest. Please feel welcome to review that part of the code while I finalize the remaining work.

Here is my full plan. Please let me know if you think I'm missing something or if you'd prefer me to proceed differently.

  1. Handle null response in extractProofs.
  2. Use open/closed lock symbols for leaf nodes.
  3. Make the tableau scroll on overflow.
  4. Horizontally center proof tab on root node.
  5. Take rule into account when calculating node widths. I intend to get at least this far tomorrow.
  6. Wait for you to merge Feature/langpro parse results #78 (it's OK if you do this later than tomorrow).
  7. Rebase on develop.
  8. Add "real" types for the proof representations to the types that you wrote for Feature/langpro parse results #78.
  9. Invite you for another review.
  10. Address the "no trigger unless viewing parse tab" problem, if that still persists after the rebase.
  11. Merge.

@XanderVertegaal
Copy link
Copy Markdown
Contributor

Your plan corresponds well to what we agreed, so please go ahead with the implementation. The only item on the list I would like you to clarify is 10: what do you mean with the 'no trigger unless viewing parse tab'?

Comment thread frontend/src/app/annotate/annotation-menu/annotation-menu.component.html Outdated
Copy link
Copy Markdown
Contributor

@XanderVertegaal XanderVertegaal left a comment

Choose a reason for hiding this comment

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

Very neat, thank you for the documentation! Have a look at the alignment of the circles+symbols at the end of the trees. Otherwise, please carry on!

@if(end) {
<svg:circle [attr.cx]="headX + 5" [attr.cy]="12" r="10" fill="salmon"/>
<svg:text #headText [attr.x]="headX" y="12" dominant-baseline="middle" font-weight="bold" fill="white"></svg:text>
<svg:circle [attr.cx]="headX + 8" [attr.cy]="11" r="10" fill="salmon"/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like the new symbols, but they are sadly still not aligned on my screen:

Image

Increasing the value of [attr.cx] seems to help, with totalW/2 getting it close to perfect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Argh! Which browser is this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

By the way, I investigated using faLock and faLockOpen instead, but the icons would display with zero width and become invisible. I wasn't able to make it work and I suspect it is because the SVG icon is nested in an HTML element which is then again nested inside the larger SVG tableau.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Argh! Which browser is this?

Never mind, it happens in my browser, too. Apparently I had to rebuild the container to see it.

Copy link
Copy Markdown
Contributor

@XanderVertegaal XanderVertegaal Mar 20, 2026

Choose a reason for hiding this comment

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

I see. For what it is worth: I am using Firefox.

this.cdref.detectChanges();
const width = this.treeDimensions.width;
if (width) this.element.nativeElement.parentElement.scroll({
left: Math.max(0, width / 2 - 500)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is 500 here chosen as an arbitrary (but sensible) minimal width?

Copy link
Copy Markdown
Contributor Author

@jgonggrijp jgonggrijp Mar 19, 2026

Choose a reason for hiding this comment

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

It's deliberately slightly more than half of the constrained body width. Without it, the center of the root would be on the left edge. I went with a slight bias towards the right because the description of the node starts on the left.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You seem to have been right that the root is always at the middle of the top edge, by the way.

// of the todo list. tsc is apparently unable to infer that `todo` is
// guaranteed to be nonempty on the next line, hence the silencing
// comment.
// @ts-ignore
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I understand why the @ts-ignore is here, but instead of silencing it completely, maybe a type assertion todo.pop() as [any, any] or a non-null assertion todo.pop()! would be neater.

This is not something I insist on, but rather something you might want to keep in mind when you add the types.

// guaranteed to be nonempty on the next line, hence the silencing
// comment.
// @ts-ignore
let task: [any, any] = todo.pop();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are these lets rather than consts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because I'm still used to var semantics and mentally hoisting them to the top of the function scope. 😅 (To be honest, I don't believe that let and const were an improvement to the language and I'm only using them in order to respect the local code convention.) I'll change it to const.

// of
// https://en.wikipedia.org/wiki/Trampoline_(computing)#High-level_programming.
// The gist is this: instead of recursing, functions return pieces of work
// ("thunks") that should be executed next. The trampoline, which in this
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Interesting. Would you say trampolining is generally superior to recursion when building nested datastructures like this? Are there any trade-offs, or areas in which recursion is preferable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When the datastructure might be arbitrarily deeply nested, you kind of "have" to use trampolining in order to avoid stack overflows. I became aware of this because of GHSA-qpx9-hpmf-5gmw and now cannot un-see that potential problem anymore, hence my vigilance. I'm actually planning to add a generalized _.trampoline to Underscore in order to provide a foundation for solutions like this one.

Suprisingly, there is no significant cost to trampolining versus recursion performancewise. The number of function calls is the same and the memory consumption is similar.

Conceptually, trampolining is one possible way to implement continuation passing style. Which has some theoretical elegance, but is more taxing on the brain than the conventional "direct" style. I would say that recursion is still preferable when you can get away with it, but in this case I went straight for trampolining because I think there might actually be a real risk that these trees get (slightly) too large.

tree.nodes.push(node);
// Tease out the different parts of the stringified node payload. The
// following lines will be rewritten using regular expressions.
const lines = nltk.node.split('\n');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps this string decoding could live in its own function (so we can test it in isolation), but you might want to wait for that until you have introduced the regexes.

jgonggrijp added a commit that referenced this pull request Mar 19, 2026
Review suggestion in #79

Co-authored-by: Xander Vertegaal <73882506+XanderVertegaal@users.noreply.github.com>
@jgonggrijp
Copy link
Copy Markdown
Contributor Author

Your plan corresponds well to what we agreed, so please go ahead with the implementation. The only item on the list I would like you to clarify is 10: what do you mean with the 'no trigger unless viewing parse tab'?

The effect that I described to you previously, where the tableau would not render unless the "CCG / LLF" tab was selected while I pressed the "Parse and prove" button. However, this seems to have been solved already, possibly because I accepted your suggestion to add the share operator. It does not matter anymore which tab is shown when I press the button.

I'll double-check after rebasing just to make sure, though.

- Adopt same names for tree node parts as in LangPro API.
- Distinguish node ID vs. numerical index of appearance.
- Distinguish node vs. term vs. head. Mods and args contain terms, too.
Based on actual output from the LangPro container.
jgonggrijp added a commit that referenced this pull request Mar 19, 2026
jgonggrijp added a commit that referenced this pull request Mar 19, 2026
@jgonggrijp jgonggrijp force-pushed the feature/actual-proof-svg branch from cf484da to 23b81c3 Compare March 19, 2026 17:50
@jgonggrijp jgonggrijp force-pushed the feature/actual-proof-svg branch from 23b81c3 to 5899df2 Compare March 19, 2026 22:46
jgonggrijp added a commit that referenced this pull request Mar 19, 2026
I initially distinguished between terminal and internal trees as well,
but applying this typing in the logic and convincing tsc that
everything was fine became too unwildly.
jgonggrijp added a commit that referenced this pull request Mar 19, 2026
@jgonggrijp
Copy link
Copy Markdown
Contributor Author

@XanderVertegaal I force-pushed again because I needed to insert some of the new types early in history. Otherwise, all of the intermediate commits would be uncompilable.

I did docker compose down in order to rebase and then did docker compose --build up afterwards to check that all the tests would still pass. They did, but when trying to visit the frontend, I discovered that my templates don't typecheck yet. I will fix that later but I need to leave the library now. You are welcome to review the types in the meanwhile.

The only changes I still expect to make is type fixes in the templates and/or a simplification in the @/types, where I simply collapse all ProofNode subtypes into a single object type with all fields optional. That is a bit less correct but easier to work with. In the latter case I might rebase the last two commits one more time.

I initially distinguished between terminal and internal trees as well
as between terminal nodes, internal nodes and unsupported nodes, but
applying this typing in the logic and convincing tsc that everything
was fine became too unwildly.
@jgonggrijp jgonggrijp force-pushed the feature/actual-proof-svg branch from 5899df2 to a5d3aca Compare March 23, 2026 21:52
@jgonggrijp
Copy link
Copy Markdown
Contributor Author

Done.

Copy link
Copy Markdown
Contributor

@XanderVertegaal XanderVertegaal left a comment

Choose a reason for hiding this comment

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

Great work! 👍

Comment thread frontend/src/app/annotate/annotation-menu/annotation-menu.component.html Outdated
}
]
}
public readonly tree = input.required<ProofTree>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(Utterly minor: I think inputs are conventionally put at the top of the component. At least, that is where most people will expect to find them.)

}

set tree(value: any) {
set tree(value: ProofTree) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It feels so good to see these anys replaced with actual types <3

Comment thread frontend/src/app/services/parse.service.ts Outdated
@XanderVertegaal
Copy link
Copy Markdown
Contributor

image

Glorious! 😍

jgonggrijp and others added 2 commits March 25, 2026 14:49
Co-authored-by: Xander Vertegaal <73882506+XanderVertegaal@users.noreply.github.com>
@jgonggrijp jgonggrijp merged commit 0ad5ca8 into develop Mar 25, 2026
1 check passed
@jgonggrijp jgonggrijp deleted the feature/actual-proof-svg branch March 25, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants