summaryrefslogtreecommitdiff
path: root/chapter30/README.md
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
committerCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
commit06fb5aceeb65a75b1cd643debd667c52fdcafcb9 (patch)
treec99813eaf27d98f840245302c4d6e923c964307e /chapter30/README.md
parent6560a92433a38825fd1d44d9c66ffba9f26990ff (diff)
downloadSimple-ch30.tar.gz
Simple-ch30.zip
Better error messagesch30
Better error messages. Buffs to the std lib; getch() to fetch a single character; streaming I/O handles retries, and large sizes, with better error reporting. Formatted strings. Default int moved to i32. TicTacToe, the game.
Diffstat (limited to 'chapter30/README.md')
-rw-r--r--chapter30/README.md221
1 files changed, 217 insertions, 4 deletions
diff --git a/chapter30/README.md b/chapter30/README.md
index 843ccc69..3e27c874 100644
--- a/chapter30/README.md
+++ b/chapter30/README.md
@@ -1,12 +1,209 @@
-# Chapter 30: Work in Progress
+# Chapter 30: Text Formatting and Diagnostics
English | [日本語](README.ja.md)
[Previous: Chapter 29 — Tech Debt Cleanup](../chapter29/README.md)
-This chapter starts from the completed Chapter 29 compiler. Its next topic is
-not yet selected. The parser organization, constructor checks, partial field
-views, and regression tests are inherited unchanged.
+This chapter uses a small game to drive improvements in the compiler and the
+`sys` standard library. The game provides a concrete workload; the chapter's
+subject is the language and library work uncovered.
+
+The first program is ASCII [tic-tac-toe](docs/examples/TicTacToe.smp).
+The player moves first with single keystrokes; the CPU responds, occupied
+squares are rejected, and play ends with a win or draw.
+Runnable programs, including sorting, file I/O, and shortest-path examples,
+live in [`docs/examples`](docs/examples/).
+
+## Terminal input and diagnostics
+
+[`sys.io.getch()`](src/main/smp/sys/io.smp) reads one byte without waiting for
+Enter or echoing it. A C adapter changes and restores the terminal mode;
+redirected files and pipes also work. It returns `0..255`, or `-1` with
+`sys.libc.errno()` set to `sys.libc.EOF` or an OS error. Escape and UTF-8
+sequences arrive as multiple bytes.
+
+The game echoes accepted moves, ignores Enter, and quits on `q`, Escape,
+Ctrl-C, Ctrl-D, or end of input.
+
+We also exposed a parser bug: branch guards from one function could affect the
+next function. Function boundaries now isolate those guards. Loop-path merges
+also preserve forward references discovered after a break or continue.
+
+The CLI reports parser and type errors as `file:line:column: error: message`,
+followed by the source line and an underscore-and-caret pointer - suitable for
+older text editors (VI, Emacs). Language errors now exit with status 1 instead
+of throwing an exception; unexpected compiler errors keep their Java stack
+traces. Quite a few error reporting points are cleaned up: malformed numbers,
+invalid escapes, unterminated comments or quoted text are a few.
+
+## Stream I/O and resizable arrays
+
+`sys.io.read(fd)` grows its buffer and continues through short reads until
+EOF. It returns an exact-sized byte array, empty for immediate EOF, or `null`
+on error with `sys.libc.errno()` preserved. Partial input is discarded on an
+error. `stdin()` uses the same behavior.
+
+`write`, `writeOff`, and `aryu8.write` write the complete requested data,
+retrying partial writes. Reads and writes retry `EINTR`; other errors return
+to the caller. Writes return a 64-bit byte count, or `-1` with errno set; a
+failure can follow a successfully written prefix. A zero-progress write
+reports `EIO`. `writeOff` rejects offsets past the array end with `EINVAL`,
+caps the length at the remaining bytes, and accepts empty slices at the end.
+
+The buffering and retry loops are Simple code. C supplies access to errno and
+its platform constants; the native read/write bindings use 64-bit counts and
+results on the supported targets. Tests cover files, pipes, empty and large
+inputs, and injected short transfers, interruptions, and failures.
+
+`aryu8` and `aryi64` store their lengths as `u32`, matching built-in array
+lengths. Growth handles empty backing arrays, uses existing capacity when it
+fits exactly, and calculates the next capacity in 64-bit arithmetic before
+narrowing. Requests beyond the `u32` length limit are rejected: stream reads
+return `null` with `EOVERFLOW`, while direct resizable-array growth exits with
+status 1. The tests check arithmetic at the unsigned boundary without allocating
+multi-gigabyte buffers.
+
+## Default integer type
+
+`int` now aliases `i32`. Its fields and array elements occupy four bytes;
+assignments and updates use the existing signed 32-bit narrowing rules.
+Explicit `i64` remains available for wide arithmetic and native addresses.
+Literal inference, `var`/`val` inference, and intermediate arithmetic retain
+their existing rules: `int x=2147483647; return x+1;` produces `2147483648`,
+whereas assigning that sum back to `x` produces `-2147483648`.
+
+The standard library spells out `i64` where it needs full-width formatting or
+native addresses/sizes. Resizable-array lengths, growth counts, and indices
+use explicit `u32`.
+
+Function entries normalize narrow signed/unsigned parameters, including
+stack arguments, using the backend extension operations. This also applies
+to internal Simple calls. Loop type widening preserves signed and unsigned
+32-bit bounds of values narrowed on every assignment.
+
+## Strings and formatting
+
+Strings can embed expressions from the surrounding scope:
+
+```java
+sys.io.p("CPU plays %(reply+1).\n");
+sys.io.p("score=%04d(score), average=%.2f(average)\n");
+sys.io.p("name=%-12.8s(name), key=%c(key)\n");
+```
+
+`%(expression)` selects decimal integer, floating-point, or byte-string
+formatting from the expression's type. Booleans are integer aliases, so the
+default prints `0` or `1`; `%b(expression)` prints `false` or `true`.
+Expressions execute once, from left to right. Each value is formatted before
+evaluating the next expression. The result is an ordinary immutable `u8[~]`
+with its exact byte length. Strings without embedded expressions remain
+constants.
+
+An explicit format has the form `%[flags][width][.precision]kind(expression)`:
+
+| Kind | Argument and output |
+| --- | --- |
+| `d`, `i` | Integer, signed decimal |
+| `u`, `o`, `x`, `X` | Integer, unsigned decimal, octal, or hexadecimal; negative values use their 64-bit representation |
+| `f`, `F`, `e`, `E`, `g`, `G`, `a`, `A` | Float, using the corresponding C format |
+| `c` | Integer, one byte from its low eight bits |
+| `s` | Byte string, including embedded zero bytes |
+| `b` | Integer, `false` for zero and `true` otherwise |
+
+Flags are `-` (left alignment), `+` (explicit sign), space (positive sign
+padding), `#` (alternate form), and `0` (zero padding), where applicable.
+Width and precision are literal decimal integers. String precision limits
+bytes; integer precision supplies a minimum digit count. `c` and `b` allow
+width and left alignment, but no precision. Default floating-point formatting
+uses `g` with 17 significant digits; explicit floating formats use C defaults
+unless a precision is given. Positional arguments, `*` widths, C length
+modifiers, and `%n` are not supported.
+
+Write `%%` or `\%` for a literal percent. Every other `%` must introduce a
+complete format with parentheses. Inside those parentheses the normal
+expression parser resumes, so nested strings work directly:
+
+```java
+val text = "outer %("inner %(value)")";
+```
+
+[`sys.text`](src/main/smp/sys/text.smp) provides explicitly named converters:
+`int`, `flt`, `f32`, `f64`, `char`, `bool`, and `hex`. For example,
+`sys.text.int(17)` returns `"17"`. Primitive type names are allowed as member
+names and in member declarations; they remain reserved for local variables and
+parameters. `sys.io.p` continues to require a byte string.
+
+`Strings` builds ordinary calls to `sys.text`, with normal module lookup,
+class initialization, and control/memory edges. A small `FormatNode`
+selects the default typed formatter as normal type flow resolves an expression,
+including forward references; it folds away in valid programs.
+
+The formatting library is written in Simple: `begin` creates an `aryu8`,
+`integer` and `string` append formatted values, and `end` copies the populated
+bytes into the immutable result. The integer formatter covers signed and
+unsigned 64-bit values, including minimum `i64`, and handles signs, prefixes,
+width, and precision. Character and boolean formatting use the same builder.
+`aryu8.num` shares the integer formatter, including its full-range handling.
+Only binary floating-point conversion calls C, through one fixed-signature
+adapter returning a byte string. There is no boxing or variadic Simple call.
+
+`sys.text` uses no interpolated strings, including in its named converters, so
+the library builds from source without a staged bootstrap. Non-float
+formatting can also run in the IR evaluator. The lexer shares its string and
+format scanner with deferred constructor-body and loop-update scanning.
+
+## Runtime support in Simple
+
+Native startup also uses Simple: [`simple_args.smp`](src/main/smp/sys/rt/simple_args.smp)
+allocates the argument array and copies each argument into a byte array. The
+C entry adapter calls this compiled routine before `simple_main`; C supplies
+only access to `argv[i]`, `strlen`, and `memcpy`. The converter fills every
+nullable array slot before publishing the result as `u8[~][]`, including the
+program name at index zero. It builds independently of `sys`, so even programs
+without standard-library imports use it.
+
+The `sys.rt` module holds Run Time support, like Java's `rt.jar` or C's `crt.o`.
+Make emits `simple_args.o` alongside `simple_crt.o`; the native linker driver
+includes both runtime objects automatically.
+
+## Register allocation
+
+The allocator handles each function independently, retaining its completed
+register assignments while reusing scratch space for the next function.
+Building live ranges, constructing interferences, coalescing, coloring, and
+splitting all run on that function's scheduled blocks. The eight-round limit
+applies per function. A hard register-mask conflict in one function therefore
+cannot prevent another function from reaching coloring or consume its retry
+budget.
+
+Two changes avoid unnecessary retries. First, liveness ignores inputs whose
+operand register mask is null: these are metadata or scheduling dependencies,
+not runtime register uses. In particular, a static call's unused hidden class
+argument must not keep its class-pointer constant live across other calls and
+trigger rematerialization.
+
+Second, splitting retains both transfers when a value must move out of a fixed
+register before a kill and back into it afterward. A buggy attempt at early
+coalescing would undo a split needed to dodge the kill. Interference-aware
+coalescing decides when they can be joined.
+
+Loop self-conflicts initially try cold transfers. If that does not separate
+the ranges, the fallback also splits backedge inputs. Distinct backedge nodes
+can still share a range through two-address instructions; copying only the Phi
+outputs leaves that conflict intact.
+
+The spill-ranking heuristic and calling conventions are unchanged. Saving a
+callee-save register can still require several coloring attempts as pressure
+changes. The [allocator regressions](src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java)
+check function independence in both scheduling orders, preservation across
+register kills, and dependencies that do not consume registers.
+
+## Related follow-up work
+
+[Narrow integer instruction selection](../docs/roadmap.md#integer-instruction-widths)
+and [per-function calling conventions](../docs/roadmap.md#per-function-calling-conventions)
+remain separate backend projects. C pointer types and compact references are
+also deferred; the `int` change does not shrink pointers.
The [grammar and code map](docs/30-grammar.md),
[source layout](src/main/java/com/seaofnodes/simple/README.md), and
@@ -21,6 +218,22 @@ From the repository root:
make lib tests release CHAPTERS=chapter30 CTAGS=
```
+Build the game without starting it:
+
+```sh
+make -C chapter30 tictactoe CTAGS=
+```
+
+The Windows/Cygwin build produces
+`chapter30/build/examples/x86_64_v2_win64/TicTacToe.exe`, which can be launched
+from Cygwin or PowerShell. Linux produces
+`chapter30/build/examples/x86_64_v2_SystemV/TicTacToe`.
+When building in WSL alongside Windows, keep Java build outputs separate:
+
+```sh
+make -C chapter30 tictactoe CLZDIR=build/classes-linux CTAGS=
+```
+
IntelliJ has a `chapter30` module and a **chapter30 tests** run configuration.
Build the native runtime and `sys` library with Make before debugging tests
that need them; see [the IntelliJ setup](../docs/intellij.md).