비트셋 — bitset 타입
플래그 묶음을 시트에 적는 표기와, 그것을 64비트로 싣는 설계입니다. 함께 정한 것이 하나 더 있습니다 — 진법 리터럴을 숫자 타입 전체가 받게 하는 것이고, 그쪽은 이 타입과 별개로 성립합니다(3절).
1. 문제 — 패턴을 적을 자리의 부재
테빗의 타입 행에는 비트 패턴을 뜻하는 이름이 없습니다. 플래그 묶음을 담으려면 bigint로
적어야 하고, 그러면 두 가지가 동시에 사라집니다.
| 사라지는 것 | 결과 |
|---|---|
| 의도 | 16진수가 들어찬 bigint 컬럼은 크기인지 마스크인지 구별되지 않습니다. 타입 행은 시트를 읽는 사람이 보는 유일한 자리입니다 |
| 엄격함 | bigint는 크기이므로 부호와 천 단위 구분자가 정당합니다. 그 위에서는 -1도 1,000도 거부할 근거가 없습니다 |
named-range 레이아웃은 이 자리를 자기 표기로 메워 두었습니다. bit 컬럼을 bigint로 눕히고, 맨
숫자를 2진수로 읽으며(1111111이 127), 별도로 0x를 숫자 컬럼 전체에 대해 변환합니다. 셋
다 코어에는 없는 처리입니다.
2. 타입으로 두는 결정
결정. bitset은 정식 ValueType입니다. Int64 위의 역할이 아닙니다.
바로 앞 개정이 반대 방향을 택했으므로 그 대비를 적어 둡니다. text와 asset은 String으로
남고 역할만 필드에 실립니다 — 값이 무엇인지는 같고, 그 값으로 무엇을 더 하는지가 다르기
때문입니다. 문자열을 모으느냐 파일 존재를 확인하느냐는 파싱 이후의 이야기이고, 그래서 13개
생성기가 바뀔 것이 없었습니다.
bitset은 그 조건에 해당하지 않습니다. 받아들이는 표기 자체가 다릅니다.
| 구분 | text · asset | bitset |
|---|---|---|
| 파싱 | string과 동일 | 다릅니다 — 진법·부호·구분자의 허용이 갈립니다 |
| 파싱 이후 | 다릅니다 — 수집·존재 확인 | 같습니다 — 64비트 정수 |
| 판정 | 역할 | 타입 |
한 줄로 정리하면 엄격함은 의도가 선언되어야 정의됩니다. 컬럼에 「이것은 플래그다」가
적혀 있지 않으면 -1을 거부할 근거가 없습니다.
파싱 이후의 접기
bitset은 파싱이 지속되는 동안만 타입입니다. 셀이 값이 된 뒤에는 64비트 정수이고,
ModelCooker가 그 자리에서 Int64로 접습니다.
| 시점 | 타입 |
|---|---|
| 타입 행을 읽을 때 | bitset — 이름 표가 받습니다 |
| 셀을 읽을 때 | bitset — 표기 규격이 여기서 적용됩니다 |
ParseRawModel 직후 | Int64로 접힘 |
| 와이어·모든 생성기·익스포터·이력 | Int64 |
이것이 설계의 일부입니다. 처음에는 Int64가 매핑된 자리마다 한 줄을 붙일 계획이었는데,
세어 보니 생성기·익스포터·이력에 걸쳐 100곳 가까이였고 그중 어느 것도 컴파일러가 검사하지
않습니다. 조회 표는 새 멤버를 모르면 생성 시점에 예외를 throw하지만, default가 있는 switch는
틀린 결과를 냅니다. 100곳은 절반만 되어 있을 수 있는 상태이고, 접는 것은 그럴 수
없습니다.
셀을 읽는 호출은 전부 레이아웃 파서 안에 있으므로, 접는 시점에는 모두 끝나 있습니다.
접기는 파생 뷰를 무효화해야 합니다. Table.WireColumns와 Table.SerialFields는 필드
목록에 대한 캐시이고, 와이어 컬럼은 필드의 타입을 스냅샷합니다(ElementType이 init).
태그 할당이 시트를 파싱하는 도중에 그 캐시를 데워 놓으므로, 무효화하지 않으면 접기가 보이지
않습니다 — 모델의 필드는 Int64인데 익스포터가 받는 와이어 컬럼은 Bitset인 상태가 됩니다.
태그는 이 재구성으로 잃지 않습니다. 태그는 그것을 지닌 필드에 있고, 재구성은 같은 필드를 같은
순서로 읽습니다.
Field.TypeName은 계속 bitset으로 남습니다 — 시트에 그렇게 적혀 있고, 그 컬럼에 대한 보고가
그 이름으로 불러야 하기 때문입니다.
이 접기가 하는 일은 역할이 다운스트림에 대해 해 주었을 일과 같습니다. 그럼에도 타입인 이유는 파서의 분기와 진단의 이름이 필요한 자리가 접기 이전이기 때문입니다. 역할로는 그 이름을 얻지 못하고, 타입으로는 접기로 그 비용을 내지 않습니다.
번호
| 멤버 | 번호 |
|---|---|
ValueType.Bitset | 13 — 스칼라의 다음 빈자리 |
ValueType.BitsetArray | 43 — ForeignRecordArray 다음 |
3. 표기
진법 리터럴 — 숫자 타입 전체
0x·0b를 int·bigint·float·double이 모두 받습니다. bitset만의 표기가 아닙니다.
float·double을 포함하는 것이 판단이 필요한 자리인데, 근거가 둘입니다. 색상값을 16진수로
적는 것은 특정 프로젝트의 습관이 아니고, 타입 검출의 기본값이 double인 구성에서 정수
타입만 검사하면 그 컬럼들을 놓칩니다. 검출을 좁히지 않은 상태가 이 도구의 일반적인
구성이므로, 진법 허용을 정수로 한정하면 기본 구성에서 읽히지 않는 값이 생깁니다.
타입별 허용
| 표기 | int · bigint | float · double | bitset |
|---|---|---|---|
| 10진수 | 받습니다 | 받습니다 | 받습니다 |
0x… · 0b… | 받습니다 — 크기 표기 | 받습니다 — 크기 표기 | 받습니다 — 비트 패턴 |
| 진법 리터럴의 범위 | 타입의 범위. 0xFFFFFFFF는 int에서 오류 | 정확히 표현되어야 합니다. double은 2^53, float은 2^24 | 64비트 전체. 비트 63 포함 |
부호 -0x10 | 받습니다 | 받습니다 | 거부 |
| 소수점 | — | 받습니다 | 거부 — 1.0도 |
지수 1e3 | — | 받습니다 | 거부 |
천 단위 1,000 | 받습니다 | 받습니다 | 거부 |
밑줄 0b1010_1010 | 받습니다 | 받습니다 | 받습니다 — 7절 |
| 10진수 2^53 초과 | 받습니다 | 받습니다 | 거부 — 5절 ① |
| 빈 칸 | 오류 | 오류 | 오류 — 비트 패턴에 빈 칸이 뜻할 것이 없습니다 |
- | 오류 | 오류 | ?일 때만. 값이 없습니다 (빈 칸과 없음) |
규칙 셋
- 진법은 표기이고 타입의 범위를 바꾸지 않습니다.
0xFFFFFFFF가int에서 오류인 이유입니다. 32비트 마스크를 적고 싶으면 그 컬럼은bitset입니다. - 부호와 천 단위는 뜻이 있는 곳에서만 허용됩니다. 크기에는 뜻이 있고 비트 패턴에는 없습니다.
- 비트 패턴의 재해석은
bitset에서만 일어납니다.0xFFFFFFFFFFFFFFFF가 부호 있는 64비트의-1로 실리는 것은 이 타입 안에서의 규칙입니다.
1.0을 거부하는 것은 「소수부가 0이면 봐준다」가 모호함의 시작이기 때문입니다. 소수점이
있으면 표기가 실수이고, 그 컬럼은 bitset이 아닙니다.
배열과 옵셔널
bitset[]과 bitset?이 다른 타입과 같은 규칙으로 성립합니다. 배열 원소는 셀 파서의 같은
경로를 지나므로 0x10;0x20이 별도 작업 없이 읽힙니다.
4. 저장 — i64, 형식 무변경
| 항목 | 값 |
|---|---|
| 와이어 엘리먼트 | ElementI64 — 기존 것 |
| 형식 버전 | 변경 없음 |
| 모든 리더 | 변경 없음 |
| JSON | bigint와 동일 |
| 생성 코드의 타입 | bigint와 동일 (long·int64_t·BigInt 등) |
64비트가 상한이므로 새 엘리먼트가 필요하지 않고, 값은 크기가 아니라 비트 패턴이므로 부호 있는 64비트에 그대로 싣습니다.
생성 API를 이 개정에서 만들지 않습니다. 언어별 Bitset 래퍼와 Has(n) 액세서는 13개
언어 × 새 값 타입 × JSON·바이너리 판독 경로이고, 비트에 이름이 없으면 Has(3)이
& (1 << 3)보다 나은 것이 없습니다. 시트가 비트 이름을 선언하는 표기가 생긴 뒤에 판단할
일입니다(7절).
이 선택의 결과로 bigint와 다운스트림이 완전히 같아집니다. 어떤 컬럼을 bigint에서
bitset으로 바꿔도 내보낸 바이트가 한 개도 움직이지 않고, 골든 트리가 그것을 확인합니다.
5. 두 개의 함정
① 2^53 초과 10진수의 무성 반올림
Excel의 숫자 셀은 double이고, 임포터는 그것을 "R" 포맷으로 문자열화합니다. 실측입니다.
| 셀 값 | "R" 결과 | 판정 |
|---|---|---|
1111 | 1111 | 정상 |
1.5 | 1.5 | 거부 — 의도대로 |
1111111111111111 | 1111111111111111 | 정상. 2^53 아래 |
9007199254740993 | 9007199254740992 | 1 어긋납니다 |
18446744073709551615 | 1.8446744073709552E+19 | 지수 표기 → 거부 |
파싱에 쓰는 IntegerStyles에 AllowExponent가 없으므로 지수 표기는 거부됩니다. 위험 구간은
2^53 초과 ~ 1e17 미만 — 지수로 넘어가지 않으면서 이미 반올림된 값이고, 이것은 64비트
비트셋의 상위 10비트입니다.
대응 — 10진수 bitset 리터럴이 2^53을 넘으면 그 셀을 가리켜 거부하고 0x·0b를 지시합니다.
Excel의 숫자 셀은 유효자리 15개를 넘겨 담지 못하므로 이 거부로 잃는 표현이 없습니다.
셀의 출처(숫자 셀 / 텍스트 셀)를 원시 셀에 실어 구별하면 더 정밀하지만 임포터까지 손이 가므로, 시트 작성자가 실제로 불편을 제기한 뒤에 판단합니다.
② 맨 숫자의 2진수 해석 — 레이아웃 존치
bit 컬럼의 1111111은 지금 127입니다. bitset이 맨 숫자를 10진수로 읽으므로, 그대로
옮기면 같은 셀이 1,111,111이 됩니다. 값 대조로 드러나지 않는 종류의 변화입니다.
대응 — 레이아웃이 0b를 붙여서 코어에 넘깁니다.
시트의 bit 컬럼: 1111111 → 레이아웃이 "0b1111111"로 → 코어 bitset이 127
맨 숫자를 2진수로 읽는 것은 그 시트들의 표기이므로 레이아웃 파일에 남고, 코어 타입은 모호하지 않은 표기만 받습니다. 코어에 「어느 레이아웃에서는 맨 숫자가 2진수」라는 예외가 생기지 않습니다.
같은 이관에서 그 레이아웃의 0x 분기와 그 헬퍼가 삭제됩니다 — 3절이 코어로 옮긴 것과
같은 처리이기 때문입니다. LooksHexadecimal은 남습니다. 변환에 쓰이던 것이 아니라 타입
검출이 16진수 셀을 건너뛰는 데 쓰이고, 그쪽은 여전히 필요합니다 — 폭을 고르려고 셀을
10진수로 파싱하는 자리이고 0x5f0300은 10진수가 아닙니다.
이관의 실측 결과. 이전 대규모 코퍼스의 부분 빌드 7개 테이블(278컬럼)을 이관 전후로 내보내 비교했습니다.
| 비교 | 결과 |
|---|---|
.tcb 7개 | 바이트 동일 |
| 인코딩 보고서 | 동일 |
| 매니페스트 | 타임스탬프만 다름 |
| 이전 대규모 코퍼스의 커밋된 산출물 | diff 0 |
그리고 이 게이트가 bit 경로를 실제로 밟았는지를 변이 프로브로 확인했습니다 — 접두를 0b
대신 0x로 바꾸면 Roster.tcb와 GearSlot.tcb의 바이트가 달라집니다. 통과가 「경로가 없어서
통과」가 아니라는 뜻입니다.