diff options
| -rw-r--r-- | chapter25/Makefile | 2 | ||||
| -rw-r--r-- | chapter25/WIP-HANDOFF.md | 387 | ||||
| -rw-r--r-- | chapter25/docs/examples/A_helloWorld.smp | 2 | ||||
| -rw-r--r-- | chapter25/parser-simplification-plan.md | 371 | ||||
| -rw-r--r-- | chapter25/src/main/java/com/seaofnodes/simple/Parser.java | 26 | ||||
| -rw-r--r-- | chapter25/src/test/java/com/seaofnodes/simple/Chapter25Test.java | 13 | ||||
| -rw-r--r-- | chapter25/src/test/java/com/seaofnodes/simple/Simple.java | 5 |
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()); |
