Commit Graph
122 Commits
Author SHA1 Message Date
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 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 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 7a25447281 trial: unblock the DOS build (local only, not for merge) 2026-08-16 23:02:43 +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
정시원 f946b14bde fix: keep M7 return context through if branches 2026-08-16 22:09:04 +09:00
정시원 24a99abd37 restore M7 fixture README 2026-08-16 22:01:07 +09:00
정시원 7d76167179 document M7 smoke coverage 2026-08-16 21:57:34 +09:00
정시원 53f4d79c4f expose ownership flags to M7 emitter 2026-08-16 21:55:28 +09:00
정시원 a2d7dffcae wire M7 DOS build 2026-08-16 21:55:00 +09:00
정시원 1856e99a61 wire M7 checker and emitter 2026-08-16 21:54:50 +09:00
정시원 8a2e6ddd1e implement M7 C emission 2026-08-16 21:52:57 +09:00
정시원 1e90eba1be implement M7 semantic checker 2026-08-16 21:44:46 +09:00
정시원 369aeb3b86 initialize contextual semantic metadata 2026-08-16 21:42:29 +09:00
정시원 a32ad3182a carry contextual M7 type metadata 2026-08-16 21:42:20 +09:00
정시원 c5e2496415 remove temporary M7 ownership overlay 2026-08-16 21:40:31 +09:00
정시원 b79970241a extend ownership through M7 wrappers 2026-08-16 21:40:22 +09:00
정시원 e0232a3e92 extend ownership for M7 wrapper types 2026-08-16 21:37:31 +09:00
정시원 5be7a5b852 preserve optional if-let patterns 2026-08-16 21:35:11 +09:00
정시원 8e23a6269b implement M7 optional and nominal result types 2026-08-16 21:34:10 +09:00
정시원 8ac76aed39 extend M7 type construction 2026-08-16 21:33:02 +09:00
정시원 411c86afc2 extend M7 semantic helpers 2026-08-16 21:32:46 +09:00
정시원 8b1efe3562 complete M7 type metadata 2026-08-16 21:32:34 +09:00
정시원 d43f28d06a wire M7 foundation into DOS build 2026-08-16 21:17:57 +09:00
정시원 bc9792532d wire M7 foundation into host build 2026-08-16 21:17:45 +09:00
정시원 47ef8420a9 add optional type kind 2026-08-16 21:17:33 +09:00
정시원 8aae7d9a63 implement cleanup lowering plan 2026-08-16 21:17:15 +09:00
정시원 40ec131ffc add cleanup lowering plan 2026-08-16 21:16:46 +09:00
정시원 c4f3accbb3 implement M7 semantic foundation 2026-08-16 21:16:30 +09:00
정시원 9c3cae7406 add M7 semantic foundation 2026-08-16 21:16:14 +09:00
coolguy aba9370ea6 implement M6 borrow checker integration 2026-08-16 20:46:45 +09:00
정시원 db0ed4bbe0 Merge pull request #6 from sebastianrcnt/agent/m6-ownership-core
implement M6 ownership analysis core
2026-08-16 20:01:32 +09:00
coolguy 748505513d test: audit M6-M9 fixtures for v0.1.8 2026-08-16 20:00:27 +09:00
정시원 3bcf6a33f1 tighten M6 ownership core semantics 2026-08-16 19:59:01 +09:00
정시원 8c4ec8a6e6 complete M6 ownership core primitives 2026-08-16 19:58:18 +09:00
정시원 db2b681f6b implement M6 ownership core 2026-08-16 19:57:15 +09:00
정시원 5981d2b5b1 add M6 ownership core interfaces 2026-08-16 19:56:15 +09:00
정시원 db51611225 route M5 ownership state through own.c 2026-08-16 19:47:19 +09:00
정시원 5c88872ef0 wire ownership helper into DOS build 2026-08-16 19:45:20 +09:00