diff --git a/src/views/component-viewer/data-host/byte-encoding.ts b/src/views/component-viewer/data-host/byte-encoding.ts new file mode 100644 index 00000000..4ea4030f --- /dev/null +++ b/src/views/component-viewer/data-host/byte-encoding.ts @@ -0,0 +1,185 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// generated with AI + +import type { EvalValue, ScalarType } from '../parser-evaluator/ref-container'; + +// --- Little-endian encode helpers --- + +/** + * Encode a JS number as a little-endian Uint8Array of `size` bytes. + * For size > 4, delegates to the bigint path to preserve sign extension + * beyond the 32-bit range of `>>> 0`. + */ +export function leIntToBytes(v: number, size: number): Uint8Array { + if (size > 4) { + return leBigIntToBytes(BigInt(v), size); + } + const out = new Uint8Array(size); + let tmp = v >>> 0; + for (let i = 0; i < size; i++) { + out.set([tmp & 0xff], i); + tmp >>>= 8; + } + return out; +} + +/** Encode a BigInt as a little-endian Uint8Array of `size` bytes. */ +export function leBigIntToBytes(v: bigint, size: number): Uint8Array { + const out = new Uint8Array(size); + let tmp = v; + for (let i = 0; i < size; i++) { + // eslint-disable-next-line security/detect-object-injection + out[i] = Number(tmp & 0xffn); + tmp >>= 8n; + } + return out; +} + +/** + * Encode a value (number, bigint, or Uint8Array) to exactly `size` LE bytes. + * - number → little-endian integer + * - bigint → little-endian integer + * - Uint8Array → truncated or zero-padded to `size` + */ +export function encodeToLeBytes(value: number | bigint | Uint8Array, size: number): Uint8Array { + if (typeof value === 'number') { + return leIntToBytes(Math.trunc(value), size); + } + if (typeof value === 'bigint') { + return leBigIntToBytes(value, size); + } + // Uint8Array: avoid extra allocation when already the right size. + // Safe to return the original reference because callers (e.g. MemoryContainer.write) + // always copy via TypedArray.set(). + if (value.length === size) { + return value; + } + const out = new Uint8Array(size); + out.set(value.subarray(0, size), 0); + return out; +} + +// --- Little-endian decode helpers --- + +/** + * Decode a little-endian Uint8Array as an unsigned 32-bit number. + * Only valid for up to 4 bytes; larger inputs silently overflow. + */ +export function leToNumber(bytes: Uint8Array): number { + if (bytes.length > 4) { + // Only the low 4 bytes are meaningful in 32-bit arithmetic. + + bytes = bytes.subarray(0, 4); + } + let out = 0; + for (const b of Array.from(bytes).reverse()) { + out = (out << 8) | (b & 0xff); + } + return out >>> 0; +} + +/** Decode a little-endian Uint8Array as a signed integer (sign-extended). */ +export function leToSignedNumber(bytes: Uint8Array): number { + const unsigned = leToNumber(bytes); + const bits = bytes.length * 8; + if (bits <= 0 || bits >= 32) { + return unsigned | 0; + } + const signBit = 1 << (bits - 1); + return (unsigned & signBit) ? (unsigned | (~0 << bits)) : unsigned; +} + +/** Decode a 2-byte little-endian IEEE 754 half-precision float. */ +export function leToFloat16(bytes: Uint8Array): number { + if (bytes.length < 2) { + return NaN; + } + const half = bytes[0] | (bytes[1] << 8); + const sign = (half & 0x8000) ? -1 : 1; + const exp = (half >> 10) & 0x1f; + const frac = half & 0x03ff; + if (exp === 0) { + if (frac === 0) { + return sign < 0 ? -0 : 0; + } + return sign * Math.pow(2, -14) * (frac / 1024); + } + if (exp === 0x1f) { + return frac === 0 ? (sign * Infinity) : NaN; + } + return sign * Math.pow(2, exp - 15) * (1 + frac / 1024); +} + +/** + * Interpret raw LE bytes as a typed EvalValue. + * + * This is the type-interpretation logic extracted from the old MemoryHost.readValue. + * The caller is responsible for reading the raw bytes from the byte store. + * + * @param raw The raw bytes to interpret. + * @param widthBytes How many bytes were read. + * @param valueType Scalar type hint (optional). + * @returns The decoded value as a number, bigint, or Uint8Array copy. + */ +export function decodeBytesToValue( + raw: Uint8Array, + widthBytes: number, + valueType: ScalarType | undefined, +): EvalValue { + // Check if widthBytes exceeds the natural type size. + // This indicates a multi-byte value (e.g., uint8_t with size="4" for IP address) + // that should remain as raw bytes rather than being converted to a number. + const typeSize = valueType?.bits ? valueType.bits / 8 : undefined; + if (typeSize && widthBytes > typeSize && valueType?.kind !== 'float') { + return raw.slice(); + } + + // Float kinds decode as float16/float32/float64 + if (valueType?.kind === 'float') { + if (widthBytes === 2) { + return leToFloat16(raw); + } + if (widthBytes === 4) { + const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); + return dv.getFloat32(0, true); + } + if (widthBytes === 8) { + const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); + return dv.getFloat64(0, true); + } + // Non-standard float width: return a copy of the raw bytes + return raw.slice(); + } + + // ≤4 bytes: JS number (uint32 or signed) + if (widthBytes <= 4) { + return valueType?.kind === 'int' ? leToSignedNumber(raw) : leToNumber(raw); + } + + // 8 bytes: BigInt for full 64-bit integer fidelity + if (widthBytes === 8) { + let out = 0n; + for (let i = 0; i < 8; i++) { + // eslint-disable-next-line security/detect-object-injection + out |= BigInt(raw[i]) << BigInt(8 * i); + } + return out; + } + + // >8 bytes: return a copy of the raw bytes + return raw.slice(); +} diff --git a/src/views/component-viewer/data-host/memory-host.ts b/src/views/component-viewer/data-host/memory-host.ts index d82c9ce4..a9abf8e3 100644 --- a/src/views/component-viewer/data-host/memory-host.ts +++ b/src/views/component-viewer/data-host/memory-host.ts @@ -16,7 +16,7 @@ // generated with AI import { componentViewerLogger } from '../../../logger'; -import { EvalValue, RefContainer } from '../parser-evaluator/model-host'; +import { encodeToLeBytes } from './byte-encoding'; import { ValidatingCache } from './validating-cache'; export class MemoryContainer { @@ -79,61 +79,6 @@ export class MemoryContainer { } } -// --- helpers (LE encoding) --- -function leToNumber(bytes: Uint8Array): number { - let out = 0; - for (const b of Array.from(bytes).reverse()) { - out = (out << 8) | (b & 0xff); - } - return out >>> 0; -} - -function leToSignedNumber(bytes: Uint8Array): number { - const unsigned = leToNumber(bytes); - const bits = bytes.length * 8; - if (bits <= 0 || bits >= 32) { - return unsigned | 0; - } - const signBit = 1 << (bits - 1); - return (unsigned & signBit) ? (unsigned | (~0 << bits)) : unsigned; -} - -function leToFloat16(bytes: Uint8Array): number { - if (bytes.length < 2) { - return NaN; - } - const half = bytes[0] | (bytes[1] << 8); - const sign = (half & 0x8000) ? -1 : 1; - const exp = (half >> 10) & 0x1f; - const frac = half & 0x03ff; - if (exp === 0) { - if (frac === 0) { - return sign < 0 ? -0 : 0; - } - return sign * Math.pow(2, -14) * (frac / 1024); - } - if (exp === 0x1f) { - return frac === 0 ? (sign * Infinity) : NaN; - } - return sign * Math.pow(2, exp - 15) * (1 + frac / 1024); -} - -export const __test__ = { - leToFloat16, -}; -function leIntToBytes(v: number, size: number): Uint8Array { - const out = new Uint8Array(size); - let tmp = v >>> 0; - for (let i = 0; i < size; i++) { - out.set([tmp & 0xff], i); - tmp >>>= 8; - } - return out; -} - -export type Endianness = 'little'; -export interface HostOptions { endianness?: Endianness; } - type ElementMeta = { offsets: number[]; // append offsets within the symbol sizes: number[]; // logical size (actualSize) per append @@ -141,10 +86,9 @@ type ElementMeta = { elementSize?: number; // known uniform stride when consistent }; -// The piece your host delegates to for readValue/writeValue. +// Pure byte store for SCVD variables. No type interpretation — that lives in the evaluator. export class MemoryHost { private cache = new ValidatingCache(); - private endianness: Endianness; private elementMeta = new Map(); private getOrInitMeta(name: string): ElementMeta { @@ -168,160 +112,63 @@ export class MemoryHost { } constructor() { - this.endianness = 'little'; } private getContainer(varName: string): MemoryContainer { return this.cache.ensure(varName, () => new MemoryContainer(varName), false); } - // Read a value, using byte-only offsets and widths. - public async readValue(ref: RefContainer): Promise { - const variableName = ref.anchor?.name; - const widthBytes = ref.widthBytes ?? 0; - if (!variableName || widthBytes <= 0) { + /** + * Read raw bytes from a named variable at the given byte offset. + * + * For sizes ≤ 8 an exact match is required (returns undefined when the + * store is shorter than offset + size). For sizes > 8 a partial read is + * allowed so that callers reading large buffers don't fail on undersized stores. + * + * Always returns a **copy** so callers never alias internal storage. + */ + public read(name: string, offset: number, size: number): Uint8Array | undefined { + if (!name || size <= 0) { return undefined; } - - const container = this.getContainer(variableName); - const byteOff = ref.offsetBytes ?? 0; - - const raw = widthBytes > 8 - ? container.readPartial(byteOff, widthBytes) - : container.readExact(byteOff, widthBytes); - if (!raw) { - componentViewerLogger.trace(`[MemoryHost.readValue] MISS: var="${variableName}" offset=${byteOff} width=${widthBytes}`); + const container = this.cache.get(name); + if (!container) { return undefined; } - componentViewerLogger.trace(`[MemoryHost.readValue] var="${variableName}" offset=${byteOff} width=${widthBytes} data=[${Array.from(raw).map(b => b.toString(16).padStart(2, '0')).join(' ')}]`); - - if (this.endianness !== 'little') { - // TOIMPL: add BE support if needed - } - - // Check if widthBytes exceeds the natural type size - // This indicates a multi-byte value (e.g., uint8_t with size="4" for IP address) - // that should remain as raw bytes rather than being converted to a number - const typeSize = ref.valueType?.bits ? ref.valueType.bits / 8 : undefined; - if (typeSize && widthBytes > typeSize && ref.valueType?.kind !== 'float') { - componentViewerLogger.trace(`[MemoryHost.readValue] → raw bytes (width=${widthBytes} > typeSize=${typeSize})`); - return raw.slice(); - } - - // Interpret the bytes: - // - float kinds decode as float32/float64 - // - ≤4 bytes: JS number (uint32) - // - 8 bytes: BigInt for full 64-bit integer fidelity - // - >8 bytes: return a copy of the raw bytes - if (ref.valueType?.kind === 'float') { - if (widthBytes === 2) { - return leToFloat16(raw); - } - if (widthBytes === 4) { - const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); - return dv.getFloat32(0, true); - } - if (widthBytes === 8) { - const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); - return dv.getFloat64(0, true); - } - } - if (widthBytes <= 4) { - const value = ref.valueType?.kind === 'int' ? leToSignedNumber(raw) : leToNumber(raw); - componentViewerLogger.trace(`[MemoryHost.readValue] → decoded as ${ref.valueType?.kind === 'int' ? 'int' : 'uint'}: ${value}`); - return value; + const raw = size > 8 + ? container.readPartial(offset, size) + : container.readExact(offset, size); + if (!raw) { + componentViewerLogger.trace(`[MemoryHost.read] MISS: var="${name}" offset=${offset} size=${size}`); + return undefined; } - if (widthBytes === 8) { - let out = 0n; - for (let i = 0; i < 8; i++) { - // raw is a Uint8Array; indexed access is safe here. - // eslint-disable-next-line security/detect-object-injection - out |= BigInt(raw[i]) << BigInt(8 * i); - } - componentViewerLogger.trace(`[MemoryHost.readValue] → decoded as bigint: ${out}`); + componentViewerLogger.trace(`[MemoryHost.read] var="${name}" offset=${offset} size=${size} data=[${Array.from(raw).map(b => b.toString(16).padStart(2, '0')).join(' ')}]`); + // For partial reads that returned fewer bytes than requested, zero-pad to the requested size + if (raw.length < size) { + const out = new Uint8Array(size); + out.set(raw, 0); return out; } - // for larger widths, return a copy of the bytes - componentViewerLogger.trace(`[MemoryHost.readValue] → raw bytes (len=${raw.length})`); return raw.slice(); } - // Read raw bytes without interpretation. - public async readRaw(ref: RefContainer, size: number): Promise { - const variableName = ref.anchor?.name; - if (!variableName || size <= 0) { - return undefined; - } - const container = this.getContainer(variableName); - const byteOff = ref.offsetBytes ?? 0; - const raw = container.readPartial(byteOff, size); - if (!raw) { - return undefined; - } - if (raw.length === size) { - return raw.slice(); - } - const out = new Uint8Array(size); - out.set(raw, 0); - return out; + /** + * Write raw bytes into a named variable's buffer at the given offset. + * The buffer is zero-filled to `totalSize` bytes when provided. + */ + public write(name: string, offset: number, data: Uint8Array, totalSize?: number): void { + const container = this.getContainer(name); + container.write(offset, data, totalSize); + this.cache.set(name, container, true); } - // Write a value, using byte-only offsets and widths. - public async writeValue(ref: RefContainer, value: EvalValue, virtualSize?: number): Promise { - const variableName = ref.anchor?.name; - const widthBytes = ref.widthBytes ?? 0; - if (!variableName || widthBytes <= 0) { - return; - } - - const container = this.getContainer(variableName); - const byteOff = ref.offsetBytes ?? 0; - - let buf: Uint8Array; - - if (value instanceof Uint8Array) { - if (value.length === widthBytes) { - buf = value; - } else { - // truncate or pad to widthBytes - buf = new Uint8Array(widthBytes); - buf.set(value.subarray(0, widthBytes), 0); - } - } else { - // normalize value to number then to bytes - let valNum: number | bigint; - if (typeof value === 'boolean') { - valNum = value ? 1 : 0; - } else if (typeof value === 'number') { - valNum = Math.trunc(value); - } else if (typeof value === 'bigint') { - valNum = value; - } else { - componentViewerLogger.error('writeValue: unsupported value type'); - return; - } - - if (typeof valNum === 'bigint') { - buf = new Uint8Array(widthBytes); - let tmp = valNum; - for (let i = 0; i < widthBytes; i++) { - // Indexing into a Uint8Array is safe here. - // eslint-disable-next-line security/detect-object-injection - buf[i] = Number(tmp & 0xFFn); - tmp >>= 8n; - } - } else { - buf = leIntToBytes(valNum, widthBytes); - } - } - - if (virtualSize !== undefined && virtualSize < widthBytes) { - componentViewerLogger.error(`writeValue: virtualSize (${virtualSize}) must be >= widthBytes (${widthBytes})`); - return; - } - - const total = virtualSize ?? widthBytes; - container.write(byteOff, buf, total); + /** + * Return the total byte length of a named variable's backing buffer, + * or 0 if the variable does not exist. + */ + public getByteLength(name: string): number { + const container = this.cache.get(name); + return container?.byteLength ?? 0; } public setVariable( @@ -351,22 +198,8 @@ export class MemoryHost { } // normalize payload to exactly `size` bytes (numbers LE-encoded) - let buf: Uint8Array; - if (typeof value === 'number') { - buf = leIntToBytes(Math.trunc(value), size); - } else if (typeof value === 'bigint') { - buf = new Uint8Array(size); - let tmp = value; - for (let i = 0; i < size; i++) { - // Indexing into a Uint8Array is safe here. - // eslint-disable-next-line security/detect-object-injection - buf[i] = Number(tmp & 0xffn); - tmp >>= 8n; - } - } else if (value instanceof Uint8Array) { - // Avoid an extra allocation when already the right size - buf = value.length === size ? value : new Uint8Array(value.subarray(0, size)); - } else { + const buf = encodeToLeBytes(value, size); + if (buf.length !== size) { componentViewerLogger.error('setVariable: unsupported value type'); return; } @@ -449,4 +282,5 @@ export class MemoryHost { componentViewerLogger.trace(`[MemoryHost.getElementTargetBase] var="${name}" index=${index} → targetBase=0x${targetBase?.toString(16)}`); return targetBase; } + } diff --git a/src/views/component-viewer/model/scvd-var.ts b/src/views/component-viewer/model/scvd-var.ts index b764fc92..c24a6f0d 100644 --- a/src/views/component-viewer/model/scvd-var.ts +++ b/src/views/component-viewer/model/scvd-var.ts @@ -23,6 +23,7 @@ import { ScvdExpression } from './scvd-expression'; import { Json } from './scvd-base'; import { ScvdNode } from './scvd-node'; import { getArrayFromJson, getStringFromJson } from './scvd-utils'; +import { encodeStringToBytes } from '../parser-evaluator/string-ops'; export class ScvdVar extends ScvdNode { private _value: ScvdExpression | undefined; @@ -88,7 +89,7 @@ export class ScvdVar extends ScvdNode { } } - public override async getValue(): Promise { + public override async getValue(): Promise { if (this._value === undefined) { return undefined; } @@ -96,6 +97,9 @@ export class ScvdVar extends ScvdNode { if (typeof val === 'number' || typeof val === 'bigint') { return val; } + if (typeof val === 'string') { + return encodeStringToBytes(val, this.getTypeSize() ?? 1); + } return undefined; } diff --git a/src/views/component-viewer/parser-evaluator/intrinsics.ts b/src/views/component-viewer/parser-evaluator/intrinsics.ts index 5c64292c..ff71ff93 100644 --- a/src/views/component-viewer/parser-evaluator/intrinsics.ts +++ b/src/views/component-viewer/parser-evaluator/intrinsics.ts @@ -238,14 +238,33 @@ export async function handlePseudoMember( container.member = baseRef; container.current = baseRef; container.valueType = undefined; - const fn = property === '_count' ? data._count : data._addr; - if (typeof fn !== 'function') { - onError?.(`Missing pseudo-member ${property}`); - return undefined; + + if (property === '_count') { + const fn = data._count; + if (typeof fn !== 'function') { + onError?.('Missing pseudo-member _count'); + return undefined; + } + const out = await fn.call(data, container); + if (out === undefined) { + onError?.('Pseudo-member _count returned undefined'); + } + return out; } - const out = await fn.call(data, container); - if (out === undefined) { - onError?.(`Pseudo-member ${property} returned undefined`); + + if (property === '_addr') { + const fn = data._addr; + if (typeof fn !== 'function') { + onError?.('Missing pseudo-member _addr'); + return undefined; + } + const out = await fn.call(data, container); + if (out === undefined) { + onError?.('Pseudo-member _addr returned undefined'); + } + return out; } - return out; + + onError?.(`Unknown pseudo-member ${property}`); + return undefined; } diff --git a/src/views/component-viewer/parser-evaluator/string-ops.ts b/src/views/component-viewer/parser-evaluator/string-ops.ts new file mode 100644 index 00000000..d073d778 --- /dev/null +++ b/src/views/component-viewer/parser-evaluator/string-ops.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// generated with AI + +/** + * Shared string helpers used by model (ScvdVar) and intrinsics. + */ + +/** + * Encode a string to a byte array based on the element type width. + * - typeSize 1 (or unspecified): UTF-8 encoding (1 byte per ASCII char) + * - typeSize 2: UTF-16 LE code units (2 bytes per char) + * - typeSize 4: Unicode code points, UTF-32 LE (4 bytes per char) + * + * Returns character data only — no trailing \\0. + * The caller (typically {@link MemoryContainer}) is responsible for + * providing the null terminator via zero-padding in the backing buffer. + */ +export function encodeStringToBytes(text: string, typeSize = 1): Uint8Array { + if (typeSize === 2) { + // UTF-16 LE + const buf = new Uint8Array(text.length * 2); + for (let i = 0; i < text.length; i++) { + const code = text.charCodeAt(i); + buf[i * 2] = code & 0xFF; + buf[i * 2 + 1] = (code >> 8) & 0xFF; + } + return buf; + } + if (typeSize === 4) { + // UTF-32 LE (code points) + const codePoints = [...text].map(ch => ch.codePointAt(0) ?? 0); + const buf = new Uint8Array(codePoints.length * 4); + const view = new DataView(buf.buffer); + for (const [i, cp] of codePoints.entries()) { + view.setUint32(i * 4, cp, true); + } + return buf; + } + // Default: UTF-8 + return new TextEncoder().encode(text); +} diff --git a/src/views/component-viewer/scvd-eval-interface.ts b/src/views/component-viewer/scvd-eval-interface.ts index 07cbd29e..9480f4fc 100644 --- a/src/views/component-viewer/scvd-eval-interface.ts +++ b/src/views/component-viewer/scvd-eval-interface.ts @@ -17,6 +17,7 @@ import { componentViewerLogger } from '../../logger'; import { DataAccessHost, EvalValue, ModelHost, RefContainer, ScalarType } from './parser-evaluator/model-host'; import type { IntrinsicProvider } from './parser-evaluator/intrinsics'; +import { decodeBytesToValue, leBigIntToBytes, leIntToBytes } from './data-host/byte-encoding'; import { ScvdNode } from './model/scvd-node'; import { MemoryHost } from './data-host/memory-host'; import { RegisterHost } from './data-host/register-host'; @@ -365,9 +366,16 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr const width = container.widthBytes ?? 0; componentViewerLogger.trace(`[ScvdEvalInterface.readValue] container: var="${varName}" offset=${offset} width=${width}`); try { - const value = await this._memHost.readValue(container); + if (!varName || varName === '?' || width <= 0) { + return undefined; + } + const raw = this._memHost.read(varName, offset, width); + if (!raw) { + return undefined; + } + const value = decodeBytesToValue(raw, width, container.valueType); componentViewerLogger.trace(`[ScvdEvalInterface.readValue] → ${value}`); - return value as EvalValue; + return value; } catch (e) { componentViewerLogger.error(`ScvdEvalInterface.readValue: exception for container with base=${container.base.getDisplayLabel()}: ${e}`); return undefined; @@ -383,7 +391,15 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr const width = container.widthBytes ?? 0; componentViewerLogger.trace(`[ScvdEvalInterface.writeValue] container: var="${varName}" offset=${offset} width=${width} value=${value}`); try { - await this._memHost.writeValue(container, value); + if (!varName || varName === '?' || width <= 0) { + return undefined; + } + const buf = this.encodeEvalValue(value, width); + if (!buf) { + componentViewerLogger.error('writeValue: unsupported value type'); + return undefined; + } + this._memHost.write(varName, offset, buf, width); return value; } catch (e) { componentViewerLogger.error(`ScvdEvalInterface.writeValue: exception for container with base=${container.base.getDisplayLabel()}: ${e}`); @@ -393,6 +409,28 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr } } + /** Encode an EvalValue to exactly `widthBytes` LE bytes for writing. */ + private encodeEvalValue(value: EvalValue, widthBytes: number): Uint8Array | undefined { + if (value instanceof Uint8Array) { + if (value.length === widthBytes) { + return value; + } + const buf = new Uint8Array(widthBytes); + buf.set(value.subarray(0, widthBytes), 0); + return buf; + } + if (typeof value === 'boolean') { + return leIntToBytes(value ? 1 : 0, widthBytes); + } + if (typeof value === 'number') { + return leIntToBytes(Math.trunc(value), widthBytes); + } + if (typeof value === 'bigint') { + return leBigIntToBytes(value, widthBytes); + } + return undefined; + } + /* ---------------- Intrinsics ---------------- */ public async __FindSymbol(symbolName: string): Promise { @@ -630,9 +668,13 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr if (value instanceof Uint8Array) { return this._formatSpecifier.format(spec, this.ensureNullTerminated(value), { typeInfo, allowUnknownSpec: true }); } - const raw = await this.readRawBytesFromContainer(container, base, formatRef); - if (raw !== undefined) { - return this._formatSpecifier.format(spec, this.ensureNullTerminated(raw), { typeInfo, allowUnknownSpec: true }); + const tAnchor = container.anchor ?? base; + const tWidth = container.widthBytes ?? typeInfo.widthBytes; + if (tAnchor?.name && tWidth && tWidth > 0) { + const raw = this._memHost.read(tAnchor.name, container.offsetBytes ?? 0, tWidth); + if (raw) { + return this._formatSpecifier.format(spec, this.ensureNullTerminated(raw), { typeInfo, allowUnknownSpec: true }); + } } return this._formatSpecifier.format(spec, value, { typeInfo, allowUnknownSpec: true }); } @@ -640,9 +682,13 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr if (value instanceof Uint8Array) { return this._formatSpecifier.format(spec, value, { typeInfo, allowUnknownSpec: true }); } - const raw = await this.readRawBytesFromContainer(container, base, formatRef, 6); - if (raw !== undefined) { - return this._formatSpecifier.format(spec, raw, { typeInfo, allowUnknownSpec: true }); + const mAnchor = container.anchor ?? base; + const mWidth = container.widthBytes ?? typeInfo.widthBytes ?? 6; + if (mAnchor?.name && mWidth > 0) { + const raw = this._memHost.read(mAnchor.name, container.offsetBytes ?? 0, mWidth); + if (raw) { + return this._formatSpecifier.format(spec, raw, { typeInfo, allowUnknownSpec: true }); + } } const isPointer = formatRef?.getIsPointer?.() ?? false; if (isPointer && typeof value === 'number') { @@ -824,35 +870,6 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr return await this.readBytesFromPointer(value, byteCount); } - /** - * Read raw bytes from container using anchor and width information. - * Used by %t and %M format specifiers for text and MAC addresses. - */ - private async readRawBytesFromContainer( - container: RefContainer, - base: ScvdNode | undefined, - formatRef: ScvdNode | undefined, - defaultWidth?: number - ): Promise { - const anchor = container.anchor ?? base; - let width = container.widthBytes; - if (width === undefined) { - width = formatRef ? await this.getByteWidth(formatRef) : undefined; - } - if (width === undefined && defaultWidth !== undefined) { - width = defaultWidth; - } - if (anchor?.name !== undefined && width !== undefined && width > 0) { - const cacheRef: RefContainer = { - ...container, - anchor, - widthBytes: width - }; - return await this._memHost.readRaw(cacheRef, width); - } - return undefined; - } - /** * Ensure a byte array is null-terminated for string formatting. */ diff --git a/src/views/component-viewer/statement-engine/statement-var.ts b/src/views/component-viewer/statement-engine/statement-var.ts index c7afd420..29d6abb1 100644 --- a/src/views/component-viewer/statement-engine/statement-var.ts +++ b/src/views/component-viewer/statement-engine/statement-var.ts @@ -43,9 +43,21 @@ export class StatementVar extends StatementBase { return; } - const initValue = value ?? 0; + let initValue: number | bigint | Uint8Array; + if (value instanceof Uint8Array) { + // String values arrive as encoded byte arrays from ScvdVar. + // Fit into the target buffer (truncate if necessary). + // No explicit \0 — MemoryContainer zero-fills unused space. + if (value.length > targetSize) { + initValue = value.subarray(0, targetSize); + } else { + initValue = value; + } + } else { + initValue = value ?? 0; + } executionContext.memoryHost.setVariable(name, targetSize, initValue, -1, 0); - componentViewerLogger.debug(`Line: ${this.line}: Variable "${name}" created with value: ${value}`); + componentViewerLogger.debug(`Line: ${this.line}: Variable "${name}" created with value: ${initValue}`); } } } diff --git a/src/views/component-viewer/test/integration/cache-host-combined.test.ts b/src/views/component-viewer/test/integration/cache-host-combined.test.ts index 35e6e7c3..8874e1ae 100644 --- a/src/views/component-viewer/test/integration/cache-host-combined.test.ts +++ b/src/views/component-viewer/test/integration/cache-host-combined.test.ts @@ -37,27 +37,10 @@ import { TargetReadCache } from '../../target-read-cache'; import { MemoryHost } from '../../data-host/memory-host'; -import { RefContainer } from '../../parser-evaluator/model-host'; -import { ScvdNode } from '../../model/scvd-node'; +import { leToNumber } from '../../data-host/byte-encoding'; // ---------- helpers ---------- -class NamedStubBase extends ScvdNode { - constructor(name: string) { - super(undefined); - this.name = name; - } -} - -const makeContainer = (name: string, widthBytes: number, offsetBytes = 0): RefContainer => ({ - base: new NamedStubBase(name), - anchor: new NamedStubBase(name), - current: new NamedStubBase(name), - offsetBytes, - widthBytes, - valueType: undefined, -}); - function makeBlock(nextAddr: number, len: number, id: number): Uint8Array { const buf = new Uint8Array(9); const view = new DataView(buf.buffer); @@ -206,8 +189,9 @@ describe('Combined: TargetReadCache + MemoryHost – RTX linked-list walk', () = } // Verify sentinel's len (max_used pattern) - const lastLenRef = makeContainer('mem_list_com', 4, (result.count - 1) * 9 + 4); - expect(await memHost.readValue(lastLenRef)).toBe(30000); + const lastLenBytes = memHost.read('mem_list_com', (result.count - 1) * 9 + 4, 4); + expect(lastLenBytes).toBeDefined(); + expect(leToNumber(lastLenBytes!)).toBe(30000); }); it('cycle 2: clearNonConst + prefetch + walk sees updated target memory', async () => { @@ -264,13 +248,14 @@ describe('Combined: TargetReadCache + MemoryHost – RTX linked-list walk', () = expect(result.count).toBe(6); // Verify updated block at 0x1050 now points to 0x1078 (not sentinel) - const block3NextRef = makeContainer('mem_list_com', 4, 2 * 9); // 3rd element, offset 0 - const block3Next = await memHost.readValue(block3NextRef); - expect(block3Next).toBe(0x1078); + const block3NextBytes = memHost.read('mem_list_com', 2 * 9, 4); // 3rd element, offset 0 + expect(block3NextBytes).toBeDefined(); + expect(leToNumber(block3NextBytes!)).toBe(0x1078); // Verify sentinel len updated - const lastLenRef = makeContainer('mem_list_com', 4, (result.count - 1) * 9 + 4); - expect(await memHost.readValue(lastLenRef)).toBe(29000); + const lastLenBytes = memHost.read('mem_list_com', (result.count - 1) * 9 + 4, 4); + expect(lastLenBytes).toBeDefined(); + expect(leToNumber(lastLenBytes!)).toBe(29000); }); it('cycle 2 prefetch correctly updates data for blocks seen in cycle 1', async () => { @@ -333,8 +318,8 @@ describe('Combined: TargetReadCache + MemoryHost – RTX linked-list walk', () = let tcbCount = 0; for (let i = 0; i < result.count - 1; i++) { const addr = memHost.getElementTargetBase('mem_list_com', i)!; - const idRef = makeContainer('mem_list_com', 1, i * 9 + 8); - const id = await memHost.readValue(idRef); + const idBytes = memHost.read('mem_list_com', i * 9 + 8, 1); + const id = idBytes ? leToNumber(idBytes) : undefined; if (id === 0xF1) { tcbCount++; @@ -477,7 +462,7 @@ describe('Combined: TargetReadCache + MemoryHost – RTX linked-list walk', () = // Both should exist expect(memHost.getArrayElementCount('mem_list_com')).toBe(2); - expect(await memHost.readRaw(makeContainer('os_Config', 4), 4)).toEqual( + expect(memHost.read('os_Config', 0, 4)).toEqual( new Uint8Array([0x20, 0x0D, 0x01, 0x20]) ); @@ -485,8 +470,8 @@ describe('Combined: TargetReadCache + MemoryHost – RTX linked-list walk', () = memHost.clearNonConst(); // mem_list_com gone, os_Config survives - expect(await memHost.readRaw(makeContainer('mem_list_com', 9), 9)).toBeUndefined(); - expect(await memHost.readRaw(makeContainer('os_Config', 4), 4)).toEqual( + expect(memHost.read('mem_list_com', 0, 9)).toBeUndefined(); + expect(memHost.read('os_Config', 0, 4)).toEqual( new Uint8Array([0x20, 0x0D, 0x01, 0x20]) ); }); diff --git a/src/views/component-viewer/test/integration/memory-host/append-virtualsize-stride.test.ts b/src/views/component-viewer/test/integration/memory-host/append-virtualsize-stride.test.ts index 84b202e5..f8d7691f 100644 --- a/src/views/component-viewer/test/integration/memory-host/append-virtualsize-stride.test.ts +++ b/src/views/component-viewer/test/integration/memory-host/append-virtualsize-stride.test.ts @@ -42,30 +42,10 @@ */ import { MemoryHost, MemoryContainer } from '../../../data-host/memory-host'; -import { RefContainer } from '../../../parser-evaluator/model-host'; -import { ScvdNode } from '../../../model/scvd-node'; +import { leToNumber } from '../../../data-host/byte-encoding'; // ---------- helpers ---------- -class NamedStub extends ScvdNode { - constructor(name: string) { - super(undefined); - this.name = name; - } -} - -const makeRef = (name: string, widthBytes: number, offsetBytes = 0): RefContainer => { - const node = new NamedStub(name); - return { - base: node, - anchor: node, - current: node, - offsetBytes, - widthBytes, - valueType: undefined, - }; -}; - /** Build a 9-byte mem_block_t: [next:4][len:4][id:1] in little-endian. */ function makeBlock9(nextAddr: number, len: number, id: number): Uint8Array { const buf = new Uint8Array(9); @@ -122,7 +102,7 @@ describe('Append (-1): 9-byte items with virtualSize=9 (mem_block_t)', () => { expect(host.getArrayElementCount('bl')).toBe(5); }); - it('stride-based readback: element[i] at offset i*9 returns correct data', async () => { + it('stride-based readback: element[i] at offset i*9 returns correct data', () => { const stride = 9; const ids = [0xA1, 0xB2, 0xC3, 0xD4, 0xE5]; for (let i = 0; i < ids.length; i++) { @@ -136,52 +116,52 @@ describe('Append (-1): 9-byte items with virtualSize=9 (mem_block_t)', () => { const base = i * stride; // next (offset 0, 4 bytes) - const next = await host.readValue(makeRef('bl', 4, base + 0)); + const next = leToNumber(host.read('bl', base + 0, 4)!); expect(next).toBe(0x100 * (i + 1)); // len (offset 4, 4 bytes) - const len = await host.readValue(makeRef('bl', 4, base + 4)); + const len = leToNumber(host.read('bl', base + 4, 4)!); expect(len).toBe(41); // id (offset 8, 1 byte) - const id = await host.readValue(makeRef('bl', 1, base + 8)); + const id = host.read('bl', base + 8, 1)![0]; expect(id).toBe(ids.at(i)); } }); - it('reading one byte BEFORE an element boundary returns previous element tail', async () => { + it('reading one byte BEFORE an element boundary returns previous element tail', () => { // Element 0: bytes [0..8], Element 1: bytes [9..17] host.setVariable('bl', 9, makeBlock9(0, 0, 0xAA), -1, 0x1000, 9); host.setVariable('bl', 9, makeBlock9(0, 0, 0xBB), -1, 0x2000, 9); // Byte 8 = last byte of element 0 = id byte = 0xAA - const atBoundaryMinus1 = await host.readValue(makeRef('bl', 1, 8)); + const atBoundaryMinus1 = host.read('bl', 8, 1)![0]; expect(atBoundaryMinus1).toBe(0xAA); // Byte 9 = first byte of element 1 = low byte of next field = 0x00 - const atBoundary = await host.readValue(makeRef('bl', 1, 9)); + const atBoundary = host.read('bl', 9, 1)![0]; expect(atBoundary).toBe(0x00); }); - it('reading one byte AFTER an element boundary returns next element head', async () => { + it('reading one byte AFTER an element boundary returns next element head', () => { host.setVariable('bl', 9, makeBlock9(0xDEADBEEF, 0, 0), -1, 0x1000, 9); host.setVariable('bl', 9, makeBlock9(0xCAFEBABE, 0, 0), -1, 0x2000, 9); // Byte 9 = first byte of element 1 next field = 0xBE (LE of 0xCAFEBABE) - const firstByteEl1 = await host.readValue(makeRef('bl', 1, 9)); + const firstByteEl1 = host.read('bl', 9, 1)![0]; expect(firstByteEl1).toBe(0xBE); // Byte 10 = second byte of element 1 next field = 0xBA - const secondByteEl1 = await host.readValue(makeRef('bl', 1, 10)); + const secondByteEl1 = host.read('bl', 10, 1)![0]; expect(secondByteEl1).toBe(0xBA); }); - it('readRaw spanning an element boundary returns contiguous bytes', async () => { + it('readRaw spanning an element boundary returns contiguous bytes', () => { host.setVariable('bl', 9, makeBlock9(0x11223344, 0x55667788, 0x99), -1, 0x1000, 9); host.setVariable('bl', 9, makeBlock9(0xAABBCCDD, 0xEEFF0011, 0x22), -1, 0x2000, 9); // Read 4 bytes from offset 7 → last 2 bytes of el0 (len high) + first 2 of el1 (next low) - const raw = await host.readRaw(makeRef('bl', 4, 7), 4); + const raw = host.read('bl', 7, 4); expect(raw).toBeDefined(); // Byte 7 = len byte[3] of el0 = 0x55 (LE of 0x55667788 → [0x88, 0x77, 0x66, 0x55]) // Wait: LE of 0x55667788 = [0x88, 0x77, 0x66, 0x55] @@ -209,7 +189,7 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => host = new MemoryHost(); }); - it('container grows by virtualSize (129) per append, not targetSize (80)', async () => { + it('container grows by virtualSize (129) per append, not targetSize (80)', () => { // After 3 appends: 3 * 129 = 387 bytes total for (let i = 0; i < 3; i++) { host.setVariable('TCB', TARGET_SIZE, makeTCB80(0x1000 + i), -1, 0x20000 + i * 80, VIRTUAL_SIZE); @@ -217,11 +197,11 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => expect(host.getArrayElementCount('TCB')).toBe(3); // Verify we can read the start of element 2 at offset 2*129 = 258 - const marker2 = await host.readValue(makeRef('TCB', 4, 2 * VIRTUAL_SIZE)); + const marker2 = leToNumber(host.read('TCB', 2 * VIRTUAL_SIZE, 4)!); expect(marker2).toBe(0x1002); }); - it('stride-based indexing: TCB[i] at offset i*129 returns correct marker', async () => { + it('stride-based indexing: TCB[i] at offset i*129 returns correct marker', () => { const markers = [0xDEAD0001, 0xDEAD0002, 0xDEAD0003, 0xDEAD0004, 0xDEAD0005]; for (const m of markers) { host.setVariable('TCB', TARGET_SIZE, makeTCB80(m), -1, 0x20000, VIRTUAL_SIZE); @@ -229,12 +209,12 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => for (let i = 0; i < markers.length; i++) { const offset = i * VIRTUAL_SIZE; - const val = await host.readValue(makeRef('TCB', 4, offset)); + const val = leToNumber(host.read('TCB', offset, 4)!); expect(val).toBe(markers.at(i)); } }); - it('zero padding between targetSize and virtualSize is correct', async () => { + it('zero padding between targetSize and virtualSize is correct', () => { host.setVariable('TCB', TARGET_SIZE, makeTCB80(0x42), -1, 0x20000, VIRTUAL_SIZE); // Bytes 0..79 contain the 80-byte TCB data @@ -243,7 +223,7 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => const padEnd = VIRTUAL_SIZE; // 129 const padSize = padEnd - padStart; // 49 - const raw = await host.readRaw(makeRef('TCB', padSize, padStart), padSize); + const raw = host.read('TCB', padStart, padSize); expect(raw).toBeDefined(); expect(raw!.length).toBe(padSize); // All padding bytes must be zero @@ -252,53 +232,53 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => } }); - it('element 1 data starts at EXACTLY byte 129, not 128 or 130', async () => { + it('element 1 data starts at EXACTLY byte 129, not 128 or 130', () => { host.setVariable('TCB', TARGET_SIZE, makeTCB80(0xAAAA), -1, 0x20000, VIRTUAL_SIZE); host.setVariable('TCB', TARGET_SIZE, makeTCB80(0xBBBB), -1, 0x20050, VIRTUAL_SIZE); // Byte 128 (= virtualSize - 1) should be zero (padding of element 0) - const at128 = await host.readValue(makeRef('TCB', 1, 128)); + const at128 = host.read('TCB', 128, 1)![0]; expect(at128).toBe(0); // Byte 129 (= virtualSize) should be low byte of element 1's marker // 0xBBBB in LE = [0xBB, 0xBB, 0x00, 0x00] - const at129 = await host.readValue(makeRef('TCB', 1, 129)); + const at129 = host.read('TCB', 129, 1)![0]; expect(at129).toBe(0xBB); // Byte 130 should be second byte of element 1's marker - const at130 = await host.readValue(makeRef('TCB', 1, 130)); + const at130 = host.read('TCB', 130, 1)![0]; expect(at130).toBe(0xBB); // Read full u32 at byte 129 = element 1 marker - const marker1 = await host.readValue(makeRef('TCB', 4, 129)); + const marker1 = leToNumber(host.read('TCB', 129, 4)!); expect(marker1).toBe(0xBBBB); }); - it('element 2 data starts at byte 258 (2*129), not 256 or 260', async () => { + it('element 2 data starts at byte 258 (2*129), not 256 or 260', () => { host.setVariable('TCB', TARGET_SIZE, makeTCB80(0x1111), -1, 0x20000, VIRTUAL_SIZE); host.setVariable('TCB', TARGET_SIZE, makeTCB80(0x2222), -1, 0x20050, VIRTUAL_SIZE); host.setVariable('TCB', TARGET_SIZE, makeTCB80(0x3333), -1, 0x200A0, VIRTUAL_SIZE); // Byte 257 = virtualSize*2 - 1 = zero padding of element 1 - const at257 = await host.readValue(makeRef('TCB', 1, 257)); + const at257 = host.read('TCB', 257, 1)![0]; expect(at257).toBe(0); // Byte 258 = element 2 starts here - const at258 = await host.readValue(makeRef('TCB', 1, 258)); + const at258 = host.read('TCB', 258, 1)![0]; expect(at258).toBe(0x33); // Full u32 at 258 - const marker2 = await host.readValue(makeRef('TCB', 4, 258)); + const marker2 = leToNumber(host.read('TCB', 258, 4)!); expect(marker2).toBe(0x3333); // WRONG offsets for reference (should NOT match marker): - const at256 = await host.readValue(makeRef('TCB', 4, 256)); + const at256 = leToNumber(host.read('TCB', 256, 4)!); expect(at256).not.toBe(0x3333); - const at260 = await host.readValue(makeRef('TCB', 4, 260)); + const at260 = leToNumber(host.read('TCB', 260, 4)!); expect(at260).not.toBe(0x3333); }); - it('member access: TCB[i].field4 at stride*i + memberOffset reads correctly', async () => { + it('member access: TCB[i].field4 at stride*i + memberOffset reads correctly', () => { // Simulate reading TCB[i].field_at_offset_4 (bytes 4-7 of each 80-byte block) const markers = [0xF0F0F0F0, 0xA0A0A0A0, 0xC0C0C0C0]; const blocks: Uint8Array[] = []; @@ -318,13 +298,13 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => const MEMBER_OFFSET = 4; for (let i = 0; i < markers.length; i++) { const byteOff = i * VIRTUAL_SIZE + MEMBER_OFFSET; - const val = await host.readValue(makeRef('TCB', 4, byteOff)); + const val = leToNumber(host.read('TCB', byteOff, 4)!); const marker = markers.at(i); expect(val).toBe(marker !== undefined ? (~marker) >>> 0 : undefined); } }); - it('member access: TCB[i].byte8 at stride*i + 8 reads the single-byte marker', async () => { + it('member access: TCB[i].byte8 at stride*i + 8 reads the single-byte marker', () => { const markers = [0x10, 0x20, 0x30, 0x40, 0x50]; for (const m of markers) { const tcb = makeTCB80(m); @@ -334,7 +314,7 @@ describe('Append (-1): 80-byte data with virtualSize=129 (osRtxThread_t)', () => for (let i = 0; i < markers.length; i++) { const byteOff = i * VIRTUAL_SIZE + 8; - const val = await host.readValue(makeRef('TCB', 1, byteOff)); + const val = host.read('TCB', byteOff, 1)![0]; expect(val).toBe(markers.at(i)); } }); @@ -358,7 +338,7 @@ describe('Append (-1): odd and prime virtualSize values', () => { { targetSize: 13, virtualSize: 17 }, // twin primes { targetSize: 9, virtualSize: 16 }, // 9-byte data, DWORD-aligned stride { targetSize: 80, virtualSize: 129 }, // real RTX TCB - ])('targetSize=$targetSize, virtualSize=$virtualSize: 4 appends and stride readback', async ({ targetSize, virtualSize }) => { + ])('targetSize=$targetSize, virtualSize=$virtualSize: 4 appends and stride readback', ({ targetSize, virtualSize }) => { const elements: Uint8Array[] = []; for (let i = 0; i < 4; i++) { const data = new Uint8Array(targetSize); @@ -376,7 +356,7 @@ describe('Append (-1): odd and prime virtualSize values', () => { // Verify stride-based read of each element's first byte for (let i = 0; i < 4; i++) { const offset = i * virtualSize; - const val = await host.readValue(makeRef('arr', 1, offset)); + const val = host.read('arr', offset, 1)![0]; const elem = elements.at(i); expect(val).toBe(elem?.at(0)); } @@ -384,7 +364,7 @@ describe('Append (-1): odd and prime virtualSize values', () => { // Verify stride-based read of each element's LAST data byte for (let i = 0; i < 4; i++) { const offset = i * virtualSize + (targetSize - 1); - const val = await host.readValue(makeRef('arr', 1, offset)); + const val = host.read('arr', offset, 1)![0]; const elem = elements.at(i); expect(val).toBe(elem?.at(targetSize - 1)); } @@ -393,7 +373,7 @@ describe('Append (-1): odd and prime virtualSize values', () => { if (virtualSize > targetSize) { for (let i = 0; i < 4; i++) { const paddingOffset = i * virtualSize + targetSize; - const val = await host.readValue(makeRef('arr', 1, paddingOffset)); + const val = host.read('arr', paddingOffset, 1)![0]; expect(val).toBe(0); } } @@ -411,7 +391,7 @@ describe('Append (-1): boundary probes ±1 byte', () => { host = new MemoryHost(); }); - it('probes ±1 around each element boundary with virtualSize=9', async () => { + it('probes ±1 around each element boundary with virtualSize=9', () => { // 3 elements with distinct data patterns const data = [ new Uint8Array([0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19]), @@ -424,18 +404,18 @@ describe('Append (-1): boundary probes ±1 byte', () => { // Boundary between element 0 and 1 is at byte 9 // byte 8 = last byte of el0 = 0x19 - expect(await host.readValue(makeRef('blk', 1, 8))).toBe(0x19); + expect(host.read('blk', 8, 1)![0]).toBe(0x19); // byte 9 = first byte of el1 = 0x21 - expect(await host.readValue(makeRef('blk', 1, 9))).toBe(0x21); + expect(host.read('blk', 9, 1)![0]).toBe(0x21); // Boundary between element 1 and 2 is at byte 18 // byte 17 = last byte of el1 = 0x29 - expect(await host.readValue(makeRef('blk', 1, 17))).toBe(0x29); + expect(host.read('blk', 17, 1)![0]).toBe(0x29); // byte 18 = first byte of el2 = 0x31 - expect(await host.readValue(makeRef('blk', 1, 18))).toBe(0x31); + expect(host.read('blk', 18, 1)![0]).toBe(0x31); }); - it('probes ±1 around each element boundary with virtualSize=129 (49-byte padding)', async () => { + it('probes ±1 around each element boundary with virtualSize=129 (49-byte padding)', () => { const stride = 129; // Use identifiable first bytes const el0 = new Uint8Array(80); el0[0] = 0xAA; el0[79] = 0x0A; @@ -448,26 +428,26 @@ describe('Append (-1): boundary probes ±1 byte', () => { // --- Element 0 → 1 boundary at byte 129 --- // Byte 79 = last data byte of el0 = 0x0A - expect(await host.readValue(makeRef('TCB', 1, 79))).toBe(0x0A); + expect(host.read('TCB', 79, 1)![0]).toBe(0x0A); // Byte 80 = first padding byte of el0 = 0 - expect(await host.readValue(makeRef('TCB', 1, 80))).toBe(0); + expect(host.read('TCB', 80, 1)![0]).toBe(0); // Byte 128 = last padding byte of el0 = 0 - expect(await host.readValue(makeRef('TCB', 1, 128))).toBe(0); + expect(host.read('TCB', 128, 1)![0]).toBe(0); // Byte 129 = first data byte of el1 = 0xBB - expect(await host.readValue(makeRef('TCB', 1, 129))).toBe(0xBB); + expect(host.read('TCB', 129, 1)![0]).toBe(0xBB); // Byte 130 = second data byte of el1 = 0 (default fill) - expect(await host.readValue(makeRef('TCB', 1, 130))).toBe(0); + expect(host.read('TCB', 130, 1)![0]).toBe(0); // --- Element 1 → 2 boundary at byte 258 --- // Byte 208 = last data byte of el1 = el1[79] = 0x0B // (el1 starts at 129, data 0..79 → byte 129+79 = 208) - expect(await host.readValue(makeRef('TCB', 1, 129 + 79))).toBe(0x0B); + expect(host.read('TCB', 129 + 79, 1)![0]).toBe(0x0B); // Byte 209 = first padding of el1 = 0 - expect(await host.readValue(makeRef('TCB', 1, 209))).toBe(0); + expect(host.read('TCB', 209, 1)![0]).toBe(0); // Byte 257 = last padding of el1 = 0 - expect(await host.readValue(makeRef('TCB', 1, 257))).toBe(0); + expect(host.read('TCB', 257, 1)![0]).toBe(0); // Byte 258 = first data byte of el2 = 0xCC - expect(await host.readValue(makeRef('TCB', 1, 258))).toBe(0xCC); + expect(host.read('TCB', 258, 1)![0]).toBe(0xCC); }); }); @@ -584,7 +564,7 @@ describe('Append (-1): off-by-one virtualSize sensitivity', () => { host = new MemoryHost(); }); - it('virtualSize = targetSize + 1: element[1] at offset targetSize+1 is correct', async () => { + it('virtualSize = targetSize + 1: element[1] at offset targetSize+1 is correct', () => { const targetSize = 9; const virtualSize = 10; // one extra padding byte @@ -595,17 +575,17 @@ describe('Append (-1): off-by-one virtualSize sensitivity', () => { // Element 1: bytes [10..19], data [10..18], pad [19] // The padding byte at offset 9 must be 0 - expect(await host.readValue(makeRef('v', 1, 9))).toBe(0); + expect(host.read('v', 9, 1)![0]).toBe(0); // Element 1 starts at offset 10 - const next1 = await host.readValue(makeRef('v', 4, 10)); + const next1 = leToNumber(host.read('v', 10, 4)!); expect(next1).toBe(0x200); - const id1 = await host.readValue(makeRef('v', 1, 18)); + const id1 = host.read('v', 18, 1)![0]; expect(id1).toBe(0xBB); }); - it('virtualSize = targetSize - 1 is rejected (logged error, data still written)', async () => { + it('virtualSize = targetSize - 1 is rejected (logged error, data still written)', () => { const targetSize = 9; const virtualSize = 8; // LESS than target — should be rejected by validation @@ -617,7 +597,7 @@ describe('Append (-1): off-by-one virtualSize sensitivity', () => { expect(host.getArrayElementCount('v')).toBe(1); // defaults to 1 when unknown }); - it('consistent stride across 5 elements: no drift with virtualSize=129', async () => { + it('consistent stride across 5 elements: no drift with virtualSize=129', () => { const TARGET = 80; const VIRTUAL = 129; const COUNT = 5; @@ -634,14 +614,14 @@ describe('Append (-1): off-by-one virtualSize sensitivity', () => { // Verify each element's marker using stride indexing (the evaluator's method) for (let i = 0; i < COUNT; i++) { const byteOffset = i * VIRTUAL; - const val = await host.readValue(makeRef('TCB', 4, byteOffset)); + const val = leToNumber(host.read('TCB', byteOffset, 4)!); expect(val).toBe(markers.at(i)); } // Also check the LAST data byte (offset 79 within each element) for (let i = 0; i < COUNT; i++) { const byteOffset = i * VIRTUAL + 79; - const lastByte = await host.readValue(makeRef('TCB', 1, byteOffset)); + const lastByte = host.read('TCB', byteOffset, 1)![0]; // makeTCB80 fills [9..79] with (marker & 0xFF) ^ 0xAA const marker = markers.at(i); const expected = marker !== undefined ? (marker & 0xFF) ^ 0xAA : undefined; @@ -649,7 +629,7 @@ describe('Append (-1): off-by-one virtualSize sensitivity', () => { } }); - it('verifies cumulative byteLength after N appends does not drift', async () => { + it('verifies cumulative byteLength after N appends does not drift', () => { const TARGET = 9; const VIRTUAL = 9; const COUNT = 100; // push many elements to detect any cumulative drift @@ -662,11 +642,11 @@ describe('Append (-1): off-by-one virtualSize sensitivity', () => { // Spot-check element 99 const off99 = 99 * VIRTUAL; - const val = await host.readValue(makeRef('many', 4, off99)); + const val = leToNumber(host.read('many', off99, 4)!); expect(val).toBe(99); // id byte of element 50 - const id50 = await host.readValue(makeRef('many', 1, 50 * VIRTUAL + 8)); + const id50 = host.read('many', 50 * VIRTUAL + 8, 1)![0]; expect(id50).toBe(50); }); }); @@ -697,22 +677,22 @@ describe('Append (-1): unaligned target base addresses', () => { } }); - it('target base address does not affect in-host byte offsets', async () => { + it('target base address does not affect in-host byte offsets', () => { // Two elements with very different (unaligned) target addresses // but both should be stored contiguously in the container host.setVariable('bl', 9, makeBlock9(0xAAAA, 41, 0x11), -1, 0x20010D29, 9); host.setVariable('bl', 9, makeBlock9(0xBBBB, 42, 0x22), -1, 0x20010D52, 9); // Element 0 at offset 0 - expect(await host.readValue(makeRef('bl', 4, 0))).toBe(0xAAAA); - expect(await host.readValue(makeRef('bl', 1, 8))).toBe(0x11); + expect(leToNumber(host.read('bl', 0, 4)!)).toBe(0xAAAA); + expect(host.read('bl', 8, 1)![0]).toBe(0x11); // Element 1 at offset 9 (= 1 * stride) - expect(await host.readValue(makeRef('bl', 4, 9))).toBe(0xBBBB); - expect(await host.readValue(makeRef('bl', 1, 17))).toBe(0x22); + expect(leToNumber(host.read('bl', 9, 4)!)).toBe(0xBBBB); + expect(host.read('bl', 17, 1)![0]).toBe(0x22); }); - it('non-DWORD-aligned addresses with virtualSize=129 store and read correctly', async () => { + it('non-DWORD-aligned addresses with virtualSize=129 store and read correctly', () => { // Addresses that are 1, 2, 3 bytes off from DWORD alignment const addrs = [0x20010001, 0x20010052, 0x200100A3]; const markers = [0x111, 0x222, 0x333]; @@ -725,7 +705,7 @@ describe('Append (-1): unaligned target base addresses', () => { for (let i = 0; i < 3; i++) { expect(host.getElementTargetBase('TCB', i)).toBe(addrs.at(i)); - const val = await host.readValue(makeRef('TCB', 4, i * 129)); + const val = leToNumber(host.read('TCB', i * 129, 4)!); expect(val).toBe(markers.at(i)); } }); @@ -742,7 +722,7 @@ describe('Append (-1): multiple variables with different strides', () => { host = new MemoryHost(); }); - it('mem_list_com (9/9) and TCB (80/129) coexist without interference', async () => { + it('mem_list_com (9/9) and TCB (80/129) coexist without interference', () => { // Append 3 blocks to mem_list_com host.setVariable('mem_list_com', 9, makeBlock9(0x100, 41, 0xF1), -1, 0x1000, 9); host.setVariable('mem_list_com', 9, makeBlock9(0x200, 41, 0xF5), -1, 0x2000, 9); @@ -754,16 +734,16 @@ describe('Append (-1): multiple variables with different strides', () => { // Verify mem_list_com expect(host.getArrayElementCount('mem_list_com')).toBe(3); - expect(await host.readValue(makeRef('mem_list_com', 1, 8))).toBe(0xF1); - expect(await host.readValue(makeRef('mem_list_com', 1, 17))).toBe(0xF5); + expect(host.read('mem_list_com', 8, 1)![0]).toBe(0xF1); + expect(host.read('mem_list_com', 17, 1)![0]).toBe(0xF5); // Verify TCB — should not be affected by mem_list_com writes expect(host.getArrayElementCount('TCB')).toBe(2); - expect(await host.readValue(makeRef('TCB', 4, 0))).toBe(0xAABB); - expect(await host.readValue(makeRef('TCB', 4, 129))).toBe(0xCCDD); + expect(leToNumber(host.read('TCB', 0, 4)!)).toBe(0xAABB); + expect(leToNumber(host.read('TCB', 129, 4)!)).toBe(0xCCDD); }); - it('clearNonConst wipes both variables, re-append produces fresh layout', async () => { + it('clearNonConst wipes both variables, re-append produces fresh layout', () => { host.setVariable('bl', 9, makeBlock9(0x100, 41, 0xF1), -1, 0x1000, 9); host.setVariable('TCB', 80, makeTCB80(0xAAAA), -1, 0x20000, 129); @@ -777,8 +757,8 @@ describe('Append (-1): multiple variables with different strides', () => { host.setVariable('bl', 9, makeBlock9(0x999, 99, 0xEE), -1, 0x5000, 9); host.setVariable('TCB', 80, makeTCB80(0xDDDD), -1, 0x30000, 129); - expect(await host.readValue(makeRef('bl', 1, 8))).toBe(0xEE); - expect(await host.readValue(makeRef('TCB', 4, 0))).toBe(0xDDDD); + expect(host.read('bl', 8, 1)![0]).toBe(0xEE); + expect(leToNumber(host.read('TCB', 0, 4)!)).toBe(0xDDDD); }); }); @@ -793,7 +773,7 @@ describe('Append (-1): simulated RTX readList → TCB[i].sp (end-to-end stride)' host = new MemoryHost(); }); - it('5 TCBs appended with 80/129, then TCB[i].sp accessed via stride*i + offset', async () => { + it('5 TCBs appended with 80/129, then TCB[i].sp accessed via stride*i + offset', () => { // The SCVD osRtxThread_t has sp at member offset 56 (byte 56 within the 80-byte struct) const SP_MEMBER_OFFSET = 56; const TARGET_SIZE = 80; @@ -820,7 +800,7 @@ describe('Append (-1): simulated RTX readList → TCB[i].sp (end-to-end stride)' // = i * 129 + 56 for (let i = 0; i < 5; i++) { const byteOff = i * VIRTUAL_SIZE + SP_MEMBER_OFFSET; - const sp = await host.readValue(makeRef('TCB', 4, byteOff)); + const sp = leToNumber(host.read('TCB', byteOff, 4)!); expect(sp).toBe(spValues.at(i)); } @@ -828,7 +808,7 @@ describe('Append (-1): simulated RTX readList → TCB[i].sp (end-to-end stride)' // (which would happen if stride were off by even 1 byte) for (let i = 0; i < 5; i++) { const byteOff = i * VIRTUAL_SIZE + SP_MEMBER_OFFSET; - const sp = await host.readValue(makeRef('TCB', 4, byteOff)) as number; + const sp = leToNumber(host.read('TCB', byteOff, 4)!); // Must not equal any OTHER thread's sp for (let j = 0; j < 5; j++) { if (j !== i) { @@ -851,7 +831,7 @@ describe('Append (-1): simulated RTX readList → TCB[i].sp (end-to-end stride)' } }); - it('2x same TCB address would require setVariable to be called twice for same addr', async () => { + it('2x same TCB address would require setVariable to be called twice for same addr', () => { // This simulates the "2x app_main" bug scenario: // If the readList produces the same target address twice, both // appends store the same data but at different stride indices. @@ -868,8 +848,8 @@ describe('Append (-1): simulated RTX readList → TCB[i].sp (end-to-end stride)' expect(host.getElementTargetBase('TCB', 0)).toBe(sameAddr); expect(host.getElementTargetBase('TCB', 1)).toBe(sameAddr); - const marker0 = await host.readValue(makeRef('TCB', 4, 0)); - const marker1 = await host.readValue(makeRef('TCB', 4, 129)); + const marker0 = leToNumber(host.read('TCB', 0, 4)!); + const marker1 = leToNumber(host.read('TCB', 129, 4)!); expect(marker0).toBe(marker1); // same data → same marker → "2x app_main" }); }); diff --git a/src/views/component-viewer/test/integration/memory-host/memory-host-deep.test.ts b/src/views/component-viewer/test/integration/memory-host/memory-host-deep.test.ts index 0a077ef8..76d92c39 100644 --- a/src/views/component-viewer/test/integration/memory-host/memory-host-deep.test.ts +++ b/src/views/component-viewer/test/integration/memory-host/memory-host-deep.test.ts @@ -25,34 +25,14 @@ * Models the real RTX SCVD scenario where: * · mem_list_com appends 9-byte mem_block_t items via setVariable(..., -1) * · Each item has targetBase = the address where it was read from - * · Members are accessed via readRaw at offsets within each element + * · Members are accessed via read() at offsets within each element */ import { MemoryHost } from '../../../data-host/memory-host'; -import { RefContainer } from '../../../parser-evaluator/model-host'; -import { ScvdNode } from '../../../model/scvd-node'; +import { leToNumber } from '../../../data-host/byte-encoding'; // ---------- helpers ---------- -class NamedStubBase extends ScvdNode { - constructor(name: string) { - super(undefined); - this.name = name; - } -} - -const makeContainer = (name: string, widthBytes: number, offsetBytes = 0): RefContainer => { - const ref = new NamedStubBase(name); - return { - base: ref, - anchor: ref, - current: ref, - offsetBytes, - widthBytes, - valueType: undefined, - }; -}; - /** Build a 9-byte mem_block_t: [next:4][len:4][id:1] in little-endian. */ function makeBlockBytes(nextAddr: number, len: number, id: number): Uint8Array { const buf = new Uint8Array(9); @@ -66,7 +46,7 @@ function makeBlockBytes(nextAddr: number, len: number, id: number): Uint8Array { // ---------- tests ---------- describe('MemoryHost – 9-byte append items', () => { - it('appends multiple 9-byte items and reads them back correctly', async () => { + it('appends multiple 9-byte items and reads them back correctly', () => { const host = new MemoryHost(); const blocks = [ makeBlockBytes(0x20010d50, 41, 0xF5), @@ -85,14 +65,9 @@ describe('MemoryHost – 9-byte append items', () => { for (let i = 0; i < blocks.length; i++) { const offset = i * 9; // next (offset 0, 4 bytes) - const nextRef = makeContainer('mem_list_com', 4, offset); - const nextBytes = await host.readRaw(nextRef, 4); + const nextBytes = host.read('mem_list_com', offset, 4); expect(nextBytes).toBeDefined(); - const b0 = nextBytes!.at(0) ?? 0; - const b1 = nextBytes!.at(1) ?? 0; - const b2 = nextBytes!.at(2) ?? 0; - const b3 = nextBytes!.at(3) ?? 0; - const nextVal = (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0; + const nextVal = leToNumber(nextBytes!); const blockData = blocks.at(i); if (blockData) { const expectedNext = new DataView(blockData.buffer).getUint32(0, true); @@ -100,21 +75,14 @@ describe('MemoryHost – 9-byte append items', () => { } // len (offset 4, 4 bytes) - const lenRef = makeContainer('mem_list_com', 4, offset + 4); - const lenBytes = await host.readRaw(lenRef, 4); + const lenBytes = host.read('mem_list_com', offset + 4, 4); expect(lenBytes).toBeDefined(); - const lb0 = lenBytes!.at(0) ?? 0; - const lb1 = lenBytes!.at(1) ?? 0; - const lb2 = lenBytes!.at(2) ?? 0; - const lb3 = lenBytes!.at(3) ?? 0; - const lenVal = (lb0 | (lb1 << 8) | (lb2 << 16) | (lb3 << 24)) >>> 0; - expect(lenVal).toBe(41); + expect(leToNumber(lenBytes!)).toBe(41); // id (offset 8, 1 byte) - const idRef = makeContainer('mem_list_com', 1, offset + 8); - const idBytes = await host.readRaw(idRef, 1); + const idBytes = host.read('mem_list_com', offset + 8, 1); expect(idBytes).toBeDefined(); - expect(idBytes!.at(0)).toBe(blockData?.at(8)); + expect(idBytes![0]).toBe(blockData?.at(8)); } }); @@ -131,7 +99,7 @@ describe('MemoryHost – 9-byte append items', () => { } }); - it('clears non-const 9-byte items but preserves const ones', async () => { + it('clears non-const 9-byte items but preserves const ones', () => { const host = new MemoryHost(); // Non-const items (like mem_list_com — re-read each cycle) @@ -147,16 +115,14 @@ describe('MemoryHost – 9-byte append items', () => { host.clearNonConst(); // mem_list_com should be gone - const ref = makeContainer('mem_list_com', 9, 0); - expect(await host.readRaw(ref, 9)).toBeUndefined(); + expect(host.read('mem_list_com', 0, 9)).toBeUndefined(); expect(host.getArrayElementCount('mem_list_com')).toBe(1); // defaults to 1 when unknown // cfg_const should survive - const constRef = makeContainer('cfg_const', 4, 0); - expect(await host.readRaw(constRef, 4)).toEqual(new Uint8Array([1, 2, 3, 4])); + expect(host.read('cfg_const', 0, 4)).toEqual(new Uint8Array([1, 2, 3, 4])); }); - it('re-populates from scratch after clearNonConst', async () => { + it('re-populates from scratch after clearNonConst', () => { const host = new MemoryHost(); // Cycle 1: 3 items @@ -180,33 +146,33 @@ describe('MemoryHost – 9-byte append items', () => { expect(host.getElementTargetBase('items', 2)).toBe(0x3000); // Read id byte of 3rd element (index 2, offset = 2*9 + 8 = 26) - const idRef = makeContainer('items', 1, 26); - const id = await host.readRaw(idRef, 1); + const id = host.read('items', 26, 1); expect(id![0]).toBe(0xF1); }); }); -describe('MemoryHost – readValue for mem_block_t members', () => { - it('reads uint32 len field and uint8 id field from appended 9-byte items', async () => { +describe('MemoryHost – read for mem_block_t members', () => { + it('reads uint32 len field and uint8 id field from appended 9-byte items', () => { const host = new MemoryHost(); const block = makeBlockBytes(0x20010d50, 0x29 | 1, 0xF1); // len=41, allocated host.setVariable('bl', 9, block, -1, 0x20010d28, 9); // Read len as uint32 (offset 4) - const lenRef = makeContainer('bl', 4, 4); - const lenVal = await host.readValue(lenRef); + const lenBytes = host.read('bl', 4, 4); + expect(lenBytes).toBeDefined(); + const lenVal = leToNumber(lenBytes!); expect(lenVal).toBe(0x29 | 1); // Read id as uint8 (offset 8) - const idRef = makeContainer('bl', 1, 8); - const idVal = await host.readValue(idRef); - expect(idVal).toBe(0xF1); + const idBytes = host.read('bl', 8, 1); + expect(idBytes).toBeDefined(); + expect(idBytes![0]).toBe(0xF1); // Check len & 1 (allocated flag) - expect(((lenVal as number) & 1)).toBe(1); + expect(lenVal & 1).toBe(1); }); - it('reads the last element (sentinel) len correctly', async () => { + it('reads the last element (sentinel) len correctly', () => { const host = new MemoryHost(); // Simulate appending: 2 allocated blocks + sentinel host.setVariable('bl', 9, makeBlockBytes(0x2000, 41 | 1, 0xF1), -1, 0x1000, 9); @@ -218,22 +184,21 @@ describe('MemoryHost – readValue for mem_block_t members', () => { // The SCVD calc: mem_head_com.max_used = mem_list_com[_count-1].len // Last element (index 2) len at offset = 2*9 + 4 = 22 - const lastLenRef = makeContainer('bl', 4, (count - 1) * 9 + 4); - const lastLen = await host.readValue(lastLenRef); - expect(lastLen).toBe(6552); + const lastLenBytes = host.read('bl', (count - 1) * 9 + 4, 4); + expect(lastLenBytes).toBeDefined(); + expect(leToNumber(lastLenBytes!)).toBe(6552); }); }); describe('MemoryHost – virtual size with 9-byte items', () => { - it('pads 9-byte data to virtualSize when virtualSize > data size', async () => { + it('pads 9-byte data to virtualSize when virtualSize > data size', () => { const host = new MemoryHost(); // virtualSize = 12 (pad 3 extra bytes) const block = makeBlockBytes(0x100, 41, 0xF5); host.setVariable('padded', 9, block, 0, undefined, 12); // Read 12 bytes: first 9 are data, last 3 are zero - const ref = makeContainer('padded', 12, 0); - const raw = await host.readRaw(ref, 12); + const raw = host.read('padded', 0, 12); expect(raw).toBeDefined(); expect(raw!.subarray(0, 9)).toEqual(block); expect(raw!.subarray(9, 12)).toEqual(new Uint8Array([0, 0, 0])); @@ -253,7 +218,7 @@ describe('MemoryHost – virtual size with 9-byte items', () => { }); describe('MemoryHost – edge cases with the readlist _count-1 pattern', () => { - it('correctly handles _count and accessing last element in SCVD pattern', async () => { + it('correctly handles _count and accessing last element in SCVD pattern', () => { const host = new MemoryHost(); // Simulate the exact RTX pattern: append blocks, then access [_count-1] @@ -279,11 +244,11 @@ describe('MemoryHost – edge cases with the readlist _count-1 pattern', () => { const blockEntry = blocks.at(i); expect(addr).toBe(blockEntry?.addr); - // Read id byte to check condition (len & 1) && (id == 0xF1) - const lenRef = makeContainer('mem_list_com', 4, i * 9 + 4); - const len = await host.readValue(lenRef) as number; - const idRef = makeContainer('mem_list_com', 1, i * 9 + 8); - const id = await host.readValue(idRef) as number; + // Read len and id bytes to check condition (len & 1) && (id == 0xF1) + const lenBytes = host.read('mem_list_com', i * 9 + 4, 4); + const len = lenBytes ? leToNumber(lenBytes) : 0; + const idBytes = host.read('mem_list_com', i * 9 + 8, 1); + const id = idBytes ? idBytes[0] : 0; expect(len & 1).toBe(1); // all allocated expect(id).toBe(blockEntry?.data.at(8)); @@ -291,8 +256,8 @@ describe('MemoryHost – edge cases with the readlist _count-1 pattern', () => { // max_used = last element's len const lastIdx = count - 1; - const lastLenRef = makeContainer('mem_list_com', 4, lastIdx * 9 + 4); - const maxUsed = await host.readValue(lastLenRef); - expect(maxUsed).toBe(6552); + const lastLenBytes = host.read('mem_list_com', lastIdx * 9 + 4, 4); + expect(lastLenBytes).toBeDefined(); + expect(leToNumber(lastLenBytes!)).toBe(6552); }); }); diff --git a/src/views/component-viewer/test/integration/memory-host/memory-host.test.ts b/src/views/component-viewer/test/integration/memory-host/memory-host.test.ts index 65da79ee..416d9353 100644 --- a/src/views/component-viewer/test/integration/memory-host/memory-host.test.ts +++ b/src/views/component-viewer/test/integration/memory-host/memory-host.test.ts @@ -16,53 +16,32 @@ // generated with AI /** - * Integration test for MemoryHost. + * Integration test for MemoryHost (pure byte store). */ import { MemoryHost } from '../../../data-host/memory-host'; -import { RefContainer } from '../../../parser-evaluator/model-host'; -import { ScvdNode } from '../../../model/scvd-node'; - -class NamedStubBase extends ScvdNode { - constructor(name: string) { - super(undefined); - this.name = name; - } -} - -const makeContainer = (name: string, widthBytes: number, offsetBytes = 0): RefContainer => { - const ref = new NamedStubBase(name); - return { - base: ref, - anchor: ref, - current: ref, - offsetBytes, - widthBytes, - valueType: undefined, - }; -}; describe('MemoryHost', () => { - it('stores and retrieves numeric values with explicit offsets', async () => { + it('stores and retrieves numeric values with explicit offsets', () => { const host = new MemoryHost(); host.setVariable('foo', 4, 0x12345678, 0); - expect(await host.readRaw(makeContainer('foo', 4, 0), 4)).toEqual(new Uint8Array([0x78, 0x56, 0x34, 0x12])); + expect(host.read('foo', 0, 4)).toEqual(new Uint8Array([0x78, 0x56, 0x34, 0x12])); host.setVariable('foo', 2, 0xabcd, 4); - expect(await host.readRaw(makeContainer('foo', 2, 4), 2)).toEqual(new Uint8Array([0xcd, 0xab])); + expect(host.read('foo', 4, 2)).toEqual(new Uint8Array([0xcd, 0xab])); }); - it('appends when offset is -1 and tracks element count', async () => { + it('appends when offset is -1 and tracks element count', () => { const host = new MemoryHost(); host.setVariable('arr', 4, 1, -1); host.setVariable('arr', 4, 2, -1); host.setVariable('arr', 4, 3, -1); expect(host.getArrayElementCount('arr')).toBe(3); - expect(await host.readRaw(makeContainer('arr', 4, 0), 4)).toEqual(new Uint8Array([1, 0, 0, 0])); - expect(await host.readRaw(makeContainer('arr', 4, 4), 4)).toEqual(new Uint8Array([2, 0, 0, 0])); - expect(await host.readRaw(makeContainer('arr', 4, 8), 4)).toEqual(new Uint8Array([3, 0, 0, 0])); + expect(host.read('arr', 0, 4)).toEqual(new Uint8Array([1, 0, 0, 0])); + expect(host.read('arr', 4, 4)).toEqual(new Uint8Array([2, 0, 0, 0])); + expect(host.read('arr', 8, 4)).toEqual(new Uint8Array([3, 0, 0, 0])); }); it('tracks target bases per element', () => { @@ -74,114 +53,94 @@ describe('MemoryHost', () => { expect(host.getElementTargetBase('sym', 1)).toBe(0x2000); }); - it('supports readValue/writeValue round-trips for numbers', async () => { - const host = new MemoryHost(); - const container = makeContainer('num', 4); - - await host.writeValue(container, 0xdeadbeef); - const out = await host.readValue(container); - expect(out).toBe(0xdeadbeef >>> 0); - }); - - it('supports readValue/writeValue for byte arrays', async () => { + it('stores and reads byte arrays', () => { const host = new MemoryHost(); const bytes = new Uint8Array([1, 2, 3, 4, 5, 6]); - const container = makeContainer('blob', bytes.length); - await host.writeValue(container, bytes); - const out = await host.readValue(container); + host.setVariable('blob', bytes.length, bytes, 0); + const out = host.read('blob', 0, bytes.length); expect(out).toEqual(bytes); }); - it('writes and reads raw bytes at offsets', async () => { + it('writes and reads raw bytes at offsets', () => { const host = new MemoryHost(); - const container = makeContainer('raw', 4, 2); - await host.writeValue(container, new Uint8Array([9, 8, 7, 6])); - const out = await host.readRaw(container, 4); + host.write('raw', 2, new Uint8Array([9, 8, 7, 6])); + const out = host.read('raw', 2, 4); expect(out).toEqual(new Uint8Array([9, 8, 7, 6])); }); - it('round-trips via setVariable for a simple read', async () => { + it('round-trips via setVariable for a simple store', () => { const host = new MemoryHost(); host.setVariable('simple', 2, 0x1234, 0); - expect(await host.readValue(makeContainer('simple', 2, 0))).toBe(0x1234); + expect(host.read('simple', 0, 2)).toEqual(new Uint8Array([0x34, 0x12])); }); - it('preserves untouched bytes on partial overwrites', async () => { + it('preserves untouched bytes on partial overwrites', () => { const host = new MemoryHost(); - const base = makeContainer('overlap', 4, 0); - const tail = makeContainer('overlap', 2, 2); - await host.writeValue(base, new Uint8Array([1, 2, 3, 4])); - await host.writeValue(tail, new Uint8Array([9, 8])); + host.write('overlap', 0, new Uint8Array([1, 2, 3, 4])); + host.write('overlap', 2, new Uint8Array([9, 8])); - const out = await host.readRaw(base, 4); + const out = host.read('overlap', 0, 4); expect(out).toEqual(new Uint8Array([1, 2, 9, 8])); }); - it('partial setVariable writes only affect the specified range', async () => { + it('partial setVariable writes only affect the specified range', () => { const host = new MemoryHost(); host.setVariable('window', 4, new Uint8Array([1, 2, 3, 4]), 0); host.setVariable('window', 2, new Uint8Array([9, 8]), 2); - expect(await host.readRaw(makeContainer('window', 2, 0), 2)).toEqual(new Uint8Array([1, 2])); - expect(await host.readRaw(makeContainer('window', 2, 2), 2)).toEqual(new Uint8Array([9, 8])); + expect(host.read('window', 0, 2)).toEqual(new Uint8Array([1, 2])); + expect(host.read('window', 2, 2)).toEqual(new Uint8Array([9, 8])); }); - it('zero-fills virtual size and supports writes into virtual space', async () => { + it('zero-fills virtual size and supports writes into virtual space', () => { const host = new MemoryHost(); host.setVariable('struct', 2, new Uint8Array([0xAA, 0xBB]), 0, undefined, 6); - const base = makeContainer('struct', 2, 0); - const mid = makeContainer('struct', 2, 2); - const tail = makeContainer('struct', 2, 4); - - expect(await host.readRaw(base, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); - expect(await host.readRaw(mid, 2)).toEqual(new Uint8Array([0x00, 0x00])); - expect(await host.readRaw(tail, 2)).toEqual(new Uint8Array([0x00, 0x00])); + expect(host.read('struct', 0, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); + expect(host.read('struct', 2, 2)).toEqual(new Uint8Array([0x00, 0x00])); + expect(host.read('struct', 4, 2)).toEqual(new Uint8Array([0x00, 0x00])); - await host.writeValue(mid, new Uint8Array([0x11, 0x22])); + host.write('struct', 2, new Uint8Array([0x11, 0x22])); - expect(await host.readRaw(base, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); - expect(await host.readRaw(mid, 2)).toEqual(new Uint8Array([0x11, 0x22])); - expect(await host.readRaw(tail, 2)).toEqual(new Uint8Array([0x00, 0x00])); + expect(host.read('struct', 0, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); + expect(host.read('struct', 2, 2)).toEqual(new Uint8Array([0x11, 0x22])); + expect(host.read('struct', 4, 2)).toEqual(new Uint8Array([0x00, 0x00])); - await host.writeValue(tail, new Uint8Array([0x33, 0x44])); + host.write('struct', 4, new Uint8Array([0x33, 0x44])); - expect(await host.readRaw(base, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); - expect(await host.readRaw(mid, 2)).toEqual(new Uint8Array([0x11, 0x22])); - expect(await host.readRaw(tail, 2)).toEqual(new Uint8Array([0x33, 0x44])); + expect(host.read('struct', 0, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); + expect(host.read('struct', 2, 2)).toEqual(new Uint8Array([0x11, 0x22])); + expect(host.read('struct', 4, 2)).toEqual(new Uint8Array([0x33, 0x44])); }); - it('expands the backing buffer when writing beyond current size', async () => { + it('expands the backing buffer when writing beyond current size', () => { const host = new MemoryHost(); - const head = makeContainer('grow', 2, 0); - const tail = makeContainer('grow', 2, 6); - await host.writeValue(head, new Uint8Array([1, 2])); - await host.writeValue(tail, new Uint8Array([9, 8])); + host.write('grow', 0, new Uint8Array([1, 2])); + host.write('grow', 6, new Uint8Array([9, 8])); - expect(await host.readRaw(head, 2)).toEqual(new Uint8Array([1, 2])); - expect(await host.readRaw(tail, 2)).toEqual(new Uint8Array([9, 8])); + expect(host.read('grow', 0, 2)).toEqual(new Uint8Array([1, 2])); + expect(host.read('grow', 6, 2)).toEqual(new Uint8Array([9, 8])); }); - it('appends when offset is -1 and later writes can expand via the interface', async () => { + it('appends when offset is -1 and later writes can expand via the interface', () => { const host = new MemoryHost(); host.setVariable('arr', 2, new Uint8Array([1, 2]), -1); host.setVariable('arr', 2, new Uint8Array([3, 4]), -1); - const appended = makeContainer('arr', 2, 4); - await host.writeValue(appended, new Uint8Array([5, 6])); + host.write('arr', 4, new Uint8Array([5, 6])); expect(host.getArrayElementCount('arr')).toBe(2); - expect(await host.readRaw(makeContainer('arr', 2, 0), 2)).toEqual(new Uint8Array([1, 2])); - expect(await host.readRaw(makeContainer('arr', 2, 2), 2)).toEqual(new Uint8Array([3, 4])); - expect(await host.readRaw(makeContainer('arr', 2, 4), 2)).toEqual(new Uint8Array([5, 6])); + expect(host.read('arr', 0, 2)).toEqual(new Uint8Array([1, 2])); + expect(host.read('arr', 2, 2)).toEqual(new Uint8Array([3, 4])); + expect(host.read('arr', 4, 2)).toEqual(new Uint8Array([5, 6])); }); }); diff --git a/src/views/component-viewer/test/integration/parser-evaluator/eval-interface/scvd-eval-interface.formatPrintf.test.ts b/src/views/component-viewer/test/integration/parser-evaluator/eval-interface/scvd-eval-interface.formatPrintf.test.ts index 7d0358bb..a7fa344f 100644 --- a/src/views/component-viewer/test/integration/parser-evaluator/eval-interface/scvd-eval-interface.formatPrintf.test.ts +++ b/src/views/component-viewer/test/integration/parser-evaluator/eval-interface/scvd-eval-interface.formatPrintf.test.ts @@ -137,7 +137,7 @@ function makeContainer(typeName?: string, current?: ScvdNode): RefContainer { } function makeEvalInterface(symbolMap: Map, memoryMap: Map, contextMap?: Map) { - const memHost = {} as unknown as MemoryHost; + const memHost = { read: jest.fn().mockReturnValue(undefined) } as unknown as MemoryHost; const regHost = {} as unknown as RegisterHost; const debugTarget = new FakeDebugTarget(symbolMap, memoryMap, contextMap) as unknown as ScvdDebugTarget; const formatter = new ScvdFormatSpecifier(); @@ -266,7 +266,7 @@ describe('ScvdEvalInterface.formatPrintf (CMSIS-View value_output)', () => { }); it('formats %t when cached bytes are unavailable', async () => { - const memHost = { readRaw: jest.fn().mockResolvedValue(undefined) } as unknown as MemoryHost; + const memHost = { read: jest.fn().mockReturnValue(undefined) } as unknown as MemoryHost; const debugTarget = new FakeDebugTarget(new Map(), new Map()) as unknown as ScvdDebugTarget; const scvdWithMock = new ScvdEvalInterface( memHost, diff --git a/src/views/component-viewer/test/unit/memory-host/byte-encoding.test.ts b/src/views/component-viewer/test/unit/memory-host/byte-encoding.test.ts new file mode 100644 index 00000000..bf984d2b --- /dev/null +++ b/src/views/component-viewer/test/unit/memory-host/byte-encoding.test.ts @@ -0,0 +1,223 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// generated with AI + +/** + * Unit tests for byte-encoding helpers (encode/decode LE bytes ↔ typed values). + */ + +import { + leIntToBytes, + leBigIntToBytes, + encodeToLeBytes, + leToNumber, + leToSignedNumber, + leToFloat16, + decodeBytesToValue, +} from '../../../data-host/byte-encoding'; + +describe('byte-encoding', () => { + describe('leIntToBytes', () => { + it('encodes a 32-bit number as LE bytes', () => { + expect(leIntToBytes(0x12345678, 4)).toEqual(new Uint8Array([0x78, 0x56, 0x34, 0x12])); + }); + + it('encodes a 16-bit number', () => { + expect(leIntToBytes(0xABCD, 2)).toEqual(new Uint8Array([0xCD, 0xAB])); + }); + + it('encodes a single byte', () => { + expect(leIntToBytes(0xFF, 1)).toEqual(new Uint8Array([0xFF])); + }); + + it('delegates to bigint path for size > 4', () => { + // -1 as a 64-bit LE integer should be all 0xFF bytes + const out = leIntToBytes(-1, 8); + expect(out).toEqual(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])); + }); + + it('preserves positive values for size > 4', () => { + const out = leIntToBytes(0x12, 6); + expect(out).toEqual(new Uint8Array([0x12, 0x00, 0x00, 0x00, 0x00, 0x00])); + }); + }); + + describe('leBigIntToBytes', () => { + it('encodes a 64-bit bigint as LE bytes', () => { + const out = leBigIntToBytes(0x0102030405060708n, 8); + expect(out).toEqual(new Uint8Array([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01])); + }); + }); + + describe('encodeToLeBytes', () => { + it('encodes number', () => { + expect(encodeToLeBytes(0x1234, 2)).toEqual(new Uint8Array([0x34, 0x12])); + }); + + it('encodes bigint', () => { + expect(encodeToLeBytes(0x0102n, 2)).toEqual(new Uint8Array([0x02, 0x01])); + }); + + it('passes through Uint8Array of correct size', () => { + const data = new Uint8Array([1, 2, 3]); + expect(encodeToLeBytes(data, 3)).toBe(data); // same reference + }); + + it('truncates Uint8Array if too large', () => { + const data = new Uint8Array([1, 2, 3, 4]); + const out = encodeToLeBytes(data, 2); + expect(out).toEqual(new Uint8Array([1, 2])); + expect(out).not.toBe(data); + }); + + it('zero-pads Uint8Array if too small', () => { + const data = new Uint8Array([1, 2]); + const out = encodeToLeBytes(data, 4); + expect(out).toEqual(new Uint8Array([1, 2, 0, 0])); + }); + + it('truncates float to int', () => { + expect(encodeToLeBytes(3.7, 1)).toEqual(new Uint8Array([3])); + }); + }); + + describe('leToNumber', () => { + it('decodes LE bytes as unsigned 32-bit', () => { + expect(leToNumber(new Uint8Array([0x78, 0x56, 0x34, 0x12]))).toBe(0x12345678); + }); + + it('wraps to unsigned', () => { + expect(leToNumber(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF]))).toBe(0xFFFFFFFF); + }); + + it('clamps to low 4 bytes when input exceeds 4 bytes', () => { + // 5-byte input: only the first 4 bytes should be decoded + expect(leToNumber(new Uint8Array([0x78, 0x56, 0x34, 0x12, 0xFF]))).toBe(0x12345678); + }); + }); + + describe('leToSignedNumber', () => { + it('sign-extends 8-bit', () => { + expect(leToSignedNumber(new Uint8Array([0x80]))).toBe(-128); + expect(leToSignedNumber(new Uint8Array([0x7F]))).toBe(127); + }); + + it('sign-extends 16-bit', () => { + expect(leToSignedNumber(new Uint8Array([0x00, 0x80]))).toBe(-32768); + }); + + it('sign-extends 32-bit (uses |0)', () => { + expect(leToSignedNumber(new Uint8Array([0x00, 0x00, 0x00, 0x80]))).toBe(-2147483648); + }); + }); + + describe('leToFloat16', () => { + it('decodes +1.0', () => { + expect(leToFloat16(new Uint8Array([0x00, 0x3c]))).toBeCloseTo(1.0); + }); + + it('decodes +0', () => { + expect(leToFloat16(new Uint8Array([0x00, 0x00]))).toBe(0); + }); + + it('decodes -0', () => { + expect(Object.is(leToFloat16(new Uint8Array([0x00, 0x80])), -0)).toBe(true); + }); + + it('decodes subnormal', () => { + expect(leToFloat16(new Uint8Array([0x01, 0x00]))).toBeGreaterThan(0); + }); + + it('decodes +Infinity', () => { + expect(leToFloat16(new Uint8Array([0x00, 0x7c]))).toBe(Infinity); + }); + + it('decodes NaN', () => { + expect(Number.isNaN(leToFloat16(new Uint8Array([0x01, 0x7c])))).toBe(true); + }); + + it('returns NaN for too-short input', () => { + expect(Number.isNaN(leToFloat16(new Uint8Array([0x00])))).toBe(true); + }); + }); + + describe('decodeBytesToValue', () => { + it('decodes uint ≤4 bytes', () => { + expect(decodeBytesToValue(new Uint8Array([0x78, 0x56, 0x34, 0x12]), 4, { kind: 'uint' })) + .toBe(0x12345678); + }); + + it('decodes int ≤4 bytes (sign-extended)', () => { + expect(decodeBytesToValue(new Uint8Array([0x80]), 1, { kind: 'int' })).toBe(-128); + expect(decodeBytesToValue(new Uint8Array([0x7F]), 1, { kind: 'int' })).toBe(127); + }); + + it('defaults to unsigned for unknown kind', () => { + expect(decodeBytesToValue(new Uint8Array([0xFF]), 1, undefined)).toBe(255); + }); + + it('decodes 8-byte value as bigint', () => { + const bytes = new Uint8Array([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]); + expect(decodeBytesToValue(bytes, 8, { kind: 'uint' })).toBe(0x0102030405060708n); + }); + + it('decodes >8 bytes as raw copy', () => { + const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + const out = decodeBytesToValue(bytes, 10, undefined); + expect(out).toEqual(bytes); + expect(out).not.toBe(bytes); // must be a copy + }); + + it('decodes float16', () => { + const val = decodeBytesToValue(new Uint8Array([0x00, 0x3c]), 2, { kind: 'float' }); + expect(val).toBeCloseTo(1.0); + }); + + it('decodes float32', () => { + const buf = new ArrayBuffer(4); + new DataView(buf).setFloat32(0, 1.25, true); + const val = decodeBytesToValue(new Uint8Array(buf), 4, { kind: 'float' }); + expect(val).toBeCloseTo(1.25); + }); + + it('decodes float64', () => { + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, 2.5, true); + const val = decodeBytesToValue(new Uint8Array(buf), 8, { kind: 'float' }); + expect(val).toBeCloseTo(2.5); + }); + + it('falls back to raw bytes for non-standard float widths', () => { + const bytes = new Uint8Array([1, 2, 3, 4, 5, 6]); + expect(decodeBytesToValue(bytes, 6, { kind: 'float' })).toEqual(bytes); + }); + + it('returns raw bytes when widthBytes exceeds natural type size', () => { + // IPv4 address: uint8_t with size="4" should return raw bytes + const bytes = new Uint8Array([192, 168, 1, 1]); + const out = decodeBytesToValue(bytes, 4, { kind: 'uint', bits: 8 }); + expect(out).toEqual(bytes); + }); + + it('does NOT return raw bytes for floats even when widthBytes > typeSize', () => { + // floats should still decode via the float path + const buf = new ArrayBuffer(4); + new DataView(buf).setFloat32(0, 1.5, true); + const out = decodeBytesToValue(new Uint8Array(buf), 4, { kind: 'float', bits: 16 }); + expect(out).toBeCloseTo(1.5); + }); + }); +}); diff --git a/src/views/component-viewer/test/unit/memory-host/memory-host.test.ts b/src/views/component-viewer/test/unit/memory-host/memory-host.test.ts index 6665c213..ee66aa52 100644 --- a/src/views/component-viewer/test/unit/memory-host/memory-host.test.ts +++ b/src/views/component-viewer/test/unit/memory-host/memory-host.test.ts @@ -16,50 +16,13 @@ // generated with AI /** - * Unit test for MemoryHost. + * Unit test for MemoryHost (pure byte store – no type interpretation). */ import { componentViewerLogger } from '../../../../../logger'; -import { MemoryContainer, MemoryHost, __test__ as memoryHostTest } from '../../../data-host/memory-host'; -import { RefContainer } from '../../../parser-evaluator/model-host'; -import { ScvdNode } from '../../../model/scvd-node'; - -class NamedStubBase extends ScvdNode { - constructor(name: string) { - super(undefined); - this.name = name; - } -} - -const makeRef = ( - name: string, - widthBytes: number, - offsetBytes = 0, - valueType?: RefContainer['valueType'], - withAnchor = true -): RefContainer => { - const ref = new NamedStubBase(name); - return { - base: ref, - anchor: withAnchor ? ref : undefined, - current: ref, - offsetBytes, - widthBytes, - valueType: valueType ?? undefined, - }; -}; +import { MemoryContainer, MemoryHost } from '../../../data-host/memory-host'; describe('MemoryHost', () => { - it('roundtrips numeric values', async () => { - const host = new MemoryHost(); - const ref = makeRef('num', 4); - - await host.writeValue(ref, 0x12345678); - - const out = await host.readValue(ref); - expect(out).toBe(0x12345678 >>> 0); - }); - it('reads and writes via MemoryContainer', () => { const container = new MemoryContainer('blob'); container.write(0, new Uint8Array([1, 2, 3, 4])); @@ -100,51 +63,39 @@ describe('MemoryHost', () => { expect(container.readPartial(0, 2)).toEqual(new Uint8Array([1])); }); - it('handles readValue float types and raw byte output', async () => { + it('roundtrips bytes via setVariable and read', () => { const host = new MemoryHost(); - const f32 = new DataView(new ArrayBuffer(4)); - f32.setFloat32(0, 1.25, true); - host.setVariable('f32', 4, new Uint8Array(f32.buffer), 0); - - const f64 = new DataView(new ArrayBuffer(8)); - f64.setFloat64(0, 2.5, true); - host.setVariable('f64', 8, new Uint8Array(f64.buffer), 0); - host.setVariable('f16', 2, new Uint8Array([0x00, 0x3c]), 0); - - const f32Ref = makeRef('f32', 4, 0, { kind: 'float' }); - const f64Ref = makeRef('f64', 8, 0, { kind: 'float' }); - const f16Ref = makeRef('f16', 2, 0, { kind: 'float' }); - - expect(await host.readValue(f32Ref)).toBeCloseTo(1.25); - expect(await host.readValue(f64Ref)).toBeCloseTo(2.5); - expect(await host.readValue(f16Ref)).toBeCloseTo(1.0); + host.setVariable('num', 4, 0x12345678, 0); + const out = host.read('num', 0, 4); + expect(out).toEqual(new Uint8Array([0x78, 0x56, 0x34, 0x12])); + }); - const bigRef = makeRef('blob', 10); + it('stores and reads byte arrays', () => { + const host = new MemoryHost(); const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - await host.writeValue(bigRef, bytes); - const out = await host.readValue(bigRef); + host.setVariable('blob', 10, bytes, 0); + const out = host.read('blob', 0, 10); expect(out).toEqual(bytes); - expect(out).not.toBe(bytes); - - const raw = await host.readRaw(bigRef, 4); - expect(raw).toEqual(new Uint8Array([1, 2, 3, 4])); + expect(out).not.toBe(bytes); // must be a copy }); - it('falls back to raw bytes for non-standard float widths', async () => { + it('stores float bytes and reads back raw', () => { const host = new MemoryHost(); - host.setVariable('f6', 6, new Uint8Array([1, 2, 3, 4, 5, 6]), 0); - const ref = makeRef('f6', 6, 0, { kind: 'float' }); - - const out = await host.readValue(ref); - expect(out).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6])); + const f32 = new DataView(new ArrayBuffer(4)); + f32.setFloat32(0, 1.25, true); + host.setVariable('f32', 4, new Uint8Array(f32.buffer), 0); + const out = host.read('f32', 0, 4); + expect(out).toBeDefined(); + const dv = new DataView(out!.buffer, out!.byteOffset, out!.byteLength); + expect(dv.getFloat32(0, true)).toBeCloseTo(1.25); }); - it('appends when offset is -1', async () => { + it('appends when offset is -1', () => { const host = new MemoryHost(); host.setVariable('arr', 2, new Uint8Array([1, 2]), -1); host.setVariable('arr', 2, new Uint8Array([3, 4]), -1); - const out = await host.readRaw(makeRef('arr', 4), 4); + const out = host.read('arr', 0, 4); expect(out).toEqual(new Uint8Array([1, 2, 3, 4])); }); @@ -159,103 +110,75 @@ describe('MemoryHost', () => { host.setVariable('arr', 2, new Uint8Array([3, 4]), -1); }); - it('covers float16 edge cases', async () => { + it('returns partial buffers for oversized reads (size > 8)', () => { const host = new MemoryHost(); - host.setVariable('f16zero', 2, new Uint8Array([0x00, 0x00]), 0); - host.setVariable('f16negzero', 2, new Uint8Array([0x00, 0x80]), 0); - host.setVariable('f16sub', 2, new Uint8Array([0x01, 0x00]), 0); - host.setVariable('f16inf', 2, new Uint8Array([0x00, 0x7c]), 0); - host.setVariable('f16nan', 2, new Uint8Array([0x01, 0x7c]), 0); - - const f16Ref = (name: string) => makeRef(name, 2, 0, { kind: 'float' }); - expect(await host.readValue(f16Ref('f16zero'))).toBe(0); - expect(Object.is(await host.readValue(f16Ref('f16negzero')), -0)).toBe(true); - expect(await host.readValue(f16Ref('f16sub'))).toBeGreaterThan(0); - expect(await host.readValue(f16Ref('f16inf'))).toBe(Infinity); - expect(Number.isNaN(await host.readValue(f16Ref('f16nan')) as number)).toBe(true); - - expect(Number.isNaN(memoryHostTest.leToFloat16(new Uint8Array([0x00])))).toBe(true); + host.setVariable('short', 4, new Uint8Array([1, 2, 3, 4]), 0); + + // size > 8 allows partial reads, zero-padded + const out = host.read('short', 0, 12); + expect(out).toEqual(new Uint8Array([1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0])); }); - it('sign-extends int scalar reads', async () => { + it('zero-pads partial reads when container is shorter than requested size', () => { const host = new MemoryHost(); - host.setVariable('i8', 1, new Uint8Array([0x80]), 0); - host.setVariable('i16', 2, new Uint8Array([0x00, 0x80]), 0); - host.setVariable('i32', 4, new Uint8Array([0x00, 0x00, 0x00, 0x80]), 0); - host.setVariable('i8pos', 1, new Uint8Array([0x7f]), 0); - - expect(await host.readValue(makeRef('i8', 1, 0, { kind: 'int' }))).toBe(-128); - expect(await host.readValue(makeRef('i16', 2, 0, { kind: 'int' }))).toBe(-32768); - expect(await host.readValue(makeRef('i32', 4, 0, { kind: 'int' }))).toBe(-2147483648); - expect(await host.readValue(makeRef('i8pos', 1, 0, { kind: 'int' }))).toBe(127); + // Write only 6 bytes but request 10 (> 8 triggers readPartial path) + host.setVariable('partial', 6, new Uint8Array([10, 20, 30, 40, 50, 60]), 0); - expect(await host.readValue(makeRef('i8', 1, 0, { kind: 'uint' }))).toBe(128); + const out = host.read('partial', 0, 10); + expect(out).toEqual(new Uint8Array([10, 20, 30, 40, 50, 60, 0, 0, 0, 0])); + expect(out?.length).toBe(10); }); - it('returns partial buffers for oversized reads', async () => { + it('returns undefined for missing or invalid reads', () => { const host = new MemoryHost(); - host.setVariable('short', 4, new Uint8Array([1, 2, 3, 4]), 0); - - const largeRef = makeRef('short', 12); - const out = await host.readValue(largeRef); - expect(out).toEqual(new Uint8Array([1, 2, 3, 4])); - - const raw = await host.readRaw(makeRef('short', 4), 10); - expect(raw).toEqual(new Uint8Array([1, 2, 3, 4, 0, 0, 0, 0, 0, 0])); + expect(host.read('missing', 0, 4)).toBeUndefined(); + expect(host.read('', 0, 4)).toBeUndefined(); + expect(host.read('any', 0, 0)).toBeUndefined(); + expect(host.read('any', 0, -1)).toBeUndefined(); }); - it('handles bigint reads and non-little endianness branch', async () => { + it('bigint values are stored as LE bytes', () => { const host = new MemoryHost(); - const ref = makeRef('big', 8); - await host.writeValue(ref, 0x0102030405060708n); - const out = await host.readValue(ref); - expect(out).toBe(0x0102030405060708n); - - (host as unknown as { endianness: string }).endianness = 'big'; - expect(await host.readValue(ref)).toBe(0x0102030405060708n); + host.setVariable('big', 8, 0x0102030405060708n, 0); + const out = host.read('big', 0, 8); + expect(out).toEqual(new Uint8Array([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01])); }); - it('returns undefined for invalid readValue/readRaw inputs', async () => { + it('write and read raw bytes at offsets', () => { const host = new MemoryHost(); - const missing = makeRef('missing', 4, 0, undefined, false); - - expect(await host.readValue(missing)).toBeUndefined(); - expect(await host.readValue(makeRef('missing', 4))).toBeUndefined(); - expect(await host.readValue(makeRef('bad', 0))).toBeUndefined(); - const undefWidth: RefContainer = { - ...makeRef('undef', 1), - widthBytes: undefined, - }; - expect(await host.readValue(undefWidth)).toBeUndefined(); - expect(await host.readRaw(missing, 4)).toBeUndefined(); - expect(await host.readRaw(makeRef('bad', 4), 0)).toBeUndefined(); - - await host.writeValue(makeRef('bad', 0), 1); + host.write('raw', 2, new Uint8Array([9, 8, 7, 6])); + const out = host.read('raw', 2, 4); + expect(out).toEqual(new Uint8Array([9, 8, 7, 6])); }); - it('writes values with coercion and validates virtualSize', async () => { + it('getByteLength returns correct size', () => { const host = new MemoryHost(); - const ref = makeRef('bytes', 4); - await host.writeValue(ref, new Uint8Array([1, 2])); - expect(await host.readRaw(ref, 4)).toEqual(new Uint8Array([1, 2, 0, 0])); - - await host.writeValue(ref, true); - expect(await host.readValue(ref)).toBe(1); - await host.writeValue(ref, false); - expect(await host.readValue(ref)).toBe(0); - - const bigRef = makeRef('bigint', 8); - await host.writeValue(bigRef, 0x0102n); - expect(await host.readValue(bigRef)).toBe(0x0102n); + expect(host.getByteLength('missing')).toBe(0); + host.setVariable('v', 4, 0x12345678, 0); + expect(host.getByteLength('v')).toBe(4); + host.setVariable('v', 2, 0x9999, 4); + expect(host.getByteLength('v')).toBe(6); + }); - await host.writeValue(ref, new Uint8Array([9, 8, 7, 6])); - expect(await host.readRaw(ref, 4)).toEqual(new Uint8Array([9, 8, 7, 6])); + it('read returns a copy (no aliasing)', () => { + const host = new MemoryHost(); + const data = new Uint8Array([1, 2, 3, 4]); + host.setVariable('v', 4, data, 0); + const bytes = host.read('v', 0, 4)!; + expect(bytes).toEqual(data); + // Must be a copy, not a reference to internal storage + bytes[0] = 0xFF; + expect(host.read('v', 0, 4)![0]).toBe(1); + }); - const errorSpy = jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {}); - await host.writeValue(ref, 'bad' as unknown as number); - await host.writeValue(ref, 5, 2); - expect(errorSpy).toHaveBeenCalled(); - errorSpy.mockRestore(); + it('write writes raw bytes with zero-fill', () => { + const host = new MemoryHost(); + host.setVariable('buf', 8, new Uint8Array(8), 0); + const payload = new Uint8Array([0x41, 0x42, 0x43]); // "ABC" + host.write('buf', 0, payload, 8); + const result = host.read('buf', 0, 8)!; + expect(result.subarray(0, 3)).toEqual(new Uint8Array([0x41, 0x42, 0x43])); + expect(result.subarray(3, 8)).toEqual(new Uint8Array([0, 0, 0, 0, 0])); }); it('handles setVariable metadata and error cases', () => { @@ -264,7 +187,6 @@ describe('MemoryHost', () => { host.setVariable('badOffset', 1, 1, Number.NaN); host.setVariable('neg', 1, 1, -2); - host.setVariable('badType', 1, 'oops' as unknown as number, 0); host.setVariable('badSize', 1, 1, 0, undefined, 0); host.setVariable('arr', 2, 1, -1, 0x1000, 4); @@ -286,17 +208,15 @@ describe('MemoryHost', () => { expect(host.clearVariable('missing')).toBe(false); }); - it('preserves const variables when clearing non-const data', async () => { + it('preserves const variables when clearing non-const data', () => { const host = new MemoryHost(); host.setVariable('const', 2, 0x1111, 0, undefined, 2, true); host.setVariable('temp', 2, 0x2222, 0); host.clearNonConst(); - const constRef = makeRef('const', 2, 0); - const tempRef = makeRef('temp', 2, 0); - expect(await host.readRaw(constRef, 2)).toEqual(new Uint8Array([0x11, 0x11])); - expect(await host.readRaw(tempRef, 2)).toBeUndefined(); + expect(host.read('const', 0, 2)).toEqual(new Uint8Array([0x11, 0x11])); + expect(host.read('temp', 0, 2)).toBeUndefined(); }); it('handles clearNonConst entries without clear methods and empty element counts', () => { @@ -327,46 +247,32 @@ describe('MemoryHost', () => { errorSpy.mockRestore(); }); - it('exercises nullish defaults for offsets and sizes', async () => { - const host = new MemoryHost(); - const ref = makeRef('defaults', 2); - const customRef: RefContainer = { - ...ref, - offsetBytes: undefined, - widthBytes: undefined, - }; - await host.writeValue(customRef, 1); - - const readRef: RefContainer = { - ...ref, - offsetBytes: undefined, - }; - await host.writeValue(readRef, new Uint8Array([0xAA, 0xBB])); - expect(await host.readRaw(readRef, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); - - const readValueRef: RefContainer = { - ...readRef, - widthBytes: 2, - }; - expect(await host.readValue(readValueRef)).toBe(0xBBAA); - }); - - it('handles empty raw reads after clearVariable', async () => { + it('handles empty reads after clearVariable', () => { const host = new MemoryHost(); - const ref = makeRef('empty', 2, 0); host.setVariable('empty', 2, 0x1234, 0); host.clearVariable('empty'); - expect(await host.readRaw(ref, 2)).toBeUndefined(); + expect(host.read('empty', 0, 2)).toBeUndefined(); }); - it('returns raw bytes when widthBytes exceeds natural type size for non-float', async () => { + it('zero-fills virtual size and supports writes into virtual space', () => { const host = new MemoryHost(); - // IPv4 address: uint8_t with size="4" should return raw bytes instead of being converted to number - host.setVariable('ipv4', 4, new Uint8Array([192, 168, 1, 1]), 0); - const ref = makeRef('ipv4', 4, 0, { kind: 'uint', bits: 8 }); - const out = await host.readValue(ref); - expect(out).toEqual(new Uint8Array([192, 168, 1, 1])); - expect(out).not.toBe(0xC0A80101); // Should not be converted to number + host.setVariable('struct', 2, new Uint8Array([0xAA, 0xBB]), 0, undefined, 6); + + expect(host.read('struct', 0, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); + expect(host.read('struct', 2, 2)).toEqual(new Uint8Array([0x00, 0x00])); + expect(host.read('struct', 4, 2)).toEqual(new Uint8Array([0x00, 0x00])); + + host.write('struct', 2, new Uint8Array([0x11, 0x22])); + + expect(host.read('struct', 0, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); + expect(host.read('struct', 2, 2)).toEqual(new Uint8Array([0x11, 0x22])); + expect(host.read('struct', 4, 2)).toEqual(new Uint8Array([0x00, 0x00])); + + host.write('struct', 4, new Uint8Array([0x33, 0x44])); + + expect(host.read('struct', 0, 2)).toEqual(new Uint8Array([0xAA, 0xBB])); + expect(host.read('struct', 2, 2)).toEqual(new Uint8Array([0x11, 0x22])); + expect(host.read('struct', 4, 2)).toEqual(new Uint8Array([0x33, 0x44])); }); }); diff --git a/src/views/component-viewer/test/unit/model/scvd-var.test.ts b/src/views/component-viewer/test/unit/model/scvd-var.test.ts index ea062807..34244bfe 100644 --- a/src/views/component-viewer/test/unit/model/scvd-var.test.ts +++ b/src/views/component-viewer/test/unit/model/scvd-var.test.ts @@ -71,10 +71,11 @@ describe('ScvdVar', () => { }; await expect(item.getValue()).resolves.toBe(3); + // String values are now encoded to Uint8Array (UTF-8 by default) (item as unknown as { _value?: { getValue: () => Promise } })._value = { getValue: async () => 'bad' }; - await expect(item.getValue()).resolves.toBeUndefined(); + await expect(item.getValue()).resolves.toEqual(new Uint8Array([98, 97, 100])); }); it('computes sizes, member lookups, and offsets', async () => { diff --git a/src/views/component-viewer/test/unit/parser-evaluator/eval-interface/scvd-eval-interface.test.ts b/src/views/component-viewer/test/unit/parser-evaluator/eval-interface/scvd-eval-interface.test.ts index a4805d69..eaf721d0 100644 --- a/src/views/component-viewer/test/unit/parser-evaluator/eval-interface/scvd-eval-interface.test.ts +++ b/src/views/component-viewer/test/unit/parser-evaluator/eval-interface/scvd-eval-interface.test.ts @@ -83,21 +83,26 @@ class LocalFakeMember extends ScvdMember { } } -function makeEval(overrides: Partial & Partial & Partial = {}) { - const merged = overrides ?? {}; +interface MakeEvalOptions { + mem?: Partial; + reg?: Partial; + debug?: Partial; +} + +function makeEval({ mem = {}, reg = {}, debug = {} }: MakeEvalOptions = {}) { const memHost: Partial = { - readValue: jest.fn(), - readRaw: jest.fn().mockResolvedValue(undefined), - writeValue: jest.fn(), + read: jest.fn().mockReturnValue(undefined), + write: jest.fn(), + getByteLength: jest.fn().mockReturnValue(0), getArrayElementCount: jest.fn().mockReturnValue(3), getElementTargetBase: jest.fn().mockReturnValue(0xbeef), - ...merged + ...mem, }; const regHost: Partial = { read: jest.fn().mockReturnValue(undefined), write: jest.fn(), clear: jest.fn(), - ...merged + ...reg, }; const debugTarget: Partial = { readRegister: jest.fn().mockResolvedValue(123), @@ -106,7 +111,7 @@ function makeEval(overrides: Partial & Partial & Pa getNumArrayElements: jest.fn().mockResolvedValue(7), getTargetIsRunning: jest.fn().mockResolvedValue(true), findSymbolAddress: jest.fn().mockResolvedValue(undefined), - ...merged + ...debug, }; const formatter = new ScvdFormatSpecifier(); const evalIf = new ScvdEvalInterface( @@ -120,21 +125,21 @@ function makeEval(overrides: Partial & Partial & Pa describe('ScvdEvalInterface intrinsics and helpers', () => { it('reads register with cache and normalization', async () => { - const regHost = { + const regOverride: Partial = { read: jest.fn().mockReturnValueOnce(undefined).mockReturnValueOnce(999), write: jest.fn() - } as unknown as RegisterHost; - const debugTarget = { readRegister: jest.fn().mockResolvedValue(321) } as unknown as ScvdDebugTarget; - const { evalIf } = makeEval({ ...regHost, ...debugTarget }); + }; + const debugOverride: Partial = { readRegister: jest.fn().mockResolvedValue(321) }; + const { evalIf } = makeEval({ reg: regOverride, debug: debugOverride }); await expect(evalIf.__GetRegVal(' r0 ')).resolves.toBe(321); - expect(regHost.write).toHaveBeenCalledWith('r0', 321); + expect(regOverride.write).toHaveBeenCalledWith('r0', 321); await expect(evalIf.__GetRegVal(' r0 ')).resolves.toBe(999); }); it('__Symbol_exists and __FindSymbol normalize names and map found/not found', async () => { const findSymbolAddress = jest.fn().mockResolvedValue(0x1234); - const { evalIf } = makeEval({ findSymbolAddress }); + const { evalIf } = makeEval({ debug: { findSymbolAddress } }); await expect(evalIf.__Symbol_exists(' ')).resolves.toBe(0); await expect(evalIf.__Symbol_exists('MySym')).resolves.toBe(1); await expect(evalIf.__FindSymbol('MySym')).resolves.toBe(0x1234); @@ -142,16 +147,16 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { it('__CalcMemUsed forwards params', async () => { const calculateMemoryUsage = jest.fn().mockResolvedValue(0xf00d); - const { evalIf } = makeEval({ calculateMemoryUsage }); + const { evalIf } = makeEval({ debug: { calculateMemoryUsage } }); await expect(evalIf.__CalcMemUsed(1, 2, 3, 4)).resolves.toBe(0xf00d); expect(calculateMemoryUsage).toHaveBeenCalledWith(1, 2, 3, 4); }); it('__size_of returns element count from getNumArrayElements', async () => { - const debugTarget: Partial = { + const debugOverride: Partial = { getNumArrayElements: jest.fn().mockResolvedValueOnce(5).mockResolvedValueOnce(undefined) }; - const { evalIf } = makeEval(debugTarget); + const { evalIf } = makeEval({ debug: debugOverride }); await expect(evalIf.__size_of('sym')).resolves.toBe(5); await expect(evalIf.__size_of('sym')).resolves.toBeUndefined(); }); @@ -159,7 +164,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { it('__Offset_of and __Running', async () => { const member = new DummyNode('m', { memberOffset: 12 }); const container: RefContainer = { base: new DummyNode('base', { symbolMap: new Map([['member', member]]) }), current: undefined, valueType: undefined }; - const { evalIf, debugTarget } = makeEval({ getTargetIsRunning: jest.fn().mockResolvedValue(false) }); + const { evalIf, debugTarget } = makeEval({ debug: { getTargetIsRunning: jest.fn().mockResolvedValue(false) } }); await expect(evalIf.__Offset_of(container, 'member')).resolves.toBe(12); await expect(evalIf.__Offset_of(container, 'missing')).resolves.toBeUndefined(); await expect(evalIf.__Running()).resolves.toBe(0); @@ -252,11 +257,11 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { it('_count and _addr defer to MemoryHost', async () => { const base = new DummyNode('arr'); const container: RefContainer = { base, current: base, valueType: undefined }; - const memHost = { + const memOverride: Partial = { getArrayElementCount: jest.fn().mockReturnValue(10), getElementTargetBase: jest.fn().mockReturnValue(0xbeef) - } as unknown as MemoryHost; - const { evalIf } = makeEval(memHost); + }; + const { evalIf } = makeEval({ mem: memOverride }); expect(await evalIf._count(container)).toBe(10); expect(await evalIf._addr(container)).toBe(0xbeef); }); @@ -434,14 +439,67 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { }); }); + it('readValue returns undefined when width is zero or name is missing', async () => { + const { evalIf } = makeEval(); + const noWidth: RefContainer = { base: new DummyNode('b'), anchor: new DummyNode('b'), current: new DummyNode('b'), widthBytes: 0, valueType: undefined }; + expect(await evalIf.readValue(noWidth)).toBeUndefined(); + const noAnchor: RefContainer = { base: new DummyNode('b'), current: new DummyNode('b'), widthBytes: 4, valueType: undefined }; + expect(await evalIf.readValue(noAnchor)).toBeUndefined(); + }); + + it('readValue returns undefined when memHost.read returns undefined', async () => { + const { evalIf } = makeEval({ mem: { read: jest.fn().mockReturnValue(undefined) } }); + const container: RefContainer = { base: new DummyNode('v'), anchor: new DummyNode('v'), current: new DummyNode('v'), widthBytes: 4, valueType: undefined }; + expect(await evalIf.readValue(container)).toBeUndefined(); + }); + + it('writeValue returns undefined when width is zero', async () => { + const { evalIf } = makeEval(); + const noWidth: RefContainer = { base: new DummyNode('b'), anchor: new DummyNode('b'), current: new DummyNode('b'), widthBytes: 0, valueType: undefined }; + expect(await evalIf.writeValue(noWidth, 42)).toBeUndefined(); + }); + + it('writeValue handles Uint8Array values (same size and truncated)', async () => { + const writeMock = jest.fn(); + const { evalIf } = makeEval({ mem: { write: writeMock } }); + const container: RefContainer = { base: new DummyNode('v'), anchor: new DummyNode('v'), current: new DummyNode('v'), widthBytes: 4, valueType: undefined }; + + // Uint8Array with same size as width + const exact = new Uint8Array([1, 2, 3, 4]); + expect(await evalIf.writeValue(container, exact)).toBe(exact); + + // Uint8Array longer than width → truncated + const longer = new Uint8Array([5, 6, 7, 8, 9, 10]); + expect(await evalIf.writeValue(container, longer)).toBe(longer); + expect(writeMock).toHaveBeenCalledWith('v', 0, new Uint8Array([5, 6, 7, 8]), 4); + }); + + it('writeValue handles boolean and bigint values', async () => { + const writeMock = jest.fn(); + const { evalIf } = makeEval({ mem: { write: writeMock } }); + const container: RefContainer = { base: new DummyNode('v'), anchor: new DummyNode('v'), current: new DummyNode('v'), widthBytes: 4, valueType: undefined }; + + expect(await evalIf.writeValue(container, true)).toBe(true); + expect(await evalIf.writeValue(container, false)).toBe(false); + expect(await evalIf.writeValue(container, 0x12345678n)).toBe(0x12345678n); + }); + + it('writeValue returns undefined for unsupported value types', async () => { + const { evalIf } = makeEval(); + jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {}); + const container: RefContainer = { base: new DummyNode('v'), anchor: new DummyNode('v'), current: new DummyNode('v'), widthBytes: 4, valueType: undefined }; + expect(await evalIf.writeValue(container, 'string' as unknown as number)).toBeUndefined(); + (componentViewerLogger.error as unknown as jest.Mock).mockRestore(); + }); + it('read/write value wrap host errors', async () => { - const memHost = { - readValue: jest.fn(() => { throw new Error('boom'); }), - writeValue: jest.fn(() => { throw new Error('boom'); }) - } as unknown as MemoryHost; - const { evalIf } = makeEval(memHost); + const memOverride: Partial = { + read: jest.fn(() => { throw new Error('boom'); }), + write: jest.fn(() => { throw new Error('boom'); }) + }; + const { evalIf } = makeEval({ mem: memOverride }); jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {}); - const container: RefContainer = { base: new DummyNode('b'), current: new DummyNode('b'), valueType: undefined }; + const container: RefContainer = { base: new DummyNode('b'), anchor: new DummyNode('b'), current: new DummyNode('b'), widthBytes: 4, valueType: undefined }; expect(await evalIf.readValue(container)).toBeUndefined(); expect(await evalIf.writeValue(container, 1)).toBeUndefined(); (componentViewerLogger.error as unknown as jest.Mock).mockRestore(); @@ -489,7 +547,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { }); it('normalizeScalarType and helpers handle undefined and invalid pointers', async () => { - const { evalIf, debugTarget } = makeEval({ readMemory: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])) }); + const { evalIf, debugTarget } = makeEval({ debug: { readMemory: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])) } }); const norm = (evalIf as unknown as { normalizeScalarType(v: unknown): unknown }).normalizeScalarType(' double64 '); expect(norm).toEqual({ kind: 'float', name: 'double64', bits: 64 }); @@ -525,29 +583,30 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { }); it('covers member offset success, read/write success, and _count/_addr undefined', async () => { - const memHost = { - readValue: jest.fn().mockReturnValue(7), - writeValue: jest.fn(), + const memOverride: Partial = { + read: jest.fn().mockReturnValue(new Uint8Array([7, 0, 0, 0])), + write: jest.fn(), + getByteLength: jest.fn().mockReturnValue(4), getArrayElementCount: jest.fn().mockReturnValue(5), getElementTargetBase: jest.fn().mockReturnValue(0xabc) - } as unknown as MemoryHost; - const { evalIf } = makeEval(memHost); + }; + const { evalIf } = makeEval({ mem: memOverride }); const member = new DummyNode('m', { memberOffset: 8 }); await expect(evalIf.getMemberOffset(new DummyNode('b'), member)).resolves.toBe(8); - const container: RefContainer = { base: new DummyNode('b'), current: new DummyNode('b'), valueType: undefined }; + const container: RefContainer = { base: new DummyNode('b'), anchor: new DummyNode('b'), current: new DummyNode('b'), widthBytes: 4, valueType: undefined }; expect(await evalIf.readValue(container)).toBe(7); expect(await evalIf.writeValue(container, 9)).toBe(9); expect(await evalIf._count({ base: new DummyNode(undefined), current: new DummyNode(undefined), valueType: undefined } as unknown as RefContainer)).toBeUndefined(); expect(await evalIf._addr({ base: new DummyNode(undefined), current: new DummyNode(undefined), valueType: undefined } as unknown as RefContainer)).toBeUndefined(); - const regHost = { read: jest.fn().mockReturnValueOnce(undefined).mockReturnValueOnce(undefined), write: jest.fn() } as unknown as RegisterHost; - const debugTarget = { readRegister: jest.fn().mockResolvedValue(5) } as unknown as ScvdDebugTarget; - const { evalIf: evalReg } = makeEval({ ...memHost, ...regHost, ...debugTarget }); + const regOverride: Partial = { read: jest.fn().mockReturnValueOnce(undefined).mockReturnValueOnce(undefined), write: jest.fn() }; + const debugOverride: Partial = { readRegister: jest.fn().mockResolvedValue(5) }; + const { evalIf: evalReg } = makeEval({ mem: memOverride, reg: regOverride, debug: debugOverride }); await expect(evalReg.__GetRegVal(' ')).resolves.toBeUndefined(); await expect(evalReg.__GetRegVal('r1')).resolves.toBe(5); - const { evalIf: evalSize } = makeEval({ getSymbolSize: jest.fn().mockResolvedValue(undefined), getNumArrayElements: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget); + const { evalIf: evalSize } = makeEval({ debug: { getSymbolSize: jest.fn().mockResolvedValue(undefined), getNumArrayElements: jest.fn().mockResolvedValue(undefined) } }); await expect(evalSize.__size_of('sym')).resolves.toBeUndefined(); }); @@ -555,7 +614,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { const readUint8ArrayStrFromPointer = jest.fn().mockResolvedValue(undefined); const findSymbolNameAtAddress = jest.fn().mockResolvedValue(undefined); const debugTarget = { readUint8ArrayStrFromPointer, findSymbolNameAtAddress, readMemory: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget; - const { evalIf } = makeEval(debugTarget); + const { evalIf } = makeEval({ debug: debugTarget }); const container: RefContainer = { base: new DummyNode('b'), current: new DummyNode('b'), valueType: undefined }; jest.spyOn(componentViewerLogger, 'error').mockImplementation(() => {}); await expect(evalIf.formatPrintf('C', 'noaddr' as unknown as number, container)).resolves.toBe('noaddr'); @@ -578,7 +637,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { readUint8ArrayStrFromPointer: jest.fn().mockResolvedValue(new Uint8Array([65, 0, 0, 0])), readMemory: jest.fn(async (addr: number, len: number) => (memoryMap.get(addr)?.subarray(0, len))) } as unknown as ScvdDebugTarget; - const formatterEval = new ScvdEvalInterface({} as MemoryHost, {} as RegisterHost, debugTarget, new ScvdFormatSpecifier()); + const formatterEval = new ScvdEvalInterface({ read: jest.fn() } as unknown as MemoryHost, {} as RegisterHost, debugTarget, new ScvdFormatSpecifier()); const member = new LocalFakeMember(); const container: RefContainer = { base: member, current: member, valueType: undefined }; expect(await formatterEval.formatPrintf('C', 0x2000, container)).toBe('CTX'); @@ -612,9 +671,9 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { .getScalarInfo({ base: node, current: node, valueType: undefined } as unknown as RefContainer); expect(info.bits).toBe(64); // clamp from 80 - const regHost = { read: jest.fn().mockReturnValue(undefined), write: jest.fn() } as unknown as RegisterHost; - const debugTarget = { readRegister: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget; - const { evalIf: evalReg } = makeEval({ ...regHost, ...debugTarget }); + const regOverride: Partial = { read: jest.fn().mockReturnValue(undefined), write: jest.fn() }; + const debugOverride: Partial = { readRegister: jest.fn().mockResolvedValue(undefined) }; + const { evalIf: evalReg } = makeEval({ reg: regOverride, debug: debugOverride }); await expect(evalReg.__GetRegVal('r2')).resolves.toBeUndefined(); }); @@ -631,7 +690,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { findSymbolContextAtAddress: jest.fn().mockResolvedValue(undefined), findSymbolNameAtAddress: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget; - const memHost = { readRaw: jest.fn().mockResolvedValue(undefined) } as unknown as MemoryHost; + const memHost = { read: jest.fn().mockReturnValue(undefined) } as unknown as MemoryHost; const formatterEval = new ScvdEvalInterface(memHost, {} as RegisterHost, dbg, new ScvdFormatSpecifier()); const container: RefContainer = { base: new DummyNode('b'), current: new DummyNode('b'), valueType: undefined }; expect(await formatterEval.formatPrintf('x', BigInt(5) as unknown as number, container)).toBe('0x00000005'); @@ -648,21 +707,21 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { }); it('formats %M using cached bytes with inferred width', async () => { - const memHost = { readRaw: jest.fn().mockResolvedValue(new Uint8Array([0x1E, 0x30, 0x6C, 0xA2, 0x45, 0x5F])) } as unknown as MemoryHost; + const memHost = { read: jest.fn().mockReturnValue(new Uint8Array([0x1E, 0x30, 0x6C, 0xA2, 0x45, 0x5F])) } as unknown as MemoryHost; const dbg = { readMemory: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget; const evalIf = new ScvdEvalInterface(memHost, {} as RegisterHost, dbg, new ScvdFormatSpecifier()); const node = new DummyNode('mac', { targetSize: 6 }); const container: RefContainer = { base: node, current: node, anchor: node, valueType: undefined }; - const getByteWidthSpy = jest.spyOn(evalIf, 'getByteWidth'); const out = await evalIf.formatPrintf('M', 0, container); - expect(getByteWidthSpy).toHaveBeenCalledWith(node); + // Width is inferred from getTargetSize via getScalarInfo; read is called with that width + expect(memHost.read).toHaveBeenCalledWith('mac', 0, 6); expect(out).toBe('1E-30-6C-A2-45-5F'); }); it('falls back to default MAC width when container has no base', async () => { - const memHost = { readRaw: jest.fn().mockResolvedValue(undefined) } as unknown as MemoryHost; + const memHost = { read: jest.fn().mockReturnValue(undefined) } as unknown as MemoryHost; const dbg = { readMemory: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget; const evalIf = new ScvdEvalInterface(memHost, {} as RegisterHost, dbg, new ScvdFormatSpecifier()); const container = { base: undefined, current: undefined, valueType: undefined } as unknown as RefContainer; @@ -671,7 +730,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { }); it('uses existing widthBytes for cached MAC reads', async () => { - const memHost = { readRaw: jest.fn().mockResolvedValue(new Uint8Array([0x1E, 0x30, 0x6C, 0xA2, 0x45, 0x5F])) } as unknown as MemoryHost; + const memHost = { read: jest.fn().mockReturnValue(new Uint8Array([0x1E, 0x30, 0x6C, 0xA2, 0x45, 0x5F])) } as unknown as MemoryHost; const dbg = { readMemory: jest.fn().mockResolvedValue(undefined) } as unknown as ScvdDebugTarget; const evalIf = new ScvdEvalInterface(memHost, {} as RegisterHost, dbg, new ScvdFormatSpecifier()); const node = new DummyNode('mac', { targetSize: 6 }); @@ -848,7 +907,7 @@ describe('ScvdEvalInterface intrinsics and helpers', () => { it('covers %M MAC format with pointer dereference', async () => { const memHost = { - readRaw: jest.fn(async () => undefined) + read: jest.fn(() => undefined) } as unknown as MemoryHost; const dbg = { readMemory: jest.fn(async (addr: number, len: number) => { diff --git a/src/views/component-viewer/test/unit/parser-evaluator/intrinsics/intrinsics.test.ts b/src/views/component-viewer/test/unit/parser-evaluator/intrinsics/intrinsics.test.ts index 11090d11..dde25740 100644 --- a/src/views/component-viewer/test/unit/parser-evaluator/intrinsics/intrinsics.test.ts +++ b/src/views/component-viewer/test/unit/parser-evaluator/intrinsics/intrinsics.test.ts @@ -46,6 +46,7 @@ describe('intrinsics', () => { onError.mockClear(); await expect(handleIntrinsic(host, container(), '__CalcMemUsed', [1, 2, 3, 4, 5], onError)).resolves.toBeUndefined(); expect(onError).toHaveBeenCalledWith('Intrinsic __CalcMemUsed expects at most 4 argument(s)'); + }); it('runs numeric intrinsics and coercions', async () => { @@ -222,13 +223,26 @@ describe('intrinsics', () => { await expect(handlePseudoMember(missing, container(), '_count', base, onError)).resolves.toBeUndefined(); expect(onError).toHaveBeenCalledWith('Missing pseudo-member _count'); + onError.mockClear(); + await expect(handlePseudoMember(missing, container(), '_addr', base, onError)).resolves.toBeUndefined(); + expect(onError).toHaveBeenCalledWith('Missing pseudo-member _addr'); + const undef = { _count: jest.fn(async () => undefined), _addr: jest.fn(async () => undefined), } as unknown as IntrinsicProvider; + onError.mockClear(); + await expect(handlePseudoMember(undef, container(), '_count', base, onError)).resolves.toBeUndefined(); + expect(onError).toHaveBeenCalledWith('Pseudo-member _count returned undefined'); + onError.mockClear(); await expect(handlePseudoMember(undef, container(), '_addr', base, onError)).resolves.toBeUndefined(); expect(onError).toHaveBeenCalledWith('Pseudo-member _addr returned undefined'); + + // Unknown pseudo-member should report error + onError.mockClear(); + await expect(handlePseudoMember(host, container(), '_unknown' as '_count', base, onError)).resolves.toBeUndefined(); + expect(onError).toHaveBeenCalledWith('Unknown pseudo-member _unknown'); }); it('keeps intrinsic definitions aligned', () => { diff --git a/src/views/component-viewer/test/unit/parser-evaluator/string-ops.test.ts b/src/views/component-viewer/test/unit/parser-evaluator/string-ops.test.ts new file mode 100644 index 00000000..3646d83f --- /dev/null +++ b/src/views/component-viewer/test/unit/parser-evaluator/string-ops.test.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// generated with AI + +/** + * Unit tests for encodeStringToBytes (string-ops). + */ + +import { encodeStringToBytes } from '../../../parser-evaluator/string-ops'; + +describe('encodeStringToBytes', () => { + describe('UTF-8 (typeSize=1, default)', () => { + it('encodes ASCII text', () => { + const out = encodeStringToBytes('ABC'); + expect(out).toEqual(new Uint8Array([0x41, 0x42, 0x43])); + }); + + it('encodes multi-byte UTF-8 characters', () => { + const out = encodeStringToBytes('\u00e9'); // é = 0xC3 0xA9 + expect(out).toEqual(new Uint8Array([0xc3, 0xa9])); + }); + + it('uses UTF-8 when typeSize defaults to 1', () => { + const out = encodeStringToBytes('A'); + expect(out).toEqual(new Uint8Array([0x41])); + }); + + it('returns empty Uint8Array for empty string', () => { + expect(encodeStringToBytes('')).toEqual(new Uint8Array([])); + }); + }); + + describe('UTF-16 LE (typeSize=2)', () => { + it('encodes ASCII as 2-byte LE code units', () => { + const out = encodeStringToBytes('AB', 2); + expect(out).toEqual(new Uint8Array([0x41, 0x00, 0x42, 0x00])); + }); + + it('encodes non-ASCII BMP characters', () => { + // 'é' = U+00E9 → LE: 0xE9, 0x00 + const out = encodeStringToBytes('\u00e9', 2); + expect(out).toEqual(new Uint8Array([0xe9, 0x00])); + }); + + it('returns empty for empty string', () => { + expect(encodeStringToBytes('', 2)).toEqual(new Uint8Array([])); + }); + + it('encodes CJK character', () => { + // '中' = U+4E2D → LE: 0x2D, 0x4E + const out = encodeStringToBytes('\u4e2d', 2); + expect(out).toEqual(new Uint8Array([0x2d, 0x4e])); + }); + }); + + describe('UTF-32 LE (typeSize=4)', () => { + it('encodes ASCII as 4-byte LE code points', () => { + const out = encodeStringToBytes('A', 4); + expect(out).toEqual(new Uint8Array([0x41, 0x00, 0x00, 0x00])); + }); + + it('encodes supplementary plane character (emoji)', () => { + // '😀' = U+1F600 → LE: 0x00, 0xF6, 0x01, 0x00 + const out = encodeStringToBytes('😀', 4); + const view = new DataView(out.buffer); + expect(view.getUint32(0, true)).toBe(0x1f600); + }); + + it('encodes multiple characters', () => { + const out = encodeStringToBytes('AB', 4); + expect(out.length).toBe(8); + const view = new DataView(out.buffer); + expect(view.getUint32(0, true)).toBe(0x41); + expect(view.getUint32(4, true)).toBe(0x42); + }); + + it('returns empty for empty string', () => { + expect(encodeStringToBytes('', 4)).toEqual(new Uint8Array([])); + }); + }); +}); diff --git a/src/views/component-viewer/test/unit/statement-engine/statement-readlist.test.ts b/src/views/component-viewer/test/unit/statement-engine/statement-readlist.test.ts index 1e00f5db..8fadd4d7 100644 --- a/src/views/component-viewer/test/unit/statement-engine/statement-readlist.test.ts +++ b/src/views/component-viewer/test/unit/statement-engine/statement-readlist.test.ts @@ -26,7 +26,6 @@ import type { ScvdDebugTarget } from '../../../scvd-debug-target'; import { StatementReadList } from '../../../statement-engine/statement-readList'; import { createExecutionContext, TestNode } from '../helpers/statement-engine-helpers'; import type { ScvdNode } from '../../../model/scvd-node'; -import type { RefContainer } from '../../../parser-evaluator/model-host'; import { ScvdExpression } from '../../../model/scvd-expression'; class ExposedStatementReadList extends StatementReadList { @@ -63,19 +62,6 @@ function createMemberNode(targetSize: number | undefined, memberOffset: number | return node; } -function makeRef(name: string, widthBytes: number, offsetBytes = 0): RefContainer { - const ref = new TestNode(undefined); - ref.name = name; - return { - base: ref, - anchor: ref, - current: ref, - offsetBytes, - widthBytes, - valueType: undefined, - }; -} - describe('StatementReadList', () => { it('skips when mustRead is false', async () => { const readList = createReadList(); @@ -530,7 +516,7 @@ describe('StatementReadList', () => { await stmt.executeStatement(ctx, guiTree); - expect(await ctx.memoryHost.readRaw(makeRef('list', 4, 0), 4)).toBeDefined(); + expect(ctx.memoryHost.read('list', 0, 4)).toBeDefined(); }); it('detects linked list loops', async () => { @@ -572,7 +558,7 @@ describe('StatementReadList', () => { await stmt.executeStatement(ctx, guiTree); (ScvdReadList as unknown as { READ_SIZE_MAX: number }).READ_SIZE_MAX = originalMax; - expect(await ctx.memoryHost.readRaw(makeRef('list', 4, 0), 4)).toBeDefined(); + expect(ctx.memoryHost.read('list', 0, 4)).toBeDefined(); }); it('marks const readlists as initialized', async () => { diff --git a/src/views/component-viewer/test/unit/statement-engine/statement-var.test.ts b/src/views/component-viewer/test/unit/statement-engine/statement-var.test.ts index 20fc3c03..caadaf14 100644 --- a/src/views/component-viewer/test/unit/statement-engine/statement-var.test.ts +++ b/src/views/component-viewer/test/unit/statement-engine/statement-var.test.ts @@ -120,4 +120,38 @@ describe('StatementVar', () => { expect(spy).not.toHaveBeenCalled(); }); + + it('passes Uint8Array value through when it fits in targetSize', async () => { + const item = new ScvdVar(undefined); + item.name = 'strVar'; + jest.spyOn(item, 'getTargetSize').mockResolvedValue(8); + jest.spyOn(item, 'getValue').mockResolvedValue(new Uint8Array([72, 101, 108, 108, 111])); // "Hello" + + const stmt = new StatementVar(item, undefined); + const ctx = createExecutionContext(item); + const spy = jest.spyOn(ctx.memoryHost, 'setVariable'); + const guiTree = new ScvdGuiTree(undefined); + + await stmt.executeStatement(ctx, guiTree); + + // Uint8Array fits in targetSize=8 so passed as-is + expect(spy).toHaveBeenCalledWith('strVar', 8, new Uint8Array([72, 101, 108, 108, 111]), -1, 0); + }); + + it('truncates Uint8Array value when it exceeds targetSize', async () => { + const item = new ScvdVar(undefined); + item.name = 'strTrunc'; + jest.spyOn(item, 'getTargetSize').mockResolvedValue(3); + jest.spyOn(item, 'getValue').mockResolvedValue(new Uint8Array([72, 101, 108, 108, 111])); // "Hello" + + const stmt = new StatementVar(item, undefined); + const ctx = createExecutionContext(item); + const spy = jest.spyOn(ctx.memoryHost, 'setVariable'); + const guiTree = new ScvdGuiTree(undefined); + + await stmt.executeStatement(ctx, guiTree); + + // Uint8Array truncated to targetSize=3 + expect(spy).toHaveBeenCalledWith('strTrunc', 3, new Uint8Array([72, 101, 108]), -1, 0); + }); });