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",