diff --git a/packages/@ember/helper/index.ts b/packages/@ember/helper/index.ts
index 579aa49daf2..77fc4a1113d 100644
--- a/packages/@ember/helper/index.ts
+++ b/packages/@ember/helper/index.ts
@@ -10,6 +10,10 @@ import {
concat as glimmerConcat,
get as glimmerGet,
fn as glimmerFn,
+ gt as glimmerGt,
+ gte as glimmerGte,
+ lt as glimmerLt,
+ lte as glimmerLte,
} from '@glimmer/runtime';
import { element as glimmerElement, uniqueId as glimmerUniqueId } from '@ember/-internals/glimmer';
import { type Opaque } from '@ember/-internals/utility-types';
@@ -470,6 +474,102 @@ export interface GetHelper extends Opaque<'helper:get'> {}
export const fn = glimmerFn as FnHelper;
export interface FnHelper extends Opaque<'helper:fn'> {}
+/**
+ * The `{{gt}}` helper returns `true` if the first argument is greater than
+ * the second argument.
+ *
+ * ```js
+ * import { gt } from '@ember/helper';
+ *
+ *
+ * {{if (gt @score 100) "High score!" "Keep trying"}}
+ *
+ * ```
+ *
+ * In strict-mode (gjs/gts) templates, `gt` is available as a keyword and
+ * does not need to be imported.
+ *
+ * @method gt
+ * @param {number} left
+ * @param {number} right
+ * @return {boolean}
+ * @public
+ */
+export const gt = glimmerGt as unknown as GtHelper;
+export interface GtHelper extends Opaque<'helper:gt'> {}
+
+/**
+ * The `{{gte}}` helper returns `true` if the first argument is greater than
+ * or equal to the second argument.
+ *
+ * ```js
+ * import { gte } from '@ember/helper';
+ *
+ *
+ * {{if (gte @age 18) "Adult" "Minor"}}
+ *
+ * ```
+ *
+ * In strict-mode (gjs/gts) templates, `gte` is available as a keyword and
+ * does not need to be imported.
+ *
+ * @method gte
+ * @param {number} left
+ * @param {number} right
+ * @return {boolean}
+ * @public
+ */
+export const gte = glimmerGte as unknown as GteHelper;
+export interface GteHelper extends Opaque<'helper:gte'> {}
+
+/**
+ * The `{{lt}}` helper returns `true` if the first argument is less than
+ * the second argument.
+ *
+ * ```js
+ * import { lt } from '@ember/helper';
+ *
+ *
+ * {{if (lt @temperature 0) "Freezing" "Above zero"}}
+ *
+ * ```
+ *
+ * In strict-mode (gjs/gts) templates, `lt` is available as a keyword and
+ * does not need to be imported.
+ *
+ * @method lt
+ * @param {number} left
+ * @param {number} right
+ * @return {boolean}
+ * @public
+ */
+export const lt = glimmerLt as unknown as LtHelper;
+export interface LtHelper extends Opaque<'helper:lt'> {}
+
+/**
+ * The `{{lte}}` helper returns `true` if the first argument is less than
+ * or equal to the second argument.
+ *
+ * ```js
+ * import { lte } from '@ember/helper';
+ *
+ *
+ * {{if (lte @count 0) "Empty" "Has items"}}
+ *
+ * ```
+ *
+ * In strict-mode (gjs/gts) templates, `lte` is available as a keyword and
+ * does not need to be imported.
+ *
+ * @method lte
+ * @param {number} left
+ * @param {number} right
+ * @return {boolean}
+ * @public
+ */
+export const lte = glimmerLte as unknown as LteHelper;
+export interface LteHelper extends Opaque<'helper:lte'> {}
+
/**
* The `element` helper lets you dynamically set the tag name of an element.
*
diff --git a/packages/@ember/template-compiler/lib/compile-options.ts b/packages/@ember/template-compiler/lib/compile-options.ts
index c9db7a1777b..64fdc7f2fb6 100644
--- a/packages/@ember/template-compiler/lib/compile-options.ts
+++ b/packages/@ember/template-compiler/lib/compile-options.ts
@@ -1,4 +1,4 @@
-import { fn } from '@ember/helper';
+import { fn, gt, gte, lt, lte } from '@ember/helper';
import { on } from '@ember/modifier';
import { assert } from '@ember/debug';
import {
@@ -26,6 +26,10 @@ export const RUNTIME_KEYWORDS_NAME = '__ember_keywords__';
export const keywords: Record = {
fn,
+ gt,
+ gte,
+ lt,
+ lte,
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 64503a3b5d9..a4e92ff715a 100644
--- a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts
+++ b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts
@@ -30,11 +30,35 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
if (isFn(node, hasLocal)) {
rewriteKeyword(env, node, 'fn', '@ember/helper');
}
+ if (isGt(node, hasLocal)) {
+ rewriteKeyword(env, node, 'gt', '@ember/helper');
+ }
+ if (isGte(node, hasLocal)) {
+ rewriteKeyword(env, node, 'gte', '@ember/helper');
+ }
+ if (isLt(node, hasLocal)) {
+ rewriteKeyword(env, node, 'lt', '@ember/helper');
+ }
+ if (isLte(node, hasLocal)) {
+ rewriteKeyword(env, node, 'lte', '@ember/helper');
+ }
},
MustacheStatement(node: AST.MustacheStatement) {
if (isFn(node, hasLocal)) {
rewriteKeyword(env, node, 'fn', '@ember/helper');
}
+ if (isGt(node, hasLocal)) {
+ rewriteKeyword(env, node, 'gt', '@ember/helper');
+ }
+ if (isGte(node, hasLocal)) {
+ rewriteKeyword(env, node, 'gte', '@ember/helper');
+ }
+ if (isLt(node, hasLocal)) {
+ rewriteKeyword(env, node, 'lt', '@ember/helper');
+ }
+ if (isLte(node, hasLocal)) {
+ rewriteKeyword(env, node, 'lte', '@ember/helper');
+ }
},
},
};
@@ -68,3 +92,31 @@ function isFn(
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
return isPath(node.path) && node.path.original === 'fn' && !hasLocal('fn');
}
+
+function isGt(
+ 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 === 'gt' && !hasLocal('gt');
+}
+
+function isGte(
+ 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 === 'gte' && !hasLocal('gte');
+}
+
+function isLt(
+ 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 === 'lt' && !hasLocal('lt');
+}
+
+function isLte(
+ 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 === 'lte' && !hasLocal('lte');
+}
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts
new file mode 100644
index 00000000000..14ae5e655c0
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-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 KeywordGtRuntime extends RenderTest {
+ static suiteName = 'keyword helper: gt (runtime)';
+
+ @test
+ 'explicit scope'() {
+ const compiled = template('{{if (gt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ a: 3, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'implicit scope (eval)'() {
+ let a = 3;
+ let b = 2;
+ hide(a);
+ hide(b);
+ const compiled = template('{{if (gt 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 {
+ a = 3;
+ b = 2;
+ static {
+ template('{{if (gt this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ }
+ this.renderComponent(Foo);
+ this.assertHTML('yes');
+ }
+}
+
+jitSuite(KeywordGtRuntime);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts
new file mode 100644
index 00000000000..c55bfdd8b9b
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-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 { gt } from '@ember/helper';
+
+class KeywordGt extends RenderTest {
+ static suiteName = 'keyword helper: gt';
+
+ @test
+ 'returns true when first arg is greater'() {
+ let a = 3;
+ let b = 2;
+ const compiled = template('{{if (gt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gt, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'returns false when first arg is equal'() {
+ let a = 2;
+ let b = 2;
+ const compiled = template('{{if (gt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gt, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('no');
+ }
+
+ @test
+ 'returns false when first arg is less'() {
+ let a = 1;
+ let b = 2;
+ const compiled = template('{{if (gt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gt, 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('{{gt a}}', {
+ strictMode: true,
+ scope: () => ({ gt, a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`gt` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordGt);
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts
new file mode 100644
index 00000000000..3b440a23080
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts
@@ -0,0 +1,40 @@
+import {
+ GlimmerishComponent,
+ jitSuite,
+ RenderTest,
+ test,
+} from '@glimmer-workspace/integration-tests';
+
+import { template } from '@ember/template-compiler/runtime';
+
+class KeywordGteRuntime extends RenderTest {
+ static suiteName = 'keyword helper: gte (runtime)';
+
+ @test
+ 'explicit scope'() {
+ const compiled = template('{{if (gte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ a: 2, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'no eval and no scope'() {
+ class Foo extends GlimmerishComponent {
+ a = 2;
+ b = 2;
+ static {
+ template('{{if (gte this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ }
+ this.renderComponent(Foo);
+ this.assertHTML('yes');
+ }
+}
+
+jitSuite(KeywordGteRuntime);
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts
new file mode 100644
index 00000000000..bcaca7d7035
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-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 { gte } from '@ember/helper';
+
+class KeywordGte extends RenderTest {
+ static suiteName = 'keyword helper: gte';
+
+ @test
+ 'returns true when first arg is greater'() {
+ let a = 3;
+ let b = 2;
+ const compiled = template('{{if (gte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gte, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'returns true when first arg is equal'() {
+ let a = 2;
+ let b = 2;
+ const compiled = template('{{if (gte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gte, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'returns false when first arg is less'() {
+ let a = 1;
+ let b = 2;
+ const compiled = template('{{if (gte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gte, 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('{{gte a}}', {
+ strictMode: true,
+ scope: () => ({ gte, a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`gte` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordGte);
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts
new file mode 100644
index 00000000000..bf1a86230e1
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-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 KeywordLtRuntime extends RenderTest {
+ static suiteName = 'keyword helper: lt (runtime)';
+
+ @test
+ 'explicit scope'() {
+ const compiled = template('{{if (lt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ a: 1, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'implicit scope (eval)'() {
+ let a = 1;
+ let b = 2;
+ hide(a);
+ hide(b);
+ const compiled = template('{{if (lt 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 {
+ a = 1;
+ b = 2;
+ static {
+ template('{{if (lt this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ }
+ this.renderComponent(Foo);
+ this.assertHTML('yes');
+ }
+}
+
+jitSuite(KeywordLtRuntime);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts
new file mode 100644
index 00000000000..122ee8a6592
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-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 { lt } from '@ember/helper';
+
+class KeywordLt extends RenderTest {
+ static suiteName = 'keyword helper: lt';
+
+ @test
+ 'returns true when first arg is less'() {
+ let a = 1;
+ let b = 2;
+ const compiled = template('{{if (lt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lt, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'returns false when first arg is equal'() {
+ let a = 2;
+ let b = 2;
+ const compiled = template('{{if (lt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lt, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('no');
+ }
+
+ @test
+ 'returns false when first arg is greater'() {
+ let a = 3;
+ let b = 2;
+ const compiled = template('{{if (lt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lt, 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('{{lt a}}', {
+ strictMode: true,
+ scope: () => ({ lt, a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`lt` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordLt);
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts
new file mode 100644
index 00000000000..a5007816398
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts
@@ -0,0 +1,40 @@
+import {
+ GlimmerishComponent,
+ jitSuite,
+ RenderTest,
+ test,
+} from '@glimmer-workspace/integration-tests';
+
+import { template } from '@ember/template-compiler/runtime';
+
+class KeywordLteRuntime extends RenderTest {
+ static suiteName = 'keyword helper: lte (runtime)';
+
+ @test
+ 'explicit scope'() {
+ const compiled = template('{{if (lte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ a: 2, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'no eval and no scope'() {
+ class Foo extends GlimmerishComponent {
+ a = 2;
+ b = 2;
+ static {
+ template('{{if (lte this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ }
+ this.renderComponent(Foo);
+ this.assertHTML('yes');
+ }
+}
+
+jitSuite(KeywordLteRuntime);
diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts
new file mode 100644
index 00000000000..fd985049ff3
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-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 { lte } from '@ember/helper';
+
+class KeywordLte extends RenderTest {
+ static suiteName = 'keyword helper: lte';
+
+ @test
+ 'returns true when first arg is less'() {
+ let a = 1;
+ let b = 2;
+ const compiled = template('{{if (lte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lte, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'returns true when first arg is equal'() {
+ let a = 2;
+ let b = 2;
+ const compiled = template('{{if (lte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lte, a, b }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @test
+ 'returns false when first arg is greater'() {
+ let a = 3;
+ let b = 2;
+ const compiled = template('{{if (lte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lte, 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('{{lte a}}', {
+ strictMode: true,
+ scope: () => ({ lte, a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`lte` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordLte);
diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts
index 9ec4eb2b603..559a0c96cd6 100644
--- a/packages/@glimmer/runtime/index.ts
+++ b/packages/@glimmer/runtime/index.ts
@@ -35,7 +35,11 @@ export { array } from './lib/helpers/array';
export { concat } from './lib/helpers/concat';
export { fn } from './lib/helpers/fn';
export { get } from './lib/helpers/get';
+export { gt } from './lib/helpers/gt';
+export { gte } from './lib/helpers/gte';
export { hash } from './lib/helpers/hash';
+export { lt } from './lib/helpers/lt';
+export { lte } from './lib/helpers/lte';
export { invokeHelper } from './lib/helpers/invoke';
export { on } from './lib/modifiers/on';
export { renderComponent, renderMain, renderSync } from './lib/render';
diff --git a/packages/@glimmer/runtime/lib/helpers/gt.ts b/packages/@glimmer/runtime/lib/helpers/gt.ts
new file mode 100644
index 00000000000..38101c1b566
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/gt.ts
@@ -0,0 +1,9 @@
+import { DEBUG } from '@glimmer/env';
+
+export const gt = (...args: unknown[]) => {
+ if (DEBUG && args.length !== 2) {
+ throw new Error(`\`gt\` expects exactly two arguments, but received ${args.length}.`);
+ }
+
+ return (args[0] as number) > (args[1] as number);
+};
diff --git a/packages/@glimmer/runtime/lib/helpers/gte.ts b/packages/@glimmer/runtime/lib/helpers/gte.ts
new file mode 100644
index 00000000000..2b69a96e28e
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/gte.ts
@@ -0,0 +1,9 @@
+import { DEBUG } from '@glimmer/env';
+
+export const gte = (...args: unknown[]) => {
+ if (DEBUG && args.length !== 2) {
+ throw new Error(`\`gte\` expects exactly two arguments, but received ${args.length}.`);
+ }
+
+ return (args[0] as number) >= (args[1] as number);
+};
diff --git a/packages/@glimmer/runtime/lib/helpers/lt.ts b/packages/@glimmer/runtime/lib/helpers/lt.ts
new file mode 100644
index 00000000000..949b3c96ad8
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/lt.ts
@@ -0,0 +1,9 @@
+import { DEBUG } from '@glimmer/env';
+
+export const lt = (...args: unknown[]) => {
+ if (DEBUG && args.length !== 2) {
+ throw new Error(`\`lt\` expects exactly two arguments, but received ${args.length}.`);
+ }
+
+ return (args[0] as number) < (args[1] as number);
+};
diff --git a/packages/@glimmer/runtime/lib/helpers/lte.ts b/packages/@glimmer/runtime/lib/helpers/lte.ts
new file mode 100644
index 00000000000..d67a5441318
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/lte.ts
@@ -0,0 +1,9 @@
+import { DEBUG } from '@glimmer/env';
+
+export const lte = (...args: unknown[]) => {
+ if (DEBUG && args.length !== 2) {
+ throw new Error(`\`lte\` expects exactly two arguments, but received ${args.length}.`);
+ }
+
+ return (args[0] as number) <= (args[1] as number);
+};
diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts
index 1a4399e4151..83d7c0f7fdd 100644
--- a/smoke-tests/scenarios/basic-test.ts
+++ b/smoke-tests/scenarios/basic-test.ts
@@ -401,6 +401,38 @@ function basicTest(scenarios: Scenarios, appName: string) {
});
});
`,
+ 'comparison-helpers-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 LtDemo extends Component {
+
+ {{if (lt 1 2) "yes" "no"}}
+ {{if (lte 2 2) "yes" "no"}}
+ {{if (gt 3 2) "yes" "no"}}
+ {{if (gte 2 2) "yes" "no"}}
+ {{if (lt 3 2) "yes" "no"}}
+ {{if (gt 1 2) "yes" "no"}}
+
+ }
+
+ module('comparison helpers as keywords', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('lt, lte, gt, gte work without imports', async function(assert) {
+ await render(LtDemo);
+ assert.dom('[data-test="lt"]').hasText('yes');
+ assert.dom('[data-test="lte-equal"]').hasText('yes');
+ assert.dom('[data-test="gt"]').hasText('yes');
+ assert.dom('[data-test="gte-equal"]').hasText('yes');
+ assert.dom('[data-test="lt-false"]').hasText('no');
+ assert.dom('[data-test="gt-false"]').hasText('no');
+ });
+ });
+ `,
'fn-as-keyword-but-its-shadowed-test.gjs': `
import QUnit, { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';