From 15096e9ef95b1c3d03b0e870190c2362dcd63e52 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 13 Nov 2024 11:48:16 -0700 Subject: [PATCH] More trace log improvements --- .vscode/settings.json | 27 ++- bin/setup-bench.mjs | 79 ++++---- package.json | 2 +- .../integration-tests/lib/helpers.ts | 11 +- .../test/chaos-rehydration-test.ts | 4 +- .../lib/passes/1-normalization/index.ts | 21 +- packages/@glimmer/compiler/package.json | 1 + packages/@glimmer/debug-util/index.ts | 2 +- .../@glimmer/debug-util/lib/debug-brand.ts | 118 +++++++++++ packages/@glimmer/debug/index.ts | 4 +- packages/@glimmer/debug/lib/dism/opcode.ts | 186 ++++++++++++++---- packages/@glimmer/debug/lib/render/basic.ts | 63 +++++- .../@glimmer/debug/lib/render/combinators.ts | 6 +- .../debug/lib/render/fragment-type.ts | 12 +- .../@glimmer/debug/lib/render/fragment.ts | 16 +- packages/@glimmer/debug/lib/render/ref.ts | 2 +- packages/@glimmer/debug/lib/render/styles.ts | 4 +- packages/@glimmer/debug/lib/vm/snapshot.ts | 42 ++-- packages/@glimmer/interfaces/lib/core.d.ts | 2 + .../interfaces/lib/dom/attributes.d.ts | 2 + .../@glimmer/interfaces/lib/references.d.ts | 2 +- packages/@glimmer/reference/lib/iterable.ts | 6 +- packages/@glimmer/runtime/lib/bounds.ts | 6 +- .../runtime/lib/compiled/opcodes/content.ts | 4 +- .../runtime/lib/compiled/opcodes/dom.ts | 4 +- .../lib/compiled/opcodes/expressions.ts | 4 +- packages/@glimmer/runtime/lib/opcodes.ts | 17 +- .../runtime/lib/references/curry-value.ts | 4 +- packages/@glimmer/runtime/lib/vm/arguments.ts | 18 +- .../runtime/lib/vm/element-builder.ts | 24 ++- packages/@glimmer/syntax/lib/source/source.ts | 6 +- packages/@glimmer/syntax/lib/symbol-table.ts | 14 +- .../@glimmer/syntax/lib/v2/objects/node.ts | 7 +- packages/@glimmer/util/index.ts | 2 +- packages/@glimmer/util/lib/collections.ts | 2 +- pnpm-lock.yaml | 51 ++--- 36 files changed, 592 insertions(+), 183 deletions(-) create mode 100644 packages/@glimmer/debug-util/lib/debug-brand.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index cc7bb0ac30..dd28b1ac36 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,10 +28,18 @@ "mode": "auto" } ], - "eslint.validate": ["javascript", "typescript", "json", "jsonc"], + "eslint.validate": [ + "javascript", + "typescript", + "json", + "jsonc" + ], "files.exclude": { "**/.DS_Store": true, - "**/.git": true + "**/.git": true, + "**/node_modules": true, + "**/dist": true, + "tracerbench-results": true, }, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, @@ -52,9 +60,15 @@ "eslint.problems.shortenToSingleLine": true, "typescript.experimental.expandableHover": true, "inline-bookmarks.expert.custom.words.mapping": { - "warn": ["@premerge(\\s|$)"], - "active": ["@active(\\s|$)"], - "fixme": ["@fixme(\\s|$)"] + "warn": [ + "@premerge(\\s|$)" + ], + "active": [ + "@active(\\s|$)" + ], + "fixme": [ + "@fixme(\\s|$)" + ] }, "inline-bookmarks.expert.custom.styles": { "active": { @@ -226,5 +240,6 @@ "rewrap.onSave": false, "rewrap.autoWrap.enabled": true, "rewrap.reformat": true, - "rewrap.wholeComment": false + "rewrap.wholeComment": false, + "explorer.excludeGitIgnore": true } diff --git a/bin/setup-bench.mjs b/bin/setup-bench.mjs index c0564cb6f2..49acb85201 100644 --- a/bin/setup-bench.mjs +++ b/bin/setup-bench.mjs @@ -6,7 +6,8 @@ import { readFile, writeFile } from 'node:fs/promises'; const ROOT = new URL('..', import.meta.url).pathname; $.verbose = true; -const REUSE_CONTROL = !!process.env['REUSE_CONTROL']; +const REUSE_CONTROL = !!(process.env['REUSE_DIRS'] || process.env['REUSE_CONTROL']); +const REUSE_EXPERIMENT = !!(process.env['REUSE_DIRS'] || process.env['REUSE_EXPERIMENT']); /* @@ -81,8 +82,10 @@ if (!REUSE_CONTROL) { await $`mkdir ${CONTROL_DIR}`; } -await $`rm -rf ${EXPERIMENT_DIR}`; -await $`mkdir ${EXPERIMENT_DIR}`; +if (!REUSE_EXPERIMENT) { + await $`rm -rf ${EXPERIMENT_DIR}`; + await $`mkdir ${EXPERIMENT_DIR}`; +} // Intentionally use the same folder for both experiment and control to make it easier to // make changes to the benchmark suite itself and compare the results. @@ -138,16 +141,10 @@ console.info({ }); // setup experiment -await within(async () => { - await buildRepo(EXPERIMENT_DIR, experimentRef); -}); +await buildRepo(EXPERIMENT_DIR, experimentRef, REUSE_EXPERIMENT); -if (!REUSE_CONTROL) { - // setup control - await within(async () => { - await buildRepo(CONTROL_DIR, controlRef); - }); -} +// setup control +await buildRepo(CONTROL_DIR, controlRef, REUSE_CONTROL); // start build assets $`cd ${CONTROL_BENCH_DIR} && pnpm vite preview --port ${CONTROL_PORT}`; @@ -177,36 +174,48 @@ process.exit(0); /** * @param {string} directory the directory to clone into * @param {string} ref the ref to checkout + * @param {boolean} reuse reuse the existing directory */ -async function buildRepo(directory, ref) { - // the benchmark directory is located in `packages/@glimmer/benchmark` in each of the - // experiment and control checkouts - const benchDir = join(directory, 'benchmark', 'benchmarks', 'krausest'); +async function buildRepo(directory, ref, reuse) { + if (!reuse) { + await $`rm -rf ${directory}`; + await $`mkdir ${directory}`; + } - await cd(directory); + await within(async () => { + // the benchmark directory is located in `packages/@glimmer/benchmark` in each of the + // experiment and control checkouts + const benchDir = join(directory, 'benchmark', 'benchmarks', 'krausest'); - // write the `pwd` to the output to make it easier to debug if something goes wrong - await $`pwd`; + await cd(directory); - // clone the raw git repo for the experiment - await $`git clone ${join(ROOT, '.git')} .`; + // write the `pwd` to the output to make it easier to debug if something goes wrong + await $`pwd`; - // checkout the repo to the HEAD of the current branch - await $`git checkout --force ${ref}`; + if (reuse) { + await $`git fetch`; + } else { + // clone the raw git repo for the experiment + await $`git clone ${join(ROOT, '.git')} .`; + } - // recreate the benchmark directory - await $`rm -rf ./benchmark`; - // intentionally use the same folder for both experiment and control - await $`cp -r ${BENCHMARK_FOLDER} ./benchmark`; + // checkout the repo to the HEAD of the current branch + await $`git checkout --force ${ref}`; - // `pnpm install` and build the repo - await $`pnpm install --no-frozen-lockfile`; - await $`pnpm build`; + // recreate the benchmark directory + await $`rm -rf ./benchmark`; + // intentionally use the same folder for both experiment and control + await $`cp -r ${BENCHMARK_FOLDER} ./benchmark`; - // rewrite all `package.json`s to behave like published packages - await rewritePackageJson(); + // `pnpm install` and build the repo + await $`pnpm install --no-frozen-lockfile`; + await $`pnpm build`; - // build the benchmarks using vite - await cd(benchDir); - await $`pnpm vite build`; + // rewrite all `package.json`s to behave like published packages + await rewritePackageJson(); + + // build the benchmarks using vite + await cd(benchDir); + await $`pnpm vite build`; + }); } diff --git a/package.json b/package.json index 33307bf169..a8574a9534 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "tracerbench": "^8.0.1", "ts-node": "^10.9.1", "turbo": "^1.9.3", - "typescript": "^5.0.4", + "typescript": "~5.0.4", "vite": "^5.4.10", "xo": "^0.54.2", "zx": "^8.1.9" diff --git a/packages/@glimmer-workspace/integration-tests/lib/helpers.ts b/packages/@glimmer-workspace/integration-tests/lib/helpers.ts index 02a1ca7c49..82be255877 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/helpers.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/helpers.ts @@ -1,10 +1,19 @@ import type { CapturedArguments, Dict } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference'; +import { setLocalDebugType } from '@glimmer/debug-util'; +import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { createComputeRef } from '@glimmer/reference'; import { reifyNamed, reifyPositional } from '@glimmer/runtime'; export type UserHelper = (args: ReadonlyArray, named: Dict) => unknown; export function createHelperRef(helper: UserHelper, args: CapturedArguments): Reference { - return createComputeRef(() => helper(reifyPositional(args.positional), reifyNamed(args.named))); + return createComputeRef( + () => helper(reifyPositional(args.positional), reifyNamed(args.named)), + undefined + ); +} + +if (LOCAL_DEBUG) { + setLocalDebugType('factory:helper', createHelperRef, { name: 'createHelper' }); } diff --git a/packages/@glimmer-workspace/integration-tests/test/chaos-rehydration-test.ts b/packages/@glimmer-workspace/integration-tests/test/chaos-rehydration-test.ts index 5b4bc6a027..a1df39e578 100644 --- a/packages/@glimmer-workspace/integration-tests/test/chaos-rehydration-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/chaos-rehydration-test.ts @@ -1,7 +1,7 @@ import type { Dict, Nullable, SimpleElement } from '@glimmer/interfaces'; import { COMMENT_NODE, ELEMENT_NODE } from '@glimmer/constants'; import { castToBrowser, castToSimple, expect } from '@glimmer/debug-util'; -import { isObject, LOCAL_LOGGER } from '@glimmer/util'; +import { isIndexable, LOCAL_LOGGER } from '@glimmer/util'; import type { ComponentBlueprint, Content } from '..'; @@ -165,7 +165,7 @@ abstract class AbstractChaosMonkeyTest extends RenderTest { } function getErrorMessage(assert: Assert, error: unknown): string { - if (isObject(error) && 'message' in error && typeof error.message === 'string') { + if (isIndexable(error) && 'message' in error && typeof error.message === 'string') { return error.message; } else { assert.pushResult({ diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts index 117f216b85..820942ddce 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts @@ -1,4 +1,5 @@ import type { ASTv2, src } from '@glimmer/syntax'; +import { DebugLogger, frag, fragment, valueFragment } from '@glimmer/debug'; import { LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; import { LOCAL_LOGGER } from '@glimmer/util'; @@ -56,16 +57,28 @@ export default function normalize( let state = new NormalizationState(root.table, isStrict); if (LOCAL_TRACE_LOGGING) { - LOCAL_LOGGER.groupCollapsed(`pass0: visiting`); - LOCAL_LOGGER.debug('symbols', root.table); - LOCAL_LOGGER.debug('source', source); - LOCAL_LOGGER.groupEnd(); + const logger = DebugLogger.configured(); + const done = logger.group(`pass0: visiting`).collapsed(); + logger.log(valueFragment(root.table)); + // LOCAL_LOGGER.debug('symbols', root.table); + logger.log(valueFragment(source)); + done(); } let body = VISIT_STMTS.visitList(root.body, state); if (LOCAL_TRACE_LOGGING) { + const logger = DebugLogger.configured(); + if (body.isOk) { + const done = logger.group(frag`pass0: out`).collapsed(); + const ops = body.value.toPresentArray(); + + if (ops) { + const full = frag` ${valueFragment(ops)}`.subtle(); + logger.log(frag`${fragment.array(ops.map((op) => valueFragment(op)))}${full}`); + } + done(); LOCAL_LOGGER.debug('-> pass0: out', body.value); } else { LOCAL_LOGGER.debug('-> pass0: error', body.reason); diff --git a/packages/@glimmer/compiler/package.json b/packages/@glimmer/compiler/package.json index b0b9f4520c..10a2a1d096 100644 --- a/packages/@glimmer/compiler/package.json +++ b/packages/@glimmer/compiler/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@glimmer-workspace/build-support": "workspace:*", "@glimmer/constants": "workspace:*", + "@glimmer/debug": "workspace:*", "@glimmer/debug-util": "workspace:*", "@glimmer/local-debug-flags": "workspace:*", "@types/node": "^20.9.4", diff --git a/packages/@glimmer/debug-util/index.ts b/packages/@glimmer/debug-util/index.ts index 8a7dba281c..84eb5160b5 100644 --- a/packages/@glimmer/debug-util/index.ts +++ b/packages/@glimmer/debug-util/index.ts @@ -1,4 +1,5 @@ export { default as assert, assertNever, deprecate } from './lib/assert'; +export * from './lib/debug-brand'; export { default as debugToString } from './lib/debug-to-string'; export * from './lib/platform-utils'; export * from './lib/present'; @@ -11,5 +12,4 @@ export { } from './lib/simple-cast'; export * from './lib/template'; export { default as buildUntouchableThis } from './lib/untouchable-this'; - export type FIXME = (T & S) | T; diff --git a/packages/@glimmer/debug-util/lib/debug-brand.ts b/packages/@glimmer/debug-util/lib/debug-brand.ts new file mode 100644 index 0000000000..e2d54f5ea0 --- /dev/null +++ b/packages/@glimmer/debug-util/lib/debug-brand.ts @@ -0,0 +1,118 @@ +import type { + AnyFn, + AppendingBlock, + BlockArguments, + Cursor, + Dict, + NamedArguments, + PositionalArguments, + VMArguments, +} from '@glimmer/interfaces'; +import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; + +const LOCAL_DEBUG_BRAND = new WeakMap(); + +/** + * An object branded with a local debug type has special local trace logging + * behavior. + * + * If `LOCAL_DEBUG` is `false`, this function does nothing (and is removed + * by the minifier in builder). + */ +export function setLocalDebugType

( + type: P, + ...brand: SetLocalDebugArgs

+): void; +export function setLocalDebugType(type: string, ...brand: [value: object, options?: object]) { + if (LOCAL_DEBUG) { + if (brand.length === 1) { + const [value] = brand; + LOCAL_DEBUG_BRAND.set(value, { type, value } as ClassifiedLocalDebug); + } else { + const [value, options] = brand; + LOCAL_DEBUG_BRAND.set(value, { type, value, options } as ClassifiedLocalDebug); + } + } +} + +/** + * An object branded with a local debug type has special local trace logging + * behavior. + * + * If `LOCAL_DEBUG` is `false`, this function always returns undefined. However, + * this function should only be called by the trace logger, which should only + * run in trace `LOCAL_DEBUG` + `LOCAL_TRACE_LOGGING` mode. + */ +export function getLocalDebugType(value: object): ClassifiedLocalDebug | void { + if (LOCAL_DEBUG) { + return LOCAL_DEBUG_BRAND.get(value); + } +} + +interface SourcePosition { + line: number; + column: number; +} + +export interface LocalDebugMap { + args: [VMArguments]; + 'args:positional': [PositionalArguments]; + 'args:named': [NamedArguments]; + 'args:blocks': [BlockArguments]; + cursor: [Cursor]; + 'block:simple': [AppendingBlock]; + 'block:remote': [AppendingBlock]; + 'block:resettable': [AppendingBlock]; + 'factory:helper': [AnyFn, { name: string }]; + + 'syntax:source': [{ readonly source: string; readonly module: string }]; + 'syntax:symbol-table:program': [object, { debug?: () => DebugProgramSymbolTable }]; + + 'syntax:mir:node': [ + { loc: { startPosition: SourcePosition; endPosition: SourcePosition }; type: string }, + ]; +} + +export interface DebugProgramSymbolTable { + readonly templateLocals: readonly string[]; + readonly keywords: readonly string[]; + readonly symbols: readonly string[]; + readonly upvars: readonly string[]; + readonly named: Dict; + readonly blocks: Dict; + readonly hasDebugger: boolean; +} + +export type LocalDebugType = keyof LocalDebugMap; + +export type SetLocalDebugArgs = { + [P in D]: LocalDebugMap[P] extends [infer This extends object, infer Options extends object] + ? [This, Options] + : LocalDebugMap[P] extends [infer This extends object] + ? [This] + : never; +}[D]; + +export type ClassifiedLocalDebug = { + [P in LocalDebugType]: LocalDebugMap[P] extends [infer T, infer Options] + ? { type: P; value: T; options: Options } + : LocalDebugMap[P] extends [infer T] + ? { type: P; value: T } + : never; +}[LocalDebugType]; + +export type ClassifiedLocalDebugFor = LocalDebugMap[N] extends [ + infer T, + infer Options, +] + ? { type: N; value: T; options: Options } + : LocalDebugMap[N] extends [infer T] + ? { type: N; value: T } + : never; + +export type ClassifiedOptions = LocalDebugMap[N] extends [ + unknown, + infer Options, +] + ? Options + : never; diff --git a/packages/@glimmer/debug/index.ts b/packages/@glimmer/debug/index.ts index d3d4f908e9..deaa630b90 100644 --- a/packages/@glimmer/debug/index.ts +++ b/packages/@glimmer/debug/index.ts @@ -1,5 +1,6 @@ export type { DebugOp, SomeDisassembledOperand } from './lib/debug'; export { debugOp, describeOpcode, logOpcodeSlice } from './lib/debug'; +export { describeOp } from './lib/dism/opcode'; export { buildEnum, buildMetas, @@ -12,6 +13,8 @@ export { strip, } from './lib/metadata'; export { opcodeMetadata } from './lib/opcode-metadata'; +export { value as valueFragment } from './lib/render/basic'; +export * as fragment from './lib/render/combinators'; export type { IntoFragment } from './lib/render/fragment'; export { as, frag, Fragment, intoFragment } from './lib/render/fragment'; export { DebugLogger } from './lib/render/logger'; @@ -46,7 +49,6 @@ export { wrap, } from './lib/stack-check'; export { type VmDiff, VmSnapshot, type VmSnapshotValueDiff } from './lib/vm/snapshot'; - // Types are optimized await automatically export type { NormalizedMetadata, diff --git a/packages/@glimmer/debug/lib/dism/opcode.ts b/packages/@glimmer/debug/lib/dism/opcode.ts index 5acc73200a..520499594a 100644 --- a/packages/@glimmer/debug/lib/dism/opcode.ts +++ b/packages/@glimmer/debug/lib/dism/opcode.ts @@ -1,31 +1,37 @@ +import type { ClassifiedLocalDebug, ClassifiedLocalDebugFor } from '@glimmer/debug-util'; import type { + AppendingBlock, BlockMetadata, BlockSymbolNames, - CompilableTemplate, + Cursor, + NamedArguments, Nullable, + PositionalArguments, Program, - Reference, RuntimeOp, + VMArguments, } from '@glimmer/interfaces'; -import { IS_COMPILABLE_TEMPLATE } from '@glimmer/constants'; -import { exhausted } from '@glimmer/debug-util'; -import { REFERENCE } from '@glimmer/reference'; +import { dev, exhausted, getLocalDebugType } from '@glimmer/debug-util'; +import { isIndexable } from '@glimmer/util'; import type { DisassembledOperand } from '../debug'; import type { ValueRefOptions } from '../render/basic'; -import type { Fragment, IntoFragment } from '../render/fragment'; +import type { IntoFragment } from '../render/fragment'; import type { RegisterName, SomeDisassembledOperand } from './dism'; import { debugOp } from '../debug'; -import { empty, join, value } from '../render/basic'; +import { empty, join, unknownValue, value } from '../render/basic'; import { array } from '../render/combinators'; -import { as, frag } from '../render/fragment'; -import { describeRef } from '../render/ref'; +import { as, frag, Fragment } from '../render/fragment'; -export function describeOp(op: RuntimeOp, program: Program, meta: BlockMetadata): Fragment { +export function describeOp( + op: RuntimeOp, + program: Program, + meta: Nullable +): Fragment { const { name, params } = debugOp(program, op, meta)!; - const block = new SerializeBlockContext(meta.symbols); + const block = new SerializeBlockContext(meta?.symbols ?? null); let args: IntoFragment[] = Object.entries(params).map( ([p, v]) => frag`${as.attrName(p)}=${block.serialize(v)}` @@ -52,7 +58,7 @@ export class SerializeBlockContext { return array(param.value?.map((value) => this.#stringify(value, 'unknown')) ?? []); case 'dynamic': case 'constant': - return stackValue(param.value); + return value(param.value); case 'register': return this.#stringify(param.value, 'register'); case 'instruction': @@ -86,7 +92,7 @@ export class SerializeBlockContext { #stringify(value: number, type: 'constant'): string; #stringify(value: RegisterName, type: 'register'): string; #stringify(value: number, type: 'variable' | 'pc'): string; - #stringify(value: DisassembledOperand['value'], type: 'stringify' | 'unknown'): string; + #stringify(value: DisassembledOperand['value'], type: 'stringify' | 'unknown'): IntoFragment; #stringify( value: unknown, type: 'stringify' | 'constant' | 'register' | 'variable' | 'pc' | 'unknown' @@ -155,38 +161,144 @@ export class SerializeBlockContext { } } -export function stackValue(element: unknown, options?: ValueRefOptions): Fragment { - if (isReference(element)) { - return describeRef(element); - } else if (isCompilable(element)) { - const table = element.symbolTable; - - if ('parameters' in table) { - const blockParams = - table.parameters.length === 0 - ? empty() - : frag` as |${join( - table.parameters.map((s) => element.meta.symbols.lexical?.at(s - 1) ?? `?${s}`), - ' ' - )}|`; - return value(element, { - ref: 'block', - value: frag`<${as.kw('block')}${blockParams}>`, - }); +export function debugValue(item: unknown, options?: ValueRefOptions): Fragment { + if (isIndexable(item)) { + const classified = getLocalDebugType(item)!; + + if (classified) return describeValue(classified, options); + } + + return unknownValue(item, options); +} + +function describeValue(classified: ClassifiedLocalDebug, options?: ValueRefOptions): Fragment { + switch (classified.type) { + case 'args': + return describeArgs(classified.value); + + case 'args:positional': + return positionalArgs(classified.value); + + case 'args:named': + // return entries + return namedArgs(classified.value); + + case 'args:blocks': + return frag``; + + case 'cursor': + return describeCursor(classified.value); + + case 'block:simple': + case 'block:remote': + case 'block:resettable': + return describeBlock(classified.value, classified.type); + + case 'factory:helper': + return Fragment.special(classified.value); + + case 'syntax:source': + return describeSyntaxSource(classified); + + case 'syntax:symbol-table:program': + return describeProgramSymbolTable(classified); + + case 'syntax:mir:node': + return describeMirNode(classified); + } +} + +function describeArgs(args: VMArguments) { + const { positional, named, length } = args; + + if (length === 0) { + return frag`${as.type('args')} { ${as.dim('empty')} }`; + } else { + const posFrag = positional.length === 0 ? empty() : positionalArgs(positional); + const namedFrag = named.length === 0 ? empty() : namedArgs(named); + const argsFrag = join([posFrag, namedFrag], ' '); + + return frag`${as.type('args')} { ${argsFrag} }`; + } +} + +function positionalArgs(args: PositionalArguments) { + return join( + args.capture().map((item) => value(item)), + ' ' + ); +} + +function namedArgs(args: NamedArguments) { + return join( + Object.entries(args.capture()).map(([k, v]) => frag`${as.kw(k)}=${value(v)}`), + ' ' + ); +} + +function describeCursor(cursor: Cursor) { + const { element, nextSibling } = cursor; + + if (nextSibling) { + return frag`${as.type('cursor')} { ${as.kw('before')} ${Fragment.special(nextSibling)} }`; + } else { + return frag`${as.type('cursor')} { ${as.kw('append to')} ${Fragment.special(element)} }`; + } +} + +function describeBlock( + block: AppendingBlock, + type: 'block:simple' | 'block:remote' | 'block:resettable' +) { + const kind = type.split(':').at(1) as string; + + const debug = block.debug; + const first = debug?.first(); + const last = debug?.last(); + + if (first === last) { + if (first === null) { + return frag`${as.type('block bounds')} { ${as.kw(kind)} ${as.null('uninitialized')} }`; } else { - return frag` <${as.kw('template')} ${element.meta.moduleName ?? '(unknown module)'}>`; + return frag`${as.type('block bounds')} { ${as.kw(kind)} ${value(first)} }`; } } else { - return value(element, options); + return frag`${as.type('block bounds')} { ${as.kw(kind)} ${value(first)} .. ${value(last)} }`; } } -function isCompilable(element: unknown): element is CompilableTemplate { - return !!(element && typeof element === 'object' && IS_COMPILABLE_TEMPLATE in element); +function describeProgramSymbolTable( + classified: ClassifiedLocalDebugFor<'syntax:symbol-table:program'> +) { + const debug = dev(classified.options.debug); + + const hasDebugger = debug.hasDebugger + ? frag`(${as.kw('has debugger')})` + : frag`(${as.dim('no debugger')})`.subtle(); + const keywords = labelledList('keywords', debug.keywords); + const upvars = labelledList('upvars', debug.upvars); + const atNames = labelledList('@-names', Object.keys(debug.named)); + const blocks = labelledList('blocks', Object.keys(debug.blocks)); + + const fields = join([hasDebugger, keywords, atNames, upvars, blocks], ' '); + + const full = frag` ${value(debug, { ref: 'debug' })}`.subtle(); + + return frag`${as.kw('program')} ${as.type('symbol table')} { ${fields} }${full}`; +} + +function describeSyntaxSource(classified: ClassifiedLocalDebugFor<'syntax:source'>) { + return frag`${as.kw('source')} { ${value(classified.value.source)} }`; +} + +function describeMirNode(classified: ClassifiedLocalDebugFor<'syntax:mir:node'>) { + return frag`${as.type('mir')} { ${as.kw(classified.value.type)} }`; } -function isReference(element: unknown): element is Reference { - return !!(element && typeof element === 'object' && REFERENCE in element); +function labelledList(name: string, list: readonly unknown[]) { + return list.length === 0 + ? frag`(${as.dim('no')} ${as.dim(name)})`.subtle() + : frag`${as.attrName(name)}=${array(list.map((v) => value(v)))}`; } export type SerializableKey = { diff --git a/packages/@glimmer/debug/lib/render/basic.ts b/packages/@glimmer/debug/lib/render/basic.ts index ad4c5f76ca..68d75020c4 100644 --- a/packages/@glimmer/debug/lib/render/basic.ts +++ b/packages/@glimmer/debug/lib/render/basic.ts @@ -1,9 +1,14 @@ -import type { Optional } from '@glimmer/interfaces'; +import type { CompilableTemplate, Optional, Reference, SimpleNode } from '@glimmer/interfaces'; +import { IS_COMPILABLE_TEMPLATE } from '@glimmer/constants'; +import { REFERENCE } from '@glimmer/reference'; +import { isIndexable } from '@glimmer/util'; import type { IntoFragment } from './fragment'; import type { LeafFragment, ValueFragment } from './fragment-type'; -import { Fragment, intoFragment } from './fragment'; +import { debugValue } from '../dism/opcode'; +import { as, frag, Fragment, intoFragment } from '../render/fragment'; +import { describeRef } from '../render/ref'; export function empty(): LeafFragment { return new Fragment({ kind: 'string', value: '' }); @@ -36,7 +41,7 @@ export function join(frags: IntoFragment[], separator?: Optional): } output.push(...fragment.leaves()); - seenUnsubtle = !isSubtle; + seenUnsubtle ||= !isSubtle; seenAny = true; } @@ -45,7 +50,37 @@ export function join(frags: IntoFragment[], separator?: Optional): export type ValueRefOptions = { annotation: string } | { ref: string; value?: IntoFragment }; -export function value(val: unknown, options?: ValueRefOptions): LeafFragment { +export function value(item: unknown, options?: ValueRefOptions): Fragment { + if (typeof item === 'function' || Array.isArray(item)) { + return Fragment.special(item); + } else if (isReference(item)) { + return describeRef(item); + } else if (isCompilable(item)) { + const table = item.symbolTable; + + if ('parameters' in table) { + const blockParams = + table.parameters.length === 0 + ? empty() + : frag` as |${join( + table.parameters.map((s) => item.meta.symbols.lexical?.at(s - 1) ?? `?${s}`), + ' ' + )}|`; + return debugValue(item, { + ref: 'block', + value: frag`<${as.kw('block')}${blockParams}>`, + }); + } else { + return frag` <${as.kw('template')} ${item.meta.moduleName ?? '(unknown module)'}>`; + } + } else if (isDom(item)) { + return Fragment.special(item); + } + + return debugValue(item, options); +} + +export function unknownValue(val: unknown, options?: ValueRefOptions): LeafFragment { const normalize = (): ValueFragment['display'] => { if (options === undefined) return; @@ -69,3 +104,23 @@ export function value(val: unknown, options?: ValueRefOptions): LeafFragment { export function group(...frags: IntoFragment[]): Fragment { return new Fragment({ kind: 'multi', value: frags.flatMap((f) => intoFragment(f).leaves()) }); } + +function isCompilable(element: unknown): element is CompilableTemplate { + return !!(element && typeof element === 'object' && IS_COMPILABLE_TEMPLATE in element); +} + +function isReference(element: unknown): element is Reference { + return !!(element && typeof element === 'object' && REFERENCE in element); +} + +function isDom(element: unknown): element is Node | SimpleNode { + if (!isIndexable(element)) { + return false; + } + + if (typeof Node !== 'undefined') { + return element instanceof Node; + } else { + return 'nodeType' in element && typeof element.nodeType === 'number'; + } +} diff --git a/packages/@glimmer/debug/lib/render/combinators.ts b/packages/@glimmer/debug/lib/render/combinators.ts index a88c002c7d..edf4cc3e53 100644 --- a/packages/@glimmer/debug/lib/render/combinators.ts +++ b/packages/@glimmer/debug/lib/render/combinators.ts @@ -1,6 +1,6 @@ import type { Fragment, IntoFragment } from './fragment'; -import { group, join } from './basic'; +import { group, join, value } from './basic'; import { as, frag, intoFragment } from './fragment'; /** @@ -84,6 +84,10 @@ export function compactArray( } } +export function dictionary(entries: Iterable<[key: string, value: unknown]>) { + return frag`{ ${[...entries].map(([k, v]) => frag`${as.attrName(k)}=${value(v)}`)} }`; +} + export function array(items: IntoFragment[]): Fragment; export function array(items: T[] | readonly T[], options: EntriesOptions): Fragment; export function array( diff --git a/packages/@glimmer/debug/lib/render/fragment-type.ts b/packages/@glimmer/debug/lib/render/fragment-type.ts index 75fe531aa5..39250113fc 100644 --- a/packages/@glimmer/debug/lib/render/fragment-type.ts +++ b/packages/@glimmer/debug/lib/render/fragment-type.ts @@ -1,4 +1,4 @@ -import type { SimpleNode } from '@glimmer/interfaces'; +import type { AnyFn, SimpleNode } from '@glimmer/interfaces'; import type { Fragment } from './fragment'; @@ -7,7 +7,7 @@ export const FORMATTERS = { string: '%s', integer: '%d', float: '%f', - dom: '%o', + special: '%o', } as const; interface AbstractLeafFragment { @@ -80,9 +80,9 @@ export interface FloatFragment extends AbstractLeafFragment { * * Corresponds to the `%o` format specifier. */ -export interface DomFragment extends AbstractLeafFragment { - readonly kind: 'dom'; - readonly value: SimpleNode | Node; +export interface SpecialFragment extends AbstractLeafFragment { + readonly kind: 'special'; + readonly value: SimpleNode | Node | AnyFn | unknown[]; } /** @@ -94,7 +94,7 @@ export type LeafFragmentType = | IntegerFragment | FloatFragment | ValueFragment - | DomFragment; + | SpecialFragment; export type FragmentType = | LeafFragmentType diff --git a/packages/@glimmer/debug/lib/render/fragment.ts b/packages/@glimmer/debug/lib/render/fragment.ts index 3936127566..0cb4f9c681 100644 --- a/packages/@glimmer/debug/lib/render/fragment.ts +++ b/packages/@glimmer/debug/lib/render/fragment.ts @@ -1,14 +1,14 @@ -import type { SimpleNode } from '@glimmer/interfaces'; +import type { AnyFn, SimpleNode } from '@glimmer/interfaces'; import { assertNever } from '@glimmer/debug-util'; import type { Loggable } from './entry'; import type { IntoFormat } from './format'; import type { - DomFragment, FloatFragment, FragmentType, IntegerFragment, LeafFragment, + SpecialFragment, StringFragment, } from './fragment-type'; import type { DisplayFragmentOptions } from './logger'; @@ -51,12 +51,12 @@ export class Fragment { return new Fragment({ kind: 'string', value, ...options }); } - static dom( + static special( this: void, - value: Node | SimpleNode, - options?: Omit | undefined - ): Fragment { - return new Fragment({ kind: 'dom', value, ...options }); + value: Node | SimpleNode | AnyFn | unknown[], + options?: Omit | undefined + ): Fragment { + return new Fragment({ kind: 'special', value, ...options }); } readonly #type: T; @@ -295,7 +295,7 @@ export class Fragment { break; // the remaining value types are represented as footnotes // dom nodes are appended to the footnote line using %o - case 'dom': + case 'special': // values are appended to the footnote line using %O case 'value': { // If a fragment has an associated annotation, we'll use the annotation as the diff --git a/packages/@glimmer/debug/lib/render/ref.ts b/packages/@glimmer/debug/lib/render/ref.ts index 94d32dca29..54dec98ca6 100644 --- a/packages/@glimmer/debug/lib/render/ref.ts +++ b/packages/@glimmer/debug/lib/render/ref.ts @@ -9,7 +9,7 @@ import { as, frag } from './fragment'; export function describeRef(ref: Reference): Fragment { const debug = ref.debugLabel; - const label = as.type(debug ?? ''); + const label = as.type(debug || ''); const result = valueForRef(ref); return frag`<${as.kw('ref')} ${join([label, value(result)], ' ')}>`; diff --git a/packages/@glimmer/debug/lib/render/styles.ts b/packages/@glimmer/debug/lib/render/styles.ts index bc49919323..4993f69a86 100644 --- a/packages/@glimmer/debug/lib/render/styles.ts +++ b/packages/@glimmer/debug/lib/render/styles.ts @@ -24,11 +24,11 @@ export const STYLES = { meta: 'color: grey', register: 'color: purple', constant: 'color: purple', - dim: 'color: lightgrey', + dim: 'color: grey', internals: 'color: lightgrey; font-style: italic', diffAdd: 'color: Highlight', - diffDelete: 'color: SelectedItem', + diffDelete: 'color: SelectedItemText; background-color: SelectedItem', diffChange: 'color: MarkText; background-color: Mark', sublabel: 'font-style: italic; color: grey', diff --git a/packages/@glimmer/debug/lib/vm/snapshot.ts b/packages/@glimmer/debug/lib/vm/snapshot.ts index 7920c7a4f8..d7f5a53ee1 100644 --- a/packages/@glimmer/debug/lib/vm/snapshot.ts +++ b/packages/@glimmer/debug/lib/vm/snapshot.ts @@ -16,7 +16,7 @@ import { $fp, $pc } from '@glimmer/vm'; import type { Fragment } from '../render/fragment'; import { decodeRegister } from '../debug'; -import { stackValue } from '../dism/opcode'; +import { value } from '../render/basic'; import { array } from '../render/combinators'; import { as, frag } from '../render/fragment'; @@ -50,12 +50,12 @@ 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 stack: VmSnapshotArrayDiff<'stack', unknown[]>; + readonly blocks: VmSnapshotArrayDiff<'blocks', object[]>; + readonly cursors: VmSnapshotArrayDiff<'cursors', Cursor[]>; readonly constructing: VmSnapshotValueDiff<'constructing', Nullable>; - readonly destructors: VSnapshotArrayDiff<'destructors', object[]>; - readonly scope: VSnapshotArrayDiff<'scope', ScopeSlot[]>; + readonly destructors: VmSnapshotArrayDiff<'destructors', object[]>; + readonly scope: VmSnapshotArrayDiff<'scope', ScopeSlot[]>; constructor(opcode: RuntimeOpSnapshot, before: DebugVmSnapshot, after: DebugVmSnapshot) { this.opcode = opcode; @@ -74,14 +74,14 @@ export class VmDiff { this.registers = registers as unknown as RegisterDiffs; const frameChange = this.registers[$fp].didChange; - this.stack = new VSnapshotArrayDiff( + this.stack = new VmSnapshotArrayDiff( 'stack', before.stack, after.stack, frameChange ? 'reset' : undefined ); - this.blocks = new VSnapshotArrayDiff('blocks', before.elements.blocks, after.elements.blocks); + this.blocks = new VmSnapshotArrayDiff('blocks', before.elements.blocks, after.elements.blocks); this.constructing = new VmSnapshotValueDiff( 'constructing', @@ -89,23 +89,23 @@ export class VmDiff { after.elements.constructing ); - this.cursors = new VSnapshotArrayDiff( + this.cursors = new VmSnapshotArrayDiff( 'cursors', before.elements.cursors, after.elements.cursors ); - this.destructors = new VSnapshotArrayDiff( + this.destructors = new VmSnapshotArrayDiff( 'destructors', before.stacks.destroyable, after.stacks.destroyable ); - this.scope = new VSnapshotArrayDiff('scope', before.scope, after.scope); + this.scope = new VmSnapshotArrayDiff('scope', before.scope, after.scope); } } -export class VSnapshotArrayDiff { +export class VmSnapshotArrayDiff { readonly name: N; readonly before: T; readonly after: T; @@ -125,7 +125,7 @@ export class VSnapshotArrayDiff if (this.change === 'reset') { return frag`${as.kw(this.name)}: ${as.dim('reset to')} ${array( - this.after.map((v) => stackValue(v)) + this.after.map((v) => value(v)) )}`; } @@ -136,12 +136,12 @@ export class VSnapshotArrayDiff 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()); + fragments.push(value(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')); + fragments.push(value(before, { ref: `${i}` }).styleAll('dim')); } else { fragments.push(as.dim(``)); } @@ -157,9 +157,9 @@ export class VSnapshotArrayDiff let pre: Fragment; if (op === 'pop') { - pre = frag`${stackValue(before, { ref: `${i}:popped` })} -> `; + pre = frag`${value(before, { ref: `${i}:popped` })} -> `; } else if (op === 'retain') { - pre = frag`${stackValue(before, { ref: `${i}:before` })} -> `; + pre = frag`${value(before, { ref: `${i}:before` })} -> `; } else if (op === 'push') { pre = frag`push -> `.subtle(); } else { @@ -169,9 +169,9 @@ export class VSnapshotArrayDiff let post: Fragment; if (op === 'push') { - post = stackValue(after, { ref: `${i}:push` }); + post = value(after, { ref: `${i}:push` }); } else if (op === 'retain') { - post = stackValue(after, { ref: `${i}:after` }); + post = value(after, { ref: `${i}:after` }); } else if (op === 'pop') { post = frag`${as.diffDelete('')}`; } else { @@ -201,10 +201,10 @@ export class VmSnapshotValueDiff { describe(): Fragment { if (!this.didChange) { - return frag`${as.register(this.name)}: ${stackValue(this.after)}`.subtle(); + return frag`${as.register(this.name)}: ${value(this.after)}`.subtle(); } - return frag`${as.register(this.name)}: ${stackValue(this.before)} -> ${stackValue(this.after)}`; + return frag`${as.register(this.name)}: ${value(this.before)} -> ${value(this.after)}`; } } diff --git a/packages/@glimmer/interfaces/lib/core.d.ts b/packages/@glimmer/interfaces/lib/core.d.ts index cd3eaca9f8..badb6e3447 100644 --- a/packages/@glimmer/interfaces/lib/core.d.ts +++ b/packages/@glimmer/interfaces/lib/core.d.ts @@ -14,6 +14,8 @@ export interface Unique { export type Recast = (T & U) | U; +export type AnyFn = Function; + /** * This is needed because the normal IteratorResult in the TypeScript * standard library is generic over the value in each tick and not over diff --git a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts index 36656854b7..578159f167 100644 --- a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts +++ b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts @@ -18,6 +18,8 @@ import type { * or `ResettableBlock` during the updating phase. */ export interface AppendingBlock extends Bounds { + debug?: { first: () => Nullable; last: () => Nullable }; + openElement(element: SimpleElement): void; closeElement(): void; didAppendNode(node: SimpleNode): void; diff --git a/packages/@glimmer/interfaces/lib/references.d.ts b/packages/@glimmer/interfaces/lib/references.d.ts index c25708f21c..9bb1415eb5 100644 --- a/packages/@glimmer/interfaces/lib/references.d.ts +++ b/packages/@glimmer/interfaces/lib/references.d.ts @@ -23,7 +23,7 @@ export type ReferenceSymbol = typeof REFERENCE; export interface Reference { [REFERENCE]: ReferenceType; - debugLabel?: string | undefined; + debugLabel?: string | false | undefined; compute: Nullable<() => T>; children: null | Map; } diff --git a/packages/@glimmer/reference/lib/iterable.ts b/packages/@glimmer/reference/lib/iterable.ts index a23681bf31..d76d3e35c7 100644 --- a/packages/@glimmer/reference/lib/iterable.ts +++ b/packages/@glimmer/reference/lib/iterable.ts @@ -1,6 +1,6 @@ import type { Dict, Nullable } from '@glimmer/interfaces'; import { getPath, toIterator } from '@glimmer/global-context'; -import { EMPTY_ARRAY, isObject } from '@glimmer/util'; +import { EMPTY_ARRAY, isIndexable } from '@glimmer/util'; import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; import type { Reference, ReferenceEnvironment } from './reference'; @@ -88,7 +88,7 @@ class WeakMapWithPrimitives { } set(key: unknown, value: T) { - if (isObject(key)) { + if (isIndexable(key)) { this.weakMap.set(key, value); } else { this.primitiveMap.set(key, value); @@ -96,7 +96,7 @@ class WeakMapWithPrimitives { } get(key: unknown): T | undefined { - if (isObject(key)) { + if (isIndexable(key)) { return this.weakMap.get(key); } else { return this.primitiveMap.get(key); diff --git a/packages/@glimmer/runtime/lib/bounds.ts b/packages/@glimmer/runtime/lib/bounds.ts index 862a0fab2a..9b8e635e3e 100644 --- a/packages/@glimmer/runtime/lib/bounds.ts +++ b/packages/@glimmer/runtime/lib/bounds.ts @@ -1,11 +1,13 @@ import type { Bounds, Cursor, Nullable, SimpleElement, SimpleNode } from '@glimmer/interfaces'; -import { expect } from '@glimmer/debug-util'; +import { expect, setLocalDebugType } from '@glimmer/debug-util'; export class CursorImpl implements Cursor { constructor( public element: SimpleElement, public nextSibling: Nullable - ) {} + ) { + setLocalDebugType('cursor', this); + } } export type DestroyableBounds = Bounds; diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts index a13fbbe353..9b69b0b3f7 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts @@ -18,7 +18,7 @@ import { } from '@glimmer/debug'; import { hasInternalComponentManager, hasInternalHelperManager } from '@glimmer/manager'; import { isConstRef, valueForRef } from '@glimmer/reference'; -import { isObject } from '@glimmer/util'; +import { isIndexable } from '@glimmer/util'; import { ContentType } from '@glimmer/vm'; import { isCurriedType } from '../../curried-value'; @@ -50,7 +50,7 @@ function toContentType(value: unknown) { } function toDynamicContentType(value: unknown) { - if (!isObject(value)) { + if (!isIndexable(value)) { return ContentType.String; } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index d3beb752e2..b2a56e23a5 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -38,7 +38,7 @@ import { debugToString, expect } from '@glimmer/debug-util'; import { associateDestroyableChild, destroy, registerDestructor } from '@glimmer/destroyable'; import { getInternalModifierManager } from '@glimmer/manager'; import { createComputeRef, isConstRef, valueForRef } from '@glimmer/reference'; -import { isObject } from '@glimmer/util'; +import { isIndexable } from '@glimmer/util'; import { consumeTag, CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator'; import { $t0 } from '@glimmer/vm'; @@ -205,7 +205,7 @@ APPEND_OPCODES.add(VM_DYNAMIC_MODIFIER_OP, (vm) => { let value = valueForRef(ref); let owner: Owner; - if (!isObject(value)) { + if (!isIndexable(value)) { return; } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts index e6e9b249c6..6a57bd81a3 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts @@ -47,7 +47,7 @@ import { UNDEFINED_REFERENCE, valueForRef, } from '@glimmer/reference'; -import { assign, isObject } from '@glimmer/util'; +import { assign, isIndexable } from '@glimmer/util'; import { $v0 } from '@glimmer/vm'; import { isCurriedType, resolveCurriedValue } from '../../curried-value'; @@ -119,7 +119,7 @@ APPEND_OPCODES.add(VM_DYNAMIC_HELPER_OP, (vm) => { helperRef = helper(args, owner); associateDestroyableChild(helperInstanceRef, helperRef); - } else if (isObject(definition)) { + } else if (isIndexable(definition)) { let helper = resolveHelper(definition, ref); helperRef = helper(args, initialOwner); diff --git a/packages/@glimmer/runtime/lib/opcodes.ts b/packages/@glimmer/runtime/lib/opcodes.ts index c6fe059047..0dfcd22d12 100644 --- a/packages/@glimmer/runtime/lib/opcodes.ts +++ b/packages/@glimmer/runtime/lib/opcodes.ts @@ -14,7 +14,9 @@ import { VM_SYSCALL_SIZE } from '@glimmer/constants'; import { DebugLogger, debugOp, + describeOp, describeOpcode, + frag, opcodeMetadata, recordStackSize, VmSnapshot, @@ -87,7 +89,9 @@ export class AppendOpcodes { op = debugOp(debug.context.program, opcode, debug.template); - closeGroup = logger.group(`${pos}. ${describeOpcode(op.name, op.params)}`).expanded(); + closeGroup = logger + .group(frag`${pos}. ${describeOp(opcode, debug.context.program, debug.template)}`) + .expanded(); let debugParams = []; for (let [name, param] of Object.entries(op.params)) { @@ -150,10 +154,13 @@ export class AppendOpcodes { 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(); + try { + logger.log(diff.constructing.describe()); + logger.log(diff.blocks.describe()); + logger.log(diff.cursors.describe()); + } finally { + done(); + } } pre.closeGroup?.(); diff --git a/packages/@glimmer/runtime/lib/references/curry-value.ts b/packages/@glimmer/runtime/lib/references/curry-value.ts index dec57c39c1..e2faa22d47 100644 --- a/packages/@glimmer/runtime/lib/references/curry-value.ts +++ b/packages/@glimmer/runtime/lib/references/curry-value.ts @@ -11,7 +11,7 @@ import type { Reference } from '@glimmer/reference'; import { CURRIED_COMPONENT } from '@glimmer/constants'; import { expect } from '@glimmer/debug-util'; import { createComputeRef, valueForRef } from '@glimmer/reference'; -import { isObject } from '@glimmer/util'; +import { isIndexable } from '@glimmer/util'; import { curry, isCurriedType } from '../curried-value'; @@ -58,7 +58,7 @@ export default function createCurryRef( } curriedDefinition = curry(type, value, owner, args); - } else if (isObject(value)) { + } else if (isIndexable(value)) { curriedDefinition = curry(type, value, owner, args); } else { curriedDefinition = null; diff --git a/packages/@glimmer/runtime/lib/vm/arguments.ts b/packages/@glimmer/runtime/lib/vm/arguments.ts index f576650cf0..1c3b7e48bb 100644 --- a/packages/@glimmer/runtime/lib/vm/arguments.ts +++ b/packages/@glimmer/runtime/lib/vm/arguments.ts @@ -19,7 +19,7 @@ import type { import type { Reference } from '@glimmer/reference'; import type { Tag } from '@glimmer/validator'; import { check, CheckBlockSymbolTable, CheckHandle, CheckNullable, CheckOr } from '@glimmer/debug'; -import { unwrap } from '@glimmer/debug-util'; +import { setLocalDebugType, unwrap } from '@glimmer/debug-util'; import { createDebugAliasRef, UNDEFINED_REFERENCE, valueForRef } from '@glimmer/reference'; import { dict, EMPTY_STRING_ARRAY, emptyArray, enumerate } from '@glimmer/util'; import { CONSTANT_TAG } from '@glimmer/validator'; @@ -43,6 +43,10 @@ export class VMArgumentsImpl implements VMArguments { public named = new NamedArgumentsImpl(); public blocks = new BlockArgumentsImpl(); + constructor() { + setLocalDebugType('args', this); + } + empty(stack: EvaluationStack): this { let base = stack.registers[$sp] + 1; @@ -141,6 +145,10 @@ export class PositionalArgumentsImpl implements PositionalArguments { private _references: Nullable = null; + constructor() { + setLocalDebugType('args:positional', this); + } + empty(stack: EvaluationStack, base: number) { this.stack = stack; this.base = base; @@ -215,6 +223,10 @@ export class NamedArgumentsImpl implements NamedArguments { private _names: Nullable = EMPTY_STRING_ARRAY; private _atNames: Nullable = EMPTY_STRING_ARRAY; + constructor() { + setLocalDebugType('args:named', this); + } + empty(stack: EvaluationStack, base: number) { this.stack = stack; this.base = base; @@ -372,6 +384,10 @@ export class BlockArgumentsImpl implements BlockArguments { public length = 0; public base = 0; + constructor() { + setLocalDebugType('args:blocks', this); + } + empty(stack: EvaluationStack, base: number) { this.stack = stack; this.names = EMPTY_STRING_ARRAY; diff --git a/packages/@glimmer/runtime/lib/vm/element-builder.ts b/packages/@glimmer/runtime/lib/vm/element-builder.ts index d1d6750168..fa749cf571 100644 --- a/packages/@glimmer/runtime/lib/vm/element-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/element-builder.ts @@ -18,7 +18,7 @@ import type { SimpleNode, SimpleText, } from '@glimmer/interfaces'; -import { assert, expect } from '@glimmer/debug-util'; +import { assert, expect, setLocalDebugType } from '@glimmer/debug-util'; import { destroy, registerDestructor } from '@glimmer/destroyable'; import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { Stack } from '@glimmer/util'; @@ -29,10 +29,12 @@ import { clear, ConcreteBounds, CursorImpl } from '../bounds'; import { dynamicAttribute } from './attributes/dynamic'; export interface FirstNode { + debug?: { first: () => Nullable }; firstNode(): SimpleNode; } export interface LastNode { + debug?: { last: () => Nullable }; lastNode(): SimpleNode; } @@ -386,11 +388,22 @@ export class NewElementBuilder implements ElementBuilder { } export class SimpleLiveBlock implements AppendingBlock { + declare debug?: { first: () => Nullable; last: () => Nullable }; + protected first: Nullable = null; protected last: Nullable = null; protected nesting = 0; - constructor(private parent: SimpleElement) {} + constructor(private parent: SimpleElement) { + setLocalDebugType('block:simple', this); + + if (LOCAL_DEBUG) { + this.debug = { + first: () => this.first?.debug?.first() ?? null, + last: () => this.last?.debug?.last() ?? null, + }; + } + } parentElement() { return this.parent; @@ -454,6 +467,8 @@ export class RemoteLiveBlock extends SimpleLiveBlock { constructor(parent: SimpleElement) { super(parent); + setLocalDebugType('block:remote', this); + registerDestructor(this, () => { // In general, you only need to clear the root of a hierarchy, and should never // need to clear any child nodes. This is an important constraint that gives us @@ -487,6 +502,11 @@ export class RemoteLiveBlock extends SimpleLiveBlock { } export class ResettableBlockImpl extends SimpleLiveBlock implements ResettableBlock { + constructor(parent: SimpleElement) { + super(parent); + setLocalDebugType('block:resettable', this); + } + reset(): Nullable { destroy(this); let nextSibling = clear(this); diff --git a/packages/@glimmer/syntax/lib/source/source.ts b/packages/@glimmer/syntax/lib/source/source.ts index 03b487c240..4f390aee60 100644 --- a/packages/@glimmer/syntax/lib/source/source.ts +++ b/packages/@glimmer/syntax/lib/source/source.ts @@ -1,5 +1,5 @@ import type { Nullable } from '@glimmer/interfaces'; -import { assert } from '@glimmer/debug-util'; +import { assert, setLocalDebugType } from '@glimmer/debug-util'; import type { PrecompileOptions } from '../parser/tokenizer-event-handlers'; import type { SourceLocation, SourcePosition } from './location'; @@ -14,7 +14,9 @@ export class Source { constructor( readonly source: string, readonly module = 'an unknown module' - ) {} + ) { + setLocalDebugType('syntax:source', this); + } /** * Validate that the character offset represents a position in the source string. diff --git a/packages/@glimmer/syntax/lib/symbol-table.ts b/packages/@glimmer/syntax/lib/symbol-table.ts index bf878b6d11..286f8c0258 100644 --- a/packages/@glimmer/syntax/lib/symbol-table.ts +++ b/packages/@glimmer/syntax/lib/symbol-table.ts @@ -1,5 +1,5 @@ import type { Core, Dict } from '@glimmer/interfaces'; -import { unwrap } from '@glimmer/debug-util'; +import { setLocalDebugType, unwrap } from '@glimmer/debug-util'; import { dict } from '@glimmer/util'; import { SexpOpcodes } from '@glimmer/wire-format'; @@ -54,6 +54,18 @@ export class ProgramSymbolTable extends SymbolTable { private options: SymbolTableOptions ) { super(); + + setLocalDebugType('syntax:symbol-table:program', this, { + debug: () => ({ + templateLocals: this.templateLocals, + keywords: this.keywords, + symbols: this.symbols, + upvars: this.upvars, + named: this.named, + blocks: this.blocks, + hasDebugger: this.hasDebugger, + }), + }); } public symbols: string[] = []; diff --git a/packages/@glimmer/syntax/lib/v2/objects/node.ts b/packages/@glimmer/syntax/lib/v2/objects/node.ts index c7f4db141c..b5596015b4 100644 --- a/packages/@glimmer/syntax/lib/v2/objects/node.ts +++ b/packages/@glimmer/syntax/lib/v2/objects/node.ts @@ -1,3 +1,4 @@ +import { setLocalDebugType } from '@glimmer/debug-util'; import { assign } from '@glimmer/util'; import type { SourceSpan } from '../../source/span'; @@ -63,6 +64,8 @@ export function node( constructor(fields: BaseNodeFields & Fields) { this.type = type; assign(this, fields); + + setLocalDebugType('syntax:mir:node', this); } } as TypedNodeConstructor; }, @@ -87,7 +90,9 @@ export interface NodeConstructor { new (fields: Fields): Readonly; } -type TypedNode = { type: T } & Readonly; +type TypedNode = { + type: T; +} & Readonly; export interface TypedNodeConstructor { new (options: Fields): TypedNode; diff --git a/packages/@glimmer/util/index.ts b/packages/@glimmer/util/index.ts index 0910cdb718..e76dbf936d 100644 --- a/packages/@glimmer/util/index.ts +++ b/packages/@glimmer/util/index.ts @@ -1,5 +1,5 @@ export * from './lib/array-utils'; -export { dict, isDict, isObject, StackImpl as Stack } from './lib/collections'; +export { dict, isDict, isIndexable, StackImpl as Stack } from './lib/collections'; export { beginTestSteps, endTestSteps, logStep, verifySteps } from './lib/debug-steps'; export * from './lib/dom'; export { default as intern } from './lib/intern'; diff --git a/packages/@glimmer/util/lib/collections.ts b/packages/@glimmer/util/lib/collections.ts index ff53ef1857..c736372a8f 100644 --- a/packages/@glimmer/util/lib/collections.ts +++ b/packages/@glimmer/util/lib/collections.ts @@ -9,7 +9,7 @@ export function isDict(u: T): u is Dict & T { return u !== null && u !== undefined; } -export function isObject(u: T): u is object & T { +export function isIndexable(u: T): u is object & T { return typeof u === 'function' || (typeof u === 'object' && u !== null); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4172ac93e1..46ef522218 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,7 +3,7 @@ lockfileVersion: '6.0' overrides: '@rollup/pluginutils': ^5.0.2 '@types/node': ^20.9.4 - typescript: ^5.0.4 + typescript: ~5.0.4 importers: @@ -208,7 +208,7 @@ importers: specifier: ^1.9.3 version: 1.9.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 vite: specifier: ^5.4.10 @@ -410,7 +410,7 @@ importers: specifier: ^20.9.4 version: 20.9.4 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer-workspace/eslint-plugin: @@ -599,6 +599,9 @@ importers: '@glimmer/constants': specifier: workspace:* version: link:../constants + '@glimmer/debug': + specifier: workspace:* + version: link:../debug '@glimmer/debug-util': specifier: workspace:* version: link:../debug-util @@ -618,7 +621,7 @@ importers: specifier: ^4.24.3 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/compiler/test: @@ -671,7 +674,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/constants/test: @@ -733,7 +736,7 @@ importers: specifier: ^3.0.0 version: 3.0.0 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/debug-util: @@ -764,7 +767,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/debug-util/test: @@ -817,7 +820,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/destroyable/test: @@ -858,7 +861,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/global-context: @@ -876,7 +879,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/interfaces: @@ -898,7 +901,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/local-debug-babel-plugin: {} @@ -958,7 +961,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/manager/test: @@ -1013,7 +1016,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/opcode-compiler: @@ -1071,7 +1074,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/owner: @@ -1096,7 +1099,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/owner/test: @@ -1154,7 +1157,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/program/test: @@ -1200,7 +1203,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/reference/test: @@ -1292,7 +1295,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/syntax: @@ -1335,7 +1338,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/syntax/test: @@ -1394,7 +1397,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/util/test: @@ -1437,7 +1440,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/validator/test: @@ -1474,7 +1477,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/vm-babel-plugins: @@ -1502,7 +1505,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@glimmer/wire-format: @@ -1527,7 +1530,7 @@ importers: specifier: ^4.5.1 version: 4.24.3 typescript: - specifier: ^5.0.4 + specifier: ~5.0.4 version: 5.0.4 packages/@types/js-reporters: {}