hm-pt-core 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/trace/parse.d.ts +12 -1
- package/dist/trace/parse.js +162 -19
- package/dist/trace/printf.d.ts +9 -0
- package/dist/trace/printf.js +204 -38
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type { RecordSampleHeader, CallchainEntry, RawEntry, CallchainFramesEntry
|
|
|
5
5
|
export { isPerfDataLike } from "./perf/is_perf_data_like.js";
|
|
6
6
|
export type { HitraceRecordSnapshot, HitraceThreadIndexSnapshot, } from "./hitrace/contract.js";
|
|
7
7
|
export type { TraceFormatField, ParsedTraceFormat, FieldDictTransformFn, TraceParserRegistry, DecodedRawSample, TracePrintMode, DecodeRawByRegistryOptions, } from "./trace/types.js";
|
|
8
|
-
export type { Endian } from "./trace/parse.js";
|
|
8
|
+
export type { Endian, BuildTraceParserRegistryOptions } from "./trace/parse.js";
|
|
9
|
+
export { TraceFieldDecodeError } from "./trace/parse.js";
|
|
9
10
|
export type { PrintfSpec } from "./trace/printf.js";
|
|
10
11
|
export { parseTraceFormat, parseCommonFieldsFromRaw, parseAllFieldsFromRaw, rawHexLinesToBuffer, bufferToRawHexLines, buildTraceParserRegistry, } from "./trace/parse.js";
|
|
11
12
|
export { formatPrintfValue, tokenizePrintfFormat } from "./trace/printf.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* hm-pt-core:perf trace 公共类型与 trace format 解码原语。
|
|
3
3
|
*/
|
|
4
4
|
export { isPerfDataLike } from "./perf/is_perf_data_like.js";
|
|
5
|
+
export { TraceFieldDecodeError } from "./trace/parse.js";
|
|
5
6
|
export { parseTraceFormat, parseCommonFieldsFromRaw, parseAllFieldsFromRaw, rawHexLinesToBuffer, bufferToRawHexLines, buildTraceParserRegistry, } from "./trace/parse.js";
|
|
6
7
|
export { formatPrintfValue, tokenizePrintfFormat } from "./trace/printf.js";
|
|
7
8
|
export { decodeRawByRegistry } from "./trace/decode_raw.js";
|
package/dist/trace/parse.d.ts
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { FieldDictTransformFn, ParsedTraceFormat, TraceParserRegistry } from "./types.js";
|
|
6
6
|
export type Endian = "le" | "be";
|
|
7
|
+
export declare class TraceFieldDecodeError extends Error {
|
|
8
|
+
readonly fieldName: string;
|
|
9
|
+
readonly offset: number;
|
|
10
|
+
readonly size: number;
|
|
11
|
+
readonly rawLength: number;
|
|
12
|
+
constructor(message: string, fieldName: string, offset: number, size: number, rawLength: number);
|
|
13
|
+
}
|
|
14
|
+
export interface BuildTraceParserRegistryOptions {
|
|
15
|
+
/** 允许后注册的 format 覆盖同 ID 事件;默认 false,重复 ID 会抛错 */
|
|
16
|
+
allowDuplicateEventIds?: boolean;
|
|
17
|
+
}
|
|
7
18
|
/**
|
|
8
19
|
* 解析 sample/trace_format 风格的文本,得到字段列表
|
|
9
20
|
*/
|
|
@@ -45,4 +56,4 @@ export declare function bufferToRawHexLines(raw: Uint8Array, bytesPerLine?: numb
|
|
|
45
56
|
export declare function buildTraceParserRegistry(formatTexts: Array<string | {
|
|
46
57
|
text: string;
|
|
47
58
|
transformFieldDict?: FieldDictTransformFn;
|
|
48
|
-
}
|
|
59
|
+
}>, options?: BuildTraceParserRegistryOptions): TraceParserRegistry;
|
package/dist/trace/parse.js
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
* @module traceFormat
|
|
4
4
|
*/
|
|
5
5
|
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: false });
|
|
6
|
+
export class TraceFieldDecodeError extends Error {
|
|
7
|
+
fieldName;
|
|
8
|
+
offset;
|
|
9
|
+
size;
|
|
10
|
+
rawLength;
|
|
11
|
+
constructor(message, fieldName, offset, size, rawLength) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.fieldName = fieldName;
|
|
14
|
+
this.offset = offset;
|
|
15
|
+
this.size = size;
|
|
16
|
+
this.rawLength = rawLength;
|
|
17
|
+
this.name = "TraceFieldDecodeError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
6
20
|
const FIELD_LINE_RE = /^\s*field\s*:\s*([^;]+)\s*;\s*offset\s*:\s*(\d+)\s*;\s*size\s*:\s*(\d+)\s*;\s*signed\s*:\s*([01])\s*;?\s*$/i;
|
|
7
21
|
function hexNibble(ch) {
|
|
8
22
|
const code = ch.charCodeAt(0);
|
|
@@ -16,10 +30,129 @@ function hexNibble(ch) {
|
|
|
16
30
|
function hexPairToByte(hi, lo) {
|
|
17
31
|
const h = hexNibble(hi);
|
|
18
32
|
const l = hexNibble(lo);
|
|
19
|
-
if (h < 0 || l < 0)
|
|
20
|
-
|
|
33
|
+
if (h < 0 || l < 0) {
|
|
34
|
+
throw new Error(`Invalid hex byte: "${hi}${lo}"`);
|
|
35
|
+
}
|
|
21
36
|
return (h << 4) | l;
|
|
22
37
|
}
|
|
38
|
+
function readCharArrayAsString(raw, offset, size, fieldName) {
|
|
39
|
+
if (offset < 0 || offset + size > raw.length) {
|
|
40
|
+
throw new TraceFieldDecodeError(`Field "${fieldName}" out of bounds: offset=${offset}, size=${size}, raw.length=${raw.length}`, fieldName, offset, size, raw.length);
|
|
41
|
+
}
|
|
42
|
+
const bytes = raw.subarray(offset, offset + size);
|
|
43
|
+
const nul = bytes.indexOf(0);
|
|
44
|
+
const slice = nul >= 0 ? bytes.subarray(0, nul) : bytes;
|
|
45
|
+
return UTF8_DECODER.decode(slice);
|
|
46
|
+
}
|
|
47
|
+
function decodePrintFmtEscape(line, escapeStart) {
|
|
48
|
+
let i = escapeStart + 1;
|
|
49
|
+
if (i >= line.length) {
|
|
50
|
+
throw new Error(`Incomplete escape in print fmt string literal at index ${escapeStart}`);
|
|
51
|
+
}
|
|
52
|
+
const esc = line[i];
|
|
53
|
+
i++;
|
|
54
|
+
switch (esc) {
|
|
55
|
+
case '"':
|
|
56
|
+
return { value: '"', next: i };
|
|
57
|
+
case "\\":
|
|
58
|
+
return { value: "\\", next: i };
|
|
59
|
+
case "n":
|
|
60
|
+
return { value: "\n", next: i };
|
|
61
|
+
case "t":
|
|
62
|
+
return { value: "\t", next: i };
|
|
63
|
+
case "r":
|
|
64
|
+
return { value: "\r", next: i };
|
|
65
|
+
case "x": {
|
|
66
|
+
if (i + 1 >= line.length) {
|
|
67
|
+
throw new Error(`Incomplete \\x escape in print fmt string literal at index ${escapeStart}`);
|
|
68
|
+
}
|
|
69
|
+
const hi = hexNibble(line[i]);
|
|
70
|
+
const lo = hexNibble(line[i + 1]);
|
|
71
|
+
if (hi < 0 || lo < 0) {
|
|
72
|
+
throw new Error(`Invalid \\x escape in print fmt string literal at index ${escapeStart}`);
|
|
73
|
+
}
|
|
74
|
+
return { value: String.fromCharCode((hi << 4) | lo), next: i + 2 };
|
|
75
|
+
}
|
|
76
|
+
default: {
|
|
77
|
+
if (esc >= "0" && esc <= "7") {
|
|
78
|
+
let oct = esc.charCodeAt(0) - 48;
|
|
79
|
+
let digits = 1;
|
|
80
|
+
while (digits < 3 && i < line.length && line[i] >= "0" && line[i] <= "7") {
|
|
81
|
+
oct = oct * 8 + (line[i].charCodeAt(0) - 48);
|
|
82
|
+
i++;
|
|
83
|
+
digits++;
|
|
84
|
+
}
|
|
85
|
+
return { value: String.fromCharCode(oct & 0xff), next: i };
|
|
86
|
+
}
|
|
87
|
+
return { value: "\\" + esc, next: i };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** 扫描 print fmt 行中的双引号字符串字面量,支持常见 C 字符串转义 */
|
|
92
|
+
function parsePrintFmtStringLiteral(line, start) {
|
|
93
|
+
if (line[start] !== '"')
|
|
94
|
+
return undefined;
|
|
95
|
+
let i = start + 1;
|
|
96
|
+
let value = "";
|
|
97
|
+
while (i < line.length) {
|
|
98
|
+
const ch = line[i];
|
|
99
|
+
if (ch === "\\") {
|
|
100
|
+
const decoded = decodePrintFmtEscape(line, i);
|
|
101
|
+
value += decoded.value;
|
|
102
|
+
i = decoded.next;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (ch === '"') {
|
|
106
|
+
return { value, end: i + 1 };
|
|
107
|
+
}
|
|
108
|
+
value += ch;
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function scanBalancedPrintArgs(argsPart) {
|
|
114
|
+
const args = [];
|
|
115
|
+
let i = 0;
|
|
116
|
+
while (i < argsPart.length) {
|
|
117
|
+
while (i < argsPart.length && /\s/.test(argsPart[i]))
|
|
118
|
+
i++;
|
|
119
|
+
if (i >= argsPart.length)
|
|
120
|
+
break;
|
|
121
|
+
const start = i;
|
|
122
|
+
let depth = 0;
|
|
123
|
+
while (i < argsPart.length) {
|
|
124
|
+
const ch = argsPart[i];
|
|
125
|
+
if (ch === "(")
|
|
126
|
+
depth++;
|
|
127
|
+
else if (ch === ")")
|
|
128
|
+
depth = Math.max(0, depth - 1);
|
|
129
|
+
else if (ch === "," && depth === 0)
|
|
130
|
+
break;
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
const expr = argsPart.slice(start, i).trim();
|
|
134
|
+
if (expr)
|
|
135
|
+
args.push(expr);
|
|
136
|
+
if (argsPart[i] === ",")
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
return args;
|
|
140
|
+
}
|
|
141
|
+
function parsePrintFmtLine(line) {
|
|
142
|
+
const prefix = line.match(/^print\s+fmt\s*:\s*/i);
|
|
143
|
+
if (!prefix)
|
|
144
|
+
return undefined;
|
|
145
|
+
const lit = parsePrintFmtStringLiteral(line, prefix[0].length);
|
|
146
|
+
if (!lit)
|
|
147
|
+
return undefined;
|
|
148
|
+
const rest = line.slice(lit.end).trim();
|
|
149
|
+
if (!rest)
|
|
150
|
+
return { printFmt: lit.value, printArgs: [] };
|
|
151
|
+
const argsMatch = rest.match(/^,\s*(.*)$/);
|
|
152
|
+
if (!argsMatch)
|
|
153
|
+
return undefined;
|
|
154
|
+
return { printFmt: lit.value, printArgs: scanBalancedPrintArgs(argsMatch[1]) };
|
|
155
|
+
}
|
|
23
156
|
function splitTypeAndName(rest) {
|
|
24
157
|
const trimmed = rest.trim();
|
|
25
158
|
const m = trimmed.match(/^(.+?)\s+(\w+(?:\[[^\]]*])?)$/);
|
|
@@ -61,17 +194,10 @@ export function parseTraceFormat(text) {
|
|
|
61
194
|
continue;
|
|
62
195
|
}
|
|
63
196
|
if (/^print\s+fmt\s*:/i.test(t)) {
|
|
64
|
-
const pf = t
|
|
197
|
+
const pf = parsePrintFmtLine(t);
|
|
65
198
|
if (pf) {
|
|
66
|
-
printFmt = pf
|
|
67
|
-
|
|
68
|
-
if (argsPart) {
|
|
69
|
-
const exprs = argsPart.match(/REC->[^,]+/g);
|
|
70
|
-
printArgs = exprs ? exprs.map((s) => s.trim()) : [];
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
printArgs = [];
|
|
74
|
-
}
|
|
199
|
+
printFmt = pf.printFmt;
|
|
200
|
+
printArgs = pf.printArgs;
|
|
75
201
|
}
|
|
76
202
|
break;
|
|
77
203
|
}
|
|
@@ -92,6 +218,10 @@ export function parseTraceFormat(text) {
|
|
|
92
218
|
function normalizeType(t) {
|
|
93
219
|
return t.replace(/\s+/g, " ").trim().toLowerCase();
|
|
94
220
|
}
|
|
221
|
+
function isCharType(typeName) {
|
|
222
|
+
const t = normalizeType(typeName);
|
|
223
|
+
return t === "char" || t === "signed char" || t === "unsigned char";
|
|
224
|
+
}
|
|
95
225
|
/**
|
|
96
226
|
* 从 little-endian 原始缓冲区按字段描述读一个标量
|
|
97
227
|
*/
|
|
@@ -202,6 +332,9 @@ export function parseAllFieldsFromRaw(raw, format) {
|
|
|
202
332
|
out[locKey] = loc;
|
|
203
333
|
const offset = loc & 0xffff;
|
|
204
334
|
const len = (loc >>> 16) & 0xffff;
|
|
335
|
+
if (len > 0 && (offset < 0 || offset + len > raw.length)) {
|
|
336
|
+
throw new TraceFieldDecodeError(`Field "${field.name}" __data_loc out of bounds: offset=${offset}, len=${len}, raw.length=${raw.length}`, field.name, offset, len, raw.length);
|
|
337
|
+
}
|
|
205
338
|
if (offset >= 0 && len > 0 && offset + len <= raw.length) {
|
|
206
339
|
const bytes = raw.subarray(offset, offset + len);
|
|
207
340
|
// 去掉末尾 \0
|
|
@@ -215,6 +348,10 @@ export function parseAllFieldsFromRaw(raw, format) {
|
|
|
215
348
|
}
|
|
216
349
|
continue;
|
|
217
350
|
}
|
|
351
|
+
if (arr && isCharType(field.typeName)) {
|
|
352
|
+
out[key] = readCharArrayAsString(raw, field.offset, field.size, field.name);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
218
355
|
const v = readFieldValue(view, field);
|
|
219
356
|
if (v !== undefined) {
|
|
220
357
|
out[key] = v;
|
|
@@ -233,8 +370,9 @@ export function rawHexLinesToBuffer(lines, endian = "le") {
|
|
|
233
370
|
let totalBytes = 0;
|
|
234
371
|
for (const { hex } of lines) {
|
|
235
372
|
let s = hex.replace(/^0x/i, "").trim();
|
|
236
|
-
if (!s)
|
|
237
|
-
|
|
373
|
+
if (!s) {
|
|
374
|
+
throw new Error(`Invalid hex token: "${hex}"`);
|
|
375
|
+
}
|
|
238
376
|
if (s.length % 2 === 1)
|
|
239
377
|
s = "0" + s;
|
|
240
378
|
normalizedTokens.push(s);
|
|
@@ -261,8 +399,9 @@ export function rawHexLinesToBuffer(lines, endian = "le") {
|
|
|
261
399
|
* 默认每行 4 字节(即 8 hex digits)。
|
|
262
400
|
*/
|
|
263
401
|
export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
|
|
264
|
-
if (bytesPerLine <= 0)
|
|
265
|
-
|
|
402
|
+
if (!Number.isSafeInteger(bytesPerLine) || bytesPerLine <= 0) {
|
|
403
|
+
throw new RangeError(`bytesPerLine must be a positive safe integer, got ${bytesPerLine}`);
|
|
404
|
+
}
|
|
266
405
|
const out = [];
|
|
267
406
|
for (let i = 0; i < raw.length; i += bytesPerLine) {
|
|
268
407
|
const end = Math.min(i + bytesPerLine, raw.length);
|
|
@@ -292,14 +431,18 @@ export function bufferToRawHexLines(raw, bytesPerLine = 4, endian = "le") {
|
|
|
292
431
|
*
|
|
293
432
|
* 若用户未配置 transformFieldDict,或未使用 fieldDict 模式,则不会做任何额外处理。
|
|
294
433
|
*/
|
|
295
|
-
export function buildTraceParserRegistry(formatTexts) {
|
|
434
|
+
export function buildTraceParserRegistry(formatTexts, options = {}) {
|
|
296
435
|
const byEventId = new Map();
|
|
297
436
|
let commonFormat;
|
|
298
437
|
const transformFieldDictByEventId = new Map();
|
|
299
|
-
|
|
438
|
+
formatTexts.forEach((src, sourceIndex) => {
|
|
300
439
|
const text = typeof src === "string" ? src : src.text;
|
|
301
440
|
const fmt = parseTraceFormat(text);
|
|
302
441
|
if (fmt.eventId !== undefined) {
|
|
442
|
+
const existing = byEventId.get(fmt.eventId);
|
|
443
|
+
if (existing && !options.allowDuplicateEventIds) {
|
|
444
|
+
throw new Error(`Duplicate event ID ${fmt.eventId}: "${existing.eventName ?? "unknown"}" conflicts with "${fmt.eventName ?? "unknown"}" at format index ${sourceIndex}`);
|
|
445
|
+
}
|
|
303
446
|
byEventId.set(fmt.eventId, fmt);
|
|
304
447
|
if (!commonFormat && fmt.fields.some((f) => f.name === "common_type")) {
|
|
305
448
|
commonFormat = fmt;
|
|
@@ -308,7 +451,7 @@ export function buildTraceParserRegistry(formatTexts) {
|
|
|
308
451
|
transformFieldDictByEventId.set(fmt.eventId, src.transformFieldDict);
|
|
309
452
|
}
|
|
310
453
|
}
|
|
311
|
-
}
|
|
454
|
+
});
|
|
312
455
|
return {
|
|
313
456
|
byEventId,
|
|
314
457
|
commonFormat,
|
package/dist/trace/printf.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @module traceFormat
|
|
4
4
|
*/
|
|
5
5
|
type PrintFieldMap = Record<string, number | bigint | string | Array<number | bigint>>;
|
|
6
|
+
/** printf 格式串解析缓存上限(LRU 淘汰) */
|
|
7
|
+
export declare const PRINTF_CACHE_MAX_SIZE = 256;
|
|
6
8
|
/** 与 C printf 接近的转换说明符(不含 * 动态宽度等) */
|
|
7
9
|
export interface PrintfSpec {
|
|
8
10
|
flags: string;
|
|
@@ -27,6 +29,13 @@ export declare function tokenizePrintfFormat(printFmt: string): Array<{
|
|
|
27
29
|
kind: "spec";
|
|
28
30
|
spec: PrintfSpec;
|
|
29
31
|
}>;
|
|
32
|
+
/** 测试专用:重置 printf 解析缓存 */
|
|
33
|
+
export declare function resetPrintfCachesForTest(): void;
|
|
34
|
+
/** 测试专用:读取 printf 解析缓存条目数 */
|
|
35
|
+
export declare function getPrintfCacheSizesForTest(): {
|
|
36
|
+
tokens: number;
|
|
37
|
+
specs: number;
|
|
38
|
+
};
|
|
30
39
|
export declare function renderPrintFmtAsFieldDict(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap): Record<string, string>;
|
|
31
40
|
export declare function renderPrintFmt(printFmt: string, printArgs: string[] | undefined, fieldMap: PrintFieldMap): string;
|
|
32
41
|
export {};
|
package/dist/trace/printf.js
CHANGED
|
@@ -2,8 +2,42 @@
|
|
|
2
2
|
* printf / fieldDict 格式化与 print fmt 渲染。
|
|
3
3
|
* @module traceFormat
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
/** printf 格式串解析缓存上限(LRU 淘汰) */
|
|
6
|
+
export const PRINTF_CACHE_MAX_SIZE = 256;
|
|
7
|
+
class LruCache {
|
|
8
|
+
maxSize;
|
|
9
|
+
map = new Map();
|
|
10
|
+
constructor(maxSize) {
|
|
11
|
+
this.maxSize = maxSize;
|
|
12
|
+
}
|
|
13
|
+
get(key) {
|
|
14
|
+
const value = this.map.get(key);
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
this.map.delete(key);
|
|
18
|
+
this.map.set(key, value);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
set(key, value) {
|
|
22
|
+
if (this.map.has(key))
|
|
23
|
+
this.map.delete(key);
|
|
24
|
+
this.map.set(key, value);
|
|
25
|
+
while (this.map.size > this.maxSize) {
|
|
26
|
+
const oldest = this.map.keys().next().value;
|
|
27
|
+
if (oldest === undefined)
|
|
28
|
+
break;
|
|
29
|
+
this.map.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get size() {
|
|
33
|
+
return this.map.size;
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.map.clear();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const PRINT_FMT_TOKEN_CACHE = new LruCache(PRINTF_CACHE_MAX_SIZE);
|
|
40
|
+
const PRINT_FMT_SPEC_CACHE = new LruCache(PRINTF_CACHE_MAX_SIZE);
|
|
7
41
|
const NORMALIZED_PRINT_ARGS_CACHE = new WeakMap();
|
|
8
42
|
const EMPTY_PRINT_ARGS = [];
|
|
9
43
|
const PRINT_FLAG_CHARS = new Set(["-", "+", "#", "0", " "]);
|
|
@@ -24,6 +58,47 @@ function toBigIntForPrintf(value) {
|
|
|
24
58
|
function asUInt64(n) {
|
|
25
59
|
return n & ((1n << 64n) - 1n);
|
|
26
60
|
}
|
|
61
|
+
/** 按长度修饰符截断无符号值(C printf 语义) */
|
|
62
|
+
function maskUnsignedByLength(n, length) {
|
|
63
|
+
switch (length) {
|
|
64
|
+
case "hh":
|
|
65
|
+
return n & 0xffn;
|
|
66
|
+
case "h":
|
|
67
|
+
return n & 0xffffn;
|
|
68
|
+
case "l":
|
|
69
|
+
case "ll":
|
|
70
|
+
case "j":
|
|
71
|
+
case "z":
|
|
72
|
+
case "t":
|
|
73
|
+
case "L":
|
|
74
|
+
return asUInt64(n);
|
|
75
|
+
default:
|
|
76
|
+
return n & 0xffffffffn;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function signExtend(value, bits) {
|
|
80
|
+
const mask = (1n << BigInt(bits)) - 1n;
|
|
81
|
+
const v = value & mask;
|
|
82
|
+
const signBit = 1n << BigInt(bits - 1);
|
|
83
|
+
return v >= signBit ? v - (1n << BigInt(bits)) : v;
|
|
84
|
+
}
|
|
85
|
+
/** 按长度修饰符截断并符号扩展有符号值(C printf 语义) */
|
|
86
|
+
function maskSignedByLength(n, length) {
|
|
87
|
+
switch (length) {
|
|
88
|
+
case "hh":
|
|
89
|
+
return signExtend(n, 8);
|
|
90
|
+
case "h":
|
|
91
|
+
return signExtend(n, 16);
|
|
92
|
+
case "l":
|
|
93
|
+
case "ll":
|
|
94
|
+
case "j":
|
|
95
|
+
case "z":
|
|
96
|
+
case "t":
|
|
97
|
+
return signExtend(asUInt64(n), 64);
|
|
98
|
+
default:
|
|
99
|
+
return signExtend(n & 0xffffffffn, 32);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
27
102
|
function applyWidth(s, width, leftAlign, zeroPad) {
|
|
28
103
|
if (width === undefined || s.length >= width)
|
|
29
104
|
return s;
|
|
@@ -37,9 +112,15 @@ function applyWidth(s, width, leftAlign, zeroPad) {
|
|
|
37
112
|
function formatPrintfD(n, spec) {
|
|
38
113
|
const neg = n < 0n;
|
|
39
114
|
const abs = neg ? -n : n;
|
|
40
|
-
let digits = abs.toString(10);
|
|
41
115
|
const prec = spec.precision !== undefined ? spec.precision : 1;
|
|
42
|
-
|
|
116
|
+
let digits;
|
|
117
|
+
if (prec === 0 && abs === 0n) {
|
|
118
|
+
digits = "";
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
digits = abs.toString(10);
|
|
122
|
+
digits = digits.padStart(Math.max(prec, digits.length), "0");
|
|
123
|
+
}
|
|
43
124
|
let sign = "";
|
|
44
125
|
if (neg)
|
|
45
126
|
sign = "-";
|
|
@@ -61,24 +142,42 @@ function formatPrintfD(n, spec) {
|
|
|
61
142
|
return " ".repeat(padLen) + body;
|
|
62
143
|
}
|
|
63
144
|
function formatPrintfUnsignedRadix(unsigned, spec, radix, upper) {
|
|
64
|
-
let
|
|
65
|
-
? upper
|
|
66
|
-
? unsigned.toString(16).toUpperCase()
|
|
67
|
-
: unsigned.toString(16)
|
|
68
|
-
: unsigned.toString(radix);
|
|
145
|
+
let prefix = "";
|
|
69
146
|
const precDefault = 1;
|
|
70
147
|
const prec = spec.precision !== undefined ? spec.precision : precDefault;
|
|
71
|
-
|
|
72
|
-
if (
|
|
148
|
+
let body;
|
|
149
|
+
if (prec === 0 && unsigned === 0n) {
|
|
150
|
+
body = "";
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
body =
|
|
154
|
+
radix === 16
|
|
155
|
+
? upper
|
|
156
|
+
? unsigned.toString(16).toUpperCase()
|
|
157
|
+
: unsigned.toString(16)
|
|
158
|
+
: unsigned.toString(radix);
|
|
159
|
+
body = body.padStart(Math.max(prec, body.length), "0");
|
|
160
|
+
}
|
|
161
|
+
if (hasPrintfFlag(spec.flags, "#") && unsigned !== 0n) {
|
|
73
162
|
if (radix === 16)
|
|
74
|
-
|
|
163
|
+
prefix = upper ? "0X" : "0x";
|
|
75
164
|
else if (radix === 8 && body !== "0" && !body.startsWith("0"))
|
|
76
|
-
|
|
165
|
+
prefix = "0";
|
|
77
166
|
}
|
|
78
167
|
const zeroPad = hasPrintfFlag(spec.flags, "0") &&
|
|
79
168
|
!hasPrintfFlag(spec.flags, "-") &&
|
|
80
169
|
spec.precision === undefined;
|
|
81
|
-
|
|
170
|
+
const leftAlign = hasPrintfFlag(spec.flags, "-");
|
|
171
|
+
if (zeroPad && prefix) {
|
|
172
|
+
const width = spec.width;
|
|
173
|
+
if (width !== undefined) {
|
|
174
|
+
const bodyWidth = Math.max(width - prefix.length, body.length);
|
|
175
|
+
body = body.padStart(bodyWidth, "0");
|
|
176
|
+
}
|
|
177
|
+
return prefix + body;
|
|
178
|
+
}
|
|
179
|
+
const combined = prefix + body;
|
|
180
|
+
return applyWidth(combined, spec.width, leftAlign, zeroPad);
|
|
82
181
|
}
|
|
83
182
|
/**
|
|
84
183
|
* 按 C printf 子集格式化:标志 -+ #0 空格,宽度,精度,长度 hl ll hh,转换 d i u x X o s p。
|
|
@@ -106,14 +205,20 @@ export function formatPrintfValue(value, spec) {
|
|
|
106
205
|
return applyWidth(body, spec.width, hasPrintfFlag(spec.flags, "-"), zeroPad);
|
|
107
206
|
}
|
|
108
207
|
if (cl === "d" || cl === "i") {
|
|
109
|
-
return formatPrintfD(n, spec);
|
|
208
|
+
return formatPrintfD(maskSignedByLength(n, spec.length), spec);
|
|
110
209
|
}
|
|
111
210
|
if (cl === "u" || cl === "x" || cl === "X" || cl === "o") {
|
|
112
|
-
const u =
|
|
211
|
+
const u = maskUnsignedByLength(n, spec.length);
|
|
113
212
|
if (cl === "u") {
|
|
114
|
-
let body = u.toString(10);
|
|
115
213
|
const prec = spec.precision !== undefined ? spec.precision : 1;
|
|
116
|
-
|
|
214
|
+
let body;
|
|
215
|
+
if (prec === 0 && u === 0n) {
|
|
216
|
+
body = "";
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
body = u.toString(10);
|
|
220
|
+
body = body.padStart(Math.max(prec, body.length), "0");
|
|
221
|
+
}
|
|
117
222
|
const zeroPad = hasPrintfFlag(spec.flags, "0") &&
|
|
118
223
|
!hasPrintfFlag(spec.flags, "-") &&
|
|
119
224
|
spec.precision === undefined;
|
|
@@ -246,12 +351,34 @@ function getCachedPrintfTokens(printFmt) {
|
|
|
246
351
|
PRINT_FMT_TOKEN_CACHE.set(printFmt, tokens);
|
|
247
352
|
return tokens;
|
|
248
353
|
}
|
|
354
|
+
/** 测试专用:重置 printf 解析缓存 */
|
|
355
|
+
export function resetPrintfCachesForTest() {
|
|
356
|
+
PRINT_FMT_TOKEN_CACHE.clear();
|
|
357
|
+
PRINT_FMT_SPEC_CACHE.clear();
|
|
358
|
+
}
|
|
359
|
+
/** 测试专用:读取 printf 解析缓存条目数 */
|
|
360
|
+
export function getPrintfCacheSizesForTest() {
|
|
361
|
+
return { tokens: PRINT_FMT_TOKEN_CACHE.size, specs: PRINT_FMT_SPEC_CACHE.size };
|
|
362
|
+
}
|
|
249
363
|
function normalizePrintExpr(exprRaw) {
|
|
250
364
|
let expr = exprRaw.trim();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
365
|
+
if (expr.startsWith("(") && expr.endsWith(")")) {
|
|
366
|
+
let depth = 0;
|
|
367
|
+
let fullyWrapped = true;
|
|
368
|
+
for (let i = 0; i < expr.length; i++) {
|
|
369
|
+
if (expr[i] === "(")
|
|
370
|
+
depth++;
|
|
371
|
+
else if (expr[i] === ")")
|
|
372
|
+
depth--;
|
|
373
|
+
if (depth === 0 && i < expr.length - 1) {
|
|
374
|
+
fullyWrapped = false;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (fullyWrapped && depth === 0) {
|
|
379
|
+
expr = expr.slice(1, -1).trim();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
255
382
|
expr = expr.replace(/\s+/g, "");
|
|
256
383
|
return expr;
|
|
257
384
|
}
|
|
@@ -267,7 +394,21 @@ function getNormalizedPrintArgs(printArgs) {
|
|
|
267
394
|
}
|
|
268
395
|
function evalPrintArgExpr(exprRaw, fieldMap) {
|
|
269
396
|
const expr = normalizePrintExpr(exprRaw);
|
|
270
|
-
let m = expr.match(/^
|
|
397
|
+
let m = expr.match(/^__get_str\((\w+)\)$/);
|
|
398
|
+
if (m) {
|
|
399
|
+
const v = fieldMap[m[1]];
|
|
400
|
+
if (typeof v === "string")
|
|
401
|
+
return v;
|
|
402
|
+
throw new Error(`__get_str(${m[1]}) requires a string field, got ${typeof v}`);
|
|
403
|
+
}
|
|
404
|
+
m = expr.match(/^__get_dynamic_array\((\w+)\)$/);
|
|
405
|
+
if (m) {
|
|
406
|
+
const v = fieldMap[m[1]];
|
|
407
|
+
if (Array.isArray(v))
|
|
408
|
+
return v;
|
|
409
|
+
throw new Error(`__get_dynamic_array(${m[1]}) requires an array field, got ${typeof v}`);
|
|
410
|
+
}
|
|
411
|
+
m = expr.match(/^REC->(\w+)&0xffff$/);
|
|
271
412
|
if (m) {
|
|
272
413
|
const v = fieldMap[m[1]];
|
|
273
414
|
const n = typeof v === "number" ? v : 0;
|
|
@@ -280,32 +421,49 @@ function evalPrintArgExpr(exprRaw, fieldMap) {
|
|
|
280
421
|
return (n >>> 16) & 0xffff;
|
|
281
422
|
}
|
|
282
423
|
m = expr.match(/^REC->(\w+)(?:\[(\d+)])?$/);
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
424
|
+
if (m) {
|
|
425
|
+
const name = m[1];
|
|
426
|
+
const idxRaw = m[2];
|
|
427
|
+
const v = fieldMap[name];
|
|
428
|
+
if (idxRaw !== undefined) {
|
|
429
|
+
const idx = parseInt(idxRaw, 10);
|
|
430
|
+
if (Array.isArray(v) && idx >= 0 && idx < v.length) {
|
|
431
|
+
return v[idx];
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`Invalid print arg index for REC->${name}[${idxRaw}]`);
|
|
292
434
|
}
|
|
293
|
-
|
|
435
|
+
if (Array.isArray(v))
|
|
436
|
+
return v[0] ?? 0;
|
|
437
|
+
if (v !== undefined)
|
|
438
|
+
return v;
|
|
439
|
+
throw new Error(`Missing field for print arg REC->${name}`);
|
|
294
440
|
}
|
|
295
|
-
|
|
296
|
-
return v[0] ?? 0;
|
|
297
|
-
return v ?? 0;
|
|
441
|
+
throw new Error(`Unknown print arg expression: ${exprRaw.trim()}`);
|
|
298
442
|
}
|
|
299
443
|
function buildPrintArgValues(printArgs, fieldMap) {
|
|
300
444
|
const normalizedArgs = getNormalizedPrintArgs(printArgs);
|
|
301
445
|
const values = (printArgs ?? []).map((e) => evalPrintArgExpr(e, fieldMap));
|
|
302
446
|
return { normalizedArgs, values };
|
|
303
447
|
}
|
|
448
|
+
function resolvePrintArgValue(value) {
|
|
449
|
+
if (value === undefined)
|
|
450
|
+
return 0;
|
|
451
|
+
if (Array.isArray(value))
|
|
452
|
+
return value[0] ?? 0;
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
304
455
|
function formatOnePrintArg(idx, spec, fieldMap, normalizedArgs, values) {
|
|
305
|
-
const v = values[idx]
|
|
456
|
+
const v = resolvePrintArgValue(values[idx]);
|
|
306
457
|
const argExpr = normalizedArgs[idx] ?? "";
|
|
307
458
|
if (spec.conv === "s" || spec.conv === "S") {
|
|
308
|
-
|
|
459
|
+
let m = argExpr.match(/^__get_str\((\w+)\)$/);
|
|
460
|
+
if (m) {
|
|
461
|
+
const s = fieldMap[m[1]];
|
|
462
|
+
if (typeof s === "string") {
|
|
463
|
+
return formatPrintfValue(s, spec);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
m = argExpr.match(/^REC->__data_loc_(\w+)(?:&0xffff)?(?:>>16)?$/);
|
|
309
467
|
if (m) {
|
|
310
468
|
const s = fieldMap[m[1]];
|
|
311
469
|
if (typeof s === "string") {
|
|
@@ -319,6 +477,14 @@ function fieldKeyFromNormalizedArg(normalizedArg, index) {
|
|
|
319
477
|
let k = normalizedArg;
|
|
320
478
|
if (k.startsWith("REC->"))
|
|
321
479
|
k = k.slice(5);
|
|
480
|
+
const mGetStr = k.match(/^__get_str\((\w+)\)$/);
|
|
481
|
+
if (mGetStr) {
|
|
482
|
+
return mGetStr[1];
|
|
483
|
+
}
|
|
484
|
+
const mGetArr = k.match(/^__get_dynamic_array\((\w+)\)$/);
|
|
485
|
+
if (mGetArr) {
|
|
486
|
+
return mGetArr[1];
|
|
487
|
+
}
|
|
322
488
|
// REC->__data_loc_path&0xffff 在 fieldDict 中收敛为 path 作为键名
|
|
323
489
|
const mDataLocOff = k.match(/^__data_loc_(\w+)&0xffff$/);
|
|
324
490
|
if (mDataLocOff) {
|