Skip to content

Commit 06b830d

Browse files
evaluator coverage test
1 parent 6368427 commit 06b830d

File tree

5 files changed

+559
-19
lines changed

5 files changed

+559
-19
lines changed

src/views/component-viewer/evaluator.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,3 +1064,23 @@ export async function evaluateParseResult(pr: ParseResult, ctx: EvalContext, con
10641064
ctx.container.valueType = saved.valueType;
10651065
}
10661066
}
1067+
1068+
// Test-only access to internal helpers.
1069+
export const __test__ = {
1070+
findReferenceNode,
1071+
asNumber,
1072+
integerDiv,
1073+
integerMod,
1074+
evalArgsForIntrinsic,
1075+
mustRef,
1076+
formatValue,
1077+
eqVals,
1078+
ltVals,
1079+
lteVals,
1080+
gtVals,
1081+
gteVals,
1082+
getScalarTypeForContainer,
1083+
captureContainerForReference,
1084+
evalBinary,
1085+
normalizeEvaluateResult,
1086+
};

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ describe('ScvdEvalInterface intrinsics and helpers', () => {
194194
(console.error as unknown as jest.Mock).mockRestore();
195195
});
196196

197+
it('resolveColonPath and getElementRef fall back to undefined', async () => {
198+
const child = new DummyNode('child');
199+
const base = new DummyNode('base', { symbolMap: { child } });
200+
const container: RefContainer = { base, current: base, valueType: undefined };
201+
const { evalIf } = makeEval();
202+
await expect(evalIf.resolveColonPath(container, ['a', 'b'])).resolves.toBeUndefined();
203+
await expect(evalIf.getElementRef(base)).resolves.toBeUndefined();
204+
});
205+
197206
it('read/write value wrap host errors', async () => {
198207
const memHost = {
199208
readValue: jest.fn(() => { throw new Error('boom'); }),
@@ -240,6 +249,14 @@ describe('ScvdEvalInterface intrinsics and helpers', () => {
240249
expect(widen.bits).toBe(128);
241250
});
242251

252+
it('covers scalar info for array types', async () => {
253+
const arrayNode = new DummyNode('arr', { arraySize: 4, valueType: 'uint8_t' });
254+
const container: RefContainer = { base: arrayNode, current: arrayNode, valueType: undefined };
255+
const { evalIf } = makeEval();
256+
const formatted = await evalIf.formatPrintf('x', 1, container);
257+
expect(formatted).toBeDefined();
258+
});
259+
243260
it('normalizeScalarType and helpers handle undefined and invalid pointers', async () => {
244261
const { evalIf, debugTarget } = makeEval({ readMemory: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])) });
245262
const norm = (evalIf as unknown as { normalizeScalarType(v: unknown): unknown }).normalizeScalarType(' double64 ');

src/views/component-viewer/test/evaluator/evaluator.coverage.test.ts

Lines changed: 200 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ import type { FullDataHost } from '../helpers/full-data-host';
2424
import { ScvdNode } from '../../model/scvd-node';
2525

2626
class FakeNode extends ScvdNode {
27-
constructor(public readonly id: string, parent?: ScvdNode, public value: string | number | bigint | Uint8Array | undefined = undefined, private members: Record<string, ScvdNode> = {}) {
27+
constructor(public readonly id: string, parent?: ScvdNode, public value: EvalValue = undefined, private members: Record<string, ScvdNode> = {}) {
2828
super(parent);
2929
}
30-
public async setValue(v: string | number): Promise<string | number | undefined> {
30+
public async setValue(v: number | string): Promise<number | string | undefined> {
3131
this.value = v;
3232
return v;
3333
}
34-
public async getValue(): Promise<string | number | bigint | Uint8Array | undefined> {
35-
return this.value;
34+
public async getValue(): Promise<string | number | bigint | Uint8Array<ArrayBufferLike> | undefined> {
35+
return this.value as unknown as string | number | bigint | Uint8Array<ArrayBufferLike> | undefined;
3636
}
3737
public getSymbol(name: string): ScvdNode | undefined {
3838
return this.members[name];
@@ -207,6 +207,33 @@ describe('evaluator coverage', () => {
207207
(console.error as unknown as jest.Mock).mockRestore();
208208
});
209209

210+
it('applies member offsets when provided by host', async () => {
211+
class OffsetHost extends Host {
212+
async getMemberOffset(): Promise<number | undefined> {
213+
return 8;
214+
}
215+
}
216+
217+
const base = new FakeNode('base');
218+
const child = new FakeNode('child', base, 1);
219+
const obj = new FakeNode('obj', base, undefined, { child });
220+
const values = new Map<string, FakeNode>([['obj', obj], ['child', child]]);
221+
const host = new OffsetHost(values);
222+
const ctx = new EvalContext({ data: host, container: base });
223+
224+
await expect(evalNode(parseExpression('obj.child', false).ast, ctx)).resolves.toBe(child['value']);
225+
expect(ctx.container.offsetBytes).toBe(8);
226+
});
227+
228+
it('evaluates a complex conditional expression', async () => {
229+
const base = new FakeNode('base');
230+
const count = new FakeNode('Count', base, 2);
231+
const values = new Map<string, FakeNode>([['Count', count]]);
232+
const host = new Host(values);
233+
const expr = '0==( (Count==0) || (Count==1) || (Count==8) || (Count==9) || (Count==10) )';
234+
await expect(evalExpr(expr, host, base)).resolves.toBe(1);
235+
});
236+
210237
it('covers intrinsics, pseudo members, and string/unary paths', async () => {
211238
const base = new FakeNode('base');
212239
const arrElem = new FakeNode('arr[0]', base, 1);
@@ -468,4 +495,173 @@ describe('evaluator edge coverage', () => {
468495
host.formatPrintf = async () => 'fmt';
469496
await expect(evalNode(printfAst, ctx)).resolves.toContain('fmt');
470497
});
498+
499+
it('resolves array member via fast path with stride and element refs', async () => {
500+
const base = new BranchNode('base');
501+
const member = new BranchNode('m', base, 42);
502+
const element = new BranchNode('elem', base, 0, { m: member });
503+
const arr = new BranchNode('arr', base, undefined, { '1': element });
504+
(arr as unknown as { getElementRef(): BranchNode }).getElementRef = () => element;
505+
506+
const values = new Map<string, BranchNode>([
507+
['arr', arr],
508+
['elem', element],
509+
['m', member],
510+
]);
511+
const host = new BranchHost(values);
512+
host.getMemberOffset = async () => 4;
513+
host.getElementStride = async () => 8;
514+
host.getByteWidth = async () => 4;
515+
516+
const ctx = new EvalContext({ data: host, container: base });
517+
await expect(evalNode(parseExpression('arr[1].m', false).ast, ctx)).resolves.toBe(42);
518+
});
519+
520+
it('covers bigint comparisons and compound assignments', async () => {
521+
const base = new BranchNode('base');
522+
const makeHost = (entries: [string, BranchNode][]) => new BranchHost(new Map(entries));
523+
524+
// Bigint comparisons exercise eq/lt/gte bigint paths and string equality coercion
525+
const bigA = new BranchNode('bigA', base, 5n);
526+
const bigB = new BranchNode('bigB', base, 7n);
527+
const compareCtx = new EvalContext({ data: makeHost([['bigA', bigA], ['bigB', bigB]]), container: base });
528+
await expect(evalNode(parseExpression('bigA == bigA', false).ast, compareCtx)).resolves.toBeTruthy();
529+
await expect(evalNode(parseExpression('bigA < bigB', false).ast, compareCtx)).resolves.toBeTruthy();
530+
await expect(evalNode(parseExpression('bigB >= bigA', false).ast, compareCtx)).resolves.toBeTruthy();
531+
await expect(evalNode(parseExpression('"1" == 1', false).ast, compareCtx)).resolves.toBeTruthy();
532+
533+
// Compound assignment operators cover arithmetic/bitwise branches
534+
const target = new BranchNode('c', base, 8);
535+
const other = new BranchNode('d', base, 3);
536+
const ctx = new EvalContext({ data: makeHost([['c', target], ['d', other]]), container: base });
537+
const run = async (expr: string, expected: number) => {
538+
target.value = expr.startsWith('c %=')
539+
? 9 // ensure a value divisible by d for %= branch
540+
: target.value;
541+
await expect(evalNode(parseExpression(expr, false).ast, ctx)).resolves.toBe(expected);
542+
};
543+
544+
target.value = 8; await run('c += d', 11);
545+
target.value = 8; await run('c -= d', 5);
546+
target.value = 8; await run('c *= d', 24);
547+
target.value = 8; await run('c /= d', 2);
548+
target.value = 9; await run('c %= d', 0);
549+
target.value = 1; await run('c <<= d', 8);
550+
target.value = 8; await run('c >>= d', 1);
551+
target.value = 6; await run('c &= d', 2);
552+
target.value = 6; await run('c ^= d', 5);
553+
target.value = 6; await run('c |= d', 7);
554+
});
555+
556+
it('normalizes scalar types that only provide a typename', async () => {
557+
const base = new BranchNode('base');
558+
let sawTypename = false;
559+
class TypenameHost extends BranchHost {
560+
async getValueType(container: RefContainer): Promise<string | ScalarType | undefined> {
561+
const cur = container.current as BranchNode | undefined;
562+
if (cur?.name === 'alias') {
563+
sawTypename = true;
564+
return { kind: 'int', bits: 16, typename: 'alias_t' };
565+
}
566+
return super.getValueType(container);
567+
}
568+
}
569+
570+
const host = new TypenameHost(new Map<string, BranchNode>([['alias', new BranchNode('alias', base, 1)]]));
571+
const ctx = new EvalContext({ data: host, container: base });
572+
await expect(evalNode(parseExpression('alias + 1', false).ast, ctx)).resolves.toBe(2);
573+
expect(sawTypename).toBe(true);
574+
});
575+
576+
it('recovers references across all findReferenceNode branches', async () => {
577+
class ClearingHost extends Host {
578+
async readValue(container: RefContainer): Promise<EvalValue> {
579+
const v = await super.readValue(container);
580+
container.current = undefined; // force recovery path
581+
return v;
582+
}
583+
}
584+
585+
const base = new FakeNode('base');
586+
const fn = new FakeNode('fn', base, ((n: number) => n + 1) as unknown as EvalValue);
587+
const val = new FakeNode('v', base, 1);
588+
const values = new Map<string, FakeNode>([['fn', fn], ['v', val], ['__Running', new FakeNode('__Running', base, 1)]]);
589+
const host = new ClearingHost(values);
590+
host.__Running = async () => 1;
591+
592+
const ctx = new EvalContext({ data: host, container: base });
593+
594+
const assignmentSeg = segFromAst(parseExpression('v = 2', false).ast);
595+
const callSeg = segFromAst(parseExpression('fn(3)', false).ast);
596+
const evalPointSeg: FormatSegment = {
597+
kind: 'FormatSegment',
598+
spec: 'd',
599+
value: { kind: 'EvalPointCall', intrinsic: '__Running', callee: { kind: 'Identifier', name: '__Running', start: 0, end: 0 } as Identifier, args: [], start: 0, end: 0 },
600+
start: 0,
601+
end: 0,
602+
};
603+
const printfSeg = segFromAst({
604+
kind: 'PrintfExpression',
605+
segments: [{ kind: 'TextSegment', text: 'x', start: 0, end: 0 }, segFromAst(parseExpression('v', false).ast)],
606+
resultType: 'string',
607+
start: 0,
608+
end: 0,
609+
} as PrintfExpression);
610+
const literalSeg = segFromAst({ kind: 'NumberLiteral', value: 9, raw: '9', valueType: 'number', constValue: 9, start: 0, end: 1 } as ASTNode);
611+
612+
await expect(evalNode(assignmentSeg, ctx)).resolves.toBeDefined();
613+
await expect(evalNode(callSeg, ctx)).resolves.toBeDefined();
614+
await expect(evalNode(evalPointSeg, ctx)).resolves.toBeDefined();
615+
await expect(evalNode(printfSeg, ctx)).resolves.toBeDefined();
616+
await expect(evalNode(literalSeg, ctx)).resolves.toBeDefined();
617+
});
618+
619+
it('handles call expressions and read/write failures', async () => {
620+
class FnHost extends Host {
621+
async writeValue(): Promise<EvalValue> {
622+
return undefined;
623+
}
624+
}
625+
class UndefinedReadHost extends Host {
626+
async readValue(): Promise<EvalValue> {
627+
return undefined;
628+
}
629+
}
630+
631+
const base = new FakeNode('base');
632+
const fnNode = new FakeNode('fn', base, ((a: number, b: number) => a + b) as unknown as EvalValue);
633+
const valNode = new FakeNode('x', base, 1);
634+
635+
const fnHost = new FnHost(new Map<string, FakeNode>([['fn', fnNode], ['x', valNode]]));
636+
const fnCtx = new EvalContext({ data: fnHost, container: base });
637+
await expect(evalNode(parseExpression('fn(2, 3)', false).ast, fnCtx)).resolves.toBe(5);
638+
await expect(evalNode(parseExpression('x(1)', false).ast, fnCtx)).rejects.toThrow('Callee is not callable.');
639+
await expect(evalNode(parseExpression('x = 2', false).ast, fnCtx)).rejects.toThrow('Write returned undefined');
640+
641+
const readHost = new UndefinedReadHost(new Map<string, FakeNode>([['x', valNode]]));
642+
const readCtx = new EvalContext({ data: readHost, container: base });
643+
await expect(evalNode(parseExpression('x', false).ast, readCtx)).rejects.toThrow('Undefined value');
644+
});
645+
646+
it('formats values across printf specs and NaN paths', async () => {
647+
const base = new BranchNode('base');
648+
const host = new BranchHost(new Map());
649+
const ctx = new EvalContext({ data: host, container: base });
650+
651+
const specs: Array<{ spec: FormatSegment['spec']; ast: ASTNode; expect: string }> = [
652+
{ spec: '%', ast: { kind: 'NumberLiteral', value: 0, raw: '0', valueType: 'number', constValue: 0, start: 0, end: 1 }, expect: '%' },
653+
{ spec: 'd', ast: { kind: 'NumberLiteral', value: Number.POSITIVE_INFINITY, raw: 'inf', valueType: 'number', constValue: undefined, start: 0, end: 1 }, expect: 'NaN' },
654+
{ spec: 'u', ast: { kind: 'NumberLiteral', value: Number.NaN, raw: 'NaN', valueType: 'number', constValue: undefined, start: 0, end: 1 }, expect: 'NaN' },
655+
{ spec: 'u', ast: { kind: 'NumberLiteral', value: -5, raw: '-5', valueType: 'number', constValue: -5, start: 0, end: 2 }, expect: String(((-5) >>> 0)) },
656+
{ spec: 'x', ast: { kind: 'NumberLiteral', value: 255, raw: '255', valueType: 'number', constValue: 255, start: 0, end: 3 }, expect: 'ff' },
657+
{ spec: 't', ast: { kind: 'BooleanLiteral', value: false, valueType: 'boolean', start: 0, end: 1 } as ASTNode, expect: 'false' },
658+
{ spec: 'S', ast: { kind: 'StringLiteral', value: 'hi', raw: '"hi"', valueType: 'string', constValue: 'hi', start: 0, end: 2 } as ASTNode, expect: 'hi' },
659+
{ spec: 'C', ast: { kind: 'StringLiteral', value: 'foo', raw: '"foo"', valueType: 'string', constValue: 'foo', start: 0, end: 3 } as ASTNode, expect: 'foo' },
660+
];
661+
662+
for (const { spec, ast, expect: expected } of specs) {
663+
const seg: FormatSegment = { kind: 'FormatSegment', spec, value: ast, start: 0, end: 0 };
664+
await expect(evalNode(seg, ctx)).resolves.toBe(expected);
665+
}
666+
});
471667
});

0 commit comments

Comments
 (0)