docs(dispatch): PreRef 소진 센티널을 ProcessedPreRef로 교체, Length/Offset 등록 갭 해소
PreRef pre-pass 소진 슬롯이 None으로 뭉뚱그려져 있어 setLength/ setOffsetSource 등록 책임자가 불분명했던 갭(같은 세션 조사에서 발견)을, 전용 센티널 ProcessedPreRef + ProcessedPreRefHandler로 교체해 "이 위치를 처음 매치한 Handler가 등록 책임을 진다"는 기존 계약에 특수 취급 없이 편입시킴. 파생 서술(동적 경로 가드, 취소 개념 없음 근거) 정정 포함. PostRef 백로그 스케치도 같은 원리로 갱신 — 별도 후행 재순회 없이 PreRef pre-pass 한 번의 스윕에서 isPostRef도 같이 소진하고 postRefList에 적재해두는 안으로 PreRef/PostRef 소진 메커니즘을 완전히 대칭화. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9f9a68f0c1
commit
e0ef7ce34b
5 changed files with 171 additions and 58 deletions
|
|
@ -356,11 +356,20 @@ end
|
||||||
KV 매치와 무관.
|
KV 매치와 무관.
|
||||||
**이 `NoneHandler`는 해시 파트(프로퍼티/이벤트) 전용 — 배열 파트에서
|
**이 `NoneHandler`는 해시 파트(프로퍼티/이벤트) 전용 — 배열 파트에서
|
||||||
`None`을 만나는 건 완전히 다른 규칙(2026-08-07 열 번째 세션, "PreRef"
|
`None`을 만나는 건 완전히 다른 규칙(2026-08-07 열 번째 세션, "PreRef"
|
||||||
절 "호이스팅의 실제 구현" 참고).** 배열 파트의 `None`은 "빈 슬롯"
|
절 "호이스팅의 실제 구현" 참고).** 배열 파트의 `None`(`props.Ref or
|
||||||
|
None`처럼 애초에 아무것도 놓인 적 없는 자리)은 "빈 슬롯"
|
||||||
표시일 뿐 처리할 핸들러 자체가 없으므로, `Dispatch.drive`의 두 패스
|
표시일 뿐 처리할 핸들러 자체가 없으므로, `Dispatch.drive`의 두 패스
|
||||||
루프 자신이 `NoneHandler`/`Dispatch.process`를 거치지 않고 바로
|
루프 자신이 `NoneHandler`/`Dispatch.process`를 거치지 않고 바로
|
||||||
건너뜀 — 같은 센티널 값이지만 배열 파트냐 해시 파트냐에 따라 처리
|
건너뜀 — 같은 센티널 값이지만 배열 파트냐 해시 파트냐에 따라 처리
|
||||||
경로가 다르다는 점에 유의.
|
경로가 다르다는 점에 유의. **[정정, 2026-08-14 두 번째 세션] "PreRef
|
||||||
|
pre-pass가 소진시킨 자리"는 이 규칙의 예가 아님** — 그 자리는 `None`이
|
||||||
|
아니라 별도 센티널 `ProcessedPreRef`로 소진되고, `ProcessedPreRefHandler`
|
||||||
|
(`base/ref-plan.md`의 "PreRef" 절)를 통해 정상 `Dispatch.process`
|
||||||
|
경로를 그대로 탐(아래 "Length/Offset" 절 참고) — 예전엔 이 둘(원래부터
|
||||||
|
빈 자리 vs 한때 PreRef였다가 소진된 자리)이 똑같이 `None`으로 뭉뚱그려져
|
||||||
|
`setLength`/`setOffsetSource` 등록 책임 소재가 불분명한 갭이 있었음
|
||||||
|
(2026-08-14 첫 번째 세션 조사에서 발견), 지금은 서로 다른 센티널로
|
||||||
|
명확히 분리됨.
|
||||||
`NoneHandler.isHandlable`은 `v == None`(센티널 자체)을 잡는 것이지
|
`NoneHandler.isHandlable`은 `v == None`(센티널 자체)을 잡는 것이지
|
||||||
`v == nil`이 아님 — 진짜 `nil`은 애초에 테이블 순회로 나올 수 없다는 게
|
`v == nil`이 아님 — 진짜 `nil`은 애초에 테이블 순회로 나올 수 없다는 게
|
||||||
이 문제의 출발점이었으므로, 매치 대상은 항상 `None` 마커.
|
이 문제의 출발점이었으므로, 매치 대상은 항상 `None` 마커.
|
||||||
|
|
@ -915,12 +924,21 @@ Dispatch.setOffsetSource(inst, i, offset: Source<number> | None)
|
||||||
만들어 `Frame { LayoutOrder = layoutOrder:With(offset):Compute(fn) }`처럼
|
만들어 `Frame { LayoutOrder = layoutOrder:With(offset):Compute(fn) }`처럼
|
||||||
써넣으면 됨 — 새 메커니즘 불필요. 상세는 `base/slot-plan.md`의
|
써넣으면 됨 — 새 메커니즘 불필요. 상세는 `base/slot-plan.md`의
|
||||||
`Slot:List` 절 참고. **실제 마운트를 하지 않는 위치는 `None`을 등록** — 순서 계산에
|
`Slot:List` 절 참고. **실제 마운트를 하지 않는 위치는 `None`을 등록** — 순서 계산에
|
||||||
참여할 게 없다는 명시적 선언. 대상은 Ref/PreRef뿐 아니라 **그 배열
|
참여할 게 없다는 명시적 선언. 대상은 일반 `Ref`뿐 아니라 **그 배열
|
||||||
위치의 값 자체가 `None`인 모든 경우**(예: `props.Ref or None` 관용구로
|
위치의 값 자체가 `None`인 모든 경우**(예: `props.Ref or None` 관용구로
|
||||||
캐우칭된 미전달 Ref, PreRef pre-pass가 소진시킨 슬롯 등) — `setLength`도
|
캐우칭된 미전달 Ref) — `setLength`도
|
||||||
같은 위치엔 짝을 맞춰 `0`으로 등록해야 함(위 `setLength` 항목의
|
같은 위치엔 짝을 맞춰 `0`으로 등록해야 함(위 `setLength` 항목의
|
||||||
"`nil`/`None`이면 `0`" 규칙과 항상 같이 감, 둘 중 하나만 반영되면
|
"`nil`/`None`이면 `0`" 규칙과 항상 같이 감, 둘 중 하나만 반영되면
|
||||||
길이 합계와 실제 순서 계산이 어긋남).
|
길이 합계와 실제 순서 계산이 어긋남). **[정정, 2026-08-14 두 번째 세션]
|
||||||
|
`PreRef` pre-pass가 소진시킨 슬롯은 더 이상 이 목록에 없음** — 예전엔
|
||||||
|
그 슬롯도 `None`으로 뭉뚱그려 등록해야 한다고만 서술돼 있었는데,
|
||||||
|
`None` 소진 슬롯은 정의상 어떤 Handler도 안 거치므로(위 "`None` 센티널"
|
||||||
|
절) "누가 이 등록을 실제로 호출하는가"가 답 없는 갭이었음(2026-08-14
|
||||||
|
첫 번째 세션 조사에서 발견). 지금은 그 슬롯이 전용 센티널
|
||||||
|
`ProcessedPreRef`로 소진되고, **`ProcessedPreRefHandler`(`base/
|
||||||
|
ref-plan.md`의 "PreRef" 절)가 정상 매치 과정에서 직접 `setLength(0)`/
|
||||||
|
`setOffsetSource(None)`을 등록** — "이 위치를 처음 매치한 Handler가
|
||||||
|
등록 책임을 진다"는 위 원칙을 특수 취급 없이 그대로 만족.
|
||||||
|
|
||||||
**해제(그 자리가 더 이상 기여하지 않게 될 때)는 `setOffsetSource(...,None)`
|
**해제(그 자리가 더 이상 기여하지 않게 될 때)는 `setOffsetSource(...,None)`
|
||||||
→ `setLength(...,0)` 순서로 (2026-08-13 여섯 번째 세션, 사용자 지적).**
|
→ `setLength(...,0)` 순서로 (2026-08-13 여섯 번째 세션, 사용자 지적).**
|
||||||
|
|
@ -949,12 +967,15 @@ lazy 생성.
|
||||||
원칙 재사용** — 모든 number 인덱스를 반드시 채워야 하는데(위 UB 규칙)
|
원칙 재사용** — 모든 number 인덱스를 반드시 채워야 하는데(위 UB 규칙)
|
||||||
`nil`을 넣으면 (1) 그 자리가 "안 채워짐"과 구별이 안 되고 (2) 배열이
|
`nil`을 넣으면 (1) 그 자리가 "안 채워짐"과 구별이 안 되고 (2) 배열이
|
||||||
구멍 나면서 순수 array 취급이 깨져 접근 비용이 올라감(해시 파트로 밀림)
|
구멍 나면서 순수 array 취급이 깨져 접근 비용이 올라감(해시 파트로 밀림)
|
||||||
— `None`은 실재하는 값이라 자리를 "채워짐"으로 유지시켜줌, PreRef
|
— `None`은 실재하는 값이라 자리를 "채워짐"으로 유지시켜줌, `flattened`
|
||||||
pre-pass 소진 슬롯에 이미 적용된 것과 같은 원칙(`ref-plan.md`의 "PreRef" 절의
|
배열이 진짜 빈 자리(`None`, 예: `props.Ref or None`)와 PreRef pre-pass
|
||||||
"왜 `None`이 아니라 `nil`인가" 참고 — **단, 그 절에서 최종적으로 `nil`로
|
소진 자리(`ProcessedPreRef`, 2026-08-14 두 번째 세션 이전엔 여기도 `None`)
|
||||||
되돌아간 건 Ref 콜백/대기자 배열 한정**이고 `sourceList`/PreRef
|
둘 다 실재하는 센티널로 채워 구멍을 피하는 것과 같은 원칙(`ref-plan.md`의
|
||||||
pre-pass처럼 순서가 실제로 중요하거나 "채워짐 여부"를 엄밀히 구별해야
|
"Ref 일반화" 절 "왜 `None`이 아니라 `nil`인가" 참고 — **단, 그 절에서
|
||||||
하는 배열은 여전히 `None`이 맞음, 헷갈리지 말 것). 다만 `recompute`가
|
최종적으로 `nil`로 되돌아간 건 Ref 콜백/대기자 배열 한정**이고
|
||||||
|
`sourceList`/`flattened`처럼 순서가 실제로 중요하거나 "채워짐 여부"를
|
||||||
|
엄밀히 구별해야 하는 배열은 여전히 실재하는 센티널이 맞음, 헷갈리지
|
||||||
|
말 것). 다만 `recompute`가
|
||||||
`1..N` 고정 범위를 도는 인덱스 `for`라 애초에 성긴 정수 키 순회 문제
|
`1..N` 고정 범위를 도는 인덱스 `for`라 애초에 성긴 정수 키 순회 문제
|
||||||
자체는 안 생김 — `None`이 필요한 이유는 순회 순서 보존이 아니라 "채워짐
|
자체는 안 생김 — `None`이 필요한 이유는 순회 순서 보존이 아니라 "채워짐
|
||||||
여부 구별과 접근 비용" 쪽.
|
여부 구별과 접근 비용" 쪽.
|
||||||
|
|
|
||||||
|
|
@ -382,23 +382,35 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
||||||
`Dispatch.drive(inst, flattened)`는 같은 `flattened` 배열을 **두 번
|
`Dispatch.drive(inst, flattened)`는 같은 `flattened` 배열을 **두 번
|
||||||
순회**한다 — (1) pre-pass: 배열 파트 전체를 index 순서대로 훑으며
|
순회**한다 — (1) pre-pass: 배열 파트 전체를 index 순서대로 훑으며
|
||||||
`isPreRef(v)`인 슬롯을 찾아 그 자리에서 fire하고 즉시 **`flattened[i]
|
`isPreRef(v)`인 슬롯을 찾아 그 자리에서 fire하고 즉시 **`flattened[i]
|
||||||
= None`**으로 소진(`nil`이 아님, 2026-08-07 열 번째 세션 정정: `nil`로
|
= ProcessedPreRef`**로 소진(`nil`이 아님, 2026-08-07 열 번째 세션 정정: `nil`로
|
||||||
지우면 그 순간 테이블이 "구멍 있는" 상태가 되어 이어지는 (2)의 순회
|
지우면 그 순간 테이블이 "구멍 있는" 상태가 되어 이어지는 (2)의 순회
|
||||||
순서 보장 자체가 깨질 위험이 있음 — 정확히 이 pre-pass가 의존하는
|
순서 보장 자체가 깨질 위험이 있음 — 정확히 이 pre-pass가 의존하는
|
||||||
바로 그 보장이라 치명적. **[주의, 2026-08-09 열한 번째 세션] Ref
|
바로 그 보장이라 치명적. **[주의, 2026-08-09 열한 번째 세션] Ref
|
||||||
자신의 콜백/대기자 배열은 이 이유가 적용되지 않아 `nil` 소진으로
|
자신의 콜백/대기자 배열은 이 이유가 적용되지 않아 `nil` 소진으로
|
||||||
되돌아갔음(위 "Ref 일반화" 절 참고) — 여기 PreRef pre-pass는 순서
|
되돌아갔음(위 "Ref 일반화" 절 참고) — 여기 PreRef pre-pass는 순서
|
||||||
보장이 실제로 필요한 별개 케이스라 `None` 소진이 계속 맞음, 두
|
보장이 실제로 필요한 별개 케이스라 실재하는 센티널 소진이 계속 맞음, 두
|
||||||
사례를 혼동하지 말 것**). (2) 그 다음에야 비로소 평소의 배열→해시 두 패스가
|
사례를 혼동하지 말 것**). **[정정, 2026-08-14 두 번째 세션] 소진 값은
|
||||||
**같은 테이블**을 다시 순회 — 이때 `None`으로 소진된 슬롯은 **정상
|
`None`이 아니라 전용 센티널 `ProcessedPreRef`(단일 `{}`, `None`과 같은
|
||||||
`Dispatch.process`/`NoneHandler` 경로를 안 타고 두 패스 루프 자신이
|
급의 유니크 키 — 사용자 제안).** 옛 설계는 소진 값을 `None`으로 뭉뚱그려
|
||||||
직접 건너뜀**(`if v == None then continue end`, 배열 파트 전용
|
"그 자리가 원래부터 빈 자리"였던 경우(`props.Ref or None`)와 "한때
|
||||||
특수 취급). **해시 파트의 `None`(Modifier 필드 명시적 지우기)과는
|
PreRef였다가 방금 fire되어 소진된 자리"를 구별 못 했고, 그 결과
|
||||||
의미가 다름** — 해시 파트 `None`은 "이 프로퍼티 핸들러야, 방금
|
아래 "Length/Offset" 계약(`base/dispatch-core-plan.md`, 2026-08-09
|
||||||
지워진 걸 알고 알아서 처리해"라는 의미 있는 재디스패치(`NoneHandler`
|
여섯 번째 세션 확정)이 "이 위치를 처음 매치한 Handler가
|
||||||
경유)지만, 배열 파트 `None`은 그냥 "여기 원래 아무것도 없었던 것과
|
`setLength`/`setOffsetSource` 등록 책임을 진다"고 못박아 놨는데도
|
||||||
같다"는 순수 빈 슬롯 표시라 처리할 핸들러 자체가 없음 — 재디스패치
|
`None` 소진 슬롯은 정의상 어떤 Handler도 안 거쳐서(아래 참고) "그럼
|
||||||
없이 루프가 직접 스킵하는 게 맞음. "호이스팅"은 PreRef를 배열의 맨
|
누가 그 등록을 실제로 호출하는가"가 문서 어디에도 없는 갭이었음
|
||||||
|
(2026-08-14 첫 번째 세션 조사에서 발견). `ProcessedPreRef`로 소진처를
|
||||||
|
분리하면 이 갭이 구조적으로 사라짐 — 아래 `ProcessedPreRefHandler`
|
||||||
|
참고. (2) 그 다음에야 비로소 평소의 배열→해시 두 패스가
|
||||||
|
**같은 테이블**을 다시 순회 — 이때 `ProcessedPreRef`로 소진된 슬롯은
|
||||||
|
**정상 `Dispatch.process` 경로를 그대로 탄다**(아래
|
||||||
|
`ProcessedPreRefHandler`가 매치, **[정정] 예전엔 `None`이라 두 패스
|
||||||
|
루프 자신이 `if v == None then continue end`로 직접 건너뛰고 어떤
|
||||||
|
Handler도 안 거쳤으나, 지금은 일부러 정상 경로를 태워 Length/Offset
|
||||||
|
등록 책임을 기존 계약에 특수 취급 없이 그대로 얹음**). 진짜로
|
||||||
|
원래부터 빈 자리인 `None`(`props.Ref or None` 등)은 여전히 두 패스
|
||||||
|
루프가 직접 건너뜀 — 두 센티널이 이제 서로 다른 경로를 타므로
|
||||||
|
혼동 금지. "호이스팅"은 PreRef를 배열의 맨
|
||||||
앞으로 물리적으로 옮기는 게 아니라, **PreRef 전용 선행 루프가
|
앞으로 물리적으로 옮기는 게 아니라, **PreRef 전용 선행 루프가
|
||||||
통째로 먼저 끝난 뒤에야 나머지 처리가 시작된다는 뜻** — 그래서
|
통째로 먼저 끝난 뒤에야 나머지 처리가 시작된다는 뜻** — 그래서
|
||||||
소스에서 마지막 child로 적었어도 무조건 다른 모든 처리보다 먼저
|
소스에서 마지막 child로 적었어도 무조건 다른 모든 처리보다 먼저
|
||||||
|
|
@ -408,7 +420,31 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
||||||
두 번째(정상) 패스가 이미 정당하게 처리된 그 PreRef를
|
두 번째(정상) 패스가 이미 정당하게 처리된 그 PreRef를
|
||||||
`Dispatch.process`로 다시 넘기게 되고, 그러면 이 가드 Handler가
|
`Dispatch.process`로 다시 넘기게 되고, 그러면 이 가드 Handler가
|
||||||
엉뚱하게 매치되어 **정상적인 PreRef 사용에도 에러가 터짐** — 소진은
|
엉뚱하게 매치되어 **정상적인 PreRef 사용에도 에러가 터짐** — 소진은
|
||||||
이 오탐을 막기 위해 반드시 필요.
|
이 오탐을 막기 위해 반드시 필요(`ProcessedPreRef`는 `isPreRef(v)`가
|
||||||
|
거짓이라 이 가드 Handler와는 애초에 안 겹침).
|
||||||
|
- **`ProcessedPreRefHandler` — 소진된 슬롯이 Length/Offset에 "0 기여"를
|
||||||
|
등록하는 전담 Handler (2026-08-14 두 번째 세션, 사용자 제안 — 위 갭의
|
||||||
|
해소).**
|
||||||
|
```lua
|
||||||
|
ProcessedPreRefHandler.priority = <매우 높음, NoneHandler와 동급>
|
||||||
|
ProcessedPreRefHandler.isHandlable(inst, k, v) = (v == ProcessedPreRef)
|
||||||
|
function ProcessedPreRefHandler.process(inst, i, v)
|
||||||
|
Dispatch.setLength(inst, i, 0)
|
||||||
|
Dispatch.setOffsetSource(inst, i, None)
|
||||||
|
return function() end -- no-op retract, 이 자리는 fire가 끝나
|
||||||
|
-- 되돌릴 상태 자체가 없음
|
||||||
|
end
|
||||||
|
```
|
||||||
|
`isHandlable`이 `v == ProcessedPreRef`만 잡으므로 배열 파트 전용(해시
|
||||||
|
파트엔 이 센티널이 등장할 경로 자체가 없음). 이걸로 `base/
|
||||||
|
dispatch-core-plan.md`의 "Length/Offset" 절이 이미 확정해둔 "이
|
||||||
|
위치를 처음 매치한 Handler가 등록 책임을 진다"는 계약을 특수 취급
|
||||||
|
없이 그대로 만족시킴 — 매치되는 Handler 자신이 곧 등록자라 "누가
|
||||||
|
등록하는가"라는 질문 자체가 안 생김. 반환하는 retract는 하드코딩된
|
||||||
|
no-op인데, 이건 "PreRef는 취소 개념이 없다" 절(아래)이 말하는 것과
|
||||||
|
같은 이유 — fire가 이미 실행한 부작용은 되돌릴 수 없으므로 이 자리가
|
||||||
|
dispatch 체인에 실제로 올라가 있어도(**[정정] 예전 서술과 달리 이제는
|
||||||
|
올라가 있음** — 아래 참고) retract가 할 일이 없는 것뿐.
|
||||||
- **명확화(2026-08-09 열한 번째 세션, 확인 질문에 답변) —
|
- **명확화(2026-08-09 열한 번째 세션, 확인 질문에 답변) —
|
||||||
`NoneHandler.isHandlable(inst,k,v) = (v == None)`은 `k` 타입을 전혀
|
`NoneHandler.isHandlable(inst,k,v) = (v == None)`은 `k` 타입을 전혀
|
||||||
안 가리므로 숫자 키(`k=number`)에서도 이론상 매치될 수 있어 보이지만,
|
안 가리므로 숫자 키(`k=number`)에서도 이론상 매치될 수 있어 보이지만,
|
||||||
|
|
@ -426,7 +462,8 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
||||||
번째 세션). 같은 세션에서 사용자가 직접 `{[1]=1,[2222]=2222,
|
번째 세션). 같은 세션에서 사용자가 직접 `{[1]=1,[2222]=2222,
|
||||||
[211]=211,...}`류 **키가 듬성듬성한(sparse)** 테이블을 REPL로
|
[211]=211,...}`류 **키가 듬성듬성한(sparse)** 테이블을 REPL로
|
||||||
실측해, 그런 테이블은 순회 순서가 index 오름차순이 전혀 아님(해시
|
실측해, 그런 테이블은 순회 순서가 index 오름차순이 전혀 아님(해시
|
||||||
버킷 순서)을 확인함 — 그래서 위 pre-pass는 (nil이 아니라) `None`으로
|
버킷 순서)을 확인함 — 그래서 위 pre-pass는 (nil이 아니라) 실재하는
|
||||||
|
센티널(`ProcessedPreRef`, 2026-08-14 두 번째 세션 이전엔 `None`)로
|
||||||
소진해 테이블을 "구멍 없이 촘촘한" 상태로 계속 유지하는 전략으로
|
소진해 테이블을 "구멍 없이 촘촘한" 상태로 계속 유지하는 전략으로
|
||||||
이 위험을 원천 회피함(검증 불필요, 애초에 구멍을 안 만드므로).
|
이 위험을 원천 회피함(검증 불필요, 애초에 구멍을 안 만드므로).
|
||||||
**여전히 M0에서 검증해야 하는 건 다른 케이스**: `props.Modifier`/
|
**여전히 M0에서 검증해야 하는 건 다른 케이스**: `props.Modifier`/
|
||||||
|
|
@ -468,20 +505,27 @@ flatten된 값은 해시 파트(프로퍼티 키)로 존재하게 되고, Store
|
||||||
"한 값 종류만 전담하는 Handler" 패턴 재사용, 새 메커니즘 아님. 이
|
"한 값 종류만 전담하는 Handler" 패턴 재사용, 새 메커니즘 아님. 이
|
||||||
Handler는 **`Dispatch.process`/`getHandler`의 정상 우선순위 스캔에
|
Handler는 **`Dispatch.process`/`getHandler`의 정상 우선순위 스캔에
|
||||||
등록**되는 반면(pre-pass처럼 그 밖에서 도는 게 아님), 리터럴 배열의
|
등록**되는 반면(pre-pass처럼 그 밖에서 도는 게 아님), 리터럴 배열의
|
||||||
`PreRef`는 pre-pass가 fire와 동시에 해당 슬롯을 소진(`None` 처리,
|
`PreRef`는 pre-pass가 fire와 동시에 해당 슬롯을 소진(**[정정,
|
||||||
`nil` 아님)해 정상 두 패스 스캔에 다시 노출되지 않게 하므로, 이
|
2026-08-14 두 번째 세션] `None`이 아니라 `ProcessedPreRef` 처리**,
|
||||||
Handler가 실제로
|
위 "호이스팅의 실제 구현" 절)해 **이 가드 Handler(`isPreRef(v)`만
|
||||||
|
매치)에는 다시 노출되지 않게** 하므로(정상 두 패스 스캔 자체엔
|
||||||
|
`ProcessedPreRefHandler`를 통해 여전히 노출됨 — "스캔에 안 걸림"이
|
||||||
|
아니라 "이 가드에 안 걸림"이 정확한 설명), 이 Handler가 실제로
|
||||||
매치되는 경우는 오직 "타입이 막았어야 했는데 어떻게든 동적으로
|
매치되는 경우는 오직 "타입이 막았어야 했는데 어떻게든 동적으로
|
||||||
새어들어온" 버그 케이스뿐 — 그래서 no-op이 아니라 즉시 `error`.
|
새어들어온" 버그 케이스뿐 — 그래서 no-op이 아니라 즉시 `error`.
|
||||||
- **PreRef는 "취소"라는 개념이 없다 — 1회용, 재사용은 즉시 error
|
- **PreRef는 "취소"라는 개념이 없다 — 1회용, 재사용은 즉시 error
|
||||||
(2026-08-12 여섯 번째 세션, 사용자 제안 채택).** `Ref`가 "다른 값으로
|
(2026-08-12 여섯 번째 세션, 사용자 제안 채택).** `Ref`가 "다른 값으로
|
||||||
교체되면 `retract`로 취소됨"이라는 의미의 취소를 가질 수 있는 건 정상
|
교체되면 `retract`로 취소됨"이라는 의미의 취소를 가질 수 있는 건 정상
|
||||||
우선순위 스캔의 `(inst,k)` 디스패치 체인에 실제로 참여해서임 —
|
우선순위 스캔의 `(inst,k)` 디스패치 체인에 실제로 참여해서임 —
|
||||||
`Dispatch.retractFrom`이 그 체인을 대상으로 동작함. `PreRef`는 애초에
|
`Dispatch.retractFrom`이 그 체인을 대상으로 동작함. **[정정,
|
||||||
그 체인에 올라간 적이 없음(pre-pass에서 fire와 동시에 `None`으로
|
2026-08-14 두 번째 세션] "그 체인에 올라간 적이 없다"는 근거는 더 이상
|
||||||
소진되고 정상 두 패스는 건드리지 않음, 위 "호이스팅의 실제 구현" 절) —
|
정확하지 않음** — `ProcessedPreRefHandler` 신설로 소진된 슬롯도 이제
|
||||||
그래서 "취소 가능 여부" 자체가 성립할 토대가 없었던 게 구조적으로
|
정상 `Dispatch.process` 경로를 타 체인에 올라감(위 "호이스팅의 실제
|
||||||
이미 사실이었음, 이번 세션은 그걸 명문화한 것뿐. 진짜 위험은 취소가
|
구현" 절). "취소 개념이 없다"는 결론 자체는 그대로 유효하지만 이유가
|
||||||
|
바뀜: 체인에 없어서가 아니라, **그 자리의 retract가 하드코딩된
|
||||||
|
no-op이기 때문** — PreRef의 fire는 `fn(inst)`를 실제로 실행하는
|
||||||
|
부작용이라 애초에 "되돌릴 상태"가 없고, 그래서 체인에 올라가 있어도
|
||||||
|
retract가 할 일 자체가 없음. 진짜 위험은 취소가
|
||||||
아니라 **재사용**: 이미 한 번 fire된 `PreRef` 객체를 두 번째
|
아니라 **재사용**: 이미 한 번 fire된 `PreRef` 객체를 두 번째
|
||||||
construction의 children 배열에 다시 놓으면, 거기서 등록하는
|
construction의 children 배열에 다시 놓으면, 거기서 등록하는
|
||||||
`:Callback(fn)`이 "이미 채워져 있으면 즉시 1회 호출"이라는 규칙(위
|
`:Callback(fn)`이 "이미 채워져 있으면 즉시 1회 호출"이라는 규칙(위
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,14 @@
|
||||||
문제를 풀고 있었다는 것:
|
문제를 풀고 있었다는 것:
|
||||||
|
|
||||||
- **순서가 중요한 배열(PreRef pre-pass 소진 슬롯, Length/Offset의
|
- **순서가 중요한 배열(PreRef pre-pass 소진 슬롯, Length/Offset의
|
||||||
sourceList)**: 계속 `None` — 구멍이 생기면 해시 파트로 밀려
|
sourceList)**: 계속 실재하는 센티널(구멍이 아닌 non-nil 값) — 구멍이
|
||||||
index 순회 순서가 깨지므로, "채워짐"을 유지해야 함.
|
생기면 해시 파트로 밀려 index 순회 순서가 깨지므로, "채워짐"을
|
||||||
|
유지해야 함. 이 스크립트는 그 센티널을 `None`으로 대표해 검증하지만,
|
||||||
|
**[2026-08-14 두 번째 세션] PreRef pre-pass 소진 슬롯의 실제 값은
|
||||||
|
이후 `None`에서 전용 센티널 `ProcessedPreRef`로 갈렸음**(`base/
|
||||||
|
ref-plan.md` "PreRef" 절) — 여기서 검증하는 성질(실재하는 non-nil
|
||||||
|
값이면 순서/`#t`가 안 깨진다) 자체는 어느 센티널을 쓰든 동일해서
|
||||||
|
이 결과는 계속 유효, 재실행 불필요.
|
||||||
- **순서가 안 중요하고 슬롯 재사용이 필요한 배열(Ref 콜백/대기자
|
- **순서가 안 중요하고 슬롯 재사용이 필요한 배열(Ref 콜백/대기자
|
||||||
리스트)**: `nil` + "빈 슬롯을 선형 탐색해 재사용"하는 등록 함수
|
리스트)**: `nil` + "빈 슬롯을 선형 탐색해 재사용"하는 등록 함수
|
||||||
(`table.insert`는 안 씀 — 구멍 있는 테이블에서 `#t`가 미정의
|
(`table.insert`는 안 씀 — 구멍 있는 테이블에서 `#t`가 미정의
|
||||||
|
|
|
||||||
|
|
@ -153,32 +153,60 @@ construction에 재사용**하는 것("이미 한 번 fire된 PreRef 객체를
|
||||||
`OnDestroyed`와 달리 **진짜로 공짜가 아님** — 그래서 지금 채택하지
|
`OnDestroyed`와 달리 **진짜로 공짜가 아님** — 그래서 지금 채택하지
|
||||||
않기로 함.
|
않기로 함.
|
||||||
|
|
||||||
**`PostRef` 스케치(사용자 제안, 2026-08-14)** — 완전히 새로운
|
**`PostRef` 스케치(사용자 제안, 2026-08-14, 두 번째 세션에 `ProcessedPreRef`
|
||||||
메커니즘을 발명할 필요는 없어 보임. `PreRef`의 pre-pass가 "두 패스
|
선례 반영해 갱신, 같은 세션 후속 제안으로 다시 갱신 — "후행 스캔" 초안
|
||||||
루프를 돌기 **전에** 배열 파트를 미리 한 번 훑어 fire하고 소진하는"
|
폐기)** — 완전히 새로운 메커니즘을 발명할 필요는 없어 보임. **핵심 통찰
|
||||||
별도 선행 스캔이었던 것(`base/ref-plan.md` "PreRef" 절, "호이스팅의
|
(사용자 제안): `PreRef`의 pre-pass가 이미 배열 파트 전체를 index 순서로
|
||||||
실제 구현" 항목)과 **정확히 대칭인 후행 스캔**을 만들면 됨 — 두 패스가
|
한 번 훑고 있으니, 같은 스윕에서 `isPostRef(v)`도 같이 잡아내면 되고
|
||||||
끝난 **뒤에** 배열 파트를 다시 한번 훑어 `PostRef` 슬롯만 골라 fire.
|
`PostRef` 전용 후행 재순회(두 번째 전체 `for`)는 아예 필요 없음.**
|
||||||
`PreRef`가 이미 갖고 있는 장치(호이스팅 없이 그냥 후행이면 되므로 오히려
|
|
||||||
더 단순할 수 있음, 1회용 `_fired` 가드, `None` 소진 대신 이 시점엔
|
- **pre-pass 한 번으로 `PreRef`/`PostRef` 둘 다 처리**: 같은 루프 안에서
|
||||||
순서 보장이 더 이상 필요 없으니 `nil` 소진도 검토 가능)를 그대로
|
`isPreRef(v)`면 기존 그대로 그 자리에서 즉시 fire하고
|
||||||
거울상으로 재사용하는 구현이라 **새 개념이 아니라 기존 `PreRef` 코드의
|
`flattened[i] = ProcessedPreRef`로 소진. `isPostRef(v)`면 **아직
|
||||||
변형**에 가까움 — 다만 `Dispatch.drive`에 실제 루프 한 번이 추가되는
|
fire하지 않고**, 이 `Dispatch.drive(inst, flattened)` 호출 하나에만
|
||||||
비용은 여전히 있으므로 "공짜"까지는 아님. `OnRendered(fn)`은
|
로컬인 평범한 배열 `postRefList`(`Relate` 같은 별도 저장소 불필요 —
|
||||||
`PostRef():Callback(fn)`을 반환하는 팩토리로, 위 `OnCreated`와 완전히
|
이 함수 콜스택 안에서만 살면 됨)에 그 인스턴스를 순서대로 push하고
|
||||||
같은 패턴이 됨.
|
즉시 `flattened[i] = ProcessedPostRef`로 소진(1회용 재사용 가드
|
||||||
|
`_fired`도 이 시점에 세팅 — "슬롯이 소진되는 시점"과 "재사용 방지가
|
||||||
|
걸리는 시점"을 `PreRef`와 동일하게 맞춤, 실제 콜백 fire와 시점이
|
||||||
|
갈리는 건 아래 항목뿐).
|
||||||
|
- **`ProcessedPostRefHandler`는 `ProcessedPreRefHandler`와 완전히
|
||||||
|
대칭**: 정상 두 패스가 `ProcessedPostRef`를 매치해
|
||||||
|
`setLength(0)`/`setOffsetSource(None)`을 등록하고 no-op retract를
|
||||||
|
반환 — 새 비대칭 규칙이 필요 없음. **[정정] 이전 초안은 "PostRef는
|
||||||
|
소진 전 원본 값이 정상 두 패스의 매치 대상이어야 한다"고
|
||||||
|
잘못 짚었었는데, pre-pass에서 미리 소진해두면 그 비대칭 자체가 안
|
||||||
|
생김** — `PreRef`의 "동적 경로 가드" Handler(정상 스캔에서
|
||||||
|
`isPreRef(v)`를 잡아 즉시 error)와 짝이 되는 `PostRef`용 가드 Handler도
|
||||||
|
똑같이 필요(pre-pass가 놓쳤을 때만 매치되는 버그 케이스 전용, `error`).
|
||||||
|
- **두 패스가 끝난 뒤, `Dispatch.drive`가 `postRefList`를 그 순서 그대로
|
||||||
|
순회하며 각 `PostRef`를 fire** — 별도 후행 전체 재순회가 필요 없음,
|
||||||
|
pre-pass가 이미 만들어둔 목록을 그대로 소비하면 끝. 복수 `PostRef`
|
||||||
|
간 순서는 복수 `PreRef`와 같은 원칙(배열 index 순서 그대로)이 자연히
|
||||||
|
적용됨.
|
||||||
|
- 결과적으로 `PreRef`와 `PostRef`는 **소진 메커니즘이 완전히 대칭**
|
||||||
|
(둘 다 pre-pass에서 즉시 `Processed*` 센티널로 소진, 둘 다 전담
|
||||||
|
`Processed*Handler`가 Length/Offset을 등록) — 유일한 차이는 "실제
|
||||||
|
콜백을 언제 부르는가"(`PreRef`는 pre-pass 그 자리, `PostRef`는 두
|
||||||
|
패스가 다 끝난 뒤 `postRefList` 순회) 하나뿐. `_fired` 1회용 가드도
|
||||||
|
거울상 그대로 재사용. 비용도 애초 우려("루프 한 번이 추가되는 비용")보다
|
||||||
|
작음 — 추가되는 건 전체 배열 재순회가 아니라 `postRefList`(실제
|
||||||
|
`PostRef` 개수만큼)의 순회뿐이라, "공짜"는 아니어도 이전 "후행 스캔"
|
||||||
|
초안보다 훨씬 저렴. `OnRendered(fn)`은 `PostRef():Callback(fn)`을
|
||||||
|
반환하는 팩토리로, 위 `OnCreated`와 완전히 같은 패턴이 됨.
|
||||||
|
|
||||||
**스코프도 여전히 불명확함**: "렌더 완료"가 (a) 이 인스턴스 자신의
|
**스코프도 여전히 불명확함**: "렌더 완료"가 (a) 이 인스턴스 자신의
|
||||||
프로퍼티/이벤트 세팅만 끝나면 되는지, (b) 이 인스턴스의 **자식들까지
|
프로퍼티/이벤트 세팅만 끝나면 되는지, (b) 이 인스턴스의 **자식들까지
|
||||||
전부 마운트를 끝내야** 하는지 — React류 `on*Rendered` 이름들은 보통
|
전부 마운트를 끝내야** 하는지 — React류 `on*Rendered` 이름들은 보통
|
||||||
(b)(서브트리 전체 완료)를 뜻하는 경우가 많아, 이름만 보고 (a)로 기대하는
|
(b)(서브트리 전체 완료)를 뜻하는 경우가 많아, 이름만 보고 (a)로 기대하는
|
||||||
사람과 실제 구현이 (b)라면(또는 반대라면) 기대치가 어긋날 위험이 있음.
|
사람과 실제 구현이 (b)라면(또는 반대라면) 기대치가 어긋날 위험이 있음.
|
||||||
`PostRef` 후행 스캔은 자연스럽게 (a)만 줌 — (b)를 원하면 자식 서브트리
|
`PostRef`의 `postRefList` 소비(위 스케치)는 자연스럽게 (a)만 줌 — (b)를
|
||||||
전체의 마운트 완료를 기다리는 별도 신호가 있어야 해서 훨씬 큰 작업.
|
원하면 자식 서브트리 전체의 마운트 완료를 기다리는 별도 신호가 있어야
|
||||||
|
해서 훨씬 큰 작업.
|
||||||
|
|
||||||
**착수 시점에 판단할 선택지(지금은 고르지 않음)**:
|
**착수 시점에 판단할 선택지(지금은 고르지 않음)**:
|
||||||
- (a) 위 `PostRef` 스케치대로 진짜 post-pass를 만든다 — (a) 스코프
|
- (a) 위 `PostRef` 스케치대로 두 패스 뒤 `postRefList`를 소비한다 —
|
||||||
(자기 자신 세팅 완료)의 정확한 보장.
|
(a) 스코프(자기 자신 세팅 완료)의 정확한 보장.
|
||||||
- (b) 새 메커니즘 없이 일반 `Ref`로 근사한다 — Store를 통해 늦게
|
- (b) 새 메커니즘 없이 일반 `Ref`로 근사한다 — Store를 통해 늦게
|
||||||
도착하는 값으로 "대충 렌더 이후"를 흉내내되, "완전한 보장은 없음"을
|
도착하는 값으로 "대충 렌더 이후"를 흉내내되, "완전한 보장은 없음"을
|
||||||
문서에 명시하는 선에서 타협.
|
문서에 명시하는 선에서 타협.
|
||||||
|
|
|
||||||
24
ROADMAP.md
24
ROADMAP.md
|
|
@ -44,11 +44,17 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
일반 `Ref`의 위치 기반 순서까지 최소 스파이크로 검증
|
일반 `Ref`의 위치 기반 순서까지 최소 스파이크로 검증
|
||||||
(2026-08-07 세 번째 세션, `base/ref-plan.md` "`phase` 옵션
|
(2026-08-07 세 번째 세션, `base/ref-plan.md` "`phase` 옵션
|
||||||
폐기 → 위치로 표현, `PreRef` 신설" 절) — **PreRef pre-pass의 소진은
|
폐기 → 위치로 표현, `PreRef` 신설" 절) — **PreRef pre-pass의 소진은
|
||||||
`nil`이 아니라 `None`으로(2026-08-07 열 번째 세션 정정, 사용자가
|
`nil`이 아니라 실재하는 센티널로(2026-08-07 열 번째 세션 정정, 사용자가
|
||||||
Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를
|
Luau REPL로 반례 제시 — 키가 듬성듬성해지면 순회가 index 순서를
|
||||||
전혀 안 지킴), 이 경로는 nil-hole 위험이 아예 없도록 설계됐으므로
|
전혀 안 지킴), 이 경로는 nil-hole 위험이 아예 없도록 설계됐으므로
|
||||||
"구멍 있는 테이블 순회" 자체를 검증할 필요는 없어짐(같은 절 "왜
|
"구멍 있는 테이블 순회" 자체를 검증할 필요는 없어짐(같은 절 "왜
|
||||||
`nil`이 아니라 `None`인가" 참고)**
|
`nil`이 아니라 `None`인가" 참고). **[정정, 2026-08-14 두 번째 세션]
|
||||||
|
소진 값은 이제 `None`이 아니라 전용 센티널 `ProcessedPreRef`** —
|
||||||
|
정상 두 패스가 그 자리를 `ProcessedPreRefHandler`로 매치해
|
||||||
|
`Dispatch.setLength(0)`/`setOffsetSource(None)`을 등록하도록 재설계됨
|
||||||
|
(`base/ref-plan.md` "PreRef" 절, `base/dispatch-core-plan.md`
|
||||||
|
"Length/Offset" 절) — 아래 `PreRef` pre-pass/동적 경로 가드
|
||||||
|
체크리스트 항목도 이 값으로 스파이크할 것.
|
||||||
- [ ] `props.Modifier`/`props.Ref` named-parameter로 받는 컴포넌트 하나 작성,
|
- [ ] `props.Modifier`/`props.Ref` named-parameter로 받는 컴포넌트 하나 작성,
|
||||||
`export type Params = {...}`로 타입 체크되는지 확인
|
`export type Params = {...}`로 타입 체크되는지 확인
|
||||||
(`component-composition-plan.md` 최종 결론 1번) — **`props.Modifier or
|
(`component-composition-plan.md` 최종 결론 1번) — **`props.Modifier or
|
||||||
|
|
@ -536,14 +542,22 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
`PreRef` 항목만 fire(Dispatch.process/getHandler 우회하는 raw 루프,
|
`PreRef` 항목만 fire(Dispatch.process/getHandler 우회하는 raw 루프,
|
||||||
`flatten` 함수에는 얹지 않음 — 재바인드 시 flatten 재호출 가능성과
|
`flatten` 함수에는 얹지 않음 — 재바인드 시 flatten 재호출 가능성과
|
||||||
충돌하므로 기각). 복수 `PreRef`는 배열 index 순서 그대로(별도 규칙
|
충돌하므로 기각). 복수 `PreRef`는 배열 index 순서 그대로(별도 규칙
|
||||||
없음). fire된 슬롯은 그 자리에서 소진(`None` 처리, `nil` 아님 —
|
없음). fire된 슬롯은 그 자리에서 소진(**[정정, 2026-08-14 두 번째
|
||||||
2026-08-07 열 번째 세션 정정)해 이어지는 정상 두 패스에 다시 노출
|
세션] `None`이 아니라 전용 센티널 `ProcessedPreRef` 처리** — 아래
|
||||||
안 되게 함 — `base/ref-plan.md` "PreRef" 절
|
`ProcessedPreRefHandler` 항목이 그 자리를 정상 두 패스로 마저 처리)
|
||||||
|
— `base/ref-plan.md` "PreRef" 절
|
||||||
- [ ] `PreRef` 동적 경로 가드 Handler — `{isHandlable = v is PreRef,
|
- [ ] `PreRef` 동적 경로 가드 Handler — `{isHandlable = v is PreRef,
|
||||||
process = error(...)}` 형태로 정상 우선순위 레지스트리에 등록,
|
process = error(...)}` 형태로 정상 우선순위 레지스트리에 등록,
|
||||||
`NoneHandler`와 같은 "한 값 종류 전담" 패턴. 리터럴 배열 경로는
|
`NoneHandler`와 같은 "한 값 종류 전담" 패턴. 리터럴 배열 경로는
|
||||||
pre-pass가 이미 소진시키므로 이 Handler가 매치되면 곧 타입 차단을
|
pre-pass가 이미 소진시키므로 이 Handler가 매치되면 곧 타입 차단을
|
||||||
우회한 버그라는 뜻 — 같은 절 참고
|
우회한 버그라는 뜻 — 같은 절 참고
|
||||||
|
- [ ] **[2026-08-14 두 번째 세션 신설]** `ProcessedPreRefHandler` —
|
||||||
|
`{isHandlable = v == ProcessedPreRef, process = setLength(0)+
|
||||||
|
setOffsetSource(None)+no-op retract}` 형태로 정상 우선순위
|
||||||
|
레지스트리에 등록, `NoneHandler`와 같은 "한 값 종류 전담" 패턴.
|
||||||
|
PreRef pre-pass가 소진시킨 자리가 Length/Offset에 "0 기여"를 등록할
|
||||||
|
책임을 지는 자리 — `base/ref-plan.md` "PreRef" 절, `base/
|
||||||
|
dispatch-core-plan.md` "Length/Offset" 절
|
||||||
- [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면
|
- [ ] Ref 콜백/대기자 실행 루프(`type(v)=="thread"`면
|
||||||
`coroutine.resume(v, self)`+`nil`로 소진(2026-08-09 열한 번째
|
`coroutine.resume(v, self)`+`nil`로 소진(2026-08-09 열한 번째
|
||||||
세션 최종 정정 — 순서 안 중요 + 슬롯 재사용 위해 `None`이 아닌
|
세션 최종 정정 — 순서 안 중요 + 슬롯 재사용 위해 `None`이 아닌
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue