diff --git a/AGENTS.md b/AGENTS.md index ad78332..9f7b7af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,7 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | 파일 | 역할 | |---|---| -| `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | -| `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | +| `SPEC.md` | 언어 명세. 유일한 규범 문서. 구현 지시서와 표준 라이브러리 명세는 별도 문서 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | 개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md deleted file mode 100644 index 2cf1441..0000000 --- a/SPEC.AUDIT.md +++ /dev/null @@ -1,575 +0,0 @@ -# Ferro specification audit log - -`SPEC.md`를 항상 최신 규범 문서로 유지하고, 최초 `AUDIT.md` 반영 이후 설계 판단으로 -바뀐 사항은 이 파일에 누적한다. - -## 2026-08-16 — v0.1.3 - -### `char`와 `u8` 사이의 변환 - -- 문제: §4.1은 `char`를 `u8`과 별개 타입으로 규정하고 암묵 변환을 금지하지만, - §6.4의 줄 수 계산 예제는 `[]u8`에서 얻은 값을 문자 리터럴과 직접 비교했다. -- 결정: 별개 타입과 암묵 변환 금지 원칙을 유지한다. 저장, 대입, 비교 모두 명시적인 - `as`가 필요하며 문자 리터럴도 문맥에 따라 자동으로 `u8`이 되지 않는다. -- 명세 반영: 예제의 비교를 `c.^ == '\n' as u8`로 수정하고 §4.1에 규칙을 명시했다. -- 구현 영향: 타입 검사기는 `char`를 독립 기본 타입으로 취급해야 하고, C 방출 시 같은 - 크기의 정수 표현을 사용할 수 있어도 Ferro 단계에서는 `char`/`u8` 혼용을 거부해야 한다. - -## 2026-08-16 — v0.1.4 - -### `match` scrutinee와 구조체 초기화의 중괄호 모호성 - -- 문제: `match expr { arms }`와 `Type{ fields }`가 모두 식별자 뒤에 `{`를 사용하므로 - `match value { ... }`의 arm 블록을 구조체 초기화로 잘못 소비할 수 있었다. -- 결정: `match` scrutinee 바로 뒤의 `{`는 항상 arm 블록으로 해석한다. 구조체 초기화식 - 자체를 scrutinee로 쓸 때는 `match (Type{ ... }) { ... }`처럼 괄호가 필수다. -- 구현 영향: match 문맥의 식 파서는 최상위 `{` 앞에서 scrutinee 파싱을 멈춰야 하며, - 괄호 안에서는 일반 구조체 초기화 규칙을 그대로 적용한다. 오류 복구는 모든 반복에서 - 적어도 한 토큰을 소비해 같은 진단을 무한 반복하지 않아야 한다. - -## 2026-08-16 — v0.1.5 - -### 제어 흐름 헤더와 구조체 초기화의 중괄호 모호성 일반화 - -- 문제: v0.1.4의 모호성은 `match`뿐 아니라 `if flag {}`, `while flag {}` 및 - `for x in values {}`처럼 식 직후 본문이 시작되는 모든 제어 흐름에 동일하게 발생한다. -- 결정: `if`, `while`, `for`, `match`, `comptime if` 헤더 바로 뒤의 최상위 `{`는 항상 - 제어 흐름 블록을 시작한다. 헤더 최상위에 구조체 초기화식을 쓰려면 괄호가 필수다. -- 구현 영향: 제어 흐름 헤더의 최상위 식 파싱에서 구조체 초기화를 금지하되 괄호 안에서는 - 일반 식 파싱 상태를 복원한다. 단순 식별자 조건과 배열·슬라이스 반복은 본문 `{` 앞에서 - 정상적으로 종료되어야 한다. - -## 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:`에서 빌드 - 금지)은 별도 문서로 옮겨야 한다. - -## 2026-08-16 — v0.1.7 - -외부 전면 audit에서 발견된 안전성 모순과 M6~M10 구현 전 미결정 사항을 통합했다. -이 절의 결정은 v0.1.6의 `str` nominal 타입, 함수 포인터 Writer, R8 단일 파라미터 -규칙 및 own.c 스코프 끝 해제 결정을 명시적으로 대체한다. - -### 슬라이스·문자열·소유 버퍼 - -- 문제: R4는 일반 `^T`의 대상에 `[]T`를 금지하면서 `^[]T`, `List.items`, - `mem.alloc_slice`를 요구했다. 또한 하나뿐인 `[]T`가 읽기/쓰기를 모두 나타내어 - R6의 공유·배타 대여를 표현할 수 없었다. -- 결정: `[]T`는 공유·읽기 전용, `[]mut T`는 배타·쓰기 가능 slice다. var place만 - mutable slice를 만들 수 있다. 호출 인자 위치의 `[]mut T → []T`, `&mut T → &T`는 - 원래 Exclusive 상태를 유지하는 암묵 재대여로 한정한다. -- 결정: `^[]T`는 일반 포인터 합성이 아닌 `(ptr,len)` 독립 소유 타입이다. - `^[]T`/`?^[]T`만 R4의 대상 제한에서 예외이고 `*[]T`/`*[]mut T`는 금지한다. -- 재검토: v0.1.6은 문자열 리터럴의 불변성을 위해 `str`을 nominal 타입으로 만들었지만, - `[]T` 자체가 불변이 되면서 근거가 사라졌다. `str`을 미리 선언된 `[]u8` 완전 동일 - alias로 내렸다. UTF-8 검증은 없으며 별도 cast·C 표현·쓰기 금지 규칙이 필요 없다. -- 결과: `str`도 R4를 그대로 적용받아 field/element에 저장할 수 없다. 문자열을 - 소유하려면 `String{ bytes: ^[]u8 }`를 쓰며 `as_str()`은 파생 shared slice를 반환한다. - -### 안전한 Writer/Reader와 포매팅 분리 - -- 문제: 안전한 `File.writer() -> Writer`가 대여 대상을 `*void`에 숨겨 반환하여 - `make() -> Writer`만으로 safe-code dangling을 만들 수 있었다. 문서의 "사용자 책임"은 - §1의 memory-safety 보장과 충돌하며 Reader도 동일하게 불건전했다. -- 원칙: 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 - 반환할 수 없다. R9 내부에서 unsafe 변환을 한 번 감쌌다는 사실은 safe API를 - 건전하게 만들지 않는다. -- 결정: v0.1.2 Writer/Reader는 함수 포인터 struct 대신 정수 payload만 가진 Copy handle - enum이다. `Writer{Stdout,Stderr,File(u16),Null}`, `Reader{Stdin,File(u16)}`와 - `io.write(Writer, []u8)`, `io.read(Reader, []mut u8)`를 쓴다. fd 재사용은 논리적 I/O - 오류일 수 있으나 memory dangling은 아니다. buffer Writer는 두지 않는다. -- 결정: fmt는 sink를 모른다. `fmt.fmt_int_i32(tmp: []mut u8, v) -> str`처럼 임시 - buffer에 쓰고 R8(a) 파생 slice를 반환하는 순수 함수 한 벌만 둔다. `@print`/`@fprint`는 - 결과를 `io.write`, `@sprint`는 `mem.copy`로 이어 붙인다. v0.2의 `dyn Writer` 전환은 - 안전성 수정이 아닌 기능 확장이다. - -### 소유권·R4·R8 - -- `str`/`[]T`/조건부·에러 union/배열의 재귀 Copy 규칙을 완성하고 `[]mut T`와 - `^[]T`는 non-Copy로 정했다. -- field/index/optional projection에서 non-Copy 값을 부분 이동하는 것을 금지했다. - own.c는 변수 단위 상태를 유지하며 `mem.replace(&mut place, replacement)`만 추출을 - 허용한다. `.?`/field/index는 값을 즉시 꺼내는 연산이 아니라 place projection이다. -- mutable borrow·slice와 `&mut Self` 호출은 var place에서만 허용한다. consuming - `self: Self`는 메서드 내부에서 invalid sentinel을 남길 수 있는 local owner다. -- R8 메서드는 파생 원본을 self로 고정한다. 추가 참조 인자는 받을 수 있지만 반환이 - 그 인자에서 파생될 수 없다. 자유 함수만 참조성 파라미터 정확히 하나를 요구한다. - `?&T`, `?&mut T`, `?[]T`, `?[]mut T` 반환을 포함한다. -- own.c의 오래된 "스코프 끝 해제"와 "R8 결과 바인딩 거부"를 삭제했다. 역방향 - liveness pass로 마지막 사용을 계산하고 defer 사용은 스코프 끝까지 연장한다. - -### drop, File, heap 초기화 - -- `File.close(=drop)` 모순을 제거했다. `close(self: Self) -> !void`는 소비하는 일반 - 메서드이며 내부 handle을 invalid로 만든 후 오류를 반환한다. 자동 drop은 열린 handle만 - 조용히 닫는다. `drop` 직접 호출 금지는 유지한다. -- `mem.create(T) -> !^T`는 초기화되지 않은 안전 힙을 반환하므로 삭제했다. - `mem.create(value: T) -> !^T`로 바꾸고 T는 값에서 추론한다. - -### error와 결정적 build - -- `try`는 operand와 현재 함수의 nominal error 타입이 같을 때만 허용한다. 다른 타입은 - catch에서 명시 매핑한다. `catch`는 error-return 함수 밖에서도 허용하며 void 결과의 - handler block은 정상 fallthrough할 수 있다. -- 정렬 기반 error code 표는 결정성과 fixpoint를 위해 유지한다. build-directory 이력에 - 의존하는 append-only 표는 기각했다. -- 드라이버는 단일 `fe_errors.h`에 정렬된 `#define`을 생성한다. 이름 집합 변경 시 유닛 - C를 재방출하지 않고 header 의존 object만 다시 컴파일한다. switch 상수 요건도 유지한다. - -### 제네릭·증분 build - -- 제네릭 이름 해석은 사용 유닛이 아니라 정의 유닛 scope에서 한다. `.fei`는 본문 token과 - generic 전용 private signature를 함께 기록한다. -- driver가 전체 인스턴스 요청을 합쳐 단일 `fe_generics.c`에 중복 없이 방출한다. - 사용 유닛별 external 중복 심볼과 static 코드 복제를 모두 피한다. -- comptime type 비교와 최소 introspection `@is_int`, `@is_ptr`를 추가했다. -- `.fei` cache key에 source hash뿐 아니라 dependency `.fei` hash를 포함한다. - -### interrupt/shared와 panic - -- shared C 방출을 `volatile`로 정하고 critical 진입·이탈에 compiler barrier를 둔다. - bits16의 한 명령 크기 8/16비트 atomic load/store는 interrupt 경계에서 원자적이므로 - 자동 critical 없이 volatile 한 명령만 방출한다. far pointer와 RMW는 explicit critical이다. -- `interrupt_safe`에서 critical, port/volatile builtin, asm과 필요한 unsafe를 허용한다. - 금지 목록은 heap, DOS/DPMI, blocking I/O, FPU, non-interrupt-safe 호출로 한정했다. -- panic은 일반 defer unwind를 하지 않지만 interrupt vector 복원용 고정 크기 - `sys.on_exit` callback을 실행한다. bits32 interrupt/shared/critical은 v0.2로 명시했다. - -### 문법·표기 정리 - -- generic struct/enum parameter, declaration-level comptime if, for 전용 range, - bool/char pattern, `[]mut T`를 EBNF에 추가했다. -- 정의되지 않은 단항 `^`를 삭제했다. field/index/slice/method의 `&`/`&mut`/`^` - projection과 raw/optional의 비자동 역참조를 명문화했다. -- method가 function-pointer field보다 우선하며 field 호출은 `(x.f)(...)`로 고정했다. -- `@seg_ptr(T, seg, off)`로 타입 인자를 명시하고 type-valued const alias를 허용했다. -- 단항 직후 cast는 `(-x) as T` 또는 `-(x as T)`처럼 괄호를 강제한다. - -### 구현 및 milestone 영향 - -- M3: shared/mutable slice와 str alias를 재검증한다. -- M4: 함수 포인터 Writer를 handle enum + 순수 fmt 함수로 교체한다. -- M5: `^[]T`, consuming close, projection 부분 이동, 초기화된 create를 반영한다. -- M6: 역방향 liveness와 self-source R8을 구현한다. -- M7: try nominal error 일치와 일반 catch를 구현한다. -- M8/M9: `fe_errors.h`, dependency hash, `fe_generics.c`를 구현한다. -- M10: volatile/barrier, interrupt-safe 허용 목록, on_exit 복원을 검증한다. - -## 2026-08-16 — v0.1.8 - -M6~M9 구현 전에 함수-local 소유권 분석, optional/error 의미, 계층형 unit/import, -`.fei` cache와 제네릭 모노모피제이션을 동결했다. compiler A/B가 같은 작은 상태 기계를 -구현하고 DOS와 host에서 같은 source graph를 선택하며 M12 fixpoint에서 byte-identical -출력을 만들 수 있는지가 공통 판단 기준이다. - -### Deterministic expression evaluation order - -- 문제: C는 일반 호출 인자와 많은 operand의 평가 순서를 보장하지 않는다. Ferro가 이를 - 그대로 상속하면 side effect뿐 아니라 move, borrow, `try`, defer/drop cleanup 결과가 C - compiler와 최적화에 따라 달라진다. -- 결정: Ferro 일반 표현식은 left-to-right다. 호출은 callee 먼저, 이어서 source 순서의 - 인자, 이항식은 왼쪽 operand 먼저다. `and`/`or`, `orelse`, `catch`는 필요한 우변만 - 평가하는 lazy 연산이다. -- 근거: source만으로 동작과 cleanup 순서를 예측할 수 있고 compiler A/B의 lower 결과가 - 동일해진다. 함수-local 분석 원칙도 그대로 유지한다. -- 기각한 대안: target C의 평가 순서에 맡기기. host compiler와 build option에 따라 의미가 - 변해 M12 결정성을 깨므로 기각했다. -- 구현 영향: lower는 C에서 순서가 보장되지 않는 식을 ordered temporary statement로 - 분해하고 own.c도 같은 순서로 place effect를 처리한다. - -### M6 root-granularity borrow tracking - -- 문제: field/index별 독립 대여를 허용하려면 projection overlap, 동적 index 동등성, - union/alias까지 다루는 별도 alias analysis가 필요하다. -- 결정: v0.1 대여 상태는 root local/parameter 단위다. `&mut p.a`는 `p` 전체를 잠그고 - `xs[0]`과 `xs[1]`도 같은 root의 충돌 대여다. projection은 root를 찾는 데만 쓴다. -- 근거: R1~R8을 함수 하나의 작은 상태 기계로 검사할 수 있어 compiler A와 M11의 B가 - 단순해진다. 보수적 거부일 뿐 memory safety나 표현 결정성은 약화하지 않는다. -- 기각한 대안: field-sensitive/index-sensitive borrow checking. 편의는 늘지만 compiler A의 - 구현량과 진단 상태가 크게 증가하고 동적 index에는 결국 보수성이 남아 기각했다. -- 구현 영향: own.c의 borrow key는 projection이 아니라 root symbol이다. M6에는 disjoint - field/index도 충돌하는 pass/fail 경계를 고정한다. - -### M6 reborrow/coercion 제한 - -- 문제: `&mut → &`와 `[]mut → []`를 일반 암묵 변환으로 허용하면 새 shared borrow의 - 수명과 원래 exclusive borrow의 재활성화를 결정하는 숨은 coercion/lifetime 시스템이 - 필요하다. -- 결정: 암묵 약화는 호출 인자 위치의 호출 기간 read-only reborrow만 허용한다. 원래 - exclusive borrow는 원래 last-use까지 유지하며 일반 `let`/대입의 암묵 약화는 에러다. -- 근거: API 호출 편의는 확보하면서 수명 annotation 없이 함수-local R6 분석을 유지한다. -- 기각한 대안: arbitrary implicit reborrow/coercion과 `let s: &T = m` 허용. 대여 종료 - 시점이 숨고 compiler A/B가 별도 coercion graph를 가져야 하므로 기각했다. -- 구현 영향: check/own은 call argument에만 임시 shared view 전이를 만들고 assignment - conversion table에는 추가하지 않는다. - -### R8 provenance lattice - -- 문제: 여러 return path의 static/parameter 파생 결과, optional null 경로와 method의 - 추가 참조 인자를 합칠 명시 규칙이 없으면 caller borrow가 구현 순서에 따라 달라진다. -- 결정: provenance를 `Static`과 `Param(N)`으로 정규화한다. method는 `Param(self)`만, - 자유 함수는 유일한 참조성 parameter만 허용한다. `Static + Param(N)`은 `Param(N)`, - 서로 다른 `Param`의 합류는 에러다. null 경로는 caller borrow가 없는 경로다. -- 근거: provenance가 작은 lattice라 함수 본문만 보고 계산하고 시그니처로 전달할 수 있다. - lifetime annotation이나 interprocedural inference가 필요 없다. -- 기각한 대안: arbitrary parameter union provenance 또는 lifetime parameter. caller에서 - 숨은 alias set/전역 분석이 필요해 Ferro 철학과 맞지 않는다. -- 구현 영향: own.c가 return CFG에서 lattice를 합치고 lowered signature와 `.fei`가 - provenance metadata를 보존한다. - -### M6 branch merge와 loop fixed point - -- 문제: `Owned/Moved`, 초기화 여부와 live borrow가 branch/backedge에서 만날 때 단순히 - 한쪽 상태를 고르면 use-after-move를 놓치거나 안전한 borrow를 너무 일찍 푼다. -- 결정: `Owned + Moved → MaybeMoved`, 경로별 초기화 차이는 `MaybeUninit` 동등 상태로 - 합친다. live borrow는 합집합을 보수적으로 유지하고 incompatible borrow는 약화하지 - 않는다. loop은 진입/종료 상태를 합쳐 두 번째 pass를 돌리고 안정되지 않으면 에러다. -- 근거: 유한한 함수-local lattice와 기존 2-pass만으로 모든 iteration을 보수적으로 - 근사한다. 첫 iteration만 검사하는 불건전성을 피한다. -- 기각한 대안: 첫 pass만 검사, 또는 merge에서 borrow를 `Owned`로 되돌리기. loop-carried - alias와 조건부 move를 놓치므로 기각했다. -- 구현 영향: own.c는 branch exit 전에 last-use를 반영하고 merge table/bit state를 - 구현한다. runtime drop에는 `MaybeMoved` live flag가 필요하다. - -### M7 contextual null/error-union construction - -- 문제: `null`에 독립 타입을 주거나 error union을 일반 implicit conversion으로 다루면 - 타입 추론·overload 후보가 늘고 nominal error 경계가 흐려진다. -- 결정: `null`은 expected optional/pointer-like type이 유일할 때만 구성된다. expected - `E!T` 위치에서는 T가 success, E가 failure를 구성하며 return도 같다. E1/E2 및 - nominal error/`core.Error` 자동 변환은 없다. -- 근거: contextual expected type 한 개만 보면 되어 compiler A/B의 local type checker가 - 결정적이고 nominal error 안전성도 유지된다. -- 기각한 대안: polymorphic null, 일반 union injection conversion, error widening. 숨은 - conversion 및 overload resolution을 요구하므로 기각했다. -- 구현 영향: check는 expected-type 전달 위치에서만 null/error construction을 허용하고 - 문맥 없는 `let p = null`을 진단한다. - -### M7 error declaration uniqueness - -- 문제: code 0 외에도 한 nominal error 선언 안의 중복 member 이름이나 숫자 code는 - match/format 결과를 모호하게 만든다. -- 결정: code 0, 중복 이름, 중복 숫자 code를 모두 compile error로 한다. 서로 다른 nominal - error 선언끼리는 같은 숫자를 사용할 수 있지만 타입은 계속 다르다. -- 근거: 선언 하나의 symbol/code table만 검사하면 되고 runtime representation은 바뀌지 - 않는다. -- 구현 영향: error declaration check가 두 deterministic set을 만들고 중복 위치를 note로 - 표시한다. - -### M7 lazy recovery operators / non-Copy extraction - -- 문제: `orelse`/`catch` RHS를 eager 평가하면 불필요한 side effect와 move가 생긴다. - 또한 `Some(x)` pattern이나 projection이 non-Copy payload를 암묵 이동하면 R7과 - 조건부 drop이 불명확해진다. -- 결정: recovery RHS/handler는 failure 경로에서만 평가한다. optional pattern은 place의 - borrow/view이고 Copy만 복사한다. non-Copy owned payload 추출은 `mem.replace`로만 하며 - temporary optional 자동 추출 예외도 두지 않는다. -- 근거: 평가와 소유권 효과가 같은 CFG 경로를 따르고 기존 projection/R7 상태 기계를 - 재사용한다. -- 기각한 대안: pattern별 destructive move와 temporary 특례. hidden move와 추가 drop - 상태를 만들고 source에서 비용이 보이지 않아 기각했다. -- 구현 영향: lower는 lazy branch를 만들고 own은 실행 경로별 effect를 합친다. pattern - binding은 place mutability에 따른 shared/mutable borrow다. - -### Hierarchical dotted unit namespace - -- 문제: 단일 `unit foo` namespace는 외부 source library가 늘 때 `util`, `types`, `parse` - 같은 이름 충돌을 피할 수 없다. -- 결정: `unit_path := ident ('.' ident)*`, `import unit_path [as ident]`의 계층형 canonical - 이름을 도입한다. import binding은 마지막 segment이고 항상 `binding.member`로 접근한다. -- 근거: Go와 비슷한 단순 unit 전체 import를 유지하면서 namespace 충돌만 해결한다. - resolver에는 relative scope walk나 symbol import가 필요 없다. -- 기각한 대안: relative/glob/selective imports, re-export, friend/package-private visibility. - 이름 해석과 캐시 의존 graph가 복잡해져 v0.1에서 제외했다. -- 구현 영향: lexer keyword 추가는 없고 parser/resolve/diagnostic이 canonical dotted path와 - optional alias를 보존한다. - -### DOS-safe unit naming - -- 문제: host의 case sensitivity와 FAT 8.3 규칙이 다르면 같은 source tree가 다른 unit을 - 찾거나 긴 이름 전송 시 변형될 수 있다. -- 결정: unit segment는 lowercase ASCII, 첫 글자 letter, 이후 letter/digit/underscore, - 최대 8자로 제한한다. dotted path는 root 아래 `segment/.../last.fe`와 정확히 대응하고 - 비교는 규범적 ASCII case-insensitive mapping을 쓴다. -- 근거: unit identity가 DOS와 host에서 같고 8.3 alias 생성에 기대지 않는다. -- 기각한 대안: 일반 Ferro identifier/임의 길이 허용 후 host별 normalization. case-fold와 - truncation 충돌이 platform-dependent라 기각했다. -- 구현 영향: entry path suffix로 project root를 계산하고 mismatch/case-variant duplicate를 - 진단한다. canonical identity는 항상 lowercase dotted form이다. - -### Deterministic import-root resolution / ambiguity rejection - -- 문제: project root, 여러 `-I`, std root를 first-match-wins로 검색하면 `-I` 순서나 host - directory 상태가 실제 선택 source를 바꾼다. -- 결정: 모든 candidate root를 조사하고 동일 unit에 서로 다른 canonical file이 둘 이상 - 있으면 ambiguous error와 path note를 낸다. 같은 실제 file alias만 dedup한다. -- 근거: build/order/platform과 무관한 source graph를 만들어 `.fei`, generated C와 M12 - fixpoint를 안정시킨다. -- 기각한 대안: first-match-wins. 편하지만 shadowing이 command-line order에 숨어 결정성을 - 깨므로 기각했다. -- 구현 영향: driver는 후보를 canonicalize·정렬한 뒤 identity를 비교하고 첫 성공에서 - 검색을 중단하지 않는다. - -### Reserved std namespace - -- 문제: flat `io`, `mem`, `fmt`, `sys`는 user library 이름과 충돌하고 compiler 내장 std - root를 일반 user root처럼 검색하면 같은 이름이 환경에 따라 shadow된다. -- 결정: `std` top-level을 compiler-reserved로 하고 `std.io`, `std.mem`, `std.fmt`, - `std.sys`를 canonical unit으로 쓴다. 마지막 segment binding 때문에 사용 표면은 - `io.write`, `mem.replace`로 유지한다. `str`은 import unit이 아니다. -- 근거: std lookup이 명시적이고 deterministic이며 향후 source package와 충돌하지 않는다. -- 구현 영향: M8에서 std source layout/unit 선언을 이동하고 builtin std root는 `std.*`에만 - 후보가 된다. - -### Source-only external libraries - -- 문제: v0.1에서 package manifest/solver나 stable binary ABI까지 정의하면 M8 범위를 넘어 - `.fei` encoding과 target C ABI를 영구 호환 계약으로 굳히게 된다. -- 결정: 외부 library는 source tree를 `-I` root로 제공한다. package manager/registry/version - solver/manifest 문법과 `.fei + .obj/.lib` binary-only 배포 ABI는 지원하지 않는다. -- 근거: 언어 import 의미는 작게 유지하고 target/model별로 source에서 결정적으로 - 재컴파일할 수 있다. DOS 배포 도구도 단순하다. -- 기각한 대안: package manager를 언어 의미론에 결합, binary-only ABI. compiler A와 - M12 전에 해결할 필요가 없고 호환 부담이 커 기각했다. -- 구현 영향: `-I`는 source-only candidate root이며 미래 도구도 root 구성만 담당한다. - -### `.fei` interface/cache role과 deterministic schema - -- 문제: source hash, public interface hash와 compile cache key가 섞여 있었고 `.fei`의 - 최소 논리 정보·결정적 직렬화 조건이 없어 private 변경이 전체 rebuild를 유발하거나 - host path/timestamp가 fixpoint에 섞일 수 있었다. -- 결정: 세 hash 개념을 분리하고 `.fei`에 version/target/model, canonical unit, public - signatures/layout, anonymous errors, exported generic body/support closure, direct dependency - names/hashes를 기록한다. canonical key 순으로 직렬화하며 absolute path/time/build dir를 - 금지한다. -- 근거: private non-generic 변경은 자기 unit만 재컴파일하고 interface가 같으면 dependent를 - 유지할 수 있다. 같은 graph+target의 `.fei`는 host/build order와 무관하게 byte-identical하다. -- 기각한 대안: source hash를 interface hash로 재사용, unordered serializer. 구현은 짧지만 - 불필요한 rebuild와 M12 비결정성을 만들어 기각했다. -- 구현 영향: `.fei` encoding은 자유지만 논리 schema와 sorted serialization을 만족하고 - driver cache가 dependency interface hash를 사용해야 한다. - -### Ferro visibility vs backend linkage - -- 문제: Ferro private를 무조건 C `static`으로 방출하면 통합 `fe_generics.c`의 exported - instance가 definition unit private helper를 호출할 수 없다. -- 결정: Ferro private는 resolver visibility일 뿐 C linkage와 동일하지 않다. generic - support에 필요한 private top-level symbol은 deterministic unit-mangled linkage와 internal - prototype을 가질 수 있다. -- 근거: source 접근 권한은 유지하면서 단일 통합 generic body 방출을 가능하게 한다. -- 기각한 대안: `C static == Ferro private`, 또는 private helper를 instance마다 복제. - 전자는 linkage 실패, 후자는 중복과 비결정적 출력 때문에 기각했다. -- 구현 영향: resolver는 여전히 cross-unit private 참조를 거부하고 backend/internal header만 - `.fei` support metadata를 통해 symbol을 연결한다. - -### M9 type-only generics - -- 문제: user comptime value generic까지 허용하면 값 canonicalization, mangling, expression - evaluator와 instance explosion 정책을 M9에서 함께 설계해야 한다. -- 결정: v0.1 user generic parameter는 `type`만 지원한다. `struct Box(T)`는 - `comptime T: type` shorthand이며 builtin comptime value와 구별한다. -- 근거: List/Map과 compiler B에 필요한 추상화를 충족하면서 instance key를 canonical type - list로 제한한다. trait/bound도 추가하지 않는다. -- 기각한 대안: integer/string/bool value generics. M11 필수 기능이 아니고 구현/결정성 - 부담이 커 v0.1 이후로 미룬다. -- 구현 영향: parser/check가 user generic parameter type을 제한하고 value generic fixture를 - 명시적으로 거부한다. - -### M9 no type inference - -- 문제: `id(3)`에서 T를 추론하려면 argument constraints, conversion 후보와 향후 overload - 규칙을 정의해야 하고 진단/instance 발견 순서도 복잡해진다. -- 결정: generic type argument는 `id(i32, 3)`처럼 항상 명시한다. compiler-known - `mem.create(value)` inference는 별도 intrinsic 규칙이다. -- 근거: call syntax만 보고 instance key가 결정되어 compiler A/B가 단순하고 deterministic하다. -- 기각한 대안: generic type inference. 편의보다 숨은 constraint solver 비용이 커 기각했다. -- 구현 영향: generic call arity/type argument check는 명시 목록만 검사하며 inference - fallback을 시도하지 않는다. - -### Definition-site resolution - -- 문제: generic body의 non-dependent 이름을 caller scope에서 다시 찾으면 caller의 import와 - shadowing에 따라 같은 generic이 다른 코드를 만든다. -- 결정: 이름은 definition unit에서 고정하고 type-dependent operation만 instantiation 때 - 검사한다. `comptime if`의 선택되지 않은 branch는 parse만 하고 semantic 처리하지 않는다. -- 근거: lexical 의미와 private support를 유지하고 caller/build order와 무관한 instance를 - 만든다. -- 기각한 대안: use-site lookup과 selected-out branch의 eager type check. 전자는 의미가 - 불안정하고 후자는 type-specific branch를 불가능하게 해 기각했다. -- 구현 영향: `.fei`가 body token과 definition-scope symbol/support identity를 전달하고 - generic.c가 그 환경에서 재검사한다. - -### Deterministic monomorphization - -- 문제: request 발견 순서대로 instance를 방출하면 unit traversal/hash iteration/parallel - build에 따라 `fe_generics.c`와 symbol 순서가 바뀐다. -- 결정: canonical definition unit + declaration identity + normalized canonical type args를 - key로 dedup하고 byte ordering으로 정렬한다. prototype 전부를 먼저, body 전부를 나중에 - 같은 canonical 순서로 방출한다. -- 근거: alias instance가 중복되지 않고 recursion/cross-instance call을 지원하며 M12에서 - byte-identical output을 만든다. -- 기각한 대안: first-request order 또는 pointer/insertion-order key. platform/build order에 - 의존해 기각했다. -- 구현 영향: driver/generic.c가 global request set을 정렬하고 alias를 underlying interned - identity로 normalize한다. - -### Generic private-support closure - -- 문제: exported generic이 private helper/type/const/private generic을 참조하면 signature만 - 담은 `.fei`로 다른 unit에서 안전하게 instantiate할 수 없다. -- 결정: `.fei`는 필요한 private support dependency의 transitive closure를 compiler-only - metadata로 제공한다. 이는 Ferro visibility를 public으로 바꾸지 않는다. -- 근거: definition-site semantics와 source private API를 동시에 지키며 `fe_generics.c`에서 - 정확한 backend symbol/layout을 사용할 수 있다. -- 기각한 대안: 모든 support를 source `pub`으로 강제하거나 generic body를 definition unit마다 - static 복제. API 누출 또는 중복/링크 문제 때문에 기각했다. -- 구현 영향: interface hash는 exported generic이 관찰하는 support 변화에 반응하고 - serializer는 closure를 canonical 순서로 기록한다. - -### Recursive instantiation semantics - -- 문제: 단순 재귀 호출이 같은 instance를 다시 요청할 때마다 depth를 올리면 정상 generic - recursion도 limit에 걸리고, 반대로 growing type chain을 dedup만으로 허용하면 무한 생성된다. -- 결정: pending/known 동일 key 재요청은 재사용하고 depth를 소비하지 않는다. 새로운 distinct - instance chain만 증가시키며 32 초과를 에러로 한다. -- 근거: ordinary recursion은 prototype-first 방출로 처리하고 실제 instance explosion만 - 유한한 local driver 상태로 차단한다. -- 구현 영향: generic.c는 pending/known set과 distinct chain stack을 구별하고 최초/현재 - instantiation 위치를 note로 출력한다. - -### Canonical C mangling and type identity - -- 문제: unit이 계층화되고 generic이 통합 방출되면 host path, pointer address 또는 insertion - order 기반 이름은 충돌하거나 run마다 달라질 수 있다. -- 결정: mangling은 canonical dotted unit + declaration + normalized canonical type args만 - 사용한다. nominal identity는 fully-qualified defining unit+name, alias는 underlying identity다. -- 근거: collision-free backend linkage와 M12 fixpoint를 동시에 보장한다. -- 구현 영향: 정확한 escaping 문자는 구현 세부지만 deterministic separator encoding과 - collision 검사가 필요하며 absolute path/address를 symbol에 포함할 수 없다. - -### `try` enforcement is blocked on M7 contextual construction - -- 문제: `SPEC.md`는 `try`를 에러 유니온 반환 함수 안에서만 허용하는데, `check.c`의 검사가 - `FE_N_EXPR_STMT`에만 걸려 있어 `var x = try e;`와 `x = try e;`를 통과시킨다. - `fec/tests/m5/runtime.fe`의 `run`과 `owned.fe`의 `main`이 이 구멍에 의존하고 있었다. -- 결정: 구멍은 M7과 함께 닫는다. M7 이전 master에서는 닫을 수 없다. (M7에서 해소됨) -- 근거: 검사를 `try` 표현식으로 옮기면 `run`이 에러 유니온을 반환해야 하는데, master는 - `-> !i32`에서 `return ;`도, `-> !void`에서 명시적 `return;`도 거부한다 - ("return type mismatch" / "void expression returned from value function"). 둘 다 - contextual success construction이 필요하고 그것은 M7 작업이다. `catch`와 `@trap`도 - master에는 없어서 우회로가 없다. 실측으로 세 경로를 모두 확인했다. -- 구현 영향: M7 병합 시 `check.c`의 검사를 `check_expr`의 `try` 분기로 옮기고 - `FE_N_EXPR_STMT`의 중복 검사를 제거한다. `runtime.fe`의 `run`은 그때 에러 유니온 - 반환으로 바꾸고 `runtime.c`의 `extern long fe_m5_runtime_run(long)`을 함께 고친다. - `owned.fe`의 `main`은 M7 없이도 합법인 `-> !void`로 먼저 고쳐 두었다. diff --git a/SPEC.md b/SPEC.md index 5941711..8eb5444 100644 --- a/SPEC.md +++ b/SPEC.md @@ -3,7 +3,8 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. -이 문서는 언어 명세 + 컴파일러 구현 지시서를 겸한다. 구현 중 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. +이 문서는 Ferro 언어 명세만 다룬다. 컴파일러 구현 지시서와 표준 라이브러리 상세 명세는 +별도 문서에서 다룬다. 명세 판단이 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. --- @@ -13,7 +14,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다. 3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. 4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. -5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §13에 기록한다. +5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §11에 기록한다. 6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다. --- @@ -28,10 +29,8 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 | 메모리 모델 | small, large | flat | | 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | -- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. -- 표준 라이브러리는 코어 공용, `std.sys` 유닛만 타깃별 구현. --- @@ -172,7 +171,7 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 - `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. - `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. - `try e`: 에러면 현재 함수에서 즉시 반환한다. -- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) +- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §11 참조.) - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. @@ -588,15 +587,13 @@ pub fn main() -> !void { - 이항 연산자는 왼쪽 operand를 먼저, 오른쪽 operand를 나중에 평가한다. - `and`와 `or`는 왼쪽 operand로 결과가 정해지면 오른쪽을 평가하지 않는다. `orelse`와 `catch`도 §4.5·§4.6에 따라 우변/handler가 필요한 경로에서만 평가한다. - 이 순서는 부수 효과뿐 아니라 move, borrow의 시작·마지막 사용, `try` 전파와 defer/drop cleanup 순서를 결정한다. -- C backend는 C 자체의 미지정 평가 순서에 의존할 수 없다. C에서 순서가 보장되지 않는 호출 인자, 일반 이항 operand 등의 부수 효과·이동·대여는 lower 단계에서 순서가 명시된 temporary statement로 분해한다. `and`/`or` 같은 C의 단락 규칙을 직접 사용하더라도 Ferro의 lazy 의미를 그대로 보존해야 한다. --- -## 8. 유닛과 빌드 +## 8. 유닛 파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며 -모든 cross-unit 타입·선언·제네릭 identity, `.fei`, C mangling, cache key와 진단에서 같은 -이름을 사용한다. +모든 cross-unit 타입·선언·제네릭 identity에서 같은 이름을 사용한다. ```fe unit game.main; @@ -612,8 +609,12 @@ import는 항상 유닛 전체를 가져오며 member는 local unit binding으 `parse.read(...)`, `import std.io;` 뒤에는 `io.write(...)`를 쓴다. `as`가 있으면 그 alias가 binding이다. +`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 표준 유닛은 +`import std.io;`, `import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다. +`str`은 계속 built-in `[]u8` alias와 alias-method namespace이며 import unit이 아니다. + v0.1은 relative import(`.foo`, `..foo`), glob/selective import, `pub import` re-export, -package-private/friend visibility를 지원하지 않는다. +package-private/friend visibility, package manager를 지원하지 않는다. ### 8.1 unit 이름과 source path @@ -631,42 +632,7 @@ tinyjson.parse -> tinyjson/parse.fe game.world.map -> game/world/map.fe ``` -entry source의 선언이 `unit game.main;`이고 실제 파일이 -`C:/PROJECT/SRC/GAME/MAIN.FE`이면 file path 끝의 `game/main.fe` suffix를 ASCII -case-insensitive 방식으로 비교해 제거하고 project source root `C:/PROJECT/SRC`를 얻는다. -unit path/file path 대응은 host filesystem의 기본 case 규칙이 아니라 이 규범적 비교를 -써서 DOS와 모든 host에서 같게 처리한다. suffix가 일치하지 않으면 컴파일 에러이며 같은 -unit 선언을 임의 위치에서 조용히 허용하지 않는다. case-sensitive host에 `game/main.fe`와 -`GAME/MAIN.FE`가 별도 실제 파일로 함께 있으면 §8.2의 서로 다른 후보이므로 ambiguous다. - -### 8.2 import root와 모호성 - -candidate root는 다음 집합이다. - -1. entry source에서 계산한 project source root -2. 사용자가 지정한 각 `-I ` -3. `std.*`에 대해서만 compiler 내장 std root - -`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 일반 user -unit은 compiler std root에서 찾지 않는다. 표준 유닛은 `import std.io;`, -`import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다. `str`은 계속 built-in -`[]u8` alias와 alias-method namespace이며 import unit이 아니다. - -root를 순서대로 검사해 첫 성공을 고르지 않고 모든 candidate를 조사한다. 동일 canonical -unit path에 대해 서로 다른 실제 source file이 둘 이상 발견되면 다음 형태의 compile -error를 내고 각 실제 path를 note로 표시한다. - -``` -ambiguous unit 'foo.bar' -note: ... -note: ... -``` - -filesystem canonicalization 결과 같은 실제 파일이 여러 root 또는 path alias로 발견된 -경우만 하나로 취급할 수 있다. 따라서 `-I` 순서, 디렉터리 열거 순서, host 환경이 source -선택을 바꾸지 않는다. - -### 8.3 binding, visibility와 외부 library +### 8.2 binding과 visibility 한 unit에서 import binding은 다른 unit-scope declaration/import binding과 충돌할 수 없다. `import foo.net; import bar.net;`은 둘 다 `net`을 만들므로 에러이며 두 번째를 @@ -679,74 +645,12 @@ function parameter/return, public field 등 외부 signature에 나타나는 nom importer가 이름을 해석할 수 있어야 하며 private nominal type을 public API에 노출하면 컴파일 에러다. -v0.1 외부 library는 source-only import root다. 예를 들어 `-I deps`와 -`deps/tinyjson/parse.fe`가 있으면 `import tinyjson.parse;`로 사용한다. package manager, -registry, version solver, manifest dependency 문법은 v0.1에 없다. 미래 package manager도 -dependency source tree를 import root에 배치하고 `-I`를 구성하는 도구일 뿐 Ferro import -의미론을 바꾸지 않는다. `.fei + .obj/.lib`만 배포하는 binary-only package ABI도 v0.1은 -지원하지 않는다. - -### 8.4 `.fei` interface와 cache - -`.fei`는 incremental compilation interface, build cache metadata, 다른 unit에서의 generic -instantiation을 위한 compiler interface다. 물리적 binary/text encoding은 구현 세부지만 -논리적으로 최소한 다음을 표현할 수 있어야 한다. - -- magic/format version, Ferro SPEC/compiler interface version -- target과 해당하는 경우 bits16 memory model -- canonical unit name -- public symbol signature와 public nominal type identity/layout -- anonymous `error.Name` name set -- exported generic declaration metadata와 body token stream -- exported generic이 요구하는 private support symbol metadata -- direct dependency canonical unit name과 dependency interface hash - -`.fei` serialization은 결정적이어야 한다. unordered container iteration을 그대로 쓰지 -않고 canonical key/name의 byte ordering으로 정렬해 serialize한다. absolute host path, -timestamp, build directory를 기록하지 않는다. 같은 source/interface graph와 target/model이면 -build order와 host path에 관계없이 byte-identical `.fei`가 목표다. - -다음 hash는 구별한다. - -- **source hash**: 해당 unit source 내용 변화 감지 -- **interface hash**: dependent unit이 관찰하는 `.fei` 의미 정보의 hash -- **compile cache key**: source hash, target/model/options와 실제 재컴파일에 필요한 - direct/indirect dependency interface hash를 포함한 key - -private non-generic 구현만 바뀌어 public/generic-visible interface가 같으면 해당 unit은 -재컴파일하지만 interface hash는 유지되어 dependent unit을 재컴파일하지 않아도 된다. -public signature/layout 또는 exported generic이 관찰하는 private support 정보가 바뀌면 -interface hash가 바뀐다. - -### 8.5 순환과 canonical identity +### 8.3 순환과 canonical identity 순환 import는 컴파일 에러다. fully-qualified dotted unit path와 선언 이름이 nominal identity의 기준이므로 `tinyjson.value.Value`와 `tinyjson.value.Box`처럼 표시한다. nominal struct/enum/error는 defining unit + declaration name으로 구별되고 type alias는 새 nominal -identity를 만들지 않는다. 이 canonical identity 규칙은 §9 generic cache와 §11.4 C -mangling에도 그대로 적용한다. - -``` -fec main.fe --target=bits32 -o game.exe -fec main.fe --target=bits16 --model=large --no-checks -o game.exe -fec main.fe --emit-c -o out/ # 트랜스파일 결과만 -fec --dump-ast main.fe -``` - -### 8.6 CLI 플래그 (전체) - -| 플래그 | 의미 | 규정 | -|---|---|---| -| `--target=bits16\|bits32` | 타깃 선택 | §2 | -| `--model=small\|large` | `bits16` 메모리 모델 | §2 | -| `-o <경로>` | 출력 파일 또는 디렉터리 | §8 | -| `-I <디렉터리>` | source-only import candidate root 추가 | §8.2·§8.3 | -| `--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 | +identity를 만들지 않는다. --- @@ -798,16 +702,9 @@ var xs: List(u8) = List(u8).new(); identity + canonical type argument list**다. type alias는 새 nominal identity가 아니므로 underlying/interned canonical type identity로 정규화한다. 따라서 `const Word = i32;` 뒤의 `id(Word, 1)`과 `id(i32, 2)`는 같은 instance다. -- 최종 build driver는 모든 unit의 instance request를 모아 canonical key로 중복 제거하고 - key의 byte ordering으로 정렬한다. 먼저 필요한 prototype을 결정적 순서로 방출하고 이어서 - body를 같은 순서로 단일 `fe_generics.c`에 방출한다. request 발견 순서, hash iteration, - build order에 의존하거나 사용 unit별 external/static 중복 코드를 만들지 않는다. -- exported generic이 definition unit의 private symbol을 참조하면 `.fei`는 다른 unit에서 - instantiate하는 데 필요한 support dependency의 transitive closure를 기록한다. private - non-generic function은 signature와 backend link identity, private nominal type은 필요한 - identity/layout/signature, comptime const는 evaluated value/type, private generic은 body - token stream과 자기 support dependency를 제공한다. 이 compiler/link metadata는 Ferro - source visibility를 public으로 바꾸지 않는다. +- exported generic은 definition unit의 private symbol을 참조할 수 있다. 이는 컴파일러 + 수준의 처리이며 Ferro source visibility를 public으로 바꾸지 않는다 — 다른 Ferro source는 + 여전히 그 private 심볼을 직접 참조할 수 없다. ```fe unit lib; @@ -820,8 +717,8 @@ pub fn bump(comptime T: type, x: T) -> T { } ``` -다른 unit이 요청한 `lib.bump(i32)` instance는 generated internal C symbol을 통해 -`helper`를 호출할 수 있지만, 다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. +다른 unit이 요청한 `lib.bump(i32)` instance는 내부적으로 `helper`를 호출할 수 있지만, +다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. - 재귀적 인스턴스화의 distinct-instance chain 제한은 32다. 이미 pending/known인 동일 canonical instance key를 다시 요청하는 recursion은 pending instance를 재사용하고 depth를 @@ -832,264 +729,31 @@ pub fn bump(comptime T: type, x: T) -> T { --- -## 10. 표준 라이브러리 (최소 집합) +## 10. 표준 라이브러리 -표준 라이브러리는 reserved `std` namespace 아래에 있으며 `import std.io;`처럼 명시적으로 -가져온다. import 뒤의 local binding은 마지막 segment라 기존처럼 `io.write`, `mem.replace` -형태로 사용한다. 실제 `fec/std` source 배치는 M8에서 이 canonical unit path에 맞춘다. +표준 라이브러리 상세 명세는 별도 문서에서 다룬다. 표준 라이브러리는 reserved `std` +namespace 아래에 있으며 `import std.io;`처럼 명시적으로 가져온다. import 뒤의 local +binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다. -- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. -- **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다. -- **문자열/바이트**: `str`은 `[]u8` alias다. 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr`를 `line.trim()`처럼 호출하며 `str` 이름의 import 유닛은 두지 않는다. 소유 문자열 `String`은 `^[]u8`을 감싸고 `as_str(self: &Self) -> str`을 제공한다. -- **`std.list`**: `List(T)`. -- **`std.map`**: `Map(K, V)`(오픈 어드레싱, K는 정수 또는 `String`). `String` key map은 key buffer를 소유하고 조회에는 `get_str(self: &Self, key: str) -> ?&V`를 제공한다. -- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. +아래는 언어 규칙(§4~§7)이 직접 참조하거나 언어 표면(빌트인, 예제)이 전제하는 최소 +표면만 남긴 것이다. `std.list`, `std.map`, `std.io.File`의 전체 API, `std.sys`의 OS +접근 함수 등 나머지 모듈의 정확한 시그니처는 표준 라이브러리 명세가 정의한다. + +- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합, §4.6), `assert`. `panic`/`set_panic_handler`는 §7.4 트랩 동작이 참조한다. +- **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다(§4.5, §5 R3·R7·R11). +- **문자열/바이트**: `str`은 `[]u8` alias다(§4.2). 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr`를 `line.trim()`처럼 호출하며(§6.4 예제) `str` 이름의 import 유닛은 두지 않는다. +- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음이며 `@print`/`@fprint`/`@sprint`(§6.3.1)가 의존한다. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. - **`std.io`**: ```fe pub enum Writer { Stdout, Stderr, File(u16), Null } pub enum Reader { Stdin, File(u16) } ``` - 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다. `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. 닫힌 fd 또는 재사용된 fd를 가진 복사 handle은 I/O 오류나 의도하지 않은 파일 접근이라는 논리 오류를 만들 수 있지만 dangling memory access는 만들지 않는다. - - `File{ open, create, read(self: &mut Self, []mut u8), write(self: &mut Self, []u8), seek, size, writer, reader, close }`. - - `close(self: Self) -> !void`는 File을 소비하는 일반 메서드이며 `drop`이 아니다. 내부 handle을 먼저 invalid 상태로 만든 뒤 닫기 오류를 반환하므로 함수 종료의 자동 drop은 no-op이다. `drop`은 아직 열린 handle만 오류를 무시하고 닫는다. `drop` 직접 호출 금지는 유지한다. - - 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 반환해서는 안 된다. 따라서 v0.1에는 함수 포인터/`*void` 기반 Writer·Reader나 buffer Writer가 없다. `@sprint`는 대상 slice에 직접 복사한다. -- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `on_exit`은 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. + 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다(§5 R8 예제). `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. +- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`. `on_exit`은 §7.4 트랩 동작이 참조하는 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. --- -## 11. 컴파일러 구현 - -### 11.1 부트스트랩 전략 - -1. **컴파일러 A** — C89로 작성. Ferro → C 트랜스파일러. 호스트는 현대 PC 또는 DOS. 출력 C는 DJGPP(gcc, bits32) / Open Watcom(bits16, bits32) / Borland C(bits16)로 컴파일. -2. **컴파일러 B** — Ferro로 A와 동일 구조를 재작성. A로 빌드. -3. **셀프호스팅** — B로 B를 빌드. 그 결과로 다시 B를 빌드해 출력이 바이트 동일(fixpoint)하면 완료. A 폐기. -4. **네이티브 백엔드** — B에 386 코드 생성기 추가, 이후 8086 코드 생성기. - -A는 버릴 코드다. 최적화하지 말고 B를 컴파일할 수 있는 최소 언어 부분집합만 지원한다. - -### 11.2 파이프라인 - -``` -소스 → lexer → parser(AST) → resolve(이름/import) → check(타입) - → own(소유권·참조) → lower(소멸자/defer/try/for 전개 → LIR) - → emit_c(C 소스) [또는 emit_x86] -``` - -각 단계는 실패해도 가능한 한 진행해 에러를 모아 보고한다(문장 단위 복구). - -### 11.3 디렉터리 - -``` -fec/ - src/ - lexer.c/h 토큰화. 위치(파일, 줄, 열) 보존. - ast.c/h 노드 정의, 아레나 할당자. - parser.c/h LL(1) 재귀하강. 에러 복구는 다음 ';' 또는 '}'까지 스킵. - types.c/h 타입 인터닝(포인터 비교로 동등성), 레이아웃 계산(타깃별). - resolve.c/h 스코프 체인, 심볼 테이블, import, .fei 읽기/쓰기. - check.c/h 타입 검사, 리터럴 타입 결정, match 완전성, R4 위치 검사. - own.c/h §11.5 알고리즘. - lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. - emit_c.c/h LIR → C. §11.4 규칙. - generic.c/h 인스턴스 캐시, 토큰 재파싱. - driver.c CLI, 유닛 의존 순서, .fei 캐시, fe_errors.h와 fe_generics.c 생성, - 외부 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 | -|---|---| -| `i16`, `u32` 등 | `int16_t`, `uint32_t` (`` 없으면 자체 typedef) | -| `usize` | `uint16_t`(bits16) / `uint32_t`(bits32) | -| `bool` | `unsigned char` | -| 일반 `^T`, `*T` | `T*` | -| `^[]T` | `typedef struct { T* p; fe_usize n; } fe_owned_slice_T;` | -| `&T` | `const T*` | -| `&mut T` | `T*` | -| `far X` | `__far X` (Watcom/Borland), bits32는 무시 | -| `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | -| `[]T`/`str` | `typedef struct { const T* p; fe_usize n; } fe_slice_T;` | -| `[]mut T` | `typedef struct { T* p; fe_usize n; } fe_mut_slice_T;` | -| `?T` (포인터류) | 원래 포인터, null 사용 | -| `?^[]T` | `struct { unsigned char has; fe_owned_slice_T v; }` | -| `?T` (그 외) | `struct { unsigned char has; T v; }` | -| `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | -| `shared [atomic] var x: T` | `volatile T x` | -| struct | `struct fe__` | -| enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` | -| 함수 | `fe__`, 메서드는 `fe___` | -| 제네릭 인스턴스 | `fe____` | - -세부: -- **canonical mangling**: C symbol은 canonical dotted unit path + declaration name + canonical - type argument list를 collision-free하게 encode한다. `.`의 separator/escaping 문자는 구현 - 세부지만 host path, pointer address, insertion/hash iteration order를 사용할 수 없다. - type alias는 canonical underlying identity를 쓰고 nominal struct/enum/error는 fully-qualified - defining unit + name을 쓴다. generic instance 이름도 같은 key에서만 생성해 M12 fixpoint에서 - byte-identical해야 한다. -- **Ferro visibility와 C linkage**: Ferro `private`는 source name visibility이며 반드시 C - `static`을 뜻하지 않는다. exported generic instance가 definition unit의 private helper를 - 호출할 수 있도록 generic body가 필요로 하는 private top-level function/global을 - deterministic unit-mangled external C symbol로 방출하고 내부 generated header에 prototype을 - 제공할 수 있다. 이는 resolver의 private 접근을 완화하지 않으며 다른 Ferro unit source의 - 직접 참조는 계속 에러다. 필요한 closure는 §9와 `.fei` metadata가 제공한다. -- **오버플로 검사**: `fe_add_i16(a, b, LINE)` 인라인 함수. `--no-checks`면 매크로가 `((a)+(b))`로 축약. -- **경계 검사**: `fe_idx_T(s, i, LINE)` → `(i < s.n ? s.p[i] : (fe_trap_bounds(LINE), s.p[0]))`. `for` 루프는 직접 인덱스. -- **`try`**: `{ Ttmp t = expr; if (t.e) return (RetT){ t.e }; }` 후 `t.v` 사용. defer/소멸자가 있으면 return 전에 정리 코드 삽입. -- **`catch`**: `t.e`가 참일 때만 block 또는 짧은 RHS를 평가하고 바인딩 변수는 `t.e`다. -- **`defer`/소멸자**: lower 단계에서 스코프 종료 지점(정상 흐름, `return`, `break`, `continue`, `try` 전파)마다 역순 호출을 명시적으로 삽입. C의 goto 라벨을 써도 되고 복제해도 된다(A는 복제, B는 goto 권장). -- **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. -- **`match`**: `switch (x.tag)`. Copy payload는 지역 변수로 복사하고 non-Copy projection payload는 R7에 따라 참조로만 바인딩한다. 소유값 추출은 match 전 `mem.replace`로 수행한다. -- **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. -- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 `fmt.fmt_*`로 임시 `[]mut u8`에 변환하고 `io.write` 또는 `mem.copy`를 호출하는 나열로 전개한다. `@print`는 각 I/O 오류를 버리고, `@fprint`는 첫 오류를 전파하며, `@sprint`는 남은 길이를 추적해 잘라 쓴 실제 길이를 반환한다. 포맷 문자열 조각은 static const shared slice로 방출하고 동일 문자열은 중복 제거한다. -- **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다. -- **에러 코드**: 드라이버가 emit 전에 확정한 `error.Name`의 `u16` 코드를 단일 `fe_errors.h`의 `#define`으로 방출한다(§4.6). 모든 유닛 C가 이 헤더를 include하므로 에러 `match`를 `switch`로 방출할 수 있고 이름 집합 변경 시 C 재방출 없이 오브젝트만 무효화한다. -- **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. -- **평가 순서**: §7.6의 callee-first, operand/argument left-to-right 순서를 지킨다. C가 - 순서를 보장하지 않는 일반 호출 인자와 이항 operand에 부수 효과·move·borrow가 있으면 - lower가 순서대로 temporary statement를 만들고 emit은 그 결과만 조합한다. cleanup과 - `try` 전파도 같은 순서를 따른다. -- **공유 상태와 임계 구역**: `shared`는 `volatile`로 방출한다. bits16의 `critical`은 compiler barrier → FLAGS 저장 → `cli` 순서로 진입하고 모든 이탈에서 저장한 FLAGS 복원 → compiler barrier 순서로 끝낸다. GCC 계열은 `asm volatile("" ::: "memory")`, Open Watcom/Borland는 optimizer가 내용을 볼 수 없는 별도 runtime 함수 호출 경계를 사용한다. 한 명령 크기의 `shared atomic` 단일 접근은 volatile load/store만 방출한다. -- **방출 순서**: `fe_errors.h` → typedef 전방선언 → struct 정의(동률을 canonical name으로 - 끊는 의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문 → 통합 `fe_generics.c`. - unordered container나 source 발견 순서에 기대지 않는다. `fe_generics.c`는 canonical - instance prototype을 먼저, body를 나중에 각각 key byte ordering으로 방출한다. -- 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성한다. 제네릭 인스턴스 본문은 유닛 C에 중복 방출하지 않는다. - -### 11.5 own.c 알고리즘 - -함수 단위. 각 지역 변수/파라미터에 상태: -``` -Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive -``` - -초기화 여부가 경로마다 다른 합류에는 `MaybeUninit` 또는 같은 의미의 별도 bit/state를 -사용할 수 있다. projection은 root local/parameter를 찾는 데만 사용하고 field/index별 -대여 상태는 만들지 않는다(R6). - -1. 먼저 AST를 역방향 순회해 각 참조 변수의 경로별 마지막 사용을 계산한다. `defer` 안의 사용은 해당 스코프 끝으로 올린다. -2. AST를 §7.6의 평가 순서로 순회하며 상태 전이한다. 표현식의 place 사용을 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기 / projection으로 분류하고 projection의 root 상태를 갱신한다. -3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 표시). field/index/`.?` projection의 비-Copy 이동은 R7에 따라 거부하고 `mem.replace`만 허용한다. -4. `&place`: root의 `Owned → Shared(n+1)`. `&mut place`: root의 `Owned → Exclusive`. 역방향 pass가 계산한 마지막 사용에서 해제하며 임시는 문장 끝에 해제한다. 서로 다른 field/index도 같은 root 상태와 충돌한다. -5. 호출 인자의 `&mut → &`, `[]mut → []`만 Exclusive를 유지하는 호출 기간의 임시 shared view로 검사한다. 일반 `let`/대입에는 이 implicit transition을 적용하지 않는다. -6. `Shared`/`Exclusive` 상태에서 금지된 쓰기/이동/재대여를 진단한다(R6). -7. **분기 합류**: `if`/`match`의 각 branch를 독립 상태로 계산한다. branch exit 전에 last-use/liveness에 따라 끝난 borrow를 먼저 해제하고 다음 규칙으로 병합한다. - - | 왼쪽 | 오른쪽 | 결과 | - |---|---|---| - | 같은 상태 | 같은 상태 | 같은 상태 | - | `Owned` | `Moved` | `MaybeMoved` | - | `Moved` | `Owned` | `MaybeMoved` | - | `MaybeMoved` | `Owned`/`Moved`/`MaybeMoved` | `MaybeMoved` | - | `Uninit` | `Owned` 등 initialized 상태 | `MaybeUninit` 또는 동등 상태 | - - merge는 좌우 대칭이다. `MaybeMoved`/`MaybeUninit` 값의 이후 읽기·이동은 에러이고 drop은 필요한 runtime live - flag를 쓴다. 한 경로에서만 borrow가 계속 살아 있으면 합류 뒤에도 살아 있는 것으로 - 보수적으로 취급한다. `Shared(n)`과 `Shared(m)`은 필요한 live shared borrow의 합집합을 - 보존하고 단순 count 구현에서는 적어도 `Shared(max(n,m))`으로 합친다. 한쪽만 - `Exclusive`가 live여도 합류 뒤 root를 `Exclusive` 효과로 잠근다. 서로 양립할 수 없는 - `Shared`/`Exclusive` 상태는 더 약한 상태로 풀지 않고 양쪽 효과를 보존하는 보수적 - 상태로 합치거나 compile error를 낸다. 구현은 state enum/bitset을 확장할 수 있지만 이 - 의미를 만족해야 한다. -8. **루프 fixed point**: loop 진입 상태로 body를 한 번 분석하고 종료/backedge 상태를 - 진입 상태와 위 규칙으로 병합한다. 그 merged 상태로 body를 두 번째 분석한다. v0.1 - compiler A는 이 2-pass를 사용하며 두 번째 분석 뒤에도 의미 상태가 안정되지 않으면 - compile error다. loop 밖에서 생성되어 loop 안에서 이후 사용되는 borrow는 필요하면 - loop 전체에 걸쳐 live로 보고, body에서 생성된 borrow가 backedge를 넘는 경우도 같은 - fixed-point에 포함한다. 첫 iteration만 안전하다는 이유로 허용하지 않는다. -9. R4 위반은 check 단계에서 타입만 보고 거부한다. 단 `static str`은 initializer가 문자열 리터럴인지 함께 확인한다. -10. R8 반환 경로마다 `Static`/`Param(N)` provenance를 계산하고 §5 R8 lattice로 합류한다. - 결과 바인딩을 허용하며 `Param(N)` 원본 대여를 결과의 마지막 사용까지 전파한다. - 메서드는 `Param(self)`만, 자유 함수는 유일한 참조성 parameter의 `Param(N)`만 허용하고 - provenance를 lowered signature/`.fei`에 기록한다. - -에러 메시지 형식: `file:line:col: error: <설명>` + 관련 위치 `file:line:col: note: <최초 이동/대여 위치>`. - -### 11.6 마일스톤 - -| # | 내용 | 완료 기준 | -|---|---|---| -| M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | -| M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | -| M3 | struct, enum, match, 배열, `[]T`/`[]mut T`, 경계 검사, `str` alias | 공유/배타 슬라이스와 문자열 처리 예제 통과 | -| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), handle enum `io.Writer`, 순수 `fmt.fmt_*` | `@print` 오류 삼킴, `@fprint` 전파, `@sprint` 잘림/길이와 인자 타입 진단, safe Writer dangling 불가 | -| M5 | `^T`/`^[]T`, drop, defer, 이동·부분 이동 검사 | 누수/이중해제, 소비 close, `mem.replace` 테스트 통과 | -| M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | -| M7 | `?T`, `E!T`, try/catch | `std.io` 유닛 동작 | -| M8 | 유닛/import/.fei, 의존 hash, `fe_errors.h`, 분리 컴파일, std 초안 | 다중 유닛 증분·결정적 빌드 | -| M9 | **제네릭** (통합 모노모피제이션) | `fe_generics.c`로 `List(T)`, `Map(K,V)` 중복 없이 빌드 | -| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn, `shared`/`atomic`/`critical`/`interrupt_safe` | QEMU FreeDOS에서 자동화된 far 포인터·인터럽트 공유 상태 테스트 통과(VGA 데모는 수동/멀티모달 검증 대상이라 완료 게이트에서 제외) | -| M11 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M10 테스트 통과 | -| M12 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | -| M13 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | -| M14 | 8086 네이티브 백엔드 | Watcom 없이 bits16 빌드 | - -배치 근거: -- **M4(포매팅)를 앞에 두는 이유**: 구현이 작고(check + lower 합쳐 300줄 안팎) 언어 표면에 새 개념을 추가하지 않는다. 이후 모든 마일스톤의 디버깅과 M11의 컴파일러 B 에러 출력이 여기에 의존한다. -- **M9(제네릭)가 M10보다 앞인 이유**: 제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화한다. -- **인터페이스(`dyn`)는 마일스톤에 없다**: 부트스트랩 경로에 불필요하고 타입 시스템 전반에 영향을 준다. §13의 v0.2 1순위로 미룬다. 그때까지 dangling이 불가능한 Copy handle enum `io.Writer`/`io.Reader`를 사용한다. - ---- - -## 12. 테스트 - -``` -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(필드에 공유/배타 slice, `*[]T`), R5(스코프 초과), R6(배타성 위반), R7(무효화·projection 부분 이동), R8(자유 함수 참조성 파라미터 2개, 메서드의 non-self 파생, 가변성 승격), R9(unsafe 밖 raw 역참조, `*void` 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `let`에서 mutable slice 생성. -- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `fmt_*` 없는 타입, 닫히지 않은 `{`, `try @print(...)`(void). -- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print` 오류 삼킴, `@sprint` 길이/잘림, handle enum `@fprint`, 동일 `fmt.fmt_*` 결과 재사용. -- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `line.trim()` 형태의 슬라이스 반환 연쇄. -- `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 - `core.Error` 등록, 결정적 `fe_errors.h`, `--strip-error-names`, `fmt.fmt_error`를 각각 pass/fail로 검증한다. -- **M6 필수 edge case**: - - 서로 다른 struct field의 `&mut`라도 같은 root borrow 충돌, 서로 다른 array index도 같은 root borrow 충돌. - - 호출 인자의 `&mut → &`/`[]mut → []` 약화 성공과 일반 `let` binding의 같은 암묵 약화 실패. - - R8 `Static`/`Param(N)` 합류 성공, 서로 다른 `Param` provenance 합류 실패. - - 한 branch에서만 live인 borrow의 보수적 합류, `Owned`/`Moved` 및 초기화 상태 합류. - - loop-carried borrow와 move가 2-pass fixed point에서 안정되는 경우와 불안정해 거부되는 경우. -- **M7 필수 edge case**: - - 문맥 없는 `null` 실패와 parameter/명시 타입으로 결정되는 contextual `null` 성공. - - error declaration의 code 0, 중복 member 이름, 중복 숫자 code 실패. - - `E!T` expected 위치의 success/failure contextual construction과 nominal error 자동 변환 실패. - - `orelse`, 짧은/block `catch`의 lazy side effect·move·borrow. - - non-Copy `Some` pattern이 destructive extraction이 아님을 확인하고 projection 소유 추출에는 `mem.replace`가 필요함을 검증. -- **M8 필수 edge case**: - - dotted unit/import와 alias, 마지막 segment binding, binding conflict. - - unit path/file suffix mismatch, uppercase segment와 8자 초과 segment 거부. - - 서로 다른 root의 동일 canonical unit ambiguity와 같은 canonical file 중복 발견의 dedup. - - reserved `std.*` lookup, 일반 user unit이 builtin std root에서 발견되지 않음. - - private type을 public API에 노출하는 경우 실패와 dotted prefix가 private 권한을 주지 않음. - - private-only non-generic 구현 변경 뒤 dependency interface hash 안정 및 dependent cache hit. - - source/interface graph와 target/model이 같을 때 build order, `-I` order, absolute checkout path가 달라도 byte-identical `.fei`. -- **M9 필수 edge case**: - - value generic과 generic type inference 거부, 명시 type argument 성공. - - alias/underlying type의 instance dedup과 동일 canonical instance body 1회 방출. - - exported generic이 definition-unit private helper/private generic을 호출하는 support closure. - - 같은 instance recursion의 pending 재사용과 distinct growing-instance chain depth 32 초과 실패. - - generic request 발견 순서와 build order가 달라도 byte-identical `fe_generics.c`. - - invalid dependent operation의 definition 위치 primary error와 `instantiated here` chain note. -- 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. -- 정식 회귀 실행은 QEMU FreeDOS 내부 Open Watcom의 `TEST-DOS.BAT`로 전 타깃·마일스톤 - gate를 확인한다. host compiler 결과는 편집 보조일 뿐 완료 판정에 사용하지 않는다. - ---- - -## 13. 의도적으로 제외한 기능 +## 11. 의도적으로 제외한 기능 **등급 정의** - `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다. @@ -1100,7 +764,7 @@ tests/ | 기능 | 등급 | 제외 이유 | 대체 수단 | |---|---|---|---| | 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) | -| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 M10 범위 밖 | bits16 실행 파일 또는 polling | +| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 아직 범위 밖 | bits16 실행 파일 또는 polling | | 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | | 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | @@ -1117,7 +781,7 @@ tests/ | 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | | 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | -### 13.1 인터페이스 설계 스케치 (v0.2 예정) +### 11.1 인터페이스 설계 스케치 (v0.2 예정) 지금 구현하지 않되, 나중에 `io.Writer` Copy handle enum을 무리 없이 대체할 수 있도록 방향만 고정해 둔다. @@ -1134,7 +798,7 @@ dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환 - `dyn I`의 표현은 `(ctx, vtable)` 팻 포인터. vtable은 `(인터페이스, 구현 타입)` 쌍마다 `static const` 하나. - **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. - `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). -- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립 마일스톤으로 다룬다. +- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립적으로 다룬다. - 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. 위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토.