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
This commit is contained in:
2026-08-16 16:54:34 +09:00
co-authored by Claude Opus 5
parent dc2285b4d4
commit 41de8aa2dc
3 changed files with 215 additions and 233 deletions
-212
View File
@@ -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 <DOS_PATH_HEX> <OFFSET>
WRITE <DOS_PATH_HEX> T|A <CONTENT_HEX>
EXEC <COMMAND_HEX>
```
- `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로 표시하지 않는다.
+116
View File
@@ -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<N>/`와 달랐다. 두 축을 모두 인정하도록 했다.
R8 변경에 따라 `fail/` 목록의 "참조 반환 바인딩" 항목을 교체하고 R10 전역 대여,
`str` 변환 케이스를 추가했으며 R6·R8 `pass/` 케이스를 신설했다.
### 삭제
- `HANDOFF.md`를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model,
링크는 `*.obj`, M4 Watcom 테스트는 `-wx -wcd=202`, 8.3 파일명 제약, `D:`에서 빌드
금지)은 별도 문서로 옮겨야 한다.
+99 -21
View File
@@ -1,4 +1,4 @@
# Ferro 언어 명세 v0.1.5 # Ferro 언어 명세 v0.1.6
DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다.
파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`.
@@ -81,7 +81,7 @@ and or not orelse
|---|---|---| |---|---|---|
| `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | | `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` |
| `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` | | `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` |
| `str` | `[]u8` 불변 별칭 | `(ptr, len)` | | `str` | 불변 바이트 슬라이스. `[]u8`**별개 타입** | `(ptr, len)` |
| `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 | | `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 |
| `&T` | 공유 참조 | 포인터 | | `&T` | 공유 참조 | 포인터 |
| `&mut T` | 배타 참조 | 포인터 | | `&mut T` | 배타 참조 | 포인터 |
@@ -94,6 +94,7 @@ and or not orelse
- **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다.
- 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`.
- 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환. - 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환.
- `str`은 별칭이 아니라 별개의 내장 타입이다. `[]u8`에서 `str`로는 `as str`로 변환할 수 있다(가변성 약화이므로 안전). `str`에서 `[]u8`로의 변환은 불변성을 깨므로 금지한다. `str`의 원소는 읽기만 가능하며 `s[i] = v`는 컴파일 에러다. 문자열 리터럴은 읽기 전용 저장 영역에 놓이므로 이 구분이 필요하다.
### 4.3 구조체 ### 4.3 구조체
@@ -141,6 +142,7 @@ let v = p orelse default_node; // null이면 우변
- `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음). - `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음).
- 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요.
- `Some``None``if let``match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다.
### 4.6 에러 ### 4.6 에러
@@ -162,18 +164,23 @@ fn read_all(path: str) -> IoError!^[]u8 {
- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. - `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가.
- `E!T` 함수만 `try`/`catch` 사용 가능. - `E!T` 함수만 `try`/`catch` 사용 가능.
- `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함). - `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함).
- `e catch |x| { ... }`: 블록은 값을 만들거나 `return`/`break`로 탈출. - `e catch |x| { ... }`: 블록은 값을 만들 수 없다. `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.)
- `e catch default_value`: 짧은 형태. - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다.
- 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑.
- 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음.
- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. - 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다.
`error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는
멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다.
최종 빌드에서 모든 유닛의 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을
정렬하여 1부터 안정적인 `u16` 코드를 부여한다. 따라서 서로 다른 유닛의 합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다.
`error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이다. 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도
코드는 최종 링크용 생성 헤더의 심볼로 참조하므로 분리 컴파일에서도 일관된다. 결과가 결정적이며, 방출되는 C에서 **컴파일타임 정수 상수**이므로 `switch` case
라벨로 쓸 수 있다.
이름 집합이 바뀌면 코드가 재배치되므로 방출된 `.c`와 오브젝트 캐시는 전부
무효화된다. `.fei` 자체는 코드가 아니라 이름만 기록하므로 무효화되지 않는다.
유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`
확정된 표를 받아야 하며, 없으면 컴파일 에러다.
이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal
타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동
변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는
@@ -185,6 +192,7 @@ fn read_all(path: str) -> IoError!^[]u8 {
### 4.7 타입 동등성 ### 4.7 타입 동등성
이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. 별칭은 `const Alias = Type;`으로 만들며 완전 동일 취급. 이름 기반(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 (참조 무효화).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. **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 ```fe
list.at(0).x = 5; // OK pub fn trim(s: str) -> str { ... } // R8(a)
let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 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 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. **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` 밖의 비원자 공유 접근이 금지된다. - `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다.
- `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다. - `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다.
- `interrupt fn``interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점, `critical` 및 안전하지 않은 함수 호출을 사용할 수 없으며 컴파일러가 함수 본문만 보고 검증한다. 이 효과는 `.fei` 시그니처에 기록한다. - `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`에서 사용하면 컴파일 에러다. - 전역에는 `^T``drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다.
**R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. **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 fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident
'(' [param (',' param)*] ')' ['->' type] (block | ';') '(' [param (',' param)*] ')' ['->' type] (block | ';')
param := ['comptime'] ident ':' type 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 ',' field := ident ':' type ','
enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}'
variant := ident | ident '(' type ')' | ident '{' field* '}' variant := ident | ident '(' type ')' | ident '{' vfield* '}'
error_decl := 'error' ident '{' (ident '=' int ',')* '}' vfield := ident ':' type ','
error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}'
const_decl := 'const' ident [':' type] '=' expr ';' const_decl := 'const' ident [':' type] '=' expr ';'
global_decl := 'static' ident ':' type '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';'
| 'var' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';'
@@ -298,10 +341,20 @@ type := ident ['.' ident]
| '[' expr ']' type | '[' ']' type | '[' expr ']' type | '[' ']' type
| 'fn' '(' [type (',' type)*] ')' ['->' type] | 'fn' '(' [type (',' type)*] ')' ['->' type]
| ident '(' 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 표현식 우선순위 (낮음 → 높음) ### 6.2 표현식 우선순위 (낮음 → 높음)
표현식 문법은 다음 우선순위 표가 규범이다. 각 단계는 명시가 없으면 좌결합 이항
연산으로 전개하며, 단항·후위·기본 단계에 나열된 형태가 그대로 프로덕션이 된다.
`catch``orelse`의 구체적 형태는 §6.1을 따른다.
``` ```
1 orelse, catch 1 orelse, catch
2 or 2 or
@@ -499,6 +552,21 @@ fec main.fe --emit-c -o out/ # 트랜스파일 결과만
fec --dump-ast main.fe 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. 제네릭 ## 9. 제네릭
@@ -602,12 +670,16 @@ fec/
lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개.
emit_c.c/h LIR → C. §11.4 규칙. emit_c.c/h LIR → C. §11.4 규칙.
generic.c/h 인스턴스 캐시, 토큰 재파싱. generic.c/h 인스턴스 캐시, 토큰 재파싱.
driver.c CLI, 유닛 의존 순서, .fei 캐시, 외부 C 컴파일러 호출. driver.c CLI, 유닛 의존 순서, .fei 캐시, 에러 이름 표 수집과 코드 부여(§4.6),
외부 C 컴파일러 호출.
rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim
std/ 표준 라이브러리 (.fe) std/ 표준 라이브러리 (.fe)
tests/ §12 tests/ §12
``` ```
위는 목표 구조다. M5까지는 이름 해석·소유권·lower가 `check.c`/`emit_c.c`에 통합되어
있다. R1~R8 전체를 다루는 M6 착수 시점에 `own.c/h`를 분리한다.
### 11.4 C 방출 규칙 ### 11.4 C 방출 규칙
| Ferro | C | | Ferro | C |
@@ -621,11 +693,12 @@ fec/
| `far X` | `__far X` (Watcom/Borland), bits32는 무시 | | `far X` | `__far X` (Watcom/Borland), bits32는 무시 |
| `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | | `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) |
| `[]T` | `typedef struct { T* p; fe_usize n; } fe_slice_T;` | | `[]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` (포인터류) | 원래 포인터, null 사용 |
| `?T` (그 외) | `struct { unsigned char has; T v; }` | | `?T` (그 외) | `struct { unsigned char has; T v; }` |
| `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | | `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` |
| struct | `struct fe_<unit>_<Name>` | | struct | `struct fe_<unit>_<Name>` |
| enum | `struct { uint8_t tag; union { ... } u; }` | | enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` |
| 함수 | `fe_<unit>_<name>`, 메서드는 `fe_<unit>_<Type>_<name>` | | 함수 | `fe_<unit>_<name>`, 메서드는 `fe_<unit>_<Type>_<name>` |
| 제네릭 인스턴스 | `fe_<unit>_<Name>__<타입인자맹글>` | | 제네릭 인스턴스 | `fe_<unit>_<Name>__<타입인자맹글>` |
@@ -639,6 +712,8 @@ fec/
- **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. - **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터.
- **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다.
- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. `@print`는 각 호출의 오류 코드를 명시적으로 버리고 `void`가 되며, `@fprint`는 첫 오류를 전파하고, `@sprint`는 남은 버퍼 길이를 추적해 잘라 쓴 뒤 실제 길이를 반환한다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. - **`@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의 시퀀스 포인트와 단축 평가를 그대로 사용한다. - **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다.
- **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다. - **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다.
- **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. - **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문.
@@ -693,15 +768,17 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive
``` ```
tests/ tests/
m<N>/ 마일스톤별 fixture. bad-*.fe는 fail 규약을 따른다
pass/*.fe + *.expected 컴파일→실행→stdout 비교 pass/*.fe + *.expected 컴파일→실행→stdout 비교
fail/*.fe 첫 줄 "// ERROR:<line>:<메시지 일부>" fail/*.fe 첫 줄 "// ERROR:<line>:<메시지 일부>"
run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교 run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교
boot/ A/B 출력 비교, fixpoint 검증 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). - 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`, `try @print(...)`(void).
- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print`의 오류 삼킴, `@sprint` 반환 길이/잘림 검증, `@fprint`를 `io.buf_writer`로 호출. - 포매팅 `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`의 - `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의
`core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다. `core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다.
- 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다.
@@ -727,7 +804,8 @@ tests/
| 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 |
| `inline fn` | 편의 | — | C 방출 시 `static inline` | | `inline fn` | 편의 | — | C 방출 시 `static inline` |
| `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | | `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` |
| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), 인덱스 핸들 | | 블록 표현식 | 편의 | 값을 만드는 블록이 없으면 `catch`가 짧은 형태로 충분하고, 문법 표면이 작아진다 (§4.6) | `catch <식>`, `return`으로 탈출 |
| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), R8 파생 반환, 인덱스 핸들 |
| 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — | | 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — |
| 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | | 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` |
| 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 | | 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 |