summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 14:40:04 -0700
committerCliff Click <cliffc@acm.org>2026-08-10 14:40:04 -0700
commit34c2efae1f2dac2449a52fd2913b4737111e692c (patch)
tree7d84c3ac4043cb03d6c3e1229bfb4e2cc2d4a8d4
parent8f5b226cf472bc249b935320b8f3b7ac71d09579 (diff)
downloadSimple-34c2efae1f2dac2449a52fd2913b4737111e692c.tar.gz
Simple-34c2efae1f2dac2449a52fd2913b4737111e692c.zip
Handle some escaped chars in strings
Add a explicit "emit_main" flag in case you use no_run. Fix a make parallel bug
-rw-r--r--chapter25/Makefile2
-rw-r--r--chapter25/WIP-HANDOFF.md387
-rw-r--r--chapter25/docs/examples/A_helloWorld.smp2
-rw-r--r--chapter25/parser-simplification-plan.md371
-rw-r--r--chapter25/src/main/java/com/seaofnodes/simple/Parser.java26
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java13
-rw-r--r--chapter25/src/test/java/com/seaofnodes/simple/Simple.java5
7 files changed, 38 insertions, 768 deletions
diff --git a/chapter25/Makefile b/chapter25/Makefile
index 7497f98f..1ab66a60 100644
--- a/chapter25/Makefile
+++ b/chapter25/Makefile
@@ -146,7 +146,7 @@ CPU_ABI:= $(CPU_PORT)_$(CALL_CONV)
RELEASE:= build/release
RELEASE_SYS:= $(RELEASE)/$(CPU_ABI)/sys.o
-$(OBJS)/lib_$(CPU_ABI)/sys.o: $(CLZDIR)/main/.mtag $(SYS_SMPS)
+$(OBJS)/lib_$(CPU_ABI)/sys.o: $(CLZDIR)/main/.mtag $(CLZDIR)/test/.ttag $(SYS_SMPS)
@echo "simping " $@
@$(JVM) com.seaofnodes.simple.Simple --cpu $(CPU_PORT) --abi $(CALL_CONV) --norun -o $@ src/main/smp/sys.smp
diff --git a/chapter25/WIP-HANDOFF.md b/chapter25/WIP-HANDOFF.md
deleted file mode 100644
index 9c4503d8..00000000
--- a/chapter25/WIP-HANDOFF.md
+++ /dev/null
@@ -1,387 +0,0 @@
-# Chapter 25 direct-to-SSA parser rewrite handoff
-
-Updated 2026-08-04 for transfer to another AI session/machine.
-
-## Read this first
-
-- Repository: `Simple/chapter25`
-- Branch: `ch25-parser-simplification`
-- Preservation tag: `ch25-parser-before-simplification` at `4d6a59ed`
-- Last pushed commit: `4f8759b3 Rotate random seed and fix more bugs`
-- Remote branch was at the same commit when this handoff was written.
-- The working tree is dirty **after** that pushed checkpoint. Preserve it.
-- The full original design/rollback plan is in
- `parser-simplification-plan.md`. This file supersedes the old WIP handoff.
-
-Do not reset, clean, or mechanically restore the working tree. The dirty files
-contain Cliff's next experiment. Untracked `.argsM.txt` and `.out.txt` are
-transient local files, not source changes; `.argsM.txt` was locked when the
-handoff test run was attempted.
-
-## Why this branch exists
-
-The constructor syntax rewrite exposed a deeper architectural flaw. This repo
-is an experiment in parsing a high-level language directly into SSA in one
-pass, without first constructing an AST. Forward declarations therefore remain
-unknown during parsing. The parser had nevertheless been consulting provisional
-types to choose permanent graph structure.
-
-The central rule of the rewrite is:
-
-> Syntax and resolved lexical binding choose SSA topology. Types may sharpen,
-> optimize, and reject that graph, but provisional types must not decide its
-> shape.
-
-The branch is deliberately unsquashed. Git, especially the preservation tag,
-is the “this was a giant mistake; walk it back” path. Removed mechanisms should
-be restored by named revert/cherry-pick/new commits, not by copying mysterious
-pieces out of history.
-
-## Original goals
-
-The original plan had these major parts:
-
-1. Remove parser-supplied `_con` lower-bound types from Phi, Load, Store, and
- similar nodes. Make `compute()` total on weak inputs.
-2. Stop using inferred types for integer-versus-floating arithmetic topology.
-3. Separate unresolved lexical field binding from type inference, eventually
- using scope-owned unresolved loads/stores resolved at scope close.
-4. Parse unresolved stores against bulk alias `#1`, then recover precise alias
- structure monotonically after the alias is known.
-5. Keep call shape syntactic: `ptr.fld(args)` always carries `ptr` in the hidden
- self slot, even if the loaded function is ultimately a top-level function.
-6. Separate authoritative declared function arguments from inferred return and
- call-site information; reconsider/remove `FunNode._sig` as cached state.
-7. Replace boolean field-final knowledge with four states: unknown, mutable,
- final, and conflicting/BAD_FINAL, including TypeTest coverage.
-8. Preserve `Type.closeOver()` and canonical cyclic named-type closure without
- making parser topology depend on whether closure has happened yet.
-9. Advance chapter-by-chapter, keeping earlier chapters and TypeTest green.
-
-Those remain the completion goals unless explicitly revised below.
-
-## Goals and invariants learned during implementation
-
-The following refinements became equally important:
-
-- Optimization/worklist order must not affect semantics or trigger structural
- assertions. Random seed changes are a diagnostic tool, not a way to select a
- preferred result. Minor graph-print and register-spill goldens may vary, but
- should eventually be hardened where practical.
-- `compute()` must be monotonic and simpler than `idealize()`. When both inspect
- the same facts, `idealize()` should inspect them in the same order.
-- An idealization must preserve already-known type information immediately;
- the expensive `IterPeeps.progressOnList` assertion is intentionally catching
- one-step Church-Rosser/monotonicity failures.
-- Maintain Node keep/unkeep lifetime discipline when moving, cloning, or
- replacing graph pieces.
-- The parser carries only one bulk memory state. Precise alias slicing and
- `MemMergeNode` are graph semantics, not nested parser state.
-- At every program slice, alias memories are disjoint and collectively cover
- every alias exactly once. Bulk memory means “all aliases not split out here.”
-- Calls, call ends, returns, and memory parameters deliberately use opaque bulk
- memory to control graph size. Precision across those boundaries comes from
- careful inlining.
-- Public concrete bulk alias-`#1` memory is never final. A single final bit
- cannot truthfully describe an aggregate of unrelated unsplit aliases.
- Private singleton memory and precise slices may retain finality.
-- Precise aliases can peek through a `MemMerge`. Bulk memory can peek through
- only when its excluded-alias coverage agrees with the merge.
-- A bulk Phi carries an exclusion BitSet (`All - E`); parallel `MemPhiNode`s
- carry the precise aliases in `E`.
-- Mode selection for unified numeric nodes is a one-shot structural event.
- `_mode` participates in GVN identity; flipping it unlocks and reinitializes
- the node before IterPeeps observes it.
-
-## What has been completed on this branch
-
-### Preservation and early constraint removal
-
-- Created `parser-simplification-plan.md`, the dedicated branch, and permanent
- pre-experiment tag.
-- Fixed the `_Scan` constructor migration that initially obscured the work.
-- Removed the parser constraint from ordinary Phis. Weak loop Phi inputs are
- tolerated. The dead ordinary-Phi type field and serialization were removed.
-- Split memory Phis out of ordinary Phi logic:
- - `MemPhiNode` represents one precise alias.
- - `BulkMemPhiNode` represents `All - exclusions`.
- - Memory-specific compute/ideal behavior moved out of base Phi where useful.
-
-### Casts, guards, and conversions
-
-- Split the old overloaded Cast concept into:
- - `GuardNode`: delayed zero/nonzero branch refinement with direction.
- - `CheckCastNode`: proof/runtime type check which must fold or diagnose.
- - `ConvertNode`: the one numeric/value conversion abstraction.
-- Removed the old `liftExpr`-style broad parser helper in favor of explicit,
- smaller conversion sites.
-- Removed the obsolete `Evaluator`; `Eval2` is authoritative.
-
-### Parser memory simplification and alias recovery
-
-- Removed the parser's eager nested precise-memory slicing. Parser `ScopeNode`
- state now tracks bulk memory rather than a parser-owned `MemMerge` bundle.
-- Alias `#1` is the unresolved Store state. A Store sharpens once to `#N`; all
- later alias evidence must agree.
-- Store sharpening inserts/reconstructs precise memory plus a `MemMerge`,
- preserving the bulk remainder locally.
-- Added lazy creation/widening of precise memory Phis from bulk Phis and
- MemMerges, including recovery when a parallel precise Phi already collapsed.
-- Added checks around the exactly-once memory-slice invariant and fixed several
- load/store push-through oscillations and missing slice cases.
-- `EscapeNode` is recognized as a precise alias consumer of a bulk memory Phi.
-- Parser constructor handling now recognizes named constructors structurally
- and threads private constructor memory correctly; class-initializer return
- memory handling was also repaired.
-
-### Unified integer/floating operations
-
-- Replaced split integer/FP Add/Sub/Mul/Div opcodes with unified source-level
- operations carrying a one-shot mode: unknown, integer, or float.
-- Bool and Minus share the small `ModeNode` contract where useful; Bool output
- remains integer even for floating inputs.
-- ULT is permanently integer-only.
-- Added mode serialization and removed obsolete commented float-node/tag code.
-- Recursive numeric SCCs are resolved after SCCP using graph evidence; float
- wins when both families are present because integer operands can convert.
-- Mode resolution now reinitializes cached type immediately before IterPeeps.
-- Error checking for Arith/Bool/Minus was unified without allocating argument
- arrays.
-
-### Load and memory type cleanup
-
-- `LoadNode` no longer stores a parser-provided `_con`. Its declared field type
- is obtained via an accessor once the pointer/field exists; late type checking
- reports missing/invalid fields.
-- Deeply read-only pointer information is honored by loads instead of being
- widened back to the generic declared struct field type.
-- `MemPhiNode` deliberately starts weak enough that later precise field
- knowledge remains monotonic.
-- Public concrete bulk memory is forced non-final in `TypeMem`; cyclic dual
- construction handles its temporary null payload. `TypeTest` passed after
- this Type change.
-
-### Calls, cloning, and optimizer order
-
-- Instance-call hidden-self shape is syntactic rather than inferred from the
- eventual function value.
-- Function-body cloning/inlining was repaired for loop backedges; an earlier
- bug left cloned inlined nodes pointing into the original function loop.
-- IterPeeps replacement scheduling now revisits both old and replacement inputs
- and replacement outputs.
-- Monotonicity diagnostics now include node IDs and detailed input types/classes.
-- Worklist seeds 124, 125, and 126 exposed and drove fixes for collapsed
- precise Phis, bulk finality, numeric-mode cached types, and Escape alias use.
-
-## Known-good pushed checkpoint
-
-At pushed commit `4f8759b3`:
-
-- Default worklist seed is `126` (also used by `TestC`).
-- `TypeTest` passed: 6 tests.
-- Chapters 1 through 20 passed: 347 tests, zero failures.
-- The seed-126 Ch5 graph shape and Ch20 x86 spill count were accepted as
- goldens after structural failures were eliminated.
-- `git diff --check` was clean before that commit was pushed.
-
-Recent milestone commits, newest first:
-
-```text
-4f8759b3 Rotate random seed and fix more bugs
-14a1a028 Fix general memory split issues
-eea3d783 Fix cloning functions with loops
-f036a040 minor fixes, thru ch18
-eaee86b8 Handle ambiguous self-recursive int/flt conversions
-b9434562 Fixed up to ch17, plus some ch18
-18f07318 Uniform int/fp errors
-f729484c Hey ch1-13 working again
-58e012e6 Remove LoadNode dependence on _con
-0bc690ae Final cleanup around unified int/fp ops
-9720093f Finish off unified int/flt opcodes
-54e5b83b Refactor Phi into MemPhi, BulkMemPhi
-fe7ce3c4 WIP parse ordinary memory as bulk partitions
-21352840 WIP rethread stores after alias resolution
-1f7cb4ac Replace Lift with one destination ConvertNode
-bbe7ebdd Rename proof casts to CheckCast
-b3da12fd Delay branch zero refinements with GuardNode
-fa4f33e7 Remove parser type constraint from ordinary phis
-```
-
-## Current dirty WIP after `4f8759b3`
-
-At handoff, tracked modifications are:
-
-```text
-src/main/java/com/seaofnodes/simple/codegen/Opto.java
-src/main/java/com/seaofnodes/simple/node/LoadNode.java
-src/main/java/com/seaofnodes/simple/node/StoreNode.java
-src/test/java/com/seaofnodes/simple/Chapter10Test.java
-src/test/java/com/seaofnodes/simple/Chapter21Test.java
-```
-
-These appear to be an active experiment around weak Load/Store computation and
-preserving precise Store users while an unresolved bulk Store sharpens:
-
-- `LoadNode.compute()` was reordered to inspect pointer and memory weak states
- explicitly. A non-pointer low input currently falls back to `mem._t`.
-- `StoreNode.compute()` currently comments out the `err() -> BOTTOM` fallback,
- and `escapesFrom` was changed from the met value `t` to the stored `val`.
-- When an alias-1 Store expands into a precise Store plus MemMerge, users which
- already require the same alias are repointed directly to the new precise
- Store before the bulk Store is replaced.
-- Several Chapter 10 goldens currently retain a `Stop[...]` with returned
- memory instead of expecting it to disappear.
-- Chapter 21 `testAntiDeps1` x86 spill expectation is changed from 7 to 9.
-- `Opto.linkStart` has a commented-out high-escape-set early return. There is
- also an accidental comment typo (`asn inst`); clean that before committing.
-
-Treat all of those as WIP, not settled design. In particular, reconsider
-whether changing goldens to retain memory hides incomplete dead-memory cleanup.
-Do not discard the changes before understanding the failing case that motivated
-them.
-
-The attempted handoff test command could not run because Make reported:
-
-```text
-Makefile:111: *** open: .argsM.txt: Permission denied. Stop.
-```
-
-This is probably another active-process/Windows file-lock issue. Previously an
-IntelliJ Java debugger was conclusively found to lock JAR/class outputs; killing
-the debug process allowed compilation immediately. Close relevant Java/IDE or
-Make processes, preserve or remove only the transient `.argsM.txt` as
-appropriate, then rerun tests. Do not delete source files to work around it.
-
-## Work still required to finish the rewrite
-
-### Immediate frontier
-
-1. Recover the intent and test state of the five dirty WIP files.
-2. Clear the `.argsM.txt` lock and run, in order:
- - the motivating focused test;
- - `TypeTest`;
- - Chapters 1–20;
- - Chapter 21, especially `testAntiDeps1` and `testStringExport`;
- - then all of `tests_raw0`.
-3. Decide whether same-alias Store users being repointed during sharpening is
- the canonical local rewrite. Verify it preserves every user's old type in
- one step and the exactly-once alias coverage invariant.
-4. Restore the `Opto.linkStart` high-set error handling unless a reduced test
- proves it wrong; never iterate `XInt.next` over a high/infinite set blindly.
-5. Separate legitimate golden changes from leaked/dead returned memory.
-
-### Store `_con` and weak diagnostics
-
-`LoadNode._con` is gone, but `StoreNode._con` remains. It still sharpens from a
-Field and trims stored values. Continue the original small-step experiment:
-
-- Determine whether `_con` is authoritative declared storage width or merely a
- cache of `ptr.field(name)._t`.
-- If it is derivable, replace uses with an accessor as was done for Load.
-- Keep error reporting in `err()`/type checking rather than manufacturing
- parser-time graph topology or accidental BOTTOM propagation.
-- Use a diagnostic whose offending node has no BOTTOM inputs when validating
- late error selection.
-
-### True unresolved lexical fields (original Phases 2/3)
-
-The memory alias machinery now tolerates an alias-1 Store and sharpens it
-later, but the full lexical design is not complete:
-
-- Unqualified instance/class field references before declarations must resolve
- by closed lexical scope, not by whichever pointer type happens to sharpen.
-- A scope-owned unresolved load/store representation may still be needed.
-- Resolution must select self versus class initializer base, precise alias, and
- constructor `init` status together.
-- Locals remain define-before-use and need no unresolved mechanism.
-- Verify loops can lazily widen precise alias runs without global O(n²)
- reconstruction or Phi/MemMerge oscillation.
-
-### Field finality (original Phase 6)
-
-The planned two-bit four-state Field finality has not been implemented:
-
-```text
-00 unknown
-01 declared mutable
-10 declared final
-11 conflicting / BAD_FINAL
-```
-
-This still needs accessors, meet/join specification, equality, hashing, dual,
-printing/serialization as needed, representative `gather()` entries, and full
-`TypeTest`. Parser-declared final stores require the structural `init` marker;
-non-init writes to a resolved final field are errors. Cyclic named structs must
-merge finality during `Type.closeOver()`.
-
-Do not confuse Field finality with bulk-memory finality: public bulk memory is
-now always conservatively non-final.
-
-### Function contracts (original Phase 5)
-
-`FunNode._sig` still deserves an audit:
-
-- Declared argument syntax is required and authoritative.
-- Return type is graph-derived.
-- Private functions may sharpen from known call sites.
-- Escaping functions need a worst-case/default unknown caller tied to the
- declared type binding.
-- Forward-declared argument types need a changing type source, not a true
- ConstantNode.
-- Remove cached signature state that can be derived from Fun/Parm/Return edges;
- retain explicit declared contracts.
-
-### Final acceptance
-
-- Sweep several deterministic worklist seeds, not just 126. Semantic graph
- results must converge independent of order.
-- Harden graph-print and spill-count tests where harmless scheduling variation
- remains. Do not weaken monotonicity or memory-coverage assertions.
-- Run `tests_raw0`, then `tests_raw1`, then `tests_alone`.
-- Run `TypeTest` after every Type/Field lattice change.
-- Confirm serialization/deserialization after every new semantic node field or
- mode bit.
-- Audit the parser for remaining type-dependent structural choices, especially
- conversions, field base selection, function self shape, and alias slicing.
-
-## Debugging guidance
-
-- Node IDs are central to debugging; refer to nodes as `Store#107`, `Phi#105`,
- etc.
-- Stop at the earliest `IterPeeps` count where the graph becomes invalid.
-- `Opto.worklistCheck` catches missed/non-monotonic SCCP updates.
-- `IterPeeps.progressOnList` catches one-step type and idealization failures;
- its current detailed input dump was added specifically for order bugs.
-- For memory bugs, check both:
- 1. each program slice covers every alias exactly once; and
- 2. a precise consumer never obtains an alias excluded from its bulk input
- without a parallel precise slice.
-- A precise `MemPhi#N` fed by a `MemMerge` whose default bulk excludes `N` and
- which has no explicit `N` input is malformed.
-- Avoid “fixes” which merely queue a favored node earlier. Change the seed and
- demand order-independent semantics.
-- Golden changes are acceptable only after semantic evaluation, type
- monotonicity, and graph invariants are known good.
-
-## Workflow and safety
-
-- Cliff wants to review every push. Ask permission immediately before pushing.
- In this handoff Cliff said he would push the current completed checkpoint;
- do not assume that grants future push permission.
-- Push remote checkpoints for backup once approved.
-- Commit at useful test frontiers with messages describing the architectural
- change, not only the symptom.
-- Preserve unrelated dirty edits. Emacs temporary files are common and need
- not be reported repeatedly.
-- Windows IntelliJ debug sessions may lock JARs/class files. Report the lock,
- have Cliff stop the debugger, and retry before changing build machinery.
-- Prefer narrow changes; unwind experiments that cause cascading regressions.
-
-## Rollback paths
-
-- Whole experiment: branch from tag `ch25-parser-before-simplification`.
-- Individual mechanism: revert/cherry-pick the narrow branch commits.
-- Inspect old implementation without mutating this branch:
- `git diff ch25-parser-before-simplification..HEAD` or `git show <old>:<path>`.
-- Do not squash until the rewrite is accepted; the commit sequence is part of
- the recovery design.
diff --git a/chapter25/docs/examples/A_helloWorld.smp b/chapter25/docs/examples/A_helloWorld.smp
index 07ff935f..ac399661 100644
--- a/chapter25/docs/examples/A_helloWorld.smp
+++ b/chapter25/docs/examples/A_helloWorld.smp
@@ -1,3 +1,3 @@
// -*- mode: java; -*-
-sys.io.p("Hello, World!");
+sys.io.p("Hello, World!\n");
return 0;
diff --git a/chapter25/parser-simplification-plan.md b/chapter25/parser-simplification-plan.md
deleted file mode 100644
index 986643ea..00000000
--- a/chapter25/parser-simplification-plan.md
+++ /dev/null
@@ -1,371 +0,0 @@
-# Direct-to-SSA Parser Simplification Plan
-
-## Purpose
-
-The constructor rewrite exposed a deeper problem: the one-pass parser sometimes
-uses provisional type information to choose SSA graph structure. Forward
-references make that information incomplete and parse-order dependent.
-
-The design rule for this work is:
-
-> Syntax and resolved lexical bindings determine graph shape. Types refine and
-> validate the graph, but provisional types do not decide its shape.
-
-This is an experiment, not an irreversible cleanup. We need to be able to
-abandon the simplification completely, or restore individual mechanisms that
-turn out to have been necessary.
-
-## Preserve the old design before removing it
-
-Do not preserve old code as large commented-out blocks. Git is the archive;
-the working implementation should remain small enough to understand.
-
-Before implementation begins:
-
-1. Confirm the worktree is clean and record the current failing-test baseline.
-2. Create a named, permanent anchor at the last commit before simplification,
- for example an annotated tag `ch25-parser-before-simplification`.
-3. Do the experiment on a dedicated branch, for example
- `ch25-parser-simplification`.
-4. Push the anchor and branch if a remote backup is desired before large
- deletions.
-5. Never squash the experimental commits until the design has proved itself.
-
-The anchor is the complete "this was a giant mistake" escape hatch. Switching
-back to a branch made from that anchor restores the old implementation exactly.
-No reset of the experimental branch is required.
-
-Removed mechanisms must each have their own small commit. A commit should
-remove one idea, not mix its deletion with several replacements. Suggested
-commit sequence:
-
-1. Add characterization tests and inventories.
-2. Remove one old constraint mechanism.
-3. Make weak-input computation total.
-4. Add the smallest replacement demanded by a failing test.
-
-This history supports several levels of recovery:
-
-- Recover the entire old design by creating or switching to a branch at the
- preservation tag.
-- Restore a complete deleted file from the tag with `git restore --source`.
-- Restore selected hunks interactively with `git restore -p --source`.
-- Reintroduce a logically complete mechanism by reverting its deletion commit
- or cherry-picking a later restoration commit.
-- Compare the experiment with the old implementation at any time using the
- preservation tag as the diff base.
-
-Avoid using a stash as the archive: stashes are unnamed working state, are easy
-to drop, and do not provide the reviewable sequence needed for selective
-restoration.
-
-When an old mechanism is restored, bring it back in a new, named commit and
-record the test that required it. Do not silently copy code out of history.
-That preserves evidence about which complexity is essential.
-
-## Working discipline
-
-Work through `tests_raw0` chapter by chapter. Keep all earlier chapters passing
-before advancing. The expected difficulty landmarks are Chapter 10, where
-functions are exercised seriously, and Chapter 13, where recursive forward
-references and cyclic types become prominent.
-
-Use the following test ladder:
-
-1. The single test or test class driving the current change.
-2. The current chapter.
-3. All earlier raw0 chapters.
-4. `tests_raw0` when a milestone is reached.
-5. `TypeTest` after every change to `Type`, `Field`, lattice operations,
- interning inputs, equality, hashing, or duality.
-
-Golden-string mismatches are not a gate during structural work unless the
-string reveals a semantic or graph-shape regression. Record and defer expected
-text churn rather than continually rewriting goldens.
-
-Treat these as compiler invariant failures, not ordinary test failures:
-
-- `compute()` must be monotonic, including when inputs are weak or unresolved.
-- `Opto.worklistCheck` failures require finding the non-monotonic computation;
- they are not assertions to weaken.
-- IterPeeps' `Non-monotonic peep` check applies the same rule to iterative
- transformations.
-- `ideal()` runs only during iterative optimization, not Opto, but its
- replacement must preserve the type knowledge already established.
-
-Commit whenever the test frontier advances or one architectural mechanism is
-removed. Each commit message should state the highest passing chapter and
-whether `TypeTest` was run.
-
-## Phase 0: Baseline and inventory
-
-1. Fix the local `_Scan` constructor migration so it does not obscure parser
- work.
-2. Record the current raw0 failures by chapter and test name.
-3. Inventory parser-written constraints and type-sharpening side channels:
- `_con` fields, mutable signatures, parser mutations of `TYPES`, forced memory
- types, and assertions that assume already-sharp inputs.
-4. For every candidate deletion, record its writers, readers, and the first
- test that appears to need it.
-5. Add small characterization tests when behavior is important but undocumented.
-
-The inventory is a deletion checklist, not a commitment to replace every item.
-Code that no passing test requires stays deleted.
-
-## Phase 1: Remove parser-supplied node constraints
-
-Start with `_con`-style fields in `PhiNode`, `LoadNode`, `StoreNode`, and related
-nodes.
-
-For each node kind:
-
-1. Delete the parser writer and the node field in a narrow commit.
-2. Make `compute()` accept the weakest legal inputs. A `Type.BOTTOM` input must
- produce a conservative weak output instead of triggering a cast or sharpness
- assertion.
-3. Separate structural slot invariants from type-sharpness assumptions. It is
- valid to know that an input position is the memory edge without knowing a
- sharp `TypeMem` for it.
-4. Restore only the structure that a failing test proves necessary.
-
-If an ordinary Phi cannot preserve the structural fact that it merges memory,
-introduce a `MemPhiNode` or an explicit immutable Phi kind. It may lift weak
-inputs to the weakest memory type. It must not contain a parser-chosen sharp
-lower bound disguised as another `_con`.
-
-### Keep arithmetic flavor out of the parser
-
-Arithmetic syntax determines the operation (`+`, `-`, `*`, `/`, unary `-`),
-but not necessarily its integer or floating-point machine flavor. The current
-`Node.widen()` is a parser-time semantic rewrite: it inspects the operands'
-current types, replaces integer nodes with FP nodes, and inserts `ToFloatNode`s.
-A weak forward-reference input can therefore permanently choose the wrong SSA
-shape.
-
-Replace this decision with a generic or unresolved arithmetic node, or make the
-syntax-level arithmetic node itself generic until its operand family resolves.
-It must:
-
-- compute a weak result while either operand family is unknown;
-- resolve monotonically to integer or floating-point arithmetic when sufficient
- information arrives;
-- insert integer-to-float conversions only after the FP choice is definitive;
-- report mixed or invalid operands during type checking rather than guessing
- during parsing; and
-- be completely resolved before instruction selection.
-
-Compound assignments and increment/decrement use the same mechanism; they must
-not retain a separate parser-time `widen()` decision path.
-
-## Phase 2: Separate name binding from type inference
-
-Locals remain define-before-use. The unresolved case is an unqualified field
-name used inside a function before a later instance or class field declaration
-determines its owner.
-
-Introduce unresolved SSA operations rather than guessing from provisional
-types:
-
-- `UnresolvedLoad` records the identifier, source position, definitive lexical
- scope object, ordered possible base pointers, and bulk memory input.
-- `UnresolvedStore` records the same binding information plus its value and the
- constructor context needed to decide initialization legality later.
-- Closing the relevant scope resolves all pending operations by lexical order.
-- Resolution selects the base pointer and alias and rewrites the node to the
- ordinary resolved operation.
-
-Do not use pointer-type sharpening to choose ordinary lexical bindings. Types
-may reject a resolved operation, but the closed scope is authoritative about
-which declaration the name denotes.
-
-The `init` bit on a resolved Store is decided at this same point. It is true
-only when the winning declaration belongs to the receiver being initialized by
-the current constructor. Merely being textually inside some constructor is not
-enough.
-
-## Phase 3: Recover alias precision after unresolved stores
-
-Before binding, an unknown store uses bulk alias `#1` and conservatively updates
-bulk memory. Preserve enough information in `UnresolvedStore` to recover the
-pre-store alias bundle when its target becomes known.
-
-After resolution, Opto may transform a Phi of memory merges into a merge of
-per-alias Phis:
-
-```text
-Phi(MemMerge(entry...), MemMerge(back...))
- =>
-MemMerge(aliasA = Phi(entryA, backA),
- aliasB = Phi(entryB, backB), ...)
-```
-
-For a resolved store to alias A, alias A's loop backedge passes through the
-store; unrelated aliases bypass it.
-
-Requirements for this idealization:
-
-- Handle missing slices through the MemMerge default/bulk input.
-- Create alias Phis on demand; aliases need not all be known at loop creation.
-- Memoize a replacement loop Phi before recursively processing its backedge.
-- Use only the canonical direction shown above to prevent transform oscillation.
-- Keep unresolved memory operations resistant to destructive idealization until
- their owning scope closes.
-
-Pre-splitting memory during parsing remains an eager optimization when aliases
-are already known. This transform is the lazy equivalent when they are not.
-
-## Phase 4: Keep call shape syntactic
-
-Call SSA shape must not depend on whether a provisional function type currently
-looks like an instance function.
-
-The syntax `ptr.fld(args)` always supplies `ptr` in the hidden self slot:
-
-```text
-call(load(ptr, fld), self=ptr, args...)
-```
-
-This remains true if the field later resolves to a stored top-level function;
-that function ignores the universal self argument. An ordinary call through a
-separately loaded function value uses the normal non-instance/default self.
-
-Function types validate calls after construction. They do not change call
-arity or add/remove SSA inputs after resolution.
-
-## Phase 5: Split declared function contracts from inferred signatures
-
-Audit `FunNode._sig` after the earlier constraints are removed.
-
-Function arguments have authoritative declared syntax, while returns are graph
-derived. A declared forward type such as `B` has stable symbolic identity but
-only a provisional current realization.
-
-Represent the external/default caller with a non-constant source node tied to
-the declared type binding:
-
-```text
-UnknownCaller(B-binding)
-```
-
-Its computed runtime value type begins as weak open `B{...}` and monotonically
-sharpens when the binding resolves. Updating the binding schedules its users.
-Private functions with completely known call sites may omit this default caller
-and sharpen their parameters from actual arguments. Escaping functions retain
-it as their worst-case caller.
-
-Classify every use of `_sig` as one of:
-
-- immutable declared argument contract;
-- cached graph-derived information;
-- unresolved symbolic binding.
-
-Delete cached derived state. Keep authoritative declarations explicitly.
-Derive return information from Fun/Parm/Return graph edges. Recursive functions
-must converge through weak-input-tolerant `compute()` rather than parser mutation
-of a parallel signature cache.
-
-## Phase 6: Make field finality a four-state type fact
-
-Use two bits in `Field`, with accessors hiding the representation:
-
-```text
-00 unknown
-01 declared mutable
-10 declared final
-11 conflicting finality (BAD_FINAL)
-```
-
-The discovery-combination operation combines evidence; with this encoding it
-can normally be bitwise OR. `UNKNOWN` is the identity and `BAD_FINAL` is
-absorbing. Confirm the operation matches the repository's meet/join lattice
-orientation.
-
-Add the bits to equality, hashing, duality, printing, and all Field construction
-paths. A weak forward reference carries `UNKNOWN`, never guessed mutable.
-Conflicting authoritative declarations produce `BAD_FINAL`; computation remains
-total and type checking later reports the program error.
-
-Add representative Fields for all four states to TypeTest's `gather()` universe.
-Verify pairwise operations plus commutativity, associativity, idempotence, duals,
-hashing, and interning behavior. Cyclic handling and interning should fall out
-of the existing machinery, but TypeTest must demonstrate that they do.
-
-Final-store validation happens only after binding and type closure:
-
-- final and initializing store: legal;
-- final and non-initializing store: error;
-- mutable field: initialization marker is irrelevant;
-- bad finality: declaration/type error.
-
-## Phase 7: Preserve cyclic type closure
-
-Keep `Type.closeOver()` responsible for tying recursive named types together.
-For mutually recursive A/B structures it must collect partial versions, merge
-same-name fields (including four-state finality), remove the open marker when
-the declaration is complete, and make recursive occurrences reference the same
-canonical Java object.
-
-Do not make SSA construction depend on whether `closeOver()` has already run.
-Closing types sharpens existing nodes and wakes their users; it does not rebuild
-parser-chosen graph topology.
-
-Chapter 13 is the first major acceptance gate for this phase.
-
-## Phase 8: Advance the chapter frontier
-
-For each failing chapter:
-
-1. Reduce the first semantic or invariant failure to the smallest useful test.
-2. Decide whether it demonstrates missing required structure or merely an old
- golden string.
-3. Add back only the smallest mechanism justified by the test.
-4. Run the chapter, all earlier chapters, and TypeTest when applicable.
-5. Commit the new frontier before moving on.
-
-Expected order of pressure:
-
-- Chapters 1-9: establish that weak-input computation did not disturb basics.
-- Chapter 10: functions, calls, and parameter computation.
-- Chapter 13: recursive forward functions and cyclic structures.
-- Later chapters: fields, finality, memory alias recovery, escaping functions,
- and separate compilation interactions.
-
-Do not begin by restoring later-chapter machinery preemptively.
-
-## Decision gates and rollback criteria
-
-Pause and compare against the preservation anchor when any of these occurs:
-
-- A replacement requires more state or special cases than the mechanism removed.
-- Parser graph shape still depends on provisional types.
-- Alias recovery requires global reconstruction with no monotonic canonical form.
-- Recursive type or function convergence requires non-monotonic mutation.
-- Earlier chapters repeatedly regress as later mechanisms are added.
-
-At a gate, choose explicitly among:
-
-1. Continue with the simplified mechanism.
-2. Restore one old component and document the test proving its value.
-3. Start a narrower experiment branch from the preservation anchor.
-4. Abandon the experiment and resume from the preserved design.
-
-Abandoning the experiment is a supported result, not a failed recovery. The
-tag, unsquashed commits, and narrow deletions guarantee that both the complete
-old parser and individual removed mechanisms remain available.
-
-## Completion criteria
-
-The simplification is successful when:
-
-- `tests_raw0`, then `tests_raw1`, and finally the broader suite pass apart from
- deliberately deferred golden updates;
-- `TypeTest` passes with all finality states included;
-- no parser decision about SSA topology depends on a provisional inferred type;
-- unresolved lexical operations resolve deterministically at scope close;
-- memory precision lost during unresolved parsing is recovered monotonically;
-- calls obtain self from syntax, not inferred callee kind;
-- declared function arguments remain authoritative without a parallel mutable
- inferred signature;
-- removed code that was never demanded by tests remains absent; and
-- the preservation anchor and commit sequence still provide complete and
- selective rollback paths.
diff --git a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
index 98f8698f..4c2dfc84 100644
--- a/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
+++ b/chapter25/src/main/java/com/seaofnodes/simple/Parser.java
@@ -2396,11 +2396,27 @@ public class Parser {
private String parseString() {
if( !peek('"') ) return null;
_lexer.inc();
- int start = pos();
- while( !_lexer.isEOF() && _lexer.nextChar()!= '"' ) ;
- if( _lexer.isEOF() )
- throw error("Unclosed string");
- return new String(_lexer._input,start,pos()-start-1);
+ StringBuilder sb = new StringBuilder();
+ while( !_lexer.isEOF() ) {
+ char c = _lexer.nextChar();
+ if( c=='"' ) return sb.toString();
+ if( c!='\\' ) {
+ sb.append(c);
+ continue;
+ }
+ if( _lexer.isEOF() )
+ throw error("Unclosed string");
+ char esc = _lexer.nextChar();
+ sb.append(switch( esc ) {
+ case 'n' -> '\n';
+ case 't' -> '\t';
+ case 'r' -> '\r';
+ case '\\' -> '\\';
+ case '"' -> '"';
+ default -> throw error("Unknown string escape \\\\"+esc);
+ });
+ }
+ throw error("Unclosed string");
}
// Already parsed "'"
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index cf9d764e..313ae1cf 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -38,6 +38,15 @@ public class Chapter25Test {
assertEquals("1",Eval2.eval(code,0));
}
+ @Test
+ public void testStringEscapes() {
+ String src = "u8[~] s=\"A\\n\\t\\r\\\\\\\"B\"; " +
+ "return s#==7 && s[0]=='A' && s[1]==10 && s[2]==9 && s[3]==13 && " +
+ "s[4]==92 && s[5]==34 && s[6]=='B';";
+ CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("1",Eval2.eval(code,0));
+ }
+
@Test @Ignore
public void testModule0() throws IOException {
@@ -197,7 +206,7 @@ public class Chapter25Test {
File sys_file = buildTestSys(false);
assertTrue("Missing "+sys_file+"; testHelloWorld depends on testSys building it", sys_file.exists());
- String expected = "Hello, World!";
+ String expected = "Hello, World!\n";
String prog = "return sys.io.p(\""+expected+"\") - "+expected.length()+";";
TestC.run(prog,"helloWorld",new Ary<>(new String[]{SYS_BLDDIR}),
TestC.CALL_CONVENTION, null, null, expected,0);
@@ -223,7 +232,7 @@ public class Chapter25Test {
assertTrue("Missing "+sys_file+"; testHelloWorldNoInline depends on testSys building it", sys_file.exists());
String base = "helloWorldNoInline";
- String expected = "Hello, World!";
+ String expected = "Hello, World!\n";
String prog = "return sys.io.p_noInline(\""+expected+"\") - "+expected.length()+";";
CodeGen code = new CodeGen(null,"build/objs",new Ary<>(new String[]{SYS_BLDDIR}),
base,prog,123L,TypeInteger.BOT);
diff --git a/chapter25/src/test/java/com/seaofnodes/simple/Simple.java b/chapter25/src/test/java/com/seaofnodes/simple/Simple.java
index 870792c0..35b9b922 100644
--- a/chapter25/src/test/java/com/seaofnodes/simple/Simple.java
+++ b/chapter25/src/test/java/com/seaofnodes/simple/Simple.java
@@ -35,6 +35,7 @@ Options:
-L path - add an external Simple object search path
--eval - (slowly) evaluate the compiled code in emulator
--run - run the compiled code natively; this is the default
+ --main - export the top-level initializer as C `main`
--cpu <cpu-name> - use specific CPU (x86_64_v2, riscv, arm)
--abi <abi-name> - use specific ABI variant (SystemV)
--target - print native CPU and ABI
@@ -89,6 +90,7 @@ Options:
boolean do_eval = false;
boolean do_run = true;
boolean do_codegen = false;
+ boolean emit_main = false;
boolean dump_dot = false;
boolean print_time = false;
boolean print_size = false;
@@ -129,6 +131,7 @@ Options:
case "--eval": do_eval = true ; do_run = false; break;
case "--run": do_run = true ; break;
case "--norun": do_run = false; break;
+ case "--main": emit_main = true; break;
case "--dump-size": print_size = true; break;
case "--dump-time": print_time = true; break;
case "-o": if (out != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage();
@@ -221,7 +224,7 @@ Options:
// Compilation pipeline
Ary<String> externPaths = libPaths.isEmpty() ? null : new Ary<>(libPaths.toArray(new String[0]));
CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), externPaths, srcName, src, 456, TypeInteger.BOT );
- code.driver(Phase.LastPhase,cpu,abi, false, do_run || do_eval, dump);
+ code.driver(Phase.LastPhase,cpu,abi, false, emit_main || do_run || do_eval, dump);
if( do_codegen && print_asm )
System.out.println(code.asm());