From 35817f76252fcc37703a2ce4a7dd0db152c4c886 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:32:34 -0500 Subject: [PATCH] Various improvements, squashed for convenience Move curried types to constants Improvements to the local debug output Further clean up metadata and fragment infra Improve symbols - Test `@names` in the debugger - Consolidate symbol names in multiple places - Rename "eval" names (a holdover from the partial days) to "debug" names. Future work could include further consolidating symbol names to rationalize lexical scope so that lexical names work in the debugger. Trace log cleanup This commit is mostly focused on changing the internals so that we can create complete trace logs without having to pass in the entire VM. Mostly finished implementing improved logging. More trace log improvements Fix lints --- .prettierignore | 3 +- .vscode/settings.json | 10 +- benchmark/benchmarks/krausest/tsconfig.json | 2 - bin/setup-bench.mjs | 79 +- package.json | 2 +- .../lib/benchmark/create-registry.ts | 29 +- .../lib/benchmark/render-benchmark.ts | 2 + .../integration-tests/lib/helpers.ts | 11 +- .../lib/modes/jit/delegate.ts | 4 +- .../lib/modes/rehydration/builder.ts | 8 +- .../lib/modes/rehydration/delegate.ts | 8 +- .../partial-rehydration-delegate.ts | 8 +- .../integration-tests/lib/suites/debugger.ts | 40 +- .../test/chaos-rehydration-test.ts | 4 +- .../test/helpers/hash-test.ts | 4 +- .../integration-tests/test/jit-suites-test.ts | 2 +- .../test/keywords/log-test.ts | 6 +- .../lib/passes/1-normalization/index.ts | 21 +- .../passes/1-normalization/keywords/README.md | 2 +- .../compiler/lib/passes/2-encoding/index.ts | 2 +- .../@glimmer/compiler/lib/wire-encoding.md | 6 +- .../compiler/lib/wire-format-debug.ts | 2 +- packages/@glimmer/compiler/package.json | 1 + packages/@glimmer/constants/index.ts | 1 + packages/@glimmer/constants/lib/brand.ts | 2 + packages/@glimmer/debug-util/index.ts | 4 +- .../@glimmer/debug-util/lib/debug-brand.ts | 118 +++ .../@glimmer/debug-util/lib/platform-utils.ts | 49 +- packages/@glimmer/debug/index.ts | 12 +- packages/@glimmer/debug/lib/debug.ts | 481 ++++++++--- packages/@glimmer/debug/lib/dism/dism.ts | 80 ++ packages/@glimmer/debug/lib/dism/opcode.ts | 306 +++++++ .../@glimmer/debug/lib/dism/operand-types.ts | 74 ++ packages/@glimmer/debug/lib/dism/operands.ts | 117 +++ packages/@glimmer/debug/lib/metadata.ts | 47 +- .../@glimmer/debug/lib/opcode-metadata.ts | 782 ++---------------- .../@glimmer/debug/lib/render/annotations.ts | 9 + packages/@glimmer/debug/lib/render/basic.ts | 126 +++ packages/@glimmer/debug/lib/render/buffer.ts | 167 ++++ .../@glimmer/debug/lib/render/combinators.ts | 111 +++ packages/@glimmer/debug/lib/render/entry.ts | 30 + packages/@glimmer/debug/lib/render/format.ts | 18 + .../debug/lib/render/fragment-type.ts | 106 +++ .../@glimmer/debug/lib/render/fragment.md | 27 + .../@glimmer/debug/lib/render/fragment.ts | 409 +++++++++ packages/@glimmer/debug/lib/render/logger.ts | 109 +++ packages/@glimmer/debug/lib/render/ref.ts | 16 + packages/@glimmer/debug/lib/render/styles.ts | 55 ++ packages/@glimmer/debug/lib/stack-check.ts | 6 +- packages/@glimmer/debug/lib/vm/snapshot.ts | 213 +++++ packages/@glimmer/debug/package.json | 3 +- packages/@glimmer/interfaces/index.d.ts | 47 +- .../lib/compile/wire-format/api.d.ts | 8 +- packages/@glimmer/interfaces/lib/core.d.ts | 2 + .../interfaces/lib/dom/attributes.d.ts | 17 +- .../lib/managers/internal/component.d.ts | 2 +- packages/@glimmer/interfaces/lib/program.d.ts | 18 +- .../@glimmer/interfaces/lib/references.d.ts | 2 +- packages/@glimmer/interfaces/lib/runtime.d.ts | 1 - .../interfaces/lib/runtime/environment.d.ts | 2 +- .../interfaces/lib/runtime/local-debug.d.ts | 64 ++ .../interfaces/lib/runtime/scope.d.ts | 13 +- .../interfaces/lib/runtime/vm-state.d.ts | 41 + .../@glimmer/interfaces/lib/runtime/vm.d.ts | 20 +- packages/@glimmer/interfaces/lib/stack.d.ts | 5 + .../@glimmer/interfaces/lib/template.d.ts | 13 +- .../interfaces/lib/tier1/symbol-table.d.ts | 2 +- packages/@glimmer/interfaces/tsconfig.json | 2 +- packages/@glimmer/local-debug-flags/index.ts | 6 +- .../@glimmer/node/lib/serialize-builder.ts | 4 +- .../lib/compilable-template.ts | 11 +- .../@glimmer/opcode-compiler/lib/compiler.ts | 4 +- .../lib/opcode-builder/encoder.ts | 11 +- .../lib/opcode-builder/helpers/components.ts | 7 +- .../lib/opcode-builder/helpers/resolution.ts | 75 +- .../lib/opcode-builder/helpers/shared.ts | 19 +- .../lib/opcode-builder/helpers/stdlib.ts | 7 +- .../opcode-compiler/lib/wrapped-component.ts | 6 +- packages/@glimmer/program/lib/constants.ts | 4 + packages/@glimmer/program/lib/program.ts | 6 +- packages/@glimmer/reference/lib/iterable.ts | 6 +- packages/@glimmer/runtime/index.ts | 8 +- packages/@glimmer/runtime/lib/bounds.ts | 6 +- .../runtime/lib/compiled/opcodes/component.ts | 8 +- .../runtime/lib/compiled/opcodes/content.ts | 5 +- .../runtime/lib/compiled/opcodes/debugger.ts | 8 +- .../runtime/lib/compiled/opcodes/dom.ts | 6 +- .../lib/compiled/opcodes/expressions.ts | 14 +- packages/@glimmer/runtime/lib/environment.ts | 10 +- packages/@glimmer/runtime/lib/opcodes.ts | 261 +++--- .../runtime/lib/references/curry-value.ts | 4 +- packages/@glimmer/runtime/lib/render.ts | 26 +- packages/@glimmer/runtime/lib/scope.ts | 28 +- packages/@glimmer/runtime/lib/vm/append.ts | 511 ++++++++---- packages/@glimmer/runtime/lib/vm/arguments.ts | 18 +- .../runtime/lib/vm/element-builder.ts | 96 ++- packages/@glimmer/runtime/lib/vm/low-level.ts | 7 +- .../runtime/lib/vm/rehydrate-builder.ts | 24 +- packages/@glimmer/runtime/lib/vm/stack.ts | 18 +- packages/@glimmer/runtime/lib/vm/update.ts | 15 +- packages/@glimmer/syntax/lib/source/source.ts | 6 +- packages/@glimmer/syntax/lib/symbol-table.ts | 16 +- packages/@glimmer/syntax/lib/v2/README.md | 50 +- .../@glimmer/syntax/lib/v2/objects/node.ts | 7 +- packages/@glimmer/util/index.ts | 2 +- packages/@glimmer/util/lib/array-utils.ts | 34 + packages/@glimmer/util/lib/collections.ts | 6 +- packages/@glimmer/vm-babel-plugins/README.md | 2 +- packages/@glimmer/vm/lib/opcodes.toml | 717 ---------------- pnpm-lock.yaml | 54 +- tsconfig.json | 28 +- turbo.json | 2 +- 112 files changed, 3851 insertions(+), 2305 deletions(-) create mode 100644 packages/@glimmer/constants/lib/brand.ts create mode 100644 packages/@glimmer/debug-util/lib/debug-brand.ts create mode 100644 packages/@glimmer/debug/lib/dism/dism.ts create mode 100644 packages/@glimmer/debug/lib/dism/opcode.ts create mode 100644 packages/@glimmer/debug/lib/dism/operand-types.ts create mode 100644 packages/@glimmer/debug/lib/dism/operands.ts create mode 100644 packages/@glimmer/debug/lib/render/annotations.ts create mode 100644 packages/@glimmer/debug/lib/render/basic.ts create mode 100644 packages/@glimmer/debug/lib/render/buffer.ts create mode 100644 packages/@glimmer/debug/lib/render/combinators.ts create mode 100644 packages/@glimmer/debug/lib/render/entry.ts create mode 100644 packages/@glimmer/debug/lib/render/format.ts create mode 100644 packages/@glimmer/debug/lib/render/fragment-type.ts create mode 100644 packages/@glimmer/debug/lib/render/fragment.md create mode 100644 packages/@glimmer/debug/lib/render/fragment.ts create mode 100644 packages/@glimmer/debug/lib/render/logger.ts create mode 100644 packages/@glimmer/debug/lib/render/ref.ts create mode 100644 packages/@glimmer/debug/lib/render/styles.ts create mode 100644 packages/@glimmer/debug/lib/vm/snapshot.ts delete mode 100644 packages/@glimmer/vm/lib/opcodes.toml diff --git a/.prettierignore b/.prettierignore index 7d146f00a4..d129f1cfe1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,13 +1,12 @@ - node_modules/ # output directories dist/ ts-dist/ - # We don't need prettier here *.md +!packages/**/*.md *.yaml *.yml guides/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 01f215ef8a..6004717a8a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,7 +31,10 @@ "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, @@ -46,7 +49,7 @@ "typescript.preferences.importModuleSpecifierEnding": "auto", "typescript.preferences.useAliasesForRenames": false, "typescript.tsdk": "node_modules/typescript/lib", - "typescript.tsserver.experimental.enableProjectDiagnostics": false, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, "typescript.workspaceSymbols.scope": "currentProject", "typescript.experimental.updateImportsOnPaste": true, "eslint.problems.shortenToSingleLine": true, @@ -226,5 +229,6 @@ "rewrap.onSave": false, "rewrap.autoWrap.enabled": true, "rewrap.reformat": true, - "rewrap.wholeComment": false + "rewrap.wholeComment": false, + "explorer.excludeGitIgnore": true } diff --git a/benchmark/benchmarks/krausest/tsconfig.json b/benchmark/benchmarks/krausest/tsconfig.json index e0b2a52cb4..fbf010f78d 100644 --- a/benchmark/benchmarks/krausest/tsconfig.json +++ b/benchmark/benchmarks/krausest/tsconfig.json @@ -5,13 +5,11 @@ "baseUrl": ".", "allowJs": true, "checkJs": true, - "target": "es2020", "module": "esnext", "moduleResolution": "bundler", "verbatimModuleSyntax": true, "noErrorTruncation": true, - "suppressImplicitAnyIndexErrors": false, "useDefineForClassFields": false, "exactOptionalPropertyTypes": 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/benchmark-env/lib/benchmark/create-registry.ts b/packages/@glimmer-workspace/benchmark-env/lib/benchmark/create-registry.ts index 1bdbee6d19..5a6194ce98 100644 --- a/packages/@glimmer-workspace/benchmark-env/lib/benchmark/create-registry.ts +++ b/packages/@glimmer-workspace/benchmark-env/lib/benchmark/create-registry.ts @@ -1,4 +1,5 @@ import type { + ClassicResolver, Dict, Helper, HelperDefinitionState, @@ -16,7 +17,7 @@ import { } from '@glimmer/manager'; import { EvaluationContextImpl } from '@glimmer/opcode-compiler'; import { artifacts, RuntimeOpImpl } from '@glimmer/program'; -import { runtimeContext } from '@glimmer/runtime'; +import { runtimeOptions } from '@glimmer/runtime'; import type { UpdateBenchmark } from '../interfaces'; @@ -84,21 +85,17 @@ export default function createRegistry(): Registry { const sharedArtifacts = artifacts(); const document = element.ownerDocument as SimpleDocument; const envDelegate = createEnvDelegate(isInteractive ?? true); - const runtime = runtimeContext( - { - document, - }, - envDelegate, - sharedArtifacts, - { - lookupHelper: (name) => helpers.get(name) ?? null, - lookupModifier: (name) => modifiers.get(name) ?? null, - lookupComponent: (name) => components.get(name) ?? null, - lookupBuiltInHelper: () => null, - lookupBuiltInModifier: () => null, - } - ); + const resolver = { + lookupHelper: (name) => helpers.get(name) ?? null, + lookupModifier: (name) => modifiers.get(name) ?? null, + lookupComponent: (name) => components.get(name) ?? null, + + lookupBuiltInHelper: () => null, + lookupBuiltInModifier: () => null, + } satisfies ClassicResolver; + + const runtime = runtimeOptions({ document }, envDelegate, sharedArtifacts, resolver); const context = new EvaluationContextImpl( sharedArtifacts, @@ -110,7 +107,7 @@ export default function createRegistry(): Registry { throw new Error(`missing ${entry} component`); } - return renderBenchmark(context, component, args, element as SimpleElement); + return renderBenchmark(sharedArtifacts, context, component, args, element as SimpleElement); }, }; } diff --git a/packages/@glimmer-workspace/benchmark-env/lib/benchmark/render-benchmark.ts b/packages/@glimmer-workspace/benchmark-env/lib/benchmark/render-benchmark.ts index 36820a949d..2199a13c85 100644 --- a/packages/@glimmer-workspace/benchmark-env/lib/benchmark/render-benchmark.ts +++ b/packages/@glimmer-workspace/benchmark-env/lib/benchmark/render-benchmark.ts @@ -2,6 +2,7 @@ import type { Dict, EvaluationContext, ResolvedComponentDefinition, + RuntimeArtifacts, SimpleElement, } from '@glimmer/interfaces'; import { NewTreeBuilder, renderComponent, renderSync } from '@glimmer/runtime'; @@ -12,6 +13,7 @@ import { registerResult } from './create-env-delegate'; import { measureRender } from './util'; export default async function renderBenchmark( + artifacts: RuntimeArtifacts, context: EvaluationContext, component: ResolvedComponentDefinition, args: Dict, 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/lib/modes/jit/delegate.ts b/packages/@glimmer-workspace/integration-tests/lib/modes/jit/delegate.ts index 197e1881e5..e606644b6e 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/modes/jit/delegate.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/modes/jit/delegate.ts @@ -33,7 +33,7 @@ import { on, renderComponent, renderSync, - runtimeContext, + runtimeOptions, } from '@glimmer/runtime'; import { assign } from '@glimmer/util'; @@ -63,7 +63,7 @@ export function JitDelegateContext( env: EnvironmentDelegate ): EvaluationContext { let sharedArtifacts = artifacts(); - let runtime = runtimeContext( + let runtime = runtimeOptions( { document: doc }, env, sharedArtifacts, diff --git a/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/builder.ts b/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/builder.ts index 0017a8d6c4..a604d44722 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/builder.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/builder.ts @@ -1,8 +1,8 @@ import type { Cursor, Environment, SimpleNode, TreeBuilder } from '@glimmer/interfaces'; import { COMMENT_NODE, ELEMENT_NODE } from '@glimmer/constants'; -import { RehydrateBuilder } from '@glimmer/runtime'; +import { RehydrateTree } from '@glimmer/runtime'; -export class DebugRehydrationBuilder extends RehydrateBuilder { +export class DebugRehydrateTree extends RehydrateTree { clearedNodes: SimpleNode[] = []; override remove(node: SimpleNode) { @@ -23,6 +23,6 @@ export class DebugRehydrationBuilder extends RehydrateBuilder { } } -export function debugRehydration(env: Environment, cursor: Cursor): TreeBuilder { - return DebugRehydrationBuilder.forInitialRender(env, cursor); +export function debugRehydrateTree(env: Environment, cursor: Cursor): TreeBuilder { + return DebugRehydrateTree.forInitialRender(env, cursor); } diff --git a/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/delegate.ts b/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/delegate.ts index 9faef8a24d..fcc61bb79f 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/delegate.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/delegate.ts @@ -27,7 +27,7 @@ import type { UserHelper } from '../../helpers'; import type { TestModifierConstructor } from '../../modifiers'; import type RenderDelegate from '../../render-delegate'; import type { RenderDelegateOptions } from '../../render-delegate'; -import type { DebugRehydrationBuilder } from './builder'; +import type { DebugRehydrateTree } from './builder'; import { BaseEnv } from '../../base-env'; import { replaceHTML, toInnerHTML } from '../../dom/simple-utils'; @@ -41,7 +41,7 @@ import { import { TestJitRegistry } from '../jit/registry'; import { renderTemplate } from '../jit/render'; import { TestJitRuntimeResolver } from '../jit/resolver'; -import { debugRehydration } from './builder'; +import { debugRehydrateTree } from './builder'; export interface RehydrationStats { clearedNodes: SimpleNode[]; @@ -105,7 +105,7 @@ export class RehydrationDelegate implements RenderDelegate { getElementBuilder(env: Environment, cursor: Cursor): TreeBuilder { if (cursor.element instanceof Node) { - return debugRehydration(env, cursor); + return debugRehydrateTree(env, cursor); } return serializeBuilder(env, cursor); @@ -152,7 +152,7 @@ export class RehydrationDelegate implements RenderDelegate { // Client-side rehydration let cursor = { element, nextSibling: null }; - let builder = this.getElementBuilder(env, cursor) as DebugRehydrationBuilder; + let builder = this.getElementBuilder(env, cursor) as DebugRehydrateTree; let result = renderTemplate( template, this.clientContext, diff --git a/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/partial-rehydration-delegate.ts b/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/partial-rehydration-delegate.ts index 60b8783f5f..9d9b19a92f 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/partial-rehydration-delegate.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/modes/rehydration/partial-rehydration-delegate.ts @@ -1,7 +1,7 @@ import type { Dict, RenderResult, SimpleElement } from '@glimmer/interfaces'; import { renderComponent, renderSync } from '@glimmer/runtime'; -import type { DebugRehydrationBuilder } from './builder'; +import type { DebugRehydrateTree } from './builder'; import { RehydrationDelegate } from './delegate'; @@ -17,15 +17,15 @@ export class PartialRehydrationDelegate extends RehydrationDelegate { ): RenderResult { let cursor = { element, nextSibling: null }; let context = this.clientContext; - let builder = this.getElementBuilder(context.env, cursor) as DebugRehydrationBuilder; + let tree = this.getElementBuilder(context.env, cursor) as DebugRehydrateTree; let component = this.clientRegistry.lookupComponent(name)!; - let iterator = renderComponent(context, builder, {}, component.state, args); + let iterator = renderComponent(context, tree, {}, component.state, args); const result = renderSync(context.env, iterator); this.rehydrationStats = { - clearedNodes: builder.clearedNodes, + clearedNodes: tree.clearedNodes, }; return result; diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts index c8cdea29e4..c446b6a503 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts @@ -1,5 +1,6 @@ import { resetDebuggerCallback, setDebuggerCallback } from '@glimmer/runtime'; +import { GlimmerishComponent } from '../components'; import { RenderTest } from '../render-test'; import { test } from '../test-decorator'; @@ -10,28 +11,51 @@ export class DebuggerSuite extends RenderTest { resetDebuggerCallback(); } - @test + @test({ + kind: 'templateOnly', + }) 'basic debugger statement'() { let expectedContext = { foo: 'bar', a: { b: true, }, + used: 'named', }; let callbackExecuted = 0; setDebuggerCallback((context: any, get) => { callbackExecuted++; - this.assert.strictEqual(context.foo, expectedContext.foo); - this.assert.strictEqual(get('foo'), expectedContext.foo); + this.assert.strictEqual(context.foo, expectedContext.foo, 'reading from the context'); + this.assert.strictEqual(get('foo'), expectedContext.foo, 'reading from a local'); + this.assert.strictEqual(get('@a'), expectedContext.a, 'reading from an unused named args'); + this.assert.strictEqual(get('@used'), expectedContext.used, 'reading from a used named args'); }); + this.registerComponent( + 'Glimmer', + 'MyComponent', + '{{#if this.a.b}}true{{debugger}}{{else}}false{{debugger}}{{/if}}{{@used}}', + class extends GlimmerishComponent { + declare args: { a: { b: boolean }; foo: string; used: string }; + + get a() { + return this.args.a; + } + + get foo() { + return this.args.foo; + } + } + ); + this.render( - '{{#if this.a.b}}true{{debugger}}{{else}}false{{debugger}}{{/if}}', + ``, expectedContext ); + this.assert.strictEqual(callbackExecuted, 1); - this.assertHTML('true'); + this.assertHTML('truenamed'); this.assertStableRerender(); expectedContext = { @@ -39,10 +63,11 @@ export class DebuggerSuite extends RenderTest { a: { b: false, }, + used: 'named', }; this.rerender(expectedContext); this.assert.strictEqual(callbackExecuted, 2); - this.assertHTML('false'); + this.assertHTML('falsenamed'); this.assertStableNodes(); expectedContext = { @@ -50,10 +75,11 @@ export class DebuggerSuite extends RenderTest { a: { b: true, }, + used: 'named', }; this.rerender(expectedContext); this.assert.strictEqual(callbackExecuted, 3); - this.assertHTML('true'); + this.assertHTML('truenamed'); this.assertStableNodes(); } 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 69c6b5ebec..89d5c6ef0e 100644 --- a/packages/@glimmer-workspace/integration-tests/test/chaos-rehydration-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/chaos-rehydration-test.ts @@ -2,7 +2,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 '..'; @@ -164,7 +164,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-workspace/integration-tests/test/helpers/hash-test.ts b/packages/@glimmer-workspace/integration-tests/test/helpers/hash-test.ts index 3b498af2b0..73af8aae10 100644 --- a/packages/@glimmer-workspace/integration-tests/test/helpers/hash-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/helpers/hash-test.ts @@ -1,3 +1,5 @@ +import { LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; + import { GlimmerishComponent, jitSuite, RenderTest, test, tracked } from '../..'; class HashTest extends RenderTest { @@ -161,7 +163,7 @@ class HashTest extends RenderTest { this.assertHTML('Chad Hietala'); } - @test + @test({ skip: LOCAL_TRACE_LOGGING }) 'individual hash values are accessed lazily'(assert: Assert) { class FooBar extends GlimmerishComponent { firstName = 'Godfrey'; diff --git a/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts b/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts index 0318ff4932..8e10f7d5fd 100644 --- a/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/jit-suites-test.ts @@ -15,7 +15,7 @@ import { YieldSuite, } from '..'; -jitSuite(DebuggerSuite); +jitComponentSuite(DebuggerSuite); jitSuite(EachSuite); jitSuite(InElementSuite); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/log-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/log-test.ts index 703f79997a..6db6d0c16b 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/log-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/log-test.ts @@ -1,3 +1,5 @@ +import { LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; + import { jitSuite, RenderTest, test } from '../..'; class LogTest extends RenderTest { @@ -80,4 +82,6 @@ class LogTest extends RenderTest { } } -jitSuite(LogTest); +if (!LOCAL_TRACE_LOGGING) { + jitSuite(LogTest); +} 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/lib/passes/1-normalization/keywords/README.md b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/README.md index e8d1186da0..6802c98c0d 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/README.md +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/README.md @@ -1,4 +1,4 @@ --- -noteId: 'a5195d00ecb511eaa450939780d4843d' +noteId: "a5195d00ecb511eaa450939780d4843d" tags: [] --- diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/index.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/index.ts index 9896df1d1b..715946773e 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/index.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/index.ts @@ -13,7 +13,7 @@ export function visit(template: mir.Template): WireFormat.SerializedTemplateBloc let block: WireFormat.SerializedTemplateBlock = [ statements, scope.symbols, - scope.hasEval, + scope.hasDebugger, scope.upvars, ]; diff --git a/packages/@glimmer/compiler/lib/wire-encoding.md b/packages/@glimmer/compiler/lib/wire-encoding.md index 106ca1c403..a01940de7a 100644 --- a/packages/@glimmer/compiler/lib/wire-encoding.md +++ b/packages/@glimmer/compiler/lib/wire-encoding.md @@ -130,9 +130,9 @@ when otherwise explicitly stated. ## Flags -| 0 | 1 | 2 | 3 | 4 | 5 | -| -------- | -------- | -------- | -------- | ---------- | ------- | -| reserved | reserved | reserved | reserved | has upvars | hasEval | +| 0 | 1 | 2 | 3 | 4 | 5 | +| -------- | -------- | -------- | -------- | ---------- | ----------- | +| reserved | reserved | reserved | reserved | has upvars | hasDebugger | # Expression diff --git a/packages/@glimmer/compiler/lib/wire-format-debug.ts b/packages/@glimmer/compiler/lib/wire-format-debug.ts index f7c4beda72..0688d6c5dc 100644 --- a/packages/@glimmer/compiler/lib/wire-format-debug.ts +++ b/packages/@glimmer/compiler/lib/wire-format-debug.ts @@ -16,7 +16,7 @@ export default class WireFormatDebugger { private upvars: string[]; private symbols: string[]; - constructor([_statements, symbols, _hasEval, upvars]: SerializedTemplateBlock) { + constructor([_statements, symbols, _hasDebugger, upvars]: SerializedTemplateBlock) { this.upvars = upvars; this.symbols = symbols; } 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/constants/index.ts b/packages/@glimmer/constants/index.ts index f5ccfdd228..e1447e2bb2 100644 --- a/packages/@glimmer/constants/index.ts +++ b/packages/@glimmer/constants/index.ts @@ -1,3 +1,4 @@ +export * from './lib/brand'; export * from './lib/builder-constants'; export * from './lib/curried'; export * from './lib/dom'; diff --git a/packages/@glimmer/constants/lib/brand.ts b/packages/@glimmer/constants/lib/brand.ts new file mode 100644 index 0000000000..687d570fbe --- /dev/null +++ b/packages/@glimmer/constants/lib/brand.ts @@ -0,0 +1,2 @@ +export const IS_COMPILABLE_TEMPLATE = Symbol('IS_COMPILABLE_TEMPLATE'); +export type IS_COMPILABLE_TEMPLATE = typeof IS_COMPILABLE_TEMPLATE; diff --git a/packages/@glimmer/debug-util/index.ts b/packages/@glimmer/debug-util/index.ts index 7bc4a6300e..84eb5160b5 100644 --- a/packages/@glimmer/debug-util/index.ts +++ b/packages/@glimmer/debug-util/index.ts @@ -1,4 +1,5 @@ -export { default as assert, deprecate } from './lib/assert'; +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-util/lib/platform-utils.ts b/packages/@glimmer/debug-util/lib/platform-utils.ts index 9aa6d007d5..baaa9a7581 100644 --- a/packages/@glimmer/debug-util/lib/platform-utils.ts +++ b/packages/@glimmer/debug-util/lib/platform-utils.ts @@ -1,4 +1,4 @@ -import type { Maybe, Present } from '@glimmer/interfaces'; +import type { Maybe, Optional } from '@glimmer/interfaces'; import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; export type Factory = new (...args: unknown[]) => T; @@ -10,28 +10,33 @@ export function unwrap(val: Maybe): T { return val as T; } -export const expect = (LOCAL_DEBUG - ? (value: T, _message: string) => value - : (val: T, message: string): Present => { - if (LOCAL_DEBUG) if (val === null || val === undefined) throw new Error(message); - return val as Present; - }) as (value: T, message: string) => NonNullable as ( - value: T, - message: string -) => Present; +/** + * This function takes an optional function and returns its result. It's + * expected to be used with optional debug methods, in the context of an + * existing `LOCAL_DEBUG` check. + */ +export function dev(val: Optional<() => T>): T { + if (val === null || val === undefined) { + throw new Error( + `Expected debug method to be present. Make sure you're calling \`dev()\` in the context of a \`LOCAL_DEBUG\` check.` + ); + } -export const unreachable = LOCAL_DEBUG - ? () => {} - : (message = 'unreachable'): Error => new Error(message); + return val(); +} -export const exhausted = ( - LOCAL_DEBUG - ? () => {} - : (value: never): never => { - throw new Error(`Exhausted ${String(value)}`); - } -) as (value: never) => never; +export function expect(val: Maybe, message: string): T; +export function expect(val: unknown, message: string): unknown { + if (LOCAL_DEBUG) if (val === null || val === undefined) throw new Error(message); + return val; +} -export type Lit = string | number | boolean | undefined | null | void | {}; +export function unreachable(message?: string): never; +export function unreachable(message?: string): void { + if (LOCAL_DEBUG) throw new Error(message); +} -export const tuple = (...args: T) => args; +export function exhausted(value: never): never; +export function exhausted(value: never): void { + if (LOCAL_DEBUG) throw new Error(`Exhausted ${String(value)}`); +} diff --git a/packages/@glimmer/debug/index.ts b/packages/@glimmer/debug/index.ts index 3993b7c94c..deaa630b90 100644 --- a/packages/@glimmer/debug/index.ts +++ b/packages/@glimmer/debug/index.ts @@ -1,4 +1,6 @@ -export { debug, debugSlice, logOpcode } from './lib/debug'; +export type { DebugOp, SomeDisassembledOperand } from './lib/debug'; +export { debugOp, describeOpcode, logOpcodeSlice } from './lib/debug'; +export { describeOp } from './lib/dism/opcode'; export { buildEnum, buildMetas, @@ -11,6 +13,11 @@ 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'; export { check, CheckArray, @@ -41,13 +48,12 @@ export { recordStackSize, wrap, } from './lib/stack-check'; - +export { type VmDiff, VmSnapshot, type VmSnapshotValueDiff } from './lib/vm/snapshot'; // Types are optimized await automatically export type { NormalizedMetadata, NormalizedOpcodes, Operand, - OperandList, OperandName, OperandType, RawOperandFormat, diff --git a/packages/@glimmer/debug/lib/debug.ts b/packages/@glimmer/debug/lib/debug.ts index 42004ad31b..d0af8a7a10 100644 --- a/packages/@glimmer/debug/lib/debug.ts +++ b/packages/@glimmer/debug/lib/debug.ts @@ -1,44 +1,50 @@ import type { + BlockMetadata, CompilationContext, - CompileTimeConstants, Dict, - Maybe, - Recast, - ResolutionTimeConstants, + Nullable, + Program, + ProgramConstants, RuntimeOp, } from '@glimmer/interfaces'; -import { decodeHandle, decodeImmediate } from '@glimmer/constants'; -import { LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; +import { + CURRIED_COMPONENT, + CURRIED_HELPER, + CURRIED_MODIFIER, + decodeHandle, + decodeImmediate, +} from '@glimmer/constants'; +import { exhausted, expect, unreachable } from '@glimmer/debug-util'; +import { LOCAL_DEBUG, LOCAL_SUBTLE_LOGGING, LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; import { enumerate, LOCAL_LOGGER } from '@glimmer/util'; import { $fp, $pc, $ra, $s0, $s1, $sp, $t0, $t1, $v0 } from '@glimmer/vm'; -import type { Primitive } from './stack-check'; +import type { Primitive, RegisterName } from './dism/dism'; +import type { NormalizedOperand, OperandType, ShorthandOperand } from './dism/operand-types'; +import { describeOp } from './dism/opcode'; +import { OPERANDS } from './dism/operands'; import { opcodeMetadata } from './opcode-metadata'; +import { frag } from './render/fragment'; +import { DebugLogger } from './render/logger'; -export interface DebugConstants { - getValue(handle: number): T; - getArray(value: number): T[]; -} - -export function debugSlice(context: CompilationContext, start: number, end: number) { +export function logOpcodeSlice(context: CompilationContext, start: number, end: number) { if (LOCAL_TRACE_LOGGING) { + const logger = new DebugLogger(LOCAL_LOGGER, { showSubtle: !!LOCAL_SUBTLE_LOGGING }); LOCAL_LOGGER.group(`%c${start}:${end}`, 'color: #999'); - const constants = context.evaluation.program.constants; + const program = context.evaluation.program; - let heap = context.evaluation.program.heap; + let heap = program.heap; let opcode = context.evaluation.createOp(heap); let _size = 0; - for (let i = start; i < end; i = i + _size) { + for (let i = start; i <= end; i = i + _size) { opcode.offset = i; - let [name, params] = debug( - constants as Recast, - opcode, - opcode.isMachine - )!; - LOCAL_LOGGER.debug(`${i}. ${logOpcode(name, params)}`); + const op = describeOp(opcode, program, context.meta); + + logger.log(frag`${i}. ${op}`); + _size = opcode.size; } opcode.offset = -_size; @@ -46,13 +52,13 @@ export function debugSlice(context: CompilationContext, start: number, end: numb } } -export function logOpcode(type: string, params: Maybe): string | void { - if (LOCAL_TRACE_LOGGING) { +export function describeOpcode(type: string, params: Dict): string | void { + if (LOCAL_DEBUG) { let out = type; if (params) { - let args = Object.keys(params) - .map((p) => ` ${p}=${json(params[p])}`) + let args = Object.entries(params) + .map(([p, v]) => ` ${p}=${jsonify(v)}`) .join(''); out += args; } @@ -60,132 +66,381 @@ export function logOpcode(type: string, params: Maybe): string | void { } } -function json(param: unknown) { - if (LOCAL_TRACE_LOGGING) { - if (typeof param === 'function') { - return ''; - } +function stringify(value: number, type: 'constant'): string; +function stringify(value: RegisterName, type: 'register'): string; +function stringify(value: number, type: 'variable' | 'pc'): string; +function stringify(value: DisassembledOperand['value'], type: 'stringify' | 'unknown'): string; +function stringify( + value: unknown, + type: 'stringify' | 'constant' | 'register' | 'variable' | 'pc' | 'unknown' +) { + switch (type) { + case 'stringify': + return JSON.stringify(value); + case 'constant': + return `${stringify(value, 'unknown')}`; + case 'register': + return value; + case 'variable': + return `{$fp+${value}}`; + case 'pc': + return `@${value}`; + case 'unknown': { + switch (typeof value) { + case 'function': + return ''; + case 'number': + case 'string': + case 'bigint': + case 'boolean': + return JSON.stringify(value); + case 'symbol': + return `${String(value)}`; + case 'undefined': + return 'undefined'; + case 'object': { + if (value === null) return 'null'; + if (Array.isArray(value)) return ``; - let string; - try { - string = JSON.stringify(param); - } catch (e) { - return ''; - } + let name = value.constructor.name; - if (string === undefined) { - return 'undefined'; - } + switch (name) { + case 'Error': + case 'RangeError': + case 'ReferenceError': + case 'SyntaxError': + case 'TypeError': + case 'WeakMap': + case 'WeakSet': + return `<${name}>`; + case 'Object': + return `<${name}>`; + } - let debug = JSON.parse(string); - if (typeof debug === 'object' && debug !== null && debug.GlimmerDebug !== undefined) { - return debug.GlimmerDebug; + if (value instanceof Map) { + return ``; + } else if (value instanceof Set) { + return ``; + } else { + return `<${name}>`; + } + } + } } + } +} + +function jsonify(param: SomeDisassembledOperand): string | string[] | null { + const result = json(param); - return string; + return Array.isArray(result) ? JSON.stringify(result) : result ?? 'null'; +} + +function json(param: SomeDisassembledOperand): string | string[] | null { + switch (param.type) { + case 'number': + case 'boolean': + case 'string': + case 'primitive': + return stringify(param.value, 'stringify'); + case 'array': + return ''; + case 'dynamic': + return stringify(param.value, 'unknown'); + case 'constant': + return stringify(param.value, 'constant'); + case 'register': + return stringify(param.value, 'register'); + case 'instruction': + return stringify(param.value, 'pc'); + case 'variable': + return stringify(param.value, 'variable'); + case 'error:opcode': + return `{raw:${param.value}}`; + case 'error:operand': + return `{err:${param.options.label.name}=${param.value}}`; + case 'enum': + return ``; + + default: + exhausted(param); } } -export function debug( - c: DebugConstants, - op: RuntimeOp, - isMachine: 0 | 1 -): [string, Dict] | undefined { - if (LOCAL_TRACE_LOGGING) { - let metadata = opcodeMetadata(op.type, isMachine); +export type AnyOperand = [type: string, value: never, options?: object]; +export type OperandTypeOf = O[0]; +export type OperandValueOf = O[1]; +export type OperandOptionsOf = O extends [ + type: string, + value: never, + options: infer Options, +] + ? Options + : void; +export type OperandOptionsA = O extends [ + type: string, + value: never, + options: infer Options, +] + ? Options + : {}; + +type ExtractA = O extends { a: infer A } ? A : never; +type ExpandUnion = U extends infer O ? ExtractA<{ a: O }> : never; + +export type NullableOperand = + | [OperandTypeOf, OperandValueOf, Expand & { nullable?: false }>] + | [ + OperandTypeOf, + Nullable>, + Expand & { nullable: true }>, + ]; + +export type NullableName = T extends `${infer N}?` ? N : never; + +export type WithOptions = ExpandUnion< + [OperandTypeOf, OperandValueOf, Expand & Options>] +>; + +// expands object types one level deep +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; + +type DefineOperand = undefined extends Options + ? readonly [type: T, value: V] + : readonly [type: T, value: V, options: Options]; + +type DefineNullableOperand = Options extends undefined + ? + | readonly [type: T, value: V] + | readonly [type: T, value: Nullable, options: { nullable: true }] + | readonly [type: T, value: V, options: { nullable?: false }] + : + | readonly [type: T, value: Nullable, options: Expand] + | readonly [type: T, value: V, options: Expand] + | readonly [type: T, value: V, options: Options]; + +/** + * A dynamic operand has a value that can't be easily represented as an embedded string. + */ +export type RawDynamicDisassembledOperand = + | DefineOperand<'dynamic', unknown> + | DefineOperand<'constant', number> + | DefineNullableOperand<'array', unknown[]> + | DefineOperand<'variable', number, { name?: string | null }>; + +export type RawStaticDisassembledOperand = + | DefineOperand<'error:operand', number, { label: NormalizedOperand }> + | DefineOperand<'error:opcode', number, { kind: number }> + | DefineOperand<'number', number> + | DefineOperand<'boolean', boolean> + | DefineOperand<'primitive', Primitive> + | DefineOperand<'register', RegisterName> + | DefineOperand<'instruction', number> + | DefineOperand<'enum', 'component' | 'helper' | 'modifier'> + | DefineOperand<'array', number[], { kind: typeof Number }> + | DefineNullableOperand<'array', string[], { kind: typeof String }> + /** + * A variable is a numeric offset into the stack (relative to the $fp register). + */ + | DefineNullableOperand<'string', string>; + +export type RawDisassembledOperand = RawStaticDisassembledOperand | RawDynamicDisassembledOperand; + +type ObjectForRaw = R extends RawDisassembledOperand + ? R[2] extends undefined + ? { + type: R[0]; + value: R[1]; + options?: R[2]; + } + : { + type: R[0]; + value: R[1]; + options: R[2]; + } + : never; + +export class DisassembledOperand { + static of(raw: RawDisassembledOperand): SomeDisassembledOperand { + return new DisassembledOperand(raw) as never; + } + + readonly #raw: R; + private constructor(raw: R) { + this.#raw = raw; + } + + get type(): R[0] { + return this.#raw[0]; + } + + get value(): R[1] { + return this.#raw[1]; + } + + get options(): R[2] { + return this.#raw[2]; + } +} + +export type StaticDisassembledOperand = ObjectForRaw & { + isDynamic: false; +}; +export type DynamicDisassembledOperand = ObjectForRaw & { + isDynamic: true; +}; + +export type SomeDisassembledOperand = StaticDisassembledOperand | DynamicDisassembledOperand; + +export interface DebugOp { + name: string; + params: Dict; + meta: BlockMetadata | null; +} + +export type OpSnapshot = Pick; + +export function getOpSnapshot(op: RuntimeOp): OpSnapshot { + return { + offset: op.offset, + size: op.size, + type: op.type, + op1: op.op1, + op2: op.op2, + op3: op.op3, + }; +} + +class DebugOperandInfo { + readonly #offset: number; + readonly #operand: NormalizedOperand; + readonly #value: number; + readonly #program: Program; + readonly #metadata: BlockMetadata | null; + + constructor( + offset: number, + operand: NormalizedOperand, + value: number, + program: Program, + metadata: BlockMetadata | null + ) { + this.#offset = offset; + this.#operand = operand; + this.#value = value; + this.#program = program; + this.#metadata = metadata; + } + + toDebug(): RawDisassembledOperand { + const spec = expect( + OPERANDS[this.#operand.type], + `Unknown operand type: ${this.#operand.type}` + ); + + return spec({ + offset: this.#offset, + label: this.#operand, + value: this.#value, + constants: this.#program.constants, + heap: this.#program.heap, + meta: this.#metadata, + }); + } +} + +export function debugOp(program: Program, op: OpSnapshot, meta: BlockMetadata | null): DebugOp { + if (LOCAL_DEBUG) { + let metadata = opcodeMetadata(op.type); + + let out: Dict = Object.create(null); if (!metadata) { - throw new Error(`Missing Opcode Metadata for ${op.type}`); - } + for (let i = 0; i < op.size; i++) { + out[i] = ['error:opcode', i, { kind: op.type }]; + } - let out = Object.create(null); - - for (const [index, operand] of enumerate(metadata.ops)) { - let actualOperand = opcodeOperand(op, index); - - switch (operand.type) { - case 'u32': - case 'i32': - case 'owner': - out[operand.name] = actualOperand; - break; - case 'handle': - out[operand.name] = c.getValue(actualOperand); - break; - case 'str': - case 'option-str': - case 'array': - out[operand.name] = c.getValue(actualOperand); - break; - case 'str-array': - out[operand.name] = c.getArray(actualOperand); - break; - case 'bool': - out[operand.name] = !!actualOperand; - break; - case 'primitive': - out[operand.name] = decodePrimitive(actualOperand, c); - break; - case 'register': - out[operand.name] = decodeRegister(actualOperand); - break; - case 'unknown': - out[operand.name] = c.getValue(actualOperand); - break; - case 'symbol-table': - case 'scope': - out[operand.name] = ``; - break; - default: - throw new Error(`Unexpected operand type ${operand.type} for debug output`); + return { name: `{unknown ${op.type}}`, params: fromRaw(out), meta }; + } else if (metadata.ops) { + for (const [index, operand] of enumerate(metadata.ops)) { + const normalized = normalizeOperand(operand); + const info = new DebugOperandInfo( + op.offset, + normalized, + getOperand(op, index as 0 | 1 | 2), + program, + meta + ); + out[normalized.name] = info.toDebug(); } } - - return [metadata.name, out]; + return { name: metadata.name, params: fromRaw(out), meta }; } - return undefined; + throw unreachable(`BUG: Don't try to debug opcodes while trace is disabled`); +} + +function normalizeOperand(operand: ShorthandOperand): NormalizedOperand { + const [name, type] = operand.split(':') as [string, OperandType]; + return { name, type }; } -function opcodeOperand(opcode: RuntimeOp, index: number): number { +function getOperand(op: OpSnapshot, index: 0 | 1 | 2): number { switch (index) { case 0: - return opcode.op1; + return op.op1; case 1: - return opcode.op2; + return op.op2; case 2: - return opcode.op3; + return op.op3; + } +} + +function fromRaw(operands: Dict): Dict { + return Object.fromEntries( + Object.entries(operands).map(([name, raw]) => [name, DisassembledOperand.of(raw)]) + ); +} + +export function decodeCurry(curry: number): 'component' | 'helper' | 'modifier' { + switch (curry) { + case CURRIED_COMPONENT: + return 'component'; + case CURRIED_HELPER: + return 'helper'; + case CURRIED_MODIFIER: + return 'modifier'; default: - throw new Error(`Unexpected operand index (must be 0-2)`); + throw Error(`Unexpected curry value: ${curry}`); } } -function decodeRegister(register: number): string { +export function decodeRegister(register: number): RegisterName { switch (register) { case $pc: - return 'pc'; + return '$pc'; case $ra: - return 'ra'; + return '$ra'; case $fp: - return 'fp'; + return '$fp'; case $sp: - return 'sp'; + return '$sp'; case $s0: - return 's0'; + return '$s0'; case $s1: - return 's1'; + return '$s1'; case $t0: - return 't0'; + return '$t0'; case $t1: - return 't1'; + return '$t1'; case $v0: - return 'v0'; + return '$v0'; default: - throw new Error(`Unexpected register ${register}`); + return `$bug${register}`; } } -function decodePrimitive(primitive: number, constants: DebugConstants): Primitive { +export function decodePrimitive(primitive: number, constants: ProgramConstants): Primitive { if (primitive >= 0) { return constants.getValue(decodeHandle(primitive)); } diff --git a/packages/@glimmer/debug/lib/dism/dism.ts b/packages/@glimmer/debug/lib/dism/dism.ts new file mode 100644 index 0000000000..dd323d39b9 --- /dev/null +++ b/packages/@glimmer/debug/lib/dism/dism.ts @@ -0,0 +1,80 @@ +import type { Expand, Nullable } from '@glimmer/interfaces'; + +import type { NormalizedOperand } from './operand-types'; + +export type Primitive = undefined | null | boolean | number | string; +export type RegisterName = + | '$pc' + | '$ra' + | '$fp' + | '$sp' + | '$s0' + | '$s1' + | '$t0' + | '$t1' + | '$v0' + | `$bug${number}`; + +export type StaticDisassembledOperand = ObjectForRaw & { + isDynamic: false; +}; +export type DynamicDisassembledOperand = ObjectForRaw & { + isDynamic: true; +}; + +export type SomeDisassembledOperand = StaticDisassembledOperand | DynamicDisassembledOperand; + +export type RawDisassembledOperand = RawStaticDisassembledOperand | RawDynamicDisassembledOperand; + +type DefineOperand = undefined extends Options + ? readonly [type: T, value: V] + : readonly [type: T, value: V, options: Options]; + +type DefineNullableOperand = Options extends undefined + ? + | readonly [type: T, value: V] + | readonly [type: T, value: Nullable, options: { nullable: true }] + | readonly [type: T, value: V, options: { nullable?: false }] + : + | readonly [type: T, value: Nullable, options: Expand] + | readonly [type: T, value: V, options: Expand] + | readonly [type: T, value: V, options: Options]; + +/** + * A dynamic operand has a value that can't be easily represented as an embedded string. + */ +export type RawDynamicDisassembledOperand = + | DefineOperand<'dynamic', unknown> + | DefineOperand<'constant', number> + | DefineNullableOperand<'array', unknown[]> + | DefineOperand<'variable', number, { name?: string | null }>; + +export type RawStaticDisassembledOperand = + | DefineOperand<'error:operand', number, { label: NormalizedOperand }> + | DefineOperand<'error:opcode', number, { kind: number }> + | DefineOperand<'number', number> + | DefineOperand<'boolean', boolean> + | DefineOperand<'primitive', Primitive> + | DefineOperand<'register', RegisterName> + | DefineOperand<'instruction', number> + | DefineOperand<'enum', 'component' | 'helper' | 'modifier'> + | DefineOperand<'array', number[], { kind: typeof Number }> + | DefineNullableOperand<'array', string[], { kind: typeof String }> + /** + * A variable is a numeric offset into the stack (relative to the $fp register). + */ + | DefineNullableOperand<'string', string>; + +type ObjectForRaw = R extends RawDisassembledOperand + ? R[2] extends undefined + ? { + type: R[0]; + value: R[1]; + options?: R[2]; + } + : { + type: R[0]; + value: R[1]; + options: R[2]; + } + : never; diff --git a/packages/@glimmer/debug/lib/dism/opcode.ts b/packages/@glimmer/debug/lib/dism/opcode.ts new file mode 100644 index 0000000000..caf0222c47 --- /dev/null +++ b/packages/@glimmer/debug/lib/dism/opcode.ts @@ -0,0 +1,306 @@ +import type { ClassifiedLocalDebug, ClassifiedLocalDebugFor } from '@glimmer/debug-util'; +import type { + AppendingBlock, + BlockMetadata, + BlockSymbolNames, + Cursor, + NamedArguments, + Nullable, + PositionalArguments, + Program, + RuntimeOp, + VMArguments, +} from '@glimmer/interfaces'; +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 { IntoFragment } from '../render/fragment'; +import type { RegisterName, SomeDisassembledOperand } from './dism'; + +import { debugOp } from '../debug'; +import { empty, join, unknownValue, value } from '../render/basic'; +import { array } from '../render/combinators'; +import { as, frag, Fragment } from '../render/fragment'; + +export function describeOp( + op: RuntimeOp, + program: Program, + meta: Nullable +): Fragment { + const { name, params } = debugOp(program, op, meta)!; + + const block = new SerializeBlockContext(meta?.symbols ?? null); + + let args: IntoFragment[] = Object.entries(params).map( + ([p, v]) => frag`${as.attrName(p)}=${block.serialize(v)}` + ); + + return frag`(${join([as.kw(name), ...args], ' ')})`; +} + +export class SerializeBlockContext { + readonly #symbols: Nullable; + + constructor(symbols: Nullable) { + this.#symbols = symbols; + } + + serialize(param: SomeDisassembledOperand): IntoFragment { + switch (param.type) { + case 'number': + case 'boolean': + case 'string': + case 'primitive': + return this.#stringify(param.value, 'stringify'); + case 'array': + return array(param.value?.map((value) => this.#stringify(value, 'unknown')) ?? []); + case 'dynamic': + case 'constant': + return value(param.value); + case 'register': + return this.#stringify(param.value, 'register'); + case 'instruction': + return this.#stringify(param.value, 'pc'); + case 'variable': { + const value = param.value; + if (value === 0) { + return frag`{${as.kw('this')}}`; + } else if (this.#symbols?.lexical && this.#symbols.lexical.length >= value) { + // @fixme something is wrong here -- remove the `&&` to get test failures + return frag`${as.varReference( + this.#symbols.lexical[value - 1]! + )}${frag`:${value}`.subtle()}`; + } else { + return frag`{${as.register('$fp')}+${value}}`; + } + } + + case 'error:opcode': + return `{raw:${param.value}}`; + case 'error:operand': + return `{err:${param.options.label.name}=${param.value}}`; + case 'enum': + return ``; + + default: + exhausted(param); + } + } + + #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'): IntoFragment; + #stringify( + value: unknown, + type: 'stringify' | 'constant' | 'register' | 'variable' | 'pc' | 'unknown' + ) { + switch (type) { + case 'stringify': + return JSON.stringify(value); + case 'constant': + return `${this.#stringify(value, 'unknown')}`; + case 'register': + return value; + case 'variable': { + if (value === 0) { + return `{this}`; + } else if (this.#symbols?.lexical && this.#symbols.lexical.length >= (value as number)) { + return `{${this.#symbols.lexical[(value as number) - 1]}:${value}}`; + } else { + return `{$fp+${value}}`; + } + } + case 'pc': + return `@${value}`; + case 'unknown': { + switch (typeof value) { + case 'function': + return ''; + case 'number': + case 'string': + case 'bigint': + case 'boolean': + return JSON.stringify(value); + case 'symbol': + return `${String(value)}`; + case 'undefined': + return 'undefined'; + case 'object': { + if (value === null) return 'null'; + if (Array.isArray(value)) return ``; + + let name = value.constructor.name; + + switch (name) { + case 'Error': + case 'RangeError': + case 'ReferenceError': + case 'SyntaxError': + case 'TypeError': + case 'WeakMap': + case 'WeakSet': + return `<${name}>`; + case 'Object': + return `<${name}>`; + } + + if (value instanceof Map) { + return ``; + } else if (value instanceof Set) { + return ``; + } else { + return `<${name}>`; + } + } + } + } + } + } +} + +export function debugValue(item: unknown, options?: ValueRefOptions): Fragment { + if (isIndexable(item)) { + const classified = getLocalDebugType(item)!; + + if (classified) return describeValue(classified); + } + + return unknownValue(item, options); +} + +function describeValue(classified: ClassifiedLocalDebug): 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.type('block bounds')} { ${as.kw(kind)} ${value(first)} }`; + } + } else { + return frag`${as.type('block bounds')} { ${as.kw(kind)} ${value(first)} .. ${value(last)} }`; + } +} + +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 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 = { + [K in P]: O[P] extends IntoFragment ? K : never; +}[P]; diff --git a/packages/@glimmer/debug/lib/dism/operand-types.ts b/packages/@glimmer/debug/lib/dism/operand-types.ts new file mode 100644 index 0000000000..c66c0b066c --- /dev/null +++ b/packages/@glimmer/debug/lib/dism/operand-types.ts @@ -0,0 +1,74 @@ +// @note OPERAND_TYPES +export const OPERAND_TYPES = [ + // imm means inline + 'imm/u32', + 'imm/i32', + // encoded as 0 or 1 + 'imm/bool', + // the operand is an i32 or u32, but it has a more specific meaning that should be captured here + 'imm/u32{todo}', + 'imm/i32{todo}', + + 'imm/enum', + 'imm/block:handle', + + 'imm/pc', + 'handle', + 'handle/block', + + 'const/i32[]', + 'const/str?', + 'const/any[]', + 'const/str[]?', + 'const/bool', + 'const/fn', + 'const/any', + + // could be an immediate + 'const/primitive', + 'const/definition', + + 'register', + // $pc, $ra + 'register/instruction', + // $sp, $fp + 'register/stack', + // $s0, $s1, $t0, $t1, $v0 + 'register/sN', + 'register/tN', + 'register/v0', + + 'variable', + + 'instruction/relative', +] as const; + +export function isOperandType(s: string): s is OperandType { + return OPERAND_TYPES.includes(s as never) || OPERAND_TYPES.includes(`${s}?` as never); +} + +export type OPERAND_TYPE = (typeof OPERAND_TYPES)[number]; +export type NonNullableOperandType = Exclude; +export type NullableOperandType = Extract extends `${infer S}?` + ? S + : never; +export type OperandType = NonNullableOperandType | NullableOperandType | `${NullableOperandType}?`; + +export interface NormalizedOperand { + type: OperandType; + name: string; +} + +export type NormalizedOperandList = + | [] + | [NormalizedOperand] + | [NormalizedOperand, NormalizedOperand] + | [NormalizedOperand, NormalizedOperand, NormalizedOperand]; + +export type ShorthandOperandList = + | [] + | [ShorthandOperand] + | [ShorthandOperand, ShorthandOperand] + | [ShorthandOperand, ShorthandOperand, ShorthandOperand]; + +export type ShorthandOperand = `${string}:${OperandType}`; diff --git a/packages/@glimmer/debug/lib/dism/operands.ts b/packages/@glimmer/debug/lib/dism/operands.ts new file mode 100644 index 0000000000..98bb312e93 --- /dev/null +++ b/packages/@glimmer/debug/lib/dism/operands.ts @@ -0,0 +1,117 @@ +import type { BlockMetadata, ProgramConstants, ProgramHeap } from '@glimmer/interfaces'; +import { decodeHandle } from '@glimmer/constants'; + +import type { RawDisassembledOperand } from '../debug'; +import type { + NonNullableOperandType, + NormalizedOperand, + NullableOperandType, + OperandType, +} from './operand-types'; + +import { decodeCurry, decodePrimitive, decodeRegister } from '../debug'; + +interface DisassemblyState { + readonly offset: number; + readonly label: NormalizedOperand; + readonly value: number; + readonly constants: ProgramConstants; + readonly heap: ProgramHeap; + readonly meta: BlockMetadata | null; +} + +export type OperandDisassembler = (options: DisassemblyState) => RawDisassembledOperand; + +const todo: OperandDisassembler = ({ label, value }) => ['error:operand', value, { label }]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Left> = D extends Disassembler + ? Exclude + : never; + +type AllOperands = OperandType; + +class Disassembler { + static build( + builder: (disassembler: Disassembler) => Disassembler + ): Record { + return builder(new Disassembler()).#disms as Record; + } + + readonly #disms: Record; + + private constructor() { + this.#disms = {}; + } + + addNullable & NullableOperandType>( + names: K[], + dism: OperandDisassembler + ): Disassembler { + for (const name of names) { + this.#disms[name] = dism; + this.#disms[`${name}?`] = dism; + } + + return this as Disassembler; + } + + add & NonNullableOperandType>( + names: K[], + dism: OperandDisassembler + ): Disassembler { + const add = (name: K, dism: OperandDisassembler) => (this.#disms[name] = dism); + for (const name of names) { + add(name, dism); + } + + return this; + } +} + +export const OPERANDS = Disassembler.build((d) => { + return d + .add(['imm/u32', 'imm/i32', 'imm/u32{todo}', 'imm/i32{todo}'], ({ value }) => ['number', value]) + .add(['const/i32[]'], ({ value, constants }) => [ + 'array', + constants.getArray(value), + { kind: Number }, + ]) + .add(['const/bool'], ({ value }) => ['boolean', !!value]) + .add(['imm/bool'], ({ value, constants }) => [ + 'boolean', + constants.getValue(decodeHandle(value)), + ]) + .add(['handle'], ({ constants, value }) => ['constant', constants.getValue(value)]) + .add(['handle/block'], ({ value, heap }) => ['instruction', heap.getaddr(value)]) + .add(['imm/pc'], ({ value }) => ['instruction', value]) + .add(['const/any[]'], ({ value, constants }) => ['array', constants.getArray(value)]) + .add(['const/primitive'], ({ value, constants }) => [ + 'primitive', + decodePrimitive(value, constants), + ]) + .add(['register'], ({ value }) => ['register', decodeRegister(value)]) + .add(['const/any'], ({ value, constants }) => ['dynamic', constants.getValue(value)]) + .add(['variable'], ({ value, meta }) => { + return ['variable', value, { name: meta?.symbols.lexical?.at(value) ?? null }]; + }) + .add(['register/instruction'], ({ value }) => ['instruction', value]) + .add(['imm/enum'], ({ value }) => ['enum', decodeCurry(value)]) + .addNullable(['const/str'], ({ value, constants }) => [ + 'string', + constants.getValue(value), + ]) + .addNullable(['const/str[]'], ({ value, constants }) => [ + 'array', + constants.getArray(value), + { kind: String }, + ]) + .add(['imm/block:handle'], todo) + .add(['const/definition'], todo) + .add(['const/fn'], todo) + .add(['instruction/relative'], ({ value, offset }) => ['instruction', offset + value]) + .add(['register/sN'], todo) + .add(['register/stack'], todo) + .add(['register/tN'], todo) + .add(['register/v0'], todo); +}); diff --git a/packages/@glimmer/debug/lib/metadata.ts b/packages/@glimmer/debug/lib/metadata.ts index c89ef2250b..83e2791e5c 100644 --- a/packages/@glimmer/debug/lib/metadata.ts +++ b/packages/@glimmer/debug/lib/metadata.ts @@ -1,5 +1,7 @@ import type { Dict, Nullable, PresentArray } from '@glimmer/interfaces'; +import type { ShorthandOperand, ShorthandOperandList } from './dism/operand-types'; + // TODO: How do these map onto constant and machine types? export const OPERAND_TYPES = [ 'u32', @@ -18,11 +20,6 @@ export const OPERAND_TYPES = [ 'scope', ]; -function isOperandType(s: string): s is OperandType { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return OPERAND_TYPES.indexOf(s as any) !== -1; -} - export type OperandType = (typeof OPERAND_TYPES)[number]; export interface Operand { @@ -30,24 +27,21 @@ export interface Operand { name: string; } -export type OperandList = ([] | [Operand] | [Operand, Operand] | [Operand, Operand, Operand]) & - Operand[]; - export interface NormalizedMetadata { name: string; mnemonic: string; - before: null; stackChange: Nullable; - ops: OperandList; - operands: number; - check: boolean; + /** @default [] */ + ops?: ShorthandOperandList; + /** @default true */ + check?: boolean; } export type Stack = [string[], string[]]; export interface RawOperandMetadata { kind: 'machine' | 'syscall'; - format: RawOperandFormat; + format: ShorthandOperandList; skip?: true; operation: string; 'operand-stack'?: [string[], string[]]; @@ -58,27 +52,23 @@ export type OperandName = `${string}:${string}`; export type RawOperandFormat = OperandName | PresentArray; export function normalize(key: string, input: RawOperandMetadata): NormalizedMetadata { - let name: string; + let name: ShorthandOperand; if (input.format === undefined) { throw new Error(`Missing format in ${JSON.stringify(input)}`); } if (Array.isArray(input.format)) { - name = input.format[0]; + name = input.format[0]!; } else { name = input.format; } - let ops: OperandList = Array.isArray(input.format) ? operands(input.format.slice(1)) : []; - return { name, mnemonic: key, - before: null, stackChange: stackChange(input['operand-stack']), - ops, - operands: ops.length, + ops: input.format, check: input.skip === true ? false : true, }; } @@ -105,23 +95,6 @@ function hasRest(input: string[]): boolean { return input.some((s) => s.slice(-3) === '...'); } -function operands(input: `${string}:${string}`[]): OperandList { - if (!Array.isArray(input)) { - throw new Error(`Expected operands array, got ${JSON.stringify(input)}`); - } - return input.map(op) as OperandList; -} - -function op(input: `${string}:${string}`): Operand { - let [name, type] = input.split(':') as [string, string]; - - if (isOperandType(type)) { - return { name, type }; - } else { - throw new Error(`Expected operand, found ${JSON.stringify(input)}`); - } -} - export interface NormalizedOpcodes { readonly machine: Dict; readonly syscall: Dict; diff --git a/packages/@glimmer/debug/lib/opcode-metadata.ts b/packages/@glimmer/debug/lib/opcode-metadata.ts index c80f99d374..85df9827ec 100644 --- a/packages/@glimmer/debug/lib/opcode-metadata.ts +++ b/packages/@glimmer/debug/lib/opcode-metadata.ts @@ -2,6 +2,7 @@ import type { Nullable, VmMachineOp, VmOp } from '@glimmer/interfaces'; import { + isMachineOp, VM_APPEND_DOCUMENT_FRAGMENT_OP, VM_APPEND_HTML_OP, VM_APPEND_NODE_OP, @@ -105,15 +106,12 @@ import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import type { NormalizedMetadata } from './metadata'; -export function opcodeMetadata( - op: VmMachineOp | VmOp, - isMachine: 0 | 1 -): Nullable { +export function opcodeMetadata(op: VmOp | VmMachineOp): Nullable { if (!LOCAL_DEBUG) { return null; } - let value = isMachine ? MACHINE_METADATA[op] : METADATA[op]; + let value = isMachineOp(op) ? MACHINE_METADATA[op] : METADATA[op]; return value || null; } @@ -125,1300 +123,634 @@ if (LOCAL_DEBUG) { MACHINE_METADATA[VM_PUSH_FRAME_OP] = { name: 'PushFrame', mnemonic: 'pushf', - before: null, stackChange: 2, - ops: [], - operands: 0, - check: true, }; MACHINE_METADATA[VM_POP_FRAME_OP] = { name: 'PopFrame', mnemonic: 'popf', - before: null, stackChange: -2, - ops: [], - operands: 0, check: false, }; MACHINE_METADATA[VM_INVOKE_VIRTUAL_OP] = { name: 'InvokeVirtual', mnemonic: 'vcall', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; MACHINE_METADATA[VM_INVOKE_STATIC_OP] = { name: 'InvokeStatic', mnemonic: 'scall', - before: null, stackChange: 0, - ops: [ - { - name: 'offset', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['offset:handle/block'], }; MACHINE_METADATA[VM_JUMP_OP] = { name: 'Jump', mnemonic: 'goto', - before: null, stackChange: 0, - ops: [ - { - name: 'to', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['to:instruction/relative'], }; MACHINE_METADATA[VM_RETURN_OP] = { name: 'Return', mnemonic: 'ret', - before: null, stackChange: 0, - ops: [], - operands: 0, check: false, }; MACHINE_METADATA[VM_RETURN_TO_OP] = { name: 'ReturnTo', mnemonic: 'setra', - before: null, stackChange: 0, - ops: [ - { - name: 'offset', - type: 'i32', - }, - ], - operands: 1, - check: true, + ops: ['offset:instruction/relative'], }; + METADATA[VM_HELPER_OP] = { name: 'Helper', mnemonic: 'ncall', - before: null, stackChange: null, - ops: [ - { - name: 'helper', - type: 'handle', - }, - ], - operands: 1, - check: true, + ops: ['helper:handle'], }; METADATA[VM_DYNAMIC_HELPER_OP] = { name: 'DynamicHelper', mnemonic: 'dynamiccall', - before: null, stackChange: null, - ops: [], - operands: 0, - check: true, }; METADATA[VM_SET_NAMED_VARIABLES_OP] = { name: 'SetNamedVariables', mnemonic: 'vsargs', - before: null, stackChange: 0, - ops: [ - { - name: 'register', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['register:register'], }; METADATA[VM_SET_BLOCKS_OP] = { name: 'SetBlocks', mnemonic: 'vbblocks', - before: null, stackChange: 0, - ops: [ - { - name: 'register', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['register:register'], }; METADATA[VM_SET_VARIABLE_OP] = { name: 'SetVariable', mnemonic: 'sbvar', - before: null, stackChange: -1, - ops: [ - { - name: 'symbol', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['symbol:variable'], }; METADATA[VM_SET_BLOCK_OP] = { name: 'SetBlock', mnemonic: 'sblock', - before: null, stackChange: -3, - ops: [ - { - name: 'symbol', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['symbol:variable'], }; METADATA[VM_GET_VARIABLE_OP] = { name: 'GetVariable', mnemonic: 'symload', - before: null, stackChange: 1, - ops: [ - { - name: 'symbol', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['symbol:variable'], }; METADATA[VM_GET_PROPERTY_OP] = { name: 'GetProperty', mnemonic: 'getprop', - before: null, stackChange: 0, - ops: [ - { - name: 'property', - type: 'str', - }, - ], - operands: 1, - check: true, + ops: ['property:const/str'], }; METADATA[VM_GET_BLOCK_OP] = { name: 'GetBlock', mnemonic: 'blockload', - before: null, stackChange: 1, - ops: [ - { - name: 'block', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['block:variable'], }; METADATA[VM_SPREAD_BLOCK_OP] = { name: 'SpreadBlock', mnemonic: 'blockspread', - before: null, stackChange: 2, - ops: [], - operands: 0, - check: true, }; METADATA[VM_HAS_BLOCK_OP] = { name: 'HasBlock', mnemonic: 'hasblockload', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_HAS_BLOCK_PARAMS_OP] = { name: 'HasBlockParams', mnemonic: 'hasparamsload', - before: null, stackChange: -2, - ops: [], - operands: 0, - check: true, }; METADATA[VM_CONCAT_OP] = { name: 'Concat', mnemonic: 'concat', - before: null, stackChange: null, - ops: [ - { - name: 'count', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['count:imm/u32'], }; METADATA[VM_IF_INLINE_OP] = { name: 'IfInline', mnemonic: 'ifinline', - before: null, stackChange: -2, - ops: [ - { - name: 'count', - type: 'u32', - }, - ], - operands: 1, - check: true, }; METADATA[VM_NOT_OP] = { name: 'Not', mnemonic: 'not', - before: null, stackChange: 0, - ops: [ - { - name: 'count', - type: 'u32', - }, - ], - operands: 1, - check: true, }; METADATA[VM_CONSTANT_OP] = { name: 'Constant', mnemonic: 'rconstload', - before: null, stackChange: 1, - ops: [ - { - name: 'constant', - type: 'unknown', - }, - ], - operands: 1, - check: true, + ops: ['constant:const/any'], }; METADATA[VM_CONSTANT_REFERENCE_OP] = { name: 'ConstantReference', mnemonic: 'rconstrefload', - before: null, stackChange: 1, - ops: [ - { - name: 'constant', - type: 'unknown', - }, - ], - operands: 1, - check: true, + ops: ['constant:const/any'], }; METADATA[VM_PRIMITIVE_OP] = { name: 'Primitive', mnemonic: 'pconstload', - before: null, stackChange: 1, - ops: [ - { - name: 'constant', - type: 'primitive', - }, - ], - operands: 1, - check: true, + ops: ['constant:const/primitive'], }; METADATA[VM_PRIMITIVE_REFERENCE_OP] = { name: 'PrimitiveReference', mnemonic: 'ptoref', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_REIFY_U32_OP] = { name: 'ReifyU32', mnemonic: 'reifyload', - before: null, stackChange: 1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_DUP_OP] = { name: 'Dup', mnemonic: 'dup', - before: null, stackChange: 1, - ops: [ - { - name: 'register', - type: 'u32', - }, - { - name: 'offset', - type: 'u32', - }, - ], - operands: 2, - check: true, + ops: ['register:register', 'offset:imm/u32'], }; METADATA[VM_POP_OP] = { name: 'Pop', mnemonic: 'pop', - before: null, stackChange: 0, - ops: [ - { - name: 'count', - type: 'u32', - }, - ], - operands: 1, + ops: ['count:imm/u32'], check: false, }; METADATA[VM_LOAD_OP] = { name: 'Load', mnemonic: 'put', - before: null, stackChange: -1, - ops: [ - { - name: 'register', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['register:register'], }; METADATA[VM_FETCH_OP] = { name: 'Fetch', mnemonic: 'regload', - before: null, stackChange: 1, - ops: [ - { - name: 'register', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['register:register'], }; METADATA[VM_ROOT_SCOPE_OP] = { name: 'RootScope', mnemonic: 'rscopepush', - before: null, stackChange: 0, - ops: [ - { - name: 'symbols', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['symbols:imm/u32'], }; METADATA[VM_VIRTUAL_ROOT_SCOPE_OP] = { name: 'VirtualRootScope', mnemonic: 'vrscopepush', - before: null, stackChange: 0, - ops: [ - { - name: 'register', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['register:register'], }; METADATA[VM_CHILD_SCOPE_OP] = { name: 'ChildScope', mnemonic: 'cscopepush', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_POP_SCOPE_OP] = { name: 'PopScope', mnemonic: 'scopepop', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_TEXT_OP] = { name: 'Text', mnemonic: 'apnd_text', - before: null, stackChange: 0, - ops: [ - { - name: 'contents', - type: 'str', - }, - ], - operands: 1, - check: true, + ops: ['contents:const/str'], }; METADATA[VM_COMMENT_OP] = { name: 'Comment', mnemonic: 'apnd_comment', - before: null, stackChange: 0, - ops: [ - { - name: 'contents', - type: 'str', - }, - ], - operands: 1, - check: true, + ops: ['contents:const/str'], }; METADATA[VM_APPEND_HTML_OP] = { name: 'AppendHTML', mnemonic: 'apnd_dynhtml', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_APPEND_SAFE_HTML_OP] = { name: 'AppendSafeHTML', mnemonic: 'apnd_dynshtml', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_APPEND_DOCUMENT_FRAGMENT_OP] = { name: 'AppendDocumentFragment', mnemonic: 'apnd_dynfrag', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_APPEND_NODE_OP] = { name: 'AppendNode', mnemonic: 'apnd_dynnode', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_APPEND_TEXT_OP] = { name: 'AppendText', mnemonic: 'apnd_dyntext', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_OPEN_ELEMENT_OP] = { name: 'OpenElement', mnemonic: 'apnd_tag', - before: null, stackChange: 0, - ops: [ - { - name: 'tag', - type: 'str', - }, - ], - operands: 1, - check: true, + ops: ['tag:const/str'], }; METADATA[VM_OPEN_DYNAMIC_ELEMENT_OP] = { name: 'OpenDynamicElement', mnemonic: 'apnd_dyntag', - before: null, stackChange: -1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_PUSH_REMOTE_ELEMENT_OP] = { name: 'PushRemoteElement', mnemonic: 'apnd_remotetag', - before: null, stackChange: -3, - ops: [], - operands: 0, - check: true, }; METADATA[VM_STATIC_ATTR_OP] = { name: 'StaticAttr', mnemonic: 'apnd_attr', - before: null, stackChange: 0, - ops: [ - { - name: 'name', - type: 'str', - }, - { - name: 'value', - type: 'str', - }, - { - name: 'namespace', - type: 'option-str', - }, - ], - operands: 3, - check: true, + ops: ['name:const/str', 'value:const/str', 'namespace:const/str?'], }; METADATA[VM_DYNAMIC_ATTR_OP] = { name: 'DynamicAttr', mnemonic: 'apnd_dynattr', - before: null, stackChange: -1, - ops: [ - { - name: 'name', - type: 'str', - }, - { - name: 'trusting', - type: 'bool', - }, - { - name: 'namespace', - type: 'option-str', - }, - ], - operands: 3, - check: true, + ops: ['name:const/str', 'value:const/str'], }; METADATA[VM_COMPONENT_ATTR_OP] = { name: 'ComponentAttr', mnemonic: 'apnd_cattr', - before: null, stackChange: -1, - ops: [ - { - name: 'name', - type: 'str', - }, - { - name: 'trusting', - type: 'bool', - }, - { - name: 'namespace', - type: 'option-str', - }, - ], - operands: 3, - check: true, + ops: ['name:const/str', 'value:const/str', 'namespace:const/str?'], }; METADATA[VM_FLUSH_ELEMENT_OP] = { name: 'FlushElement', mnemonic: 'apnd_flushtag', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_CLOSE_ELEMENT_OP] = { name: 'CloseElement', mnemonic: 'apnd_closetag', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_POP_REMOTE_ELEMENT_OP] = { name: 'PopRemoteElement', mnemonic: 'apnd_closeremotetag', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_MODIFIER_OP] = { name: 'Modifier', mnemonic: 'apnd_modifier', - before: null, stackChange: -1, - ops: [ - { - name: 'helper', - type: 'handle', - }, - ], - operands: 1, - check: true, + ops: ['helper:handle'], }; METADATA[VM_BIND_DYNAMIC_SCOPE_OP] = { name: 'BindDynamicScope', mnemonic: 'setdynscope', - before: null, stackChange: null, - ops: [ - { - name: 'names', - type: 'str-array', - }, - ], - operands: 1, - check: true, + ops: ['names:const/str[]'], }; METADATA[VM_PUSH_DYNAMIC_SCOPE_OP] = { name: 'PushDynamicScope', mnemonic: 'dynscopepush', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_POP_DYNAMIC_SCOPE_OP] = { name: 'PopDynamicScope', mnemonic: 'dynscopepop', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_COMPILE_BLOCK_OP] = { name: 'CompileBlock', mnemonic: 'cmpblock', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_PUSH_BLOCK_SCOPE_OP] = { name: 'PushBlockScope', mnemonic: 'scopeload', - before: null, stackChange: 1, - ops: [ - { - name: 'scope', - type: 'scope', - }, - ], - operands: 1, - check: true, }; METADATA[VM_PUSH_SYMBOL_TABLE_OP] = { name: 'PushSymbolTable', mnemonic: 'dsymload', - before: null, stackChange: 1, - ops: [ - { - name: 'table', - type: 'symbol-table', - }, - ], - operands: 1, - check: true, }; METADATA[VM_INVOKE_YIELD_OP] = { name: 'InvokeYield', mnemonic: 'invokeyield', - before: null, stackChange: null, - ops: [], - operands: 0, - check: true, }; METADATA[VM_JUMP_IF_OP] = { name: 'JumpIf', mnemonic: 'iftrue', - before: null, stackChange: -1, - ops: [ - { - name: 'to', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['to:instruction/relative'], }; METADATA[VM_JUMP_UNLESS_OP] = { name: 'JumpUnless', mnemonic: 'iffalse', - before: null, stackChange: -1, - ops: [ - { - name: 'to', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['to:instruction/relative'], }; METADATA[VM_JUMP_EQ_OP] = { name: 'JumpEq', mnemonic: 'ifeq', - before: null, stackChange: 0, - ops: [ - { - name: 'to', - type: 'i32', - }, - { - name: 'comparison', - type: 'i32', - }, - ], - operands: 2, - check: true, + ops: ['to:instruction/relative', 'comparison:imm/i32'], }; METADATA[VM_ASSERT_SAME_OP] = { name: 'AssertSame', mnemonic: 'assert_eq', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_ENTER_OP] = { name: 'Enter', mnemonic: 'blk_start', - before: null, stackChange: 0, - ops: [ - { - name: 'args', - type: 'u32', - }, - ], - operands: 1, - check: true, + ops: ['args:imm/u32'], }; METADATA[VM_EXIT_OP] = { name: 'Exit', mnemonic: 'blk_end', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_TO_BOOLEAN_OP] = { name: 'ToBoolean', mnemonic: 'anytobool', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_ENTER_LIST_OP] = { name: 'EnterList', mnemonic: 'list_start', - before: null, stackChange: null, - ops: [ - { - name: 'address', - type: 'u32', - }, - { - name: 'address', - type: 'u32', - }, - ], - operands: 2, - check: true, + ops: ['start:instruction/relative', 'else:instruction/relative'], }; METADATA[VM_EXIT_LIST_OP] = { name: 'ExitList', mnemonic: 'list_end', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_ITERATE_OP] = { name: 'Iterate', mnemonic: 'iter', - before: null, stackChange: 0, - ops: [ - { - name: 'end', - type: 'u32', - }, - ], - operands: 1, + ops: ['end:instruction/relative'], check: false, }; METADATA[VM_MAIN_OP] = { name: 'Main', mnemonic: 'main', - before: null, stackChange: -2, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_CONTENT_TYPE_OP] = { name: 'ContentType', mnemonic: 'ctload', - before: null, stackChange: 1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_DYNAMIC_CONTENT_TYPE_OP] = { name: 'DynamicContentType', mnemonic: 'dctload', - before: null, stackChange: 1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_CURRY_OP] = { name: 'Curry', mnemonic: 'curry', - before: null, stackChange: null, - ops: [ - { - name: 'type', - type: 'u32', - }, - { - name: 'is-strict', - type: 'bool', - }, - ], - operands: 2, - check: true, + ops: ['type:imm/enum', 'strict?:const/bool'], }; METADATA[VM_PUSH_COMPONENT_DEFINITION_OP] = { name: 'PushComponentDefinition', mnemonic: 'cmload', - before: null, stackChange: 1, - ops: [ - { - name: 'spec', - type: 'handle', - }, - ], - operands: 1, - check: true, + ops: ['spec:handle'], }; METADATA[VM_PUSH_DYNAMIC_COMPONENT_INSTANCE_OP] = { name: 'PushDynamicComponentInstance', mnemonic: 'dciload', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_RESOLVE_DYNAMIC_COMPONENT_OP] = { name: 'ResolveDynamicComponent', mnemonic: 'cdload', - before: null, stackChange: 0, - ops: [ - { - name: 'owner', - type: 'owner', - }, - ], - operands: 1, - check: true, + ops: ['strict?:imm/bool'], }; METADATA[VM_PUSH_ARGS_OP] = { name: 'PushArgs', mnemonic: 'argsload', - before: null, stackChange: null, - ops: [ - { - name: 'names', - type: 'str-array', - }, - { - name: 'block-names', - type: 'str-array', - }, - { - name: 'flags', - type: 'u32', - }, - ], - operands: 3, - check: true, + ops: ['names:const/str[]', 'block-names:const/str[]', 'flags:imm/u32'], }; METADATA[VM_PUSH_EMPTY_ARGS_OP] = { name: 'PushEmptyArgs', mnemonic: 'emptyargsload', - before: null, stackChange: 1, - ops: [], - operands: 0, - check: true, }; METADATA[VM_POP_ARGS_OP] = { name: 'PopArgs', mnemonic: 'argspop', - before: null, stackChange: null, - ops: [], - operands: 0, - check: true, }; METADATA[VM_PREPARE_ARGS_OP] = { name: 'PrepareArgs', mnemonic: 'argsprep', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, + ops: ['state:register'], check: false, }; METADATA[VM_CAPTURE_ARGS_OP] = { name: 'CaptureArgs', mnemonic: 'argscapture', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_CREATE_COMPONENT_OP] = { name: 'CreateComponent', mnemonic: 'comp_create', - before: null, stackChange: 0, - ops: [ - { - name: 'flags', - type: 'u32', - }, - { - name: 'state', - type: 'register', - }, - ], - operands: 2, - check: true, + ops: ['flags:imm/i32'], }; METADATA[VM_REGISTER_COMPONENT_DESTRUCTOR_OP] = { name: 'RegisterComponentDestructor', mnemonic: 'comp_dest', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_PUT_COMPONENT_OPERATIONS_OP] = { name: 'PutComponentOperations', mnemonic: 'comp_elops', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_GET_COMPONENT_SELF_OP] = { name: 'GetComponentSelf', mnemonic: 'comp_selfload', - before: null, stackChange: 1, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_GET_COMPONENT_TAG_NAME_OP] = { name: 'GetComponentTagName', mnemonic: 'comp_tagload', - before: null, stackChange: 1, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_GET_COMPONENT_LAYOUT_OP] = { name: 'GetComponentLayout', mnemonic: 'comp_layoutload', - before: null, stackChange: 2, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_BIND_DEBUGGER_SCOPE_OP] = { name: 'BindDebuggerScope', mnemonic: 'debugger_scope', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_SETUP_FOR_DEBUGGER_OP] = { name: 'SetupForDebugger', mnemonic: 'debugger_setup', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_POPULATE_LAYOUT_OP] = { name: 'PopulateLayout', mnemonic: 'comp_layoutput', - before: null, stackChange: -2, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_INVOKE_COMPONENT_LAYOUT_OP] = { name: 'InvokeComponentLayout', mnemonic: 'comp_invokelayout', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_BEGIN_COMPONENT_TRANSACTION_OP] = { name: 'BeginComponentTransaction', mnemonic: 'comp_begin', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_COMMIT_COMPONENT_TRANSACTION_OP] = { name: 'CommitComponentTransaction', mnemonic: 'comp_commit', - before: null, stackChange: 0, - ops: [], - operands: 0, - check: true, }; METADATA[VM_DID_CREATE_ELEMENT_OP] = { name: 'DidCreateElement', mnemonic: 'comp_created', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_DID_RENDER_LAYOUT_OP] = { name: 'DidRenderLayout', mnemonic: 'comp_rendered', - before: null, stackChange: 0, - ops: [ - { - name: 'state', - type: 'register', - }, - ], - operands: 1, - check: true, + ops: ['state:register'], }; METADATA[VM_DEBUGGER_OP] = { name: 'Debugger', mnemonic: 'debugger', - before: null, stackChange: 0, - ops: [ - { - name: 'symbols', - type: 'str-array', - }, - { - name: 'debugInfo', - type: 'array', - }, - ], - operands: 2, - check: true, + ops: ['symbols:const/any', 'debugInfo:const/i32[]'], }; } diff --git a/packages/@glimmer/debug/lib/render/annotations.ts b/packages/@glimmer/debug/lib/render/annotations.ts new file mode 100644 index 0000000000..70cda33b8c --- /dev/null +++ b/packages/@glimmer/debug/lib/render/annotations.ts @@ -0,0 +1,9 @@ +export const ANNOTATION_STYLES = [ + 'background-color: oklch(93% 0.03 300); color: oklch(34% 0.18 300)', + 'background-color: oklch(93% 0.03 250); color: oklch(34% 0.18 250)', + 'background-color: oklch(93% 0.03 200); color: oklch(34% 0.18 200)', + 'background-color: oklch(93% 0.03 150); color: oklch(34% 0.18 150)', + 'background-color: oklch(93% 0.03 100); color: oklch(34% 0.18 100)', + 'background-color: oklch(93% 0.03 50); color: oklch(34% 0.18 50)', + 'background-color: oklch(93% 0.03 0); color: oklch(34% 0.18 0)', +] as const; diff --git a/packages/@glimmer/debug/lib/render/basic.ts b/packages/@glimmer/debug/lib/render/basic.ts new file mode 100644 index 0000000000..68d75020c4 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/basic.ts @@ -0,0 +1,126 @@ +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 { 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: '' }); +} + +export function join(frags: IntoFragment[], separator?: Optional): Fragment { + const sep = separator ? intoFragment(separator) : empty(); + + if (frags.length === 0) { + return empty(); + } + + let seenUnsubtle = false; + let seenAny = false; + + const output: LeafFragment[] = []; + + for (const frag of frags) { + const fragment = intoFragment(frag); + const isSubtle = fragment.isSubtle(); + const sepIsSubtle = isSubtle || !seenUnsubtle; + + // If the succeeding fragment is subtle, the separator is also subtle. If the succeeding + // fragment is unstubtle, the separator is unsubtle only if we've already seen an unsubtle + // fragment. This ensures that separators are not ultimately present if the next element is not + // printed. + + if (seenAny) { + output.push(...sep.subtle(sepIsSubtle).leaves()); + } + + output.push(...fragment.leaves()); + seenUnsubtle ||= !isSubtle; + seenAny = true; + } + + return new Fragment({ kind: 'multi', value: output }); +} + +export type ValueRefOptions = { annotation: string } | { ref: string; value?: IntoFragment }; + +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; + + if ('annotation' in options) { + return { ref: options.annotation, footnote: intoFragment(options.annotation) }; + } else { + return { + ref: options.ref, + footnote: options.value ? intoFragment(options.value) : undefined, + }; + } + }; + + return new Fragment({ + kind: 'value', + value: val, + display: normalize(), + }); +} + +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/buffer.ts b/packages/@glimmer/debug/lib/render/buffer.ts new file mode 100644 index 0000000000..bf1f7e1505 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/buffer.ts @@ -0,0 +1,167 @@ +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. + * + * This queuing serves two purposes: + * + * 1. To allow the individual fragments that make up a single line to append their values to + * the current line. To accomplish this, each fragment can append static content and its + * formatting specifier (e.g. `%o`) to the accumulated {@link #template} *and* append the + * value to format to the {@link #substitutions} array. + * 2. To allow logs that refer to objects to be represented as footnotes in the current line, + * with the footnote to be printed in a later line. + * + * This allows a list of fragments, each of which represent formattable values, to be flattened + * into a single template string and an array of values to format. + * + * ## Footnotes + * + * An opcode slice containing constant references will be logged like this: + * + * ``` + * ... + * 362. (PushArgs names=[] block-names=[] flags=16) + * 366. (Helper helper=[0]) + * [0] glimmerHelper() + * 368. (PopFrame) + * 369. (Fetch register=$v0) + * 371. (Primitive constant="/index.html") + * ... + * ``` + * + * The fragment for line `366` includes an `ObjectFragment` for the helper value. When logged, + * the object will be represented as a footnote and the value will be printed in a later + * line. + */ +export class LogFragmentBuffer { + /** + * The first parameter to the `console.log` family of APIs is a *template* that can use + * format specifiers (e.g. `%c`, `%o`, and `%O`) to refer to subsequent parameters. + * + * When a fragment is appended to a line, + */ + #template = ''; + + /** + * Each format specified in the {@link #template} corresponds to a value in the + * `#substitutions` array. + */ + readonly #substitutions: unknown[] = []; + + /** + * The logging options for the buffer, which currently only contains `showSubtle`. + * + * When fragments call the buffer's {@linkcode append} method, they specify whether the + * content to append is subtle or not. If the buffer is not configured to show subtle + * content, the content is not appended. + * + * This allows fragments to append content to the buffer without having to know how the + * buffer is configured. + */ + readonly #options: DisplayFragmentOptions; + + /** + * A single line can produce multiple queued log entries. This happens when fragments + * append *footnotes* to the buffer. A *reference* to the footnote is appended to the + * primary line, and a line containing the *value* of the footnote is appended to the + * `#queued` array. + * + * Both the primary line and any queued footnotes are flushed together when the buffer + * is flushed. + */ + readonly #footnotes: QueuedEntry[] = []; + #nextFootnote = 1; + #style = 0; + + constructor(options: DisplayFragmentOptions) { + this.#options = options; + } + + /** + * Add a footnoted value to the current buffer. + * + * If the `subtle` option is set, the fragment will only be printed if the buffer is configured + * to show subtle content. + * + * This method takes two callbacks: `add` and `append`. + * + * The `append` callback behaves like {@linkcode append}, but without the `subtle` argument. If + * `addFootnoted` is called with `subtle: false`, then the callback will never be called, so + * there is no need to pass the `subtle` argument again. + * + * The `add` callback is responsible for appending the footnote itself to the buffer. The first + * parameter to `add` (`useNumber`) specifies whether the caller has used the footnote number + * to refer to the footnote. + * + * This is typically true, but fragments can specify an alternative annotation that should be used + * instead of the default footnote number. In that case, the footnote number is not used, and the + * next footnote is free to use it. + * + * 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: { n: number; style: string }, child: LogFragmentBuffer) => boolean + ) { + if (subtle && !this.#options.showSubtle) return; + + const child = new LogFragmentBuffer(this.#options); + + const style = ANNOTATION_STYLES[this.#style++ % ANNOTATION_STYLES.length] as string; + + const usedNumber = add({ n: this.#nextFootnote, style }, child); + + if (usedNumber) { + this.#nextFootnote += 1; + } + + this.#footnotes.push({ + type: 'line', + subtle: false, + template: child.#template, + substitutions: child.#substitutions, + }); + + this.#footnotes.push(...child.#footnotes); + } + + /** + * Append a fragment to the current buffer. + * + * If the `subtle` option is set, the fragment will only be printed if the buffer is configured + * to show subtle content. + */ + append(subtle: boolean, template: string, ...substitutions: unknown[]) { + if (subtle && !this.#options.showSubtle) return; + this.#template += template; + + this.#substitutions.push(...substitutions); + } + + #mapLine(line: QueuedLine): LogLine[] { + if (line.subtle && !this.#options.showSubtle) return []; + return [{ type: 'line', line: [line.template, ...line.substitutions] }]; + } + + flush(): FlushedLines { + return [ + { type: 'line', line: [this.#template, ...this.#substitutions] }, + ...this.#footnotes.flatMap((queued) => this.#mapLine(queued)), + ]; + } +} + +interface QueuedLine { + type: 'line'; + subtle: boolean; + template: string; + substitutions: unknown[]; +} + +type QueuedEntry = QueuedLine; diff --git a/packages/@glimmer/debug/lib/render/combinators.ts b/packages/@glimmer/debug/lib/render/combinators.ts new file mode 100644 index 0000000000..edf4cc3e53 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/combinators.ts @@ -0,0 +1,111 @@ +import type { Fragment, IntoFragment } from './fragment'; + +import { group, join, value } from './basic'; +import { as, frag, intoFragment } from './fragment'; + +/** + * The prepend function returns a subtle fragment if the contents are subtle. + */ +export function prepend(before: IntoFragment, contents: Fragment): Fragment { + return contents.map((f) => frag`${before}${f}`); +} + +/** + * The append function returns a subtle fragment if the contents are subtle. + */ +function append(contents: Fragment, after: IntoFragment): Fragment { + return contents.map((f) => frag`${f}${after}`); +} +/** + * The `wrap` function returns a subtle fragment if the contents are subtle. + */ +export function wrap(start: IntoFragment, contents: Fragment, end: IntoFragment) { + return append(prepend(start, contents), end); +} + +export type As = (value: T) => Fragment; + +interface EntriesOptions { + as?: As; + subtle?: boolean | undefined | ((value: T) => boolean); +} +function normalizeOptions(options: EntriesOptions | undefined): { + map: (value: T) => Fragment; + isSubtle: (value: T) => boolean; +} { + let isSubtle: (value: T) => boolean; + + const subtleOption = options?.subtle; + if (typeof subtleOption === 'boolean') { + isSubtle = () => subtleOption; + } else if (typeof subtleOption === 'function') { + isSubtle = subtleOption; + } else { + isSubtle = () => false; + } + + return { + map: options?.as ?? ((value) => intoFragment(value as IntoFragment)), + isSubtle, + }; +} + +/** + * A compact array makes the wrapping `[]` subtle if there's only one element. + */ +export function compactArray( + items: readonly T[], + options: EntriesOptions & { + when: { + allSubtle: IntoFragment; + empty?: IntoFragment; + }; + } +): Fragment { + const [first] = items; + + if (first === undefined) { + return options.when?.empty ? intoFragment(options.when.empty) : frag`[]`.subtle(); + } + + const { map, isSubtle } = normalizeOptions(options); + + const contents = items.map((item) => (isSubtle(item) ? frag`${map(item)}`.subtle() : map(item))); + const body = join(contents, ', '); + + const unsubtle = contents.filter((f) => !f.isSubtle()); + + if (unsubtle.length === 0) { + return intoFragment(options.when.allSubtle).subtle(); + } else if (unsubtle.length === 1) { + return group(frag`[`.subtle(), body, frag`]`.subtle()); + } else { + return wrap('[ ', body, ' ]'); + } +} + +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( + items: unknown[] | readonly unknown[], + options?: EntriesOptions +): Fragment { + if (items.length === 0) { + return frag`[]`; + } else { + const { map, isSubtle } = normalizeOptions(options); + + const contents = items.map((item) => + isSubtle(item) ? frag`${map(item)}`.subtle() : map(item) + ); + return wrap('[ ', join(contents, as.punct(', ')), ' ]'); + } +} + +export function ifSubtle(fragment: IntoFragment): Fragment { + return intoFragment(fragment).subtle(); +} diff --git a/packages/@glimmer/debug/lib/render/entry.ts b/packages/@glimmer/debug/lib/render/entry.ts new file mode 100644 index 0000000000..3e6fd2cfd8 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/entry.ts @@ -0,0 +1,30 @@ +/** + * A Loggable is either: + * + * 1. a single log line + * 2. a log line as a header followed by a group of log entries + */ +export type Loggable = [LogLine, ...LogEntry[]]; + +export type LogEntry = LogLine | LogGroup; + +/** + * LogLine represents a single line in the log. The line is logged *either* by passing the `line` + * values to `console.{log,info,debug,warn,error}` *or* by passing them to `console.group` to + * represent the header of a group. + */ +export interface LogLine { + readonly type: 'line'; + readonly line: unknown[]; +} + +/** + * LogGroup represents a group of log entries. It is logged by calling *either* `console.group` or + * `console.groupCollapsed` (depending on the value of `collapsed`). + */ +export interface LogGroup { + type: 'group'; + collapsed: boolean; + heading: unknown[]; + children: LogEntry[]; +} diff --git a/packages/@glimmer/debug/lib/render/format.ts b/packages/@glimmer/debug/lib/render/format.ts new file mode 100644 index 0000000000..25bb1a8289 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/format.ts @@ -0,0 +1,18 @@ +import type { StyleName } from './styles'; + +import { STYLES } from './styles'; + +export type Format = { style: string }; +export type IntoFormat = { style: string } | StyleName; + +export function intoFormat(format: IntoFormat): Format { + if (typeof format === 'string') { + return { style: STYLES[format] }; + } else { + return format; + } +} + +export function formats(...formats: IntoFormat[]) { + return formats.map((c) => intoFormat(c).style).join('; '); +} diff --git a/packages/@glimmer/debug/lib/render/fragment-type.ts b/packages/@glimmer/debug/lib/render/fragment-type.ts new file mode 100644 index 0000000000..39250113fc --- /dev/null +++ b/packages/@glimmer/debug/lib/render/fragment-type.ts @@ -0,0 +1,106 @@ +import type { AnyFn, SimpleNode } from '@glimmer/interfaces'; + +import type { Fragment } from './fragment'; + +export const FORMATTERS = { + value: '%O', + string: '%s', + integer: '%d', + float: '%f', + special: '%o', +} as const; + +interface AbstractLeafFragment { + readonly value: unknown; + readonly style?: string | undefined; + readonly subtle?: boolean; +} + +/** + * A leaf fragment that represents an arbitrary value. + * + * When the value is a primitive, the fragment is appended to the buffer as if it was an instance of + * the appropriate leaf fragment type (e.g. strings are appended as if they were `StringFragment`). + * + * Otherwise, `ValueFragment` is appended to the current line as a footnote reference and the value + * itself is appended to a later line that *defines* the footnote using the `%O` format specifier. + */ +export interface ValueFragment extends AbstractLeafFragment { + readonly kind: 'value'; + readonly value: unknown; + + /** + * The `ValueFragment` is appended to the current line as a footnote reference (e.g. `[1]`) and + * the value itself is appended to a later line that *defines* the footnote (e.g. `[1] + * ObjectHere`). + * + * By default, the footnote reference is an incrementing number per log line, and the footnote + * value is formatted using the `%O` format specifier. + * + * The `display` property can be provided to override these defaults. + */ + readonly display?: + | { ref: string; footnote?: Fragment | undefined } + | { inline: Fragment } + | undefined; +} + +/** + * A leaf fragment that represents a string value. + * + * Corresponds to the `%s` format specifier. + */ +export interface StringFragment extends AbstractLeafFragment { + readonly kind: 'string'; + readonly value: string; +} + +/** + * A leaf fragment that represents an integer value. + * + * Corresponds to the `%d` format specifier. + */ +export interface IntegerFragment extends AbstractLeafFragment { + readonly kind: 'integer'; + readonly value: number; +} + +/** + * A leaf fragment that represents a float value. + * + * Corresponds to the `%f` format specifier. + */ +export interface FloatFragment extends AbstractLeafFragment { + readonly kind: 'float'; + readonly value: number; +} + +/** + * A leaf fragment that represents a DOM node. + * + * Corresponds to the `%o` format specifier. + */ +export interface SpecialFragment extends AbstractLeafFragment { + readonly kind: 'special'; + readonly value: SimpleNode | Node | AnyFn | unknown[]; +} + +/** + * The list of leaf fragment types correspond exactly to the list of console.log + * format specifiers. + */ +export type LeafFragmentType = + | StringFragment + | IntegerFragment + | FloatFragment + | ValueFragment + | SpecialFragment; + +export type FragmentType = + | LeafFragmentType + | { + kind: 'multi'; + value: LeafFragment[]; + }; + +export type LeafFragment = Fragment; diff --git a/packages/@glimmer/debug/lib/render/fragment.md b/packages/@glimmer/debug/lib/render/fragment.md new file mode 100644 index 0000000000..f874d186e0 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/fragment.md @@ -0,0 +1,27 @@ +There are four kinds of basic fragments: + +- `string`: a fragment that contains a string +- `integer`: a fragment that contains an integer +- `dom`: a fragment that contains a value +- `value`: a fragment that contains any value + +There is also a `multi` type, which is a fragment that contains one or more fragments. + +Each leaf fragment type corresponds to a `console.log` [format specifier]: + +| Type | Formatter | +| --------- | --------- | +| `string` | `%s` | +| `integer` | `%d` | +| `float` | `%f` | +| `dom` | `%o` | +| `value` | `%O` | + +> [!NOTE] +> +> While `%o` is described in the _spec_ as "optimally useful formatting", it is documented in [the Chrome documentation] as "Formats the value as an expandable DOM element", which is a closer reflection of reality. + +[format specifier]: https://console.spec.whatwg.org/#formatting-specifiers +[the Chrome documentation]: https://developer.chrome.com/docs/devtools/console/format-style#multiple-specifiers + +## Subtle Logging diff --git a/packages/@glimmer/debug/lib/render/fragment.ts b/packages/@glimmer/debug/lib/render/fragment.ts new file mode 100644 index 0000000000..0cb4f9c681 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/fragment.ts @@ -0,0 +1,409 @@ +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 { + FloatFragment, + FragmentType, + IntegerFragment, + LeafFragment, + SpecialFragment, + StringFragment, +} from './fragment-type'; +import type { DisplayFragmentOptions } from './logger'; + +import { LogFragmentBuffer } from './buffer'; +import { formats } from './format'; +import { FORMATTERS } from './fragment-type'; +import { mergeStyle, STYLES } from './styles'; + +/** + * @import { StyleName } from './styles'; + */ + +/** + * Fragment is the most fundamental building block of the debug logger. + * + */ +export class Fragment { + static integer( + this: void, + value: number, + options?: Omit | undefined + ): Fragment { + return new Fragment({ kind: 'integer', value, ...options }); + } + + static float( + this: void, + value: number, + options?: Omit | undefined + ): Fragment { + return new Fragment({ kind: 'float', value, ...options }); + } + + static string( + this: void, + value: string, + options?: Omit | undefined + ): Fragment { + return new Fragment({ kind: 'string', value, ...options }); + } + + static special( + this: void, + value: Node | SimpleNode | AnyFn | unknown[], + options?: Omit | undefined + ): Fragment { + return new Fragment({ kind: 'special', value, ...options }); + } + + readonly #type: T; + + constructor(type: T) { + this.#type = type; + } + + /** + * A subtle fragment is only printed if the `showSubtle` option is set. + * + * Returns true if this fragment is a subtle leaf or is a multi fragment + * with all subtle leaves. + */ + isSubtle(): boolean { + return this.leaves().every((leaf) => leaf.#type.subtle); + } + + /** + * If the current fragment is not empty, apply `ifPresent` to the current + * fragment. Otherwise, do nothing. + * + * If the current fragment is subtle, the result is also subtle. + */ + map(ifPresent: (value: Fragment) => Fragment): Fragment { + if (this.isEmpty()) return this; + const fragment = ifPresent(this); + return this.isSubtle() ? fragment.subtle() : fragment; + } + + /** + * A fragment is empty if it should not be printed with the provided display options. + * + * This means that if a fragment is subtle and `showSubtle` is false, the fragment is empty. + */ + isEmpty(options: DisplayFragmentOptions = { showSubtle: true }): boolean { + return this.leaves().every((leaf) => !leaf.#shouldShow(options)); + } + + /** + * Returns an array of {@linkcode LeafFragment}s that make up the current + * fragment. + * + * This effectively flattens any number of nested multi-fragments into a flat array of leaf + * fragments. + */ + leaves(): LeafFragment[] { + if (this.#type.kind === 'multi') { + return this.#type.value.flatMap((f) => f.leaves()); + } else if (this.#type.kind === 'string' && this.#type.value === '') { + return []; + } else { + return [this as LeafFragment]; + } + } + + /** + * Returns a fragment with the specified subtle status without mutating the current fragment. + * + * If `isSubtle` is true, the fragment will also be styled with the `subtle` style. + */ + subtle(isSubtle = true): Fragment { + if (this.isSubtle() === false && isSubtle === false) { + return this; + } + + const fragment = this.#subtle(isSubtle); + return isSubtle ? fragment.styleAll('dim') : fragment; + } + + #subtle(isSubtle: boolean): Fragment { + if (this.#type.kind === 'multi') { + return new Fragment({ + ...this.#type, + value: this.leaves().flatMap((f) => f.subtle(isSubtle).leaves()), + }); + } else { + return new Fragment({ + ...this.#type, + subtle: isSubtle, + }); + } + } + + /** + * Apply the specified styles to the current fragment (if it's a leaf) or all + * of its children (if it's a multi-fragment). + * + * Keep in mind that merging styles might be very difficult to undo, so treat + * this as a low-level operation, and prefer to use higher-level concepts like + * `subtle` if you can instead. + */ + styleAll(...allFormats: IntoFormat[]): Fragment { + if (allFormats.length === 0) return this; + + if (this.#type.kind === 'multi') { + return new Fragment({ + ...this.#type, + value: this.#type.value.flatMap((f) => f.styleAll(...allFormats).leaves()), + }); + } else { + return new Fragment({ + ...this.#type, + style: mergeStyle(this.#type.style, formats(...allFormats)), + }); + } + } + + /** + * Convert the current fragment into a string with no additional formatting. + * The primary purpose for this method is to support converting a fragment + * into a string for inclusion in thrown Errors. If you're going to *log* + * a fragment, log it using `DebugLogger` and don't convert it to + * a string first. + */ + stringify(options: DisplayFragmentOptions): string { + return this.leaves() + .filter((leaf) => leaf.#shouldShow(options)) + .map((leaf) => { + const fragment = leaf.#type; + + if (fragment.kind === 'value') { + return ``; + } else { + return String(fragment.value); + } + }) + .join(''); + } + + /** + * Should the current fragment be printed with the provided display options? + * + * Importantly, if the current fragment contains subtle content but the `showSubtle` option is + * false, `#shouldShow` will return false. + * + * @see isEmpty + */ + #shouldShow(options: DisplayFragmentOptions): boolean { + return this.leaves().some((leaf) => { + const fragment = leaf.#type; + + if (fragment.subtle && !options.showSubtle) { + return false; + } else if (fragment.kind === 'string' && fragment.value === '') { + return false; + } + + return true; + }); + } + + /** + * Convert this fragment into a Loggable for logging through the `DebugLogger`. + */ + toLoggable(options: DisplayFragmentOptions): Loggable { + const buffer = new LogFragmentBuffer(options); + + for (const leaf of this.leaves()) { + leaf.appendTo(buffer); + } + + return buffer.flush(); + } + + /** + * Append this fragment to the low-level `LogFragmentBuffer`. + */ + appendTo(buffer: LogFragmentBuffer): void { + const fragment = this.#type; + const subtle = this.isSubtle(); + + // If the fragment is a multi fragment, append each of its leaves to the buffer + // and return. + if (fragment.kind === 'multi') { + for (const f of fragment.value) { + f.appendTo(buffer); + } + + return; + } + + // If the fragment is a value fragment and the value is a primitive, give it special + // treatment since we can trivially serialize it. + if (fragment.kind === 'value') { + // If the value is a string or number, convert it into a string, float or integer + // fragment and append that instead. This means that strings and numbers are + // represented the same way in logs whether they are explicitly created as string, + // float or integer fragments *or* whether they are the value of a value fragment. + if (typeof fragment.value === 'string') { + return Fragment.string(JSON.stringify(fragment.value), { + style: STYLES.string, + subtle, + }).appendTo(buffer); + } else if (typeof fragment.value === 'number') { + const f = fragment.value % 1 === 0 ? Fragment.integer : Fragment.float; + return f(fragment.value, { + style: STYLES.number, + subtle, + }).appendTo(buffer); + + // 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) { + return Fragment.string('null', { + style: STYLES.null, + subtle: this.isSubtle(), + }).appendTo(buffer); + + // 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') { + return Fragment.string(String(fragment.value), { + style: STYLES.boolean, + subtle, + }).appendTo(buffer); + } + + // All other values (i.e. objects and functions) are represented as footnotes and + // are handled below. + } + + switch (fragment.kind) { + // strings are appended using %s + case 'string': + // integers are appended using %d + case 'integer': + // floats are appended using %f + case 'float': + buffer.append( + fragment.subtle ?? false, + `%c${FORMATTERS[fragment.kind]}`, + fragment.style, + fragment.value + ); + break; + // the remaining value types are represented as footnotes + // dom nodes are appended to the footnote line using %o + 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 + // footnote rather than the footnote number. + const override = fragment.kind === 'value' ? fragment.display : undefined; + + 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 { + appendValueAsFootnote(override.ref); + } + return false; + } + + buffer.append(subtle, `%c[${n}]%c`, style, ''); + appendValueAsFootnote(String(n)); + return true; + }); + + break; + } + default: + assertNever(fragment); + } + } +} + +export type IntoFragment = Fragment | IntoFragment[] | number | string | null; +type IntoLeafFragment = LeafFragment | number | string | null; + +export function intoFragment(value: IntoFragment): Fragment { + const fragments = intoFragments(value); + const [first, ...rest] = fragments; + + if (first !== undefined && rest.length === 0) { + return first; + } + + return new Fragment({ kind: 'multi', value: fragments }); +} + +function intoFragments(value: IntoFragment): LeafFragment[] { + if (Array.isArray(value)) { + return value.flatMap(intoFragments); + } else if (typeof value === 'object' && value !== null) { + return value.leaves(); + } else { + return [intoLeafFragment(value)]; + } +} + +function intoLeafFragment(value: IntoLeafFragment): LeafFragment { + if (value === null) { + return new Fragment({ kind: 'value', value: null }); + } else if (typeof value === 'number') { + return new Fragment({ kind: 'integer', value }); + } else if (typeof value === 'string') { + // 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 }); + } + } else { + return value; + } +} + +export function frag(strings: TemplateStringsArray, ...values: IntoFragment[]): Fragment { + const buffer: LeafFragment[] = []; + + strings.forEach((string, i) => { + buffer.push(...intoFragment(string).leaves()); + const dynamic = values[i]; + if (dynamic) { + buffer.push(...intoFragment(dynamic).leaves()); + } + }); + + return new Fragment({ kind: 'multi', value: buffer }); +} + +export const as = Object.fromEntries( + Object.entries(STYLES).map(([k, v]) => [ + k, + (value: IntoFragment): Fragment => intoFragment(value).styleAll({ style: v }), + ]) +) as { + [K in keyof typeof STYLES]: ((value: IntoLeafFragment) => LeafFragment) & + ((value: IntoFragment) => Fragment); +}; diff --git a/packages/@glimmer/debug/lib/render/logger.ts b/packages/@glimmer/debug/lib/render/logger.ts new file mode 100644 index 0000000000..8850e13a8d --- /dev/null +++ b/packages/@glimmer/debug/lib/render/logger.ts @@ -0,0 +1,109 @@ +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'; +import type { IntoFragment } from './fragment'; + +import { prepend } from './combinators'; +import { as, frag, intoFragment } from './fragment'; + +export interface DisplayFragmentOptions { + readonly showSubtle: boolean; +} + +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; + + constructor(logger: typeof LOCAL_LOGGER, options: DisplayFragmentOptions) { + this.#logger = logger; + this.#options = options; + } + + #logEntry(entry: LogEntry) { + switch (entry.type) { + case 'line': { + this.#logger.debug(...entry.line); + break; + } + + case 'group': { + if (entry.collapsed) { + this.#logger.groupCollapsed(...entry.heading); + } else { + this.#logger.group(...entry.heading); + } + + for (const line of entry.children) { + this.#logEntry(line); + } + + this.#logger.groupEnd(); + } + } + } + + #lines(type: 'log' | 'debug' | 'group' | 'groupCollapsed', lines: FlushedLines): void { + const [first, ...rest] = lines; + + if (first) { + this.#logger[type](...first.line); + + for (const entry of rest) { + this.#logEntry(entry); + } + } + } + + internals(...args: IntoFragment[]): void { + this.#lines( + 'groupCollapsed', + frag`🔍 ${intoFragment('internals').styleAll('internals')}`.toLoggable(this.#options) + ); + this.#lines('debug', frag`${args}`.toLoggable(this.#options)); + this.#logger.groupEnd(); + } + + log(...args: IntoFragment[]): void { + const fragment = frag`${args}`; + + if (!fragment.isEmpty(this.#options)) this.#lines('debug', fragment.toLoggable(this.#options)); + } + + labelled(label: string, ...args: IntoFragment[]): void { + const fragment = frag`${args}`; + + const styles: IntoFormat[] = ['kw']; + + const { focus, focusColor } = getFlagValues('focus_highlight').includes(label) + ? ({ focus: ['focus'], focusColor: ['focusColor'] } as const) + : { focus: [], focusColor: [] }; + + this.log( + prepend( + frag`${as.label(label)} `.styleAll(...styles, ...focus, ...focusColor), + fragment.styleAll(...focus) + ) + ); + } + + group(...args: IntoFragment[]): { expanded: () => () => void; collapsed: () => () => void } { + return { + expanded: () => { + this.#lines('group', frag`${args}`.styleAll('unbold').toLoggable(this.#options)); + return () => this.#logger.groupEnd(); + }, + collapsed: () => { + this.#lines('groupCollapsed', frag`${args}`.styleAll('unbold').toLoggable(this.#options)); + return () => this.#logger.groupEnd(); + }, + }; + } +} diff --git a/packages/@glimmer/debug/lib/render/ref.ts b/packages/@glimmer/debug/lib/render/ref.ts new file mode 100644 index 0000000000..54dec98ca6 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/ref.ts @@ -0,0 +1,16 @@ +import type { Reference } from '@glimmer/interfaces'; +import { valueForRef } from '@glimmer/reference'; + +import type { Fragment } from './fragment'; + +import { join, value } from './basic'; +import { as, frag } from './fragment'; + +export function describeRef(ref: Reference): Fragment { + const debug = ref.debugLabel; + + 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 new file mode 100644 index 0000000000..4993f69a86 --- /dev/null +++ b/packages/@glimmer/debug/lib/render/styles.ts @@ -0,0 +1,55 @@ +// inspired by https://github.com/ChromeDevTools/devtools-frontend/blob/c2c17396c9e0da3f1ce6514c3a946f88a06b17f2/front_end/ui/legacy/themeColors.css#L65 +export const STYLES = { + var: 'color: grey', + varReference: 'color: blue; text-decoration: underline', + varBinding: 'color: blue;', + specialVar: 'color: blue', + prop: 'color: grey', + specialProp: 'color: red', + token: 'color: green', + def: 'color: blue', + builtin: 'color: blue', + punct: 'color: GrayText', + kw: 'color: rgb(185 0 99 / 100%);', + type: 'color: teal', + number: 'color: blue', + string: 'color: red', + null: 'color: grey', + specialString: 'color: darkred', + atom: 'color: blue', + attrName: 'color: orange', + attrValue: 'color: blue', + boolean: 'color: blue', + comment: 'color: green', + meta: 'color: grey', + register: 'color: purple', + constant: 'color: purple', + dim: 'color: grey', + internals: 'color: lightgrey; font-style: italic', + + diffAdd: 'color: Highlight', + diffDelete: 'color: SelectedItemText; background-color: SelectedItem', + diffChange: 'color: MarkText; background-color: Mark', + + sublabel: 'font-style: italic; color: grey', + error: 'color: red', + label: 'text-decoration: underline', + errorLabel: 'color: darkred; font-style: italic', + errorMessage: 'color: darkred; text-decoration: underline', + stack: 'color: grey; font-style: italic', + unbold: 'font-weight: normal', + pointer: 'background-color: lavender; color: indigo', + pointee: 'background-color: lavender; color: indigo', + focus: 'font-weight: bold', + focusColor: 'background-color: lightred; color: darkred', +} as const; + +export type StyleName = keyof typeof STYLES; + +export function mergeStyle(a?: string | undefined, b?: string | undefined): string | undefined { + if (a && b) { + return `${a}; ${b}`; + } else { + return a || b; + } +} diff --git a/packages/@glimmer/debug/lib/stack-check.ts b/packages/@glimmer/debug/lib/stack-check.ts index e8853beacb..4300065e5c 100644 --- a/packages/@glimmer/debug/lib/stack-check.ts +++ b/packages/@glimmer/debug/lib/stack-check.ts @@ -12,6 +12,8 @@ import type { MachineRegister, Register, SyscallRegister } from '@glimmer/vm'; import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { $fp, $pc, $ra, $s0, $s1, $sp, $t0, $t1, $v0 } from '@glimmer/vm'; +import type { Primitive } from './dism/dism'; + export interface Checker { type: T; @@ -67,8 +69,6 @@ class TypeofChecker implements Checker { } } -export type Primitive = undefined | null | boolean | number | string; - class PrimitiveChecker implements Checker { declare type: Primitive; @@ -511,7 +511,7 @@ export const CheckBlockSymbolTable: Checker = LOCAL_DEBUG export const CheckProgramSymbolTable: Checker = LOCAL_DEBUG ? CheckInterface({ - hasEval: CheckBoolean, + hasDebugger: CheckBoolean, symbols: CheckArray(CheckString), }) : new NoopChecker(); diff --git a/packages/@glimmer/debug/lib/vm/snapshot.ts b/packages/@glimmer/debug/lib/vm/snapshot.ts new file mode 100644 index 0000000000..d7f5a53ee1 --- /dev/null +++ b/packages/@glimmer/debug/lib/vm/snapshot.ts @@ -0,0 +1,213 @@ +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 { value } from '../render/basic'; +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: VmSnapshotArrayDiff<'stack', unknown[]>; + readonly blocks: VmSnapshotArrayDiff<'blocks', object[]>; + readonly cursors: VmSnapshotArrayDiff<'cursors', Cursor[]>; + readonly constructing: VmSnapshotValueDiff<'constructing', Nullable>; + readonly destructors: VmSnapshotArrayDiff<'destructors', object[]>; + readonly scope: VmSnapshotArrayDiff<'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 VmSnapshotArrayDiff( + 'stack', + before.stack, + after.stack, + frameChange ? 'reset' : undefined + ); + + this.blocks = new VmSnapshotArrayDiff('blocks', before.elements.blocks, after.elements.blocks); + + this.constructing = new VmSnapshotValueDiff( + 'constructing', + before.elements.constructing, + after.elements.constructing + ); + + this.cursors = new VmSnapshotArrayDiff( + 'cursors', + before.elements.cursors, + after.elements.cursors + ); + + this.destructors = new VmSnapshotArrayDiff( + 'destructors', + before.stacks.destroyable, + after.stacks.destroyable + ); + + this.scope = new VmSnapshotArrayDiff('scope', before.scope, after.scope); + } +} + +export class VmSnapshotArrayDiff { + 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) => value(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(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(value(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`${value(before, { ref: `${i}:popped` })} -> `; + } else if (op === 'retain') { + pre = frag`${value(before, { ref: `${i}:before` })} -> `; + } else if (op === 'push') { + pre = frag`push -> `.subtle(); + } else { + exhausted(op); + } + + let post: Fragment; + + if (op === 'push') { + post = value(after, { ref: `${i}:push` }); + } else if (op === 'retain') { + post = value(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)}: ${value(this.after)}`.subtle(); + } + + return frag`${as.register(this.name)}: ${value(this.before)} -> ${value(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/debug/package.json b/packages/@glimmer/debug/package.json index 4066d39a5b..baab4fcb4f 100644 --- a/packages/@glimmer/debug/package.json +++ b/packages/@glimmer/debug/package.json @@ -15,7 +15,8 @@ "dependencies": { "@glimmer/interfaces": "workspace:*", "@glimmer/util": "workspace:*", - "@glimmer/vm": "workspace:*" + "@glimmer/vm": "workspace:*", + "@glimmer/reference": "workspace:*" }, "devDependencies": { "@glimmer-workspace/build-support": "workspace:*", diff --git a/packages/@glimmer/interfaces/index.d.ts b/packages/@glimmer/interfaces/index.d.ts index ea94423790..0773e438e4 100644 --- a/packages/@glimmer/interfaces/index.d.ts +++ b/packages/@glimmer/interfaces/index.d.ts @@ -1,26 +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/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/compile/wire-format/api.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts index 8857fc8ea0..b30d38d35c 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts @@ -364,13 +364,9 @@ export type SerializedInlineBlock = [statements: Statements.Statement[], paramet * A JSON object that the compiled TemplateBlock was serialized into. */ export type SerializedTemplateBlock = [ - // statements statements: Statements.Statement[], - // symbols - symbols: string[], - // hasDebug - hasDebug: boolean, - // upvars + locals: string[], + hasDebugger: boolean, upvars: string[], lexicalSymbols?: string[], ]; 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 3518828281..d541e88f26 100644 --- a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts +++ b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts @@ -12,7 +12,14 @@ import type { SimpleText, } from './simple.js'; +/** + * `AppendingBlock` is the interface used by the `ElementBuilder` to keep track of which nodes have + * been appended to a block. Ultimately, an `AppendingBlock` is finalized and used as a `FixedBlock` + * 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; @@ -81,11 +88,13 @@ export interface TreeOperations { __setProperty(name: string, value: unknown): void; } -declare const CURSOR_STACK: unique symbol; -export type CursorStackSymbol = typeof CURSOR_STACK; - export interface TreeBuilder extends Cursor, DOMStack, TreeOperations { - [CURSOR_STACK]: Stack; + readonly cursors: Stack; + readonly debug?: () => { + blocks: AppendingBlock[]; + constructing: Nullable; + cursors: Cursor[]; + }; nextSibling: Nullable; dom: GlimmerTreeConstruction; diff --git a/packages/@glimmer/interfaces/lib/managers/internal/component.d.ts b/packages/@glimmer/interfaces/lib/managers/internal/component.d.ts index b75da461da..23182b2e6b 100644 --- a/packages/@glimmer/interfaces/lib/managers/internal/component.d.ts +++ b/packages/@glimmer/interfaces/lib/managers/internal/component.d.ts @@ -237,7 +237,7 @@ export interface WithUpdateHook export interface WithDynamicLayout< I = ComponentInstanceState, - R extends ClassicResolver = ClassicResolver, + R extends Nullable = Nullable, > extends InternalComponentManager { // Return the compiled layout to use for this component. This is called // *after* the component instance has been created, because you might diff --git a/packages/@glimmer/interfaces/lib/program.d.ts b/packages/@glimmer/interfaces/lib/program.d.ts index 536e98c19e..a24b952fb5 100644 --- a/packages/@glimmer/interfaces/lib/program.d.ts +++ b/packages/@glimmer/interfaces/lib/program.d.ts @@ -39,6 +39,14 @@ export interface ProgramHeap { sizeof(handle: number): number; getbyaddr(address: number): number; setbyaddr(address: number, value: number): void; + + /** + * Return the number of entries in the table. A handle is legal if + * it is less than this number. + * + * @debugging + */ + entries(): number; } /** @@ -142,12 +150,18 @@ export interface ResolutionTimeConstants { ): ComponentDefinition; } -export interface ReadonlyConstants { +export interface RuntimeConstants { + hasHandle(handle: number): boolean; getValue(handle: number): T; getArray(handle: number): T[]; } -export type ProgramConstants = CompileTimeConstants & ResolutionTimeConstants & ReadonlyConstants; +export type ProgramConstants = CompileTimeConstants & ResolutionTimeConstants & RuntimeConstants; + +export interface CompileTimeArtifacts { + heap: ProgramHeap; + constants: ProgramConstants; +} export interface ClassicResolver { lookupHelper?(name: string, owner: O): Nullable; 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/interfaces/lib/runtime.d.ts b/packages/@glimmer/interfaces/lib/runtime.d.ts index e1dde92c94..eeb3cb7697 100644 --- a/packages/@glimmer/interfaces/lib/runtime.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime.d.ts @@ -9,5 +9,4 @@ export * from './runtime/owner.js'; export * from './runtime/render.js'; export * from './runtime/runtime.js'; export * from './runtime/scope.js'; -export * from './runtime/vm.js'; export * from './runtime/vm-state.js'; diff --git a/packages/@glimmer/interfaces/lib/runtime/environment.d.ts b/packages/@glimmer/interfaces/lib/runtime/environment.d.ts index 26d0a76e1e..c47f1c98ab 100644 --- a/packages/@glimmer/interfaces/lib/runtime/environment.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/environment.d.ts @@ -54,5 +54,5 @@ export interface Environment { export interface RuntimeOptions { readonly env: Environment; readonly program: Program; - readonly resolver: Nullable; + readonly resolver: ClassicResolver | null; } diff --git a/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts b/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts index a85cff2b20..8fbbd59921 100644 --- a/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/local-debug.d.ts @@ -1,5 +1,26 @@ +import type { SimpleElement } from '@simple-dom/interface'; + import type { Nullable } from '../core.js'; +import type { AppendingBlock } from '../dom/attributes.js'; +import type { Cursor } from '../dom/bounds.js'; +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'; + +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; @@ -9,3 +30,46 @@ export interface DebugTemplates { willCall(handle: Handle): void; return(): void; } + +export interface DebugVmTrace { + readonly willCall: (handle: Handle) => void; + readonly return: () => void; + readonly register: (handle: Handle, metadata: BlockMetadata) => void; +} + +/** + * All parts of `DebugVmState` are _snapshots_. They will not change if the piece of VM state that + * they reference changes. + */ +export interface DebugVmSnapshot { + /** + * These values are the same for the entire program + */ + readonly context: EvaluationContext; + + /** + * These values can change for each opcode. You can get a snapshot a specific stack by calling + * `stacks..snapshot()`. + */ + readonly stacks: DebugStacks; + + readonly elements: { + blocks: AppendingBlock[]; + cursors: Cursor[]; + constructing: Nullable; + }; + + readonly stack: unknown[]; + readonly scope: ScopeSlot[]; + readonly registers: DebugRegisters; + readonly template: Nullable; +} + +export interface DebugStacks { + scope: Scope[]; + dynamicScope: DynamicScope[]; + updating: UpdatingOpcode[][]; + cache: UpdatingOpcode[]; + list: UpdatingBlockOpcode[]; + destroyable: object[]; +} diff --git a/packages/@glimmer/interfaces/lib/runtime/scope.d.ts b/packages/@glimmer/interfaces/lib/runtime/scope.d.ts index e284d79d10..825aec0737 100644 --- a/packages/@glimmer/interfaces/lib/runtime/scope.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/scope.d.ts @@ -11,19 +11,26 @@ export type BlockValue = ScopeBlock[0 | 1 | 2]; export type ScopeSlot = Reference | ScopeBlock | null; export interface Scope { - // for debug only - readonly slots: Array; + /** + * A single program can mix and match multiple owners. This can happen component is curried from a + * template with one owner and then rendered in a second owner. + * + * Note: Owners can change when new root scopes are created (including when rendering a + * component), but not in child scopes. + */ readonly owner: Owner; + // for debug only + snapshot(): ScopeSlot[]; getSelf(): Reference; getSymbol(symbol: number): Reference; getBlock(symbol: number): Nullable; getDebuggerScope(): Nullable>; + bindDebuggerScope(map: Nullable>): void; bind(symbol: number, value: ScopeSlot): void; bindSelf(self: Reference): void; bindSymbol(symbol: number, value: Reference): void; bindBlock(symbol: number, value: Nullable): void; - bindDebuggerScope(map: Nullable>): void; child(): Scope; } diff --git a/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts b/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts index 30ab689c19..a464e54a95 100644 --- a/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/vm-state.d.ts @@ -9,3 +9,44 @@ export type SyscallRegisters = [ $t1: unknown, $v0: unknown, ]; + +/** + * Registers + * + * For the most part, these follows MIPS naming conventions, however the + * register numbers are different. + */ + +// $0 or $pc (program counter): pointer into `program` for the next insturction; -1 means exit +export type $pc = 0; +declare const $pc: $pc; +// $1 or $ra (return address): pointer into `program` for the return +export type $ra = 1; +declare const $ra: $ra; +// $2 or $fp (frame pointer): pointer into the `evalStack` for the base of the stack +export type $fp = 2; +declare const $fp: $fp; +// $3 or $sp (stack pointer): pointer into the `evalStack` for the top of the stack +export type $sp = 3; +declare const $sp: $sp; +// $4-$5 or $s0-$s1 (saved): callee saved general-purpose registers +export type $s0 = 4; +declare const $s0: $s0; +export type $s1 = 5; +declare const $s1: $s1; +// $6-$7 or $t0-$t1 (temporaries): caller saved general-purpose registers +export type $t0 = 6; +declare const $t0: $t0; +export type $t1 = 7; +declare const $t1: $t1; +// $8 or $v0 (return value) +export type $v0 = 8; +declare const $v0: $v0; + +export type MachineRegister = $pc | $ra | $fp | $sp; + +export type SavedRegister = $s0 | $s1; +export type TemporaryRegister = $t0 | $t1; + +export type Register = MachineRegister | SavedRegister | TemporaryRegister | $v0; +export type SyscallRegister = SavedRegister | TemporaryRegister | $v0; diff --git a/packages/@glimmer/interfaces/lib/runtime/vm.d.ts b/packages/@glimmer/interfaces/lib/runtime/vm.d.ts index a9e99a99a9..8dec8feae6 100644 --- a/packages/@glimmer/interfaces/lib/runtime/vm.d.ts +++ b/packages/@glimmer/interfaces/lib/runtime/vm.d.ts @@ -1,23 +1,7 @@ -import type { Destroyable } from '../core.js'; +import type { Bounds } from '../dom/bounds.js'; import type { GlimmerTreeChanges } from '../dom/changes.js'; -import type { Reference } from '../references.js'; import type { Environment } from './environment.js'; -import type { Owner } from './owner.js'; import type { ExceptionHandler } from './render.js'; -import type { DynamicScope } from './scope.js'; -/** - * This is used in the Glimmer Embedding API. In particular, embeddings - * provide helpers through the `CompileTimeLookup` interface, and the - * helpers they provide implement the `Helper` interface, which is a - * function that takes a `VM` as a parameter. - */ -export interface VM { - env: Environment; - dynamicScope(): DynamicScope; - getOwner(): O; - getSelf(): Reference; - associateDestroyable(child: Destroyable): void; -} export interface UpdatingVM { env: Environment; @@ -33,3 +17,5 @@ export interface UpdatingVM { export interface UpdatingOpcode { evaluate(vm: UpdatingVM): void; } + +export interface UpdatingBlockOpcode extends UpdatingOpcode, Bounds {} diff --git a/packages/@glimmer/interfaces/lib/stack.d.ts b/packages/@glimmer/interfaces/lib/stack.d.ts index bcb67e0b76..47d95bad55 100644 --- a/packages/@glimmer/interfaces/lib/stack.d.ts +++ b/packages/@glimmer/interfaces/lib/stack.d.ts @@ -9,4 +9,9 @@ export interface Stack { nth(from: number): Nullable; isEmpty(): boolean; toArray(): T[]; + + /** + * For debugging + */ + snapshot(): T[]; } diff --git a/packages/@glimmer/interfaces/lib/template.d.ts b/packages/@glimmer/interfaces/lib/template.d.ts index 013120f952..ddbb0336b6 100644 --- a/packages/@glimmer/interfaces/lib/template.d.ts +++ b/packages/@glimmer/interfaces/lib/template.d.ts @@ -1,7 +1,7 @@ import type { PresentArray } from './array.js'; import type { EncoderError } from './compile/encoder.js'; import type { Operand, SerializedInlineBlock, SerializedTemplateBlock } from './compile/index.js'; -import type { Nullable } from './core.js'; +import type { Nullable, Optional } from './core.js'; import type { InternalComponentCapabilities } from './managers/internal/component.js'; import type { ConstantPool, EvaluationContext, SerializedHeap } from './program.js'; import type { Owner } from './runtime.js'; @@ -104,12 +104,17 @@ export interface CompilableTemplate { compile(context: EvaluationContext): HandleResult; } -export interface BlockMetadata { - evalSymbols: Nullable; +export interface BlockSymbolNames { + locals: Nullable; + lexical?: Optional; upvars: Nullable; - debugSymbols?: string[] | undefined; +} + +export interface BlockMetadata { + symbols: BlockSymbolNames; scopeValues: unknown[] | null; isStrictMode: boolean; + hasDebugger: boolean; moduleName: string; owner: Owner | null; size: number; diff --git a/packages/@glimmer/interfaces/lib/tier1/symbol-table.d.ts b/packages/@glimmer/interfaces/lib/tier1/symbol-table.d.ts index 57332a275b..f81303b3cd 100644 --- a/packages/@glimmer/interfaces/lib/tier1/symbol-table.d.ts +++ b/packages/@glimmer/interfaces/lib/tier1/symbol-table.d.ts @@ -1,5 +1,5 @@ export interface ProgramSymbolTable { - hasEval: boolean; + hasDebugger: boolean; symbols: string[]; } diff --git a/packages/@glimmer/interfaces/tsconfig.json b/packages/@glimmer/interfaces/tsconfig.json index 2103b085d9..db6fab863e 100644 --- a/packages/@glimmer/interfaces/tsconfig.json +++ b/packages/@glimmer/interfaces/tsconfig.json @@ -2,7 +2,7 @@ "extends": ["../tsconfig.json"], "compilerOptions": { "rootDir": ".", - "moduleResolution": "Node16" + "moduleResolution": "bundler" }, "include": ["index.d.ts", "lib"] } diff --git a/packages/@glimmer/local-debug-flags/index.ts b/packages/@glimmer/local-debug-flags/index.ts index b6b69e4833..b38d2eaf8f 100644 --- a/packages/@glimmer/local-debug-flags/index.ts +++ b/packages/@glimmer/local-debug-flags/index.ts @@ -7,8 +7,10 @@ declare global { } // All of these flags are expected to become constant `false` in production builds. -export const LOCAL_DEBUG = import.meta.env.VM_LOCAL_DEV && !hasFlag('disable_local_debug'); -export const LOCAL_TRACE_LOGGING = import.meta.env.VM_LOCAL_DEV && hasFlag('enable_trace_logging'); +export const LOCAL_DEBUG = !!(import.meta.env.VM_LOCAL_DEV && !hasFlag('disable_local_debug')); +export const LOCAL_TRACE_LOGGING = !!( + import.meta.env.VM_LOCAL_DEV && hasFlag('enable_trace_logging') +); export const LOCAL_EXPLAIN_LOGGING = import.meta.env.VM_LOCAL_DEV && hasFlag('enable_trace_explanations'); export const LOCAL_INTERNALS_LOGGING = diff --git a/packages/@glimmer/node/lib/serialize-builder.ts b/packages/@glimmer/node/lib/serialize-builder.ts index 1d2f7e8c36..2a5c3dab8a 100644 --- a/packages/@glimmer/node/lib/serialize-builder.ts +++ b/packages/@glimmer/node/lib/serialize-builder.ts @@ -10,7 +10,7 @@ import type { SimpleText, TreeBuilder, } from '@glimmer/interfaces'; -import type { RemoteLiveBlock } from '@glimmer/runtime'; +import type { RemoteBlock } from '@glimmer/runtime'; import { ConcreteBounds, NewTreeBuilder } from '@glimmer/runtime'; const TEXT_NODE = 3; @@ -131,7 +131,7 @@ class SerializeBuilder extends NewTreeBuilder implements TreeBuilder { element: SimpleElement, cursorId: string, insertBefore: Maybe = null - ): RemoteLiveBlock { + ): RemoteBlock { let { dom } = this; let script = dom.createElement('script'); script.setAttribute('glmr', cursorId); diff --git a/packages/@glimmer/opcode-compiler/lib/compilable-template.ts b/packages/@glimmer/opcode-compiler/lib/compilable-template.ts index 129c711dfa..f89b661191 100644 --- a/packages/@glimmer/opcode-compiler/lib/compilable-template.ts +++ b/packages/@glimmer/opcode-compiler/lib/compilable-template.ts @@ -16,6 +16,7 @@ import type { SymbolTable, WireFormat, } from '@glimmer/interfaces'; +import { IS_COMPILABLE_TEMPLATE } from '@glimmer/constants'; import { LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; import { EMPTY_ARRAY } from '@glimmer/util'; @@ -30,6 +31,12 @@ import { STATEMENTS } from './syntax/statements'; export const PLACEHOLDER_HANDLE = -1; class CompilableTemplateImpl implements CompilableTemplate { + static { + if (LOCAL_TRACE_LOGGING) { + Reflect.set(this.prototype, IS_COMPILABLE_TEMPLATE, true); + } + } + compiled: Nullable = null; constructor( @@ -48,13 +55,13 @@ class CompilableTemplateImpl implements CompilableTemplat } export function compilable(layout: LayoutWithContext, moduleName: string): CompilableProgram { - let [statements, symbols, hasEval] = layout.block; + let [statements, symbols, hasDebugger] = layout.block; return new CompilableTemplateImpl( statements, meta(layout), { symbols, - hasEval, + hasDebugger, }, moduleName ); diff --git a/packages/@glimmer/opcode-compiler/lib/compiler.ts b/packages/@glimmer/opcode-compiler/lib/compiler.ts index 726931e4bf..cb799e6d5f 100644 --- a/packages/@glimmer/opcode-compiler/lib/compiler.ts +++ b/packages/@glimmer/opcode-compiler/lib/compiler.ts @@ -1,5 +1,5 @@ import type { CompilationContext, HandleResult } from '@glimmer/interfaces'; -import { debugSlice } from '@glimmer/debug'; +import { logOpcodeSlice } from '@glimmer/debug'; import { extractHandle } from '@glimmer/debug-util'; import { LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; @@ -12,6 +12,6 @@ if (LOCAL_TRACE_LOGGING) { let start = heap.getaddr(handle); let end = start + heap.sizeof(handle); - debugSlice(context, start, end); + logOpcodeSlice(context, start, end); }; } diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts index c02c8bdf0e..a6fd2cf488 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts @@ -18,7 +18,7 @@ import type { import { encodeHandle, isMachineOp, VM_PRIMITIVE_OP, VM_RETURN_OP } from '@glimmer/constants'; import { assert, expect, isPresentArray } from '@glimmer/debug-util'; import { InstructionEncoderImpl } from '@glimmer/encoder'; -import { dict, EMPTY_STRING_ARRAY, Stack } from '@glimmer/util'; +import { dict, Stack } from '@glimmer/util'; import { ARG_SHIFT, MACHINE_MASK, TYPE_SIZE } from '@glimmer/vm'; import { compilableBlock } from '../compilable-template'; @@ -92,9 +92,10 @@ export function encodeOp( case HighLevelResolutionOpcodes.Local: { let freeVar = op[1]; - let name = expect(meta.upvars, 'BUG: attempted to resolve value but no upvars found')[ - freeVar - ]!; + let name = expect( + meta.symbols.upvars, + 'BUG: attempted to resolve value but no upvars found' + )[freeVar]!; let andThen = op[2]; andThen(name, meta.moduleName); @@ -192,7 +193,7 @@ export class EncoderImpl implements Encoder { return encodeHandle(constants.value(this.meta.isStrictMode)); case HighLevelOperands.DebugSymbols: - return encodeHandle(constants.array(this.meta.evalSymbols || EMPTY_STRING_ARRAY)); + return encodeHandle(constants.value(this.meta.symbols)); case HighLevelOperands.Block: return encodeHandle(constants.value(compilableBlock(operand.value, this.meta))); diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/components.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/components.ts index 82da7371d0..f28e762078 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/components.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/components.ts @@ -196,7 +196,8 @@ function InvokeStaticComponent( let { symbolTable } = layout; let bailOut = - symbolTable.hasEval || hasCapability(capabilities, InternalComponentCapabilities.prepareArgs); + symbolTable.hasDebugger || + hasCapability(capabilities, InternalComponentCapabilities.prepareArgs); if (bailOut) { InvokeNonStaticComponent(op, { @@ -315,7 +316,7 @@ function InvokeStaticComponent( if (hasCapability(capabilities, InternalComponentCapabilities.createInstance)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - op(VM_CREATE_COMPONENT_OP, (blocks.has('default') as any) | 0, $s0); + op(VM_CREATE_COMPONENT_OP, (blocks.has('default') as any) | 0); } op(VM_REGISTER_COMPONENT_DESTRUCTOR_OP, $s0); @@ -448,7 +449,7 @@ export function invokePreparedComponent( op(VM_PUSH_DYNAMIC_SCOPE_OP); // eslint-disable-next-line @typescript-eslint/no-explicit-any - op(VM_CREATE_COMPONENT_OP, (hasBlock as any) | 0, $s0); + op(VM_CREATE_COMPONENT_OP, (hasBlock as any) | 0); // this has to run after createComponent to allow // for late-bound layouts, but a caller is free diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts index 63457647e0..97b80336c6 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts @@ -1,5 +1,6 @@ import type { BlockMetadata, + BlockSymbolNames, ClassicResolver, Expressions, Nullable, @@ -48,12 +49,14 @@ export const isGetFreeComponentOrHelper = makeResolutionTypeVerifier( interface ResolvedBlockMetadata extends BlockMetadata { owner: Owner; - upvars: string[]; + symbols: BlockSymbolNames & { + upvars: string[]; + }; } function assertResolverInvariants(meta: BlockMetadata): ResolvedBlockMetadata { if (import.meta.env.DEV) { - if (!meta.upvars) { + if (!meta.symbols.upvars) { throw new Error( 'Attempted to resolve a component, helper, or modifier, but no free vars were found' ); @@ -89,13 +92,17 @@ export function resolveComponent( throw new Error( `Attempted to resolve a component in a strict mode template, but that value was not in scope: ${ - meta.upvars![expr[1]] ?? '{unknown variable}' + meta.symbols.upvars![expr[1]] ?? '{unknown variable}' }` ); } if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, owner, debugSymbols } = meta; + let { + scopeValues, + owner, + symbols: { lexical }, + } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; @@ -105,11 +112,14 @@ export function resolveComponent( definition as object, expect(owner, 'BUG: expected owner when resolving component definition'), false, - debugSymbols?.at(expr[1]) + lexical?.at(expr[1]) ) ); } else { - let { upvars, owner } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + owner, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let definition = resolver?.lookupComponent?.(name, owner) ?? null; @@ -152,7 +162,10 @@ export function resolveHelper( lookupBuiltInHelper(expr as Expressions.GetStrictFree, resolver, meta, constants, 'helper') ); } else { - let { upvars, owner } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + owner, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let helper = resolver?.lookupHelper?.(name, owner) ?? null; @@ -185,14 +198,19 @@ export function resolveModifier( let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, debugSymbols } = meta; + let { + scopeValues, + symbols: { lexical }, + } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; - then(constants.modifier(definition as object, debugSymbols?.at(expr[1]) ?? undefined)); + then(constants.modifier(definition as object, lexical?.at(expr[1]) ?? undefined)); } else if (type === SexpOpcodes.GetStrictKeyword) { - let { upvars } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let modifier = resolver?.lookupBuiltInModifier?.(name) ?? null; @@ -206,7 +224,10 @@ export function resolveModifier( then(constants.modifier(modifier!, name)); } else { - let { upvars, owner } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + owner, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let modifier = resolver?.lookupModifier?.(name, owner) ?? null; @@ -239,7 +260,11 @@ export function resolveComponentOrHelper( let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, owner, debugSymbols } = meta; + let { + scopeValues, + owner, + symbols: { lexical }, + } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; @@ -248,7 +273,7 @@ export function resolveComponentOrHelper( definition as object, expect(owner, 'BUG: expected owner when resolving component definition'), true, - debugSymbols?.at(expr[1]) + lexical?.at(expr[1]) ); if (component !== null) { @@ -280,7 +305,10 @@ export function resolveComponentOrHelper( ) ); } else { - let { upvars, owner } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + owner, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let definition = resolver?.lookupComponent?.(name, owner) ?? null; @@ -320,7 +348,11 @@ export function resolveOptionalComponentOrHelper( let type = expr[0]; if (type === SexpOpcodes.GetLexicalSymbol) { - let { scopeValues, owner, debugSymbols } = meta; + let { + scopeValues, + owner, + symbols: { lexical }, + } = meta; let definition = expect(scopeValues, 'BUG: scopeValues must exist if template symbol is used')[ expr[1] ]; @@ -338,7 +370,7 @@ export function resolveOptionalComponentOrHelper( definition, expect(owner, 'BUG: expected owner when resolving component definition'), true, - debugSymbols?.at(expr[1]) + lexical?.at(expr[1]) ); if (component !== null) { @@ -359,7 +391,10 @@ export function resolveOptionalComponentOrHelper( lookupBuiltInHelper(expr as Expressions.GetStrictFree, resolver, meta, constants, 'value') ); } else { - let { upvars, owner } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + owner, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let definition = resolver?.lookupComponent?.(name, owner) ?? null; @@ -384,7 +419,9 @@ function lookupBuiltInHelper( constants: ResolutionTimeConstants, type: string ): number { - let { upvars } = assertResolverInvariants(meta); + let { + symbols: { upvars }, + } = assertResolverInvariants(meta); let name = unwrap(upvars[expr[1]]); let helper = resolver?.lookupBuiltInHelper?.(name) ?? null; @@ -396,7 +433,7 @@ function lookupBuiltInHelper( // value of some kind that is not in scope throw new Error( `Attempted to resolve a ${type} in a strict mode template, but that value was not in scope: ${ - meta.upvars![expr[1]] ?? '{unknown variable}' + meta.symbols.upvars![expr[1]] ?? '{unknown variable}' }` ); } diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts index b3febe10b5..fa8230f55d 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/shared.ts @@ -106,23 +106,26 @@ export function CompilePositional( } export function meta(layout: LayoutWithContext): BlockMetadata { - let [, symbols, , upvars, debugSymbols] = layout.block; + let [, locals, hasDebugger, upvars, lexicalSymbols] = layout.block; return { - evalSymbols: evalSymbols(layout), - upvars: upvars, + symbols: { + locals, + upvars, + lexical: lexicalSymbols, + }, + hasDebugger, scopeValues: layout.scope?.() ?? null, - debugSymbols, isStrictMode: layout.isStrictMode, moduleName: layout.moduleName, owner: layout.owner, - size: symbols.length, + size: locals.length, }; } -export function evalSymbols(layout: LayoutWithContext): Nullable { +export function getDebuggerSymbols(layout: LayoutWithContext): Nullable { let { block } = layout; - let [, symbols, hasEval] = block; + let [, symbols, hasDebugger] = block; - return hasEval ? symbols : null; + return hasDebugger ? symbols : null; } diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts index 72feb8f2fc..58d3747234 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts @@ -117,8 +117,11 @@ export function compileStd(context: EvaluationContext): StdLib { } export const STDLIB_META: BlockMetadata = { - evalSymbols: null, - upvars: null, + symbols: { + locals: null, + upvars: null, + }, + hasDebugger: false, moduleName: 'stdlib', // TODO: ?? diff --git a/packages/@glimmer/opcode-compiler/lib/wrapped-component.ts b/packages/@glimmer/opcode-compiler/lib/wrapped-component.ts index b57c2d8fd3..c400a8da18 100644 --- a/packages/@glimmer/opcode-compiler/lib/wrapped-component.ts +++ b/packages/@glimmer/opcode-compiler/lib/wrapped-component.ts @@ -26,11 +26,11 @@ export class WrappedBuilder implements CompilableProgram { readonly meta: BlockMetadata; constructor( - private layout: LayoutWithContext, + private readonly layout: LayoutWithContext, public moduleName: string ) { let { block } = layout; - let [, symbols, hasEval] = block; + let [, symbols, hasDebugger] = block; symbols = symbols.slice(); @@ -43,7 +43,7 @@ export class WrappedBuilder implements CompilableProgram { } this.symbolTable = { - hasEval, + hasDebugger, symbols, }; diff --git a/packages/@glimmer/program/lib/constants.ts b/packages/@glimmer/program/lib/constants.ts index 221f04b4ca..c7275f8b52 100644 --- a/packages/@glimmer/program/lib/constants.ts +++ b/packages/@glimmer/program/lib/constants.ts @@ -85,6 +85,10 @@ export class ConstantsImpl implements ProgramConstants { return this.values; } + hasHandle(handle: number): boolean { + return this.values.length > handle; + } + helper( definitionState: HelperDefinitionState, diff --git a/packages/@glimmer/program/lib/program.ts b/packages/@glimmer/program/lib/program.ts index 7d4f538841..1ff6887592 100644 --- a/packages/@glimmer/program/lib/program.ts +++ b/packages/@glimmer/program/lib/program.ts @@ -24,7 +24,7 @@ export type StdlibPlaceholder = [number, StdLibOperand]; const PAGE_SIZE = 0x100000; /** - * The Heap is responsible for dynamically allocating + * The Program Heap is responsible for dynamically allocating * memory in which we read/write the VM's instructions * from/to. When we malloc we pass out a VMHandle, which * is used as an indirect way of accessing the memory during @@ -56,6 +56,9 @@ export class ProgramHeapImpl implements ProgramHeap { this.handleTable = []; this.handleState = []; } + entries(): number { + return this.offset; + } pushRaw(value: number): void { this.sizeCheck(); @@ -100,6 +103,7 @@ export class ProgramHeapImpl implements ProgramHeap { // if we start using the compact API, we should change this. if (LOCAL_DEBUG) { this.handleState[handle] = ALLOCATED; + this.handleTable[handle + 1] = this.offset; } } 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/index.ts b/packages/@glimmer/runtime/index.ts index c722e45102..9ec4eb2b60 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -29,7 +29,7 @@ export { type EnvironmentDelegate, EnvironmentImpl, inTransaction, - runtimeContext, + runtimeOptions, } from './lib/environment'; export { array } from './lib/helpers/array'; export { concat } from './lib/helpers/concat'; @@ -59,13 +59,13 @@ export { export { clientBuilder, NewTreeBuilder, - RemoteLiveBlock, - UpdatableBlockImpl, + RemoteBlock, + ResettableBlockImpl, } from './lib/vm/element-builder'; export { LowLevelVM } from './lib/vm/low-level'; export { isSerializationFirstNode, - RehydrateBuilder, + RehydrateTree, rehydrationBuilder, SERIALIZATION_FIRST_NODE_STRING, } from './lib/vm/rehydrate-builder'; 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/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index 56135682bd..2d9c099403 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -72,7 +72,7 @@ import { registerDestructor } from '@glimmer/destroyable'; import { managerHasCapability } from '@glimmer/manager'; import { isConstRef, valueForRef } from '@glimmer/reference'; import { assign, dict, EMPTY_STRING_ARRAY, enumerate } from '@glimmer/util'; -import { $t0, $t1, InternalComponentCapabilities } from '@glimmer/vm'; +import { $s0, $t0, $t1, InternalComponentCapabilities } from '@glimmer/vm'; import type { CurriedValue } from '../../curried-value'; import type { UpdatingVM } from '../../vm'; @@ -367,8 +367,8 @@ APPEND_OPCODES.add(VM_PREPARE_ARGS_OP, (vm, { op1: register }) => { stack.push(args); }); -APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags, op2: register }) => { - let instance = check(vm.fetchValue(check(register, CheckRegister)), CheckComponentInstance); +APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { + let instance = check(vm.fetchValue($s0), CheckComponentInstance); let { definition, manager, capabilities } = instance; if (!managerHasCapability(manager, capabilities, InternalComponentCapabilities.createInstance)) { @@ -843,7 +843,7 @@ APPEND_OPCODES.add(VM_VIRTUAL_ROOT_SCOPE_OP, (vm, { op1: register }) => { APPEND_OPCODES.add(VM_SETUP_FOR_DEBUGGER_OP, (vm, { op1: register }) => { let state = check(vm.fetchValue(check(register, CheckRegister)), CheckFinishedComponentInstance); - if (state.table.hasEval) { + if (state.table.hasDebugger) { let lookup = (state.lookup = dict()); vm.scope().bindDebuggerScope(lookup); } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts index 733b252a5d..b8a9eb1577 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; } @@ -63,7 +63,6 @@ function toDynamicContentType(value: unknown) { !hasInternalHelperManager(value) ) { throw new Error( - // eslint-disable-next-line @typescript-eslint/no-base-to-string `Attempted use a dynamic value as a component or helper, but that value did not have an associated component or helper manager. The value was: ${value}` ); } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/debugger.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/debugger.ts index 14afc9851a..60351cf137 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/debugger.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/debugger.ts @@ -1,4 +1,4 @@ -import type { Scope } from '@glimmer/interfaces'; +import type { BlockSymbolNames, Scope } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference'; import { decodeHandle, VM_DEBUGGER_OP } from '@glimmer/constants'; import { unwrap } from '@glimmer/debug-util'; @@ -38,11 +38,11 @@ class ScopeInspector { constructor( private scope: Scope, - symbols: string[], + symbols: BlockSymbolNames, debugInfo: number[] ) { for (const slot of debugInfo) { - let name = unwrap(symbols[slot - 1]); + let name = unwrap(symbols.locals?.[slot - 1]); let ref = scope.getSymbol(slot); this.locals[name] = ref; } @@ -72,7 +72,7 @@ class ScopeInspector { } APPEND_OPCODES.add(VM_DEBUGGER_OP, (vm, { op1: _symbols, op2: _debugInfo }) => { - let symbols = vm.constants.getArray(_symbols); + let symbols = vm.constants.getValue(_symbols); let debugInfo = vm.constants.getArray(decodeHandle(_debugInfo)); let inspector = new ScopeInspector(vm.scope(), symbols, debugInfo); callback(valueForRef(vm.getSelf()), (path) => valueForRef(inspector.get(path))); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 2460e01a30..425ba1f393 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'; @@ -114,7 +114,7 @@ APPEND_OPCODES.add(VM_POP_REMOTE_ELEMENT_OP, (vm) => { let bounds = vm.tree().popRemoteElement(); if (vm.env.debugRenderTree !== undefined) { - // The RemoteLiveBlock is also its bounds + // The RemoteBlock is also its bounds vm.env.debugRenderTree.didRender(bounds, bounds); } }); @@ -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 dcc208145e..6a57bd81a3 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts @@ -3,9 +3,7 @@ import type { CurriedType, Helper, HelperDefinitionState, - Owner, ScopeBlock, - VM as PublicVM, } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference'; import { @@ -49,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'; @@ -68,8 +66,6 @@ import { CheckUndefinedReference, } from './-debug-strip'; -export type FunctionExpression = (vm: PublicVM) => Reference; - APPEND_OPCODES.add(VM_CURRY_OP, (vm, { op1: type, op2: _isStrict }) => { let stack = vm.stack; @@ -98,7 +94,7 @@ APPEND_OPCODES.add(VM_DYNAMIC_HELPER_OP, (vm) => { let args = check(stack.pop(), CheckArguments).capture(); let helperRef: Reference; - let initialOwner: Owner = vm.getOwner(); + let initialOwner = vm.getOwner(); let helperInstanceRef = createComputeRef(() => { if (helperRef !== undefined) { @@ -123,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); @@ -202,8 +198,8 @@ APPEND_OPCODES.add(VM_SET_BLOCK_OP, (vm, { op1: symbol }) => { vm.scope().bindBlock(symbol, [handle, scope, table]); }); -APPEND_OPCODES.add(VM_ROOT_SCOPE_OP, (vm, { op1: symbols }) => { - vm.pushRootScope(symbols, vm.getOwner()); +APPEND_OPCODES.add(VM_ROOT_SCOPE_OP, (vm, { op1: size }) => { + vm.pushRootScope(size, vm.getOwner()); }); APPEND_OPCODES.add(VM_GET_PROPERTY_OP, (vm, { op1: _key }) => { diff --git a/packages/@glimmer/runtime/lib/environment.ts b/packages/@glimmer/runtime/lib/environment.ts index 4b10c7bef0..1ebff10a74 100644 --- a/packages/@glimmer/runtime/lib/environment.ts +++ b/packages/@glimmer/runtime/lib/environment.ts @@ -3,12 +3,12 @@ import type { ComponentInstanceWithCreate, Environment, EnvironmentOptions, - EvaluationContext, GlimmerTreeChanges, GlimmerTreeConstruction, ModifierInstance, Nullable, RuntimeArtifacts, + RuntimeOptions, Transaction, TransactionSymbol, } from '@glimmer/interfaces'; @@ -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) { @@ -199,12 +199,12 @@ export interface EnvironmentDelegate { onTransactionCommit: () => void; } -export function runtimeContext( +export function runtimeOptions( options: EnvironmentOptions, delegate: EnvironmentDelegate, artifacts: RuntimeArtifacts, - resolver: ClassicResolver -): Pick { + resolver: Nullable +): RuntimeOptions { return { env: new EnvironmentImpl(options, delegate), program: new ProgramImpl(artifacts.constants, artifacts.heap), diff --git a/packages/@glimmer/runtime/lib/opcodes.ts b/packages/@glimmer/runtime/lib/opcodes.ts index 241e555489..0dfcd22d12 100644 --- a/packages/@glimmer/runtime/lib/opcodes.ts +++ b/packages/@glimmer/runtime/lib/opcodes.ts @@ -1,24 +1,33 @@ +import type { DebugOp, SomeDisassembledOperand } from '@glimmer/debug'; import type { + DebugVmSnapshot, Dict, Maybe, Nullable, + Optional, RuntimeOp, SomeVmOp, VmMachineOp, VmOp, } from '@glimmer/interfaces'; import { VM_SYSCALL_SIZE } from '@glimmer/constants'; -import { debug, logOpcode, opcodeMetadata, recordStackSize } from '@glimmer/debug'; -import { assert, unwrap } from '@glimmer/debug-util'; +import { + DebugLogger, + debugOp, + describeOp, + describeOpcode, + frag, + 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 { isScopeReference } from './scope'; -import { CURSOR_STACK } from './vm/element-builder'; +import type { Externs } from './vm/low-level'; export interface OpcodeJSON { type: number | string; @@ -41,21 +50,124 @@ export type Evaluate = | { syscall: false; evaluate: MachineOpcode }; export type DebugState = { - pc: number; - sp: number; - type: VmMachineOp | VmOp; - isMachine: 0 | 1; - size: number; - params?: Maybe | undefined; - name?: string | undefined; - state: unknown; + opcode: { + type: VmMachineOp | VmOp; + isMachine: 0 | 1; + size: number; + }; + closeGroup?: undefined | (() => void); + params?: Maybe> | undefined; + op?: Optional; + debug: DebugVmSnapshot; + snapshot: VmSnapshot; }; export class AppendOpcodes { private evaluateOpcode: Evaluate[] = new Array(VM_SYSCALL_SIZE).fill(null); - declare debugBefore?: (vm: VM, opcode: RuntimeOp) => DebugState; - declare debugAfter?: (vm: VM, pre: DebugState) => void; + declare debugBefore?: (vm: DebugVmSnapshot, opcode: RuntimeOp) => DebugState; + declare debugAfter?: (debug: DebugVmSnapshot, pre: DebugState) => void; + + 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); + + closeGroup = logger + .group(frag`${pos}. ${describeOp(opcode, debug.context.program, debug.template)}`) + .expanded(); + + let debugParams = []; + for (let [name, param] of Object.entries(op.params)) { + const value = param.value; + if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + debugParams.push(name, '=', value); + } + } + LOCAL_LOGGER.debug(...debugParams); + } + + recordStackSize(debug.registers[$sp]); + return { + op, + closeGroup, + params, + opcode: opcodeSnapshot, + debug, + snapshot, + }; + }; + + 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 = sp.after - sp.before; + if ( + meta && + meta.check !== false && + typeof meta.stackChange! === 'number' && + meta.stackChange !== actualChange + ) { + throw new Error( + `Error in ${pre.op?.name}:\n\n${pre.debug.registers[$pc]}. ${ + 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 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(); + try { + logger.log(diff.constructing.describe()); + logger.log(diff.blocks.describe()); + logger.log(diff.cursors.describe()); + } finally { + done(); + } + } + + pre.closeGroup?.(); + } + }; + } + } add(name: Name, evaluate: Syscall): void; add(name: Name, evaluate: MachineOpcode, kind: 'machine'): void; @@ -89,113 +201,18 @@ export class AppendOpcodes { } } -if (import.meta.env.VM_LOCAL_DEV) { - Object.assign(AppendOpcodes.prototype, { - debugBefore(vm: VM, opcode: RuntimeOp): DebugState { - let params: Maybe = undefined; - let opName: string | undefined = undefined; +export function externs(vm: VM): Externs | undefined { + return LOCAL_DEBUG + ? { + debugBefore: (opcode: RuntimeOp): DebugState => { + return APPEND_OPCODES.debugBefore!(dev(vm.debug), opcode); + }, - if (LOCAL_TRACE_LOGGING) { - const lowlevel = unwrap(vm.debug).lowlevel; - let pos = lowlevel.fetchRegister($pc) - opcode.size; - - [opName, params] = debug(vm.constants, opcode, opcode.isMachine)!; - - // console.log(`${typePos(vm['pc'])}.`); - LOCAL_LOGGER.debug(`${pos}. ${logOpcode(opName, params)}`); - - let debugParams = []; - for (let prop in params) { - debugParams.push(prop, '=', params[prop]); - } - - LOCAL_LOGGER.debug(...debugParams); - } - - let sp: number; - - if (LOCAL_DEBUG) { - sp = vm.fetchValue($sp); - } - - recordStackSize(vm.fetchValue($sp)); - return { - sp: sp!, - pc: vm.fetchValue($pc), - name: opName, - params, - type: opcode.type, - isMachine: opcode.isMachine, - size: opcode.size, - state: undefined, - }; - }, - - debugAfter(vm: VM, pre: DebugState) { - let { sp, type, isMachine, pc } = pre; - - if (LOCAL_DEBUG) { - const debug = unwrap(vm.debug); - - let meta = opcodeMetadata(type, isMachine); - let actualChange = vm.fetchValue($sp) - sp; - if ( - meta && - meta.check && - typeof meta.stackChange! === 'number' && - meta.stackChange !== actualChange - ) { - throw new Error( - `Error in ${pre.name}:\n\n${pc}. ${logOpcode( - pre.name!, - pre.params - )}\n\nStack changed by ${actualChange}, expected ${meta.stackChange}` - ); - } - - if (LOCAL_TRACE_LOGGING) { - const { lowlevel, registers } = debug; - LOCAL_LOGGER.debug( - '%c -> pc: %d, ra: %d, fp: %d, sp: %d, s0: %O, s1: %O, t0: %O, t1: %O, v0: %O', - 'color: orange', - lowlevel.registers[$pc], - lowlevel.registers[$ra], - lowlevel.registers[$fp], - lowlevel.registers[$sp], - registers[$s0], - registers[$s1], - registers[$t0], - registers[$t1], - registers[$v0] - ); - LOCAL_LOGGER.debug('%c -> eval stack', 'color: red', vm.stack.toArray()); - LOCAL_LOGGER.debug('%c -> block stack', 'color: magenta', vm.tree().debugBlocks()); - LOCAL_LOGGER.debug( - '%c -> destructor stack', - 'color: violet', - debug.destroyableStack.toArray() - ); - if (debug.stacks.scope.current === null) { - LOCAL_LOGGER.debug('%c -> scope', 'color: green', 'null'); - } else { - LOCAL_LOGGER.debug( - '%c -> scope', - 'color: green', - vm.scope().slots.map((s) => (isScopeReference(s) ? valueForRef(s) : s)) - ); - } - - LOCAL_LOGGER.debug( - '%c -> elements', - 'color: blue', - vm.tree()[CURSOR_STACK].current!.element - ); - - LOCAL_LOGGER.debug('%c -> constructing', 'color: aqua', vm.tree()['constructing']); - } + debugAfter: (state: DebugState): void => { + APPEND_OPCODES.debugAfter!(dev(vm.debug), state); + }, } - }, - }); + : undefined; } export const APPEND_OPCODES = new AppendOpcodes(); diff --git a/packages/@glimmer/runtime/lib/references/curry-value.ts b/packages/@glimmer/runtime/lib/references/curry-value.ts index 1fed4acd47..385bf0e1cf 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'; @@ -59,7 +59,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/render.ts b/packages/@glimmer/runtime/lib/render.ts index 8d61b08ccf..96ebb82da5 100644 --- a/packages/@glimmer/runtime/lib/render.ts +++ b/packages/@glimmer/runtime/lib/render.ts @@ -11,7 +11,8 @@ import type { TreeBuilder, } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference'; -import { expect, unwrapHandle } from '@glimmer/debug-util'; +import { dev, expect, unwrapHandle } from '@glimmer/debug-util'; +import { LOCAL_DEBUG } from '@glimmer/local-debug-flags'; import { childRefFor, createConstRef } from '@glimmer/reference'; import { debug } from '@glimmer/validator'; @@ -46,16 +47,20 @@ export function renderMain( context: EvaluationContext, owner: Owner, self: Reference, - treeBuilder: TreeBuilder, + tree: TreeBuilder, layout: CompilableProgram, dynamicScope: DynamicScope = new DynamicScopeImpl() ): TemplateIterator { let handle = unwrapHandle(layout.compile(context)); let numSymbols = layout.symbolTable.symbols.length; + let vm = VM.initial(context, { - scope: { self, size: numSymbols }, + scope: { + self, + size: numSymbols, + }, dynamicScope, - tree: treeBuilder, + tree, handle, owner, }); @@ -109,23 +114,22 @@ function renderInvocation( vm.stack.push(invocation); vm.stack.push(reified); + if (LOCAL_DEBUG) { + dev(vm.trace).willCall(invocation.handle); + } + return new TemplateIteratorImpl(vm); } export function renderComponent( context: EvaluationContext, - treeBuilder: TreeBuilder, + tree: TreeBuilder, owner: Owner, definition: ComponentDefinitionState, args: Record = {}, dynamicScope: DynamicScope = new DynamicScopeImpl() ): TemplateIterator { - let vm = VM.empty(context, { - tree: treeBuilder, - handle: context.stdlib.main, - dynamicScope, - owner, - }); + let vm = VM.initial(context, { tree, handle: context.stdlib.main, dynamicScope, owner }); return renderInvocation(vm, context, owner, definition, recordToReference(args)); } diff --git a/packages/@glimmer/runtime/lib/scope.ts b/packages/@glimmer/runtime/lib/scope.ts index 1c9e8d8842..928a3f4205 100644 --- a/packages/@glimmer/runtime/lib/scope.ts +++ b/packages/@glimmer/runtime/lib/scope.ts @@ -61,21 +61,39 @@ export class ScopeImpl implements Scope { return new ScopeImpl(owner, refs, null, null); } + readonly owner: Owner; + + private slots: ScopeSlot[]; + private callerScope: Scope | null; + private debuggerScope: Dict | null; + constructor( - readonly owner: Owner, + owner: Owner, // the 0th slot is `self` - readonly slots: Array, + slots: Array, // a single program can mix owners via curried components, and the state lives on root scopes - private callerScope: Scope | null, + callerScope: Scope | null, // named arguments and blocks passed to a layout that uses eval - private debuggerScope: Dict | null - ) {} + debuggerScope: Dict | null + ) { + this.owner = owner; + this.slots = slots; + this.callerScope = callerScope; + this.debuggerScope = debuggerScope; + } init({ self }: { self: Reference }): this { this.slots[0] = self; return this; } + /** + * @debug + */ + snapshot(): ScopeSlot[] { + return this.slots.slice(); + } + getSelf(): Reference { return this.get>(0); } diff --git a/packages/@glimmer/runtime/lib/vm/append.ts b/packages/@glimmer/runtime/lib/vm/append.ts index 7cb6e892f0..3d4dd261af 100644 --- a/packages/@glimmer/runtime/lib/vm/append.ts +++ b/packages/@glimmer/runtime/lib/vm/append.ts @@ -1,12 +1,14 @@ import type { BlockMetadata, CompilableTemplate, + DebugStacks, DebugTemplates, + DebugVmSnapshot, + DebugVmTrace, Destroyable, DynamicScope, Environment, EvaluationContext, - Nullable, Owner, Program, ProgramConstants, @@ -17,10 +19,9 @@ import type { TreeBuilder, UpdatingOpcode, } from '@glimmer/interfaces'; -import type { RuntimeOpImpl } from '@glimmer/program'; import type { OpaqueIterationItem, OpaqueIterator, Reference } from '@glimmer/reference'; import type { MachineRegister, Register, SyscallRegister } from '@glimmer/vm'; -import { expect, unwrapHandle } from '@glimmer/debug-util'; +import { dev, expect, unwrapHandle } from '@glimmer/debug-util'; import { associateDestroyableChild } from '@glimmer/destroyable'; import { assertGlobalContextWasSet } from '@glimmer/global-context'; import { LOCAL_DEBUG, LOCAL_TRACE_LOGGING } from '@glimmer/local-debug-flags'; @@ -29,18 +30,17 @@ import { LOCAL_LOGGER, reverse, Stack } from '@glimmer/util'; import { beginTrackFrame, endTrackFrame, resetTracking } from '@glimmer/validator'; import { $pc, isLowLevelRegister } from '@glimmer/vm'; -import type { DebugState } from '../opcodes'; import type { ScopeOptions } from '../scope'; -import type { LiveBlockList } from './element-builder'; +import type { AppendingBlockList } from './element-builder'; import type { EvaluationStack } from './stack'; -import type { BlockOpcode, VMState } from './update'; +import type { BlockOpcode } from './update'; import { BeginTrackFrameOpcode, EndTrackFrameOpcode, JumpIfNotModifiedOpcode, } from '../compiled/opcodes/vm'; -import { APPEND_OPCODES } from '../opcodes'; +import { externs } from '../opcodes'; import { ScopeImpl } from '../scope'; import { VMArgumentsImpl } from './arguments'; import { LowLevelVM } from './low-level'; @@ -49,6 +49,7 @@ import EvaluationStackImpl from './stack'; import { ListBlockOpcode, ListItemOpcode, TryOpcode } from './update'; class Stacks { + declare debug?: () => DebugStacks; readonly drop: object = {}; readonly scope = new Stack(); @@ -62,6 +63,19 @@ class Stacks { this.scope.push(scope); this.dynamicScope.push(dynamicScope); this.destroyable.push(this.drop); + + if (LOCAL_DEBUG) { + this.debug = (): DebugStacks => { + return { + scope: this.scope.snapshot(), + dynamicScope: this.dynamicScope.snapshot(), + updating: this.updating.snapshot(), + cache: this.cache.snapshot(), + list: this.list.snapshot(), + destroyable: this.destroyable.snapshot(), + }; + }; + } } } @@ -93,26 +107,13 @@ if (LOCAL_DEBUG) { }; } -interface DebugVmState { - context: EvaluationContext; - trace: { templates: DebugTemplates }; - lowlevel: LowLevelVM; - registers: SyscallRegisters; - destroyableStack: Stack; - stacks: Stacks; -} - export class VM { readonly #stacks: Stacks; - readonly #destructor: object; - readonly #destroyableStack = new Stack(); - readonly context: EvaluationContext; - readonly #tree: TreeBuilder; - readonly args: VMArgumentsImpl; readonly lowlevel: LowLevelVM; - readonly debug?: DebugVmState; + readonly debug?: () => DebugVmSnapshot; + readonly trace?: () => DebugVmTrace; get stack(): EvaluationStack { return this.lowlevel.stack as EvaluationStack; @@ -197,7 +198,7 @@ export class VM { call(handle: number | null) { if (handle !== null) { if (LOCAL_DEBUG) { - this.debug?.trace.templates.willCall(handle); + dev(this.trace).willCall(handle); } this.lowlevel.call(handle); @@ -207,20 +208,19 @@ export class VM { // Return to the `program` address stored in $ra return() { if (LOCAL_DEBUG) { - this.debug?.trace.templates.return(); + dev(this.trace).return(); } this.lowlevel.return(); } - /** - * End of migrated. - */ + readonly #tree: TreeBuilder; + readonly context: EvaluationContext; constructor( - { pc, scope, dynamicScope, stack }: ClosureState, - tree: TreeBuilder, - context: EvaluationContext + { scope, dynamicScope, stack, pc }: ClosureState, + context: EvaluationContext, + tree: TreeBuilder ) { if (import.meta.env.DEV) { assertGlobalContextWasSet!(); @@ -228,48 +228,36 @@ export class VM { let evalStack = EvaluationStackImpl.restore(stack, pc); - this.context = context; this.#tree = tree; + this.context = context; this.#stacks = new Stacks(scope, dynamicScope); this.args = new VMArgumentsImpl(); - this.lowlevel = new LowLevelVM( - evalStack, - context, - import.meta.env.VM_LOCAL_DEV - ? { - debugBefore: (opcode: RuntimeOpImpl): DebugState => { - return APPEND_OPCODES.debugBefore!(this, opcode); - }, - - debugAfter: (state: DebugState): void => { - APPEND_OPCODES.debugAfter!(this, state); - }, - } - : undefined, - evalStack.registers - ); - - this.#destructor = {}; - this.#destroyableStack.push(this.#destructor); - associateDestroyableChild(this.#stacks.drop, this.#destructor); + this.lowlevel = new LowLevelVM(evalStack, context, externs(this), evalStack.registers); if (LOCAL_DEBUG) { const templates = new DebugTemplatesImpl!(); - this.debug = { - context: this.context, + this.trace = () => templates; + + this.debug = () => ({ + context, + + trace: templates, + + elements: this.tree().debug!(), - trace: { - templates, - }, + stacks: this.#stacks.debug!(), - stacks: this.#stacks, - destroyableStack: this.#destroyableStack, - lowlevel: this.lowlevel, - registers: this.#registers, - } satisfies DebugVmState; + template: templates.active, + scope: this.scope().snapshot(), + stack: this.lowlevel.stack.snapshot!(), + registers: [ + ...this.lowlevel.registers, + ...sliceTuple(this.#registers, this.lowlevel.registers), + ], + }); } this.pushUpdating(); @@ -280,49 +268,21 @@ export class VM { options.owner, options.scope ?? { self: UNDEFINED_REFERENCE, size: 0 } ); - return VM.create({ - ...options, - scope, - context, - }); - } - static create({ - scope, - dynamicScope, - handle, - tree, - context, - }: { - scope: Scope; - dynamicScope: DynamicScope; - handle: number; - tree: TreeBuilder; - context: EvaluationContext; - }) { - let state = closureState(context.program.heap.getaddr(handle), scope, dynamicScope); - let vm = new VM(state, tree, context); - return vm; - } - - static empty(context: EvaluationContext, options: InitialVmState) { - let scope = ScopeImpl.root( - options.owner, - options.scope ?? { self: UNDEFINED_REFERENCE, size: 0 } + const state = closureState( + context.program.heap.getaddr(options.handle), + scope, + options.dynamicScope ); - return VM.create({ - ...options, - scope, - context, - }); + return new VM(state, context, options.tree); } compile(block: CompilableTemplate): number { let handle = unwrapHandle(block.compile(this.context)); if (LOCAL_DEBUG) { - this.debug?.trace.templates.register(handle, block.meta); + dev(this.trace).register(handle, block.meta); } return handle; @@ -340,15 +300,6 @@ export class VM { return this.context.env; } - captureState(args: number, pc = this.lowlevel.fetchRegister($pc)): VMState { - return { - pc, - scope: this.scope(), - dynamicScope: this.dynamicScope(), - stack: this.stack.capture(args), - }; - } - private captureClosure(args: number, pc = this.lowlevel.fetchRegister($pc)): ClosureState { return { pc, @@ -362,6 +313,20 @@ export class VM { return new Closure(this.captureClosure(args, pc), this.context); } + /** + * ## Opcodes + * + * - Append: `BeginComponentTransaction` + * + * ## State Changes + * + * [ ] create `guard` (`JumpIfNotModifiedOpcode`) + * [ ] create `tracker` (`BeginTrackFrameOpcode`) + * [!] push Updating Stack <- `guard` + * [!] push Updating Stack <- `tracker` + * [!] push Cache Stack <- `guard` + * [!] push Tracking Stack + */ beginCacheGroup(name?: string) { let opcodes = this.updating(); let guard = new JumpIfNotModifiedOpcode(); @@ -373,6 +338,20 @@ export class VM { beginTrackFrame(name); } + /** + * ## Opcodes + * + * - Append: `CommitComponentTransaction` + * + * ## State Changes + * + * Create a new `EndTrackFrameOpcode` (`end`) + * + * [!] pop CacheStack -> `guard` + * [!] pop Tracking Stack -> `tag` + * [ ] create `end` (`EndTrackFrameOpcode`) with `guard` + * [-] consume `tag` + */ commitCacheGroup() { let opcodes = this.updating(); let guard = expect(this.#stacks.cache.pop(), 'VM BUG: Expected a cache group'); @@ -383,6 +362,22 @@ export class VM { guard.finalize(tag, opcodes.length); } + /** + * ## Opcodes + * + * - Append: `Enter` + * + * ## State changes + * + * [!] push Element Stack as `block` + * [ ] create `try` (`TryOpcode`) with `block`, capturing `args` from the Eval Stack + * + * Did Enter (`try`): + * [-] associate destroyable `try` + * [!] push Destroyable Stack <- `try` + * [!] push Updating List <- `try` + * [!] push Updating Stack <- `try.children` + */ enter(args: number) { let updating: UpdatingOpcode[] = []; @@ -394,6 +389,31 @@ export class VM { this.didEnter(tryOpcode); } + /** + * ## Opcodes + * + * - Append: `Iterate` + * - Update: `ListBlock` + * + * ## State changes + * + * Create a new ref for the iterator item (`value`). + * Create a new ref for the iterator key (`key`). + * + * [ ] create `valueRef` (`Reference`) from `value` + * [ ] create `keyRef` (`Reference`) from `key` + * [!] push Eval Stack <- `valueRef` + * [!] push Eval Stack <- `keyRef` + * [!] push Element Stack <- `UpdatableBlock` as `block` + * [ ] capture `closure` with *2* items from the Eval Stack + * [ ] create `iteration` (`ListItemOpcode`) with `closure`, `block`, `key`, `keyRef` and `valueRef` + * + * Did Enter (`iteration`): + * [-] associate destroyable `iteration` + * [!] push Destroyable Stack <- `iteration` + * [!] push Updating List <- `iteration` + * [!] push Updating Stack <- `iteration.children` + */ enterItem({ key, value, memo }: OpaqueIterationItem): ListItemOpcode { let { stack } = this; @@ -416,12 +436,31 @@ export class VM { this.listBlock().initializeChild(opcode); } + /** + * ## Opcodes + * + * - Append: `EnterList` + * + * ## State changes + * + * [ ] capture `closure` with *0* items from the Eval Stack, and `$pc` from `offset` + * [ ] create `updating` (empty `Array`) + * [!] push Element Stack <- `list` (`BlockList`) with `updating` + * [ ] create `list` (`ListBlockOpcode`) with `closure`, `list`, `updating` and `iterableRef` + * [!] push List Stack <- `list` + * + * Did Enter (`list`): + * [-] associate destroyable `list` + * [!] push Destroyable Stack <- `list` + * [!] push Updating List <- `list` + * [!] push Updating Stack <- `list.children` + */ enterList(iterableRef: Reference, offset: number) { let updating: ListItemOpcode[] = []; let addr = this.lowlevel.target(offset); let state = this.capture(0, addr); - let list = this.tree().pushBlockList(updating) as LiveBlockList; + let list = this.tree().pushBlockList(updating) as AppendingBlockList; let opcode = new ListBlockOpcode(state, this.context, list, updating, iterableRef); @@ -430,64 +469,226 @@ export class VM { this.didEnter(opcode); } + /** + * ## Opcodes + * + * - Append: `Enter` + * - Append: `Iterate` + * - Append: `EnterList` + * - Update: `ListBlock` + * + * ## State changes + * + * [-] associate destroyable `opcode` + * [!] push Destroyable Stack <- `opcode` + * [!] push Updating List <- `opcode` + * [!] push Updating Stack <- `opcode.children` + * + */ private didEnter(opcode: BlockOpcode) { this.associateDestroyable(opcode); - this.#destroyableStack.push(opcode); + this.#stacks.destroyable.push(opcode); this.updateWith(opcode); this.pushUpdating(opcode.children); } + /** + * ## Opcodes + * + * - Append: `Exit` + * - Append: `ExitList` + * + * ## State changes + * + * [!] pop Destroyable Stack + * [!] pop Element Stack + * [!] pop Updating Stack + */ exit() { - this.#destroyableStack.pop(); - this.tree().popBlock(); + this.#stacks.destroyable.pop(); + this.#tree.popBlock(); this.popUpdating(); } + /** + * ## Opcodes + * + * - Append: `ExitList` + * + * ## State changes + * + * Pop List: + * [!] pop Destroyable Stack + * [!] pop Element Stack + * [!] pop Updating Stack + * + * [!] pop List Stack + */ exitList() { this.exit(); this.#stacks.list.pop(); } + /** + * ## Opcodes + * + * - Append: `RootScope` + * - Append: `VirtualRootScope` + * + * ## State changes + * + * [!] push Scope Stack + */ + pushRootScope(size: number, owner: Owner): Scope { + let scope = ScopeImpl.sized(owner, size); + this.#stacks.scope.push(scope); + return scope; + } + + /** + * ## Opcodes + * + * - Append: `ChildScope` + * + * ## State changes + * + * [!] push Scope Stack <- `child` of current Scope + */ + pushChildScope() { + this.#stacks.scope.push(this.scope().child()); + } + + /** + * ## Opcodes + * + * - Append: `Yield` + * + * ## State changes + * + * [!] push Scope Stack <- `scope` + */ + pushScope(scope: Scope) { + this.#stacks.scope.push(scope); + } + + /** + * ## Opcodes + * + * - Append: `PopScope` + * + * ## State changes + * + * [!] pop Scope Stack + */ + popScope() { + this.#stacks.scope.pop(); + } + + /** + * ## Opcodes + * + * - Append: `PushDynamicScope` + * + * ## State changes: + * + * [!] push Dynamic Scope Stack <- child of current Dynamic Scope + */ + pushDynamicScope(): DynamicScope { + let child = this.dynamicScope().child(); + this.#stacks.dynamicScope.push(child); + return child; + } + + /** + * ## Opcodes + * + * - Append: `BindDynamicScope` + * + * ## State changes: + * + * [!] pop Dynamic Scope Stack `names.length` times + */ + bindDynamicScope(names: string[]) { + let scope = this.dynamicScope(); + + for (const name of reverse(names)) { + scope.set(name, this.stack.pop>()); + } + } + + /** + * ## State changes + * + * - [!] push Updating Stack + * + * @utility + */ pushUpdating(list: UpdatingOpcode[] = []): void { this.#stacks.updating.push(list); } + /** + * ## State changes + * + * [!] pop Updating Stack + * + * @utility + */ popUpdating(): UpdatingOpcode[] { return expect(this.#stacks.updating.pop(), "can't pop an empty stack"); } + /** + * ## State changes + * + * [!] push Updating List + * + * @utility + */ updateWith(opcode: UpdatingOpcode) { this.updating().push(opcode); } - listBlock(): ListBlockOpcode { + private listBlock(): ListBlockOpcode { return expect(this.#stacks.list.current, 'expected a list block'); } + /** + * ## State changes + * + * [-] associate destroyable `child` + * + * @utility + */ associateDestroyable(child: Destroyable): void { - let parent = expect(this.#destroyableStack.current, 'Expected destructor parent'); + let parent = expect(this.#stacks.destroyable.current, 'Expected destructor parent'); associateDestroyableChild(parent, child); } - tryUpdating(): Nullable { - return this.#stacks.updating.current; - } - - updating(): UpdatingOpcode[] { + private updating(): UpdatingOpcode[] { return expect( this.#stacks.updating.current, 'expected updating opcode on the updating opcode stack' ); } + /** + * Get Tree Builder + */ tree(): TreeBuilder { return this.#tree; } + /** + * Get current Scope + */ scope(): Scope { return expect(this.#stacks.scope.current, 'expected scope on the scope stack'); } + /** + * Get current Dynamic Scope + */ dynamicScope(): DynamicScope { return expect( this.#stacks.dynamicScope.current, @@ -495,30 +696,6 @@ export class VM { ); } - pushChildScope() { - this.#stacks.scope.push(this.scope().child()); - } - - pushDynamicScope(): DynamicScope { - let child = this.dynamicScope().child(); - this.#stacks.dynamicScope.push(child); - return child; - } - - pushRootScope(size: number, owner: Owner): Scope { - let scope = ScopeImpl.sized(owner, size); - this.#stacks.scope.push(scope); - return scope; - } - - pushScope(scope: Scope) { - this.#stacks.scope.push(scope); - } - - popScope() { - this.#stacks.scope.pop(); - } - popDynamicScope() { this.#stacks.dynamicScope.pop(); } @@ -587,7 +764,6 @@ export class VM { next(): RichIteratorResult { let { env } = this; - let tree = this.#tree; let opcode = this.lowlevel.nextStatement(); let result: RichIteratorResult; if (opcode !== null) { @@ -599,27 +775,16 @@ export class VM { result = { done: true, - value: new RenderResultImpl(env, this.popUpdating(), tree.popBlock(), this.#stacks.drop), + value: new RenderResultImpl( + env, + this.popUpdating(), + this.#tree.popBlock(), + this.#stacks.drop + ), }; } return result; } - - bindDynamicScope(names: string[]) { - let scope = this.dynamicScope(); - - for (const name of reverse(names)) { - scope.set(name, this.stack.pop>()); - } - } -} - -export interface InitialVmState { - handle: number; - tree: TreeBuilder; - dynamicScope: DynamicScope; - owner: Owner; - scope?: ScopeOptions; } function closureState(pc: number, scope: Scope, dynamicScope: DynamicScope): ClosureState { @@ -631,18 +796,27 @@ function closureState(pc: number, scope: Scope, dynamicScope: DynamicScope): Clo }; } -export interface MinimalInitOptions { +export interface InitialVmState { + /** + * The address of the compiled template. This is converted into a + * pc when the VM is created. + */ handle: number; - treeBuilder: TreeBuilder; + + /** + * Optionally, specify the root scope for the VM. If not specified, + * the VM will use a root scope with no `this` reference and no + * symbols. + */ + scope?: ScopeOptions; + /** + * + */ + tree: TreeBuilder; dynamicScope: DynamicScope; owner: Owner; } -export interface InitOptions extends MinimalInitOptions { - self: Reference; - numSymbols: number; -} - export interface ClosureState { /** * The program counter that subsequent evaluations should start from. @@ -684,6 +858,13 @@ export class Closure { } evaluate(tree: TreeBuilder): VM { - return new VM(this.state, tree, this.context); + 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/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 7d37a07d6b..e7c85a2ca1 100644 --- a/packages/@glimmer/runtime/lib/vm/element-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/element-builder.ts @@ -3,7 +3,6 @@ import type { AttrNamespace, Bounds, Cursor, - CursorStackSymbol, ElementOperations, Environment, GlimmerTreeChanges, @@ -19,8 +18,9 @@ import type { SimpleText, TreeBuilder, } 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'; import type { DynamicAttribute } from './attributes/dynamic'; @@ -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; } @@ -72,16 +74,20 @@ export class Fragment implements Bounds { } } -export const CURSOR_STACK: CursorStackSymbol = Symbol('CURSOR_STACK') as CursorStackSymbol; - export class NewTreeBuilder implements TreeBuilder { + declare debug?: () => { + blocks: AppendingBlock[]; + constructing: Nullable; + cursors: Cursor[]; + }; + public dom: GlimmerTreeConstruction; public updateOperations: GlimmerTreeChanges; public constructing: Nullable = null; public operations: Nullable = null; private env: Environment; - [CURSOR_STACK] = new Stack(); + readonly cursors = new Stack(); private modifierStack = new Stack>(); private blockStack = new Stack(); @@ -94,7 +100,7 @@ export class NewTreeBuilder implements TreeBuilder { let nextSibling = block.reset(env); let stack = new this(env, parentNode, nextSibling).initialize(); - stack.pushLiveBlock(block); + stack.pushBlock(block); return stack; } @@ -104,6 +110,14 @@ export class NewTreeBuilder implements TreeBuilder { this.env = env; this.dom = env.getAppendOperations(); this.updateOperations = env.getDOM(); + + if (LOCAL_DEBUG) { + this.debug = () => ({ + blocks: this.blockStack.snapshot(), + constructing: this.constructing, + cursors: this.cursors.snapshot(), + }); + } } protected initialize(): this { @@ -116,11 +130,11 @@ export class NewTreeBuilder implements TreeBuilder { } get element(): SimpleElement { - return this[CURSOR_STACK].current!.element; + return this.cursors.current!.element; } get nextSibling(): Nullable { - return this[CURSOR_STACK].current!.nextSibling; + return this.cursors.current!.nextSibling; } get hasBlocks() { @@ -132,23 +146,23 @@ export class NewTreeBuilder implements TreeBuilder { } popElement() { - this[CURSOR_STACK].pop(); - expect(this[CURSOR_STACK].current, "can't pop past the last element"); + this.cursors.pop(); + expect(this.cursors.current, "can't pop past the last element"); } pushAppendingBlock(): AppendingBlock { - return this.pushLiveBlock(new SimpleLiveBlock(this.element)); + return this.pushBlock(new AppendingBlockImpl(this.element)); } - pushResettableBlock(): UpdatableBlockImpl { - return this.pushLiveBlock(new UpdatableBlockImpl(this.element)); + pushResettableBlock(): ResettableBlockImpl { + return this.pushBlock(new ResettableBlockImpl(this.element)); } - pushBlockList(list: AppendingBlock[]): LiveBlockList { - return this.pushLiveBlock(new LiveBlockList(this.element, list)); + pushBlockList(list: AppendingBlock[]): AppendingBlockList { + return this.pushBlock(new AppendingBlockList(this.element, list)); } - protected pushLiveBlock(block: T, isRemote = false): T { + protected pushBlock(block: T, isRemote = false): T { let current = this.blockStack.current; if (current !== null) { @@ -214,7 +228,7 @@ export class NewTreeBuilder implements TreeBuilder { element: SimpleElement, guid: string, insertBefore: Maybe - ): RemoteLiveBlock { + ): RemoteBlock { return this.__pushRemoteElement(element, guid, insertBefore); } @@ -222,7 +236,7 @@ export class NewTreeBuilder implements TreeBuilder { element: SimpleElement, _guid: string, insertBefore: Maybe - ): RemoteLiveBlock { + ): RemoteBlock { this.pushElement(element, insertBefore); if (insertBefore === undefined) { @@ -231,20 +245,20 @@ export class NewTreeBuilder implements TreeBuilder { } } - let block = new RemoteLiveBlock(element); + let block = new RemoteBlock(element); - return this.pushLiveBlock(block, true); + return this.pushBlock(block, true); } - popRemoteElement(): RemoteLiveBlock { + popRemoteElement(): RemoteBlock { const block = this.popBlock(); - assert(block instanceof RemoteLiveBlock, '[BUG] expecting a RemoteLiveBlock'); + assert(block instanceof RemoteBlock, '[BUG] expecting a RemoteBlock'); this.popElement(); return block; } protected pushElement(element: SimpleElement, nextSibling: Maybe = null): void { - this[CURSOR_STACK].push(new CursorImpl(element, nextSibling)); + this.cursors.push(new CursorImpl(element, nextSibling)); } private pushModifiers(modifiers: Nullable): void { @@ -374,12 +388,23 @@ export class NewTreeBuilder implements TreeBuilder { } } -export class SimpleLiveBlock implements AppendingBlock { +export class AppendingBlockImpl 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; @@ -388,7 +413,7 @@ export class SimpleLiveBlock implements AppendingBlock { firstNode(): SimpleNode { let first = expect( this.first, - 'cannot call `firstNode()` while `SimpleLiveBlock` is still initializing' + 'cannot call `firstNode()` while `AppendingBlock` is still initializing' ); return first.firstNode(); @@ -397,7 +422,7 @@ export class SimpleLiveBlock implements AppendingBlock { lastNode(): SimpleNode { let last = expect( this.last, - 'cannot call `lastNode()` while `SimpleLiveBlock` is still initializing' + 'cannot call `lastNode()` while `AppendingBlock` is still initializing' ); return last.lastNode(); @@ -439,10 +464,12 @@ export class SimpleLiveBlock implements AppendingBlock { } } -export class RemoteLiveBlock extends SimpleLiveBlock { +export class RemoteBlock extends AppendingBlockImpl { 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 @@ -475,7 +502,12 @@ export class RemoteLiveBlock extends SimpleLiveBlock { } } -export class UpdatableBlockImpl extends SimpleLiveBlock implements ResettableBlock { +export class ResettableBlockImpl extends AppendingBlockImpl implements ResettableBlock { + constructor(parent: SimpleElement) { + super(parent); + setLocalDebugType('block:resettable', this); + } + reset(): Nullable { destroy(this); let nextSibling = clear(this); @@ -489,7 +521,7 @@ export class UpdatableBlockImpl extends SimpleLiveBlock implements ResettableBlo } // FIXME: All the noops in here indicate a modelling problem -export class LiveBlockList implements AppendingBlock { +export class AppendingBlockList implements AppendingBlock { constructor( private readonly parent: SimpleElement, public boundList: AppendingBlock[] @@ -505,7 +537,7 @@ export class LiveBlockList implements AppendingBlock { firstNode(): SimpleNode { let head = expect( this.boundList[0], - 'cannot call `firstNode()` while `LiveBlockList` is still initializing' + 'cannot call `firstNode()` while `AppendingBlockList` is still initializing' ); return head.firstNode(); @@ -516,7 +548,7 @@ export class LiveBlockList implements AppendingBlock { let tail = expect( boundList[boundList.length - 1], - 'cannot call `lastNode()` while `LiveBlockList` is still initializing' + 'cannot call `lastNode()` while `AppendingBlockList` is still initializing' ); return tail.lastNode(); diff --git a/packages/@glimmer/runtime/lib/vm/low-level.ts b/packages/@glimmer/runtime/lib/vm/low-level.ts index d4997cccee..560324a60e 100644 --- a/packages/@glimmer/runtime/lib/vm/low-level.ts +++ b/packages/@glimmer/runtime/lib/vm/low-level.ts @@ -128,10 +128,7 @@ export class LowLevelVM { } nextStatement(): Nullable { - let { - registers, - context: { program }, - } = this; + let { registers, context } = this; let pc = registers[$pc]; @@ -146,7 +143,7 @@ export class LowLevelVM { // to where we are going. We can't simply ask for the size // in a jump because we have have already incremented the // program counter to the next instruction prior to executing. - let opcode = program.opcode(pc); + let opcode = context.program.opcode(pc); let operationSize = (this.currentOpSize = opcode.size); this.registers[$pc] += operationSize; diff --git a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts index f4d224a2bb..af61c9fc2b 100644 --- a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts @@ -16,7 +16,7 @@ import { COMMENT_NODE, ELEMENT_NODE, NS_SVG, TEXT_NODE } from '@glimmer/constant import { assert, castToBrowser, castToSimple, expect } from '@glimmer/debug-util'; import { ConcreteBounds, CursorImpl } from '../bounds'; -import { CURSOR_STACK, NewTreeBuilder, RemoteLiveBlock } from './element-builder'; +import { NewTreeBuilder, RemoteBlock } from './element-builder'; export const SERIALIZATION_FIRST_NODE_STRING = '%+b:0%'; @@ -38,9 +38,9 @@ export class RehydratingCursor extends CursorImpl { } } -export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { +export class RehydrateTree extends NewTreeBuilder implements TreeBuilder { private unmatchedAttributes: Nullable = null; - declare [CURSOR_STACK]: Stack; // Hides property on base class + declare cursors: Stack; // Hides property on base class blockDepth = 0; startingBlockOffset: number; @@ -87,7 +87,7 @@ export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { } get currentCursor(): Nullable { - return this[CURSOR_STACK].current; + return this.cursors.current; } get candidate(): Nullable { @@ -125,8 +125,8 @@ export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { override pushElement( /** called from parent constructor before we initialize this */ this: - | RehydrateBuilder - | (NewTreeBuilder & Partial>), + | RehydrateTree + | (NewTreeBuilder & Partial>), element: SimpleElement, nextSibling: Maybe = null ) { @@ -147,7 +147,7 @@ export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { this.candidate = element.nextSibling; } - this[CURSOR_STACK].push(cursor); + this.cursors.push(cursor); } // clears until the end of the current container @@ -456,7 +456,7 @@ export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { element: SimpleElement, cursorId: string, insertBefore: Maybe - ): RemoteLiveBlock { + ): RemoteBlock { const marker = this.getMarker(castToBrowser(element, 'HTML'), cursorId); assert( @@ -473,7 +473,7 @@ export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { } const cursor = new RehydratingCursor(element, null, this.blockDepth); - this[CURSOR_STACK].push(cursor); + this.cursors.push(cursor); if (marker === null) { this.disableRehydration(insertBefore); @@ -481,8 +481,8 @@ export class RehydrateBuilder extends NewTreeBuilder implements TreeBuilder { this.candidate = this.remove(marker); } - const block = new RemoteLiveBlock(element); - return this.pushLiveBlock(block, true); + const block = new RemoteBlock(element); + return this.pushBlock(block, true); } override didAppendBounds(bounds: Bounds): Bounds { @@ -551,5 +551,5 @@ function findByName(array: SimpleAttr[], name: string): SimpleAttr | undefined { } export function rehydrationBuilder(env: Environment, cursor: CursorImpl): TreeBuilder { - return RehydrateBuilder.forInitialRender(env, cursor); + return RehydrateTree.forInitialRender(env, cursor); } diff --git a/packages/@glimmer/runtime/lib/vm/stack.ts b/packages/@glimmer/runtime/lib/vm/stack.ts index 4357de7356..b89cc8fbf3 100644 --- a/packages/@glimmer/runtime/lib/vm/stack.ts +++ b/packages/@glimmer/runtime/lib/vm/stack.ts @@ -19,7 +19,8 @@ export interface EvaluationStack { slice(start: number, end: number): T[]; capture(items: number): unknown[]; reset(): void; - toArray(): unknown[]; + + snapshot?(): unknown[]; } export default class EvaluationStackImpl implements EvaluationStack { @@ -45,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); } } @@ -93,7 +99,13 @@ export default class EvaluationStackImpl implements EvaluationStack { this.stack.length = 0; } - toArray() { - return this.stack.slice(this.registers[$fp], this.registers[$sp] + 1); + declare snapshot?: (this: EvaluationStackImpl) => unknown[]; + + static { + if (LOCAL_DEBUG) { + EvaluationStackImpl.prototype.snapshot = function () { + return this.stack.slice(this.registers[$fp], this.registers[$sp] + 1); + }; + } } } diff --git a/packages/@glimmer/runtime/lib/vm/update.ts b/packages/@glimmer/runtime/lib/vm/update.ts index af35ee50da..febcf59d7b 100644 --- a/packages/@glimmer/runtime/lib/vm/update.ts +++ b/packages/@glimmer/runtime/lib/vm/update.ts @@ -22,7 +22,7 @@ import { logStep, Stack } from '@glimmer/util'; import { debug, resetTracking } from '@glimmer/validator'; import type { Closure } from './append'; -import type { LiveBlockList } from './element-builder'; +import type { AppendingBlockList } from './element-builder'; import { clear, move as moveBounds } from '../bounds'; import { NewTreeBuilder } from './element-builder'; @@ -158,11 +158,9 @@ export class TryOpcode extends BlockOpcode implements ExceptionHandler { let tree = NewTreeBuilder.resume(env, bounds); let vm = state.evaluate(tree); - let updating: UpdatingOpcode[] = []; let children = (this.children = []); let result = vm.execute((vm) => { - vm.pushUpdating(updating); vm.updateWith(this); vm.pushUpdating(children); }); @@ -186,12 +184,6 @@ export class ListItemOpcode extends TryOpcode { super(state, context, bounds, []); } - updateReferences(item: OpaqueIterationItem) { - this.retained = true; - updateRef(this.value, item.value); - updateRef(this.memo, item.memo); - } - shouldRemove(): boolean { return !this.retained; } @@ -209,12 +201,12 @@ export class ListBlockOpcode extends BlockOpcode { private marker: SimpleComment | null = null; private lastIterator: OpaqueIterator; - protected declare readonly bounds: LiveBlockList; + protected declare readonly bounds: AppendingBlockList; constructor( state: Closure, context: EvaluationContext, - bounds: LiveBlockList, + bounds: AppendingBlockList, children: ListItemOpcode[], private iterableRef: Reference ) { @@ -365,7 +357,6 @@ export class ListBlockOpcode extends BlockOpcode { let vm = state.evaluate(elementStack); vm.execute((vm) => { - vm.pushUpdating(); let opcode = vm.enterItem(item); opcode.index = children.length; 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 2ce51cc2bb..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[] = []; @@ -86,7 +98,7 @@ export class ProgramSymbolTable extends SymbolTable { this.#hasDebugger = true; } - get hasEval(): boolean { + get hasDebugger(): boolean { return this.#hasDebugger; } diff --git a/packages/@glimmer/syntax/lib/v2/README.md b/packages/@glimmer/syntax/lib/v2/README.md index acb6f2fc37..704465acc2 100644 --- a/packages/@glimmer/syntax/lib/v2/README.md +++ b/packages/@glimmer/syntax/lib/v2/README.md @@ -93,13 +93,13 @@ None. Strict mode templates must be embedded in a JavaScript context where all f ### Namespaced Variable Resolution -| | | -| ------------------- | ------------------------------------------------- | -| Syntax Positions | `SubExpression`, `Block`, `Modifier`, `Component` | -| Path has dots? | ❌ | -| Arguments? | Any | -| | | -| Namespace | see table below | +| | | +| ---------------- | ------------------------------------------------- | +| Syntax Positions | `SubExpression`, `Block`, `Modifier`, `Component` | +| Path has dots? | ❌ | +| Arguments? | Any | +| | | +| Namespace | see table below | These resolutions occur in syntaxes that are definitely calls (e.g. subexpressions, blocks, modifiers, etc.). @@ -118,13 +118,13 @@ If the variable reference cannot be resolved in its namespace. ### Namespaced Resolution: Append -| | | -| ------------------- | ----------------------- | -| Syntax Positions | append | -| Path has dots? | ❌ | -| Arguments? | Any | -| | | -| Namespace | `helper` or `component` | +| | | +| ---------------- | ----------------------- | +| Syntax Positions | append | +| Path has dots? | ❌ | +| Arguments? | Any | +| | | +| Namespace | `helper` or `component` | This resolution occurs in append nodes with at least one argument, and when the path does not have dots (e.g. `{{hello world}}`). @@ -148,13 +148,13 @@ If the variable reference cannot be resolved in the `helper` or `component` name This resolution context occurs in attribute nodes with zero arguments, and when the path does not have dots. -| | | -| ------------------- | ------------------------ | -| Syntax Positions | attribute, interpolation | -| Path has dots? | ❌ | -| Arguments? | Any | -| | | -| Namespace | `helper` | +| | | +| ---------------- | ------------------------ | +| Syntax Positions | attribute, interpolation | +| Path has dots? | ❌ | +| Arguments? | Any | +| | | +| Namespace | `helper` | #### Applicable Situations @@ -193,10 +193,10 @@ Situations that meet all three of these criteria are syntax errors: #### Block, Component, Modifier, SubExpression -| | | -| ------------------- | --- | -| Path has dots? | ❌ | -| Arguments? | Any | +| | | +| -------------- | --- | +| Path has dots? | ❌ | +| Arguments? | Any | | Syntax Position | Example | | Namespace | | --------------- | ------------- | --- | ----------- | 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/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]; + } +} diff --git a/packages/@glimmer/util/lib/collections.ts b/packages/@glimmer/util/lib/collections.ts index 75ec70501b..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); } @@ -46,6 +46,10 @@ export class StackImpl implements Stack { return this.stack.length === 0; } + snapshot(): T[] { + return [...this.stack]; + } + toArray(): T[] { return this.stack; } diff --git a/packages/@glimmer/vm-babel-plugins/README.md b/packages/@glimmer/vm-babel-plugins/README.md index 8599d9bff9..0b6b0b3921 100644 --- a/packages/@glimmer/vm-babel-plugins/README.md +++ b/packages/@glimmer/vm-babel-plugins/README.md @@ -5,7 +5,7 @@ It exports a function which returns an array of babel plugins that should be added to your Babel configuration. ```js -let vmBabelPlugins = require('@glimmer/vm-babel-plugins'); +let vmBabelPlugins = require("@glimmer/vm-babel-plugins"); module.exports = { plugins: [...vmBabelPlugins({ isDebug: true })], diff --git a/packages/@glimmer/vm/lib/opcodes.toml b/packages/@glimmer/vm/lib/opcodes.toml deleted file mode 100644 index 21b385cfa9..0000000000 --- a/packages/@glimmer/vm/lib/opcodes.toml +++ /dev/null @@ -1,717 +0,0 @@ -[machine.pushf] - -format = "PushFrame" -operand-stack = [[], ["$ra", "$fp"]] -operation = "Push a stack frame" - -[machine.popf] - -format = "PopFrame" -operand-stack = [["$ra", "$fp"], []] -operation = "Pop a stack frame" -skip = true - -[machine.vcall] - -format = "InvokeVirtual" -operand-stack = [["Handle"], []] -operation = "Evaluate the handle at the top of the stack." - -[machine.scall] - -format = ["InvokeStatic", "offset:u32"] -operation = "Evaluate the handle." - -[machine.goto] - -format = ["Jump", "to:u32"] -operation = "Jump to the specified offset." - -[machine.ret] - -format = "Return" -operation = "Return to the previous frame." -skip = true - -[machine.setra] - -format = ["ReturnTo", "offset:i32"] -operation = "Return to a place in the program given an offset" - -[syscall.ncall] - -format = ["Helper", "helper:handle"] -operand-stack = [["Reference...", "Arguments"], ["Reference"]] -operation = "Evaluate a Helper." - -[syscall.dynamiccall] - -format = ["DynamicHelper"] -operand-stack = [["Reference...", "Reference", "Arguments"], ["Reference"]] -operation = "Evaluate a dynamic helper." - -[syscall.vsargs] - -format = ["SetNamedVariables", "register:u32"] -operation = """ -Bind the named arguments in the Arguments to the symbols -specified by the symbol table in the component state at register. -""" - -[syscall.vbblocks] - -format = ["SetBlocks", "register:u32"] -operation = """ -Bind the blocks in the Arguments to the symbols specified by the -symbol table in the component state at register. -""" - -[syscall.sbvar] - -format = ["SetVariable", "symbol:u32"] -operand-stack = [["Reference"], []] -operation = """ -Bind the variable represented by a symbol from -the value at the top of the stack. -""" - -[syscall.sblock] - -format = ["SetBlock", "symbol:u32"] -operand-stack = [["symbol-table", "scope", "block"], []] -operation = "Bind the block at the top of the stack." - -[syscall.symload] - -format = ["GetVariable", "symbol:u32"] -operand-stack = [[], ["Reference"]] -operation = """ -Push the contents of the variable represented by -a symbol (a positional or named argument) onto -the stack. -""" - -[syscall.getprop] - -format = ["GetProperty", "property:str"] -operand-stack = [["Reference"], ["Reference"]] -operation = """ -Pop a Reference from the top of the stack, and push a -Reference constructed by `.get(property)`. -""" - -[syscall.blockload] - -format = ["GetBlock", "block:u32"] -notes = "TODO: The three elements on the stack can be null" -operand-stack = [[], ["scope-block"]] -operation = "Push the specified bound block onto the stack." - -[syscall.blockspread] - -format = ["SpreadBlock"] -operand-stack = [["scope-block"], ["symbol-table", "scope", "handle"]] -operation = "Spread a scope block into three stack elements" - -[syscall.hasblockload] - -format = ["HasBlock"] -operand-stack = [["block?"], ["bool"]] -operation = """ -Push TRUE onto the stack if the specified block -is bound and FALSE if it is not. -""" - -[syscall.hasparamsload] - -format = ["HasBlockParams"] -operand-stack = [["block?", "scope?", "symbol-table?"], ["bool"]] -operation = """ -Push TRUE onto the stack if the specified block -is bound *and* has at least one specified formal -parameter, and FALSE otherwise. -""" - -[syscall.concat] - -format = ["Concat", "count:u32"] -operand-stack = [["Reference", "Reference..."], ["Reference"]] -operation = """ -Pop count `Reference`s off the stack and -construct a new ConcatReference from them (in reverse -order). -""" - -[syscall.ifinline] - -format = ["IfInline", "count:u32"] -operand-stack = [["Reference", "Reference", "Reference"], ["Reference"]] -operation = """ -Inline if expression -""" - -[syscall.not] - -format = ["Not", "count:u32"] -operand-stack = [["Reference"], ["Reference"]] -operation = """ -Inline not expression -""" - -[syscall.rconstload] - -format = ["Constant", "constant:unknown"] -operand-stack = [[], ["unknown"]] -operation = """ - Push an Object constant onto the stack that is not - a JavaScript primitive. -""" - -[syscall.rconstrefload] - -format = ["ConstantReference", "constant:unknown"] -operand-stack = [[], ["Reference"]] -operation = """ - Push a reference constant onto the stack that is not - a JavaScript primitive. -""" - -[syscall.pconstload] - -format = ["Primitive", "constant:primitive"] -notes = """ -The two high bits of the constant reference describe -the kind of primitive: - -00: number -01: string -10: true | false | null | undefined -""" -operand-stack = [[], ["Primitive"]] -operation = """ -Wrap a JavaScript primitive in a reference and push it -onto the stack. -""" - -[syscall.ptoref] - -format = "PrimitiveReference" -operand-stack = [["Primitive"], ["Reference"]] -operation = "Convert the top of the stack into a primitive reference." - -[syscall.reifyload] - -format = "ReifyU32" -notes = "The Reference represents a u32" -operand-stack = [["Reference"], ["Reference", "u32"]] -operation = "Convert the top of the stack into a number." - -[syscall.dup] - -format = ["Dup", "register:u32", "offset:u32"] -operand-stack = [["unknown"], ["unknown", "unknown"]] -operation = "Duplicate and push item from an offset in the stack." - -[syscall.pop] - -format = ["Pop", "count:u32"] -operation = "Pop N items off the stack and throw away the value." -skip = true - -[syscall.put] - -format = ["Load", "register:u32"] -operand-stack = [["unknown"], []] -operation = "Load a value into a register" - -[syscall.regload] - -format = ["Fetch", "register:u32"] -operand-stack = [[], ["unknown"]] -operation = "Fetch a value from a register" - -[syscall.rscopepush] - -format = ["RootScope", "symbols:u32"] -notes = """ -A root scope has no parent scope, and therefore inherits no lexical -variables. -""" -operation = "Push a new root scope onto the scope stack." - -[syscall.vrscopepush] - -format = ["VirtualRootScope", "register:u32"] -notes = """ -The symbol count is determined by the component state in -the specified register. -""" -operation = "Push a new root scope onto the scope stack." - -[syscall.cscopepush] - -format = "ChildScope" -notes = """ -A child scope inherits from the current parent scope, and therefore -shares its lexical variables. -""" -operation = "Push a new child scope onto the scope stack." - -[syscall.scopepop] - -format = "PopScope" -operation = "Pop the current scope from the scope stack." - -[syscall.apnd_text] - -format = ["Text", "contents:str"] -operation = "Append a Text node with value `contents`" - -[syscall.apnd_comment] - -format = ["Comment", "contents:str"] -operation = "Append a Comment node with value `contents`" - -[syscall.apnd_dynhtml] - -format = "AppendHTML" -operand-stack = [["Reference"], []] -operation = "Append content as HTML." - -[syscall.apnd_dynshtml] - -format = "AppendSafeHTML" -operand-stack = [["Reference"], []] -operation = "Append SafeHTML as HTML." - -[syscall.apnd_dynfrag] - -format = "AppendDocumentFragment" -operand-stack = [["Reference"], []] -operation = "Append DocumentFragment." - -[syscall.apnd_dynnode] - -format = "AppendNode" -operand-stack = [["Reference"], []] -operation = "Append Node." - -[syscall.apnd_dyntext] - -format = "AppendText" -operand-stack = [["Reference"], []] -operation = "Append content as text." - -[syscall.apnd_tag] - -format = ["OpenElement", "tag:str"] -operation = "Open a new Element named `tag`." - -[syscall.apnd_dyntag] - -format = "OpenDynamicElement" -operand-stack = [["string"], []] -operation = """ -Open a new Element with a name on the stack. -""" - -[syscall.apnd_remotetag] - -format = "PushRemoteElement" -notes = "the references represent string, node, and element, in order" -operand-stack = [["Reference", "Reference", "Reference"], []] -operation = "Open a new remote element" - -[syscall.apnd_attr] - -format = ["StaticAttr", "name:str", "value:str", "namespace:option-str"] -operation = "Add an attribute to the current Element." - -[syscall.apnd_dynattr] - -format = ["DynamicAttr", "name:str", "trusting:bool", "namespace:option-str"] -notes = """ -If `trusting` is false, the host may sanitize the attribute -based upon known risks. -""" -operand-stack = [["Reference"], []] -operation = """ -Add an attribute to the current element using the value -at the top of the stack. -""" - -[syscall.apnd_cattr] - -format = ["ComponentAttr", "name:str", "trusting:bool", "namespace:option-str"] -operand-stack = [["Reference"], []] -operation = """ -Add an attribute to the current element using the value -at the top of the stack. -""" - -[syscall.apnd_flushtag] - -format = "FlushElement" -operation = "Finish setting attributes on the current element." - -[syscall.apnd_closetag] - -format = "CloseElement" -operation = "Close the current element." - -[syscall.apnd_closeremotetag] - -format = "PopRemoteElement" -operation = "Close the current remote element" - -[syscall.apnd_modifier] - -format = ["Modifier", "helper:handle"] -operand-stack = [["Arguments"], []] -operation = "Execute the modifier represented by the handle" - -[syscall.setdynscope] - -format = ["BindDynamicScope", "names:str-array"] -notes = """ -This is used to expose `-with-dynamic-vars`, and is a -niche feature. -""" -operand-stack = [["Reference", "Reference..."], []] -operation = "Bind stack values as dynamic variables." - -[syscall.dynscopepush] - -format = "PushDynamicScope" -operation = "Push a dynamic scope frame" - -[syscall.dynscopepop] - -format = "PopDynamicScope" -operation = "Pop a dynamic scope frame" - -[syscall.cmpblock] - -format = "CompileBlock" -operand-stack = [["CompilableBlock"], ["Handle"]] -operation = "Compile the InlineBlock at the top of the stack." - -[syscall.scopeload] - -format = ["PushBlockScope", "scope:scope"] -operand-stack = [[], ["scope"]] -operation = "Push a scope onto the stack." - -[syscall.dsymload] - -format = ["PushSymbolTable", "table:symbol-table"] -operand-stack = [[], ["symbol-table"]] -operation = "Push a symbol table onto the stack." - -[syscall.invokeyield] - -format = "InvokeYield" -operand-stack = [["Reference...", "Arguments", "symbol-table", "handle"], []] -operation = "Yield to a block." - -[syscall.iftrue] - -format = ["JumpIf", "to:u32"] -operand-stack = [["Reference"], []] -operation = """ -Jump to the specified offset if the value at -the top of the stack is true. -""" - -[syscall.iffalse] - -format = ["JumpUnless", "to:u32"] -operand-stack = [["Reference"], []] -operation = """ -Jump to the specified offset if the value at -the top of the stack is false. -""" - -[syscall.ifeq] - -format = ["JumpEq", "to:i32", "comparison:i32"] -operand-stack = [["u32"], ["u32"]] -operation = """ -Jump to the specified offset if the value at -the top of the stack is the same as the -comparison. -""" - -[syscall.assert_eq] - -format = "AssertSame" -notes = "The reference is a u32" -operand-stack = [["Reference"], ["Reference"]] -operation = """ -Validate that the value at the top of the stack -hasn't changed. -""" - -[syscall.blk_start] - -format = ["Enter", "args:u32"] -notes = """ -Soon after this opcode, one of Jump, JumpIf, -JumpUnless, or JumpEq will produce an updating -assertion. If that assertion fails, the appending -VM will be re-entered, and the instructions from `from` -to `to` will be executed. - -TODO: Save and restore. -""" -operation = """ -Start tracking a new output block that could change -if one of its inputs changes. -""" - -[syscall.blk_end] - -format = "Exit" -notes = """ -This finalizes the validators that the updating -block must check to determine whether it's safe to -skip running the contents. -""" -operation = "Finish tracking the current block." - -[syscall.anytobool] - -format = "ToBoolean" -operand-stack = [["Reference"], ["Reference"]] -operation = "Convert the top of the stack into a boolean reference." - -[syscall.list_start] - -format = ["EnterList", "address:u32", "address:u32"] -operand-stack = [["Reference", "Reference"], ["Reference..."]] -operation = "Enter a list." - -[syscall.list_end] - -format = "ExitList" -operation = "Exit the current list." - -[syscall.iter] - -format = ["Iterate", "end:u32"] -notes = """ -In Form 1, the stack will have (in reverse order): - -- the key, as a string -- the current iterated value -- the memoized iterated value -""" -operation = """ -Set up the stack for iterating for a given key, -or jump to `end` if there is nothing left to -iterate. -""" -skip = true - -[syscall.main] - -format = ["Main", "state:register"] -operand-stack = [["Invocation", "ComponentDefinition"], []] -operation = "Test whether a reference contains a component definition." - -[syscall.ctload] - -format = "ContentType" -notes = "The new reference represents a ContentType" -operand-stack = [["Reference"], ["Reference", "Reference"]] -operation = "Push the content type onto the stack." - -[syscall.dctload] - -format = "DynamicContentType" -notes = "The new reference represents a DynamicContentType" -operand-stack = [["Reference"], ["Reference", "Reference"]] -operation = "Push the content type onto the stack." - -[syscall.curry] - -format = ["Curry", "type:u32", "is-strict:bool"] -notes = """ -TODO: CurriedValue is { Reference, Type, CapturedArguments } -""" -operand-stack = [ - [ - "Reference", - "Reference...", - "Arguments", - ], - [ - "CurriedComponent", - ], -] -operation = "Curry a value of type for a later invocation." - -[syscall.cmload] - -format = ["PushComponentDefinition", "spec:handle"] -notes = """ -The handle is a handle for a runtime ComponentDefinition. -""" -operand-stack = [[], ["ComponentDefinition"]] -operation = "Push an appropriate ComponentDefinition onto the stack." - -[syscall.dciload] - -format = "PushDynamicComponentInstance" -operand-stack = [["ComponentDefinition"], ["InitialComponentState"]] -operation = """ -Pushes the ComponentInstance onto the stack that is -used during the invoke. -""" - -[syscall.cdload] - -format = ["ResolveDynamicComponent", "owner:owner"] -operand-stack = [["Reference"], ["ComponentDefinition"]] -operation = "Push a resolved component definition onto the stack" - -[syscall.argsload] - -format = ["PushArgs", "names:str-array", "block-names:str-array", "flags:u32"] -notes = """ -This arguments object is only necessary when calling into -user-specified hooks. It is meant to be implemented as a -transient proxy that reads into the stack as needed. -Holding onto the Arguments after the call has completed is -illegal. -""" -operand-stack = [["Reference..."], ["Reference...", "Arguments"]] -operation = "Push a user representation of args onto the stack." - -[syscall.emptyargsload] - -format = "PushEmptyArgs" -operand-stack = [[], ["Arguments"]] -operation = "Push empty args onto the stack" - -[syscall.argspop] - -format = "PopArgs" -notes = """ -The arguments object contains the information of how many user -supplied args the component was invoked with. To clear them from -the stack we must pop it from the stack and call `clear` on it -to remove the argument values from the stack. -""" -operand-stack = [["Reference...", "Arguments"], []] -operation = "Pops Arguments from the stack and clears the next N args." - -[syscall.argsprep] - -format = ["PrepareArgs", "state:register"] -operation = "..." -skip = true - -[syscall.argscapture] - -format = "CaptureArgs" -notes = """ -The Arguments object is mutated in place because it is usually -consumed immediately after being pushed on to the stack. In -some situations, such as with curried components, information -about more than one Argument may need to exist on the stack at -once. In those cases, the CaptureArgs instruction pops an -Arguments object off the stack and replaces it with the -immutable CapturedArgs snapshot. -""" -operand-stack = [["Arguments"], ["CapturedArguments"]] -operation = "Replaces Arguments on the stack with CapturedArguments" - -[syscall.comp_create] - -format = ["CreateComponent", "flags:u32", "state:register"] -notes = """ -Flags: - -* 0b001: Has a default block -* 0b010: Has an else block -""" -operation = "Create the component and push it onto the stack." - -[syscall.comp_dest] - -format = ["RegisterComponentDestructor", "state:register"] -operation = "Register a destructor for the current component" - -[syscall.comp_elops] - -format = "PutComponentOperations" -operation = "Push a new ElementOperations for the current component." - -[syscall.comp_selfload] - -format = ["GetComponentSelf", "state:register"] -operand-stack = [[], ["Reference"]] -operation = "Push the component's `self` onto the stack." - -[syscall.comp_tagload] - -format = ["GetComponentTagName", "state:register"] -operand-stack = [[], ["option-str"]] -operation = "Push the component's `tagName` onto the stack." - -[syscall.comp_layoutload] - -format = ["GetComponentLayout", "state:register"] -operand-stack = [[], ["ProgramSymbolTable", "handle"]] -operation = "Get the component layout from the manager." - -[syscall.debugger_scope] - -format = ["BindDebuggerScope", "state:register"] -operation = "Populate the debugger lookup if necessary." - -[syscall.debugger_setup] - -format = ["SetupForDebugger", "state:register"] -operation = "Setup for debugger" - -[syscall.comp_layoutput] - -format = ["PopulateLayout", "state:register"] -operand-stack = [["ProgramSymbolTable", "handle"], []] -operation = """ -Populate the state register with the layout currently -on the stack. -""" - -[syscall.comp_invokelayout] - -format = ["InvokeComponentLayout", "state:register"] -operation = "Invoke the layout returned by the manager." - -[syscall.comp_begin] - -format = "BeginComponentTransaction" -operand-stack = [["ComponentManager", "T"], ["ComponentManager", "T"]] -operation = "Begin a new cache group" - -[syscall.comp_commit] - -format = "CommitComponentTransaction" -operation = "Commit the current cache group" - -[syscall.comp_created] - -format = ["DidCreateElement", "state:register"] -operation = "Invoke didCreateElement on the current component manager" - -[syscall.comp_rendered] - -format = ["DidRenderLayout", "state:register"] -operation = "Invoke didRenderLayout on the current component manager" - -[syscall.debugger] - -format = ["Debugger", "symbols:str-array", "debugInfo:array"] -operation = "Activate the debugger" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 938bc20086..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: @@ -698,6 +701,9 @@ importers: '@glimmer/interfaces': specifier: workspace:* version: link:../interfaces + '@glimmer/reference': + specifier: workspace:* + version: link:../reference '@glimmer/util': specifier: workspace:* version: link:../util @@ -730,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: @@ -761,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: @@ -814,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: @@ -855,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: @@ -873,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: @@ -895,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: {} @@ -955,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: @@ -1010,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: @@ -1068,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: @@ -1093,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: @@ -1151,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: @@ -1197,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: @@ -1289,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: @@ -1332,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: @@ -1391,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: @@ -1434,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: @@ -1471,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: @@ -1499,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: @@ -1524,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: {} diff --git a/tsconfig.json b/tsconfig.json index c63cb10460..e832a054a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,12 +8,26 @@ }, "files": [], "references": [ - { "path": "benchmark/benchmarks/krausest/tsconfig.json" }, - { "path": "bin/tsconfig.json" }, - { "path": "packages/@glimmer/tsconfig.json" }, - { "path": "packages/@glimmer/tsconfig.test.json" }, - { "path": "packages/@glimmer-workspace/tsconfig.json" }, - { "path": "server/tsconfig.json" }, - { "path": "tsconfig.cjs.json" } + { + "path": "./benchmark/benchmarks/krausest/tsconfig.json" + }, + { + "path": "./bin/tsconfig.json" + }, + { + "path": "./packages/@glimmer/tsconfig.json" + }, + { + "path": "./packages/@glimmer/tsconfig.test.json" + }, + { + "path": "./packages/@glimmer-workspace/tsconfig.json" + }, + { + "path": "./server/tsconfig.json" + }, + { + "path": "./tsconfig.cjs.json" + } ] } diff --git a/turbo.json b/turbo.json index 824731c289..b7065bc664 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://turbo.build/schema.json", + "$schema": "https://turbo.build/schema.v1.json", "globalDependencies": [ "pnpm-lock.yaml", "patches",