Skip to content

2.0 - #1

Open
micycle1 wants to merge 17 commits into
masterfrom
2
Open

2.0#1
micycle1 wants to merge 17 commits into
masterfrom
2

Conversation

@micycle1

Copy link
Copy Markdown
Owner

No description provided.

micycle1 and others added 17 commits August 23, 2026 18:06
Stage 0 of separating the machine/model layer from Processing. No source changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h a listener

Stages 1 and 2 of separating the machine/model layer from Processing.

Moore's per-state output symbol lived on the graphical p5.State and was read back by
the machine through the view (PApplet.view.getStateByID(s).getMoorePush()), so a Moore
machine could not run without a live GUI. It now lives in Model.stateOutput, keyed by
state ID -- the same ID those lookups already passed in, so behaviour is unchanged.
The entry is deliberately not removed on deleteState: the view parks the deleted State
in its disposedStates graveyard for undo, and dropping the output would blank the
symbol on undo.

Model no longer calls the view. The six view.rebuild()/deleteState() calls became
ModelListener events, and View implements ModelListener. View.rebuild() now asserts its
invariant (every model state has a live GUI node) so a desync fails legibly instead of
NPE-ing on an edge endpoint.

LogicalTransition's State-taking constructor, which resolved IDs via
view.getIDByState(), is gone. EntryArrow resolves head/tail to IDs once in its
constructor -- the UI boundary -- and builds the transition from ints.

machines/ and model/ no longer reference the view. Remaining UI coupling: machines still
write results into main.Step, and DPA still owns controlP5 toggles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Step class

Stage 3 of separating the machine/model layer from Processing.

Every machine used to push its verdict into main.Step via Step.setMachineOutcome(boolean),
and DPA pushed its stack via Step.setStack(String). That made the machines depend on the
graphical environment and created a Model -> Machine -> Step -> Model cycle.

Machine now returns data: RunResult(Outcome, output) from run(), and
StepResult(state, terminated, accepted, stack, output) from beginStep/stepForward/
stepBackward. beginStep returns a result too, so the DPA's initial stack still shows
before the first step. Step absorbs results in applyStepResult() and caches the stack and
output it renders, replacing the removed Model.getOutput() instanceof dispatch.

Model.runMachine became Model.run and no longer raises notifications or reaches into
p5.Notification; Controller.present(RunResult) does that, reproducing the previous
messages including the transducer "Machine Terminated" text.

Known existing bugs are preserved and marked in place with "BUG (preserved)" comments:
Mealy.run reporting COMPLETE when stuck, Moore.run using the destination state's output
where stepForward uses the source's, and DPA.run neither seeding the stack nor consulting
the accept-by-empty-stack option.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 4 of separating the machine/model layer from Processing.

DPA held two static final controlP5.Toggle fields, built in a static initialiser against
PFLAP.cp5, and read its acceptance semantics straight off them. That tied the machine to a
live GUI, tied widget creation to class-load order, and meant PFLAP.reset() had to call
DPA.hideUI() even when the current machine was a DFA.

The machine now reads a DpaOptions record from the model. The new p5.DpaOptionsPanel owns
the widgets -- same positions, colours and mutual-exclusion behaviour -- and publishes
option changes into the model. The initial stack symbol moved onto the same record, so
Model.setInitialStackSymbol and its instanceof dispatch are gone.

The lambda symbol moved to model.Symbols.LAMBDA, since it is machine semantics rather than
a presentation constant; Consts.lambda now delegates to it for the UI callers.

Checkpoint: machines/ and model/ now have zero Processing, controlP5, JavaFX, p5, main or
transitionView imports, verified by grep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 5 of separating the machine/model layer from Processing, and the point of the whole
exercise: 76 tests that construct, configure and run every machine type with no PApplet,
no controlP5 and no JavaFX anywhere on the stack.

ModelFixture builds machines directly in the model. Coverage: DFA accept/reject/FAIL,
empty input, self-loops and stepping; Mealy and Moore output-tape accumulation and
stepBackward truncation; DPA acceptance by empty stack and by accepting state, the lambda
pop wildcard, lambda push elision and stepBackward stack restoration; model-level graph
mutation, ID allocation, assureUniqueTransition and listener firing; and a parameterised
smoke test across all four types.

Known existing bugs are pinned rather than fixed, each named "BUG (preserved)":
- DPA.run() never seeds the stack, so it throws NullPointerException on the first symbol.
  This is why the shared smoke test excludes DPA from its fast-run case.
- DPA.run() consults only accept-by-accepting-state, so accept-by-empty-stack alone can
  never succeed -- and conversely an empty stack accepts even when only accept-by-state is
  selected.
- Moore.run() emits the destination state's output symbol where stepForward() emits the
  source's, so fast-run and stepping produce different tapes ("yz" vs "xy").
- Mealy.run() reports COMPLETE both when stuck and when finished.
- The ID counter diverges from the state count after a delete.

Model gains removeListener as the counterpart to addListener, so tests can deregister.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 6a of separating the machine/model layer from Processing: pure renaming, no
behaviour change. Model becomes AutomatonModel, LogicalTransition becomes Transition, and
the machines move under automaton/machines. All 76 tests still pass.

Class-name casing for the machines themselves (DFA, DPA, Mealy, Moore) is left alone --
that is cosmetic churn, deferred to the rename stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 6 of separating the machine/model layer from Processing. The model was an all-static
god object, so only one automaton could ever exist and nothing could be constructed in
isolation.

AutomatonModel is now an instance owning its graph, states, accepting set, initial state,
ID counter and DPA options. Machines take the model in their constructor rather than
reaching for statics, and MachineFactory builds the right one for a MachineType -- the
switch that used to live in the sketch's reset().

New ApplicationController is the one class that knows about both halves: it owns the model,
view, undo history and the DPA options panel, and implements AppContext. Commands are
handed that context in execute/undo instead of reaching for globals, and HistoryHandler is
an instance built around it. PFLAP.PApplet keeps a single 'app' static in place of the
several it used to hold.

Two commands stopped carrying graphical objects: deleteTransition now takes the model
transitions (the caller translates its arrow at the UI boundary) and moveState refers to
its node by model ID, resolving it through the view. PFLAP.modes is gone, replaced by
automaton.MachineType; the switches over it remain for now.

State-degree and reachability queries moved onto the model, so Notification no longer needs
access to the Guava graph.

Tests now build their own AutomatonModel per fixture, which is what lets
AllMachinesSmokeTest assert two automata of the same type coexisting -- impossible before.
84 tests pass, and the app launches and runs without error.

Preserved bugs are unchanged, including Batch.createMoveBatch recording old == new, now
commented at the site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 7 of separating the machine/model layer from Processing.

main.Step was simultaneously the stepping controller, its state, and its Processing
overlay. The bookkeeping -- visited states, position in the input, whether a verdict has
been latched -- now lives in automaton.StepSession, which has no GUI dependency. Step keeps
only the controlP5 buttons, the cached border graphic and the readout, renders a session,
and raises the accept/reject notification when the session reports it.

The overlay used to be told the verdict by the machine calling back into it. The session now
exposes justTerminated(), true on the single step that reached a verdict, so the
notification still fires exactly once. That flag needed clearing at the top of
forward()/backward(): when stepping past an already-decided verdict the model is not
consulted at all, so it would otherwise stay set and re-announce. StepSessionTest covers it.

14 new tests cover begin/end, consuming and rewinding the input, latching a verdict,
idempotence past the end, replaying recorded states after a rewind, and the stack and output
readouts. 98 tests pass; the app launches and runs without error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 8 of separating the machine/model layer from Processing. This intentionally breaks
existing .dat files, which now report the "different version of PFLAP" notification.

A save file used to be serialised graphical p5.State objects plus a log of undo commands;
the model was never saved, it was rebuilt by replaying that log on load. So the file format
was tied to the shape of the command classes and of a Processing view object, and loading
had to patch the ID counter up afterwards.

A save file is now SaveFile(formatVersion, ModelSnapshot, ViewLayout): the automaton -- its
states with their Moore output symbols, its transitions, the initial and accepting states,
the DPA options and the ID counter -- alongside where the nodes are drawn and what they are
labelled. Save and load moved from HistoryHandler onto ApplicationController, which is the
only class that sees both halves.

Consequences, all deliberate:
- Undo history is no longer saved, so Command and p5.State both drop Serializable, and
  State's fields lose their now-meaningless transient markers.
- The odd save-loop offset and the setnextStateID(nStates()) patch-up after load are gone by
  construction rather than by being fixed: the ID counter is snapshotted directly.
- Loading applies the layout before restoring the model, because the view asserts every model
  state has a graphical node and restoring fires those events.

ModelSnapshotTest round-trips through real serialisation, which immediately caught DpaOptions
not being Serializable -- saving a DPA would have failed at runtime. 107 tests pass; the app
launches and runs without error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… on the mode

Stage 9 of separating the machine/model layer from Processing.

The same switch over the current mode was repeated at six sites -- arrow labels, the
transition-entry prompts, the Moore output textfield, the step readout, and the stack-symbol
dialog in both step() and fastRun(). Adding a machine type meant finding all of them.

MachineType now answers those questions: usesStack(), hasStateOutput(), isTransducer(),
entryFields(), transitionLabel(Transition) and stepStatusLine(StepSession). These return
data -- field lists and label text -- never widgets, so the model layer stays GUI-free. A
full strategy interface would be overkill for four types and six sites.

The one switch left is in Controller.open(), which maps a loaded machine type onto which
menu item to grey out. That is a per-item lookup, not machine behaviour, so it stays.

Controller.present() is also gone: it duplicated what ApplicationController.runMachine
already does.

14 new tests cover the type behaviour; 121 pass in total. The app launches and runs without
error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A KISS pass over the decoupling work. No behaviour change; 121 tests still pass.

- Delete AutomatonState. It ended up a one-field mutable box around a String
  with no callers outside the model, so the model just holds
  Map<Integer, String> stateOutput directly.
- Delete MachineFactory. It was a file wrapping one switch; MachineType.create()
  puts it with the other per-type answers.
- ModelSnapshot no longer carries StateRecord/TransitionRecord. Transition is
  already an immutable Serializable value in the same package, so the parallel
  DTOs were duplicating it field for field.
- PFLAP.mode is gone as a readable global. It duplicated
  ApplicationController.type, which is the owner; all eight read sites now ask
  app.type(). What remains is a private PFLAP.resetMode, which is only the
  "build this type on the next reset" request channel, and is named to say so.
- Drop AutomatonModel.removeListener (no callers) and AppContext.type()
  (nothing asked for it through the interface).
- Consts.lambda was an alias for Symbols.LAMBDA; use the one name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Thirteen unused imports across Controller, PFLAP, State, AbstractArrow and
  EntryArrow, left behind when call sites moved to app.model()/app.history()
  and the machine constructors moved into MachineType.create().
- PFLAP.app's javadoc claimed it was "one static, replacing the tangle of them
  this class used to hold". It is not: p, cp5, view, controller, historyList and
  the colour fields are all still statics sitting beside it. Say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each machine had two loops -- run() and stepForward() -- and they had drifted
apart. AbstractMachine now holds the one walk, and run() drives stepForward()
rather than reimplementing it, so they cannot disagree again. Subclasses supply
only what differs: which transition matches, what taking it does, and what
counts as acceptance.

Fixed as a result:

- DPA.run() never seeded the stack, so it threw NullPointerException on the
  first symbol (char == Character unboxes null before || short-circuits).
- DPA.run() consulted only accept-by-accepting-state, so accept-by-empty-stack
  could never accept, and an empty stack accepted even when not asked for.
  Both paths now use the same condition.
- The DPA "stack" was a FIFO LinkedList (add/poll). It is now an ArrayDeque used
  as a stack: pushing "AB" leaves A above B, and the last push is popped first.
- A lambda pop consumed the stack top. It now inspects and consumes nothing,
  which is what lambda means everywhere else, and it applies to an empty stack.
- Moore.run() emitted the destination state's output while stepForward() emitted
  the source's ("yz" vs "xy"). Both now emit the output of every state occupied,
  starting with the initial one, so n symbols in give n+1 out.
- Mealy and Moore reported COMPLETE both when finished and when stuck. Getting
  stuck is now FAIL, carrying the tape written so far.
- stepBackward kept a single previous-stack snapshot, so a second backward step
  restored the wrong stack. Each machine now snapshots per step and rewinds
  exactly, without limit. Moore also truncated by the wrong state's output.
- A missing initial state threw IllegalArgumentException out of Guava. Machines
  check for it and report FAIL, and stepping terminates instead.
- assureUniqueTransition compared the whole payload, so a DFA accepted two
  transitions on one symbol as long as their destinations differed. The rule now
  belongs to the machine: a DFA looks at the symbol, a DPA also at the stack top
  (where a lambda pop clashes with everything).

Also: Machine.stepBackward() no longer takes the state to return to, since the
machine knows; Transition is immutable, tail-first, and documents why it is not
a record (Guava networks identify edges by identity).

The BUG (preserved) tests are now expected-behaviour tests. 150 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Model:
- deleteState left the automaton referring to a state that no longer existed:
  its accepting mark stayed in the set and it stayed the initial state. It now
  clears both. The state's output symbol is still kept, so undo can resurrect a
  Moore node without blanking it.

Commands:
- Replaced the per-state deleteState with one deleteStates command covering a
  whole selection. Undoing a batch of separate per-state deletions restored each
  state together with its edges, so the first one restored could carry an edge to
  a state not yet re-added; Guava silently re-adds that endpoint node without the
  view hearing about it, leaving a model state with no graphical node. Restoring
  every state first and the transitions after cannot get that wrong. The command
  also captures and restores the accepting/initial marks that deleteState now
  clears.
- Batch.undo ran forwards. It now unwinds last-first, like a series of undos.
- moveState held references to live PVectors, so what undo would restore could be
  altered by a later move. It copies on the way in and out now.
- A single-state drag recorded mouseClickXY as the origin -- the point the user
  clicked, not the node's centre. Grab a node off-centre and undo would shift it.
  PFLAP now captures the node's own position when the drag begins.

View:
- newState labelled the node with liveStates.size() while keying the model by the
  allocated ID, so after a delete the visible label and the real ID disagreed. It
  allocates once and uses that for both.

History:
- movetoIndex accepted an index up to size+1 and then looped forever, because
  redo() stops advancing once it runs out of history. The reachable range is
  [-1, size-1].
- Controller tested getHistoryStateIndex() < -1 to disable Undo, but the index
  bottoms out at -1, so Undo stayed enabled with nothing to undo. It now asks
  history.canUndo()/canRedo().

Also: a transducer that cannot consume its whole input now says so, with the
tape it managed, instead of reporting plain success.

152 tests pass. No BUG (preserved) markers remain in the tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rename, resize and Moore output were the last user actions that wrote
straight through to the model or the node, so they could not be undone
and they silently invalidated the redo stack. They are commands now.
Resize records one step for the whole slider drag rather than one per
value it passed through.

A node rebuilt from a save file handed its resize slider the default
radius instead of its own, so the first touch of the slider jumped. The
range is now set before the value, so a saved radius above the slider's
initial 0..100 is not clamped on the way in.

Loading resets the live machine as its first act, so a file that fails
later leaves the user with neither their old automaton nor the saved
one. SaveFile.validate runs before that, and rejects a state with no
node, a node with no state, an edge naming an unknown endpoint, an
initial or accepting state that does not exist, and an ID counter that
would hand out an ID already in use. View.applyLayout's javadoc claimed
the opposite load order; corrected.

AutomatonModel now refuses a transition whose endpoints it does not
know, and an initial state that does not exist. Guava's addEdge
silently creates a missing endpoint, which is exactly the desync
View.checkInvariant was written to catch -- better to refuse at the
source than to throw out of the draw loop later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant