Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/OPEN_QUESTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**로 보고한다 — "성공했는데 짧다"가 아니라 명시적 실패다.
Expand Down
5 changes: 3 additions & 2 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 러너. 이 저장소 안에서는 닫히지 않는다.
>
Expand Down
5 changes: 3 additions & 2 deletions docs/slices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<MODULE>_CONST_<NAME>`)와 지문에 나가, 번호를 바꾼 모듈을 게이트가 거부한다 | #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`)이고, 경위는 그
Expand Down Expand Up @@ -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 첫 절에 적은 채로 열렸다**.
Expand Down
169 changes: 169 additions & 0 deletions docs/slices/SPEC-checked-arithmetic.md
Original file line number Diff line number Diff line change
@@ -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 |
Loading