diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 09f4101..cb1c787 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -265,3 +265,295 @@ M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 - 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에 포함할 수 없다. diff --git a/SPEC.md b/SPEC.md index 80a869a..5941711 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.7 +# Ferro 언어 명세 v0.1.8 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -31,7 +31,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 - CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. -- 표준 라이브러리는 코어 공용, `sys` 유닛만 타깃별 구현. +- 표준 라이브러리는 코어 공용, `std.sys` 유닛만 타깃별 구현. --- @@ -95,7 +95,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. -- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 원래 대여 상태는 배타로 유지되며 다른 저장·대입에는 명시적 `as`가 필요하다. +- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. - `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 C 표현은 없다. @@ -144,10 +144,12 @@ let v = mem.replace(&mut p, null).?; // 소유값을 실제로 꺼냄 ``` - `?T`에서 T가 일반 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용한다. `?^[]T`는 빈 소유 버퍼와 null을 구별해야 하므로 `(bool, ^[]T)` 표현을 사용한다. +- `null`은 독립 runtime 타입이 없다. expected type으로 정확한 optional 또는 pointer-like 타입을 하나 결정할 수 있는 위치에서만 허용한다. `let p: ?^Node = null;`과 `takes_optional(null);`은 허용하지만 `let p = null;`처럼 문맥이 없거나 둘 이상의 타입으로 해석 가능한 경우는 컴파일 에러다. - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. - `.?`, `.field`, `[i]`는 place projection이다. projection chain은 값을 이동하지 않는다. Copy 값은 읽기에서 복사되며 비-Copy 값을 projection에서 꺼내는 것은 R7에 따라 금지한다. 실제 추출은 `mem.replace`를 사용한다. - `orelse`는 Copy payload를 복사한다. 비-Copy optional 변수 자체에 적용하면 optional 전체를 이동하며, field/index projection의 비-Copy optional에는 직접 적용할 수 없다. -- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. +- `orelse`는 optional이 `Some`이면 우변을 평가하지 않고, `None`일 때만 우변을 평가하는 lazy 연산이다. 부수 효과·이동·대여도 실행되는 경로에만 적용한다. +- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. 패턴은 place를 파괴적으로 추출하지 않는다. immutable place의 payload binding은 shared borrow/view, mutable place는 필요한 mutable borrow/view이고 Copy payload만 복사할 수 있다. non-Copy payload의 소유권을 꺼내려면 `mem.replace`가 필요하며 temporary optional만 자동 소유 추출하는 예외도 없다. ### 4.6 에러 @@ -165,15 +167,18 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 } ``` -- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. +- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약이라 사용할 수 없고, 한 선언 안에서 member 이름이나 숫자 code가 중복되면 컴파일 에러다. 서로 다른 nominal error 선언은 같은 숫자 code를 사용할 수 있지만 여전히 다른 타입이다. +- expected type이 `E!T`인 위치에서는 `T` 값은 success, `E` 값은 failure를 구성한다. 함수의 `return`도 선언된 반환 타입이 `E!T`이면 같은 규칙을 쓴다. 이는 일반 implicit conversion이 아니라 error-union 전용 contextual construction이며, `E1`과 `E2` 또는 nominal error와 `core.Error` 사이의 자동 변환은 없다. - `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. - `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. - `try e`: 에러면 현재 함수에서 즉시 반환한다. - `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. +- 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. +- `try`와 `catch`도 field/index/optional projection에서 non-Copy payload를 숨게 이동시키지 않는다. projection에서 소유값을 추출해야 하면 먼저 `mem.replace`로 유효한 대체값을 남긴다. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. -- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. +- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. @@ -183,7 +188,9 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이며 `switch` case 라벨로 쓸 수 있다. 이름 집합이 바뀌면 `fe_errors.h`와 그 헤더에 의존하는 오브젝트를 무효화하지만, -각 유닛의 `.c`는 재방출하지 않는다. `.fei`는 이름만 기록하므로 무효화되지 않는다. +각 유닛의 `.c`는 재방출하지 않는다. 각 `.fei`는 그 unit이 사용하는 이름 집합만 +기록하므로 global 번호 재배정 때문에 다시 쓰지 않으며, source에서 이름 사용 자체가 +바뀐 unit의 `.fei` interface만 갱신한다. 빌드 디렉터리 이력에 따라 번호가 달라지는 append-only 표는 금지한다. 유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 확정된 표를 받아야 하며, 없으면 컴파일 에러다. @@ -229,9 +236,23 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 **R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. +v0.1의 대여 상태는 **root local/parameter 단위**로 추적한다. projection은 place의 root를 찾는 데만 쓰며 field-sensitive/index-sensitive 독립성을 증명하지 않는다. 따라서 `&mut p.a`는 `p` 전체를 배타 대여하고 그동안 `p.b`의 읽기·쓰기·대여도 금지한다. `&mut xs[0]`과 `&mut xs[1]`도 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다. 이는 compiler A/B의 함수-local 상태 기계를 작고 결정적으로 유지하기 위한 v0.1의 의도적인 보수성이다. + +```fe +var p = Pair{ a: 1, b: 2 }; +let r = &mut p.a; +p.b = 3; // 에러: root p가 배타 대여 중 +r.^ = 4; + +let a = &mut xs[0]; +let b = &mut xs[1]; // 에러: 둘 다 root xs를 대여 +a.^ = 1; +b.^ = 2; +``` + 참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. `defer` 블록에서 사용한 참조와 그 원본의 대여는 해당 defer가 실행되는 스코프 끝까지 연장한다. -호출 인자 위치의 `&mut T → &T`, `[]mut T → []T` 약화는 새 공유 대여가 아니라 기존 배타 대여의 읽기 전용 재대여다. 호출이 끝날 때 재대여만 끝나며 원래 배타 대여의 생존 구간은 유지된다. +호출 인자 위치의 `&mut T → &T`, `[]mut T → []T` 약화는 새 장기 공유 대여가 아니라 기존 배타 대여의 읽기 전용 재대여다. callee를 평가한 뒤 해당 인자를 평가하는 시점부터 호출이 끝날 때까지만 임시 재대여가 존재하고, 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에서는 암묵 약화를 허용하지 않으므로 `let s: &i32 = m;`(`m: &mut i32`)은 컴파일 에러다. 별도의 lifetime/coercion 시스템은 두지 않는다. 이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. @@ -250,7 +271,14 @@ x += 1; // OK — 여기서 r의 대여는 이미 끝났다 **(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. -호출 지점에서 R8(a)의 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. +참조성 반환의 provenance는 인터페이스에서 다음 둘로 정규화한다. + +- `Static`: 문자열 리터럴 또는 `static`에서 파생되어 caller local borrow를 만들지 않는다. +- `Param(N)`: 시그니처로 정해진 하나의 참조성 parameter에서 파생된다. 메서드는 `Param(self)`만 허용하며 다른 참조성 인자에서 파생되면 에러다. 자유 함수는 기존 규칙대로 참조성 parameter가 정확히 하나여야 한다. + +control-flow 합류는 `Static + Static → Static`, `Static + Param(N) → Param(N)`, `Param(N) + Param(N) → Param(N)`이다. 서로 다른 `Param` provenance가 합류하면 컴파일 에러다. `?&T`/`?[]T`의 `null` 반환 경로는 caller borrow를 만들지 않는 경로이므로 static/null 경로와 `Param(N)` 경로가 합쳐지면 전체를 보수적으로 `Param(N)`으로 본다. 이 provenance는 함수 시그니처와 lowered `.fei` interface metadata에 기록할 수 있어야 한다. + +호출 지점에서 `Param(N)` 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. `Static` 결과는 caller local borrow를 만들지 않는다. ```fe let t = line.trim(); // 내장 alias 메서드 R8(a) // OK. line은 t의 대여 구간 동안 잠긴다 @@ -282,7 +310,7 @@ 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에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. **R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. @@ -293,8 +321,9 @@ fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통 ### 6.1 EBNF ``` -unit := 'unit' ident ';' import* decl* -import := 'import' ident ';' +unit := 'unit' unit_path ';' import* decl* +unit_path := ident ('.' ident)* +import := 'import' unit_path ['as' ident] ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl | const_decl | global_decl) | comptime_decl @@ -344,14 +373,15 @@ pattern := ident // 배리언트, 페이로드 없음 | 'Some' '(' ident ')' | 'None' | int_literal | char_literal | 'true' | 'false' | '_' -type := ident ['.' ident] - | '?' type | '!' type | ident '!' type +qualified_name := ident ('.' ident)* +type := qualified_name + | '?' type | '!' type | qualified_name '!' type | '^' type | '&' ['mut'] type | '*' type | 'far' ('^' | '*' | '&' ['mut']) type | 'far' 'fn' '(' [type (',' type)*] ')' ['->' type] | '[' expr ']' type | '[' ']' ['mut'] type | 'fn' '(' [type (',' type)*] ')' ['->' type] - | ident '(' type (',' type)* ')' // 제네릭 인스턴스 + | qualified_name '(' type (',' type)* ')' // 제네릭 인스턴스 catch_expr := expr 'catch' ['|' ident '|'] (expr | block) orelse_expr := expr 'orelse' expr @@ -359,6 +389,7 @@ orelse_expr := expr 'orelse' expr `member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. +`unit_path` segment의 lexical 제한과 source path 대응은 §8.1이 규정한다. ### 6.2 표현식 우선순위 (낮음 → 높음) @@ -383,6 +414,8 @@ orelse_expr := expr 'orelse' expr - `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. +- `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 + 평가하지 않는다(§4.5·§4.6). - `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. - `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다. - 비교 연산 체이닝 금지(`a < b < c`는 에러). @@ -416,7 +449,8 @@ orelse_expr := expr 'orelse' expr ```fe @print("x={} y={x} name={s}\n", a, b, s); ``` -→ lower 단계에서 개념적으로 다음처럼 전개한다: +→ lower 단계에서 개념적으로 다음처럼 전개한다. 아래 `io`와 `fmt`는 각각 canonical +`std.io`, `std.fmt` 유닛을 가리킨다. ``` io.write(out, "x="); var t1: [12]u8 = undefined; io.write(out, fmt.fmt_int_i32(t1[..], a)); @@ -432,7 +466,7 @@ io.write(out, " name="); io.write(out, s); io.write(out, "\n"); - verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. - `{}` 개수와 인자 개수 불일치 → 컴파일 에러. - 인자 타입에 대응하는 `fmt.fmt_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). -- 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.fmt_int_pad`를 직접 호출. +- 자릿수/폭/정렬 지정자는 v0.1에 없음. 필요하면 `fmt.fmt_int_pad`를 직접 호출. - `@fprint`의 첫 인자는 Copy 핸들 `io.Writer`(§10)다. - `@print`는 `io.Writer.Stdout`에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. 따라서 `try @print(...)`는 컴파일 에러다. @@ -450,7 +484,7 @@ io.write(out, " name="); io.write(out, s); io.write(out, "\n"); ```fe unit vga; -import sys; +import std.sys; const WIDTH: u16 = 320; const HEIGHT: u16 = 200; @@ -470,7 +504,7 @@ pub fn put_pixel(x: u16, y: u16, c: u8) { ```fe unit main; -import io; +import std.io; fn count_lines(path: str) -> !usize { var f = try io.open(path, io.Read); @@ -547,16 +581,150 @@ pub fn main() -> !void { - 함수의 `comptime` 파라미터는 §9 제네릭. - 재귀 평가 깊이 제한 256, 초과 시 에러. +### 7.6 표현식 평가 순서 + +- 일반 표현식의 평가 순서는 소스의 왼쪽에서 오른쪽이다. +- 함수·메서드 호출은 callee를 먼저 평가하고 인자를 소스 순서대로 왼쪽에서 오른쪽으로 평가한다. 자동 `self` projection도 callee 평가의 일부다. +- 이항 연산자는 왼쪽 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. 유닛과 빌드 -- 파일 하나 = 유닛 하나. 첫 줄은 `unit <이름>;`이며 파일명과 일치해야 함. -- `import bar;` → 같은 검색 경로의 `bar.fe`. 접근은 `bar.name`. -- `pub` 붙은 선언만 외부 노출. 구조체 필드도 개별 `pub` 필요. -- 순환 import 금지(에러). -- 유닛 컴파일 시 `.fei` 생성: pub 선언 시그니처, 타입 레이아웃, 제네릭 본문 토큰과 제네릭 전용 private 심볼 시그니처. 재컴파일 cache key는 해당 소스 해시뿐 아니라 직접·간접 의존 유닛의 `.fei` 해시를 포함한다. -- 검색 경로: `-I