From 528b8c91a53ecc74753fd191e7bc6d2fa2cc361d Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 15 Sep 2026 18:35:18 +0800 Subject: [PATCH] fix(07-03): #06-04-1 round-trip negative int64 and renumber param type codes - Correct the int64 decode multiplier to 2147483648 - Encode negative int64 as dedicated type 5 with magnitude payload - Shift float/bigint/long-string/short-string type codes to 6/7/9/length+9 - Un-skip the int64 round-trip case and add negative int64 focused cases --- .../data-common/src/replay/array.test.ts | 54 +++++++++--- packages-user/data-common/src/replay/array.ts | 85 +++++++++++-------- 2 files changed, 91 insertions(+), 48 deletions(-) diff --git a/packages-user/data-common/src/replay/array.test.ts b/packages-user/data-common/src/replay/array.test.ts index ac8e1ce..b0baf78 100644 --- a/packages-user/data-common/src/replay/array.test.ts +++ b/packages-user/data-common/src/replay/array.test.ts @@ -79,7 +79,7 @@ function expectStepTyped( expect(step.index).toBe(index); } -// 整数与浮点参数用例:值 → 期望参数类型 token(int64 见 #06-04-1 跳过) +// 整数与浮点参数用例:值 → 期望参数类型 token(int64 见下方专门用例) const paramCases: ReadonlyArray = [ [-128, 1], [127, 1], @@ -91,8 +91,8 @@ const paramCases: ReadonlyArray = [ [-32769, 3], [-2147483648, 3], [2147483647, 3], - [1.5, 5], - [-1.5, 5] + [1.5, 6], + [-1.5, 6] ]; // 异质命令序列:参数个数与类型各不相同(含 boolean、多位宽整数、float、string、bigint) @@ -271,7 +271,7 @@ describe('ReplayArray param codec', () => { array.add(0, [100n]); expect(array.get(0).params).toEqual([100n]); - expect(firstParamToken(array)).toBe(6); + expect(firstParamToken(array)).toBe(7); }); // 疑似 bug:bigint 编码循环缺少按字节右移,多字节 bigint 只能还原最低字节,详见 06-TEST-FINDINGS.md #06-04-2,修复后取消 skip @@ -283,13 +283,43 @@ describe('ReplayArray param codec', () => { expect(array.get(0).params).toEqual([value]); }); - // 疑似 bug:int64 解码乘数误用 2147483647,导致 int64 参数无法按写入值读回,详见 06-TEST-FINDINGS.md #06-04-1,修复后取消 skip - it.skip('round-trips int64 values above the int32 range', () => { + // 验证超过 int32 范围的非负 int64 参数经 type 4 精确读回 + it('round-trips int64 values above the int32 range', () => { const array = createArray(); array.add(0, [2147483648]); expect(array.get(0).params).toEqual([2147483648]); }); + // 负 int64 使用独立类型码 5,载荷为幅值 |n|,解码后取负(A8);小负值仍走更窄的位宽类型 + it('round-trips negative int64 values through the dedicated type', () => { + const negativeCases: ReadonlyArray = [ + [-1, 1], + [-2147483649, 5], + [-4294967297, 5] + ]; + + for (const [value, token] of negativeCases) { + const array = createArray(); + array.add(0, [value]); + expectParamTyped(array.get(0).params[0], value); + expect(firstParamToken(array)).toBe(token); + } + }); + + // 非负 int64 仍为 type 4,上界 2^53 - 1 精确读回,幅值编码逐位不变 + it('round-trips non-negative int64 values through type 4', () => { + const nonNegativeCases: readonly number[] = [ + 2147483648, 9007199254740991 + ]; + + for (const value of nonNegativeCases) { + const array = createArray(); + array.add(0, [value]); + expectParamTyped(array.get(0).params[0], value); + expect(firstParamToken(array)).toBe(4); + } + }); + // 疑似 bug:多字节 bigint 与超 int32 的 int64 混在同一步时同样失真,详见 06-TEST-FINDINGS.md #06-04-1/#06-04-2,修复两个编码与解码缺陷后取消 skip it.skip('round-trips a heterogeneous step mixing a multi-byte bigint and an int64 value', () => { const array = createArray(); @@ -303,32 +333,32 @@ describe('ReplayArray param codec', () => { ]); }); - // 验证短字符串参数使用内联类型 token 并读回一致 + // 验证短字符串参数使用内联类型 token(长度 + 9)并读回一致 it('round-trips a short string with an inline type token', () => { const array = createArray(); array.add(0, ['hi']); expect(array.get(0).params).toEqual(['hi']); - expect(firstParamToken(array)).toBe(9); + expect(firstParamToken(array)).toBe(11); }); - // 验证超过内联长度的字符串参数使用 type 7 并读回一致 + // 验证超过内联长度的字符串参数使用 type 9 并读回一致 it('round-trips a long string through the length-prefixed type', () => { const array = createArray(); const value = 'a'.repeat(300); array.add(0, [value]); expect(array.get(0).params).toEqual([value]); - expect(firstParamToken(array)).toBe(7); + expect(firstParamToken(array)).toBe(9); }); - // 验证空字符串参数回退到 type 7 并读回为空串 + // 验证空字符串参数回退到 type 9 并读回为空串 it('round-trips an empty string', () => { const array = createArray(); array.add(0, ['']); expect(array.get(0).params).toEqual(['']); - expect(firstParamToken(array)).toBe(7); + expect(firstParamToken(array)).toBe(9); }); // 验证单条录像步的多个不同类型参数按顺序完整读回 diff --git a/packages-user/data-common/src/replay/array.ts b/packages-user/data-common/src/replay/array.ts index 97a6825..9a4dab5 100644 --- a/packages-user/data-common/src/replay/array.ts +++ b/packages-user/data-common/src/replay/array.ts @@ -16,11 +16,13 @@ interface INormalizedParam { * - 1: int8 * - 2: int16 * - 3: int32 - * - 4: int64 - * - 5: float - * - 6: bigint - * - 7: string - * - 8 ~ 255: n - 7 长度的字符串 + * - 4: 非负 int64,载荷为幅值 + * - 5: 负 int64,载荷为幅值 + * - 6: float + * - 7: 非负 bigint,载荷为幅值 + * - 8: 负 bigint,载荷为幅值 + * - 9: string,带 int32 长度前缀 + * - 10 ~ 255: n - 9 长度的字符串 */ readonly paramType: number; @@ -237,14 +239,18 @@ export class ReplayArray implements IReplayArray { // 3 - int32 type = 3; byte = 5; - } else { - // 4 - int64 + } else if (param >= 2147483648) { + // 4 - 非负 int64 type = 4; byte = 9; + } else { + // 5 - 负 int64,载荷为幅值 + type = 5; + byte = 9; } } else { - // 5 - float - type = 5; + // 6 - float + type = 6; byte = 9; } return { @@ -253,7 +259,7 @@ export class ReplayArray implements IReplayArray { byteLength: byte }; } else if (typeof param === 'bigint') { - // 6 - bigint + // 7 - bigint const wall = 2n ** 2047n; if (param > wall - 1n || param < -wall) { logger.warn(152); @@ -269,23 +275,23 @@ export class ReplayArray implements IReplayArray { arr[i] = Number(remain); } return { - paramType: 6, + paramType: 7, paramValue: arr, byteLength: arr.length + 2 }; } else if (typeof param === 'string') { const arr = this.textEncoder.encode(param); - if (arr.length > 0 && arr.length <= 248) { - // 8 ~ 255 - string + if (arr.length > 0 && arr.length <= 246) { + // 10 ~ 255 - string return { - paramType: arr.length + 7, + paramType: arr.length + 9, paramValue: arr, byteLength: arr.length + 1 }; } else { - // 7 - string + // 9 - string return { - paramType: 7, + paramType: 9, paramValue: arr, byteLength: arr.length + 5 }; @@ -364,25 +370,26 @@ export class ReplayArray implements IReplayArray { } else if (param.paramType === 3) { // 3 - int32 this.paramView.setInt32(index + 1, num); - } else if (param.paramType === 4) { - // 4 - int64 - const high = Math.floor(num / 2147483648); - const low = num % 2147483648; + } else if (param.paramType === 4 || param.paramType === 5) { + // 4 - 非负 int64 / 5 - 负 int64,均以幅值拆分高低 32 位 + const magnitude = num < 0 ? -num : num; + const high = Math.floor(magnitude / 2147483648); + const low = magnitude - high * 2147483648; this.paramView.setInt32(index + 1, low); this.paramView.setInt32(index + 5, high); - } else if (param.paramType === 5) { - // 5 - float - this.paramView.setFloat64(index + 1, num); } else if (param.paramType === 6) { - // 6 - bigint + // 6 - float + this.paramView.setFloat64(index + 1, num); + } else if (param.paramType === 7) { + // 7 - bigint this.paramArray[index + 1] = arr.length; this.paramArray.set(arr, index + 2); - } else if (param.paramType === 7) { - // 7 - string + } else if (param.paramType === 9) { + // 9 - string this.paramView.setInt32(index + 1, arr.length); this.paramArray.set(arr, index + 5); } else { - // 8 ~ 256 - string + // 10 ~ 255 - string this.paramArray.set(arr, index + 1); } index += param.byteLength; @@ -614,17 +621,23 @@ export class ReplayArray implements IReplayArray { byte = 5; value = this.paramView.getInt32(startIndex + 1); } else if (type === 4) { - // 4 - int64 + // 4 - 非负 int64 const low = this.paramView.getInt32(startIndex + 1); const high = this.paramView.getInt32(startIndex + 5); byte = 9; - value = low + high * 2147483647; + value = low + high * 2147483648; } else if (type === 5) { - // 5 - float + // 5 - 负 int64,读回幅值后取负 + const low = this.paramView.getInt32(startIndex + 1); + const high = this.paramView.getInt32(startIndex + 5); + byte = 9; + value = -(low + high * 2147483648); + } else if (type === 6) { + // 6 - float byte = 9; value = this.paramView.getFloat64(startIndex + 1); - } else if (type === 6) { - // 6 - bigint + } else if (type === 7) { + // 7 - bigint const length = this.paramView.getInt8(startIndex + 1); let base = 0n; for (let i = 0; i < length; i++) { @@ -633,16 +646,16 @@ export class ReplayArray implements IReplayArray { } byte = length + 2; value = base; - } else if (type === 7) { - // 7 - string + } else if (type === 9) { + // 9 - string const length = this.paramView.getInt32(startIndex + 1); const endIndex = startIndex + 5 + length; const arr = this.paramArray.slice(startIndex + 5, endIndex); byte = length + 5; value = this.textDecoder.decode(arr); } else { - // 8 ~ 255 - string - const length = type - 7; + // 10 ~ 255 - string + const length = type - 9; const endIndex = startIndex + 1 + length; const arr = this.paramArray.slice(startIndex + 1, endIndex); byte = length + 1;