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
10 KiB
10 KiB
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 블록을 구조체 초기화로 잘못 소비할 수 있었다. - 결정:
matchscrutinee 바로 뒤의{는 항상 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<N>/와 달랐다. 두 축을 모두 인정하도록 했다. R8 변경에 따라fail/목록의 "참조 반환 바인딩" 항목을 교체하고 R10 전역 대여,str변환 케이스를 추가했으며 R6·R8pass/케이스를 신설했다.
삭제
HANDOFF.md를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model, 링크는*.obj, M4 Watcom 테스트는-wx -wcd=202, 8.3 파일명 제약,D:에서 빌드 금지)은 별도 문서로 옮겨야 한다.