From 95cb7858b06d6b990a7608a0b64ce4f3b4e5c5db Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:47:08 -0400 Subject: [PATCH 1/5] RFC#560 - {{eq}}, {{neq}} as keywords Add equality helpers and register them as built-in keywords so they no longer need to be imported in strict-mode (gjs/gts) templates. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/@ember/helper/index.ts | 50 ++++++++++++++++ .../template-compiler/lib/compile-options.ts | 4 +- .../lib/plugins/auto-import-builtins.ts | 26 ++++++++ .../test/keywords/eq-runtime-test.ts | 60 +++++++++++++++++++ .../test/keywords/eq-test.ts | 60 +++++++++++++++++++ .../test/keywords/neq-runtime-test.ts | 60 +++++++++++++++++++ .../test/keywords/neq-test.ts | 48 +++++++++++++++ packages/@glimmer/runtime/index.ts | 2 + packages/@glimmer/runtime/lib/helpers/eq.ts | 9 +++ packages/@glimmer/runtime/lib/helpers/neq.ts | 9 +++ smoke-tests/scenarios/basic-test.ts | 30 ++++++++++ tests/docs/expected.js | 2 + 12 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts create mode 100644 packages/@glimmer/runtime/lib/helpers/eq.ts create mode 100644 packages/@glimmer/runtime/lib/helpers/neq.ts diff --git a/packages/@ember/helper/index.ts b/packages/@ember/helper/index.ts index 579aa49daf2..bdf745a9974 100644 --- a/packages/@ember/helper/index.ts +++ b/packages/@ember/helper/index.ts @@ -8,8 +8,10 @@ import { hash as glimmerHash, array as glimmerArray, concat as glimmerConcat, + eq as glimmerEq, get as glimmerGet, fn as glimmerFn, + neq as glimmerNeq, } from '@glimmer/runtime'; import { element as glimmerElement, uniqueId as glimmerUniqueId } from '@ember/-internals/glimmer'; import { type Opaque } from '@ember/-internals/utility-types'; @@ -511,4 +513,52 @@ export interface ElementHelper extends Opaque<'helper:element'> {} export const uniqueId = glimmerUniqueId; export type UniqueIdHelper = typeof uniqueId; +/** + * The `{{eq}}` helper returns `true` if its two arguments are strictly equal + * (`===`). Takes exactly two arguments. + * + * ```js + * import { eq } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `eq` is available as a keyword and + * does not need to be imported. + * + * @method eq + * @param {unknown} left + * @param {unknown} right + * @return {boolean} + * @public + */ +export const eq = glimmerEq as unknown as EqHelper; +export interface EqHelper extends Opaque<'helper:eq'> {} + +/** + * The `{{neq}}` helper returns `true` if its two arguments are strictly + * not equal (`!==`). Takes exactly two arguments. + * + * ```js + * import { neq } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `neq` is available as a keyword and + * does not need to be imported. + * + * @method neq + * @param {unknown} left + * @param {unknown} right + * @return {boolean} + * @public + */ +export const neq = glimmerNeq as unknown as NeqHelper; +export interface NeqHelper extends Opaque<'helper:neq'> {} + /* eslint-enable @typescript-eslint/no-empty-object-type */ diff --git a/packages/@ember/template-compiler/lib/compile-options.ts b/packages/@ember/template-compiler/lib/compile-options.ts index 097786e9353..10fb8e0bf1a 100644 --- a/packages/@ember/template-compiler/lib/compile-options.ts +++ b/packages/@ember/template-compiler/lib/compile-options.ts @@ -1,4 +1,4 @@ -import { array, fn, hash } from '@ember/helper'; +import { array, eq, fn, hash, neq } from '@ember/helper'; import { on } from '@ember/modifier'; import { assert } from '@ember/debug'; import { @@ -26,8 +26,10 @@ export const RUNTIME_KEYWORDS_NAME = '__ember_keywords__'; export const keywords: Record = { array, + eq, fn, hash, + neq, on, }; diff --git a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts index e7c9a6bd1bc..bf3ee5b993d 100644 --- a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts +++ b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts @@ -36,6 +36,12 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP if (isHash(node, hasLocal)) { rewriteKeyword(env, node, 'hash', '@ember/helper'); } + if (isEq(node, hasLocal)) { + rewriteKeyword(env, node, 'eq', '@ember/helper'); + } + if (isNeq(node, hasLocal)) { + rewriteKeyword(env, node, 'neq', '@ember/helper'); + } }, MustacheStatement(node: AST.MustacheStatement) { if (isArray(node, hasLocal)) { @@ -47,6 +53,12 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP if (isHash(node, hasLocal)) { rewriteKeyword(env, node, 'hash', '@ember/helper'); } + if (isEq(node, hasLocal)) { + rewriteKeyword(env, node, 'eq', '@ember/helper'); + } + if (isNeq(node, hasLocal)) { + rewriteKeyword(env, node, 'neq', '@ember/helper'); + } }, }, }; @@ -94,3 +106,17 @@ function isHash( ): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { return isPath(node.path) && node.path.original === 'hash' && !hasLocal('hash'); } + +function isEq( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'eq' && !hasLocal('eq'); +} + +function isNeq( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'neq' && !hasLocal('neq'); +} diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts new file mode 100644 index 00000000000..35f00873486 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts @@ -0,0 +1,60 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordEqRuntime extends RenderTest { + static suiteName = 'keyword helper: eq (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 1, b: 1 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'implicit scope'() { + let a = 1; + let b = 1; + hide(a); + hide(b); + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + static { + template('{{if (eq this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + a = 1; + b = 1; + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordEqRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts new file mode 100644 index 00000000000..b2cbbf43961 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts @@ -0,0 +1,60 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; +import { eq } from '@ember/helper'; + +class KeywordEq extends RenderTest { + static suiteName = 'keyword helper: eq'; + + @test + 'returns true for equal numbers'() { + let a = 1; + let b = 1; + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ eq, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false for unequal numbers'() { + let a = 1; + let b = 2; + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ eq, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'returns true for equal strings'() { + let a = 'hello'; + let b = 'hello'; + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ eq, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{eq a}}', { + strictMode: true, + scope: () => ({ eq, a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`eq` expects exactly two arguments/); + } +} + +jitSuite(KeywordEq); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts new file mode 100644 index 00000000000..547b7517339 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts @@ -0,0 +1,60 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordNeqRuntime extends RenderTest { + static suiteName = 'keyword helper: neq (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (neq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 1, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'implicit scope'() { + let a = 1; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (neq a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + static { + template('{{if (neq this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + a = 1; + b = 2; + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordNeqRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts new file mode 100644 index 00000000000..15807c35817 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts @@ -0,0 +1,48 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; +import { neq } from '@ember/helper'; + +class KeywordNeq extends RenderTest { + static suiteName = 'keyword helper: neq'; + + @test + 'returns true for unequal numbers'() { + let a = 1; + let b = 2; + const compiled = template('{{if (neq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ neq, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false for equal numbers'() { + let a = 1; + let b = 1; + const compiled = template('{{if (neq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ neq, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{neq a}}', { + strictMode: true, + scope: () => ({ neq, a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`neq` expects exactly two arguments/); + } +} + +jitSuite(KeywordNeq); diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts index 9ec4eb2b603..617edd8ae30 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -33,10 +33,12 @@ export { } from './lib/environment'; export { array } from './lib/helpers/array'; export { concat } from './lib/helpers/concat'; +export { eq } from './lib/helpers/eq'; export { fn } from './lib/helpers/fn'; export { get } from './lib/helpers/get'; export { hash } from './lib/helpers/hash'; export { invokeHelper } from './lib/helpers/invoke'; +export { neq } from './lib/helpers/neq'; export { on } from './lib/modifiers/on'; export { renderComponent, renderMain, renderSync } from './lib/render'; export { DynamicScopeImpl, ScopeImpl } from './lib/scope'; diff --git a/packages/@glimmer/runtime/lib/helpers/eq.ts b/packages/@glimmer/runtime/lib/helpers/eq.ts new file mode 100644 index 00000000000..2af3b308b3b --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/eq.ts @@ -0,0 +1,9 @@ +import { DEBUG } from '@glimmer/env'; + +export const eq = (...args: unknown[]) => { + if (DEBUG && args.length !== 2) { + throw new Error(`\`eq\` expects exactly two arguments, but received ${args.length}.`); + } + + return args[0] === args[1]; +}; diff --git a/packages/@glimmer/runtime/lib/helpers/neq.ts b/packages/@glimmer/runtime/lib/helpers/neq.ts new file mode 100644 index 00000000000..8527b7359c4 --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/neq.ts @@ -0,0 +1,9 @@ +import { DEBUG } from '@glimmer/env'; + +export const neq = (...args: unknown[]) => { + if (DEBUG && args.length !== 2) { + throw new Error(`\`neq\` expects exactly two arguments, but received ${args.length}.`); + } + + return args[0] !== args[1]; +}; diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index 6e5d6424bf4..3eaa190118b 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -373,6 +373,36 @@ function basicTest(scenarios: Scenarios, appName: string) { }); }); `, + 'eq-neq-as-keyword-test.gjs': ` + import { module, test } from 'qunit'; + import { setupRenderingTest } from 'ember-qunit'; + import { render } from '@ember/test-helpers'; + + import Component from '@glimmer/component'; + + class EqDemo extends Component { + + } + + module('{{eq}} / {{neq}} as keywords', function(hooks) { + setupRenderingTest(hooks); + + test('eq returns true for equal values', async function(assert) { + await render(); + assert.dom('[data-test="eq"]').hasText('yes'); + assert.dom('[data-test="neq"]').hasText('no'); + }); + + test('eq returns false for unequal values', async function(assert) { + await render(); + assert.dom('[data-test="eq"]').hasText('no'); + assert.dom('[data-test="neq"]').hasText('yes'); + }); + }); + `, 'fn-as-keyword-test.gjs': ` import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; diff --git a/tests/docs/expected.js b/tests/docs/expected.js index 512b71666a1..275e5cbd6d3 100644 --- a/tests/docs/expected.js +++ b/tests/docs/expected.js @@ -180,6 +180,7 @@ module.exports = { 'engine', 'ensureInitializers', 'enter', + 'eq', 'equal', 'error', 'eventDispatcher', @@ -335,6 +336,7 @@ module.exports = { 'name', 'nearestOfType', 'nearestWithProperty', + 'neq', 'next', 'none', 'normalize', From d5a1212a582b72282da4e5624170422d8444a01d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:40:32 -0400 Subject: [PATCH 2/5] Improve tests --- .../test/keywords/eq-runtime-test.ts | 12 +++- .../test/keywords/eq-test.ts | 60 +++++++++++++++++-- .../test/keywords/neq-runtime-test.ts | 12 +++- .../test/keywords/neq-test.ts | 58 ++++++++++++++++-- 4 files changed, 131 insertions(+), 11 deletions(-) diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts index 35f00873486..18af7e094d4 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-runtime-test.ts @@ -21,7 +21,17 @@ class KeywordEqRuntime extends RenderTest { } @test - 'implicit scope'() { + 'explicit scope (shadowed)'() { + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ eq: () => false, a: 1, b: 1 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { let a = 1; let b = 1; hide(a); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts index b2cbbf43961..05fe07bddff 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/eq-test.ts @@ -2,18 +2,64 @@ import { DEBUG } from '@glimmer/env'; import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; import { template } from '@ember/template-compiler'; -import { eq } from '@ember/helper'; class KeywordEq extends RenderTest { static suiteName = 'keyword helper: eq'; + @test + 'explicit scope'() { + let a = 1; + let b = 1; + + const compiled = template('{{eq a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 1; + let b = 2; + let eq = () => 'surprise'; + const compiled = template('{{eq a b}}', { + strictMode: true, + scope: () => ({ eq, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 1; + let b = 1; + + hide(a); + hide(b); + + const compiled = template('{{if (eq a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'returns true for equal numbers'() { let a = 1; let b = 1; const compiled = template('{{if (eq a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ eq, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -25,7 +71,7 @@ class KeywordEq extends RenderTest { let b = 2; const compiled = template('{{if (eq a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ eq, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -37,7 +83,7 @@ class KeywordEq extends RenderTest { let b = 'hello'; const compiled = template('{{if (eq a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ eq, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -48,7 +94,7 @@ class KeywordEq extends RenderTest { let a = 1; const compiled = template('{{eq a}}', { strictMode: true, - scope: () => ({ eq, a }), + scope: () => ({ a }), }); assert.throws(() => { @@ -58,3 +104,7 @@ class KeywordEq extends RenderTest { } jitSuite(KeywordEq); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts index 547b7517339..f217f1702f8 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-runtime-test.ts @@ -21,7 +21,17 @@ class KeywordNeqRuntime extends RenderTest { } @test - 'implicit scope'() { + 'explicit scope (shadowed)'() { + const compiled = template('{{if (neq a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ neq: () => false, a: 1, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { let a = 1; let b = 2; hide(a); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts index 15807c35817..478e1f229f8 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/neq-test.ts @@ -2,18 +2,64 @@ import { DEBUG } from '@glimmer/env'; import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; import { template } from '@ember/template-compiler'; -import { neq } from '@ember/helper'; class KeywordNeq extends RenderTest { static suiteName = 'keyword helper: neq'; + @test + 'explicit scope'() { + let a = 1; + let b = 2; + + const compiled = template('{{neq a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 1; + let b = 1; + let neq = () => 'surprise'; + const compiled = template('{{neq a b}}', { + strictMode: true, + scope: () => ({ neq, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 1; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (neq a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'returns true for unequal numbers'() { let a = 1; let b = 2; const compiled = template('{{if (neq a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ neq, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -25,7 +71,7 @@ class KeywordNeq extends RenderTest { let b = 1; const compiled = template('{{if (neq a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ neq, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -36,7 +82,7 @@ class KeywordNeq extends RenderTest { let a = 1; const compiled = template('{{neq a}}', { strictMode: true, - scope: () => ({ neq, a }), + scope: () => ({ a }), }); assert.throws(() => { @@ -46,3 +92,7 @@ class KeywordNeq extends RenderTest { } jitSuite(KeywordNeq); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; From 0f4102a13d0da17e06aa80739dc62388226f7e25 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:46:12 -0400 Subject: [PATCH 3/5] Better scenario test --- smoke-tests/scenarios/basic-test.ts | 41 ++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index 3eaa190118b..6bd4ebeaadc 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -378,28 +378,39 @@ function basicTest(scenarios: Scenarios, appName: string) { import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; - import Component from '@glimmer/component'; - - class EqDemo extends Component { - - } - module('{{eq}} / {{neq}} as keywords', function(hooks) { setupRenderingTest(hooks); - test('eq returns true for equal values', async function(assert) { - await render(); + test('it works', async function(assert) { + let a = 1; + let b = 1; + + await render( + + ); + assert.dom('[data-test="eq"]').hasText('yes'); assert.dom('[data-test="neq"]').hasText('no'); }); - test('eq returns false for unequal values', async function(assert) { - await render(); - assert.dom('[data-test="eq"]').hasText('no'); - assert.dom('[data-test="neq"]').hasText('yes'); + test('can be shadowed', async function (assert) { + let a = 1; + let b = 1; + let eq = () => 'surprise:eq'; + let neq = () => 'surprise:neq'; + + await render( + + ); + + assert.dom('[data-test="eq"]').hasText('surprise:eq'); + assert.dom('[data-test="neq"]').hasText('surprise:neq'); }); }); `, From bfb9fb44f37b801478b62aeae461d34a34a152ef Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:57:46 -0400 Subject: [PATCH 4/5] Fix scenario test --- smoke-tests/scenarios/basic-test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index 6bd4ebeaadc..e1c1afc3cc3 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -387,13 +387,13 @@ function basicTest(scenarios: Scenarios, appName: string) { await render( ); - assert.dom('[data-test="eq"]').hasText('yes'); - assert.dom('[data-test="neq"]').hasText('no'); + assert.dom('[data-eq]').hasText('true'); + assert.dom('[data-neq]').hasText('false'); }); test('can be shadowed', async function (assert) { @@ -404,13 +404,13 @@ function basicTest(scenarios: Scenarios, appName: string) { await render( ); - assert.dom('[data-test="eq"]').hasText('surprise:eq'); - assert.dom('[data-test="neq"]').hasText('surprise:neq'); + assert.dom('[data-eq]').hasText('surprise:eq'); + assert.dom('[data-neq]').hasText('surprise:neq'); }); }); `, From 761b8872c39ecf7bcc0c288bf9e008a18a4f7e6c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:57:52 -0400 Subject: [PATCH 5/5] Use moree explicit definittion --- packages/@glimmer/runtime/lib/helpers/eq.ts | 15 ++++++++++----- packages/@glimmer/runtime/lib/helpers/neq.ts | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/@glimmer/runtime/lib/helpers/eq.ts b/packages/@glimmer/runtime/lib/helpers/eq.ts index 2af3b308b3b..d50e8760ba6 100644 --- a/packages/@glimmer/runtime/lib/helpers/eq.ts +++ b/packages/@glimmer/runtime/lib/helpers/eq.ts @@ -1,9 +1,14 @@ import { DEBUG } from '@glimmer/env'; -export const eq = (...args: unknown[]) => { - if (DEBUG && args.length !== 2) { - throw new Error(`\`eq\` expects exactly two arguments, but received ${args.length}.`); +/** + * Performs a strict equality comparison. + * + * left === right + */ +export function eq(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`eq\` expects exactly two arguments, but received ${arguments.length}.`); } - return args[0] === args[1]; -}; + return left === right; +} diff --git a/packages/@glimmer/runtime/lib/helpers/neq.ts b/packages/@glimmer/runtime/lib/helpers/neq.ts index 8527b7359c4..8605b835787 100644 --- a/packages/@glimmer/runtime/lib/helpers/neq.ts +++ b/packages/@glimmer/runtime/lib/helpers/neq.ts @@ -1,9 +1,14 @@ import { DEBUG } from '@glimmer/env'; -export const neq = (...args: unknown[]) => { - if (DEBUG && args.length !== 2) { - throw new Error(`\`neq\` expects exactly two arguments, but received ${args.length}.`); +/** + * Performs a strict inequality comparison. + * + * left !== right + */ +export function neq(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`neq\` expects exactly two arguments, but received ${arguments.length}.`); } - return args[0] !== args[1]; -}; + return left !== right; +}