diff --git a/HANDOFF.md b/HANDOFF.md deleted file mode 100644 index 8de2256..0000000 --- a/HANDOFF.md +++ /dev/null @@ -1,212 +0,0 @@ -# doslang 작업 인수인계 - -작성 시각: 2026-08-16 (Asia/Seoul) - -## 최종 목표와 사용자 지시 - -- `SPEC.md`의 최신 명세대로 M14까지 전부 구현한다. -- DOSBox는 사용하지 않는다. 실행과 권위 있는 검증은 **QEMU FreeDOS 내부**에서만 한다. -- 컴파일러 A와 생성 C 모두 VM 내부 Open Watcom으로 컴파일한다. - - 컴파일러 A / bits16: `WCL` - - bits32 생성 C: `WCL386` -- 호스트 Windows는 소스 편집, diff, Git, 직렬 전송에만 사용한다. -- WSL이나 호스트 C 컴파일러 결과를 정식 검증으로 인정하지 않는다. -- 코딩은 가능한 한 `gpt-5.6-luna` 서브에이전트에 맡기고, 루트 에이전트는 명세 감사와 QEMU 검증을 담당한다. -- VGA 데모처럼 복잡한 멀티모달 수동 검증은 완료 게이트에서 제외한다. -- 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. -- 구현 중 명세 판단이 바뀌면 `SPEC.md`를 즉시 최신화하고 `SPEC.AUDIT.md`에 변경 이유를 누적한다. - -## 현재 완료 상태 - -현재 기준 커밋과 원격 `master`는 둘 다 다음 SHA다. - -```text -53bca214b82c3abf7882ad41fb177cc488e999af -``` - -완료 및 QEMU/Open Watcom 검증된 범위: - -- M1: lexer, parser, AST dump -- M2: 기본 타입 검사와 C 방출, bits16/bits32 경로 -- M3: struct, enum, match, 배열, 슬라이스, str, 경계 검사, 반복 참조 -- M4: `@print`, `@fprint`, `@sprint`, 최소 `io.Writer` - -마지막 완료 커밋: - -```text -53bca21 feat: implement M4 formatting builtins -8e6a409 feat: implement M3 aggregate types and iteration -57b47a5 docs: disambiguate control-flow headers -95de333 docs: disambiguate match scrutinees -3aa7618 docs: clarify char and byte conversions -``` - -M4 최종 검증 증거: - -- VM의 `C:\FEC\BUILD.OK`: `OK` -- VM의 `C:\FEC\TEST.OK`: `OK` -- `C:\FEC\TEST-DOS.BAT`: exit 0 -- 오류 코드 7을 반환하는 C89 Writer harness가 `@fprint`의 정확한 오류 보존과 첫 오류 이후 단축 중단을 실행 검증한다. -- 로컬/원격 SHA 일치까지 확인하고 푸시했다. - -## 최신 명세 - -- `SPEC.md`: v0.1.5 -- `SPEC.AUDIT.md`: 최초 감사 이후 설계 변경 로그 -- 주요 후속 결정: - - `char`와 `u8`은 별개 타입이며 명시적 `as`만 허용한다. - - 제어 흐름 헤더 직후 `{`는 본문 시작이다. 헤더 최상위 구조체 초기화식은 괄호로 구분한다. - -## 현재 워크트리: 미완성 M5 - -M5 작업 도중 인수인계를 위해 Luna를 중단했다. **아래 변경은 미검증·미커밋 상태이므로 버리지 말고 먼저 감사할 것.** - -수정 파일: - -```text -fec/src/check.c -fec/src/emit_c.c -fec/src/emit_c.h -fec/src/types.c -fec/src/types.h -fec/test-dos.bat -fec/tests/run-tests.sh -fec/vm-m1.bat -``` - -새 파일: - -```text -fec/tests/m5/defer.fe -fec/tests/m5/owned.fe -fec/tests/m5/bad-move.fe -fec/tests/m5/bad-destroy.fe -``` - -현재 diff 규모는 약 251 insertions / 6 deletions이며 `git diff --check`는 통과했다. 아직 DOS로 전송하거나 빌드하지 않았다. - -현재 부분 구현에 들어간 것으로 확인된 것: - -- `FE_TYPE_OWNED` -- struct의 `has_drop` 메타데이터 일부 -- 단순 `moved` 비트 기반 이동 후 사용 진단 -- `mem.create` / `mem.destroy` 일부 checker/emitter 분기 -- owned drop 및 block cleanup helper -- return/break/continue 정리 경로를 위한 emitter 코드 일부 -- 정상 종료 defer 역순 방출 일부 - -그러나 M5 완료로 간주하면 안 된다. 첫 Luna 결과는 골격뿐이었고 다음 누락 때문에 반려했다. - -- 자동 drop/free가 모든 경로에서 정확히 한 번 실행되는지 -- `mem.create(T) -> !^T`의 실제 C ABI와 초기화 -- 재대입 전에 기존 owned 값 정리 -- defer와 drop의 선언 위치 기준 역순 병합 -- return / break / continue / try 전파에서 cleanup -- 조건부 이동의 `MaybeMoved` 상태와 런타임 live flag -- struct drop 메서드 및 필드 역순 drop -- 분기/루프 상태 합류 -- 누수와 이중 해제를 세는 런타임 harness -- R1/R3 실패 테스트 최소 수량 - -중단 직전 두 번째 Luna 패스가 cleanup 코드를 더 추가했으므로 위 항목 일부가 코드에 들어갔을 수 있다. 다음 세션은 반드시 `git diff`로 실제 구현을 재감사하고, 테스트가 증명하지 않는 기능은 완료 처리하지 말아야 한다. - -현재는 `own.c/h`가 없고 소유권 로직이 주로 `check.c`/`emit_c.c`에 들어가 있다. 복잡도가 계속 커지면 명세 §11.5대로 `own.c/h`와 cleanup/lower 계층을 분리하는 편이 안전하다. - -## 다음 세션 권장 순서 - -1. `git status --short`, `git diff --check`, M5 diff 전체를 읽는다. -2. Luna 읽기 전용 감사 에이전트로 M5와 R1~R5/§11.5를 대조한다. -3. 기존 Luna 구현 에이전트 또는 새 `gpt-5.6-luna`에게 발견된 누락을 수정시킨다. -4. 최소 다음 테스트를 갖춘다. - - owned 정상 scope 종료 cleanup - - 조기 return cleanup - - return 시 defer 실행 - - 중첩 defer/drop 역순 - - owned 재대입 시 기존 값 cleanup - - 함수 인자 이동 후 사용 실패 - - 조건부 이동 후 사용 실패 및 정확한 drop - - 직접 `.drop()` 호출 실패 - - `mem.destroy` 후 재사용/이중 destroy 실패 - - 할당/해제 카운터로 누수 0, double free 0 -5. 변경 파일과 M5 fixture를 직렬 프로토콜로 VM의 `C:\FEC`에 직접 전송한다. -6. `C:\FEC\TEST-DOS.BAT`를 실행한다. 실패하면 출력/생성 C/정확한 fixture를 좁혀 반복 수정한다. -7. `BUILD.OK`, `TEST.OK`, exit 0을 모두 확인한 뒤에만 M5 커밋 및 `git push origin master`. -8. 이후 M6~M14도 같은 방식으로 진행한다. - -## QEMU와 직렬 에이전트 상태 - -인수인계 작성 시 QEMU는 재시작 없이 살아 있다. - -```text -QEMU PID: 11700 -monitor: 127.0.0.1:4444 -DOS tool/controller: 127.0.0.1:5555 -QEMU serial relay: 127.0.0.1:5556 -observer: 127.0.0.1:5557 -``` - -5555에 ASCII 한 줄 명령을 보내는 프로토콜: - -```text -PING -READ -WRITE T|A -EXEC -``` - -- `PING` 정상 응답: `OK 504F4E47` -- `WRITE`는 안전하게 1024바이트 조각으로 보내면 된다. 첫 조각 `T`, 이후 `A`. -- authoritative workspace는 VM의 `C:\FEC`이다. -- QEMU의 vvfat `D:`는 교환용으로만 취급하고 빌드하지 않는다. 과거 D:에서 빌드하다 QEMU가 rename 처리 오류로 종료된 적이 있다. -- DOS의 8.3 파일명 때문에 긴 fixture는 `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 짧은 이름으로 전송해야 한다. - -QEMU monitor helper: - -```powershell -.\.qemu\monitor.ps1 'sendkey ctrl-c' -.\.qemu\screenshot.ps1 -``` - -긴 DOS 배치가 멈추면 QEMU를 재시작하지 말고 먼저 `sendkey ctrl-c`를 사용한다. FreeDOS가 다음 프롬프트를 보이면 monitor로 `y`, `ret`을 보낸다. - -```text -Terminate batch file ... (Yes/No/All)? -``` - -그 뒤 `PING` 복구를 확인한다. - -## 빌드 관련 함정 - -- 컴파일러 A는 16비트 large model로 빌드한다. small model은 메모리 부족이 났다. -- DOS 명령줄 길이 제한 때문에 compiler object link는 `*.obj`를 사용한다. -- 따라서 `fec/build-dos.bat`가 먼저 `C:\FEC\*.obj`를 지워 stale 32-bit test object가 섞이지 않게 한다. -- 생성 C의 보수적 미사용 helper 때문에 M4 Watcom 테스트는 `-wx -wcd=202`를 사용한다. W202만 끄고 다른 경고는 오류로 유지한다. -- 호스트에서 컴파일하지 말 것. 정적 text/diff 검사만 허용한다. -- `.qemu/qemu-screen.png`는 진단용이며 커밋하지 않는다. - -## Git과 저장소 - -```text -origin: https://github.com/sebastianrcnt/doslang.git -branch: master -repository visibility: public -``` - -사용자는 모든 검증 커밋을 항상 원격에 푸시하길 원한다. - -이 인수인계 문서는 별도 문서 커밋으로 푸시하되, 현재 미완성 M5 변경은 그 커밋에 포함하지 않는다. - -## 전체 남은 목표 - -- M5: owned/drop/defer/move — 현재 미완성 -- M6: `&`/`&mut`, R1~R8 전체 borrow checker -- M7: `?T`, `E!T`, try/catch -- M8: unit/import/.fei/분리 컴파일/std 초안 -- M9: 제네릭 및 Ferro `List`/`Map` -- M10: bits16 far/asm/interrupt/shared/atomic/critical (VGA 수동 데모 제외) -- M11: Compiler B를 Ferro로 작성하고 A로 빌드 -- M12: self-host fixpoint `B(B(B)) == B(B)` 후 A 폐기 -- M13: 386 네이티브 백엔드 -- M14: 8086 네이티브 백엔드, Watcom 없이 bits16 빌드 - -M14 전체 요구를 증명하기 전에는 goal을 complete로 표시하지 않는다. diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 038ee87..95a2cdf 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -38,3 +38,119 @@ - 구현 영향: 제어 흐름 헤더의 최상위 식 파싱에서 구조체 초기화를 금지하되 괄호 안에서는 일반 식 파싱 상태를 복원한다. 단순 식별자 조건과 배열·슬라이스 반복은 본문 `{` 앞에서 정상적으로 종료되어야 한다. + +## 2026-08-16 — v0.1.6 + +M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 결정과, M5까지 누적된 +문법·일관성 결함 정리를 함께 반영했다. + +### R8 — 파생 반환 규칙으로 전면 교체 + +- 문제: R4가 `[]T`를 함수 반환 타입에서 금지하고 기존 R8의 예외는 `&T`/`&mut T`만 + 다뤘다. 그 결과 §10이 요구하는 `str.trim`, `str.split_at`, `str.find`를 표현할 + 방법이 없었다. 기존 R8의 "결과를 지역 변수에 바인딩 불가" 제약도 `trim` 계열을 + 무의미하게 만들었다. +- 결정: R8을 파생 반환 규칙으로 교체한다. (a) 참조성 파라미터가 정확히 하나이고 + 반환값이 그것에서 파생될 때, (b) 문자열 리터럴이나 `static`에서 파생될 때 + `&T`/`&mut T`/`[]T`/`str`을 반환할 수 있다. 호출 지점에서 (a)의 결과는 그 인자를 + 대여한 것으로 취급하며, 바인딩 금지 제약은 삭제한다. +- 근거: 표기 없는 lifetime elision이며 정의와 호출 양쪽 모두 함수 하나만 보고 + 검증되므로 §1.2를 깨지 않는다. 바인딩 금지는 대여 추적을 피하려던 제약인데, + own.c가 R6를 위해 같은 상태 기계를 이미 돌리므로 추가 비용이 거의 없다. +- 구현 영향: own.c는 호출 결과에 "인자로부터의 대여" 상태를 전파해야 한다. + check.c는 시그니처만 보고 참조성 파라미터 개수와 가변성 관계를 검증한다. + 참조성 파라미터가 둘 이상이면 참조성 반환을 거부한다. + +### R6 — 대여 구간을 마지막 사용 지점까지로 축소 + +- 문제: 대여가 참조 변수의 스코프 끝까지 유지되고 블록 표현식도 없어서 + `let r = &mut x; r.^ = 1; x += 1;`이 에러였다. 회피 수단은 명시적 `{ }`뿐이며, + M11에서 컴파일러 B를 이 언어로 작성할 때 마찰이 누적된다. +- 결정: 대여 구간을 참조 변수의 마지막 사용 지점까지로 한다. 조건부 흐름에서는 + 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조는 문장 끝까지로 + 유지한다. +- 근거: 함수 지역 liveness 분석이므로 §1.2를 위반하지 않는다. +- 시점: M6 착수 전에 결정해야 한다. 나중에 좁히면 진단 메시지와 `fail/` 기대값을 + 전부 다시 써야 한다. +- 구현 영향: own.c에 역방향 liveness 스캔 한 번을 추가한다. + +### R10 — 전역에 대한 대여 금지 + +- 문제: R5가 참조 대상으로 전역을 허용하므로 `fn f(r: &mut i32) { G = 5; }`를 + `f(&mut G)`로 호출하면 R6의 배타성이 호출 경계에서 깨진다. 호출자는 `&mut G`가 + 배타적이라고 보고, 피호출자는 자기 파라미터가 `G`를 가리키는지 알 수 없다. + 함수 단위 지역 검사로는 원리적으로 검출 불가능하다. +- 결정: `static`(불변)만 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 + 대여 불가이며 직접 읽기/쓰기만 허용한다. `shared var`는 `critical` 안의 직접 + 접근만 허용한다. 전역을 참조로 넘겨야 하면 지역 변수로 복사한다. +- 구현 영향: R5의 대여 대상에서 가변 전역을 제외한다. §11.4에 방출 단계가 + aliasing을 가정하지 않는다는 규정을 추가했다. 이 규칙이 없으면 `&mut T`에 + `restrict`를 붙이거나 M13/M14 네이티브 백엔드에서 noalias를 가정하는 순간 + 불건전해진다. + +### `error.Name` 코드 부여 시점 + +- 문제: 코드를 "최종 링크용 생성 헤더의 심볼"로 참조하도록 규정했는데, 그러면 C에서 + 상수식이 아니므로 §11.4의 `switch` 방출을 쓸 수 없고 에러 `match`가 if-else + 체인으로 떨어진다. +- 검토 후 기각한 대안: 이름 문자열의 u16 해시. 유닛별 독립 계산과 캐시 유지라는 + 장점이 있으나 생일 문제로 이름 약 300개에서 충돌 확률이 50%에 달해 컴파일러 B의 + 에러 이름 규모를 감당하지 못한다. +- 결정: 정렬 기반 번호 부여는 유지하되, 부여 시점을 링크가 아니라 **드라이버의 emit + 이전 단계**로 옮긴다. 코드는 방출 C에서 컴파일타임 정수 상수가 된다. +- 대가: 이름 집합이 바뀌면 방출 `.c`와 오브젝트 캐시가 전부 무효화된다. `.fei`는 + 이름만 기록하므로 무효화되지 않는다. 유닛 단위 `--emit-c`는 `--error-table`로 + 확정 표를 받아야 한다. +- 구현 영향: driver.c가 전체 `.fei`에서 이름을 수집해 코드를 확정한 뒤 emit을 + 시작한다. + +### `str`을 `[]u8`과 별개 타입으로 + +- 문제: §4.2는 `str`을 "`[]u8` 불변 별칭"이라 하고 §4.7은 별칭을 완전 동일 취급이라 + 규정했다. 완전 동일이면 `str`을 통해 쓸 수 있는데, 문자열 리터럴은 읽기 전용 + 저장 영역에 놓이므로 안전성 구멍이다. +- 결정: `str`을 별개의 내장 타입으로 한다. `[]u8` → `str`은 `as str`로 변환 가능 + (가변성 약화이므로 안전), 역방향은 금지. `str` 원소 쓰기는 컴파일 에러. +- 구현 영향: §11.4에 `fe_str`(`const uint8_t*`) 방출 행을 추가했다. types.c는 + `str`을 `[]u8`과 다른 인터닝 엔트리로 다뤄야 한다. + +### `catch` 블록은 값을 만들지 않는다 + +- 문제: §4.6은 catch 블록이 값을 만들 수 있다고 했으나 문법에 블록 표현식이 없다. +- 결정: 블록 표현식을 도입하는 대신 catch 블록에서 값 생성을 금지한다. 블록은 + `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝난다. 값이 필요하면 + 짧은 형태 `expr catch <식>`을 쓴다. +- 근거: §1.5. 블록 표현식은 §13에 `편의` 등급으로 등재했다. + +### 문법 결함 정리 (§6.1) + +- `struct_decl`이 `field* fn_decl*`이라 §4.3 예제의 `pub fn new`가 문법 위반이었고 + §8이 요구하는 필드별 `pub`도 표현 불가였다. `member := ['pub'] (field | fn_decl)` + 으로 교체하면서 필드와 메서드의 순서 강제도 함께 풀었다. +- enum 배리언트 필드가 struct `field`를 재사용해 `pub`을 받을 수 있었다. `vfield`로 + 분리했다. +- `catch`/`orelse`는 §6.2 우선순위 표에 이름만 있고 프로덕션이 없었다. 추가했다. +- `error_decl`이 빈 에러 집합을 허용하고 마지막 쉼표를 강제했다. 최소 1개 + 선택적 + 후행 쉼표로 수정했다. +- §6.2에 `expr` 프로덕션이 없어 `try`, `as`, `@builtin`, 구조체 초기화가 EBNF + 어디에도 나오지 않았다. 우선순위 표가 표현식 문법의 규범임을 명시했다. +- `Some`/`None`이 패턴에 하드코딩돼 있으나 §3 예약어가 아니었다. 패턴 위치 전용 + 문맥 키워드임을 §4.5에 명시했다. + +### 일관성과 문서 정합 + +- §4.4의 `u16` 태그 승격 규정이 §11.4 방출 표의 `uint8_t` 고정과 어긋났다. 표를 + 수정했다. +- CLI 플래그가 §2, §7.4, §8, R3에 흩어져 있었다. §8.1로 통합했다. +- §11.3의 목표 디렉터리에 `own.c`/`lower.c`/`resolve.c`/`generic.c`/`rt/`가 있으나 + 실제로는 없다. 목표 구조는 유지하고, M5까지 `check.c`/`emit_c.c` 통합 상태이며 + M6 착수 시 `own.c/h`를 분리한다는 단서를 달았다. +- §12의 테스트 구조가 실제 `tests/m/`와 달랐다. 두 축을 모두 인정하도록 했다. + R8 변경에 따라 `fail/` 목록의 "참조 반환 바인딩" 항목을 교체하고 R10 전역 대여, + `str` 변환 케이스를 추가했으며 R6·R8 `pass/` 케이스를 신설했다. + +### 삭제 + +- `HANDOFF.md`를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model, + 링크는 `*.obj`, M4 Watcom 테스트는 `-wx -wcd=202`, 8.3 파일명 제약, `D:`에서 빌드 + 금지)은 별도 문서로 옮겨야 한다. diff --git a/SPEC.md b/SPEC.md index 1608898..b153468 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.5 +# Ferro 언어 명세 v0.1.6 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -81,7 +81,7 @@ and or not orelse |---|---|---| | `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | | `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` | -| `str` | `[]u8` 불변 별칭 | `(ptr, len)` | +| `str` | 불변 바이트 슬라이스. `[]u8`과 **별개 타입** | `(ptr, len)` | | `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 | | `&T` | 공유 참조 | 포인터 | | `&mut T` | 배타 참조 | 포인터 | @@ -94,6 +94,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. - 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환. +- `str`은 별칭이 아니라 별개의 내장 타입이다. `[]u8`에서 `str`로는 `as str`로 변환할 수 있다(가변성 약화이므로 안전). `str`에서 `[]u8`로의 변환은 불변성을 깨므로 금지한다. `str`의 원소는 읽기만 가능하며 `s[i] = v`는 컴파일 에러다. 문자열 리터럴은 읽기 전용 저장 영역에 놓이므로 이 구분이 필요하다. ### 4.3 구조체 @@ -141,6 +142,7 @@ let v = p orelse default_node; // null이면 우변 - `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음). - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. +- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. ### 4.6 에러 @@ -162,18 +164,23 @@ fn read_all(path: str) -> IoError!^[]u8 { - `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. - `E!T` 함수만 `try`/`catch` 사용 가능. - `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함). -- `e catch |x| { ... }`: 블록은 값을 만들거나 `return`/`break`로 탈출. -- `e catch default_value`: 짧은 형태. +- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) +- `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. - 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. -최종 빌드에서 모든 유닛의 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 -정렬하여 1부터 안정적인 `u16` 코드를 부여한다. 따라서 서로 다른 유닛의 -`error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이다. -코드는 최종 링크용 생성 헤더의 심볼로 참조하므로 분리 컴파일에서도 일관된다. +최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을 +합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다. +따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 +결과가 결정적이며, 방출되는 C에서 **컴파일타임 정수 상수**이므로 `switch` case +라벨로 쓸 수 있다. +이름 집합이 바뀌면 코드가 재배치되므로 방출된 `.c`와 오브젝트 캐시는 전부 +무효화된다. `.fei` 자체는 코드가 아니라 이름만 기록하므로 무효화되지 않는다. +유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 +확정된 표를 받아야 하며, 없으면 컴파일 에러다. 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal 타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 @@ -185,6 +192,7 @@ fn read_all(path: str) -> IoError!^[]u8 { ### 4.7 타입 동등성 이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. 별칭은 `const Alias = Type;`으로 만들며 완전 동일 취급. +`str`은 이 의미의 별칭이 아니라 `[]u8`과 구별되는 별개의 내장 타입이다(§4.2). --- @@ -212,19 +220,43 @@ fn read_all(path: str) -> IoError!^[]u8 { 이 한 줄이 라이프타임 표기 전체를 불필요하게 만든다. -**R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 전역이므로 스코프 중첩 확인만으로 검사된다. +**R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 `static` 전역이므로 스코프 중첩 확인만으로 검사된다. 가변 전역에 대한 대여는 R10이 금지한다. -**R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. 참조의 생존 구간은 **참조 변수의 스코프 끝까지**(NLL 아님). 임시 참조(`f(&x)`)는 그 문장 끝까지. +**R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. + +참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. + +이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. + +```fe +var x: i32 = 0; +let r = &mut x; +r.^ = 1; // r의 마지막 사용 +x += 1; // OK — 여기서 r의 대여는 이미 끝났다 +``` **R7 (참조 무효화).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. -**R8 (반환값 예외).** 메서드는 `-> &T` / `-> &mut T`를 반환할 수 있다. 단, 첫 파라미터가 `&Self`/`&mut Self`이고 반환 참조는 그 self에서 파생된 것이어야 하며(컴파일러가 확인), **호출 결과는 그 문장 안에서만 사용 가능**하고 지역 변수에 바인딩할 수 없다. +**R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `str`을 반환할 수 있다. + +**(a) 파라미터 파생.** 파라미터 중 참조성 타입(`&T`, `&mut T`, `[]T`, `str`)이 **정확히 하나**이고, 반환값이 그 파라미터에서 파생된 것임을 컴파일러가 함수 본문만 보고 확인할 수 있을 때. 파생이란 슬라이싱, 인덱싱, 필드 접근, `&`/`&mut` 취함, 그리고 다른 R8(a) 함수 호출의 연쇄를 말한다. 반환의 가변성은 원본 이하여야 한다(`&T`에서 `&mut T`를 만들 수 없다). 메서드의 `self`도 이 "하나"에 해당한다. + +**(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. + +호출 지점에서 R8(a)의 결과는 **그 인자를 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 인자 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. ```fe -list.at(0).x = 5; // OK -let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 +pub fn trim(s: str) -> str { ... } // R8(a) +let t = str.trim(line); // OK. line은 t의 대여 구간 동안 잠긴다 + +list.at(0).x = 5; // OK +let r = list.at(0); // OK. list를 &mut로 대여 +list.push(1); // 에러: r이 list를 대여 중 (R6) + +pub fn name() -> str { return "main"; } // R8(b) ``` -장수하는 접근이 필요하면 인덱스(`usize`)나 핸들을 쓴다. + +참조성 파라미터가 둘 이상인 함수는 어느 쪽에서 파생되었는지가 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 인덱스(`usize`)나 핸들을 반환한다. **R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. @@ -233,6 +265,15 @@ let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 - `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다. - `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다. - `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점, `critical` 및 안전하지 않은 함수 호출을 사용할 수 없으며 컴파일러가 함수 본문만 보고 검증한다. 이 효과는 `.fei` 시그니처에 기록한다. +- 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. `shared var`는 `critical` 안에서의 직접 접근만 허용하고 대여할 수 없다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. +- 이 제한의 근거는 R6다. 전역에 대한 대여가 살아 있는 동안 호출된 다른 함수가 같은 전역에 직접 접근할 수 있고, 그것은 함수 단위 지역 검사로 검출할 수 없다. 아래는 이 제한이 없으면 통과해 버리는 예다. + +```fe +var G: i32 = 0; +fn f(r: &mut i32) { G = 5; } // r과 G가 같은 곳을 가리키는지 f는 알 수 없다 +fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통과한다 +``` + - 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. **R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. @@ -253,11 +294,13 @@ decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type -struct_decl := ['packed'] 'struct' ident '{' field* fn_decl* '}' +struct_decl := ['packed'] 'struct' ident '{' member* '}' +member := ['pub'] (field | fn_decl) field := ident ':' type ',' enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' -variant := ident | ident '(' type ')' | ident '{' field* '}' -error_decl := 'error' ident '{' (ident '=' int ',')* '}' +variant := ident | ident '(' type ')' | ident '{' vfield* '}' +vfield := ident ':' type ',' +error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' const_decl := 'const' ident [':' type] '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';' @@ -298,10 +341,20 @@ type := ident ['.' ident] | '[' expr ']' type | '[' ']' type | 'fn' '(' [type (',' type)*] ')' ['->' type] | ident '(' type (',' type)* ')' // 제네릭 인스턴스 + +catch_expr := expr 'catch' ['|' ident '|'] (expr | block) +orelse_expr := expr 'orelse' expr ``` +`member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 +섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. + ### 6.2 표현식 우선순위 (낮음 → 높음) +표현식 문법은 다음 우선순위 표가 규범이다. 각 단계는 명시가 없으면 좌결합 이항 +연산으로 전개하며, 단항·후위·기본 단계에 나열된 형태가 그대로 프로덕션이 된다. +`catch`와 `orelse`의 구체적 형태는 §6.1을 따른다. + ``` 1 orelse, catch 2 or @@ -499,6 +552,21 @@ fec main.fe --emit-c -o out/ # 트랜스파일 결과만 fec --dump-ast main.fe ``` +### 8.1 CLI 플래그 (전체) + +| 플래그 | 의미 | 규정 | +|---|---|---| +| `--target=bits16\|bits32` | 타깃 선택 | §2 | +| `--model=small\|large` | `bits16` 메모리 모델 | §2 | +| `-o <경로>` | 출력 파일 또는 디렉터리 | §8 | +| `-I <디렉터리>` | 유닛 검색 경로 추가 | §8 | +| `--emit-c` | 트랜스파일 결과만 생성 | §8 | +| `--dump-ast` | AST 덤프 | §8 | +| `--no-checks` | 경계·오버플로·`.?` 검사 제거 | §7.4 | +| `--strip-error-names` | 실행 파일에서 에러 이름 문자열 제거 | §4.6 | +| `--error-table=<파일>` | 유닛 단위 `--emit-c`용 확정 에러 코드 표 | §4.6 | +| `--deny-recursive-drop` | 재귀 drop 경고를 에러로 승격 | §5 R3 | + --- ## 9. 제네릭 @@ -602,12 +670,16 @@ fec/ lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. emit_c.c/h LIR → C. §11.4 규칙. generic.c/h 인스턴스 캐시, 토큰 재파싱. - driver.c CLI, 유닛 의존 순서, .fei 캐시, 외부 C 컴파일러 호출. + driver.c CLI, 유닛 의존 순서, .fei 캐시, 에러 이름 표 수집과 코드 부여(§4.6), + 외부 C 컴파일러 호출. rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim std/ 표준 라이브러리 (.fe) tests/ §12 ``` +위는 목표 구조다. M5까지는 이름 해석·소유권·lower가 `check.c`/`emit_c.c`에 통합되어 +있다. R1~R8 전체를 다루는 M6 착수 시점에 `own.c/h`를 분리한다. + ### 11.4 C 방출 규칙 | Ferro | C | @@ -621,11 +693,12 @@ fec/ | `far X` | `__far X` (Watcom/Borland), bits32는 무시 | | `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | | `[]T` | `typedef struct { T* p; fe_usize n; } fe_slice_T;` | +| `str` | `typedef struct { const uint8_t* p; fe_usize n; } fe_str;` | | `?T` (포인터류) | 원래 포인터, null 사용 | | `?T` (그 외) | `struct { unsigned char has; T v; }` | | `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | | struct | `struct fe__` | -| enum | `struct { uint8_t tag; union { ... } u; }` | +| enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` | | 함수 | `fe__`, 메서드는 `fe___` | | 제네릭 인스턴스 | `fe____<타입인자맹글>` | @@ -639,6 +712,8 @@ fec/ - **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. - **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. `@print`는 각 호출의 오류 코드를 명시적으로 버리고 `void`가 되며, `@fprint`는 첫 오류를 전파하고, `@sprint`는 남은 버퍼 길이를 추적해 잘라 쓴 뒤 실제 길이를 반환한다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다. +- **에러 코드**: `error.Name`의 `u16` 코드는 드라이버가 emit 전에 확정한 정수 리터럴로 방출한다(§4.6). 따라서 에러 값에 대한 `match`도 `switch`로 방출할 수 있다. - **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. - **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다. - **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. @@ -693,15 +768,17 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive ``` tests/ + m/ 마일스톤별 fixture. bad-*.fe는 fail 규약을 따른다 pass/*.fe + *.expected 컴파일→실행→stdout 비교 fail/*.fe 첫 줄 "// ERROR::<메시지 일부>" run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교 boot/ A/B 출력 비교, fixpoint 검증 ``` -- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조 반환 바인딩), R9(unsafe 밖 raw 역참조), match 완전성, 암묵 변환, 타입 불일치. +- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조성 파라미터 2개 이상에서 참조성 반환, 가변성 승격, 파생되지 않은 반환), R9(unsafe 밖 raw 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `str`을 `[]u8`로 변환, `str` 원소 쓰기. - 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`, `try @print(...)`(void). - 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print`의 오류 삼킴, `@sprint` 반환 길이/잘림 검증, `@fprint`를 `io.buf_writer`로 호출. +- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `str.trim` 형태의 슬라이스 반환 연쇄. - `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 `core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. @@ -727,7 +804,8 @@ tests/ | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | | `inline fn` | 편의 | — | C 방출 시 `static inline` | | `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | -| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), 인덱스 핸들 | +| 블록 표현식 | 편의 | 값을 만드는 블록이 없으면 `catch`가 짧은 형태로 충분하고, 문법 표면이 작아진다 (§4.6) | `catch <식>`, `return`으로 탈출 | +| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), R8 파생 반환, 인덱스 핸들 | | 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — | | 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | | 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 |