diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md index 6762820..7a9f2fe 100644 --- a/docs/OPEN_QUESTIONS.md +++ b/docs/OPEN_QUESTIONS.md @@ -54,6 +54,24 @@ ABI 정수 상태코드+out-param, 표면 Result는 sugar. 남은 하위 → Q13 우회한 규칙 포함) 이 표로 돌아온다. ✅ **2026-09-24 사용자 확인(Grok 동의): (A).** 결정이 아니라 **열어 두는 것**이므로 `DECISIONS.md`는 바뀌지 않는다. +- Q17. **입력 문자열의 인코딩을 계약으로 정할 것인가** — 닫힌 **DP-S2**(불투명 바이트, 인코딩 미정, + `SPEC-string-input.md`)를 다시 열지의 질문이다. **DP-S2의 기록된 재개 조건은 *대소문자 무시·정규화*였고, 이번 + 근거는 그 조건이 아니다** — 그래서 재개 자체가 먼저 확인받을 일이다. + - **근거(2026-09-26, §9-69.3):** 한국어 ANSI 기계(`GetACP() = 949`)의 ctypes 호스트로 CP949 바이트를 넣어 쟀다 — + `byte_len`으로 쓴 길이 제한은 같은 이름이 UTF-8 호스트에서 거부되고 CP949 호스트에서 통과하며(가×34: 102 대 68), + 인사말에 이은 이름은 모지바케가 된다. 모듈 쪽 결과는 E2이고 PAnsiChar 변환 단계는 E1이다. 생성 `.pas`가 + 권하는 `PAnsiChar(AnsiString(S))`의 "CORRECT"는 ASCII 값("UPSN")으로만 잰 것이다(`HOST_ABI.md`). + - **반례도 같은 표본에서 나왔다:** SMS·LMS 경계처럼 **EUC-KR 바이트로 정의된 규칙**은 *UTF-8 전용* 계약이면 + 모든 준수 호스트에서 틀린다. + + | 안 | 장점 | 단점 | + |---|---|---| + | **(A) 열어 둔다 — DP-S2 그대로, 위험만 문서에 (권고)** | 결정을 뒤집지 않는다. 바이트 단위 규칙(EUC-KR 포함)이 호스트 선택으로 계속 가능하다 | 같은 규칙이 호스트 코드 페이지에 따라 다른 status 0 답을 낸다 — 저자와 호스트가 문서를 읽어야만 피한다 | + | (B) UTF-8 전용 계약 — 헤더·`.pas`가 말하고, `.pas`는 `UTF8String`을 권한다 | 소스 리터럴(UTF-8)과 입력이 같은 바이트가 된다. 비ASCII 비교(DP-U1)와 문자 단위 길이를 열 길이 생긴다 | EUC-KR 바이트 규칙이 전부 틀린다. Delphi 호스트가 변환해야 한다. `.pas` 권고가 바뀐다 | + | (C) 파라미터별 코드 페이지 선언 | 두 부류의 규칙을 다 쓸 수 있다 | 표면이 는다. 혼합 바이트(CP949 이름에 UTF-8 리터럴을 잇기)는 모듈 쪽 변환 없이 여전히 틀린다 — 할당자가 없다 | + + **권고 (A) — 재검토 조건**: 비ASCII **비교**나 **문자 단위 길이**가 필요한 슬라이스를 고를 때 이 표로 돌아온다. + ⏳ **사용자 확인 대기(2026-09-26).** - Q12. 반환 값 소유권 규약 — **닫힘(2026-08-31)**: 사용자 확정으로 **(a) caller-allocates out-buffer**. 가변 길이 데이터는 **호스트가 버퍼와 용량을 주고, 모듈이 채우고, 필요한 길이를 돌려준다**. 절단은 **D17의 음수 예약 status**로 보고한다 — "성공했는데 짧다"가 아니라 명시적 실패다. diff --git a/docs/STATUS.md b/docs/STATUS.md index 3ac0404..04d614a 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -231,8 +231,9 @@ > > **▶ 다음 세션 — 여기서 시작한다.** 순서대로: > -> 1. **다음 슬라이스 — 사용자 결정.** 먼저 §9-69.2의 결정표(체크드 산술: 기본값 A/A2 · 옵트인 · i64 · 문서만, 그리고 -> `as`·status 값·`result` 뒤·이주)와 §9-69.3의 질문(닫힌 DP-S2를 재개할지)이다. 고르면 SPEC → 확인 → 구현. +> 1. **체크드 정수 산술 SPEC 확인 — 사용자 결정.** 방향(A+as)은 확인받았다(2026-09-26). 초안은 +> `docs/slices/SPEC-checked-arithmetic.md`이고 §4의 DP-O3~O9가 확인을 기다린다. 확인되면 §6의 두 PR로 구현한다. +> 같은 날 인코딩 질문은 `OPEN_QUESTIONS.md` **Q17**로 올렸다(권고 (A) — 사용자 확인 대기). > 2. **작은 항목**: C# 호스트(Phase 4) · LSP(Phase 3, 큰 일). > 3. **외부**: `X1`·`X2` — Delphi가 있는 CI 러너. 이 저장소 안에서는 닫히지 않는다. > diff --git a/docs/slices/README.md b/docs/slices/README.md index e3f9c51..776c2be 100644 --- a/docs/slices/README.md +++ b/docs/slices/README.md @@ -62,6 +62,7 @@ TDD 구현 → Grok 검증 → PR** 순서를 따른다(`CLAUDE.md` "개발 방 | [고정 소수 포맷 `fixed(x, places)`](SPEC-fixed-decimals.md) | ✅ **구현 완료** (2026-09-15) | `fixed(x, places)` — 고정 소수 십진 표기. 모듈 안 우회가 조용히 틀리던 것(`1234.05` → `"1234.5"`)을 막는다 | §7 §9-53 / #243 | | [나눗셈 가드가 왼쪽 피연산자를 건너뛴다](SPEC-division-guard-operands.md) | ✅ **구현 완료** (2026-09-23) | 제수가 0이면 `/`·`%`의 왼쪽 피연산자(배열 경계 검사)가 실행되지 않던 결함 — 왼쪽을 먼저 묶어 평가한다 | #287 / #291 | | [상수 선언 `const` · `export const`](SPEC-constants.md) | ✅ **구현 완료** (2026-09-26) | 이름 붙은 리터럴 — `export const`만 헤더(`ML__CONST_`)와 지문에 나가, 번호를 바꾼 모듈을 게이트가 거부한다 | #325 / #326·#327 | +| [체크드 정수 산술](SPEC-checked-arithmetic.md) | 🔨 **SPEC 초안 · 확인 대기** (2026-09-26) | `!` 본문의 i32 오버플로와 `f64 as i32`가 `-3`으로 실패한다 — 방향(A+as)은 확인받았고 하위 결정 DP-O3~O9가 남았다 | §9-69 / — | > **2026-09-25에 행을 한 줄로 접었다.** 접기 전의 서술은 [`../history/slices-index.md`](../history/slices-index.md)에 > 바이트 그대로 있다. **새 행도 한 줄로 쓴다** — 행당 400 B(`every_slice_index_row_is_one_line`)이고, 경위는 그 @@ -124,8 +125,8 @@ TDD 구현 → Grok 검증 → PR** 순서를 따른다(`CLAUDE.md` "개발 방 두 번째 호스트(C#, ROADMAP Phase 4), 그리고 ROADMAP Phase 2가 이름만 두고 설계가 없던 둘 — **인터페이스 정의 파일**(설계부터)과 **개발/배포 빌드 분리**(D13, `SECURITY.md` P1). *(2026-09-25 로드맵 검토에서 더했다 — Phase 2의 다섯 항목 중 이 둘이 이 목록에 없었다.)* -§7(§9-69, 12개 도메인 72규칙과 귀속 대조)이 더한 후보: **체크드 정수 산술**(가장 강한 근거 — 기본값이냐 옵트인이냐를 -먼저 정해야 한다, §9-69.2), **i64**, **입력 문자열의 인코딩 계약**(닫힌 DP-S2의 재개 — 설계 질문, §9-69.3), 비트 연산자, +§7(§9-69, 12개 도메인 72규칙과 귀속 대조)이 더한 후보: **체크드 정수 산술**(가장 강한 근거 — 방향 A+as를 확인받아 +SPEC 초안이 있다, 위 색인), **i64**, **입력 문자열의 인코딩 계약**(닫힌 DP-S2의 재개 — 설계 질문, §9-69.3), 비트 연산자, 지역 작업 배열, 수학 내장 함수, 문자 단위 길이. **struct는 §9-1이 강제되지 않는다고 결론냈다** — 고르려면 그 절을 먼저 읽는다(§9-69도 귀속 0으로 다시 확인했다). 배열도 같은 결론이었고(§9-9가 다섯 번째로), **입력은 그 사실을 SPEC 첫 절에 적은 채로 열렸다**. diff --git a/docs/slices/SPEC-checked-arithmetic.md b/docs/slices/SPEC-checked-arithmetic.md new file mode 100644 index 0000000..da59681 --- /dev/null +++ b/docs/slices/SPEC-checked-arithmetic.md @@ -0,0 +1,169 @@ +# SPEC — 체크드 정수 산술 (`!` 본문) + +- **상태: 초안. 사용자 확인 전이며 제품 코드는 한 줄도 쓰지 않았다.** + - **방향은 확인받았다(2026-09-26)**: 설계 **A+as** — `!` 함수 본문의 i32 산술과 `f64 as i32`가 넘치면 + 예약 음수 status로 실패한다(§9-69.2의 권장안). 이 문서가 묻는 것은 **§4의 하위 결정**이다. +- 선행: `SPEC-i32.md`(DP-I4 — `+ - *` wrap) · `SPEC-unary.md`(DP-U4 — `-i32::MIN` wrap) · + `SPEC-i32-division.md`(D2 행 — `i32::MIN / -1` wrap) · `SPEC-numeric-conversion.md`(§2.3 — `f64 as i32` 포화·NaN→0) · + `SPEC-array-return.md`(DP-R9 — `result` 뒤의 실패) · `SPEC-fixed-decimals.md`(DP-M4 — `-2` 재사용 선례) +- 관련 결정: **D17**(음수 status는 런타임·ABI 예약) · **D18**(ABI 버전 심볼) +- 발단: **§7 열두 도메인 + 설계 A 시험 구현**(`STATUS.md` §9-69) + +--- + +## 0. 왜 — 측정부터 + +### 0.1 가장 넓은 조용한 오답 (2026-09-26, E2) + +§7 2라운드(72규칙, 통일 기준 + 귀속 대조)에서 i32 wrap이 **7개 도메인**에서 status 0의 그럴듯한 값을 냈다: + +| 규칙 | 입력 | 나온 값 | 맞는 값 | +|---|---|---|---| +| 근로소득공제 상한 | 총급여 20억 | 9,800,328 | 20,000,000 | +| VIP 10% 할인 | 단가 3억 × 1 | 312,949,672 | 270,000,000 | +| 일당(시급 × 분) | 시급 1,000만 | 8,417,211 | 80,000,000 | +| 수도 요금 누진 | 5,500,000 m³ | 50,021,704 | (i32 밖 — 실패여야) | + +귀속 대조는 **기본으로 켜진 체크드**일 때만 통과했다 — 옵트인 도구(체크드 내장 함수)라면 저자가 가드를 +빠뜨리듯 옵트인도 빠뜨린다. + +### 0.2 시험 구현으로 잰 것 (E2, 로컬 브랜치 `exp/checked-arith-local`, 머지 안 함) + +| 626행(규칙 × 입력) | A | **A + `as i32`** | +|---|---|---| +| 조용한 오답 161행 중 status가 됨 | 84 | **101** | +| 여전히 조용함 | 77 | 60 | +| 맞던 답이 실패가 됨(오탐) | 8 — 현실적 입력은 1 | 9 | +| 참값이 i32에 드는데 실패가 됨 | 41 | 43 | +| 기존 오라클 테스트의 **동작** 변화 | 0 | 0 | + +남는 60행: 무한(`-> i32`) 헬퍼 16 · 무한 export 본문 4 · f64 절삭·NaN 14 · 산술이 아닌 것 25 · 하류 저장 상태 1. + +### 0.3 정직한 반론 (기록) + +- **틀린 답이 정답이 되지는 않는다.** 43행은 참값이 i32에 드는데도 실패가 된다(중간값만 넘친다). 이 슬라이스의 + 입장은 *"그럴듯한 오답보다 실패"* 이고, 정답을 주는 것은 i64뿐이다 — 별도 후보로 남긴다. +- **같은 `a * b`가 시그니처에 따라 다르게 동작한다** — `!` 본문에서는 실패하고, 무한 함수에서는 wrap한다. + 무한 함수에는 실패를 알릴 채널이 없다(D17). 그 대가를 숨기지 않고 `LANGUAGE.md`에 적는다. +- **같은 소스를 다시 컴파일하면 답이 바뀐다**(wrap 값 → `-3`). 지문은 바뀌지 않는다 — §2.6. + +--- + +## 1. 범위 + +**포함** — 실패 가능 함수(`-> T!` · `-> string!` · `-> [T]!`)의 **본문 안에서**: + +1. i32 `+` `-` `*`, 단항 `-`, `/`의 `i32::MIN / -1`이 i32를 넘으면 **`ML_ST_OVERFLOW`(`-3`)** 로 실패한다. +2. `f64 as i32`가 NaN이거나 0 방향 절단 뒤 i32 밖이면 같은 status로 실패한다. +3. 그 status를 낼 수 있는 모듈의 `.h`·`.pas`가 `ML_ST_OVERFLOW`를 선언한다(`-2`와 같은 방식). +4. `HOST_ABI.md` status 표 · `LANGUAGE.md`의 wrap 문단과 공백 블록 · 수용 D(C 호스트). + +**미포함 (명시)** + +- 무한 함수(`!` 없음)의 산술과 변환 — **wrap·포화 그대로**(DP-I4 · §2.3이 그 범위에서는 유지된다). +- `!`를 강제하는 설계(A2) · i64 · 옵트인 체크드 내장 함수 · 상수식 오버플로의 컴파일 진단. +- `%`(§2.2) · `/ 0`(DP-N4 그대로 `0`) · f64 산술의 `inf`/NaN · `fixed`의 `-2`. +- `DECISIONS.md` — D17이 음수를 이미 예약했다. + +--- + +## 2. 계약 + +### 2.1 어디서, 무엇이 + +| 연산 | `!` 본문 | 무한 함수 | +|---|---|---| +| i32 `+ - *`, 단항 `-` | 넘치면 **`-3`** | wrap(오늘과 같다) | +| `i32::MIN / -1` | **`-3`**(참 몫 2^31이 i32 밖) | wrap | +| `x / 0`, `x % 0` | `0`(DP-N4) | `0` | +| `i32::MIN % -1` | **`0`** — 참 나머지이고 i32 안이다(DP-O5) | `0` | +| `f64 as i32` | NaN · 절단 뒤 범위 밖이면 **`-3`**, 아니면 절단 | 절단 + 포화, NaN → 0 | + +**피연산자는 왼쪽부터 평가되고**(LANGUAGE.md) 검사는 그 연산 자리에서 한다 — 먼저 평가된 부분식의 +효과(배열 경계 검사의 `-2` 등)가 먼저다. + +**리터럴·상수만으로 된 식도 같다**(DP-O9): `!` 본문의 `2147483647 + 1`은 컴파일 시점에 접히지 않고 **실행할 때 +`-3`** 이다. 상수는 리터럴로 접히지만(SPEC-constants DP-K5) 그 뒤의 산술은 다른 식과 똑같이 검사된다. 그런 식을 +컴파일 에러로 만드는 진단은 §5의 별도 후보다. + +### 2.2 `%`를 넣지 않는 이유 (DP-O5) + +Rust의 `checked_rem`은 `i32::MIN % -1`에서 `None`을 준다(나눗셈이 넘치므로). 그러나 **참 나머지는 `0`이고 i32에 +든다.** 시험 구현은 `checked_rem`을 썼고 그것은 맞는 답을 실패로 바꾼다 — `%`는 `wrapping_rem` 그대로 둔다. + +### 2.3 status — `ML_ST_OVERFLOW = -3` (DP-O3) + +*"이 연산의 i32 결과(또는 `f64 as i32`의 값)가 없다 — NaN 포함."* 뜻은 **값의 부재**이고, `-2`(경계 밖 접근)와 +다르다. 호스트 규칙은 그대로다: **`status != 0`이면 out·버퍼를 읽지 않는다**(DP-E3). 재시도 조건은 +여전히 `status == ML_ST_INSUFFICIENT_BUFFER`뿐이다 — `-3`에 다시 불러도 같은 답이 온다. + +### 2.4 `result` 뒤 (DP-O6) + +`-> [T]!`에서 `result n` 뒤에 넘치면 `-2`와 **똑같이** 다룬다: 그 시점에 `*ml_needed`가 쓰였을 수 있고 버퍼 내용은 +정의되지 않는다(`HOST_ABI.md` Q12 표의 "그 밖의 예약 음수" 행이 이미 그렇게 말한다). `result` 뒤의 **정적** +실패(`fail`·`try`) 거부(DP-R9)는 그대로다 — 오버플로는 입력에 달린 동적 실패이고, 범위 밖 인덱스와 같은 부류다. + +### 2.5 바인딩 + +- `compiler/src/abi.rs`에 `ML_ST_OVERFLOW: i32 = -3`. +- **선언은 낼 수 있는 모듈에만**: `ir.rs`에 `can_fail_overflow`(실패 가능 본문에 i32 `+ - * /`·단항 `-`·`f64 as i32`가 + 있는가)를 두고, `.h`는 `#ifndef ML_ST_OVERFLOW` 가드로(`-2`와 같은 이유 — 여러 헤더가 같은 정의를 낸다), `.pas`는 + `const`로 선언한다. **두 바인딩이 한 술어를 읽는다**(#238의 규칙). +- 생성 `.pas`의 문구는 그 status를 한 줄로 설명한다. + +### 2.6 지문과 ABI 버전 — 바뀌지 않는다 (DP-O4) + +예약 음수는 D17상 **ABI의 것**이고, 호스트는 모르는 음수를 *"이 호스트보다 새로운 이유의 실패"* 로 다뤄야 한다 +(`runtime/ml_abi.h`: `-2 .. -255`는 예약). 지문은 본문이 어느 예약 음수에 닿는지 보지 않는다(`iface.rs`). 그래서 +`ML_MODULE_ABI_VERSION`도 매니페스트도 바꾸지 않는다 — 기존 모듈 전부의 지문이 그대로다. + +--- + +## 3. 수용 기준 (측정 가능) + +| # | 무엇 | 어떻게 잰다 | +|---|---|---| +| **A** | §2.1 표의 `!` 열 — 연산마다 경계 바로 안은 정답, 바로 밖은 `-3`. 스칼라·`string!`·`[T]!` 세 모양 모두 | 로드한 모듈의 반환값(오라클) | +| **B** | `i32::MIN % -1 = 0`, `i32::MIN / -1 = -3`, `x / 0 = 0` | 오라클 | +| **C** | 무한 함수는 그대로 — 같은 식이 wrap·포화(기존 `i32_type`·`numeric_conversion` 테스트가 수정 없이 초록) | 기존 테스트 | +| **D** | `f64 as i32` — NaN · ±inf · 3e9 · -3e9 → `-3`, 3.9 → 3, -3.9 → -3(`!` 본문) | 오라클 | +| **E** | `ML_ST_OVERFLOW`를 술어가 참인 모듈만 선언(`.h`·`.pas` 한 술어), `cl /W4 /WX`·FPC 컴파일, **실제 C 호스트가 `-3`을 받는다** | 방출 텍스트 + 헤더/유닛 게이트 + 수용 D | +| **F** | 예제 전부의 지문 불변 · ABI 버전 불변 | 골든의 `IFACE_HASH` | +| **G** | `result` 뒤 오버플로가 `-3`이고 재호출이 같은 답(수렴하지 않는 재시도 없음) | 오라클 | +| **H** | 문서 — `LANGUAGE.md` wrap 문단·공백 블록(`language_gaps.rs`), `HOST_ABI.md` status 표 | `doc_claims` · `language_gaps` | +| **I** | §9-69의 161행을 구현본으로 다시 재어 시험 구현과 같은 결과(차이는 §2.2의 `%`뿐이어야 한다) | 측정 기록(가드 아님) | + +--- + +## 4. 설계 결정 (확인 요청) + +| DP | 결정 | 대안 | 근거 | +|---|---|---|---| +| **DP-O1** 범위 | `!` 본문만(A) | 산술이 있으면 `!` 강제(A2) | **사용자 확인**(2026-09-26, A+as). A2는 export의 C 시그니처를 바꾸고, `/0`에서 같은 모양이 기각됐다(SPEC-division-guard-operands) | +| **DP-O2** 연산 | `+ - *`, 단항 `-`, `MIN/-1`, **`f64 as i32` 포함** | `as` 제외(A) | **사용자 확인**(2026-09-26, A+as). 실측 101 대 84 | +| **DP-O3** status | **새 `-3` `ML_ST_OVERFLOW`** | `-2` 재사용(`fixed`의 DP-M4처럼) | 호스트가 경계 밖과 값 부재를 구별한다. Grok plan은 처음 **`-2`**를 골랐다(#238이 잰 비용) — 그런데 `-2`를 재사용해도 `-2` 선언 술어가 산술을 봐야 해서 **그 비용 대부분이 두 안에 같다**(Grok verify CONFIRMED). 남는 차이는 상수 하나다 | +| **DP-O4** 지문·ABI | **둘 다 불변** | 매니페스트 플래그 · ABI 버전 올림 | 예약 음수는 ABI의 것(D17, `ml_abi.h`). 올리면 모든 모듈이 거짓 거부된다. Grok plan: 불변 | +| **DP-O5** `%` | **`wrapping_rem` 유지** — `MIN % -1 = 0` | `checked_rem` → `-3` | 참 나머지 0이 i32에 든다. 시험 구현의 `checked_rem`은 맞는 답을 실패로 바꿨다. Grok plan: 유지 | +| **DP-O6** `result` 뒤 | `-2`와 같다 — 버퍼 정의 안 됨 | 새 거부 규칙 | Q12 표의 기존 행이 이미 덮는다. DP-R9(정적 실패 거부)는 그대로 | +| **DP-O7** 탈출구 | **새로 두지 않는다** — wrap이 필요하면 무한 헬퍼에 둔다 | `wrapping_*` 내장 | 기존 코퍼스에서 `!` 본문의 wrap에 기대는 동작은 0개(실측). 무한 헬퍼가 이미 명시적 탈출구다 | +| **DP-O8** 이주 | 표시 없이 바꾼다 — 문서에 적는다 | 매니페스트 표시 | DP-O4와 같은 근거. 1.0 전이다 | +| **DP-O9** 리터럴·상수식 | **실행 시 `-3`** — 접지 않는다 | 컴파일 에러(상수식 진단) | 규칙이 하나로 남는다. 진단은 가산적으로 나중에 얹을 수 있다(§5) | + +--- + +## 5. 범위 밖 / 미검증 (명시) + +- **무한 함수의 오버플로**(남는 20행) — 채널이 없다. `!`를 붙이면 이 슬라이스가 덮는다는 것이 저자의 탈출구다. +- **i64**(정답을 주는 유일한 안), **옵트인 내장 함수**, **상수식 오버플로 진단** — 별도 후보. +- **성능·`.text` 크기** — 체크 분기가 는다. `protection.rs`의 크기 가드는 `discount.dll`(무한 함수만)을 재므로 영향이 + 없을 것으로 본다(E0). 구현에서 오버플로가 있는 모듈의 크기 변화를 **재서 적는다**(가드로 만들지는 않는다). +- `runtime/ml_abi.h`는 `-1`만 정의하고 `-2`도 생성 헤더에만 있다 — 이 슬라이스는 그 배치를 따른다. + +--- + +## 6. 작업 분해 (확인 후) + +| 작업 | 수용 | 크기 | +|---|---|---| +| 1. codegen(연산 넷 + 변환, 모양별 실패 방출) · `abi.rs` · `ir.rs` 술어 · `.h`/`.pas` 선언 · 골든 · 컴파일러 테스트 | A · B · C · D · F · G(텍스트) | 한 PR | +| 2. 오라클 값 · C 호스트 수용 D · 문서 · §9-69의 161행 재측정 | A · D · E · G · H · I | 한 PR |