decide(bind-system): PreRef is single-use, no cancel concept, reuse errors

PreRef never enters the normal retract dispatch chain (consumed via None
in the pre-pass), so "cancel" was never structurally possible. The real
hazard is reuse across constructions (stale .Value silently firing
callbacks) — guard it with an explicit error on re-fire instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-12 13:26:46 +09:00
parent e6880e518c
commit cdbbdba4fb
Signed by: qwreey
GPG key ID: D28DB79297A214BD
4 changed files with 120 additions and 14 deletions

View file

@ -1148,6 +1148,37 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
Handler가 실제로 Handler가 실제로
매치되는 경우는 오직 "타입이 막았어야 했는데 어떻게든 동적으로 매치되는 경우는 오직 "타입이 막았어야 했는데 어떻게든 동적으로
새어들어온" 버그 케이스뿐 — 그래서 no-op이 아니라 즉시 `error`. 새어들어온" 버그 케이스뿐 — 그래서 no-op이 아니라 즉시 `error`.
- **PreRef는 "취소"라는 개념이 없다 — 1회용, 재사용은 즉시 error
(2026-08-12 여섯 번째 세션, 사용자 제안 채택).** `Ref`가 "다른 값으로
교체되면 `retract`로 취소됨"이라는 의미의 취소를 가질 수 있는 건 정상
우선순위 스캔의 `(inst,k)` 디스패치 체인에 실제로 참여해서임 —
`Dispatch.retractUnder`가 그 체인을 대상으로 동작함. `PreRef`는 애초에
그 체인에 올라간 적이 없음(pre-pass에서 fire와 동시에 `None`으로
소진되고 정상 두 패스는 건드리지 않음, 위 "호이스팅의 실제 구현" 절) —
그래서 "취소 가능 여부" 자체가 성립할 토대가 없었던 게 구조적으로
이미 사실이었음, 이번 세션은 그걸 명문화한 것뿐. 진짜 위험은 취소가
아니라 **재사용**: 이미 한 번 fire된 `PreRef` 객체를 두 번째
construction의 children 배열에 다시 놓으면, 거기서 등록하는
`:Callback(fn)`이 "이미 채워져 있으면 즉시 1회 호출"이라는 규칙(위
"Ref 일반화" 절) 때문에 **의도한 새 인스턴스가 아니라 첫 번째 fire
때 남은 stale `.Value`로 조용히 호출**됨 — 에러도 안 나고 엉뚱한
값을 들고 실행되는, 디버깅하기 아주 어려운 버그. `State`/`:With`를
"clone 빌더가 아니라 매번 새 노드"로 확정했던 원칙(2026-08-07 세
번째 세션, "`:With`도 새 State 노드")과 같은 클래스의 문제이자 같은
해법.
- **구현**: pre-pass가 첫 fire 때 해당 `PreRef` 객체에 내부 플래그
(`_fired = true`)를 세팅. pre-pass가 배열을 훑다 `isPreRef(v)`
슬롯을 만났는데 그 객체가 이미 `_fired`면, fire하지 않고 그 자리에서
즉시 `error("PreRef는 1회용 — 이미 다른 construction에 쓰인
PreRef를 재사용할 수 없음, 매번 새로 만들 것")`. 위 "동적 경로 가드"
Handler(정상 두 패스에서 매치)와는 별개 코드 경로 — 이 가드는
pre-pass 자신 안에, `_fired`가 아닌 정상 fire는 그대로 통과.
- **관용구**: `Slot:List``updateFn`처럼 반복 호출되는 자리에서
`PreRef`가 필요하면 **호출마다 새 `PreRef()`를 만들 것** — 클로저에
캡처해 여러 construction에 걸쳐 재사용하지 말 것. (참고: `Slot`
자체는 요소 타입으로 `Ref`/`PreRef`를 이미 금지하고 있어(위
"요소 타입 제약" 절, `slot-plan.md`) 이 관용구가 실제로 문제되는
자리는 `updateFn` 안에서 호출하는 컴포넌트 함수 내부뿐임.)
- **일반 `Ref`는 계속 Modifier/Store 어디든 자유롭게 - **일반 `Ref`는 계속 Modifier/Store 어디든 자유롭게
들어감** — Store를 통해 나중에 도착하는 Ref는 그냥 도착한 그 순간 들어감** — Store를 통해 나중에 도착하는 Ref는 그냥 도착한 그 순간
처리하면 됨, phase 개념 자체가 필요 없음("만난 순간 처리"로 충분). 처리하면 됨, phase 개념 자체가 필요 없음("만난 순간 처리"로 충분).

View file

@ -207,11 +207,15 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
값으로 교체되면 `retract`로 취소됨)에 가깝고, `PreRef`는 그 스캔 밖의 값으로 교체되면 `retract`로 취소됨)에 가깝고, `PreRef`는 그 스캔 밖의
고정 pre-pass라는 의미에서 "pre-hook"(항상 최우선 고정, 순서/취소 고정 pre-pass라는 의미에서 "pre-hook"(항상 최우선 고정, 순서/취소
개념 자체가 다름)에 가깝다는 구분 — quadnomicon 에세이로 쓸 때 이 개념 자체가 다름)에 가깝다는 구분 — quadnomicon 에세이로 쓸 때 이
"hook"/"pre-hook" 용어 자체를 채택할지부터 먼저 확인 필요(복수 `PreRef` "hook"/"pre-hook" 용어 자체를 채택할지만 아직 열려있음(복수 `PreRef`
간 순서는 2026-08-07 아홉 번째 세션에서 해소됨 — 배열 index 순서 간 순서는 2026-08-07 아홉 번째 세션에서 해소됨 — 배열 index 순서
그대로, 별도 규칙 없음, `bind-system-plan.md` "PreRef" 절 참고. 취소 그대로, 별도 규칙 없음, `bind-system-plan.md` "PreRef" 절 참고).
가능 여부는 여전히 미정 — PreRef는 fire와 동시에 소진되는 1회성 **[해소됨, 2026-08-12 여섯 번째 세션]** 취소 가능 여부 — PreRef는
pre-pass 참가자라 "취소"라는 개념 자체가 성립하는지부터 다시 볼 것). 구조적으로 `retract` 체인에 아예 안 올라가므로 취소 개념 자체가 없고,
대신 이미 fire된 PreRef를 재사용하면(두 번째 construction에 다시
놓으면) 즉시 `error` — "1회용, 재할당 불가"로 확정. `bind-system-plan.md`
"동적 경로로 도착한 PreRef는 런타임에도 명시적으로 에러" 절 바로 아래
"PreRef는 '취소'라는 개념이 없다" 항목 참고.
7. **왜 `Compute(fn, ...)`는 여러 의존성을 편하게 받고 `Effect`/`Observer`는 7. **왜 `Compute(fn, ...)`는 여러 의존성을 편하게 받고 `Effect`/`Observer`는
안 받는가** (2026-08-11 세션 원자료, `bind-system-plan.md` "`:Compute(fn, 안 받는가** (2026-08-11 세션 원자료, `bind-system-plan.md` "`:Compute(fn,
@ -259,17 +263,18 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
완전히 확정(2026-08-09 열한 번째 세션엔 `Extract(index, newElement?)` 완전히 확정(2026-08-09 열한 번째 세션엔 `Extract(index, newElement?)`
더 확장). `research/additional-primitives-plan.md`는 더 이상 열린 더 확장). `research/additional-primitives-plan.md`는 더 이상 열린
항목 없음, 배경 자료로만 유지. 항목 없음, 배경 자료로만 유지.
- **"hook"/"pre-hook" 용어 채택 여부 + `PreRef`의 취소 가능성** (2026-08-07, - **"hook"/"pre-hook" 용어 채택 여부** (2026-08-07, 위 심화 후보 6번 참고)
위 심화 후보 6번 참고)`bind-system-plan.md``PreRef`가 위치 무관 `bind-system-plan.md``PreRef`가 위치 무관 호이스팅이라는 것과 일반
호이스팅이라는 것과 일반 `Ref`가 우선순위 스캔에 참여한다는 것까지는 `Ref`가 우선순위 스캔에 참여한다는 것까지는 확정해뒀고(복수 `PreRef`
확정해뒀고(복수 `PreRef` 간 순서=배열 index 순서, 동적 경로로 도착한 간 순서=배열 index 순서, 동적 경로로 도착한 PreRef는 전용 Handler가
PreRef는 전용 Handler가 즉시 error — 둘 다 아홉 번째 세션에서 추가 즉시 error — 둘 다 아홉 번째 세션에서 추가 확정), **`PreRef`의 취소
확정), "hook 대 pre-hook"이라는 용어 자체를 문서화 시 채택할지와 가능성은 2026-08-12 여섯 번째 세션에 "취소 개념 없음, 재사용은 error"로
`PreRef`의 취소 가능성(애초에 fire와 동시에 소진되는 1회성이라 해소됨**(위 심화 후보 6번, `bind-system-plan.md` 참고) — "hook 대
"취소"가 의미 있는 개념인지부터)만 아직 미정. pre-hook"이라는 용어 자체를 문서화 시 채택할지만 아직 미정.
이 항목들은 `.claude/question.md`에도 이미 열린 질문으로 잡혀있음 — 여기선 `PreRef` 취소 가능성 항목은 실제로는 `.claude/question.md`에 별도로
"확정 전엔 문서화 대상 아님"이라는 표시만 겸함. 잡혀있던 적이 없었음(이전 서술이 부정확했음, 이번에 확인·수정) — 순수
용어 선택인 "hook"/"pre-hook" 채택 여부만 여기서 계속 추적.
## 다음 단계 ## 다음 단계

View file

@ -0,0 +1,59 @@
# 2026-08-12 일곱 번째 세션 — PreRef는 1회용, 취소 개념 없음, 재사용은 error
## 배경
이전 세션(대화 초반)에서 사용자가 "지금 확정이긴 한데 다른 의견 나오면
뒤집힐 만한 부분이 뭐가 있을지" 물어봐서 서브에이전트로 `.claude/`
전체를 감사했음. 그 결과 중 하나가 `research/documentation-content-map.md`
262-269행에 **명시적으로 미정으로 표시돼 있던 항목**: `PreRef`의 취소
가능성 — "PreRef는 fire와 동시에 소진되는 1회성 pre-pass 참가자라
'취소'라는 개념 자체가 성립하는지부터 다시 볼 것"이라고만 적혀 있고
실제 결론은 없었음.
## 논의
사용자 제안: PreRef는 취소 불가능하게 두는 게 나을 듯 — 동적으로 쓰일
수 있는 예외적 경우(Slot의 `updateFn`처럼 반복 호출되는 자리에서
컴포넌트가 내부적으로 PreRef를 만드는 경우)도 State/`:With`처럼 "매번
새로 만드는" 패턴을 따르는 게 맞다. 이전 콜백이 다시 실행되는 건 코드가
꼬이고 디버깅이 어려워지는 원인이 될 것. Ref와는 다른 결 — use only once.
Claude가 확인한 구조적 근거: `Ref`가 "다른 값으로 교체되면 `retract`
취소됨"이라는 취소 개념을 가질 수 있는 건 정상 우선순위 스캔의
`(inst,k)` 디스패치 체인에 실제로 참여하기 때문(`Dispatch.retractUnder`가
그 체인 대상으로 동작). `PreRef`는 pre-pass에서 fire와 동시에
`flattened[i] = None`으로 소진되고 정상 두 패스는 아예 건드리지 않음
(`bind-system-plan.md` "호이스팅의 실제 구현" 절 — 기존에 이미 확정돼
있던 메커니즘) — 그래서 "취소"가 애초에 도달할 방법이 없는 값이었음,
사용자의 직관이 기존 구조와 정확히 들어맞음.
더 나아가 진짜 위험은 "취소"가 아니라 **재사용**이라는 걸 짚음: 이미
fire된 PreRef 객체를 두 번째 construction에 다시 놓으면, 거기서 등록하는
`:Callback(fn)`이 "이미 채워져 있으면 즉시 1회 호출"이라는 기존 규칙
때문에 의도한 새 인스턴스가 아니라 첫 번째 fire 때 남은 stale `.Value`
조용히 호출됨 — 에러 없이 엉뚱한 값을 들고 실행되는, 디버깅하기 아주
어려운 버그. 사용자가 이 진단에 동의, "에러 가드 구현이 거의 공짜니까
재할당을 아예 불가능하게 두자"로 확정.
## 결정
1. **PreRef는 취소 개념이 없음** — 구조적으로 이미 그랬던 사실을 명문화.
2. **이미 fire된 PreRef를 재사용(두 번째 construction에 다시 놓기)하면
pre-pass가 즉시 `error`** — 내부 `_fired` 플래그로 구현, 위 "동적
경로 가드" Handler와는 별개의 코드 경로(pre-pass 자신 안에 위치).
3. **관용구**: `Slot:List``updateFn`처럼 반복 호출되는 자리에서
PreRef가 필요하면 호출마다 새 `PreRef()`를 만들 것 — 클로저 캡처로
재사용 금지. (Slot 자체는 이미 요소 타입으로 Ref/PreRef를 금지하고
있어, 이 관용구가 실제로 문제되는 자리는 `updateFn` 안에서 호출하는
컴포넌트 함수 내부뿐.)
## 반영
- `base/bind-system-plan.md` — "동적 경로로 도착한 PreRef는 런타임에도
명시적으로 에러" 절 바로 아래에 새 항목 추가(구조적 근거, 재사용
위험 시나리오, 구현 메커니즘, 관용구 전부 포함).
- `research/documentation-content-map.md` — 심화 후보 6번과 "문서화 아직
보류" 목록 두 곳에서 취소 가능성 항목을 [해소됨]으로 정정, "hook"/
"pre-hook" 용어 채택 여부만 계속 열린 채로 남김. 두 항목 다 "이미
question.md에도 열려있음"이라던 서술이 실제로는 부정확했던 것도 같이
발견·수정(grep 결과 question.md엔 해당 항목이 없었음).

View file

@ -499,3 +499,14 @@ bind-system-plan.md`) `local addTax = Sum(a,b)`처럼 만든 값을 `:Compute`
오버엔지니어링). 이걸로 열린 설계 질문이 없어져 `research/tween-plan.md` 오버엔지니어링). 이걸로 열린 설계 질문이 없어져 `research/tween-plan.md`
`base/tween-plan.md`로 승격, 라이브 크로스레퍼런스 전부 갱신(session/ `base/tween-plan.md`로 승격, 라이브 크로스레퍼런스 전부 갱신(session/
과거 기록은 원문 보존을 위해 그대로 둠). 과거 기록은 원문 보존을 위해 그대로 둠).
**2026-08-12 일곱 번째 세션 — `PreRef`는 취소 개념 없음, 재사용은 error**
(`session/2026-08-12-07-preref-single-use-no-cancel.md`)
`documentation-content-map.md`에 미정으로 남아있던 "`PreRef` 취소 가능성"
해소: `PreRef`는 pre-pass에서 fire와 동시에 소진돼 정상 `retract` 체인에
아예 안 올라가므로 취소 개념 자체가 없음(사용자 직관과 기존 구조가
정확히 일치). 진짜 위험은 취소가 아니라 재사용(stale `.Value`로 콜백이
조용히 잘못 호출됨)이라고 판단해, 이미 fire된 `PreRef`를 다시 놓으면
pre-pass가 즉시 `error`하는 가드 확정(`_fired` 플래그, 거의 공짜 구현) —
1회용, use only once. `Slot:List``updateFn`처럼 반복 호출되는 자리에선
매번 새 `PreRef()`를 만들라는 관용구도 같이 명문화.