Skip to content

Refactor flowchart placement to per-clause direct-target protocol#17

Draft
tmaier-kettering with Copilot wants to merge 4 commits into
flowchart-by-blockfrom
copilot/fix-flowchart-generation-rules
Draft

Refactor flowchart placement to per-clause direct-target protocol#17
tmaier-kettering with Copilot wants to merge 4 commits into
flowchart-by-blockfrom
copilot/fix-flowchart-generation-rules

Conversation

Copilot AI commented Nov 4, 2025

Copy link
Copy Markdown

The flowchart generator was placing entire if/elif/else chains atomically and violating the direct-target placement rule. Blocks were placing nodes they weren't directly connected to, and loop tail blocks were emitting sequential successors instead of only back edges.

Changes

structure.py - Linked clause model

  • Replaced IfChainNode with individual clause types: IfClause, ElifClause, ElseClause
  • Each clause links to its false_child (next elif/else), representing actual control flow graph
  • Added _build_clause_chain() for recursive clause construction

placer.py - Uniform direct-target placement

  • Protocol: All blocks now place only direct targets, wait for subtree completion, then place next target
  • If clause: Places true body at (+1,0), false_child at prev_row+1, then successor in if's column
  • Elif clause: Places true body, then false_child at elif_row+1. Does NOT place successor
  • Else clause: Places body only. Does NOT place successor
  • Loop: Body at (+1,+1), tail blocks emit back edge only, header places successor post-body
  • Added return values (last_block_ids, max_row, max_col) to track actual bounds

Result

Clauses now cascade vertically (each +1 row below previous), if header owns the post-chain successor, and loop tails only emit back edges:

# Before: All clauses at same row
if x < 2:     # (0, 2)
  a = 1       # (0, 3)
elif x < 4:   # (1, 3)  ← wrong, same row as if+1
  b = 2

# After: Clauses cascade  
if x < 2:     # (0, 2)
  a = 1       # (0, 3)
elif x < 4:   # (1, 3)  ← if_row + 1
  b = 2       # (1, 4)
elif x < 6:   # (2, 4)  ← elif_row + 1
  c = 3
Original prompt

The flowchart generation rules are not correct.

Global Rules (apply to every block)

  1. Direct-target only: A block may place only blocks it is directly connected to by an outgoing edge (sequential successor, body entry, or alternate/False).
  2. Wait, then resume: After placing a direct target, the placer waits until that target (and everything under it) is fully placed, then returns to the current block to place its next direct target(s).
  3. DFS-lexical order: Overall traversal is depth-first in source order (header → its first outgoing edge → finish subtree → next outgoing edge → …).
  4. Loop tails: The last child inside a loop never places a sequential successor; its only outgoing edge is the back edge. The loop header later places the loop’s successor.

Only Special Case: if / elif / else

  • Chained children: Treat each elif or else as the False child of the previous clause (if → first elif → next elifelse).
  • Successor owner: The original if header is the only block that places the post-chain successor (not the last elif/else). Branch ends connect to it, but they do not place it.

What to change (by file)

structure.py

  • Ensure the if-chain is exposed as a linked sequence of clauses:

    • IfClause (header + true body) with an optional false_child pointer to the next clause (ElifClause or ElseClause).
    • This is just linkage; no placement logic here.

placer.py

Replace any “whole-chain” placement with per-clause, direct-target placement:

  1. For every block type (default, for, while, if/elif/else) adopt the same protocol:

    • Place the header at the cursor.

    • Place the first direct target (only one at a time):

      • Sequential successor (default blocks) → below at (+1,0).
      • Body entry (headers) → per its start offset.
      • Alternate/False (where applicable) → placed only after the body subtree is done.
    • After the first target’s subtree completes, return and place the next direct target.

    • When a block has no more direct targets, return its max row/col used to the caller.

  2. If / Elif / Else specifics (using the global protocol):

    • If header:

      • Place True body entry at (+1,0); wait for the entire True body to finish.
      • Compute the next clause start as: same row as header + 1, and one column to the right of the rightmost column used so far by all prior branches.
      • If there is a false_child, place that header there and recurse (it will do the same with its own body and next false_child).
      • When the chain returns (no more false_child), the if header places the Successor in the header’s column, one row below the deepest branch, and connects all branch ends to it.
    • Elif clause:

      • Place True body entry below it; wait for it to finish.
      • Compute next clause start the same way (right of rightmost used so far).
      • If there is another false_child, place it; otherwise return without placing a successor.
    • Else clause:

      • Place Body entry below it; wait to finish.
      • Return without placing a successor (the if will).
  3. For / While headers:

    • Place Body entry at (+1,+1); wait for the full body to finish.
    • Then place the loop Successor in the header’s column, one row below the body’s last row.
    • Ensure the body’s last block emits only the back edge to the header (no sequential successor).

registry.py / models.py

  • No behavioral change; just ensure every place_* routine returns actual bounds used (max row/col) so the next sibling can be positioned “to the right of rightmost so far”.

Tests (acceptance)

  • Clause chaining: Multi-elif chains must show each clause header at header_row+1, and columns strictly increasing by at least 1, based on actual body width of prior clauses.
  • Owner successor: In chains with and without else, the if places the successor (assert its col == if_col and row == deepest_branch_row+1).
  • Loop tail: Last statement in a loop does not place a sequential successor; the loop header places the successor after the body completes.
  • Direct-target only: No block ever places a node that it is not directly connected to by an outgoing edge (inspect edge sources vs. placement calls).

This yields a single, uniform placement algorithm for all blocks, with one explicit exception: the if header owns the post-chain successor.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits November 4, 2025 17:11
Co-authored-by: tmaier-kettering <109093855+tmaier-kettering@users.noreply.github.com>
Co-authored-by: tmaier-kettering <109093855+tmaier-kettering@users.noreply.github.com>
Co-authored-by: tmaier-kettering <109093855+tmaier-kettering@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix flowchart generation rules for block placement Refactor flowchart placement to per-clause direct-target protocol Nov 4, 2025
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.

2 participants