diff --git a/packages/@ember/helper/index.ts b/packages/@ember/helper/index.ts
index bdf745a9974..06f93955567 100644
--- a/packages/@ember/helper/index.ts
+++ b/packages/@ember/helper/index.ts
@@ -12,6 +12,10 @@ import {
get as glimmerGet,
fn as glimmerFn,
neq as glimmerNeq,
+ 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';
@@ -472,6 +476,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 10fb8e0bf1a..03a5f3e78a6 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, eq, fn, hash, neq } from '@ember/helper';
+import { array, eq, fn, hash, neq, lt, lte, gt, gte } from '@ember/helper';
import { on } from '@ember/modifier';
import { assert } from '@ember/debug';
import {
@@ -30,6 +30,10 @@ export const keywords: Record = {
fn,
hash,
neq,
+ 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 bf3ee5b993d..3ae501b7e6c 100644
--- a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts
+++ b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts
@@ -42,6 +42,18 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
if (isNeq(node, hasLocal)) {
rewriteKeyword(env, node, 'neq', '@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 (isArray(node, hasLocal)) {
@@ -59,6 +71,18 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
if (isNeq(node, hasLocal)) {
rewriteKeyword(env, node, 'neq', '@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');
+ }
},
},
};
@@ -120,3 +144,31 @@ function isNeq(
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
return isPath(node.path) && node.path.original === 'neq' && !hasLocal('neq');
}
+
+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..a4503e4979b
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts
@@ -0,0 +1,70 @@
+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
+ 'explicit scope (shadowed)'() {
+ const compiled = template('{{if (gt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gt: () => false, a: 3, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('no');
+ }
+
+ @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 {
+ static {
+ template('{{if (gt this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ a = 3;
+ b = 2;
+ }
+ 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..530ba7b1c2d
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts
@@ -0,0 +1,110 @@
+import { DEBUG } from '@glimmer/env';
+import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
+
+import { template } from '@ember/template-compiler';
+
+class KeywordGt extends RenderTest {
+ static suiteName = 'keyword helper: gt';
+
+ @test
+ 'explicit scope'() {
+ let a = 3;
+ let b = 2;
+
+ const compiled = template('{{gt a b}}', {
+ strictMode: true,
+ scope: () => ({ a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('true');
+ }
+
+ @test
+ 'explicit scope (shadowed)'() {
+ let a = 3;
+ let b = 2;
+ let gt = () => 'surprise';
+ const compiled = template('{{gt a b}}', {
+ strictMode: true,
+ scope: () => ({ gt, a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('surprise');
+ }
+
+ @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
+ '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: () => ({ 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: () => ({ 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: () => ({ 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: () => ({ a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`gt` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordGt);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
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..9edbba57685
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts
@@ -0,0 +1,70 @@
+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
+ 'explicit scope (shadowed)'() {
+ const compiled = template('{{if (gte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ gte: () => false, a: 2, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('no');
+ }
+
+ @test
+ 'implicit scope (eval)'() {
+ let a = 2;
+ let b = 2;
+ hide(a);
+ hide(b);
+ const compiled = template('{{if (gte 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 (gte this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ a = 2;
+ b = 2;
+ }
+ this.renderComponent(Foo);
+ this.assertHTML('yes');
+ }
+}
+
+jitSuite(KeywordGteRuntime);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
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..b13d97b0524
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts
@@ -0,0 +1,110 @@
+import { DEBUG } from '@glimmer/env';
+import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
+
+import { template } from '@ember/template-compiler';
+
+class KeywordGte extends RenderTest {
+ static suiteName = 'keyword helper: gte';
+
+ @test
+ 'explicit scope'() {
+ let a = 2;
+ let b = 2;
+
+ const compiled = template('{{gte a b}}', {
+ strictMode: true,
+ scope: () => ({ a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('true');
+ }
+
+ @test
+ 'explicit scope (shadowed)'() {
+ let a = 3;
+ let b = 2;
+ let gte = () => 'surprise';
+ const compiled = template('{{gte a b}}', {
+ strictMode: true,
+ scope: () => ({ gte, a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('surprise');
+ }
+
+ @test
+ 'implicit scope (eval)'() {
+ let a = 2;
+ let b = 2;
+
+ hide(a);
+ hide(b);
+
+ const compiled = template('{{if (gte a b) "yes" "no"}}', {
+ strictMode: true,
+ eval() {
+ return eval(arguments[0]);
+ },
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @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: () => ({ 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: () => ({ 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: () => ({ 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: () => ({ a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`gte` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordGte);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
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..5b0913c5e1c
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts
@@ -0,0 +1,70 @@
+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
+ 'explicit scope (shadowed)'() {
+ const compiled = template('{{if (lt a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lt: () => false, a: 1, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('no');
+ }
+
+ @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 {
+ static {
+ template('{{if (lt this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ a = 1;
+ b = 2;
+ }
+ 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..49e14c56170
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts
@@ -0,0 +1,110 @@
+import { DEBUG } from '@glimmer/env';
+import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
+
+import { template } from '@ember/template-compiler';
+
+class KeywordLt extends RenderTest {
+ static suiteName = 'keyword helper: lt';
+
+ @test
+ 'explicit scope'() {
+ let a = 1;
+ let b = 2;
+
+ const compiled = template('{{lt a b}}', {
+ strictMode: true,
+ scope: () => ({ a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('true');
+ }
+
+ @test
+ 'explicit scope (shadowed)'() {
+ let a = 1;
+ let b = 2;
+ let lt = () => 'surprise';
+ const compiled = template('{{lt a b}}', {
+ strictMode: true,
+ scope: () => ({ lt, 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 (lt a b) "yes" "no"}}', {
+ strictMode: true,
+ eval() {
+ return eval(arguments[0]);
+ },
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @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: () => ({ 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: () => ({ 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: () => ({ 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: () => ({ a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`lt` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordLt);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
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..98145ec893f
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts
@@ -0,0 +1,70 @@
+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
+ 'explicit scope (shadowed)'() {
+ const compiled = template('{{if (lte a b) "yes" "no"}}', {
+ strictMode: true,
+ scope: () => ({ lte: () => false, a: 2, b: 2 }),
+ });
+ this.renderComponent(compiled);
+ this.assertHTML('no');
+ }
+
+ @test
+ 'implicit scope (eval)'() {
+ let a = 2;
+ let b = 2;
+ hide(a);
+ hide(b);
+ const compiled = template('{{if (lte 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 (lte this.a this.b) "yes" "no"}}', {
+ strictMode: true,
+ component: this,
+ });
+ }
+ a = 2;
+ b = 2;
+ }
+ this.renderComponent(Foo);
+ this.assertHTML('yes');
+ }
+}
+
+jitSuite(KeywordLteRuntime);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
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..7cd01eeaf4b
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts
@@ -0,0 +1,110 @@
+import { DEBUG } from '@glimmer/env';
+import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
+
+import { template } from '@ember/template-compiler';
+
+class KeywordLte extends RenderTest {
+ static suiteName = 'keyword helper: lte';
+
+ @test
+ 'explicit scope'() {
+ let a = 2;
+ let b = 2;
+
+ const compiled = template('{{lte a b}}', {
+ strictMode: true,
+ scope: () => ({ a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('true');
+ }
+
+ @test
+ 'explicit scope (shadowed)'() {
+ let a = 1;
+ let b = 2;
+ let lte = () => 'surprise';
+ const compiled = template('{{lte a b}}', {
+ strictMode: true,
+ scope: () => ({ lte, a, b }),
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('surprise');
+ }
+
+ @test
+ 'implicit scope (eval)'() {
+ let a = 2;
+ let b = 2;
+
+ hide(a);
+ hide(b);
+
+ const compiled = template('{{if (lte a b) "yes" "no"}}', {
+ strictMode: true,
+ eval() {
+ return eval(arguments[0]);
+ },
+ });
+
+ this.renderComponent(compiled);
+ this.assertHTML('yes');
+ }
+
+ @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: () => ({ 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: () => ({ 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: () => ({ 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: () => ({ a }),
+ });
+
+ assert.throws(() => {
+ this.renderComponent(compiled);
+ }, /`lte` expects exactly two arguments/);
+ }
+}
+
+jitSuite(KeywordLte);
+
+const hide = (variable: unknown) => {
+ new Function(`return (${JSON.stringify(variable)});`);
+};
diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts
index 617edd8ae30..2012ced0f31 100644
--- a/packages/@glimmer/runtime/index.ts
+++ b/packages/@glimmer/runtime/index.ts
@@ -36,8 +36,12 @@ 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 { gt } from './lib/helpers/gt';
+export { gte } from './lib/helpers/gte';
export { hash } from './lib/helpers/hash';
export { invokeHelper } from './lib/helpers/invoke';
+export { lt } from './lib/helpers/lt';
+export { lte } from './lib/helpers/lte';
export { neq } from './lib/helpers/neq';
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..cca687dceb6
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/gt.ts
@@ -0,0 +1,14 @@
+import { DEBUG } from '@glimmer/env';
+
+/**
+ * Performs a greater than comparison.
+ *
+ * left > right
+ */
+export function gt(left: unknown, right: unknown) {
+ if (DEBUG && arguments.length !== 2) {
+ throw new Error(`\`gt\` expects exactly two arguments, but received ${arguments.length}.`);
+ }
+
+ return (left as number) > (right 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..78621a3383d
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/gte.ts
@@ -0,0 +1,14 @@
+import { DEBUG } from '@glimmer/env';
+
+/**
+ * Performs a greater than or equal comparison.
+ *
+ * left >= right
+ */
+export function gte(left: unknown, right: unknown) {
+ if (DEBUG && arguments.length !== 2) {
+ throw new Error(`\`gte\` expects exactly two arguments, but received ${arguments.length}.`);
+ }
+
+ return (left as number) >= (right 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..95e7e39035b
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/lt.ts
@@ -0,0 +1,14 @@
+import { DEBUG } from '@glimmer/env';
+
+/**
+ * Performs a less than comparison.
+ *
+ * left < right
+ */
+export function lt(left: unknown, right: unknown) {
+ if (DEBUG && arguments.length !== 2) {
+ throw new Error(`\`lt\` expects exactly two arguments, but received ${arguments.length}.`);
+ }
+
+ return (left as number) < (right 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..5393cf76139
--- /dev/null
+++ b/packages/@glimmer/runtime/lib/helpers/lte.ts
@@ -0,0 +1,14 @@
+import { DEBUG } from '@glimmer/env';
+
+/**
+ * Performs a less than or equal comparison.
+ *
+ * left <= right
+ */
+export function lte(left: unknown, right: unknown) {
+ if (DEBUG && arguments.length !== 2) {
+ throw new Error(`\`lte\` expects exactly two arguments, but received ${arguments.length}.`);
+ }
+
+ return (left as number) <= (right as number);
+}
diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts
index e1c1afc3cc3..587b3bbcaf3 100644
--- a/smoke-tests/scenarios/basic-test.ts
+++ b/smoke-tests/scenarios/basic-test.ts
@@ -442,6 +442,57 @@ function basicTest(scenarios: Scenarios, appName: string) {
});
});
`,
+ 'lt-lte-gt-gte-as-keyword-test.gjs': `
+ import { module, test } from 'qunit';
+ import { setupRenderingTest } from 'ember-qunit';
+ import { render } from '@ember/test-helpers';
+
+ module('{{lt}} / {{lte}} / {{gt}} / {{gte}} as keywords', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it works', async function(assert) {
+ let a = 1;
+ let b = 2;
+
+ await render(
+
+ {{lt a b}}
+ {{lte a a}}
+ {{gt b a}}
+ {{gte a a}}
+
+ );
+
+ assert.dom('[data-lt]').hasText('true');
+ assert.dom('[data-lte]').hasText('true');
+ assert.dom('[data-gt]').hasText('true');
+ assert.dom('[data-gte]').hasText('true');
+ });
+
+ test('can be shadowed', async function (assert) {
+ let a = 1;
+ let b = 2;
+ let lt = () => 'surprise:lt';
+ let lte = () => 'surprise:lte';
+ let gt = () => 'surprise:gt';
+ let gte = () => 'surprise:gte';
+
+ await render(
+
+ {{lt a b}}
+ {{lte a b}}
+ {{gt a b}}
+ {{gte a b}}
+
+ );
+
+ assert.dom('[data-lt]').hasText('surprise:lt');
+ assert.dom('[data-lte]').hasText('surprise:lte');
+ assert.dom('[data-gt]').hasText('surprise:gt');
+ assert.dom('[data-gte]').hasText('surprise:gte');
+ });
+ });
+ `,
'fn-as-keyword-but-its-shadowed-test.gjs': `
import QUnit, { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';