Commit Graph
73 Commits
Author SHA1 Message Date
coolguy d1a4020087 own fixture 파일명과 unit 식별자 정리 2026-08-17 04:05:11 +09:00
coolguy 38dddc234c implement: load the unit graph -- imports, cycles, bindings
The driver handled one file. It now loads the graph rooted at the entry file
and checks every unit in it.

The import root is derived rather than configured: a unit named `a.b` read
from `<root>/a/b.fe` fixes `<root>`, so a sibling `import c.d;` is looked for
at `<root>/c/d.fe`. That is enough for the fixtures and for any tree that
follows 8.1, and it means there is no path flag to get wrong yet.

Loading is depth-first with the chain of units currently open kept on a stack,
so meeting one again is a cycle rather than a revisit -- a unit reached twice
by different paths is loaded once. Cycles, imports with no source file, and two
imports claiming the same binding are all reported.

One thing this exposed: FeDiags held a single source buffer, so once a build
spanned several files every excerpt was drawn from whichever file was parsed
last. `cycle/b.fe:3` printed the text of a.fe. fe_diags_source switches it, and
loading and checking both set it per unit.

The cycle fixtures lost their line markers on purpose. Which import closes the
cycle depends on which file you enter from -- entering at a.fe reports b.fe,
entering at b.fe reports a.fe -- so pinning a line would pin an arbitrary half
of a symmetric pair. The message is pinned; the line is not.

units: missing, bindconf and cycle pass, on top of the identity cases.
146 -> 148 of 188. What is left in units/ needs cross-unit name resolution and
visibility: an importer still cannot see `util.answer`.
2026-08-17 03:54:43 +09:00
coolguy 51e2568ba7 implement: unit identity -- dotted paths, name rules, source path
Units start here, with the part that needs no import graph: what a unit is
called and where it must live.

The parser only ever read a single identifier after `unit` and `import`, so
`unit game.main;` and `import std.io;` were syntax errors -- which is why the
dotted fixtures failed at the semicolon. It now reads a dotted path and stores
it canonically, dots included, since that spelling is the unit's identity
everywhere else. `import a.b as c;` parses too, with the alias on the node.

resolve.c is the new pass between parsing and checking, for the questions that
span files. It carries SPEC 8.1 so far: each path segment is ASCII lowercase,
starts with a letter, continues with letters, digits or underscore, and is at
most eight characters; and the dotted path must match the source path it was
read from, so game.world.map has to come from game/world/map.fe. The source
side is folded to lowercase before comparing, because a case-insensitive host
must not let two spellings become two units.

That rule then applied to the fixtures, which were not obeying it: 57 declared
a unit name unrelated to their file, left over from the milestone directories,
and eight had names too long to be legal. Both are now aligned -- the rule is
worth having only if the tree follows it.

units: badupper, badlong and unitbad pass. 138 -> 146 of 188. The rest of
units/ needs the import graph, which is the next piece: resolution, cycles,
bindings and visibility.
2026-08-17 03:47:28 +09:00
coolguy 547d8c5ec2 fix: reconcile the ERROR markers with what the checker reports
Checking the markers for the first time found five disagreements in areas that
are implemented. Four were the marker's fault:

- own/badarg pinned "self", but the rule being broken is that a returned
  reference must derive from a parameter -- `self` has nothing to do with it.
- own/badbrmov pinned line 9, which is the closing brace; the second destroy is
  on line 8.
- own/badloop pinned line 8, the destroy after the loop. The diagnostic is on
  line 6, inside it, and line 6 is right: the second iteration moves the same
  value again, so the loop body is where it is caught. Whoever wrote the marker
  expected the error after the loop.
- optional/badcatch pinned line 14, the body of the catch block. The catch
  expression on line 13 is what cannot fall through.

The fifth was the compiler's. own/badweak assigns a `&mut i32` to a `&i32` and
got "initializer type mismatch", which says nothing about why. Weakening an
exclusive borrow to a shared one is a specific rule and now says so, for
references and slices alike.

own/ is fully green: 50/50. Overall 133 -> 138 of 188. The six remaining marker
disagreements are all under units/ and generic/, where nothing is implemented
yet, so there is no diagnostic to compare against and no way to tell whether
the marker is right.
2026-08-17 03:42:06 +09:00
coolguy fb65152901 refactor: keep the front end, drop everything downstream of it
The milestone structure had stopped describing the compiler and started
shaping it: m7.c, check_m7.c, tests/m2..m9, and a checker and emitter that had
each grown past 2,500 lines because there was nowhere else to put anything.
Restart from the pipeline instead.

What is left is the front end -- lexer, parser, types, ownership, semantic
analysis -- and the fixtures that describe it. The C backend, the DOSBox-X
runner, the milestone registry and the batch build are removed. The driver now
stops after semantic analysis; a code generator attaches where emit_c did.

Fixtures move from milestone directories to what they check:

    parse/     grammar            own/       ownership and borrowing
    types/     type rules         optional/  optionals and error unions
    format/    formatting, try    units/     units and visibility
    generic/   generics           pending-backend/

pending-backend/ holds the three fixtures that can only be checked by running
a program -- that the bounds check traps, that --no-checks removes it, and that
drops and defers actually fire, verified through a fake allocator. Those are
not front-end tests and are not pretending to be; they come back first when
there is a code generator.

tests/run.py replaces the DOSBox-X harness. It builds the front end with the
pinned Watcom's Windows-hosted driver and runs every fixture in about two
seconds, and it does something the old runner structurally could not: it reads
the `// ERROR:line:text` marker each fixture carries and checks the diagnostic
against it. Those markers have been in the tree all along, unverified, because
DOS could not redirect the compiler's stderr and only the exit code was ever
compared.

133/188 pass. The 55 failures are not regressions -- they are what was already
true and invisible:

- units (27) and generic (23): `import`, `comptime` and generic declarations
  parse and are then dropped on the floor. No pass looks at them. The old
  registry did not list these fixtures at all, so nothing said so.
- five in own/, optional/ and generic/: a marker disagrees with the diagnostic
  about the line or the wording. Each is either a wrong marker or a wrong
  diagnostic and has to be read individually.

Everything removed is in git history.
2026-08-17 03:33:23 +09:00
coolguy 2696dd2abc docs: reduce SPEC.md to a language-only specification
Abandon the milestone-driven organisation of SPEC.md. The document now
contains only design philosophy (§1) and the language specification
proper (lexical structure, types, ownership/borrow rules, grammar,
semantics, module/unit semantics, and the minimal stdlib surface the
language itself depends on).

Removed:
- All milestone content (M1-M12: descriptions, completion criteria,
  ordering) and the roadmap/schedule framing.
- Compiler implementation directives: bootstrap strategy, pipeline,
  directory layout, C emission rules, own.c algorithm (§11 in full).
- Test/fixture plans and pass/fail/run16/boot fixture listings (§12
  in full).
- Build-driver/tooling detail that isn't part of the language itself:
  import-root search and ambiguity resolution, .fei cache/hash format,
  the full CLI flag reference table, C-backend evaluation-order
  lowering notes, and the generic-instance C emission ordering.
- SPEC.AUDIT.md entirely (git rm) — the accumulated change log for the
  old milestone-driven spec no longer applies.

Kept and reorganised: §2-§9 (targets, lexical structure, type system,
ownership/borrow rules R1-R11, grammar, statement/expression
semantics, unit/import/visibility semantics, generics) are otherwise
unchanged in wording. §10 (stdlib) is now a short placeholder noting
the stdlib spec is pending, while retaining the minimal surface the
language rules and builtins directly reference (core.Error, str alias
methods, mem.replace/create/destroy/alloc_slice, io.Writer/Reader,
fmt.fmt_*, sys.exit/on_exit). §13 (excluded features) is renumbered to
§11 and kept as-is since it documents language-design decisions, not
implementation.

AGENTS.md's document map is updated to drop the SPEC.AUDIT.md row and
reflect that SPEC.md is now language-only.

Implementation and stdlib specs are intended to be written as separate
documents going forward.
2026-08-17 02:42:12 +09:00
coolguy d5ba699744 dev: require the pinned toolchain, and clear the QEMU-era leftovers
The host gate accepted a WATCOM environment override and skipped when nothing
was found. Both are wrong for what it is: a system-wide Open Watcom is a
different version reporting different diagnostics, and a gate that skips is a
gate that is not running, which is the exact shape of the problem this file was
added to close. It now uses .dosboxx/watcom only and fails with the setup
command when that is absent, matching how dosboxx.py already behaves.

Nothing else in the project reaches for a system install: the DOS session sets
WATCOM=W: before calling BUILD.BAT, so the C:\DEVEL\WATCOMC fallback inside it
is unreachable.

fec/test-dos.bat was tracked but dead -- the runner generates RUN.BAT and only
copies build-dos.bat -- so it goes, along with the comment claiming it drives
the build and the three fixture READMEs that still pointed at it. The registry
decides what runs now.

(.qemu/ is untracked local debris from the same era and is left alone.)
2026-08-17 02:26:02 +09:00
coolguy 32ad50b14a dev: run the host gate as 16-bit, and drop what it found dead
The gate was using a system-wide Open Watcom that only ships the 32-bit
compiler. The project downloads its own toolchain, and .dosboxx/watcom/binnt
has the Windows-hosted 16-bit wcl.exe -- the same compiler and the same target
as the DOS build. Run that instead, with build-dos.bat's exact command.

The difference is not academic. Compiling 16-bit immediately reported three
functions the unification had orphaned: emit_type_helpers, which the M7
program emitter replaced, and emit_drop_helpers and emit_drop_fields, which it
was the only caller of. The 32-bit check had been clean.

Those warnings were going to the DOS screen, where the runner cannot see them:
COMMAND.COM redirects handle 1 only, and Watcom writes diagnostics to handle 2.
So the suite was green while the build was not quiet. Nothing in the runner
would ever have said so.

Found while chasing W210/W107 reported from a DOS screen, which this does not
yet explain -- those are not among what the compiler build emits now.

M1-M7: 214 passed.
2026-08-17 02:22:44 +09:00
coolguy ee2b417013 fix: restore the rules the M7 half never had to implement
Unifying the two engines exposed what the split had been hiding: every rule
that lived only in the M6 body was silently dropped for units the M7 half
claimed, and since M1-M6 sources never reached that half, nothing failed until
they all did. Eleven cases across m3, m5 and m6 caught it.

Checker, all from the M6 statement and lvalue cases:

- writing a struct field needs a writable place, so `p.x = 3` on a `let` is an
  error again (m3-badfield)
- `let` cannot bind a mutable slice, a var with no initializer needs a type,
  and a void expression cannot initialize (m3-bad-mlet)
- a returned reference must derive from a parameter or a static, and a void
  expression cannot be returned from a value function (m6-badarg, badret,
  badself, badtwo, badlocsl)
- rebinding a reference must not outlive its source scope, and must release the
  previous borrow (m6-badscop)
- the loop case delegates to the core, which carries the flow capture and merge
  that detects a value moved on every iteration; the M7 version had none of it
  (m5-bad-loop)

Emitter:

- builtins other than the print family (@size_of, @align_of) and the str alias
  methods are lowered by the core, which the M7 call path never reached, so
  they were emitted verbatim into the C (m3-struct)
- the trim helper is emitted from the M7 type-helper pass as well, not only the
  core one, or the call has no definition to link (m6-oktrim)

M1-M7 all green: 16, 19, 50, 20, 16, 57, 42.
2026-08-17 02:07:26 +09:00
coolguy eb85e9fa3d dev: run host syntax gates before starting DOSBox-X
There is an Open Watcom install on this host (C:\WATCOM19), and its Windows
build compiles the compiler's own sources in about a second. Every declaration
mismatch in the unification commits was found that way; each one would
otherwise have cost a DOSBox-X boot and a full compiler build to surface, with
a DOS-side message that names the wrong thing.

Add tools/tests/test_host_syntax.py: compile all twelve sources with the flags
build-dos.bat uses (-za -wx -wcd=202) and fail on any diagnostic. It skips when
Watcom is absent, so the suite still runs elsewhere. Two structural checks come
with it -- that build-dos.bat, the Makefile and the test agree on the source
list, and that no .c under fec/src is compiled by nothing. Both would have
caught check.c and emit_c.c quietly leaving the build when the M7 wrappers
included them textually.

ferro-test now runs these and the 8.3 name check before starting the VM, and
stops if they fail.

This is not verification and does not claim to be: wcc386 targets 32-bit where
the real build is 16-bit large model, so it sees syntax, types and declarations
and nothing about code generation. The DOS build and the milestone suite remain
the gate. It only moves the cheap failures earlier.
2026-08-17 01:57:06 +09:00
coolguy 4adfe60574 fix: repair the unified emitter's declarations
Compiling the merged sources with the Open Watcom install on this host
(C:\WATCOM19\binnt) found four things the merge got wrong, none of which any
amount of reading would have caught reliably:

- FE_M7_FLOW_CAP and <stdlib.h> lived in check_m7.c's preamble, above the
  textual include, and were dropped with the wrapper.
- emit_error_return takes a const char *, not a FeNode *; the hand-written
  forward declaration disagreed with the definition.
- emit_match was never defined by the M7 half, only called, so renaming it
  alongside the other delegating pairs left a declared-but-undefined static.
- type_needs_drop and emit_lvalue are used a few hundred lines before the
  declaration block, so their declarations had to be hoisted.

Also drop two locals that existed only to be cast to void.

All twelve compiler sources now compile with -za -wx -wcd=202 and produce no
warnings. That is a syntax and type check, not verification -- the DOS build
and the milestone suite remain the gate.
2026-08-17 01:54:17 +09:00
coolguy 3b8c8ac674 refactor: unify M7 lowering and C emission
emitcm7.c was the same wrapper trick as the checker: nineteen #define renames,
a textual include of emit_c.c, and a per-unit feature scan choosing between two
emitters. Five of the M7 halves fell through to their M6 counterpart, so those
become the single entry point with the old body renamed to *_core; the other
thirteen never delegated at all and simply replace the M6 version.

The scan is gone from both places it was used -- the program entry and the
expression emitter, whose switch already ended in a default that delegates.
fe_emit_c_program_core went with the dispatch that was its only caller.

emitcm7.c is deleted and the build compiles emit_c.c directly. Nothing in the
compiler now selects an implementation by looking for `?` or `!` in a unit.

This is the pair to the checker commit; together they end the two-engine split
that produced four of the five defects fixed while getting M7 to pass.
2026-08-17 01:49:02 +09:00
coolguy 3091df7a78 refactor: unify the M1-M7 checker
check_m7.c textually included check.c, renamed three entry points aside, and
selected between two whole checkers by scanning each unit for `?`, `!`, `try`
and friends. A unit that mentioned any of them was checked by a second
implementation, so an M1-M6 rule fixed in check.c never reached it -- and the
split hid real defects, since the M7 half was reached by no existing fixture
until M7 cases were registered.

The split was cheaper to undo than it looked: every M7 dispatcher already
delegated to its M6 counterpart for nodes it did not handle. So the M7 entry
points become the single check_expr/check_stmt/check_lvalue/check_call, and
the former M6 bodies become check_expr_core/check_stmt_core/check_lvalue_core,
reached as the fallback. Recursion runs through the unified entry, which is
what makes an optional nested inside otherwise-M6 code get checked at all.

m7_check_fn and m7_check_method were identical to the M6 versions apart from
which check_stmt they called, so they are dropped. The feature scanner
(m7_type_ast, m7_node_feature, m7_program_feature) is gone with the dispatch
it fed, including the loop case that still consulted it. fe_check_program and
fe_check_expr_type keep the M7 bodies, which are supersets.

The build compiles check.c directly again.

No behaviour intended to change: the unified checker applies the union of the
rules to every unit, which for M1-M6 sources is what the M6 half already did.
2026-08-17 01:45:04 +09:00
coolguy d5e7a8d8d6 dev: restore core=dynamic; the trap failures were the PATH bug
The previous commit blamed core=dynamic for the M3 bounds cases exiting 0
instead of trapping. That was wrong. Both suspects were live at the time --
the recompiler and a cache hit that skipped BUILD.BAT's `set PATH` -- and the
core was ruled out before the PATH line was restored, so the test proved
nothing.

Retested with the PATH fix in place: core=dynamic gives 50 passed on m3, the
traps included. abort() reports its exit status fine under the recompiler.

Per milestone, warm cache, all green:

    m1  2s   m2  3s   m3  7s   m4  3s
    m5  3s   m6  6s   m7  5s        29s total

against 65s on the interpreter.
2026-08-17 01:36:48 +09:00
coolguy 0132ad4135 fix: lower short catch as statements, and drop core=dynamic
Compiling okcatmov.fe's output crashed wcc386 hard enough to take DOSBox-X
down with it -- "no byte handler for write to ffffffc", emulator exit 1, the
whole suite lost rather than one case failing. `result catch fallback` in
return position lowered to `((tmp = X), tmp.e ? fallback : tmp.v)`, and with a
moved operand X is itself `(fe_live_x=0, x)`, so the compiler met a struct
assignment whose right side was a comma expression. Lower it as statements,
and clear a move flag on its own line rather than inside the assignment.

Revert core=dynamic from the previous commit. It was about 5x faster and it is
wrong: the recompiler loses abort()'s exit status, so a trapping program exits
0 and the M3 bounds cases stop reporting the trap they exist to prove. Checked
against a compiler built under core=normal, so the fault is in running the
generated program, not in building the compiler.

Also fix the build cache: BUILD.BAT puts the Watcom binaries on PATH, and
skipping it on a cache hit left the case commands without it, which silently
changed how the trap programs terminated. Reproduce that line.

Per milestone, warm cache: m1 2s, m2 5s, m3 20s, m4 7s, m5 6s, m6 14s, m7 11s.
All green, 220 cases.
2026-08-17 01:34:20 +09:00
coolguy 044c0f0e98 dev: cache the compiler build and drop unused headers
Every run rebuilt fec from scratch inside DOS, about 14 seconds, even when no
source had changed. Key a cache on the hash of fec/src plus build-dos.bat and
restore FEC.EXE when it matches; the toolchain itself is pinned by
dosboxx.lock.json so it cannot drift under a hit. Only a passing build is
cached, and the batch skips BUILD.BAT on a hit because it would delete and
rebuild the executable it was just given.

Generated C included stdio.h unconditionally, but only the M4 writer runtime
reaches it. A 26-line unit was pulling in roughly 1900 lines of headers, paid
once per compile check. Emit it only when the runtime is emitted.

--only m6, 57 passed, over three changes:

    31s   before
    17s   core=dynamic
     2s   warm compiler cache

Cold runs still pay the build once.
2026-08-17 01:17:40 +09:00
coolguy aaf31302d5 dev: compile the generated C, and fix what that caught
Thirty-one cases only ever emitted C and never built it -- m5 owned/defer, and
every ok* acceptance fixture in m6 and m7. They asserted that fec produced a
file, not that the file was a program, so a malformed emission sat unnoticed.
Compile each one with wcl386 -c. Fixtures without a main cannot be run, so this
is the floor for them; it is a conformance check on the backend's output, not an
assertion about C, so a future backend swaps the command rather than the intent.

It found two defects in the M6 emitter immediately, both predating M7:

&s[0] took the address of an rvalue. Borrowing went through emit_expr, which
lowers an index to the bounds-checking accessor, so `&s[0]` became
`&fe_idx_slice_type_2(s, 0)`. emit_lvalue spells the same element as `s.p[0]`.
The M7 emitter already did this correctly; only the M6 path was wrong.

line.trim() was never lowered. SPEC lists trim among the built-in alias methods
on str and the checker accepts it, but no emitter case existed, so it emitted
`fe_l_line_0.trim()` -- a member access on a slice struct. Emit the helper, and
only for programs that actually trim.

Also set core=dynamic and cycles=max. core=auto uses the interpreter in real
mode, which is where the 16-bit compiler build spends its time; nothing here is
timing sensitive. m6 drops from 31s to 17s. Raise the DOSBox timeout to match a
whole-suite run, which now pays a process spawn per compile check.

Verified: --only m6, 57 passed.
2026-08-17 01:14:47 +09:00
coolguy 23079ba9fb spec: mark the try enforcement entry resolved by M7 2026-08-17 00:03:26 +09:00
coolguy a2428ca5da fix: enforce the try rule at every position
SPEC.md allows `try` only inside a function returning an error union, but the
check sat in the FE_N_EXPR_STMT case, so it only ever saw a bare `try e;` and
walked past `var x = try e;` and `x = try e;`. Move it onto the try expression
in check_expr and drop the statement-level copy.

This could not land before M7: closing the hole forces m5/runtime.fe's `run` to
return an error union, and value returns from `-> !T` need contextual success
construction. That arrives with M7, and `run` is now `-> !i32`, so the rule can
be enforced. Supersedes the SPEC.AUDIT.md entry that recorded the blockage.

M1-M7: 183 passed.
2026-08-17 00:03:26 +09:00
coolguy 6713a934f5 fix: emit mem.create and mem.alloc_slice on the M7 path
m7_emit_call reimplements call emission and only carried over mem.destroy and
mem.replace, so every mem.create/mem.alloc_slice fell through to the generic
member path and emitted `fe_missing.create(0)`. wcc386 does not diagnose that
-- it terminates with exit 255, which wcl386 reports as "Unable to invoke
wcc386.exe" with no message at all.

The breakage covered all 17 allocation sites in m5/runtime.fe, and it was
invisible because m5-owned and m5-defer only emit C; m5-runtime is the one M5
fixture that compiles and links what was generated.

Also make runtime.fe legal: `run` used `try` while returning i32, which SPEC
allows only in a function returning an error union. It is `-> !i32` now, which
M7 accepts because contextual success construction lands with it, and
runtime.c takes the { error, value } struct the error union lowers to.

Found by calling wcc386 directly instead of through wcl386, which is the only
way to see a compiler crash here.

M1-M7: 183 passed.
2026-08-17 00:01:51 +09:00
coolguy 44dc5562ef fix: repair three M7 C emission defects
emitcm7.c reimplements the emitter for sources that mention M7 syntax, and
three things were lost in the port. All of them only reached M4 fixtures,
because M1-M3 and M6 take the M6 fast path.

Local initializers named "0". The LET/VAR case passed the declaration node to
emit_lvalue, which matches only IDENT/MEMBER/INDEX and otherwise falls through
to the raw expression path -- a declaration node renders there as "0", so every
`var x = init;` emitted `0 = init;`. Teach emit_lvalue that a declaration names
its own storage, which also fixes the catch path that had the same call.

Aggregate initializers were not constant. A string-literal `const` lowered to a
maker call, but C89 requires a constant expression for aggregate initializers
at file scope and for automatics alike, and the build runs with -za. Restore
the braced form for both the local and the global path.

Slice helpers were never emitted. The final loop in m7_emit_type_helpers is
commented as reusing the M3 index/slice generator but only ported the index
half, so bodies called fe_slice_*/fe_full_*/fe_tail_* that no declaration
defined. Emit the three slicers for array and slice types.

The first defect masked the other two: wcc386 died on `0 = ...` before it could
reach them, and wcl386 reports that as "Unable to invoke wcc386.exe" with no
diagnostic, which is why this needed bisecting against master's output rather
than reading an error message.

M1-M7: 7 failed, 176 passed -> 3 failed, 180 passed. The remainder is m5
runtime, which is a separate fixture issue.
2026-08-16 23:48:18 +09:00
coolguy 05ae2bea9a fix: unblock the DOS build for M7 sources 2026-08-16 23:39:05 +09:00
coolguy ff98176cc2 Merge branch 'master' into m7-trial 2026-08-16 23:30:36 +09:00
coolguyandClaude Opus 5 0a434d8a9a spec: record why the try rule cannot be enforced yet
SPEC.md allows `try` only inside a function returning an error union, but
check.c only tests it in the FE_N_EXPR_STMT case, so `var x = try e;` and
`x = try e;` walk straight past. m5/owned.fe and m5/runtime.fe both depend on
that gap.

Moving the check onto the try expression is a four-line change and it is
correct, but it cannot land yet. runtime.fe's `run` allocates and returns a
value, so closing the hole forces it to return an error union -- and master
rejects `return <value>;` in `-> !i32` ("return type mismatch") as well as a
bare `return;` in `-> !void` ("void expression returned from value function").
Both need contextual success construction, which is M7 work. `catch` and
`@trap()`, the two spellings SPEC offers as alternatives, are also M7-only, so
there is no way to express `run` legally on master today. All three paths were
tried in DOSBox-X, not assumed.

Fix owned.fe now, since `main() -> !void` is legal today and matches
m4/try-fpr.fe, and leave the checker alone until M7 lands with the rest.

Verified: 155 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
2026-08-16 23:23:47 +09:00
coolguy 9e074e2e16 Merge branch 'master' into m7-trial 2026-08-16 23:09:39 +09:00
coolguyandClaude Opus 5 86bff9a06d dev: capture compiler diagnostics under DOS
Every fec error message was written to stderr, and COMMAND.COM can only
redirect handle 1 -- ">" is the whole vocabulary, "2>" is not parsed at all.
So a failing compile recorded exit code 1 and a zero-byte log, and the actual
message went to a screen nobody reads. Confirmed directly: `FEC.EXE --check`
on a fixture that must fail produced rc=1 and 0 bytes of stdout.

That is why an unexpected compiler failure was undiagnosable. It also means
the M6 reject cases have only ever asserted "exit code was nonzero" -- the
error text they nominally check has never been observable to the runner.

Add fe_diag_stream(), which resolves once to stdout when FE_DIAG_STDOUT is set
and stderr otherwise, and route diag.c and driver.c through it. The default is
unchanged, so interactive use keeps writing to stderr; the runner sets the
variable in RUN.BAT. The stderr references in check.c and emit_c.c are the
Ferro language's own std.io.stderr writer and are deliberately untouched.

Verified in DOSBox-X: 155 passed. A rejecting compile now records its message,
source excerpt and caret in RESULTS\<key>.LOG.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
2026-08-16 23:09:30 +09:00
coolguy e457ee46b2 Merge branch 'master' into m7-trial 2026-08-16 23:02:43 +09:00
coolguy 7a25447281 trial: unblock the DOS build (local only, not for merge) 2026-08-16 23:02:43 +09:00
coolguyandClaude Opus 5 8b7d6a8d09 dev: make DOS build failures diagnosable
Reviewing an M7 branch took six DOSBox-X runs to find three build blockers
that each take a second to explain. The runner threw away everything needed to
see them.

Capture the compiler build's output. Case commands were redirected to
RESULTS\<key>.LOG but `call BUILD.BAT` was not, so the step that fails first
and blocks every case left only BUILD.FAIL containing the string "FAIL". The
twelve wcl invocations inside it were invisible; finding "Unable to open
src\emit_c_m7.c" meant hand-editing build-dos.bat to add a redirect and
re-running the VM.

Record exit codes. The batch collapsed every outcome to `if errorlevel 1`, so
a compiler that aborted and one that exited 1 with a diagnostic were the same
FAIL. RC.BAT now walks a descending errorlevel ladder into RESULTS\<key>.RC
and the host derives pass/fail from it, which immediately separates an
ordinary rejection (1) from a trap (255). Note the space in `echo 0 >FILE`:
without it DOS parses `0>` as a redirect of handle 0.

Stop falling back to CONSOLE.LOG. That is DOSBox-X's own log -- display
enumeration and INT15 chatter -- so a crashed command reported fifty lines of
emulator noise instead of saying it produced no output.

Add tools/tests/test_dos_names.py. An over-long source name reaches the DOS
build as `Unable to open "src\..."`, which reads as a missing file rather than
a name FAT cannot represent, and only after a VM boot and ten object builds.
The check runs on the host in 0.03s and flags emit_c_m7.c (9-character stem)
on the branch that prompted this.

Also pass -k through to pytest so a single case can be re-run without its
whole milestone, and print the resolved ROOT at startup: an editable install
plus a git worktree will otherwise silently build a different checkout than
the one the shell is in.

Verified on master: 155 passed, unchanged. Recorded codes are 0 for success,
1 for rejections, 255 for the three bounds traps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
2026-08-16 23:02:28 +09:00
coolguy 63bc8e8724 Merge branch 'master' into m7-trial 2026-08-16 22:37:51 +09:00
coolguyandClaude Opus 5 5529e3dc14 test: prove --no-checks removes the bounds check
The bounds-no-checks cases emitted and built but never ran, so they only
proved that --no-checks produces compilable C -- not that it removes the
check, which is the entire point of the flag.

A run case could not simply be appended. BOUNDS.FE returns the out-of-bounds
element directly, so with checks removed its exit code is whatever sits past
the array on the stack and there is no correct status to assert. Asserting on
the generated C instead does not work either: emit_c.c defines fe_trap_bounds
unconditionally and --no-checks only suppresses the call sites.

Add NOCHK.FE, which reads one element past a [2]i32 and returns x - x. That
is 0 for whatever garbage the unchecked read produced, so the same source has
a defined outcome both ways: compiled with checks it must trap, compiled with
--no-checks it must run to completion and exit 0. Register both halves and
drop the two BOUNDS-N cases they supersede.

Verified in DOSBox-X: 155 passed, including m3-nochk-trap failing as expected
and m3-nochk-off-run succeeding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
2026-08-16 22:23:41 +09:00
coolguy 594704a07d dev: promote DOSBox-X and remove QEMU support 2026-08-16 22:14:50 +09:00
coolguyandClaude Opus 5 4ad3e3097b refactor: derive the milestone bounds from the registry
The highest supported milestone was spelled out in six places across four
files: range(1, 7) and default="m6" in test_cli.py, the same pair in
test_milestones_dosboxx.py, and through=6 in both registry.py and suite.py.
Registering M7 meant finding all six, and missing one failed silently.

Derive MAX_MILESTONE and MILESTONES from CASES instead, and move the mN
selector parser to registry.milestone_number so the pytest module stops
carrying its own copy. Adding cases for a new milestone is now enough for
ferro-test to accept --through/--only for it.

No behaviour change: MAX_MILESTONE evaluates to 6, ferro-test still advertises
{m1..m6} with default m6, and the case snapshot is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
2026-08-16 22:11:36 +09:00
coolguyandClaude Opus 5 c25312135d refactor: rebuild the milestone registry as a table
registry.py was literally `cat registry_m1_m3.py registry_m4_m6.py`, which
left a duplicate `_c` definition and a stray module-level docstring at the
seam. Replace the hand-rolled append loops with five builders -- _emit, _wcl,
_triple, _rejects, _dump_ast -- and express the cases as one ordered list.

The irregularities are now parameters instead of one-off code, each with the
reason recorded:

- _triple(stem=) for M2 castwhil emitting CAST16
- _triple(build_source=) for M4 prop, which emits PROP.C but compiles
  PROPTEST.C because that file #includes it
- _triple(run_suffix="trap", run_ok=False) for the M3 bounds cases
- _triple(emit_suffix=None) for M4, whose ids lack the -emit suffix
- _emit(output_first=True) for M6, which passes -o before the input

The three scattered 8.3 output-name mappings collapse into one _OUT83 table
keyed by (milestone, name). The key needs both: bad-type is BAD-TY in M2 but
BAD-TYP in M4, and bad-cond is BAD-CO in M2 but unshortened in M5. Values are
carried over verbatim -- the shortenings are inconsistent and several were
never required, but that is a separate decision.

suite.py keeps only Case and drops the all_cases forwarder, so the
registry -> suite -> registry cycle is gone along with the function-scoped
import that worked around it. dosboxx.py still imports Case from suite and is
untouched.

Verified behaviour-preserving by snapshotting (id, milestone, command,
expect_success) for all 150 cases in order before and after: diff is empty.
pytest collects the same 151 items.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
2026-08-16 22:11:13 +09:00
coolguy 0dada80e65 test: unify registry and capture Watcom diagnostics 2026-08-16 21:54:22 +09:00
coolguy 3a7d7de0d3 test: add unified DOSBox-X milestone runner 2026-08-16 21:44:39 +09:00
coolguy aba9370ea6 implement M6 borrow checker integration 2026-08-16 20:46:45 +09:00
coolguy 748505513d test: audit M6-M9 fixtures for v0.1.8 2026-08-16 20:00:27 +09:00
coolguy 3de0564650 freeze M6-M9 semantics and unit model 2026-08-16 19:48:19 +09:00
coolguy 9986da6f12 chore: isolate Zed clangd from Watcom headers 2026-08-16 19:03:23 +09:00
coolguy d915a7e63c chore: remove obsolete host test scripts 2026-08-16 18:54:23 +09:00
coolguy deafd27939 feat: complete M5 ownership and cleanup 2026-08-16 18:48:35 +09:00
coolguy 351e5dbb23 feat: replace M4 callback writers with safe handles 2026-08-16 18:27:26 +09:00
coolguy 42abc0d7e7 feat: align M3 slices and strings with v0.1.7 2026-08-16 18:23:30 +09:00
coolguy cba2c86fbc fix: reliably answer delayed DOS batch termination prompts 2026-08-16 18:11:52 +09:00
coolguyandClaude Opus 5 ec48a60e3e feat: supervise EXEC with guest liveness instead of a stopwatch
TCPAGENT는 system()이 도는 동안 통째로 얼어 있어 응답도 진행 보고도 못 한다.
그런데 호스트는 소켓에 30초 고정 타임아웃을 걸고, 만료되면 연결 자체를 버렸다.
그래서 35초짜리 컴파일이 "느린 명령"이 아니라 "죽은 에이전트"로 취급됐다.

QEMU는 게스트가 얼어 있어도 계속 돈다. info blockstats의 idle_time_ns로
"작업 중"과 "멈춤"을 구분한다. 실측으로 확인했다: 에이전트가 완전히 벙어리인
동안에도 rd_operations가 7초당 47000씩 증가하고 idle은 0.00s를 유지한다.

- EXEC은 짧은 간격으로 깨어나 감시만 하고 소켓은 절대 안 버린다.
- --idle-timeout(기본 60s)과 --hard-timeout(기본 900s). 후자는 디스크를
  안 쓰는 CPU 바운드 멈춤용 백스톱이다.
- 중단은 QEMU 모니터로 Ctrl+C를 주입하고 COMMAND.COM의
  "Terminate batch file (Y/N/A)?" 프롬프트에 답한다.
- Ctrl+C는 DOS break check에서만 먹는다. FreeDOS 기본값 BREAK=OFF에서
  출력을 파일로 돌린 CPU 바운드 자식은 거기 도달 안 할 수 있다. 그래서
  중단은 보장이 아니라 요청으로 다루고, 명령이 안 멈춰도 RESULT를 끝까지
  수거해 스트림을 깨뜨리지 않는다.
- ferro-vm abort 추가. 실행 중에도 응답해야 하므로 파이프 서버를 요청당
  스레드로 바꿨다.
- 5558 바인딩을 SO_EXCLUSIVEADDRUSE로. Windows의 SO_REUSEADDR는 다른
  프로세스가 같은 포트를 잡아 조용히 반쯤 동작하게 만든다.

검증 (QEMU FreeDOS 실측):
- 32.4초 명령 정상 완료 (이전에는 30초에 실패)
- 실행 중 abort가 0.1초에 응답, exit=95로 종료, 부분 출력 1805B 수거,
  연결 유지
- pause처럼 디스크를 안 쓰는 명령을 idle 15s로 검출해 중단

시리얼 시절에 있다가 TCP 전환에서 사라진 TODO 3건을 복구한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 17:42:43 +09:00
coolguy f798a1359f docs: resolve ownership and standard library spec audit 2026-08-16 17:29:45 +09:00
coolguyandClaude Opus 5 8c3dee222e feat: color the agent console and log every command
DOS 콘솔에서 직접 보기 위한 네 가지 수정.

1. 컬러. Open Watcom의 conio.h에는 textattr()이 없고(cprintf/cputs/getch만
   제공) 이 FreeDOS 콘솔은 ANSI 이스케이프도 해석하지 않는다. 그래서 cprintf가
   줄을 배치하게 두고 -- 스크롤을 알아서 처리한다 -- 방금 쓴 셀의 VGA 속성
   바이트만 다시 칠한다. 커서가 그 줄 다음 행의 0열에 있다는 점을 이용해
   스크롤을 직접 추적하지 않고 대상 셀을 찾는다. 줄바꿈된 긴 줄도 처리한다.

2. GET, HASH, LIST, READ, WRITE, QUIT, 미지원 명령을 로깅한다. 이전에는
   EXEC과 PUT만 보였다. PING은 wait-ready가 초당 두 번 폴링하므로 제외한다.
   LIST는 잘림 여부를, PUT은 short write를 구분해 남긴다.

3. put_path가 채워지기만 하고 쓰이지 않아 PUT 완료 줄에 경로가 없었다.
   완료 경로가 둘(길이 0, 본문 수신 완료)이라 put_finished()로 합쳤다.

4. 틱->초 변환이 1.1% 빨랐다. BIOS 틱은 18.2065Hz이므로 delta/18이 아니라
   delta*549/100 (하루치 틱에도 32비트를 넘지 않는다) 을 쓴다.

REBUILD.BAT을 추가한다. BUILD.BAT은 현재 디렉터리에서 wmake만 실행하는데
호스트가 exec으로 부를 때의 시작 디렉터리가 거기가 아니다.

QEMU FreeDOS에서 Open Watcom C++16으로 빌드하고(no warnings, -we 활성)
콘솔 스크린샷으로 색상, 줄바꿈 색칠, 명령별 로그를 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 17:13:05 +09:00
coolguyandClaude Opus 5 eff5cbfb12 docs: make the CLI the source of truth for ferro-vm commands
명령 목록이 tools/README.md와 argparse 정의 두 곳에 손으로 동기화되고 있었다.
드리프트가 불가피하므로 목록을 --help로 단일화한다.

cli.py에 서브커맨드별 help/description, 인자 metavar, 예시 epilog를 채웠다.
put의 DOS 8.3 이름 제약처럼 명령에 직접 붙는 함정은 해당 도움말에 넣었다.
tools/README.md는 호스트 요구사항, 셋업, 자동화 구조로 줄이고 목록은 --help로
넘긴다. AGENTS.md에는 CLI가 규범이라는 포인터를 남긴다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 17:01:30 +09:00
coolguyandClaude Opus 5 2e7502bfa6 chore: ignore stray Windows nul artifact
Git Bash에서 `> nul`을 실행하면 NUL 장치가 아니라 실제 파일이 생긴다. 커밋되면
Windows 체크아웃이 깨지므로 무시하고 기존 흔적을 지운다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 16:58:42 +09:00
coolguyandClaude Opus 5 c57fa9441d docs: add AGENTS.md and link it from CLAUDE.md
HANDOFF.md 제거로 사라진 VM 빌드 함정(large model, *.obj 링크, -wx -wcd=202,
8.3 파일명, D: 빌드 금지, Ctrl+C 복구)을 되살리고 검증 규칙과 현재 마일스톤
상태를 함께 정리한다. 도구 사용법은 tools/README.md로 링크한다.

CLAUDE.md는 @AGENTS.md 참조만 둔다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 16:57:03 +09:00
coolguyandClaude Opus 5 f88559c635 wip: extend M5 ownership tests and cleanup emission
조건부 이동, 이중 destroy, 직접 drop 호출에 대한 실패 fixture를 추가하고
runtime harness와 test-dos.bat를 그에 맞춰 갱신한다.

M5는 아직 완료가 아니다. 모든 경로에서 정확히 1회 cleanup, defer/drop의 선언
역순 병합, try 전파 경로 cleanup, MaybeMoved 런타임 live flag, struct drop과
필드 역순 drop, 분기/루프 상태 합류, 누수/이중해제 카운터 harness가 남아 있다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 16:54:41 +09:00
coolguyandClaude Opus 5 41de8aa2dc docs: revise spec to v0.1.6 and drop stale handoff
R8을 파생 반환 규칙으로 교체해 str.trim 계열의 슬라이스 반환을 표현 가능하게
하고, 결과를 지역 변수에 바인딩할 수 있게 한다. R6의 대여 구간을 마지막 사용
지점까지로 좁히고, R10에 전역 대여 금지를 추가해 호출 경계에서 배타성이 깨지던
구멍을 막는다. error.Name 코드 부여를 링크 심볼에서 드라이버의 emit 이전 단계로
옮겨 컴파일타임 상수로 만들고, str을 []u8과 별개 타입으로 분리한다.

문법 결함(struct 멤버의 pub, enum 배리언트 필드, catch/orelse 프로덕션,
error_decl, expr 규범)과 CLI 플래그 산재, 문서-구현 드리프트를 함께 정리한다.
변경 사유는 SPEC.AUDIT.md에 기록했다.

HANDOFF.md는 기준 SHA와 통신 프로토콜이 모두 낡아 제거한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT
2026-08-16 16:54:34 +09:00
coolguy dc2285b4d4 feat: capture complete DOS command diagnostics 2026-08-16 16:48:47 +09:00
coolguy 5d9cd90299 fix: use clean VM reset and resilient agent listener 2026-08-16 16:36:24 +09:00
coolguy a637e591e1 feat: add verified QEMU soft reset 2026-08-16 16:22:36 +09:00
coolguy 7cbbb709eb feat: consolidate VM automation in Python daemon 2026-08-16 16:19:19 +09:00
coolguy 2477777e1a feat: add RapidOCR QEMU console capture 2026-08-16 16:02:37 +09:00
coolguy 8e8ab22d30 feat: replace serial automation with resident TCP agent 2026-08-16 15:51:42 +09:00
coolguy e5781fc168 feat: add TCP staging path for FreeDOS 2026-08-16 14:46:33 +09:00
coolguy f662c4b8f7 chore: add 115200 baud DOS staging tools 2026-08-16 14:37:20 +09:00
coolguy 631d2ebb48 wip: advance M5 ownership cleanup 2026-08-16 14:22:03 +09:00
coolguy 6acfc5918d docs: add implementation handoff 2026-08-16 13:35:41 +09:00
coolguy 53bca214b8 feat: implement M4 formatting builtins 2026-08-16 13:24:25 +09:00
coolguy 8e6a409637 feat: implement M3 aggregate types and iteration 2026-08-16 08:49:50 +09:00
coolguy 57b47a574a docs: disambiguate control-flow headers 2026-08-16 08:42:21 +09:00
coolguy 95de333da4 docs: disambiguate match scrutinees 2026-08-16 08:33:19 +09:00
coolguy 3aa7618d0c docs: clarify char and byte conversions 2026-08-16 08:16:24 +09:00
coolguy da78a615fb feat: add M2 type checking and C emission 2026-08-16 07:40:50 +09:00
coolguy 005056a5ea feat: implement M1 Ferro frontend 2026-08-16 07:10:53 +09:00
coolguy 990068f424 docs: resolve language audit for Ferro v0.1.2 2026-08-16 06:44:39 +09:00
coolguy c7b0217d43 docs: define formatting builtins and interface roadmap 2026-08-16 06:16:03 +09:00
coolguy cbfad06e6f Initial DOS VM tooling setup 2026-08-16 06:12:59 +09:00