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
This commit is contained in:
AncTe committed 2026-09-15 18:35:18 +08:00
1 parent c7e5a643c2
commit 528b8c91a5
2 files changed
+91 -48

No files matched your search

@@ -79,7 +79,7 @@ function expectStepTyped(
expect(step.index).toBe(index); expect(step.index).toBe(index);
} }
// 整数与浮点参数用例:值 → 期望参数类型 token(int64 见 #06-04-1 跳过) // 整数与浮点参数用例:值 → 期望参数类型 token(int64 见下方专门用例)
const paramCases: ReadonlyArray<readonly [ReplayParamValue, number]> = [ const paramCases: ReadonlyArray<readonly [ReplayParamValue, number]> = [
[-128, 1], [-128, 1],
[127, 1], [127, 1],
@@ -91,8 +91,8 @@ const paramCases: ReadonlyArray<readonly [ReplayParamValue, number]> = [
[-32769, 3], [-32769, 3],
[-2147483648, 3], [-2147483648, 3],
[2147483647, 3], [2147483647, 3],
[1.5, 5], [1.5, 6],
[-1.5, 5] [-1.5, 6]
]; ];
// 异质命令序列:参数个数与类型各不相同(含 boolean、多位宽整数、float、string、bigint) // 异质命令序列:参数个数与类型各不相同(含 boolean、多位宽整数、float、string、bigint)
@@ -271,7 +271,7 @@ describe('ReplayArray param codec', () => {
array.add(0, [100n]); array.add(0, [100n]);
expect(array.get(0).params).toEqual([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 // 疑似 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]); expect(array.get(0).params).toEqual([value]);
}); });
// 疑似 bug:int64 解码乘数误用 2147483647,导致 int64 参数无法按写入值读回,详见 06-TEST-FINDINGS.md #06-04-1,修复后取消 skip // 验证超过 int32 范围的非负 int64 参数经 type 4 精确读回
it.skip('round-trips int64 values above the int32 range', () => { it('round-trips int64 values above the int32 range', () => {
const array = createArray(); const array = createArray();
array.add(0, [2147483648]); array.add(0, [2147483648]);
expect(array.get(0).params).toEqual([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<readonly [number, number]> = [
[-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 // 疑似 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', () => { it.skip('round-trips a heterogeneous step mixing a multi-byte bigint and an int64 value', () => {
const array = createArray(); 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', () => { it('round-trips a short string with an inline type token', () => {
const array = createArray(); const array = createArray();
array.add(0, ['hi']); array.add(0, ['hi']);
expect(array.get(0).params).toEqual(['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', () => { it('round-trips a long string through the length-prefixed type', () => {
const array = createArray(); const array = createArray();
const value = 'a'.repeat(300); const value = 'a'.repeat(300);
array.add(0, [value]); array.add(0, [value]);
expect(array.get(0).params).toEqual([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', () => { it('round-trips an empty string', () => {
const array = createArray(); const array = createArray();
array.add(0, ['']); array.add(0, ['']);
expect(array.get(0).params).toEqual(['']); expect(array.get(0).params).toEqual(['']);
expect(firstParamToken(array)).toBe(7); expect(firstParamToken(array)).toBe(9);
}); });
// 验证单条录像步的多个不同类型参数按顺序完整读回 // 验证单条录像步的多个不同类型参数按顺序完整读回
+49 -36
View File
@@ -16,11 +16,13 @@ interface INormalizedParam {
* - 1: int8 * - 1: int8
* - 2: int16 * - 2: int16
* - 3: int32 * - 3: int32
* - 4: int64 * - 4: 非负 int64,载荷为幅值
* - 5: float * - 5: 负 int64,载荷为幅值
* - 6: bigint * - 6: float
* - 7: string * - 7: 非负 bigint,载荷为幅值
* - 8 ~ 255: n - 7 长度的字符串 * - 8: 负 bigint,载荷为幅值
* - 9: string,带 int32 长度前缀
* - 10 ~ 255: n - 9 长度的字符串
*/ */
readonly paramType: number; readonly paramType: number;
@@ -237,14 +239,18 @@ export class ReplayArray implements IReplayArray {
// 3 - int32 // 3 - int32
type = 3; type = 3;
byte = 5; byte = 5;
} else { } else if (param >= 2147483648) {
// 4 - int64 // 4 - 非负 int64
type = 4; type = 4;
byte = 9; byte = 9;
} else {
// 5 - 负 int64,载荷为幅值
type = 5;
byte = 9;
} }
} else { } else {
// 5 - float // 6 - float
type = 5; type = 6;
byte = 9; byte = 9;
} }
return { return {
@@ -253,7 +259,7 @@ export class ReplayArray implements IReplayArray {
byteLength: byte byteLength: byte
}; };
} else if (typeof param === 'bigint') { } else if (typeof param === 'bigint') {
// 6 - bigint // 7 - bigint
const wall = 2n ** 2047n; const wall = 2n ** 2047n;
if (param > wall - 1n || param < -wall) { if (param > wall - 1n || param < -wall) {
logger.warn(152); logger.warn(152);
@@ -269,23 +275,23 @@ export class ReplayArray implements IReplayArray {
arr[i] = Number(remain); arr[i] = Number(remain);
} }
return { return {
paramType: 6, paramType: 7,
paramValue: arr, paramValue: arr,
byteLength: arr.length + 2 byteLength: arr.length + 2
}; };
} else if (typeof param === 'string') { } else if (typeof param === 'string') {
const arr = this.textEncoder.encode(param); const arr = this.textEncoder.encode(param);
if (arr.length > 0 && arr.length <= 248) { if (arr.length > 0 && arr.length <= 246) {
// 8 ~ 255 - string // 10 ~ 255 - string
return { return {
paramType: arr.length + 7, paramType: arr.length + 9,
paramValue: arr, paramValue: arr,
byteLength: arr.length + 1 byteLength: arr.length + 1
}; };
} else { } else {
// 7 - string // 9 - string
return { return {
paramType: 7, paramType: 9,
paramValue: arr, paramValue: arr,
byteLength: arr.length + 5 byteLength: arr.length + 5
}; };
@@ -364,25 +370,26 @@ export class ReplayArray implements IReplayArray {
} else if (param.paramType === 3) { } else if (param.paramType === 3) {
// 3 - int32 // 3 - int32
this.paramView.setInt32(index + 1, num); this.paramView.setInt32(index + 1, num);
} else if (param.paramType === 4) { } else if (param.paramType === 4 || param.paramType === 5) {
// 4 - int64 // 4 - 非负 int64 / 5 - 负 int64,均以幅值拆分高低 32 位
const high = Math.floor(num / 2147483648); const magnitude = num < 0 ? -num : num;
const low = num % 2147483648; const high = Math.floor(magnitude / 2147483648);
const low = magnitude - high * 2147483648;
this.paramView.setInt32(index + 1, low); this.paramView.setInt32(index + 1, low);
this.paramView.setInt32(index + 5, high); this.paramView.setInt32(index + 5, high);
} else if (param.paramType === 5) {
// 5 - float
this.paramView.setFloat64(index + 1, num);
} else if (param.paramType === 6) { } 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[index + 1] = arr.length;
this.paramArray.set(arr, index + 2); this.paramArray.set(arr, index + 2);
} else if (param.paramType === 7) { } else if (param.paramType === 9) {
// 7 - string // 9 - string
this.paramView.setInt32(index + 1, arr.length); this.paramView.setInt32(index + 1, arr.length);
this.paramArray.set(arr, index + 5); this.paramArray.set(arr, index + 5);
} else { } else {
// 8 ~ 256 - string // 10 ~ 255 - string
this.paramArray.set(arr, index + 1); this.paramArray.set(arr, index + 1);
} }
index += param.byteLength; index += param.byteLength;
@@ -614,17 +621,23 @@ export class ReplayArray implements IReplayArray {
byte = 5; byte = 5;
value = this.paramView.getInt32(startIndex + 1); value = this.paramView.getInt32(startIndex + 1);
} else if (type === 4) { } else if (type === 4) {
// 4 - int64 // 4 - 非负 int64
const low = this.paramView.getInt32(startIndex + 1); const low = this.paramView.getInt32(startIndex + 1);
const high = this.paramView.getInt32(startIndex + 5); const high = this.paramView.getInt32(startIndex + 5);
byte = 9; byte = 9;
value = low + high * 2147483647; value = low + high * 2147483648;
} else if (type === 5) { } 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; byte = 9;
value = this.paramView.getFloat64(startIndex + 1); value = this.paramView.getFloat64(startIndex + 1);
} else if (type === 6) { } else if (type === 7) {
// 6 - bigint // 7 - bigint
const length = this.paramView.getInt8(startIndex + 1); const length = this.paramView.getInt8(startIndex + 1);
let base = 0n; let base = 0n;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
@@ -633,16 +646,16 @@ export class ReplayArray implements IReplayArray {
} }
byte = length + 2; byte = length + 2;
value = base; value = base;
} else if (type === 7) { } else if (type === 9) {
// 7 - string // 9 - string
const length = this.paramView.getInt32(startIndex + 1); const length = this.paramView.getInt32(startIndex + 1);
const endIndex = startIndex + 5 + length; const endIndex = startIndex + 5 + length;
const arr = this.paramArray.slice(startIndex + 5, endIndex); const arr = this.paramArray.slice(startIndex + 5, endIndex);
byte = length + 5; byte = length + 5;
value = this.textDecoder.decode(arr); value = this.textDecoder.decode(arr);
} else { } else {
// 8 ~ 255 - string // 10 ~ 255 - string
const length = type - 7; const length = type - 9;
const endIndex = startIndex + 1 + length; const endIndex = startIndex + 1 + length;
const arr = this.paramArray.slice(startIndex + 1, endIndex); const arr = this.paramArray.slice(startIndex + 1, endIndex);
byte = length + 1; byte = length + 1;