From a9717f3ea2e2024349a846c7dfa587673c586954 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 8 Nov 2024 17:21:28 -0800 Subject: [PATCH] Mostly finished implementing improved logging. --- packages/@glimmer/debug/index.ts | 4 + packages/@glimmer/debug/lib/dism/opcode.ts | 5 +- packages/@glimmer/debug/lib/render/basic.ts | 22 +- packages/@glimmer/debug/lib/render/buffer.ts | 12 +- .../@glimmer/debug/lib/render/combinators.ts | 8 +- .../debug/lib/render/fragment-type.ts | 5 +- .../@glimmer/debug/lib/render/fragment.ts | 50 ++-- packages/@glimmer/debug/lib/render/logger.ts | 8 +- packages/@glimmer/debug/lib/render/styles.ts | 8 +- packages/@glimmer/debug/lib/vm/snapshot.ts | 214 +++++++++++++++++- packages/@glimmer/interfaces/index.d.ts | 48 ++-- .../interfaces/lib/runtime/local-debug.d.ts | 25 +- .../interfaces/lib/runtime/vm-state.d.ts | 22 +- packages/@glimmer/interfaces/tsconfig.json | 11 +- packages/@glimmer/runtime/lib/environment.ts | 2 +- packages/@glimmer/runtime/lib/opcodes.ts | 111 ++++----- packages/@glimmer/runtime/lib/vm/append.ts | 13 +- packages/@glimmer/runtime/lib/vm/low-level.ts | 7 +- packages/@glimmer/runtime/lib/vm/stack.ts | 5 + packages/@glimmer/util/lib/array-utils.ts | 34 +++ 20 files changed, 461 insertions(+), 153 deletions(-) diff --git a/packages/@glimmer/debug/index.ts b/packages/@glimmer/debug/index.ts index bcbc40c038..d3d4f908e9 100644 --- a/packages/@glimmer/debug/index.ts +++ b/packages/@glimmer/debug/index.ts @@ -12,6 +12,9 @@ export { strip, } from './lib/metadata'; export { opcodeMetadata } from './lib/opcode-metadata'; +export type { IntoFragment } from './lib/render/fragment'; +export { as, frag, Fragment, intoFragment } from './lib/render/fragment'; +export { DebugLogger } from './lib/render/logger'; export { check, CheckArray, @@ -42,6 +45,7 @@ export { recordStackSize, wrap, } from './lib/stack-check'; +export { type VmDiff, VmSnapshot, type VmSnapshotValueDiff } from './lib/vm/snapshot'; // Types are optimized await automatically export type { diff --git a/packages/@glimmer/debug/lib/dism/opcode.ts b/packages/@glimmer/debug/lib/dism/opcode.ts index 0c2606616b..5acc73200a 100644 --- a/packages/@glimmer/debug/lib/dism/opcode.ts +++ b/packages/@glimmer/debug/lib/dism/opcode.ts @@ -12,6 +12,7 @@ import { exhausted } from '@glimmer/debug-util'; import { REFERENCE } from '@glimmer/reference'; import type { DisassembledOperand } from '../debug'; +import type { ValueRefOptions } from '../render/basic'; import type { Fragment, IntoFragment } from '../render/fragment'; import type { RegisterName, SomeDisassembledOperand } from './dism'; @@ -154,7 +155,7 @@ export class SerializeBlockContext { } } -export function stackValue(element: unknown): Fragment { +export function stackValue(element: unknown, options?: ValueRefOptions): Fragment { if (isReference(element)) { return describeRef(element); } else if (isCompilable(element)) { @@ -176,7 +177,7 @@ export function stackValue(element: unknown): Fragment { return frag` <${as.kw('template')} ${element.meta.moduleName ?? '(unknown module)'}>`; } } else { - return value(element); + return value(element, options); } } diff --git a/packages/@glimmer/debug/lib/render/basic.ts b/packages/@glimmer/debug/lib/render/basic.ts index e58415f9da..ad4c5f76ca 100644 --- a/packages/@glimmer/debug/lib/render/basic.ts +++ b/packages/@glimmer/debug/lib/render/basic.ts @@ -1,7 +1,7 @@ import type { Optional } from '@glimmer/interfaces'; import type { IntoFragment } from './fragment'; -import type { LeafFragment } from './fragment-type'; +import type { LeafFragment, ValueFragment } from './fragment-type'; import { Fragment, intoFragment } from './fragment'; @@ -43,24 +43,26 @@ export function join(frags: IntoFragment[], separator?: Optional): return new Fragment({ kind: 'multi', value: output }); } -export function value( - value: unknown, - options?: { annotation: string } | { ref: string; value: IntoFragment } -): LeafFragment { - const normalize = () => { +export type ValueRefOptions = { annotation: string } | { ref: string; value?: IntoFragment }; + +export function value(val: unknown, options?: ValueRefOptions): LeafFragment { + const normalize = (): ValueFragment['display'] => { if (options === undefined) return; if ('annotation' in options) { - return { ref: options.annotation, value: intoFragment(options.annotation) }; + return { ref: options.annotation, footnote: intoFragment(options.annotation) }; } else { - return { ref: options.ref, value: intoFragment(options.value) }; + return { + ref: options.ref, + footnote: options.value ? intoFragment(options.value) : undefined, + }; } }; return new Fragment({ kind: 'value', - value, - footnote: normalize(), + value: val, + display: normalize(), }); } diff --git a/packages/@glimmer/debug/lib/render/buffer.ts b/packages/@glimmer/debug/lib/render/buffer.ts index 814bb4fb37..bf1f7e1505 100644 --- a/packages/@glimmer/debug/lib/render/buffer.ts +++ b/packages/@glimmer/debug/lib/render/buffer.ts @@ -1,6 +1,8 @@ import type { LogLine } from './entry'; import type { DisplayFragmentOptions, FlushedLines } from './logger'; +import { ANNOTATION_STYLES } from './annotations'; + /** * The `LogFragmentBuffer` is responsible for collecting the fragments that are logged to the * `DebugLogger` so that they can be accumulated during a group and flushed together. @@ -74,6 +76,7 @@ export class LogFragmentBuffer { */ readonly #footnotes: QueuedEntry[] = []; #nextFootnote = 1; + #style = 0; constructor(options: DisplayFragmentOptions) { this.#options = options; @@ -102,12 +105,17 @@ export class LogFragmentBuffer { * The `add` callback also takes a template string and an optional list of substitutions, which * describe the way the footnote itself should be formatted. */ - addFootnoted(subtle: boolean, add: (footnote: number, child: LogFragmentBuffer) => boolean) { + addFootnoted( + subtle: boolean, + add: (footnote: { n: number; style: string }, child: LogFragmentBuffer) => boolean + ) { if (subtle && !this.#options.showSubtle) return; const child = new LogFragmentBuffer(this.#options); - const usedNumber = add(this.#nextFootnote, child); + const style = ANNOTATION_STYLES[this.#style++ % ANNOTATION_STYLES.length] as string; + + const usedNumber = add({ n: this.#nextFootnote, style }, child); if (usedNumber) { this.#nextFootnote += 1; diff --git a/packages/@glimmer/debug/lib/render/combinators.ts b/packages/@glimmer/debug/lib/render/combinators.ts index 809dcdebe7..a88c002c7d 100644 --- a/packages/@glimmer/debug/lib/render/combinators.ts +++ b/packages/@glimmer/debug/lib/render/combinators.ts @@ -1,7 +1,7 @@ import type { Fragment, IntoFragment } from './fragment'; import { group, join } from './basic'; -import { frag, intoFragment } from './fragment'; +import { as, frag, intoFragment } from './fragment'; /** * The prepend function returns a subtle fragment if the contents are subtle. @@ -98,6 +98,10 @@ export function array( const contents = items.map((item) => isSubtle(item) ? frag`${map(item)}`.subtle() : map(item) ); - return wrap('[ ', join(contents, ', '), ' ]'); + return wrap('[ ', join(contents, as.punct(', ')), ' ]'); } } + +export function ifSubtle(fragment: IntoFragment): Fragment { + return intoFragment(fragment).subtle(); +} diff --git a/packages/@glimmer/debug/lib/render/fragment-type.ts b/packages/@glimmer/debug/lib/render/fragment-type.ts index a36b854bbf..75fe531aa5 100644 --- a/packages/@glimmer/debug/lib/render/fragment-type.ts +++ b/packages/@glimmer/debug/lib/render/fragment-type.ts @@ -39,7 +39,10 @@ export interface ValueFragment extends AbstractLeafFragment { * * The `display` property can be provided to override these defaults. */ - readonly display?: { ref: string; footnote: Fragment } | { inline: Fragment } | undefined; + readonly display?: + | { ref: string; footnote?: Fragment | undefined } + | { inline: Fragment } + | undefined; } /** diff --git a/packages/@glimmer/debug/lib/render/fragment.ts b/packages/@glimmer/debug/lib/render/fragment.ts index 3bd150a2d8..3936127566 100644 --- a/packages/@glimmer/debug/lib/render/fragment.ts +++ b/packages/@glimmer/debug/lib/render/fragment.ts @@ -13,7 +13,6 @@ import type { } from './fragment-type'; import type { DisplayFragmentOptions } from './logger'; -import { ANNOTATION_STYLES } from './annotations'; import { LogFragmentBuffer } from './buffer'; import { formats } from './format'; import { FORMATTERS } from './fragment-type'; @@ -125,7 +124,7 @@ export class Fragment { } const fragment = this.#subtle(isSubtle); - return isSubtle ? fragment.styleAll('subtle') : fragment; + return isSubtle ? fragment.styleAll('dim') : fragment; } #subtle(isSubtle: boolean): Fragment { @@ -262,7 +261,7 @@ export class Fragment { // Alternatively, if the value of a `value` fragment is `null` or `undefined`, // append the string `null` or `undefined`, respectively with the `null` style. } else if (fragment.value === null || fragment.value === undefined) { - Fragment.string('null', { + return Fragment.string('null', { style: STYLES.null, subtle: this.isSubtle(), }).appendTo(buffer); @@ -270,7 +269,7 @@ export class Fragment { // Finally, if the value of a `value` fragment is boolean, append the string // `true` or `false` with the `boolean` style. } else if (typeof fragment.value === 'boolean') { - Fragment.string(String(fragment.value), { + return Fragment.string(String(fragment.value), { style: STYLES.boolean, subtle, }).appendTo(buffer); @@ -303,29 +302,36 @@ export class Fragment { // footnote rather than the footnote number. const override = fragment.kind === 'value' ? fragment.display : undefined; - buffer.addFootnoted(fragment.subtle ?? false, (n, footnote) => { - // Rotate the annotation styles so that the footnote references have different colors - const style = ANNOTATION_STYLES[n % ANNOTATION_STYLES.length] as string; + buffer.addFootnoted(fragment.subtle ?? false, ({ n, style }, footnote) => { + const appendValueAsFootnote = (ref: string) => + footnote.append( + subtle, + `%c| %c[${ref}]%c ${FORMATTERS[fragment.kind]}`, + STYLES.dim, + style, + '', + fragment.value + ); if (override) { if ('inline' in override) { override.inline.subtle(subtle).appendTo(footnote); + return false; + } + + buffer.append(subtle, `%c[${override.ref}]%c`, style, ''); + + if (override.footnote) { + frag`${as.dim('| ')}${override.footnote}`.subtle(subtle).appendTo(footnote); } else { - buffer.append(subtle, `%c[${override.ref}]%c `, style, ''); - override.footnote.subtle(subtle).appendTo(footnote); + appendValueAsFootnote(override.ref); } return false; - } else { - buffer.append(subtle, `%c[${n}]%c `, style, ''); - footnote.append( - subtle, - `%c[${n}]%c ${FORMATTERS[fragment.kind]}`, - style, - '', - fragment.value - ); - return true; } + + buffer.append(subtle, `%c[${n}]%c`, style, ''); + appendValueAsFootnote(String(n)); + return true; }); break; @@ -366,7 +372,9 @@ function intoLeafFragment(value: IntoLeafFragment): LeafFragment { } else if (typeof value === 'number') { return new Fragment({ kind: 'integer', value }); } else if (typeof value === 'string') { - if (/^[\s\p{P}]*$/u.test(value)) { + // If the string contains only whitespace and punctuation, we can treat it as a + // punctuation fragment. + if (/^[\s\p{P}\p{Sm}]*$/u.test(value)) { return new Fragment({ kind: 'string', value, style: STYLES.punct }); } else { return new Fragment({ kind: 'string', value }); @@ -393,7 +401,7 @@ export function frag(strings: TemplateStringsArray, ...values: IntoFragment[]): export const as = Object.fromEntries( Object.entries(STYLES).map(([k, v]) => [ k, - (value: IntoFragment) => intoFragment(value).styleAll({ style: v }), + (value: IntoFragment): Fragment => intoFragment(value).styleAll({ style: v }), ]) ) as { [K in keyof typeof STYLES]: ((value: IntoLeafFragment) => LeafFragment) & diff --git a/packages/@glimmer/debug/lib/render/logger.ts b/packages/@glimmer/debug/lib/render/logger.ts index 552c09780f..8850e13a8d 100644 --- a/packages/@glimmer/debug/lib/render/logger.ts +++ b/packages/@glimmer/debug/lib/render/logger.ts @@ -1,5 +1,5 @@ -import type { LOCAL_LOGGER } from '@glimmer/util'; -import { getFlagValues } from '@glimmer/local-debug-flags'; +import { getFlagValues, LOCAL_SUBTLE_LOGGING } from '@glimmer/local-debug-flags'; +import { LOCAL_LOGGER } from '@glimmer/util'; import type { LogEntry, LogLine } from './entry'; import type { IntoFormat } from './format'; @@ -15,6 +15,10 @@ export interface DisplayFragmentOptions { export type FlushedLines = [LogLine, ...LogEntry[]]; export class DebugLogger { + static configured() { + return new DebugLogger(LOCAL_LOGGER, { showSubtle: !!LOCAL_SUBTLE_LOGGING }); + } + readonly #logger: typeof LOCAL_LOGGER; readonly #options: DisplayFragmentOptions; diff --git a/packages/@glimmer/debug/lib/render/styles.ts b/packages/@glimmer/debug/lib/render/styles.ts index e5a4b0d186..bc49919323 100644 --- a/packages/@glimmer/debug/lib/render/styles.ts +++ b/packages/@glimmer/debug/lib/render/styles.ts @@ -9,7 +9,7 @@ export const STYLES = { token: 'color: green', def: 'color: blue', builtin: 'color: blue', - punct: 'color: grey', + punct: 'color: GrayText', kw: 'color: rgb(185 0 99 / 100%);', type: 'color: teal', number: 'color: blue', @@ -24,9 +24,13 @@ export const STYLES = { meta: 'color: grey', register: 'color: purple', constant: 'color: purple', - subtle: 'color: lightgrey', + dim: 'color: lightgrey', internals: 'color: lightgrey; font-style: italic', + diffAdd: 'color: Highlight', + diffDelete: 'color: SelectedItem', + diffChange: 'color: MarkText; background-color: Mark', + sublabel: 'font-style: italic; color: grey', error: 'color: red', label: 'text-decoration: underline', diff --git a/packages/@glimmer/debug/lib/vm/snapshot.ts b/packages/@glimmer/debug/lib/vm/snapshot.ts index 9f8b23cea9..7920c7a4f8 100644 --- a/packages/@glimmer/debug/lib/vm/snapshot.ts +++ b/packages/@glimmer/debug/lib/vm/snapshot.ts @@ -1 +1,213 @@ -export class VmSnapshot {} +import type { + Cursor, + DebugRegisters, + DebugVmSnapshot, + Nullable, + ScopeSlot, + SimpleElement, + VmMachineOp, + VmOp, +} from '@glimmer/interfaces'; +import { exhausted } from '@glimmer/debug-util'; +import { LOCAL_SUBTLE_LOGGING } from '@glimmer/local-debug-flags'; +import { zipArrays, zipTuples } from '@glimmer/util'; +import { $fp, $pc } from '@glimmer/vm'; + +import type { Fragment } from '../render/fragment'; + +import { decodeRegister } from '../debug'; +import { stackValue } from '../dism/opcode'; +import { array } from '../render/combinators'; +import { as, frag } from '../render/fragment'; + +export interface RuntimeOpSnapshot { + type: VmMachineOp | VmOp; + isMachine: 0 | 1; + size: number; +} + +export class VmSnapshot { + #opcode: RuntimeOpSnapshot; + #snapshot: DebugVmSnapshot; + + constructor(opcode: RuntimeOpSnapshot, snapshot: DebugVmSnapshot) { + this.#opcode = opcode; + this.#snapshot = snapshot; + } + + diff(other: VmSnapshot): VmDiff { + return new VmDiff(this.#opcode, this.#snapshot, other.#snapshot); + } +} + +type GetRegisterDiffs = { + [P in keyof D]: VmSnapshotValueDiff; +}; + +type RegisterDiffs = GetRegisterDiffs; + +export class VmDiff { + readonly opcode: RuntimeOpSnapshot; + + readonly registers: RegisterDiffs; + readonly stack: VSnapshotArrayDiff<'stack', unknown[]>; + readonly blocks: VSnapshotArrayDiff<'blocks', object[]>; + readonly cursors: VSnapshotArrayDiff<'cursors', Cursor[]>; + readonly constructing: VmSnapshotValueDiff<'constructing', Nullable>; + readonly destructors: VSnapshotArrayDiff<'destructors', object[]>; + readonly scope: VSnapshotArrayDiff<'scope', ScopeSlot[]>; + + constructor(opcode: RuntimeOpSnapshot, before: DebugVmSnapshot, after: DebugVmSnapshot) { + this.opcode = opcode; + const registers = [] as unknown[]; + + for (const [i, preRegister, postRegister] of zipTuples(before.registers, after.registers)) { + if (i === $pc) { + const preValue = preRegister; + const postValue = postRegister; + registers.push(new VmSnapshotValueDiff(decodeRegister(i), preValue, postValue)); + } else { + registers.push(new VmSnapshotValueDiff(decodeRegister(i), preRegister, postRegister)); + } + } + + this.registers = registers as unknown as RegisterDiffs; + + const frameChange = this.registers[$fp].didChange; + this.stack = new VSnapshotArrayDiff( + 'stack', + before.stack, + after.stack, + frameChange ? 'reset' : undefined + ); + + this.blocks = new VSnapshotArrayDiff('blocks', before.elements.blocks, after.elements.blocks); + + this.constructing = new VmSnapshotValueDiff( + 'constructing', + before.elements.constructing, + after.elements.constructing + ); + + this.cursors = new VSnapshotArrayDiff( + 'cursors', + before.elements.cursors, + after.elements.cursors + ); + + this.destructors = new VSnapshotArrayDiff( + 'destructors', + before.stacks.destroyable, + after.stacks.destroyable + ); + + this.scope = new VSnapshotArrayDiff('scope', before.scope, after.scope); + } +} + +export class VSnapshotArrayDiff { + readonly name: N; + readonly before: T; + readonly after: T; + readonly change: boolean | 'reset'; + + constructor(name: N, before: T, after: T, change: boolean | 'reset' = didChange(before, after)) { + this.name = name; + this.before = before; + this.after = after; + this.change = change; + } + + describe(): Fragment { + if (this.change === false) { + return frag`${as.kw(this.name)}: unchanged`.subtle(); + } + + if (this.change === 'reset') { + return frag`${as.kw(this.name)}: ${as.dim('reset to')} ${array( + this.after.map((v) => stackValue(v)) + )}`; + } + + const fragments: Fragment[] = []; + let seenDiff = false; + + for (const [op, i, before, after] of zipArrays(this.before, this.after)) { + if (Object.is(before, after)) { + if (!seenDiff) { + // If we haven't seen a change yet, only print the value in subtle mode. + fragments.push(stackValue(before, { ref: `${i}` }).subtle()); + } else { + // If we *have* seen a change, print the value unconditionally, but style + // it as dimmed. + if (LOCAL_SUBTLE_LOGGING) { + fragments.push(stackValue(before, { ref: `${i}` }).styleAll('dim')); + } else { + fragments.push(as.dim(``)); + } + } + continue; + } + + // The first time we see + if (!seenDiff && i > 0 && !LOCAL_SUBTLE_LOGGING) { + fragments.push(as.dim(`... ${i} items`)); + } + + let pre: Fragment; + + if (op === 'pop') { + pre = frag`${stackValue(before, { ref: `${i}:popped` })} -> `; + } else if (op === 'retain') { + pre = frag`${stackValue(before, { ref: `${i}:before` })} -> `; + } else if (op === 'push') { + pre = frag`push -> `.subtle(); + } else { + exhausted(op); + } + + let post: Fragment; + + if (op === 'push') { + post = stackValue(after, { ref: `${i}:push` }); + } else if (op === 'retain') { + post = stackValue(after, { ref: `${i}:after` }); + } else if (op === 'pop') { + post = frag`${as.diffDelete('')}`; + } else { + exhausted(op); + } + + fragments.push(frag`${pre}${post}`); + seenDiff = true; + } + + return frag`${as.kw(this.name)}: ${array(fragments)}`; + } +} + +export class VmSnapshotValueDiff { + readonly name: N; + readonly before: T; + readonly after: T; + readonly didChange: boolean; + + constructor(name: N, before: T, after: T) { + this.name = name; + this.before = before; + this.after = after; + this.didChange = !Object.is(before, after); + } + + describe(): Fragment { + if (!this.didChange) { + return frag`${as.register(this.name)}: ${stackValue(this.after)}`.subtle(); + } + + return frag`${as.register(this.name)}: ${stackValue(this.before)} -> ${stackValue(this.after)}`; + } +} + +function didChange(before: unknown[], after: unknown[]): boolean { + return before.length !== after.length || before.some((v, i) => !Object.is(v, after[i])); +} diff --git a/packages/@glimmer/interfaces/index.d.ts b/packages/@glimmer/interfaces/index.d.ts index 4126dc5a10..0773e438e4 100644 --- a/packages/@glimmer/interfaces/index.d.ts +++ b/packages/@glimmer/interfaces/index.d.ts @@ -1,27 +1,27 @@ -import * as WireFormat from './lib/compile/wire-format/api.js'; +import type * as WireFormat from './lib/compile/wire-format/api.d.ts'; -export * from './lib/array.js'; -export * from './lib/compile/index.js'; -export * from './lib/components.js'; -export * from './lib/content.js'; -export * from './lib/core.js'; -export * from './lib/curry.js'; -export * from './lib/dom/attributes.js'; -export * from './lib/dom/bounds.js'; -export * from './lib/dom/changes.js'; -export * from './lib/dom/simple.js'; -export * from './lib/dom/tree-construction.js'; -export * from './lib/managers.js'; -export * from './lib/program.js'; -export * from './lib/references.js'; -export * from './lib/runtime.js'; -export * from './lib/runtime/vm.js'; -export * from './lib/serialize.js'; -export * from './lib/stack.js'; -export * from './lib/tags.js'; -export * from './lib/template.js'; -export * from './lib/tier1/symbol-table.js'; -export * from './lib/type-utils.js'; -export * from './lib/vm-opcodes.js'; +export type * from './lib/array.d.ts'; +export type * from './lib/compile/index.d.ts'; +export type * from './lib/components.d.ts'; +export type * from './lib/content.d.ts'; +export type * from './lib/core.d.ts'; +export type * from './lib/curry.d.ts'; +export type * from './lib/dom/attributes.d.ts'; +export type * from './lib/dom/bounds.d.ts'; +export type * from './lib/dom/changes.d.ts'; +export type * from './lib/dom/simple.d.ts'; +export type * from './lib/dom/tree-construction.d.ts'; +export type * from './lib/managers.d.ts'; +export type * from './lib/program.d.ts'; +export type * from './lib/references.d.ts'; +export type * from './lib/runtime.d.ts'; +export type * from './lib/runtime/vm.d.ts'; +export type * from './lib/serialize.d.ts'; +export type * from './lib/stack.d.ts'; +export type * from './lib/tags.d.ts'; +export type * from './lib/template.d.ts'; +export type * from './lib/tier1/symbol-table.d.ts'; +export type * from './lib/type-utils.d.ts'; +export type * from './lib/vm-opcodes.d.ts'; export { WireFormat }; diff --git a/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts b/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts index fff3656c58..8fbbd59921 100644 --- a/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts @@ -7,19 +7,20 @@ import type { EvaluationContext } from '../program.js'; import type { BlockMetadata } from '../template.js'; import type { DynamicScope, Scope, ScopeSlot } from './scope.js'; import type { UpdatingBlockOpcode, UpdatingOpcode } from './vm.js'; -import type { $fp, $pc, $ra, $s0, $s1, $sp, $t0, $t1, $v0 } from './vm-state.js'; -export interface DebugRegisters extends Array { - [$pc]: number; - [$ra]: number; - [$fp]: number; - [$sp]: number; - [$s0]: unknown; - [$s1]: unknown; - [$t0]: unknown; - [$t1]: unknown; - [$v0]: unknown; -} +export type MachineRegisters = [$pc: number, $ra: number, $fp: number, $sp: number]; + +export type DebugRegisters = readonly [ + $pc: number, + $ra: number, + $fp: number, + $sp: number, + $s0: unknown, + $s1: unknown, + $t0: unknown, + $t1: unknown, + $v0: unknown, +]; type Handle = number; diff --git a/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts b/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts index bfa8254314..a464e54a95 100644 --- a/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts @@ -1,14 +1,14 @@ -export interface SyscallRegisters extends Array { - [$pc]: null; - [$ra]: null; - [$fp]: null; - [$sp]: null; - [$s0]: unknown; - [$s1]: unknown; - [$t0]: unknown; - [$t1]: unknown; - [$v0]: unknown; -} +export type SyscallRegisters = [ + $pc: null, + $ra: null, + $fp: null, + $sp: null, + $s0: unknown, + $s1: unknown, + $t0: unknown, + $t1: unknown, + $v0: unknown, +]; /** * Registers diff --git a/packages/@glimmer/interfaces/tsconfig.json b/packages/@glimmer/interfaces/tsconfig.json index 2103b085d9..d6b3ef9a7a 100644 --- a/packages/@glimmer/interfaces/tsconfig.json +++ b/packages/@glimmer/interfaces/tsconfig.json @@ -1,8 +1,13 @@ { - "extends": ["../tsconfig.json"], + "extends": [ + "../tsconfig.json" + ], "compilerOptions": { "rootDir": ".", - "moduleResolution": "Node16" + "moduleResolution": "bundler" }, - "include": ["index.d.ts", "lib"] + "include": [ + "index.d.ts", + "lib" + ] } diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index 99643cfa26..d8ac52a8c9 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -147,7 +147,7 @@ export class EnvironmentImpl implements Environment { } private get transaction(): TransactionImpl { - return expect(this[TRANSACTION]!, 'must be in a transaction'); + return expect(this[TRANSACTION], 'must be in a transaction'); } didCreate(component: ComponentInstanceWithCreate) { diff --git a/packages/@glimmer/runtime/lib/opcodes.ts b/packages/@glimmer/runtime/lib/opcodes.ts index 385ef6bad3..c6fe059047 100644 --- a/packages/@glimmer/runtime/lib/opcodes.ts +++ b/packages/@glimmer/runtime/lib/opcodes.ts @@ -11,18 +11,22 @@ import type { VmOp, } from '@glimmer/interfaces'; import { VM_SYSCALL_SIZE } from '@glimmer/constants'; -import { debugOp, describeOpcode, opcodeMetadata, recordStackSize } from '@glimmer/debug'; +import { + DebugLogger, + debugOp, + describeOpcode, + opcodeMetadata, + recordStackSize, + VmSnapshot, +} from '@glimmer/debug'; import { assert, dev, unwrap } from '@glimmer/debug-util'; import { LOCAL_DEBUG, LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; -import { valueForRef } from '@glimmer/reference'; import { LOCAL_LOGGER } from '@glimmer/util'; -import { $fp, $pc, $ra, $s0, $s1, $sp, $t0, $t1, $v0 } from '@glimmer/vm'; +import { $pc, $ra, $s0, $s1, $sp, $t0, $t1, $v0 } from '@glimmer/vm'; import type { LowLevelVM, VM } from './vm'; import type { Externs } from './vm/low-level'; -import { isScopeReference } from './scope'; - export interface OpcodeJSON { type: number | string; guid?: Nullable; @@ -44,12 +48,16 @@ export type Evaluate = | { syscall: false; evaluate: MachineOpcode }; export type DebugState = { - type: VmMachineOp | VmOp; - isMachine: 0 | 1; - size: number; + opcode: { + type: VmMachineOp | VmOp; + isMachine: 0 | 1; + size: number; + }; + closeGroup?: undefined | (() => void); params?: Maybe> | undefined; op?: Optional; debug: DebugVmSnapshot; + snapshot: VmSnapshot; }; export class AppendOpcodes { @@ -61,15 +69,25 @@ export class AppendOpcodes { constructor() { if (LOCAL_DEBUG) { this.debugBefore = (debug: DebugVmSnapshot, opcode: RuntimeOp): DebugState => { + let opcodeSnapshot = { + type: opcode.type, + size: opcode.size, + isMachine: opcode.isMachine, + } as const; + + let snapshot = new VmSnapshot(opcodeSnapshot, debug); let params: Maybe> = undefined; let op: DebugOp | undefined = undefined; + let closeGroup: (() => void) | undefined; if (LOCAL_TRACE_LOGGING) { + const logger = DebugLogger.configured(); + let pos = debug.registers[$pc] - opcode.size; op = debugOp(debug.context.program, opcode, debug.template); - LOCAL_LOGGER.debug(`${pos}. ${describeOpcode(op.name, op.params)}`); + closeGroup = logger.group(`${pos}. ${describeOpcode(op.name, op.params)}`).expanded(); let debugParams = []; for (let [name, param] of Object.entries(op.params)) { @@ -84,19 +102,25 @@ export class AppendOpcodes { recordStackSize(debug.registers[$sp]); return { op, + closeGroup, params, - type: opcode.type, - isMachine: opcode.isMachine, - size: opcode.size, + opcode: opcodeSnapshot, debug, + snapshot, }; }; - this.debugAfter = (post: DebugVmSnapshot, pre: DebugState) => { - let { type } = pre; + this.debugAfter = (postSnapshot: DebugVmSnapshot, pre: DebugState) => { + let post = new VmSnapshot(pre.opcode, postSnapshot); + let diff = pre.snapshot.diff(post); + let { + opcode: { type }, + } = pre; + + let sp = diff.registers[$sp]; let meta = opcodeMetadata(type); - let actualChange = post.registers[$sp] - pre.debug.registers[$sp]; + let actualChange = sp.after - sp.before; if ( meta && meta.check !== false && @@ -105,49 +129,34 @@ export class AppendOpcodes { ) { throw new Error( `Error in ${pre.op?.name}:\n\n${pre.debug.registers[$pc]}. ${ - pre.op - ? describeOpcode(pre.op?.name, pre.params!) - : unwrap(opcodeMetadata(pre.type)).name + pre.op ? describeOpcode(pre.op?.name, pre.params!) : unwrap(opcodeMetadata(type)).name }\n\nStack changed by ${actualChange}, expected ${meta.stackChange}` ); } if (LOCAL_TRACE_LOGGING) { - const { stack, registers, scope } = post; - - LOCAL_LOGGER.debug( - '%c -> pc: %d, ra: %d, fp: %d, sp: %d, s0: %O, s1: %O, t0: %O, t1: %O, v0: %O', - 'color: orange', - registers[$pc], - registers[$ra], - registers[$fp], - registers[$sp], - registers[$s0], - registers[$s1], - registers[$t0], - registers[$t1], - registers[$v0] - ); - LOCAL_LOGGER.debug('%c -> eval stack', 'color: red', stack); - LOCAL_LOGGER.debug('%c -> block stack', 'color: magenta', post.elements.blocks); - LOCAL_LOGGER.debug('%c -> destructor stack', 'color: violet', post.stacks.destroyable); - if (post.stacks.scope.at(-1) === undefined) { - LOCAL_LOGGER.debug('%c -> scope', 'color: green', 'null'); - } else { - LOCAL_LOGGER.debug( - '%c -> scope', - 'color: green', - scope.map((s) => (isScopeReference(s) ? valueForRef(s) : s)) - ); + const logger = DebugLogger.configured(); + + logger.log(diff.registers[$pc].describe()); + logger.log(diff.registers[$ra].describe()); + logger.log(diff.registers[$s0].describe()); + logger.log(diff.registers[$s1].describe()); + logger.log(diff.registers[$t0].describe()); + logger.log(diff.registers[$t1].describe()); + logger.log(diff.registers[$v0].describe()); + logger.log(diff.stack.describe()); + logger.log(diff.destructors.describe()); + logger.log(diff.scope.describe()); + + if (diff.constructing.didChange || diff.blocks.change) { + const done = logger.group(`tree construction`).expanded(); + logger.log(diff.constructing.describe()); + logger.log(diff.blocks.describe()); + logger.log(diff.cursors.describe()); + done(); } - LOCAL_LOGGER.debug( - '%c -> elements', - 'color: blue', - post.elements.cursors.at(-1)!.element - ); - - LOCAL_LOGGER.debug('%c -> constructing', 'color: aqua', post.elements.constructing); + pre.closeGroup?.(); } }; } diff --git a/packages/@glimmer/runtime/lib/vm/append.ts b/packages/@glimmer/runtime/lib/vm/append.ts index dc637807f7..b8cc02dd4f 100644 --- a/packages/@glimmer/runtime/lib/vm/append.ts +++ b/packages/@glimmer/runtime/lib/vm/append.ts @@ -1,7 +1,6 @@ import type { BlockMetadata, CompilableTemplate, - DebugRegisters, DebugStacks, DebugTemplates, DebugVmSnapshot, @@ -254,7 +253,10 @@ export class VM { template: templates.active, scope: this.scope().snapshot(), stack: this.lowlevel.stack.snapshot!(), - registers: [...this.lowlevel.registers, ...this.#registers] as DebugRegisters, + registers: [ + ...this.lowlevel.registers, + ...sliceTuple(this.#registers, this.lowlevel.registers), + ], }); } @@ -854,3 +856,10 @@ export class Closure { return new VM(this.state, this.context, tree); } } + +function sliceTuple( + tuple: T, + prefix: Prefix +): T extends [...Prefix, ...infer Rest] ? Rest : never { + return tuple.slice(prefix.length) as T extends [...Prefix, ...infer Rest] ? Rest : never; +} diff --git a/packages/@glimmer/runtime/lib/vm/low-level.ts b/packages/@glimmer/runtime/lib/vm/low-level.ts index 18e066ca4f..248db6187c 100644 --- a/packages/@glimmer/runtime/lib/vm/low-level.ts +++ b/packages/@glimmer/runtime/lib/vm/low-level.ts @@ -17,12 +17,7 @@ import type { VM } from './append'; import { APPEND_OPCODES } from '../opcodes'; -export interface LowLevelRegisters extends Array { - [$pc]: number; - [$ra]: number; - [$sp]: number; - [$fp]: number; -} +export type LowLevelRegisters = [$pc: number, $ra: number, $sp: number, $fp: number]; export function initializeRegisters(): LowLevelRegisters { return [0, -1, 0, 0]; diff --git a/packages/@glimmer/runtime/lib/vm/stack.ts b/packages/@glimmer/runtime/lib/vm/stack.ts index c644b1276d..b89cc8fbf3 100644 --- a/packages/@glimmer/runtime/lib/vm/stack.ts +++ b/packages/@glimmer/runtime/lib/vm/stack.ts @@ -46,6 +46,11 @@ export default class EvaluationStackImpl implements EvaluationStack { this.registers = registers; if (LOCAL_DEBUG) { + this.snapshot = () => { + const fpRegister = this.registers[$fp]; + const fp = fpRegister === -1 ? 0 : fpRegister; + return this.stack.slice(fp, this.registers[$sp] + 1); + }; Object.seal(this); } } diff --git a/packages/@glimmer/util/lib/array-utils.ts b/packages/@glimmer/util/lib/array-utils.ts index b4923e0f4c..f39073e6fb 100644 --- a/packages/@glimmer/util/lib/array-utils.ts +++ b/packages/@glimmer/util/lib/array-utils.ts @@ -27,3 +27,37 @@ export function* enumerate(input: Iterable): IterableIterator<[number, T]> yield [i++, item]; } } + +type ZipEntry = { + [P in keyof T]: P extends `${infer N extends number}` ? [N, T[P], T[P]] : never; +}[keyof T & number]; + +/** + * Zip two tuples with the same type and number of elements. + */ +export function* zipTuples( + left: T, + right: T +): IterableIterator> { + for (let i = 0; i < left.length; i++) { + yield [i, left[i], right[i]] as ZipEntry; + } +} + +export function* zipArrays( + left: T[], + right: T[] +): IterableIterator< + ['retain', number, T, T] | ['pop', number, T, undefined] | ['push', number, undefined, T] +> { + for (let i = 0; i < left.length; i++) { + const perform = i < right.length ? 'retain' : 'pop'; + yield [perform, i, left[i], right[i]] as + | ['retain', number, T, T] + | ['pop', number, T, undefined]; + } + + for (let i = left.length; i < right.length; i++) { + yield ['push', i, undefined, right[i]] as ['push', number, undefined, T]; + } +}