Skip to content

Commit 9178fe0

Browse files
Fix colon path and __size_of evaluation (#889)
* Fix colon path and __size_of evaluation Co-authored-by: Jens Reinecke <jens.reinecke@arm.com>
1 parent cfe71dc commit 9178fe0

7 files changed

Lines changed: 205 additions & 18 deletions

File tree

src/views/component-viewer/parser-evaluator/evaluator.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,14 @@ export class Evaluator {
253253
// For intrinsic calls, the callee is metadata (intrinsic name), not a reference to evaluate.
254254
// Only search the arguments for references.
255255
const c = node as EvalPointCall;
256-
if (c.args.length) {
257-
for (const arg of c.args) {
258-
const r = this.findReferenceNode(arg);
259-
if (r) {
260-
return r;
261-
}
256+
for (const arg of c.args) {
257+
const r = this.findReferenceNode(arg);
258+
if (r) {
259+
return r;
262260
}
263261
}
264-
// If no args, fall back to callee (it's still an identifier node)
265-
return this.findReferenceNode(c.callee);
262+
// Never fall back to callee – intrinsic names are not data references.
263+
return undefined;
266264
}
267265
case 'PrintfExpression': {
268266
for (const seg of (node as PrintfExpression).segments) {

src/views/component-viewer/scvd-eval-interface.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,66 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr
223223
return offset;
224224
}
225225

226+
// 3-part colon path: typedef:member:enumName → numeric enum value
227+
if (parts.length === 3) {
228+
const [typedefName, memberName, enumName] = parts;
229+
return this.resolveEnumValue(_container, typedefName, memberName, enumName);
230+
}
231+
226232
componentViewerLogger.warn(`[resolveColonPath] Unsupported colon path format with ${parts.length} parts: ${parts.join(':')}`);
227233
return undefined;
228234
}
229235

236+
/**
237+
* Resolve a 3-part colon path `typedef:member:enumName` to the numeric enum value.
238+
* E.g. `TCP_INFO4:State:Closed` → 1
239+
*/
240+
private async resolveEnumValue(_container: RefContainer, typedefName: string, memberName: string, enumName: string): Promise<EvalValue> {
241+
// Navigate to the root ScvdComponentViewer
242+
let root: ScvdNode = _container.base;
243+
while (root.parent !== undefined) {
244+
root = root.parent;
245+
}
246+
247+
if (!(root instanceof ScvdComponentViewer)) {
248+
componentViewerLogger.error('[resolveEnumValue] Root is not ScvdComponentViewer');
249+
return undefined;
250+
}
251+
252+
const typedefs = root.typedefs;
253+
if (!typedefs || !typedefs.typedef) {
254+
componentViewerLogger.error('[resolveEnumValue] No typedefs found in component viewer');
255+
return undefined;
256+
}
257+
258+
const typedef = typedefs.typedef.find((td: ScvdTypedef) => td.name === typedefName);
259+
if (!typedef) {
260+
componentViewerLogger.error(`[resolveEnumValue] Typedef "${typedefName}" not found`);
261+
return undefined;
262+
}
263+
264+
const memberRef = typedef.getMember(memberName);
265+
if (!memberRef || !(memberRef instanceof ScvdMember)) {
266+
componentViewerLogger.error(`[resolveEnumValue] Member "${memberName}" not found in typedef "${typedefName}"`);
267+
return undefined;
268+
}
269+
270+
// Find the enum by name within the member
271+
const enumItem = memberRef.enum.find(e => e.name === enumName);
272+
if (!enumItem) {
273+
componentViewerLogger.error(`[resolveEnumValue] Enum "${enumName}" not found in member "${memberName}" of typedef "${typedefName}"`);
274+
return undefined;
275+
}
276+
277+
const value = await enumItem.value?.getValue();
278+
if (typeof value !== 'number') {
279+
componentViewerLogger.warn(`[resolveEnumValue] Enum "${enumName}" value is not a number: ${value}`);
280+
return undefined;
281+
}
282+
283+
return value;
284+
}
285+
230286
public async getElementRef(ref: ScvdNode): Promise<ScvdNode | undefined> {
231287
return ref.getElementRef();
232288
}

src/views/component-viewer/statement-engine/statement-readList.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export class StatementReadList extends StatementBase {
293293
if (!didBatchRead) {
294294
const loopStart = perf?.start() ?? 0;
295295
let readIdx = 0;
296-
const visitedAddresses = new Set<number | bigint>();
296+
const visitedAddresses = new Set<number>();
297297
while (nextPtrAddr !== undefined) {
298298
// Check for external cancellation (session ended) or global timeout
299299
if (executionContext.cancellation.checkDeadline()) {
@@ -303,12 +303,24 @@ export class StatementReadList extends StatementBase {
303303

304304
const itemAddress: number | bigint | undefined = typeof nextPtrAddr === 'bigint' ? nextPtrAddr : (nextPtrAddr >>> 0);
305305

306+
// Normalize to number for consistent Set lookups (bigint and number
307+
// are different types in Set — 42n !== 42 — so always use number).
308+
const addressKey = typeof itemAddress === 'bigint' ? Number(itemAddress) : itemAddress;
309+
306310
// Detect cycles: check if we've visited this address before
307-
if (visitedAddresses.has(itemAddress)) {
308-
componentViewerLogger.error(`${this.scvdItem.getLineNoStr()}: Executing "readlist": ${scvdReadList.name}, symbol: ${symbol?.name}, detected cycle in linked list at address: ${itemAddress.toString(16)}`);
311+
if (visitedAddresses.has(addressKey)) {
312+
const message = `${this.scvdItem.getLineNoStr()}: Executing "readlist": ${scvdReadList.name}, symbol: ${symbol?.name}, detected cycle in linked list at address: ${itemAddress.toString(16)}`;
313+
if (await executionContext.debugTarget.getTargetIsRunning()) {
314+
// While the target is running, cycles may be caused by the RTOS
315+
// updating linked-list pointers concurrently — this is transient
316+
// and not necessarily an error in the SCVD definition.
317+
componentViewerLogger.warn(message);
318+
} else {
319+
componentViewerLogger.error(message);
320+
}
309321
break;
310322
}
311-
visitedAddresses.add(itemAddress);
323+
visitedAddresses.add(addressKey);
312324

313325
// Read data from target
314326
const readData = await executionContext.debugTarget.readMemory(itemAddress, readBytes);

src/views/component-viewer/test/integration/parser-evaluator/evaluator/evaluator.guards.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('evaluator guards', () => {
8686
expect(fn(assignNode)).toBe(assignNode.right);
8787

8888
const evalPoint: ASTNode = { kind: 'EvalPointCall', intrinsic: '__Running', callee: { kind: 'Identifier', name: '__Running', start: 0, end: 0 }, args: [], start: 0, end: 0 } as unknown as ASTNode;
89-
expect(fn(evalPoint)).toBe((evalPoint as CallExpression).callee);
89+
expect(fn(evalPoint)).toBeUndefined();
9090

9191
const evalPointWithArg: ASTNode = {
9292
kind: 'EvalPointCall',

src/views/component-viewer/test/unit/helpers/statement-engine-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export function createExecutionContext(
9090
readMemory: async () => undefined,
9191
readMemoryBatch: async () => new Map(),
9292
beginUpdateCycle: async () => {},
93+
getTargetIsRunning: async () => false,
9394
};
9495
const debugTarget = { ...debugTargetDefaults, ...debugTargetOverrides } as ScvdDebugTarget;
9596

src/views/component-viewer/test/unit/parser-evaluator/eval-interface/scvd-eval-interface.test.ts

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { ScvdFormatSpecifier } from '../../../../model/scvd-format-specifier';
3030
import { ScvdNode } from '../../../../model/scvd-node';
3131
import { ScvdMember } from '../../../../model/scvd-member';
3232
import { ScvdComponentViewer } from '../../../../model/scvd-component-viewer';
33-
import { ScvdTypedef } from '../../../../model/scvd-typedef';
33+
import { ScvdTypedef, ScvdTypedefs } from '../../../../model/scvd-typedef';
3434

3535
class DummyNode extends ScvdNode {
3636
private _testParent: ScvdNode | undefined;
@@ -304,16 +304,136 @@ describe('ScvdEvalInterface intrinsics and helpers', () => {
304304
await expect(evalIf.getElementRef(base)).resolves.toBeUndefined();
305305
});
306306

307-
it('resolveColonPath warns on unsupported path format with more than 2 parts', async () => {
307+
it('resolveColonPath attempts enum lookup for 3-part path (falls back gracefully)', async () => {
308308
const base = new DummyNode('base');
309309
const container: RefContainer = { base, current: base, valueType: undefined };
310310
const { evalIf } = makeEval();
311-
jest.spyOn(componentViewerLogger, 'warn').mockImplementation(() => {});
311+
jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {});
312+
// 3-part path now tries enum resolution; DummyNode root is not ScvdComponentViewer so it fails gracefully
312313
await expect(evalIf.resolveColonPath(container, ['a', 'b', 'c'])).resolves.toBeUndefined();
313-
expect(componentViewerLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Unsupported colon path format with 3 parts'));
314+
expect(componentViewerLogger.error).toHaveBeenCalledWith(expect.stringContaining('Root is not ScvdComponentViewer'));
315+
(componentViewerLogger.error as unknown as jest.Mock).mockRestore();
316+
});
317+
318+
it('resolveColonPath warns on unsupported path format with more than 3 parts', async () => {
319+
const base = new DummyNode('base');
320+
const container: RefContainer = { base, current: base, valueType: undefined };
321+
const { evalIf } = makeEval();
322+
jest.spyOn(componentViewerLogger, 'warn').mockImplementation(() => {});
323+
await expect(evalIf.resolveColonPath(container, ['a', 'b', 'c', 'd'])).resolves.toBeUndefined();
324+
expect(componentViewerLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Unsupported colon path format with 4 parts'));
314325
(componentViewerLogger.warn as unknown as jest.Mock).mockRestore();
315326
});
316327

328+
describe('resolveEnumValue (3-part colon path)', () => {
329+
function buildComponentViewerWithEnum(opts?: {
330+
typedefName?: string;
331+
memberName?: string;
332+
enumName?: string;
333+
enumValue?: number | string;
334+
skipTypedefs?: boolean;
335+
skipTypedef?: boolean;
336+
skipMember?: boolean;
337+
skipEnum?: boolean;
338+
}): { root: ScvdComponentViewer; container: RefContainer } {
339+
const root = new ScvdComponentViewer(undefined);
340+
341+
if (!opts?.skipTypedefs) {
342+
const typedefs = new ScvdTypedefs(root);
343+
(root as unknown as { _typedefs: ScvdTypedefs })._typedefs = typedefs;
344+
345+
if (!opts?.skipTypedef) {
346+
const typedef = new ScvdTypedef(typedefs);
347+
typedef.name = opts?.typedefName ?? 'MyType';
348+
typedefs.typedef.push(typedef);
349+
350+
if (!opts?.skipMember) {
351+
const member = typedef.addMember();
352+
member.name = opts?.memberName ?? 'State';
353+
354+
if (!opts?.skipEnum) {
355+
const enumItem = member.addEnum();
356+
enumItem.name = opts?.enumName ?? 'Active';
357+
// Mock getValue on the enum's value expression
358+
const mockValue = opts?.enumValue ?? 42;
359+
jest.spyOn(enumItem.value, 'getValue').mockResolvedValue(
360+
typeof mockValue === 'number' ? mockValue : mockValue
361+
);
362+
}
363+
}
364+
}
365+
}
366+
367+
// Create a DummyNode whose parent chain leads to root
368+
const base = new DummyNode('child');
369+
base.setTestParent(root);
370+
const container: RefContainer = { base, current: base, valueType: undefined };
371+
return { root, container };
372+
}
373+
374+
it('resolves enum value for valid 3-part colon path', async () => {
375+
const { container } = buildComponentViewerWithEnum({
376+
typedefName: 'TCP_INFO4',
377+
memberName: 'State',
378+
enumName: 'Closed',
379+
enumValue: 1,
380+
});
381+
const { evalIf } = makeEval();
382+
await expect(evalIf.resolveColonPath(container, ['TCP_INFO4', 'State', 'Closed'])).resolves.toBe(1);
383+
});
384+
385+
it('returns undefined when typedefs are missing', async () => {
386+
const { container } = buildComponentViewerWithEnum({ skipTypedefs: true });
387+
const { evalIf } = makeEval();
388+
jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {});
389+
await expect(evalIf.resolveColonPath(container, ['a', 'b', 'c'])).resolves.toBeUndefined();
390+
expect(componentViewerLogger.error).toHaveBeenCalledWith(expect.stringContaining('No typedefs found'));
391+
(componentViewerLogger.error as unknown as jest.Mock).mockRestore();
392+
});
393+
394+
it('returns undefined when typedef not found', async () => {
395+
const { container } = buildComponentViewerWithEnum({ typedefName: 'Existing' });
396+
const { evalIf } = makeEval();
397+
jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {});
398+
await expect(evalIf.resolveColonPath(container, ['NonExistent', 'State', 'Active'])).resolves.toBeUndefined();
399+
expect(componentViewerLogger.error).toHaveBeenCalledWith(expect.stringContaining('Typedef "NonExistent" not found'));
400+
(componentViewerLogger.error as unknown as jest.Mock).mockRestore();
401+
});
402+
403+
it('returns undefined when member not found in typedef', async () => {
404+
const { container } = buildComponentViewerWithEnum({ typedefName: 'MyType', memberName: 'State' });
405+
const { evalIf } = makeEval();
406+
jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {});
407+
await expect(evalIf.resolveColonPath(container, ['MyType', 'WrongMember', 'Active'])).resolves.toBeUndefined();
408+
expect(componentViewerLogger.error).toHaveBeenCalledWith(expect.stringContaining('Member "WrongMember" not found'));
409+
(componentViewerLogger.error as unknown as jest.Mock).mockRestore();
410+
});
411+
412+
it('returns undefined when enum not found in member', async () => {
413+
const { container } = buildComponentViewerWithEnum({ enumName: 'Active' });
414+
const { evalIf } = makeEval();
415+
jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {});
416+
await expect(evalIf.resolveColonPath(container, ['MyType', 'State', 'WrongEnum'])).resolves.toBeUndefined();
417+
expect(componentViewerLogger.error).toHaveBeenCalledWith(expect.stringContaining('Enum "WrongEnum" not found'));
418+
(componentViewerLogger.error as unknown as jest.Mock).mockRestore();
419+
});
420+
421+
it('returns undefined when enum value is not a number', async () => {
422+
const { container } = buildComponentViewerWithEnum({ enumValue: 'not-a-number' as unknown as number });
423+
const { evalIf } = makeEval();
424+
jest.spyOn(componentViewerLogger, 'warn').mockImplementation(() => {});
425+
await expect(evalIf.resolveColonPath(container, ['MyType', 'State', 'Active'])).resolves.toBeUndefined();
426+
expect(componentViewerLogger.warn).toHaveBeenCalledWith(expect.stringContaining('value is not a number'));
427+
(componentViewerLogger.warn as unknown as jest.Mock).mockRestore();
428+
});
429+
430+
it('returns enum value 0 correctly (falsy but valid)', async () => {
431+
const { container } = buildComponentViewerWithEnum({ enumValue: 0 });
432+
const { evalIf } = makeEval();
433+
await expect(evalIf.resolveColonPath(container, ['MyType', 'State', 'Active'])).resolves.toBe(0);
434+
});
435+
});
436+
317437
it('read/write value wrap host errors', async () => {
318438
const memHost = {
319439
readValue: jest.fn(() => { throw new Error('boom'); }),

src/views/component-viewer/test/unit/parser-evaluator/evaluator/evaluator.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,7 @@ describe('Evaluator coverage branches', () => {
10511051
expect(helpers.findReferenceNode(callExpr(id('fn'), [num(1), id('arg')]))?.kind).toBe('Identifier');
10521052
expect(helpers.findReferenceNode(callExpr(id('fn'), [num(1), num(2)]))?.kind).toBe('Identifier');
10531053
expect(helpers.findReferenceNode(callExpr(id('fn'), []))?.kind).toBe('Identifier');
1054-
expect(helpers.findReferenceNode(evalPoint('__Running', []))?.kind).toBe('Identifier');
1054+
expect(helpers.findReferenceNode(evalPoint('__Running', []))).toBeUndefined();
10551055
expect(helpers.findReferenceNode(evalPoint('__size_of', [num(1), id('sym')]))?.kind).toBe('Identifier');
10561056
expect(helpers.findReferenceNode(evalPoint('__size_of', [id('sym')]))?.kind).toBe('Identifier');
10571057
expect(helpers.findReferenceNode({ kind: 'ConditionalExpression', test: num(0), consequent: id('c'), alternate: num(2), ...span } as ConditionalExpression)?.kind).toBe('Identifier');

0 commit comments

Comments
 (0)