diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/call.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/call.ts index 5d6463c56..9454846f0 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/call.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/call.ts @@ -1,6 +1,7 @@ import { CurriedType } from '@glimmer/interfaces'; import { keywords } from './impl'; +import { comparisonKeyword } from './utils/comparison'; import { curryKeyword } from './utils/curry'; import { getDynamicVarKeyword } from './utils/dynamic-vars'; import { equalKeyword, notEqualKeyword } from './utils/equality'; @@ -15,6 +16,10 @@ export const CALL_KEYWORDS = keywords('Call') .kw('log', logKeyword) .kw('eq', equalKeyword, { strictOnly: true }) .kw('neq', notEqualKeyword, { strictOnly: true }) + .kw('lt', comparisonKeyword('lt'), { strictOnly: true }) + .kw('lte', comparisonKeyword('lte'), { strictOnly: true }) + .kw('gt', comparisonKeyword('gt'), { strictOnly: true }) + .kw('gte', comparisonKeyword('gte'), { strictOnly: true }) .kw('if', ifUnlessInlineKeyword('if')) .kw('unless', ifUnlessInlineKeyword('unless')) .kw('component', curryKeyword(CurriedType.Component)) diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/utils/comparison.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/utils/comparison.ts new file mode 100644 index 000000000..a440f2e15 --- /dev/null +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/utils/comparison.ts @@ -0,0 +1,85 @@ +import { ASTv2, generateSyntaxError } from '@glimmer/syntax'; + +import { Err, Ok, Result } from '../../../../shared/result'; +import * as mir from '../../../2-encoding/mir'; +import { NormalizationState } from '../../context'; +import { VISIT_EXPRS } from '../../visitors/expressions'; +import { GenericKeywordNode, KeywordDelegate } from '../impl'; + +function assertComparisonKeyword(type: string) { + return ( + node: GenericKeywordNode + ): Result<{ lOperand: ASTv2.ExpressionNode; rOperand: ASTv2.ExpressionNode }> => { + let { + args: { named, positional }, + } = node; + + if (named && !named.isEmpty()) { + return Err(generateSyntaxError(`(${type}) does not take any named arguments`, node.loc)); + } + + if (positional.size > 2) { + return Err(generateSyntaxError(`(${type}) can receive a maximum of 2 arguments`, node.loc)); + } + + let lOperand = positional?.nth(0); + if (!lOperand) { + return Err( + generateSyntaxError( + `(${type}) must receive 2 arguments - a left and right comparison values. Received no arguments.`, + node.loc + ) + ); + } + + let rOperand = positional?.nth(1); + if (!rOperand) { + return Err( + generateSyntaxError( + `(${type}) must receive 2 arguments - a left and right comparison values. Received 1 argument`, + node.loc + ) + ); + } + + return Ok({ lOperand, rOperand }); + }; +} + +function translateComparisonKeyword(type: string) { + return ( + { node, state }: { node: ASTv2.CallExpression; state: NormalizationState }, + { lOperand, rOperand }: { lOperand: ASTv2.ExpressionNode; rOperand: ASTv2.ExpressionNode } + ): Result => { + let lOperandResult = VISIT_EXPRS.visit(lOperand, state); + let rOperandResult = VISIT_EXPRS.visit(rOperand, state); + return Result.all(lOperandResult, rOperandResult).mapOk(([lOperand, rOperand]) => { + // TODO: consider typeing this. + if (type === 'lt') { + return new mir.Less({ loc: node.loc, lOperand, rOperand }); + } else if (type === 'lte') { + return new mir.LessEqual({ loc: node.loc, lOperand, rOperand }); + } else if (type === 'gt') { + return new mir.Greater({ loc: node.loc, lOperand, rOperand }); + } else { + return new mir.GreaterEqual({ loc: node.loc, lOperand, rOperand }); + } + }); + }; +} + +export function comparisonKeyword( + type: string +): KeywordDelegate< + ASTv2.CallExpression | ASTv2.AppendContent, + { + lOperand: ASTv2.ExpressionNode; + rOperand: ASTv2.ExpressionNode; + }, + mir.Less | mir.LessEqual | mir.Greater | mir.GreaterEqual +> { + return { + assert: assertComparisonKeyword(type), + translate: translateComparisonKeyword(type), + }; +} diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts index 6cbd4a26e..f2ae6c549 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts @@ -47,6 +47,14 @@ export class ExpressionEncoder { return this.Equal(expr); case 'NotEqual': return this.NotEqual(expr); + case 'Less': + return this.Less(expr); + case 'LessEqual': + return this.LessEqual(expr); + case 'Greater': + return this.Greater(expr); + case 'GreaterEqual': + return this.GreaterEqual(expr); } } @@ -183,6 +191,22 @@ export class ExpressionEncoder { NotEqual({ positional }: mir.NotEqual): WireFormat.Expressions.NotEqual { return [SexpOpcodes.NotEqual, this.Positional(positional)]; } + + Less({ lOperand, rOperand }: mir.Less): WireFormat.Expressions.Less { + return [SexpOpcodes.Less, EXPR.expr(lOperand), EXPR.expr(rOperand)]; + } + + LessEqual({ lOperand, rOperand }: mir.LessEqual): WireFormat.Expressions.LessEqual { + return [SexpOpcodes.LessEqual, EXPR.expr(lOperand), EXPR.expr(rOperand)]; + } + + Greater({ lOperand, rOperand }: mir.Greater): WireFormat.Expressions.Greater { + return [SexpOpcodes.Greater, EXPR.expr(lOperand), EXPR.expr(rOperand)]; + } + + GreaterEqual({ lOperand, rOperand }: mir.GreaterEqual): WireFormat.Expressions.GreaterEqual { + return [SexpOpcodes.GreaterEqual, EXPR.expr(lOperand), EXPR.expr(rOperand)]; + } } export const EXPR = new ExpressionEncoder(); diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts index e334fb87e..6082f157c 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts @@ -75,6 +75,26 @@ export class NotEqual extends node('NotEqual').fields<{ positional: Positional; }>() {} +export class Less extends node('Less').fields<{ + lOperand: ExpressionNode; + rOperand: ExpressionNode; +}>() {} + +export class LessEqual extends node('LessEqual').fields<{ + lOperand: ExpressionNode; + rOperand: ExpressionNode; +}>() {} + +export class Greater extends node('Greater').fields<{ + lOperand: ExpressionNode; + rOperand: ExpressionNode; +}>() {} + +export class GreaterEqual extends node('GreaterEqual').fields<{ + lOperand: ExpressionNode; + rOperand: ExpressionNode; +}>() {} + export class InvokeComponent extends node('InvokeComponent').fields<{ definition: ExpressionNode; args: Args; @@ -226,7 +246,11 @@ export type ExpressionNode = | GetDynamicVar | Log | Equal - | NotEqual; + | NotEqual + | Less + | LessEqual + | Greater + | GreaterEqual; export type ElementParameter = StaticAttr | DynamicAttr | Modifier | SplatAttr; diff --git a/packages/@glimmer/compiler/lib/wire-format-debug.ts b/packages/@glimmer/compiler/lib/wire-format-debug.ts index e8d2286ca..1466db44b 100644 --- a/packages/@glimmer/compiler/lib/wire-format-debug.ts +++ b/packages/@glimmer/compiler/lib/wire-format-debug.ts @@ -265,6 +265,18 @@ export default class WireFormatDebugger { case Op.NotEqual: return ['neq', this.formatParams(opcode[1])]; + + case Op.Less: + return ['lt']; + + case Op.LessEqual: + return ['lte']; + + case Op.Greater: + return ['gt']; + + case Op.GreaterEqual: + return ['gte']; } } else { return opcode; diff --git a/packages/@glimmer/integration-tests/test/keywords/comparison-test.ts b/packages/@glimmer/integration-tests/test/keywords/comparison-test.ts new file mode 100644 index 000000000..802951471 --- /dev/null +++ b/packages/@glimmer/integration-tests/test/keywords/comparison-test.ts @@ -0,0 +1,227 @@ +import { RenderTest, test, jitSuite, syntaxErrorFor, defineComponent, trackedObj } from '../..'; + +class LessThanTest extends RenderTest { + static suiteName = '{{lt}} keyword'; + + @test + ['it works']() { + const AComponent = defineComponent({}, '{{lt 1 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('false'); + } + + @test + ['it works falsey']() { + const AComponent = defineComponent({}, '{{lt 1 0}}'); + this.renderComponent(AComponent); + + this.assertHTML('false'); + } + + @test + ['it works truthy']() { + const AComponent = defineComponent({}, '{{lt 1 2}}'); + this.renderComponent(AComponent); + + this.assertHTML('true'); + } + + @test + ['correctly resolves when values update eq']() { + let args = trackedObj({ foo: 123, bar: 456 }); + const AComponent = defineComponent({}, '{{lt @foo @bar}}'); + this.renderComponent(AComponent, args); + + this.assertHTML('true'); + + args.foo = 456; + this.rerender({ foo: 456 }); + + this.assertHTML('false'); + + args.foo = 789; + this.rerender({ foo: 789 }); + + this.assertHTML('false'); + } + + @test + ['it errors']() { + this.assert.throws(() => { + const AComponent = defineComponent({}, '{{lt 1 2 1}}'); + this.renderComponent(AComponent); + }, syntaxErrorFor('(lt) can receive a maximum of 2 arguments', '{{lt 1 2 1}}', 'an unknown module', 1, 0)); + } +} + +class LessThanEqualTest extends RenderTest { + static suiteName = '{{lte}} keyword'; + + @test + ['it works']() { + const AComponent = defineComponent({}, '{{lte 1 2}}'); + this.renderComponent(AComponent); + + this.assertHTML('true'); + } + + @test + ['it works truthy']() { + const AComponent = defineComponent({}, '{{lte 1 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('true'); + } + + @test + ['it works falsey']() { + const AComponent = defineComponent({}, '{{lte 1 0}}'); + this.renderComponent(AComponent); + + this.assertHTML('false'); + } + + @test + ['correctly resolves when values update neq']() { + let args = trackedObj({ foo: 123, bar: 456 }); + const AComponent = defineComponent({}, '{{lte @foo @bar}}'); + this.renderComponent(AComponent, args); + + this.assertHTML('true'); + + args.foo = 456; + this.rerender(); + + this.assertHTML('true'); + + args.foo = 789; + this.rerender(); + + this.assertHTML('false'); + } + + @test + ['it errors']() { + this.assert.throws(() => { + const AComponent = defineComponent({}, '{{lte 1 2 1}}'); + this.renderComponent(AComponent); + }, syntaxErrorFor('(lte) can receive a maximum of 2 arguments', '{{lte 1 2 1}}', 'an unknown module', 1, 0)); + } +} + +jitSuite(LessThanTest); +jitSuite(LessThanEqualTest); + +class GreaterThanTest extends RenderTest { + static suiteName = '{{gt}} keyword'; + + @test + ['it works']() { + const AComponent = defineComponent({}, '{{gt 1 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('false'); + } + + @test + ['it works falsey']() { + const AComponent = defineComponent({}, '{{gt 0 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('false'); + } + + @test + ['it works truthy']() { + const AComponent = defineComponent({}, '{{gt 2 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('true'); + } + + @test + ['correctly resolves when values update eq']() { + let args = trackedObj({ foo: 123, bar: 456 }); + const AComponent = defineComponent({}, '{{gt @foo @bar}}'); + this.renderComponent(AComponent, args); + + this.assertHTML('false'); + + args.foo = 456; + this.rerender(); + + this.assertHTML('false'); + + args.foo = 789; + this.rerender(); + + this.assertHTML('true'); + } + + @test + ['it errors']() { + this.assert.throws(() => { + const AComponent = defineComponent({}, '{{gt 1 2 1}}'); + this.renderComponent(AComponent); + }, syntaxErrorFor('(gt) can receive a maximum of 2 arguments', '{{gt 1 2 1}}', 'an unknown module', 1, 0)); + } +} + +class GreaterThanEqualTest extends RenderTest { + static suiteName = '{{gte}} keyword'; + + @test + ['it works']() { + const AComponent = defineComponent({}, '{{gte 2 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('true'); + } + + @test + ['it works truthy']() { + const AComponent = defineComponent({}, '{{gte 1 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('true'); + } + + @test + ['it works falsey']() { + const AComponent = defineComponent({}, '{{gte 0 1}}'); + this.renderComponent(AComponent); + + this.assertHTML('false'); + } + + @test + ['correctly resolves when values update neq']() { + let args = trackedObj({ foo: 123, bar: 456 }); + const AComponent = defineComponent({}, '{{gte @foo @bar}}'); + this.renderComponent(AComponent, args); + + this.assertHTML('false'); + + args.foo = 456; + this.rerender(); + + this.assertHTML('true'); + + args.foo = 789; + this.rerender(); + + this.assertHTML('true'); + } + + @test + ['it errors']() { + this.assert.throws(() => { + const AComponent = defineComponent({}, '{{gte 1 2 1}}'); + this.renderComponent(AComponent); + }, syntaxErrorFor('(gte) can receive a maximum of 2 arguments', '{{gte 1 2 1}}', 'an unknown module', 1, 0)); + } +} + +jitSuite(GreaterThanTest); +jitSuite(GreaterThanEqualTest); diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts index b428cb52a..44bd661d9 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts @@ -100,6 +100,10 @@ export const enum SexpOpcodes { Log = 54, Equal = 55, NotEqual = 56, + Less = 57, + LessEqual = 58, + Greater = 59, + GreaterEqual = 60, GetStart = GetSymbol, GetEnd = GetFreeAsComponentHead, @@ -252,6 +256,10 @@ export namespace Expressions { | Not | Equal | NotEqual + | Less + | LessEqual + | Greater + | GreaterEqual | Log; // TODO get rid of undefined, which is just here to allow trailing undefined in attrs @@ -277,6 +285,18 @@ export namespace Expressions { export type NotEqual = [op: SexpOpcodes.NotEqual, positional: Params]; + export type Less = [op: SexpOpcodes.Less, lOperand: Expression, rOperand: Expression]; + + export type LessEqual = [op: SexpOpcodes.LessEqual, lOperand: Expression, rOperand: Expression]; + + export type Greater = [op: SexpOpcodes.Greater, lOperand: Expression, rOperand: Expression]; + + export type GreaterEqual = [ + op: SexpOpcodes.GreaterEqual, + lOperand: Expression, + rOperand: Expression + ]; + export type GetDynamicVar = [op: SexpOpcodes.GetDynamicVar, value: Expression]; export type Log = [op: SexpOpcodes.Log, positional: Params]; diff --git a/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts b/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts index a335e80dc..3dcfcfadc 100644 --- a/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts +++ b/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts @@ -110,4 +110,8 @@ export const enum Op { Log = 112, Equal = 113, NotEqual = 114, + Less = 115, + LessEqual = 116, + Greater = 117, + GreaterEqual = 118, } diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts b/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts index 254c532c6..31e38d15f 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts @@ -154,6 +154,30 @@ EXPRESSIONS.add(SexpOpcodes.NotEqual, (op, [, positional]) => { op(Op.NotEqual); }); +EXPRESSIONS.add(SexpOpcodes.Less, (op, [, lOperand, rOperand]) => { + expr(op, rOperand); + expr(op, lOperand); + op(Op.Less); +}); + +EXPRESSIONS.add(SexpOpcodes.LessEqual, (op, [, lOperand, rOperand]) => { + expr(op, rOperand); + expr(op, lOperand); + op(Op.LessEqual); +}); + +EXPRESSIONS.add(SexpOpcodes.Greater, (op, [, lOperand, rOperand]) => { + expr(op, rOperand); + expr(op, lOperand); + op(Op.Greater); +}); + +EXPRESSIONS.add(SexpOpcodes.GreaterEqual, (op, [, lOperand, rOperand]) => { + expr(op, rOperand); + expr(op, lOperand); + op(Op.GreaterEqual); +}); + EXPRESSIONS.add(SexpOpcodes.GetDynamicVar, (op, [, expression]) => { expr(op, expression); op(Op.GetDynamicVar); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts index 83a9a2097..36eb71afe 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts @@ -342,3 +342,47 @@ APPEND_OPCODES.add(Op.NotEqual, (vm) => { }) ); }); + +APPEND_OPCODES.add(Op.Less, (vm) => { + let lOperand = check(vm.stack.pop(), CheckReference); + let rOperand = check(vm.stack.pop(), CheckReference); + + vm.stack.push( + createComputeRef(() => { + return (valueForRef(lOperand) as any) < (valueForRef(rOperand) as any); + }) + ); +}); + +APPEND_OPCODES.add(Op.LessEqual, (vm) => { + let lOperand = check(vm.stack.pop(), CheckReference); + let rOperand = check(vm.stack.pop(), CheckReference); + + vm.stack.push( + createComputeRef(() => { + return (valueForRef(lOperand) as any) <= (valueForRef(rOperand) as any); + }) + ); +}); + +APPEND_OPCODES.add(Op.Greater, (vm) => { + let lOperand = check(vm.stack.pop(), CheckReference); + let rOperand = check(vm.stack.pop(), CheckReference); + + vm.stack.push( + createComputeRef(() => { + return (valueForRef(lOperand) as any) > (valueForRef(rOperand) as any); + }) + ); +}); + +APPEND_OPCODES.add(Op.GreaterEqual, (vm) => { + let lOperand = check(vm.stack.pop(), CheckReference); + let rOperand = check(vm.stack.pop(), CheckReference); + + vm.stack.push( + createComputeRef(() => { + return (valueForRef(lOperand) as any) >= (valueForRef(rOperand) as any); + }) + ); +}); diff --git a/packages/@glimmer/syntax/lib/keywords.ts b/packages/@glimmer/syntax/lib/keywords.ts index ee30f129e..ed5e4bdbd 100644 --- a/packages/@glimmer/syntax/lib/keywords.ts +++ b/packages/@glimmer/syntax/lib/keywords.ts @@ -23,6 +23,8 @@ export const KEYWORDS_TYPES: { [key: string]: KeywordType[] } = { log: ['Call', 'Append'], eq: ['Call', 'Append'], neq: ['Call', 'Append'], + lt: ['Call', 'Append'], + lte: ['Call', 'Append'], modifier: ['Call'], mount: ['Append'], mut: ['Call', 'Append'],