diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/append.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/append.ts index 4bd0f77524..f4e2056c87 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/append.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/append.ts @@ -8,6 +8,7 @@ import { VISIT_EXPRS } from '../visitors/expressions'; import { keywords } from './impl'; import { assertValidCurryUsage } from './utils/curry'; import { assertValidHasBlockUsage } from './utils/has-block'; +import { assertValidIfUnlessInlineUsage } from './utils/if-unless'; export const APPEND_KEYWORDS = keywords('Append') .kw('yield', { @@ -194,6 +195,72 @@ export const APPEND_KEYWORDS = keywords('Append') return Ok(new mir.AppendTextNode({ loc: node.loc, text })); }, }) + .kw('if', { + assert: assertValidIfUnlessInlineUsage('{{if}}', false), + + translate( + { node, state }: { node: ASTv2.AppendContent; state: NormalizationState }, + { + condition, + truthy, + falsy, + }: { + condition: ASTv2.ExpressionNode; + truthy: ASTv2.ExpressionNode; + falsy: ASTv2.ExpressionNode | null; + } + ): Result { + let conditionResult = VISIT_EXPRS.visit(condition, state); + let truthyResult = VISIT_EXPRS.visit(truthy, state); + let falsyResult = falsy ? VISIT_EXPRS.visit(falsy, state) : Ok(null); + + return Result.all(conditionResult, truthyResult, falsyResult).mapOk( + ([condition, truthy, falsy]) => { + let text = new mir.IfInline({ + loc: node.loc, + condition, + truthy, + falsy, + }); + + return new mir.AppendTextNode({ loc: node.loc, text }); + } + ); + }, + }) + .kw('unless', { + assert: assertValidIfUnlessInlineUsage('{{unless}}', true), + + translate( + { node, state }: { node: ASTv2.AppendContent; state: NormalizationState }, + { + condition, + truthy, + falsy, + }: { + condition: ASTv2.ExpressionNode; + truthy: ASTv2.ExpressionNode; + falsy: ASTv2.ExpressionNode | null; + } + ): Result { + let conditionResult = VISIT_EXPRS.visit(condition, state); + let truthyResult = VISIT_EXPRS.visit(truthy, state); + let falsyResult = falsy ? VISIT_EXPRS.visit(falsy, state) : Ok(null); + + return Result.all(conditionResult, truthyResult, falsyResult).mapOk( + ([condition, truthy, falsy]) => { + let text = new mir.IfInline({ + loc: node.loc, + condition: new mir.Not({ value: condition, loc: node.loc }), + truthy, + falsy, + }); + + return new mir.AppendTextNode({ loc: node.loc, text }); + } + ); + }, + }) .kw('component', { assert: assertValidCurryUsage('{{component}}', 'component', true), diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts index 72d1dc47df..6d6efe549c 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts @@ -101,8 +101,8 @@ export const BLOCK_KEYWORDS = keywords('Block') generateSyntaxError( `{{#if}} cannot receive named parameters, received ${args.named.entries .map((e) => e.name.chars) - .join(', ')}.`, - args.named.loc + .join(', ')}`, + node.loc ) ); } @@ -110,8 +110,8 @@ export const BLOCK_KEYWORDS = keywords('Block') if (args.positional.size > 1) { return Err( generateSyntaxError( - `{{#if}} can only receive one positional parameter, the conditional value. Received ${args.positional.size} parameters.`, - args.positional.loc + `{{#if}} can only receive one positional parameter in block form, the conditional value. Received ${args.positional.size} parameters`, + node.loc ) ); } @@ -121,8 +121,8 @@ export const BLOCK_KEYWORDS = keywords('Block') if (condition === null) { return Err( generateSyntaxError( - `{{#if}} requires a condition as its first positional parameter, did not receive any parameters.`, - args.loc + `{{#if}} requires a condition as its first positional parameter, did not receive any parameters`, + node.loc ) ); } @@ -165,8 +165,8 @@ export const BLOCK_KEYWORDS = keywords('Block') generateSyntaxError( `{{#unless}} cannot receive named parameters, received ${args.named.entries .map((e) => e.name.chars) - .join(', ')}.`, - args.named.loc + .join(', ')}`, + node.loc ) ); } @@ -174,8 +174,8 @@ export const BLOCK_KEYWORDS = keywords('Block') if (args.positional.size > 1) { return Err( generateSyntaxError( - `{{#unless}} can only receive one positional parameter, the conditional value. Received ${args.positional.size} parameters.`, - args.positional.loc + `{{#unless}} can only receive one positional parameter in block form, the conditional value. Received ${args.positional.size} parameters`, + node.loc ) ); } @@ -185,8 +185,8 @@ export const BLOCK_KEYWORDS = keywords('Block') if (condition === null) { return Err( generateSyntaxError( - `{{#unless}} requires a condition as its first positional parameter, did not receive any parameters.`, - args.loc + `{{#unless}} requires a condition as its first positional parameter, did not receive any parameters`, + node.loc ) ); } @@ -197,7 +197,7 @@ export const BLOCK_KEYWORDS = keywords('Block') translate( { node, state }: { node: ASTv2.InvokeBlock; state: NormalizationState }, { condition }: { condition: ASTv2.ExpressionNode } - ): Result { + ): Result { let block = node.blocks.get('default'); let inverse = node.blocks.get('else'); @@ -207,9 +207,9 @@ export const BLOCK_KEYWORDS = keywords('Block') return Result.all(conditionResult, blockResult, inverseResult).mapOk( ([condition, block, inverse]) => - new mir.Unless({ + new mir.If({ loc: node.loc, - condition, + condition: new mir.Not({ value: condition, loc: node.loc }), block, inverse, }) @@ -231,7 +231,7 @@ export const BLOCK_KEYWORDS = keywords('Block') `{{#each}} can only receive the 'key' named parameter, received ${args.named.entries .filter((e) => e.name.chars !== 'key') .map((e) => e.name.chars) - .join(', ')}.`, + .join(', ')}`, args.named.loc ) ); @@ -240,7 +240,7 @@ export const BLOCK_KEYWORDS = keywords('Block') if (args.positional.size > 1) { return Err( generateSyntaxError( - `{{#each}} can only receive one positional parameter, the collection being iterated. Received ${args.positional.size} parameters.`, + `{{#each}} can only receive one positional parameter, the collection being iterated. Received ${args.positional.size} parameters`, args.positional.loc ) ); @@ -252,7 +252,7 @@ export const BLOCK_KEYWORDS = keywords('Block') if (value === null) { return Err( generateSyntaxError( - `{{#each}} requires an iterable value to be passed as its first positional parameter, did not receive any parameters.`, + `{{#each}} requires an iterable value to be passed as its first positional parameter, did not receive any parameters`, args.loc ) ); @@ -299,7 +299,7 @@ export const BLOCK_KEYWORDS = keywords('Block') generateSyntaxError( `{{#with}} cannot receive named parameters, received ${args.named.entries .map((e) => e.name.chars) - .join(', ')}.`, + .join(', ')}`, args.named.loc ) ); @@ -308,7 +308,7 @@ export const BLOCK_KEYWORDS = keywords('Block') if (args.positional.size > 1) { return Err( generateSyntaxError( - `{{#with}} can only receive one positional parameter. Received ${args.positional.size} parameters.`, + `{{#with}} can only receive one positional parameter. Received ${args.positional.size} parameters`, args.positional.loc ) ); @@ -319,7 +319,7 @@ export const BLOCK_KEYWORDS = keywords('Block') if (value === null) { return Err( generateSyntaxError( - `{{#with}} requires a value as its first positional parameter, did not receive any parameters.`, + `{{#with}} requires a value as its first positional parameter, did not receive any parameters`, args.loc ) ); @@ -363,7 +363,7 @@ export const BLOCK_KEYWORDS = keywords('Block') generateSyntaxError( `{{#let}} cannot receive named parameters, received ${args.named.entries .map((e) => e.name.chars) - .join(', ')}.`, + .join(', ')}`, args.named.loc ) ); @@ -372,7 +372,7 @@ export const BLOCK_KEYWORDS = keywords('Block') if (args.positional.size === 0) { return Err( generateSyntaxError( - `{{#let}} requires at least one value as its first positional parameter, did not receive any parameters.`, + `{{#let}} requires at least one value as its first positional parameter, did not receive any parameters`, args.positional.loc ) ); 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 263e6861ea..a6979ea568 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/call.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/call.ts @@ -8,6 +8,7 @@ import { VISIT_EXPRS } from '../visitors/expressions'; import { keywords } from './impl'; import { assertValidCurryUsage } from './utils/curry'; import { assertValidHasBlockUsage } from './utils/has-block'; +import { assertValidIfUnlessInlineUsage } from './utils/if-unless'; export const CALL_KEYWORDS = keywords('Call') .kw('has-block', { @@ -36,6 +37,68 @@ export const CALL_KEYWORDS = keywords('Call') ); }, }) + .kw('if', { + assert: assertValidIfUnlessInlineUsage('(if)', false), + + translate( + { node, state }: { node: ASTv2.CallExpression; state: NormalizationState }, + { + condition, + truthy, + falsy, + }: { + condition: ASTv2.ExpressionNode; + truthy: ASTv2.ExpressionNode; + falsy: ASTv2.ExpressionNode | null; + } + ): Result { + let conditionResult = VISIT_EXPRS.visit(condition, state); + let truthyResult = VISIT_EXPRS.visit(truthy, state); + let falsyResult = falsy ? VISIT_EXPRS.visit(falsy, state) : Ok(null); + + return Result.all(conditionResult, truthyResult, falsyResult).mapOk( + ([condition, truthy, falsy]) => + new mir.IfInline({ + loc: node.loc, + condition, + truthy, + falsy, + }) + ); + }, + }) + .kw('unless', { + assert: assertValidIfUnlessInlineUsage('(unless)', true), + + translate( + { node, state }: { node: ASTv2.CallExpression; state: NormalizationState }, + { + condition, + falsy, + truthy, + }: { + condition: ASTv2.ExpressionNode; + truthy: ASTv2.ExpressionNode; + falsy: ASTv2.ExpressionNode | null; + } + ): Result { + let conditionResult = VISIT_EXPRS.visit(condition, state); + let truthyResult = VISIT_EXPRS.visit(truthy, state); + let falsyResult = falsy ? VISIT_EXPRS.visit(falsy, state) : Ok(null); + + return Result.all(conditionResult, truthyResult, falsyResult).mapOk( + ([condition, truthy, falsy]) => + new mir.IfInline({ + loc: node.loc, + + // We reverse the condition by inserting a Not + condition: new mir.Not({ value: condition, loc: node.loc }), + truthy, + falsy, + }) + ); + }, + }) .kw('component', { assert: assertValidCurryUsage('(component)', 'component', true), diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/utils/if-unless.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/utils/if-unless.ts new file mode 100644 index 0000000000..d0634704fe --- /dev/null +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/utils/if-unless.ts @@ -0,0 +1,70 @@ +import { ASTv2, generateSyntaxError } from '@glimmer/syntax'; + +import { Err, Ok, Result } from '../../../../shared/result'; + +export function assertValidIfUnlessInlineUsage(type: string, inverted: boolean) { + return ( + originalNode: ASTv2.AppendContent | ASTv2.ExpressionNode + ): Result<{ + condition: ASTv2.ExpressionNode; + truthy: ASTv2.ExpressionNode; + falsy: ASTv2.ExpressionNode | null; + }> => { + let node = originalNode.type === 'AppendContent' ? originalNode.value : originalNode; + let named = node.type === 'Call' ? node.args.named : null; + let positional = node.type === 'Call' ? node.args.positional : null; + + if (named && !named.isEmpty()) { + return Err( + generateSyntaxError( + `${type} cannot receive named parameters, received ${named.entries + .map((e) => e.name.chars) + .join(', ')}`, + originalNode.loc + ) + ); + } + + let condition = positional?.nth(0); + + if (!positional || !condition) { + return Err( + generateSyntaxError( + `When used inline, ${type} requires at least two parameters 1. the condition that determines the state of the ${type}, and 2. the value to return if the condition is ${ + inverted ? 'false' : 'true' + }. Did not receive any parameters`, + originalNode.loc + ) + ); + } + + let truthy = positional.nth(1); + let falsy = positional.nth(2); + + if (truthy === null) { + return Err( + generateSyntaxError( + `When used inline, ${type} requires at least two parameters 1. the condition that determines the state of the ${type}, and 2. the value to return if the condition is ${ + inverted ? 'false' : 'true' + }. Received only one parameter, the condition`, + originalNode.loc + ) + ); + } + + if (positional.size > 3) { + return Err( + generateSyntaxError( + `When used inline, ${type} can receive a maximum of three positional parameters 1. the condition that determines the state of the ${type}, 2. the value to return if the condition is ${ + inverted ? 'false' : 'true' + }, and 3. the value to return if the condition is ${ + inverted ? 'true' : 'false' + }. Received ${positional?.size ?? 0} parameters`, + originalNode.loc + ) + ); + } + + return Ok({ condition, truthy, falsy }); + }; +} diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts index 70ee7df5cc..beab52b3f5 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts @@ -64,8 +64,6 @@ export class ContentEncoder { return this.InvokeBlock(stmt); case 'If': return this.If(stmt); - case 'Unless': - return this.Unless(stmt); case 'Each': return this.Each(stmt); case 'With': @@ -194,15 +192,6 @@ export class ContentEncoder { ]; } - Unless({ condition, block, inverse }: mir.Unless): WireFormat.Statements.Unless { - return [ - SexpOpcodes.Unless, - EXPR.expr(condition), - CONTENT.NamedBlock(block)[1], - inverse ? CONTENT.NamedBlock(inverse)[1] : null, - ]; - } - Each({ value, key, block, inverse }: mir.Each): WireFormat.Statements.Each { return [ SexpOpcodes.Each, diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts index 6654a3cebc..2647bd7405 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts @@ -31,6 +31,10 @@ export class ExpressionEncoder { return this.HasBlockParams(expr); case 'Curry': return this.Curry(expr); + case 'Not': + return this.Not(expr); + case 'IfInline': + return this.IfInline(expr); case 'InterpolateExpression': return this.InterpolateExpression(expr); } @@ -132,6 +136,19 @@ export class ExpressionEncoder { return null; } } + + Not({ value }: mir.Not): WireFormat.Expressions.Not { + return [SexpOpcodes.Not, EXPR.expr(value)]; + } + + IfInline({ condition, truthy, falsy }: mir.IfInline): WireFormat.Expressions.IfInline { + return [ + SexpOpcodes.IfInline, + EXPR.expr(condition), + EXPR.expr(truthy), + falsy ? EXPR.expr(falsy) : null, + ]; + } } 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 e4b13a42d6..23c5114cf3 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts @@ -22,16 +22,18 @@ export class InElement extends node('InElement').fields<{ block: NamedBlock; }>() {} +export class Not extends node('Not').fields<{ value: ExpressionNode }>() {} + export class If extends node('If').fields<{ condition: ExpressionNode; block: NamedBlock; inverse: NamedBlock | null; }>() {} -export class Unless extends node('Unless').fields<{ +export class IfInline extends node('IfInline').fields<{ condition: ExpressionNode; - block: NamedBlock; - inverse: NamedBlock | null; + truthy: ExpressionNode; + falsy: ExpressionNode | null; }>() {} export class Each extends node('Each').fields<{ @@ -198,6 +200,8 @@ export type ExpressionNode = | ASTv2.VariableReference | InterpolateExpression | CallExpression + | Not + | IfInline | HasBlock | HasBlockParams | Curry; @@ -226,7 +230,6 @@ export type Statement = | Partial | AppendComment | If - | Unless | Each | With | Let diff --git a/packages/@glimmer/compiler/lib/wire-format-debug.ts b/packages/@glimmer/compiler/lib/wire-format-debug.ts index aeef893b6a..436f3f17ef 100644 --- a/packages/@glimmer/compiler/lib/wire-format-debug.ts +++ b/packages/@glimmer/compiler/lib/wire-format-debug.ts @@ -219,13 +219,11 @@ export default class WireFormatDebugger { opcode[3] ? this.formatBlock(opcode[3]) : null, ]; - case Op.Unless: - return [ - 'unless', - this.formatOpcode(opcode[1]), - this.formatBlock(opcode[2]), - opcode[3] ? this.formatBlock(opcode[3]) : null, - ]; + case Op.IfInline: + return ['if-inline']; + + case Op.Not: + return ['not']; case Op.Each: return [ diff --git a/packages/@glimmer/debug/lib/opcode-metadata.ts b/packages/@glimmer/debug/lib/opcode-metadata.ts index 7f6f550231..3760b1245f 100644 --- a/packages/@glimmer/debug/lib/opcode-metadata.ts +++ b/packages/@glimmer/debug/lib/opcode-metadata.ts @@ -280,6 +280,36 @@ METADATA[Op.Concat] = { check: true, }; +METADATA[Op.IfInline] = { + name: 'IfInline', + mnemonic: 'ifinline', + before: null, + stackChange: -2, + ops: [ + { + name: 'count', + type: 'u32', + }, + ], + operands: 1, + check: true, +}; + +METADATA[Op.Not] = { + name: 'Not', + mnemonic: 'not', + before: null, + stackChange: 0, + ops: [ + { + name: 'count', + type: 'u32', + }, + ], + operands: 1, + check: true, +}; + METADATA[Op.Constant] = { name: 'Constant', mnemonic: 'rconstload', diff --git a/packages/@glimmer/integration-tests/test/syntax/if-unless-test.ts b/packages/@glimmer/integration-tests/test/syntax/if-unless-test.ts new file mode 100644 index 0000000000..fcb2a334b6 --- /dev/null +++ b/packages/@glimmer/integration-tests/test/syntax/if-unless-test.ts @@ -0,0 +1,96 @@ +import { RenderTest, jitSuite, test, preprocess, syntaxErrorFor } from '../..'; + +let types = ['if', 'unless']; + +for (let type of types) { + class SyntaxErrors extends RenderTest { + static suiteName = `if/unless (${type}) keyword syntax errors`; + + @test + [`{{#${type}}} throws if it received named args`]() { + this.assert.throws(() => { + preprocess(`{{#${type} condition=true}}{{/${type}}}`, { + meta: { moduleName: 'test-module' }, + }); + }, syntaxErrorFor(`{{#${type}}} cannot receive named parameters, received condition`, `{{#${type} condition=true}}{{/${type}}}`, 'test-module', 1, 0)); + } + + @test + [`{{#${type}}} throws if it received no positional params`]() { + this.assert.throws(() => { + preprocess(`{{#${type}}}{{/${type}}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`{{#${type}}} requires a condition as its first positional parameter, did not receive any parameters`, `{{#${type}}}{{/${type}}}`, 'test-module', 1, 0)); + } + + @test + [`{{#${type}}} throws if it received more than one positional param`]() { + this.assert.throws(() => { + preprocess(`{{#${type} true false}}{{/${type}}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`{{#${type}}} can only receive one positional parameter in block form, the conditional value. Received 2 parameters`, `{{#${type} true false}}{{/${type}}}`, 'test-module', 1, 0)); + } + + @test + [`{{${type}}} throws if it received named args`]() { + this.assert.throws(() => { + preprocess(`{{${type} condition=true}}`, { + meta: { moduleName: 'test-module' }, + }); + }, syntaxErrorFor(`{{${type}}} cannot receive named parameters, received condition`, `{{${type} condition=true}}`, 'test-module', 1, 0)); + } + + @test + [`{{${type}}} throws if it received no positional params`]() { + this.assert.throws(() => { + preprocess(`{{${type}}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`When used inline, {{${type}}} requires at least two parameters 1. the condition that determines the state of the {{${type}}}, and 2. the value to return if the condition is ${type === 'if' ? 'true' : 'false'}. Did not receive any parameters`, `{{${type}}}`, 'test-module', 1, 0)); + } + + @test + [`{{${type}}} throws if it received only one positional param`]() { + this.assert.throws(() => { + preprocess(`{{${type} true}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`When used inline, {{${type}}} requires at least two parameters 1. the condition that determines the state of the {{${type}}}, and 2. the value to return if the condition is ${type === 'if' ? 'true' : 'false'}. Received only one parameter, the condition`, `{{${type} true}}`, 'test-module', 1, 0)); + } + + @test + [`{{${type}}} throws if it received more than 3 positional params`]() { + this.assert.throws(() => { + preprocess(`{{${type} true false true false}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`When used inline, {{${type}}} can receive a maximum of three positional parameters 1. the condition that determines the state of the {{${type}}}, 2. the value to return if the condition is ${type === 'if' ? 'true' : 'false'}, and 3. the value to return if the condition is ${type === 'if' ? 'false' : 'true'}. Received 4 parameters`, `{{${type} true false true false}}`, 'test-module', 1, 0)); + } + + @test + [`(${type}) throws if it received named args`]() { + this.assert.throws(() => { + preprocess(`{{foo (${type} condition=true)}}`, { + meta: { moduleName: 'test-module' }, + }); + }, syntaxErrorFor(`(${type}) cannot receive named parameters, received condition`, `(${type} condition=true)`, 'test-module', 1, 6)); + } + + @test + [`(${type}) throws if it received no positional params`]() { + this.assert.throws(() => { + preprocess(`{{foo (${type})}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`When used inline, (${type}) requires at least two parameters 1. the condition that determines the state of the (${type}), and 2. the value to return if the condition is ${type === 'if' ? 'true' : 'false'}. Did not receive any parameters`, `(${type})`, 'test-module', 1, 6)); + } + + @test + [`(${type}) throws if it received only one positional param`]() { + this.assert.throws(() => { + preprocess(`{{foo (${type} true)}}`, { meta: { moduleName: 'test-module' } }); + }, syntaxErrorFor(`When used inline, (${type}) requires at least two parameters 1. the condition that determines the state of the (${type}), and 2. the value to return if the condition is ${type === 'if' ? 'true' : 'false'}. Received only one parameter, the condition`, `(${type} true)`, 'test-module', 1, 6)); + } + + @test + [`(${type}) throws if it received more than 3 positional params`]() { + this.assert.throws(() => { + preprocess(`{{foo (${type} true false true false)}}`, { + meta: { moduleName: 'test-module' }, + }); + }, syntaxErrorFor(`When used inline, (${type}) can receive a maximum of three positional parameters 1. the condition that determines the state of the (${type}), 2. the value to return if the condition is ${type === 'if' ? 'true' : 'false'}, and 3. the value to return if the condition is ${type === 'if' ? 'false' : 'true'}. Received 4 parameters`, `(${type} true false true false)`, 'test-module', 1, 6)); + } + } + + jitSuite(SyntaxErrors); +} diff --git a/packages/@glimmer/integration-tests/test/updating-test.ts b/packages/@glimmer/integration-tests/test/updating-test.ts index 439da1a2d5..1abbcbf731 100644 --- a/packages/@glimmer/integration-tests/test/updating-test.ts +++ b/packages/@glimmer/integration-tests/test/updating-test.ts @@ -736,6 +736,130 @@ class UpdatingTest extends RenderTest { this.assertHTML('

Nothing

', 'If the condition is false, the else renders'); } + @test + 'if keyword in append position'() { + this.render('{{if this.condition "truthy"}}', { + condition: true, + }); + + this.assertHTML('truthy', 'Initial render'); + + this.rerender({ condition: false }); + this.assertHTML('', 'If the condition is false nothing renders'); + + this.rerender({ condition: true }); + this.assertHTML('truthy', 'If the condition is true, the truthy value renders'); + } + + @test + 'if keyword in append position with falsy'() { + this.render('{{if this.condition "truthy" "falsy"}}', { + condition: true, + }); + + this.assertHTML('truthy', 'Initial render'); + + this.rerender({ condition: false }); + this.assertHTML('falsy', 'If the condition is false, the falsy value renders'); + + this.rerender({ condition: true }); + this.assertHTML('truthy', 'If the condition is true, the truthy value renders'); + } + + @test + 'unless keyword in append position'() { + this.render('{{unless this.condition "falsy"}}', { + condition: false, + }); + + this.assertHTML('falsy', 'Initial render'); + + this.rerender({ condition: true }); + this.assertHTML('', 'If the condition is true nothing renders'); + + this.rerender({ condition: false }); + this.assertHTML('falsy', 'If the condition is false, the falsy value renders'); + } + + @test + 'unless keyword in append position with truthy'() { + this.render('{{unless this.condition "falsy" "truthy"}}', { + condition: false, + }); + + this.assertHTML('falsy', 'Initial render'); + + this.rerender({ condition: true }); + this.assertHTML('truthy', 'If the condition is true, the truthy value renders'); + + this.rerender({ condition: false }); + this.assertHTML('falsy', 'If the condition is false, the falsy value renders'); + } + + @test + 'if keyword in call position'() { + this.registerComponent('TemplateOnly', 'Foo', '{{@value}}'); + this.render('', { + condition: true, + }); + + this.assertHTML('truthy', 'Initial render'); + + this.rerender({ condition: false }); + this.assertHTML('', 'If the condition is false nothing renders'); + + this.rerender({ condition: true }); + this.assertHTML('truthy', 'If the condition is true, the truthy value renders'); + } + + @test + 'if keyword in call position with falsy'() { + this.registerComponent('TemplateOnly', 'Foo', '{{@value}}'); + this.render('', { + condition: true, + }); + + this.assertHTML('truthy', 'Initial render'); + + this.rerender({ condition: false }); + this.assertHTML('falsy', 'If the condition is false, the falsy value renders'); + + this.rerender({ condition: true }); + this.assertHTML('truthy', 'If the condition is true, the truthy value renders'); + } + + @test + 'unless keyword in call position'() { + this.registerComponent('TemplateOnly', 'Foo', '{{@value}}'); + this.render('', { + condition: false, + }); + + this.assertHTML('falsy', 'Initial render'); + + this.rerender({ condition: true }); + this.assertHTML('', 'If the condition is true nothing renders'); + + this.rerender({ condition: false }); + this.assertHTML('falsy', 'If the condition is false, the falsy value renders'); + } + + @test + 'unless keyword in call position with truthy'() { + this.registerComponent('TemplateOnly', 'Foo', '{{@value}}'); + this.render('', { + condition: false, + }); + + this.assertHTML('falsy', 'Initial render'); + + this.rerender({ condition: true }); + this.assertHTML('truthy', 'If the condition is true, the truthy value renders'); + + this.rerender({ condition: false }); + this.assertHTML('falsy', 'If the condition is false, the falsy value renders'); + } + @test 'a conditional that is false on the first run'() { this.render('
{{#if this.condition}}

{{this.value}}

{{/if}}
', { diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts index 5c2e4627f7..18874fa02a 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format.d.ts @@ -87,17 +87,18 @@ export const enum SexpOpcodes { // Keyword Statements InElement = 40, If = 41, - Unless = 42, - Each = 43, - With = 44, - Let = 45, - WithDynamicVars = 46, - InvokeComponent = 47, + Each = 42, + With = 43, + Let = 44, + WithDynamicVars = 45, + InvokeComponent = 46, // Keyword Expressions HasBlock = 48, HasBlockParams = 49, Curry = 50, + Not = 51, + IfInline = 52, GetStart = GetSymbol, GetEnd = GetFreeAsComponentHead, @@ -238,7 +239,9 @@ export namespace Expressions { | HasBlockParams | Curry | Helper - | Undefined; + | Undefined + | IfInline + | Not; // TODO get rid of undefined, which is just here to allow trailing undefined in attrs // it would be better to handle that as an over-the-wire encoding concern @@ -249,6 +252,15 @@ export namespace Expressions { export type HasBlock = [SexpOpcodes.HasBlock, Expression]; export type HasBlockParams = [SexpOpcodes.HasBlockParams, Expression]; export type Curry = [SexpOpcodes.Curry, Expression, CurriedType, Params, Hash]; + + export type IfInline = [ + op: SexpOpcodes.IfInline, + condition: Expression, + truthyValue: Expression, + falsyValue: Option + ]; + + export type Not = [op: SexpOpcodes.Not, value: Expression]; } export type Expression = Expressions.Expression; @@ -364,13 +376,6 @@ export namespace Statements { inverse: Option ]; - export type Unless = [ - op: SexpOpcodes.Unless, - condition: Expression, - block: SerializedInlineBlock, - inverse: Option - ]; - export type Each = [ op: SexpOpcodes.Each, condition: Expression, @@ -425,7 +430,6 @@ export namespace Statements { | Debugger | InElement | If - | Unless | Each | With | Let diff --git a/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts b/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts index ea5fa54ea0..445c2f0406 100644 --- a/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts +++ b/packages/@glimmer/interfaces/lib/vm-opcodes.d.ts @@ -105,4 +105,6 @@ export const enum Op { DynamicContentType = 106, DynamicHelper = 107, DynamicModifier = 108, + IfInline = 109, + Not = 110, } diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts b/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts index ad8a63c4bc..32d57c6e1f 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts @@ -105,3 +105,16 @@ EXPRESSIONS.add(SexpOpcodes.HasBlockParams, (op, [, block]) => { op(Op.CompileBlock); op(Op.HasBlockParams); }); + +EXPRESSIONS.add(SexpOpcodes.IfInline, (op, [, condition, truthy, falsy]) => { + // Push in reverse order + expr(op, falsy); + expr(op, truthy); + expr(op, condition); + op(Op.IfInline); +}); + +EXPRESSIONS.add(SexpOpcodes.Not, (op, [, value]) => { + expr(op, value); + op(Op.Not); +}); diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts b/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts index cd67f34e7d..79bbd5f176 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts @@ -303,34 +303,11 @@ STATEMENTS.add(SexpOpcodes.If, (op, [, condition, block, inverse]) => InvokeStaticBlock(op, block); }, - () => { - if (inverse) { - InvokeStaticBlock(op, inverse); - } - } - ) -); - -STATEMENTS.add(SexpOpcodes.Unless, (op, [, condition, block, inverse]) => - ReplayableIf( - op, - - () => { - expr(op, condition); - op(Op.ToBoolean); - - return 1; - }, - - () => { - if (inverse) { - InvokeStaticBlock(op, inverse); - } - }, - - () => { - InvokeStaticBlock(op, block); - } + inverse + ? () => { + InvokeStaticBlock(op, inverse); + } + : undefined ) ); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts index b68e93a748..079429e599 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/expressions.ts @@ -15,11 +15,13 @@ import { TRUE_REFERENCE, FALSE_REFERENCE, valueForRef, + createComputeRef, } from '@glimmer/reference'; import { $v0 } from '@glimmer/vm'; import { APPEND_OPCODES } from '../../opcodes'; import { createConcatRef } from '../expressions/concat'; import { assert, debugToString, decodeHandle } from '@glimmer/util'; +import { toBool } from '@glimmer/global-context'; import { check, CheckOption, @@ -238,3 +240,29 @@ APPEND_OPCODES.add(Op.Concat, (vm, { op1: count }) => { vm.stack.pushJs(createConcatRef(out)); }); + +APPEND_OPCODES.add(Op.IfInline, (vm) => { + let condition = check(vm.stack.popJs(), CheckReference); + let truthy = check(vm.stack.popJs(), CheckReference); + let falsy = check(vm.stack.popJs(), CheckReference); + + vm.stack.pushJs( + createComputeRef(() => { + if (toBool(valueForRef(condition)) === true) { + return valueForRef(truthy); + } else { + return valueForRef(falsy); + } + }) + ); +}); + +APPEND_OPCODES.add(Op.Not, (vm) => { + let ref = check(vm.stack.popJs(), CheckReference); + + vm.stack.pushJs( + createComputeRef(() => { + return !toBool(valueForRef(ref)); + }) + ); +}); diff --git a/packages/@glimmer/vm/lib/opcodes.toml b/packages/@glimmer/vm/lib/opcodes.toml index 42451677fc..4b3cb63641 100644 --- a/packages/@glimmer/vm/lib/opcodes.toml +++ b/packages/@glimmer/vm/lib/opcodes.toml @@ -184,6 +184,28 @@ operand-stack = [ ["Reference"] ] +[syscall.ifinline] + +format = ["IfInline", "count:u32"] +operation = """ +Inline if expression +""" +operand-stack = [ + ["Reference", "Reference", "Reference"], + ["Reference"] +] + +[syscall.not] + +format = ["Not", "count:u32"] +operation = """ +Inline not expression +""" +operand-stack = [ + ["Reference"], + ["Reference"] +] + [syscall.rconstload] format = ["Constant", "constant:unknown"]