Commit Graph
10 Commits
Author SHA1 Message Date
coolguy 730bcac282 audit: SPEC-파서 괴리 일곱을 정리한다
구현 셋, SPEC 다섯. 어느 쪽이 틀렸는지는 항목마다 따로 판정했다.

구현:
- ~ 를 넣었다. ^ 는 xor 과 .^ 가 가져가서 비트 NOT 이 자기 철자를 못 갖고
  있었다. 렉서·파서·검사·lowering(전부 1 과의 xor).
- 전역 static/var 는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의
  생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. const 는 그대로 추론한다.
- error code 는 정수 리터럴 하나다. 식이면 중복도 예약된 0 도 검사할 수 없다.

SPEC:
- 최상위 comptime if 를 뺐다. comptime 조건은 타입 술어뿐인데(§7.5) 유닛
  바깥에는 바인딩된 타입 파라미터가 없어 물어볼 것이 없다. §11 v0.2.
- 타입 이름은 [binding.]Name 이다. import 가 마지막 segment 를 바인딩하므로
  점 둘 이상은 만들어질 수 없는데 문법이 unit_path 를 쓰고 있었다.
- catch 의 EBNF 가 §4.6 보다 넓었다. 값을 주는 짧은 형태와 에러를 받는 블록
  형태 둘로 나눠 적었다.
- | 와 ^ 를 한 단계로 둔 것을 쪼갰다. 합치면 a | b ^ c 가 좌결합으로
  (a|b)^c 가 되어 C 에서 온 사람을 속인다. 구현이 C 순서로 옳았다.
- 마지막 필드 쉼표 생략을 명세에 적었다. enum 은 이미 허용하고 있었다.

그리고 tests/run.py 가 마커를 진단 스트림에만 맞춘다. --dump-ast 모드에서는
AST 덤프가 stdout 으로 먼저 나와서 parse/ fixture 는 마커를 쓸 수 없었다.

249/249, 39/39.
2026-08-17 16:52:12 +09:00
coolguy f06a12f341 GOAL P4: --report-unsafe 와 --report-instances, 그리고 예산을 CI 에
'ffec 에서 unsafe 가 몇 군데인가' 는 좋은 지표인데 세는 방법이 없으면 지표가
아니다. 이제 센다.

  unit                   unsafe       *T  unchecked
  std.io                      6        1          0
  std.sys                    10       12          0
  total                      16       13          0
  outside std                 0        0          0

Ferro 렉서와 파서, interner, 아레나, 맵을 쓰는 프로그램 전부가 std 바깥에서
0 이다. 목표치를 이미 지키고 있었던 셈인데, 그것을 아무도 확인할 수 없었다.

run.py 가 그 숫자를 검사한다. std.mem 과 std.sys 밖의 unsafe 와 *T 는 검사기가
약속한 것에 뚫린 구멍이므로 0 을 유지해야 하고, 늘어나면 알아채는 것이 아니라
빌드가 실패해야 한다. unsafe 블록 하나를 넣어 실패하는 것까지 확인했다.

--report-instances 는 제네릭이 무엇이 됐는지 센다. interns.fe 는 20 인스턴스,
타입 4 개에 저장소 80 바이트, 메서드 16 개다.

245/245, 38/38.
2026-08-17 16:31:57 +09:00
coolguy 390345ef85 refactor: check.c 와 lower.c 를 사람이 머리에 담을 크기로 나눈다
check.c 3,937 줄, lower.c 1,913 줄이었다. 가장 큰 파일이 978 줄이 됐다.

           check.c     679   스코프·심볼·흐름·소유권 접착
           checkexp.c  739   포매팅 검사와 표현식
           checkstm.c  635   문장, 함수, 메서드
           checkgen.c  618   제네릭 실체화
           checkcal.c  978   유닛 경계 호출과 옵셔널/에러 유니온
           checkpro.c  184   선언 패스와 프로그램

           lower.c     567   타입·슬롯·지역·블록·mem.*
           lowerprn.c  238   포매팅 빌트인 전개
           lowerexp.c  468   표현식
           lowerstm.c  543   문장·함수·프로그램

줄 범위로 잘랐다. 주제별로 묶는 것보다 정확한데, 한 줄도 잃거나 겹치지
않기 때문이다. 파일 순서가 이미 단계를 따라가서 경계가 실제 이음매에 떨어진다.

모든 정의가 static 을 잃고 비공개 헤더에 프로토타입을 갖는다. 대안 --
static 을 유지하고 #include 로 텍스트만 나누는 것 -- 은 결합을 보여주는 대신
숨긴다.

두 스위트 그대로: 209/209, 21/21.
2026-08-17 07:04:35 +09:00
coolguy b0c9338cf3 tests: pending-backend 격리를 없앤다
코드 생성기가 없어서 돌릴 수 없던 것들이다. 이제 돌아간다. 경계 트랩,
--no-checks 차등, 슬라이스 범위 검사, 소유권 해제가 전부 exec/ 에서
실행으로 검증되므로 격리할 이유가 없다.

run.py 205/205, exec.py 17/17.
2026-08-17 06:32:57 +09:00
coolguy 0de10f37b5 std: io 와 fmt 를 Ferro 로 쓰고, 컴파일된 프로그램이 출력한다
io.Writer 는 핸들 하나짜리 enum 이다. 참조도 컨텍스트 포인터도 담지 않으므로
Copy 이고 자유롭게 오간다 (SPEC 5 R8). fmt 는 sink 를 소유하지 않는다 --
호출자가 버퍼를 주고 앞에서 몇 바이트가 쓰였는지 돌려받는다.

lowering 에 추가: enum 변이 상수, match, 정수 캐스트, 문자열 이스케이프.

프론트엔드 정밀도 하나: 항상 빠져나가는 분기의 상태를 병합하지 않는다. 그
분기가 소비한 값이 그 분기를 지나지 않은 경로에서도 소비된 것처럼 보였다.
fmt_i32 가 이것 때문에 못 쓰였다.

extern "c" 이름은 유닛 접두사를 붙이지 않는다. 링커가 이미 아는 이름이라는
것이 그 선언의 요점이다.

run.py 199/199, exec.py 11/11.
2026-08-17 06:18:16 +09:00
coolguy e5093e690d backend: i386 어셈블리를 내고 Windows 11 실행 파일을 만든다
fec --emit-asm -> wasm -> wlink -> .exe. 툴체인은 고정된 Open Watcom 그대로다.

레지스터 할당기가 없다. 임시값마다 스택 슬롯을 주고, 명령마다 피연산자를
고정 레지스터로 읽어 계산하고 다시 저장한다. 느린 코드지만 명백히 옳은
코드이고, 옳은 것이 먼저다. 나중에 할당기를 끼워도 나머지는 모른다 --
임시값이 어디 사는지만 바뀐다.

런타임 fec/rt/start.asm 은 진입 스텁과 fe_trap 이다. trap 은 이유와 파일과
줄을 stderr 에 쓰고 3으로 끝낸다.

처음으로 Ferro 프로그램이 실행됐다:

  1..10 합         -> 55
  (7*6-2)/4        -> 10
  루프+호출+분기   -> 1

tests/build.py 가 컴파일하고 링크하고 돌린다.
2026-08-17 05:55:34 +09:00
coolguy 6ee3764667 lower: 검사된 AST 를 IR 로 내린다 (1차)
함수, 파라미터, 지역, 리터럴, 이름, 산술과 비교, 대입, if, while,
break/continue, 호출, 참조, 필드 접근, 포인터 역참조까지.

Slot 이 표현식의 결과다 -- 임시값에 든 값이거나 메모리 안의 자리다. 덩어리는
언제나 자리다. 임시값은 레지스터이고 덩어리는 거기 안 들어가기 때문이다.

and/or 는 연산이 아니라 제어 흐름으로 내린다. 오른쪽을 평가하지 않아야 하는
경우가 있어서다.

블록에 terminated 플래그를 뒀다. 없으면 분기 안의 return 이 join 으로 가는
점프에 덮인다.

--dump-ir 로 볼 수 있다. try/catch/defer/drop/for/배열/옵셔널/에러유니온과
제네릭 인스턴스는 아직이다.
2026-08-17 05:51:01 +09:00
coolguy ddea962d14 ir: 중간 표현을 정의한다
명령 12개와 종결자 4개. 함수 단위 기본 블록이고 임시값은 블록을 넘지 않아
phi 노드가 없다 -- 블록을 넘겨야 하는 값은 지역을 경유한다. 코드가 조금 더
생기지만 레지스터 할당기를 블록 단위로 유지해 준다.

Ferro 타입은 여기서 사라진다. 구조체·슬라이스·옵셔널·에러 유니온이 전부
mem<N> 이고 필드는 lowering 이 계산한 바이트 오프셋이다. 모노모피제이션이
프론트엔드에서 끝나므로 IR 에 제네릭이라는 개념도 없다.

덩어리는 언제나 주소로 오간다. 크기 임계값이 없어서 ISA 마다 다른 구조체 전달
규칙을 통째로 피해간다. trap 은 이유와 줄 번호만 남기고 파일 이름 문자열은
유닛당 하나를 공유한다.

IR.md 가 설명이고 ir.h/ir.c 가 그 형태다. 아직 아무도 만들지 않는다.
2026-08-17 05:44:56 +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 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