Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: equality operators #1277

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CurriedType } from '@glimmer/interfaces';
import { keywords } from './impl';
import { curryKeyword } from './utils/curry';
import { getDynamicVarKeyword } from './utils/dynamic-vars';
import { equalKeyword, notEqualKeyword } from './utils/equality';
import { hasBlockKeyword } from './utils/has-block';
import { ifUnlessInlineKeyword } from './utils/if-unless';
import { logKeyword } from './utils/log';
Expand All @@ -12,6 +13,8 @@ export const CALL_KEYWORDS = keywords('Call')
.kw('has-block-params', hasBlockKeyword('has-block-params'))
.kw('-get-dynamic-var', getDynamicVarKeyword)
.kw('log', logKeyword)
.kw('eq', equalKeyword, { strictOnly: true })
.kw('neq', notEqualKeyword, { strictOnly: true })
.kw('if', ifUnlessInlineKeyword('if'))
.kw('unless', ifUnlessInlineKeyword('unless'))
.kw('component', curryKeyword(CurriedType.Component))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class KeywordImpl<
constructor(
protected keyword: S,
type: KeywordType,
private delegate: KeywordDelegate<KeywordMatches[K], Param, Out>
private delegate: KeywordDelegate<KeywordMatches[K], Param, Out>,
private options?: { strictOnly: boolean }
Copy link
Contributor Author

@snewcomer snewcomer Apr 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure if this anonymous type was ok with you. (we could share an actual interface in multiple spots)

) {
let nodes = new Set<KeywordNode['type']>();
for (let nodeType of KEYWORD_NODES[type]) {
Expand All @@ -44,7 +45,17 @@ class KeywordImpl<
this.types = nodes;
}

protected match(node: KeywordCandidates[K]): node is KeywordMatches[K] {
protected match(
node: KeywordCandidates[K],
state: NormalizationState
): node is KeywordMatches[K] {
// some keywords are enabled only in strict mode. None are planned to be loose mode only
if (this.options?.strictOnly) {
if (state.isStrict === false) {
return false;
}
}

if (!this.types.has(node.type)) {
return false;
}
Expand All @@ -67,7 +78,7 @@ class KeywordImpl<
}

translate(node: KeywordMatches[K], state: NormalizationState): Result<Out> | null {
if (this.match(node)) {
if (this.match(node, state)) {
let path = getCalleeExpression(node);

if (path !== null && path.type === 'Path' && path.tail.length > 0) {
Expand Down Expand Up @@ -136,8 +147,13 @@ export function keyword<
K extends KeywordType,
D extends KeywordDelegate<KeywordMatches[K], unknown, Out>,
Out = unknown
>(keyword: string, type: K, delegate: D): Keyword<K, Out> {
return new KeywordImpl(keyword, type, delegate as KeywordDelegate<KeywordMatch, unknown, Out>);
>(keyword: string, type: K, delegate: D, options?: { strictOnly: boolean }): Keyword<K, Out> {
return new KeywordImpl(
keyword,
type,
delegate as KeywordDelegate<KeywordMatch, unknown, Out>,
options
);
}

export type PossibleKeyword = KeywordNode;
Expand Down Expand Up @@ -177,9 +193,10 @@ export class Keywords<K extends KeywordType, KeywordList extends Keyword<K> = ne

kw<S extends string = string, Out = unknown>(
name: S,
delegate: KeywordDelegate<KeywordMatches[K], unknown, Out>
delegate: KeywordDelegate<KeywordMatches[K], unknown, Out>,
options?: { strictOnly: boolean }
): Keywords<K, KeywordList | Keyword<K, Out>> {
this._keywords.push(keyword(name, this._type, delegate));
this._keywords.push(keyword(name, this._type, delegate, options));

return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 assertEqualKeyword(node: GenericKeywordNode): Result<ASTv2.PositionalArguments> {
let {
args: { named, positional },
} = node;

if (named && !named.isEmpty()) {
return Err(generateSyntaxError(`(eq) does not take any named arguments`, node.loc));
}

if (positional.size !== 2) {
return Err(
generateSyntaxError(
`(eq) must receive two positional parameters. Received ${
positional?.size ?? 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
positional?.size ?? 0
positional.size ?? 0

Since we already do positional.size on line 18?

} parameters.`,
node.loc
)
);
}

return Ok(positional);
}

function translateEqualKeyword(
{ node, state }: { node: ASTv2.CallExpression; state: NormalizationState },
positional: ASTv2.PositionalArguments
): Result<mir.Equal> {
return VISIT_EXPRS.Positional(positional, state).mapOk(
(positional) => new mir.Equal({ positional, loc: node.loc })
);
}

export const equalKeyword: KeywordDelegate<
ASTv2.CallExpression | ASTv2.AppendContent,
ASTv2.PositionalArguments,
mir.Equal
> = {
assert: assertEqualKeyword,
translate: translateEqualKeyword,
};

function assertNotEqualKeyword(node: GenericKeywordNode): Result<ASTv2.PositionalArguments> {
let {
args: { named, positional },
} = node;

if (named && !named.isEmpty()) {
return Err(generateSyntaxError(`(neq) does not take any named arguments`, node.loc));
}

if (positional.size !== 2) {
return Err(
generateSyntaxError(
`(neq) must receive two positional parameters. Received ${
positional?.size ?? 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
positional?.size ?? 0
positional.size ?? 0

Since we already do positional.size on line 59?

} parameters.`,
node.loc
)
);
}

return Ok(positional);
}

function translateNotEqualKeyword(
{ node, state }: { node: ASTv2.CallExpression; state: NormalizationState },
positional: ASTv2.PositionalArguments
): Result<mir.NotEqual> {
return VISIT_EXPRS.Positional(positional, state).mapOk(
(positional) => new mir.NotEqual({ positional, loc: node.loc })
);
}

export const notEqualKeyword: KeywordDelegate<
ASTv2.CallExpression | ASTv2.AppendContent,
ASTv2.PositionalArguments,
mir.NotEqual
> = {
assert: assertNotEqualKeyword,
translate: translateNotEqualKeyword,
};
12 changes: 12 additions & 0 deletions packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export class ExpressionEncoder {
return this.GetDynamicVar(expr);
case 'Log':
return this.Log(expr);
case 'Equal':
return this.Equal(expr);
case 'NotEqual':
return this.NotEqual(expr);
}
}

Expand Down Expand Up @@ -171,6 +175,14 @@ export class ExpressionEncoder {
Log({ positional }: mir.Log): WireFormat.Expressions.Log {
return [SexpOpcodes.Log, this.Positional(positional)];
}

Equal({ positional }: mir.Equal): WireFormat.Expressions.Equal {
return [SexpOpcodes.Equal, this.Positional(positional)];
}

NotEqual({ positional }: mir.NotEqual): WireFormat.Expressions.NotEqual {
return [SexpOpcodes.NotEqual, this.Positional(positional)];
}
}

export const EXPR = new ExpressionEncoder();
12 changes: 11 additions & 1 deletion packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export class Log extends node('Log').fields<{
positional: Positional;
}>() {}

export class Equal extends node('Equal').fields<{
positional: Positional;
}>() {}

export class NotEqual extends node('NotEqual').fields<{
positional: Positional;
}>() {}

export class InvokeComponent extends node('InvokeComponent').fields<{
definition: ExpressionNode;
args: Args;
Expand Down Expand Up @@ -216,7 +224,9 @@ export type ExpressionNode =
| HasBlockParams
| Curry
| GetDynamicVar
| Log;
| Log
| Equal
| NotEqual;

export type ElementParameter = StaticAttr | DynamicAttr | Modifier | SplatAttr;

Expand Down
6 changes: 6 additions & 0 deletions packages/@glimmer/compiler/lib/wire-format-debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ export default class WireFormatDebugger {
this.formatHash(opcode[3]),
this.formatBlocks(opcode[4]),
];

case Op.Equal:
return ['eq', this.formatParams(opcode[1])];

case Op.NotEqual:
return ['neq', this.formatParams(opcode[1])];
}
} else {
return opcode;
Expand Down
110 changes: 110 additions & 0 deletions packages/@glimmer/integration-tests/test/keywords/equality-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
RenderTest,
test,
jitSuite,
defineComponent,
preprocess,
syntaxErrorFor,
trackedObj,
} from '../..';

class EqualTest extends RenderTest {
static suiteName = '{{eq}} keyword';

@test
['it works eq']() {
const AComponent = defineComponent({}, '{{eq 1 1}}');
this.renderComponent(AComponent);

this.assertHTML('true');
}

@test
['it errors if 1 argument eq']() {
this.assert.throws(() => {
preprocess(`{{eq 1}}`, { strictMode: true, meta: { moduleName: 'test-module' } });
}, syntaxErrorFor('(eq) must receive two positional parameters. Received 1 parameters.', `{{eq 1}}`, 'test-module', 1, 0));
}

@test
['it errors if more than 2 arguments eq']() {
this.assert.throws(() => {
preprocess(`{{eq 1 1 1}}`, { strictMode: true, meta: { moduleName: 'test-module' } });
}, syntaxErrorFor('(eq) must receive two positional parameters. Received 3 parameters.', `{{eq 1 1 1}}`, 'test-module', 1, 0));
}

@test
['it works falsey eq']() {
const AComponent = defineComponent({}, '{{eq 1 2}}');
this.renderComponent(AComponent);

this.assertHTML('false');
}

@test
['correctly renders when values update eq']() {
let args = trackedObj({ foo: 123, bar: 456 });

const AComponent = defineComponent({}, '{{eq @foo @bar}}');
this.renderComponent(AComponent, args);

this.assertHTML('false');

args.foo = 456;
this.rerender();

this.assertHTML('true');
}
}

class NotEqualTest extends RenderTest {
static suiteName = '{{neq}} keyword';

@test
['it works neq']() {
const AComponent = defineComponent({}, '{{neq 1 2}}');
this.renderComponent(AComponent);

this.assertHTML('true');
}

@test
['it errors if 1 argument neq']() {
this.assert.throws(() => {
preprocess(`{{neq 1}}`, { strictMode: true, meta: { moduleName: 'test-module' } });
}, syntaxErrorFor('(neq) must receive two positional parameters. Received 1 parameters.', `{{neq 1}}`, 'test-module', 1, 0));
}

@test
['it errors if more than 2 arguments neq']() {
this.assert.throws(() => {
preprocess(`{{neq 1 1 1}}`, { strictMode: true, meta: { moduleName: 'test-module' } });
}, syntaxErrorFor('(neq) must receive two positional parameters. Received 3 parameters.', `{{neq 1 1 1}}`, 'test-module', 1, 0));
}

@test
['it works falsey neq']() {
const AComponent = defineComponent({}, '{{neq 1 1}}');
this.renderComponent(AComponent);

this.assertHTML('false');
}

@test
['correctly renders when values update neq']() {
let args = trackedObj({ foo: 123, bar: 456 });

const AComponent = defineComponent({}, '{{neq @foo @bar}}');
this.renderComponent(AComponent, args);

this.assertHTML('true');

args.foo = 456;
this.rerender({ foo: 456 });

this.assertHTML('false');
}
}

jitSuite(EqualTest);
jitSuite(NotEqualTest);
8 changes: 8 additions & 0 deletions packages/@glimmer/interfaces/lib/compile/wire-format.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export const enum SexpOpcodes {
IfInline = 52,
GetDynamicVar = 53,
Log = 54,
Equal = 55,
NotEqual = 56,

GetStart = GetSymbol,
GetEnd = GetFreeAsComponentHead,
Expand Down Expand Up @@ -248,6 +250,8 @@ export namespace Expressions {
| Undefined
| IfInline
| Not
| Equal
| NotEqual
| Log;

// TODO get rid of undefined, which is just here to allow trailing undefined in attrs
Expand All @@ -269,6 +273,10 @@ export namespace Expressions {

export type Not = [op: SexpOpcodes.Not, value: Expression];

export type Equal = [op: SexpOpcodes.Equal, positional: Params];

export type NotEqual = [op: SexpOpcodes.NotEqual, positional: Params];

export type GetDynamicVar = [op: SexpOpcodes.GetDynamicVar, value: Expression];

export type Log = [op: SexpOpcodes.Log, positional: Params];
Expand Down
2 changes: 2 additions & 0 deletions packages/@glimmer/interfaces/lib/vm-opcodes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@ export const enum Op {
Not = 110,
GetDynamicVar = 111,
Log = 112,
Equal = 113,
NotEqual = 114,
}
Loading