@glimmer/compiler
Version:
1,487 lines (1,295 loc) • 103 kB
JavaScript
import { assertNever, dict, values } from "@glimmer/util";
import { VariableResolutionContext, SexpOpcodes, WellKnownTagNames, WellKnownAttrNames } from "@glimmer/wire-format";
import { node, KEYWORDS_TYPES, ASTv2, isKeyword, generateSyntaxError, SourceSlice, src, maybeLoc, loc, normalize as normalize$1 } from "@glimmer/syntax";
/// Builder ///
const CURRIED_COMPONENT = 0, CURRIED_HELPER = 1, CURRIED_MODIFIER = 2, NS_XMLNS = "http://www.w3.org/2000/xmlns/";
function isPresentArray(list) {
return !!list && list.length > 0;
}
function mapPresentArray(list, mapper) {
if (null === list) return null;
let out = [];
for (let item of list) out.push(mapper(item));
return out;
}
function normalizeStatement(statement) {
return Array.isArray(statement) ? function(statement) {
if (!Array.isArray(statement)) return !1;
const name = statement[0];
if ("number" == typeof name) switch (name) {
case 0:
case 5:
case 6:
case 7:
case 8:
return !0;
default:
return !1;
}
return "(" === name[0];
}(statement) ? normalizeAppendExpression(statement) : function(statement) {
if (Array.isArray(statement) && "string" == typeof statement[0]) switch (statement[0][0]) {
case "(":
case "#":
case "<":
case "!":
return !0;
default:
return !1;
}
return !1;
}(statement) ? function(statement) {
const name = statement[0];
switch (name[0]) {
case "(":
{
let params = null, hash = null;
return 3 === statement.length ? (params = normalizeParams(statement[1]), hash = normalizeHash(statement[2])) : 2 === statement.length && (Array.isArray(statement[1]) ? params = normalizeParams(statement[1]) : hash = normalizeHash(statement[1])),
{
kind: "Call",
head: normalizeCallHead(name),
params: params,
hash: hash,
trusted: !1
};
}
case "#":
{
const {head: path, params: params, hash: hash, blocks: blocks, blockParams: blockParams} = normalizeBuilderBlockStatement(statement);
return {
kind: "Block",
head: path,
params: params,
hash: hash,
blocks: blocks,
blockParams: blockParams
};
}
case "!":
{
const name = statement[0].slice(1), {params: params, hash: hash, blocks: blocks, blockParams: blockParams} = normalizeBuilderBlockStatement(statement);
return {
kind: "Keyword",
name: name,
params: params,
hash: hash,
blocks: blocks,
blockParams: blockParams
};
}
case "<":
{
let attrs = dict(), block = [];
return 3 === statement.length ? (attrs = normalizeAttrs(statement[1]), block = normalizeBlock(statement[2])) : 2 === statement.length && (Array.isArray(statement[1]) ? block = normalizeBlock(statement[1]) : attrs = normalizeAttrs(statement[1])),
{
kind: "Element",
name: extractElement(name),
attrs: attrs,
block: block
};
}
default:
throw new Error(`Unreachable ${JSON.stringify(statement)} in normalizeSugaryArrayStatement`);
}
}(statement) : function(statement) {
switch (statement[0]) {
case 0:
return {
kind: "Literal",
value: statement[1]
};
case 2:
return normalizeAppendExpression(statement[1], statement[2]);
case 3:
return {
kind: "Modifier",
params: normalizeParams(statement[1]),
hash: normalizeHash(statement[2])
};
case 4:
return {
kind: "DynamicComponent",
expr: normalizeExpression(statement[1]),
hash: normalizeHash(statement[2]),
block: normalizeBlock(statement[3])
};
case 1:
return {
kind: "Comment",
value: statement[1]
};
}
}(statement) : "string" == typeof statement ? normalizeAppendHead(normalizeDottedPath(statement), !1) : void assertNever(statement);
}
function normalizeAppendHead(head, trusted) {
return "GetPath" === head.type ? {
kind: "AppendPath",
path: head,
trusted: trusted
} : {
kind: "AppendExpr",
expr: head,
trusted: trusted
};
}
function extractBlockHead(name) {
const result = /^(#|!)(.*)$/u.exec(name);
if (null === result) throw new Error("Unexpected missing # in block head");
return normalizeDottedPath(result[2]);
}
function normalizeCallHead(name) {
const result = /^\((.*)\)$/u.exec(name);
if (null === result) throw new Error("Unexpected missing () in call head");
return normalizeDottedPath(result[1]);
}
function normalizePath(head, tail = []) {
const pathHead = normalizePathHead(head);
return isPresentArray(tail) ? {
type: "GetPath",
path: {
head: pathHead,
tail: tail
}
} : {
type: "GetVar",
variable: pathHead
};
}
function normalizeDottedPath(whole) {
const {kind: kind, name: rest} = normalizePathHead(whole), [name, ...tail] = rest.split("."), variable = {
kind: kind,
name: name,
mode: "loose"
};
return isPresentArray(tail) ? {
type: "GetPath",
path: {
head: variable,
tail: tail
}
} : {
type: "GetVar",
variable: variable
};
}
function normalizePathHead(whole) {
let kind, name;
if (/^this(?:\.|$)/u.test(whole)) return {
kind: "This",
name: whole,
mode: "loose"
};
switch (whole[0]) {
case "^":
kind = "Free", name = whole.slice(1);
break;
case "@":
kind = "Arg", name = whole.slice(1);
break;
case "&":
kind = "Block", name = whole.slice(1);
break;
default:
kind = "Local", name = whole;
}
return {
kind: kind,
name: name,
mode: "loose"
};
}
function normalizeBuilderBlockStatement(statement) {
const head = statement[0];
let blocks = dict(), params = null, hash = null, blockParams = null;
return 2 === statement.length ? blocks = normalizeBlocks(statement[1]) : 3 === statement.length ? (Array.isArray(statement[1]) ? params = normalizeParams(statement[1]) : ({hash: hash, blockParams: blockParams} = normalizeBlockHash(statement[1])),
blocks = normalizeBlocks(statement[2])) : (params = normalizeParams(statement[1]),
({hash: hash, blockParams: blockParams} = normalizeBlockHash(statement[2])), blocks = normalizeBlocks(statement[3])),
{
head: extractBlockHead(head),
params: params,
hash: hash,
blockParams: blockParams,
blocks: blocks
};
}
function normalizeBlockHash(hash) {
if (null === hash) return {
hash: null,
blockParams: null
};
let out = null, blockParams = null;
return function(dict, callback) {
Object.keys(dict).forEach((key => {
const value = dict[key];
callback(key, value);
}));
}(hash, ((key, value) => {
"as" === key ? blockParams = Array.isArray(value) ? value : [ value ] : (out = out || dict(),
out[key] = normalizeExpression(value));
})), {
hash: out,
blockParams: blockParams
};
}
function normalizeBlocks(value) {
return Array.isArray(value) ? {
default: normalizeBlock(value)
} : mapObject(value, normalizeBlock);
}
function normalizeBlock(block) {
return block.map((s => normalizeStatement(s)));
}
function normalizeAttrs(attrs) {
return mapObject(attrs, (a => {
return (attr = a, "splat" === attr ? {
expr: "Splat",
trusted: !1
} : {
expr: normalizeExpression(attr),
trusted: !1
}).expr;
var attr;
}));
}
function mapObject(object, mapper) {
const out = dict();
return Object.keys(object).forEach((k => {
out[k] = mapper(object[k], k);
})), out;
}
function extractElement(input) {
const match = /^<([\d\-a-z][\d\-A-Za-z]*)>$/u.exec(input);
return match?.[1] ?? null;
}
function normalizeAppendExpression(expression, forceTrusted = !1) {
if (null == expression) return {
expr: {
type: "Literal",
value: expression
},
kind: "AppendExpr",
trusted: !1
};
if (Array.isArray(expression)) switch (expression[0]) {
case 0:
return {
expr: {
type: "Literal",
value: expression[1]
},
kind: "AppendExpr",
trusted: !1
};
case 5:
return normalizeAppendHead(normalizePath(expression[1], expression[2]), forceTrusted);
case 6:
return {
expr: {
type: "Concat",
params: normalizeParams(expression.slice(1))
},
kind: "AppendExpr",
trusted: forceTrusted
};
case 7:
return {
expr: {
type: "HasBlock",
name: expression[1]
},
kind: "AppendExpr",
trusted: forceTrusted
};
case 8:
return {
expr: {
type: "HasBlockParams",
name: expression[1]
},
kind: "AppendExpr",
trusted: forceTrusted
};
default:
if (isBuilderCallExpression(expression)) return {
expr: normalizeCallExpression(expression),
kind: "AppendExpr",
trusted: forceTrusted
};
throw new Error(`Unexpected array in expression position (wasn't a tuple expression and ${expression[0]} isn't wrapped in parens, so it isn't a call): ${JSON.stringify(expression)}`);
} else if ("object" != typeof expression) switch (typeof expression) {
case "string":
return normalizeAppendHead(normalizeDottedPath(expression), forceTrusted);
case "boolean":
case "number":
return {
expr: {
type: "Literal",
value: expression
},
kind: "AppendExpr",
trusted: !0
};
default:
assertNever(expression);
} else assertNever(expression);
}
function normalizeExpression(expression) {
if (null == expression) return {
type: "Literal",
value: expression
};
if (Array.isArray(expression)) switch (expression[0]) {
case 0:
return {
type: "Literal",
value: expression[1]
};
case 5:
return normalizePath(expression[1], expression[2]);
case 6:
return {
type: "Concat",
params: normalizeParams(expression.slice(1))
};
case 7:
return {
type: "HasBlock",
name: expression[1]
};
case 8:
return {
type: "HasBlockParams",
name: expression[1]
};
default:
if (isBuilderCallExpression(expression)) return normalizeCallExpression(expression);
throw new Error(`Unexpected array in expression position (wasn't a tuple expression and ${expression[0]} isn't wrapped in parens, so it isn't a call): ${JSON.stringify(expression)}`);
} else if ("object" != typeof expression) switch (typeof expression) {
case "string":
return normalizeDottedPath(expression);
case "boolean":
case "number":
return {
type: "Literal",
value: expression
};
default:
assertNever(expression);
} else assertNever(expression);
}
function isBuilderCallExpression(value) {
return "string" == typeof value[0] && "(" === value[0][0];
}
function normalizeParams(input) {
return input.map(normalizeExpression);
}
function normalizeHash(input) {
return null === input ? null : mapObject(input, normalizeExpression);
}
function normalizeCallExpression(expr) {
switch (expr.length) {
case 1:
return {
type: "Call",
head: normalizeCallHead(expr[0]),
params: null,
hash: null
};
case 2:
return Array.isArray(expr[1]) ? {
type: "Call",
head: normalizeCallHead(expr[0]),
params: normalizeParams(expr[1]),
hash: null
} : {
type: "Call",
head: normalizeCallHead(expr[0]),
params: null,
hash: normalizeHash(expr[1])
};
case 3:
return {
type: "Call",
head: normalizeCallHead(expr[0]),
params: normalizeParams(expr[1]),
hash: normalizeHash(expr[2])
};
}
}
class ProgramSymbols {
toSymbols() {
return this._symbols.slice(1);
}
toUpvars() {
return this._freeVariables;
}
freeVar(name) {
return addString(this._freeVariables, name);
}
block(name) {
return this.symbol(name);
}
arg(name) {
return addString(this._symbols, name);
}
local(name) {
throw new Error(`No local ${name} was found. Maybe you meant ^${name} for upvar, or !${name} for keyword?`);
}
this() {
return 0;
}
hasLocal(_name) {
return !1;
}
// any symbol
symbol(name) {
return addString(this._symbols, name);
}
child(locals) {
return new LocalSymbols(this, locals);
}
constructor() {
this._freeVariables = [], this._symbols = [ "this" ], this.top = this;
}
}
class LocalSymbols {
constructor(parent, locals) {
this.parent = parent, this.locals = dict();
for (let local of locals) this.locals[local] = parent.top.symbol(local);
}
get paramSymbols() {
return values(this.locals);
}
get top() {
return this.parent.top;
}
freeVar(name) {
return this.parent.freeVar(name);
}
arg(name) {
return this.parent.arg(name);
}
block(name) {
return this.parent.block(name);
}
local(name) {
return name in this.locals ? this.locals[name] : this.parent.local(name);
}
this() {
return this.parent.this();
}
hasLocal(name) {
return name in this.locals || this.parent.hasLocal(name);
}
child(locals) {
return new LocalSymbols(this, locals);
}
}
function addString(array, item) {
let index = array.indexOf(item);
return -1 === index ? (index = array.length, array.push(item), index) : index;
}
function unimpl(message) {
return new Error(`unimplemented ${message}`);
}
function buildStatements(statements, symbols) {
let out = [];
return statements.forEach((s => out.push(...buildStatement(normalizeStatement(s), symbols)))),
out;
}
function buildNormalizedStatements(statements, symbols) {
let out = [];
return statements.forEach((s => out.push(...buildStatement(s, symbols)))), out;
}
function buildStatement(normalized, symbols = new ProgramSymbols) {
switch (normalized.kind) {
case "AppendPath":
return [ [ normalized.trusted ? SexpOpcodes.TrustingAppend : SexpOpcodes.Append, buildGetPath(normalized.path, symbols) ] ];
case "AppendExpr":
return [ [ normalized.trusted ? SexpOpcodes.TrustingAppend : SexpOpcodes.Append, buildExpression(normalized.expr, normalized.trusted ? "TrustedAppend" : "Append", symbols) ] ];
case "Call":
{
let {head: path, params: params, hash: hash, trusted: trusted} = normalized, builtParams = params ? buildParams(params, symbols) : null, builtHash = hash ? buildHash(hash, symbols) : null, builtExpr = buildCallHead(path, trusted ? VariableResolutionContext.ResolveAsHelperHead : VariableResolutionContext.ResolveAsComponentOrHelperHead, symbols);
return [ [ trusted ? SexpOpcodes.TrustingAppend : SexpOpcodes.Append, [ SexpOpcodes.Call, builtExpr, builtParams, builtHash ] ] ];
}
case "Literal":
return [ [ SexpOpcodes.Append, normalized.value ] ];
case "Comment":
return [ [ SexpOpcodes.Comment, normalized.value ] ];
case "Block":
{
let blocks = function(blocks, blockParams, parent) {
let keys = [], values = [];
for (const [name, block] of Object.entries(blocks)) if (keys.push(name), "default" === name) {
let symbols = parent.child(blockParams || []);
values.push(buildBlock(block, symbols, symbols.paramSymbols));
} else values.push(buildBlock(block, parent, []));
return [ keys, values ];
}(normalized.blocks, normalized.blockParams, symbols), hash = buildHash(normalized.hash, symbols), params = buildParams(normalized.params, symbols), path = buildCallHead(normalized.head, VariableResolutionContext.ResolveAsComponentHead, symbols);
return [ [ SexpOpcodes.Block, path, params, hash, blocks ] ];
}
case "Keyword":
return [ buildKeyword(normalized, symbols) ];
case "Element":
return function({name: name, attrs: attrs, block: block}, symbols) {
let out = [ hasSplat(attrs) ? [ SexpOpcodes.OpenElementWithSplat, name ] : [ SexpOpcodes.OpenElement, name ] ];
if (attrs) {
let {params: params} = function(attrs, symbols) {
let params = [], keys = [], values = [];
for (const [key, value] of Object.entries(attrs)) "Splat" === value ? params.push([ SexpOpcodes.AttrSplat, symbols.block("&attrs") ]) : "@" === key[0] ? (keys.push(key),
values.push(buildExpression(value, "Strict", symbols))) : params.push(...buildAttributeValue(key, value, // TODO: extract namespace from key
extractNamespace(key), symbols));
return {
params: params,
args: isPresentArray(keys) && isPresentArray(values) ? [ keys, values ] : null
};
}(attrs, symbols);
out.push(...params);
}
return out.push([ SexpOpcodes.FlushElement ]), Array.isArray(block) && block.forEach((s => out.push(...buildStatement(s, symbols)))),
out.push([ SexpOpcodes.CloseElement ]), out;
}(normalized, symbols);
case "Modifier":
throw unimpl("modifier");
case "DynamicComponent":
throw unimpl("dynamic component");
default:
assertNever(normalized);
}
}
function s(arr, ...interpolated) {
return [ 0, arr.reduce((// eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme
(result, string, i) => result + `${string}${interpolated[i] ? String(interpolated[i]) : ""}`), "") ];
}
function c(arr, ...interpolated) {
return [ 1, arr.reduce((// eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme
(result, string, i) => result + `${string}${interpolated[i] ? String(interpolated[i]) : ""}`), "") ];
}
function unicode(charCode) {
return String.fromCharCode(parseInt(charCode, 16));
}
const NEWLINE = "\n";
function buildKeyword(normalized, symbols) {
let {name: name} = normalized, params = buildParams(normalized.params, symbols), childSymbols = symbols.child(normalized.blockParams || []), block = buildBlock(normalized.blocks.default, childSymbols, childSymbols.paramSymbols), inverse = normalized.blocks.else ? buildBlock(normalized.blocks.else, symbols, []) : null;
switch (name) {
case "let":
return [ SexpOpcodes.Let, params, block ];
case "if":
return [ SexpOpcodes.If, params[0], block, inverse ];
case "each":
{
let keyExpr = normalized.hash ? normalized.hash.key : null, key = keyExpr ? buildExpression(keyExpr, "Strict", symbols) : null;
return [ SexpOpcodes.Each, params[0], key, block, inverse ];
}
default:
throw new Error("unimplemented keyword");
}
}
function hasSplat(attrs) {
return null !== attrs && Object.keys(attrs).some((a => "Splat" === attrs[a]));
}
function extractNamespace(name) {
if ("xmlns" === name) return NS_XMLNS;
let match = /^([^:]*):([^:]*)$/u.exec(name);
if (null === match) return null;
switch (match[1]) {
case "xlink":
return "http://www.w3.org/1999/xlink";
case "xml":
return "http://www.w3.org/XML/1998/namespace";
case "xmlns":
return NS_XMLNS;
}
return null;
}
function buildAttributeValue(name, value, namespace, symbols) {
if ("Literal" === value.type) {
let val = value.value;
if (!1 === val) return [];
if (!0 === val) return [ [ SexpOpcodes.StaticAttr, name, "", namespace ?? void 0 ] ];
if ("string" == typeof val) return [ [ SexpOpcodes.StaticAttr, name, val, namespace ?? void 0 ] ];
throw new Error(`Unexpected/unimplemented literal attribute ${JSON.stringify(val)}`);
}
return [ [ SexpOpcodes.DynamicAttr, name, buildExpression(value, "AttrValue", symbols), namespace ?? void 0 ] ];
}
function varContext(context, bare) {
switch (context) {
case "Append":
return bare ? "AppendBare" : "AppendInvoke";
case "TrustedAppend":
return bare ? "TrustedAppendBare" : "TrustedAppendInvoke";
case "AttrValue":
return bare ? "AttrValueBare" : "AttrValueInvoke";
default:
return context;
}
}
function buildExpression(expr, context, symbols) {
switch (expr.type) {
case "GetPath":
return buildGetPath(expr, symbols);
case "GetVar":
return buildVar(expr.variable, varContext(context, !0), symbols);
case "Concat":
return [ SexpOpcodes.Concat, buildConcat(expr.params, symbols) ];
case "Call":
{
let builtParams = buildParams(expr.params, symbols), builtHash = buildHash(expr.hash, symbols), builtExpr = buildCallHead(expr.head, "Strict" === context ? "SubExpression" : varContext(context, !1), symbols);
return [ SexpOpcodes.Call, builtExpr, builtParams, builtHash ];
}
case "HasBlock":
return [ SexpOpcodes.HasBlock, buildVar({
kind: "Block",
name: expr.name
}, VariableResolutionContext.Strict, symbols) ];
case "HasBlockParams":
return [ SexpOpcodes.HasBlockParams, buildVar({
kind: "Block",
name: expr.name
}, VariableResolutionContext.Strict, symbols) ];
case "Literal":
return void 0 === expr.value ? [ SexpOpcodes.Undefined ] : expr.value;
default:
assertNever(expr);
}
}
function buildCallHead(callHead, context, symbols) {
return "GetVar" === callHead.type ? buildVar(callHead.variable, context, symbols) : buildGetPath(callHead, symbols);
}
function buildGetPath(head, symbols) {
return buildVar(head.path.head, VariableResolutionContext.Strict, symbols, head.path.tail);
}
function buildVar(head, context, symbols, path) {
let sym, op = SexpOpcodes.GetSymbol;
return "Free" === head.kind ? (op = "Strict" === context ? SexpOpcodes.GetStrictKeyword : "AppendBare" === context || "AppendInvoke" === context ? SexpOpcodes.GetFreeAsComponentOrHelperHead : "TrustedAppendBare" === context || "TrustedAppendInvoke" === context || "AttrValueBare" === context || "AttrValueInvoke" === context || "SubExpression" === context ? SexpOpcodes.GetFreeAsHelperHead : function(context) {
switch (context) {
case VariableResolutionContext.Strict:
return SexpOpcodes.GetStrictKeyword;
case VariableResolutionContext.ResolveAsComponentOrHelperHead:
return SexpOpcodes.GetFreeAsComponentOrHelperHead;
case VariableResolutionContext.ResolveAsHelperHead:
return SexpOpcodes.GetFreeAsHelperHead;
case VariableResolutionContext.ResolveAsModifierHead:
return SexpOpcodes.GetFreeAsModifierHead;
case VariableResolutionContext.ResolveAsComponentHead:
return SexpOpcodes.GetFreeAsComponentHead;
default:
return;
}
}(context), sym = symbols.freeVar(head.name)) : (op = SexpOpcodes.GetSymbol, sym = function(kind, symbols, name) {
switch (kind) {
case "Arg":
return symbols.arg(name);
case "Block":
return symbols.block(name);
case "Local":
return symbols.local(name);
case "This":
return symbols.this();
default:
return;
}
}(head.kind, symbols, head.name)), void 0 === path || 0 === path.length ? [ op, sym ] : (SexpOpcodes.GetStrictKeyword,
[ op, sym, path ]);
}
function buildParams(exprs, symbols) {
return null !== exprs && isPresentArray(exprs) ? exprs.map((e => buildExpression(e, "Strict", symbols))) : null;
}
function buildConcat(exprs, symbols) {
return exprs.map((e => buildExpression(e, "AttrValue", symbols)));
}
function buildHash(exprs, symbols) {
if (null === exprs) return null;
let out = [ [], [] ];
for (const [key, value] of Object.entries(exprs)) out[0].push(key), out[1].push(buildExpression(value, "Strict", symbols));
return out;
}
function buildBlock(block, symbols, locals = []) {
return [ buildNormalizedStatements(block, symbols), locals ];
}
class Template extends(node("Template").fields()){}
class InElement extends(node("InElement").fields()){}
class Not extends(node("Not").fields()){}
class If extends(node("If").fields()){}
class IfInline extends(node("IfInline").fields()){}
class Each extends(node("Each").fields()){}
class Let extends(node("Let").fields()){}
class WithDynamicVars extends(node("WithDynamicVars").fields()){}
class GetDynamicVar extends(node("GetDynamicVar").fields()){}
class Log extends(node("Log").fields()){}
class InvokeComponent extends(node("InvokeComponent").fields()){}
class NamedBlocks extends(node("NamedBlocks").fields()){}
class NamedBlock extends(node("NamedBlock").fields()){}
class AppendTrustedHTML extends(node("AppendTrustedHTML").fields()){}
class AppendTextNode extends(node("AppendTextNode").fields()){}
class AppendComment extends(node("AppendComment").fields()){}
class Component extends(node("Component").fields()){}
class StaticAttr extends(node("StaticAttr").fields()){}
class DynamicAttr extends(node("DynamicAttr").fields()){}
class SimpleElement extends(node("SimpleElement").fields()){}
class ElementParameters extends(node("ElementParameters").fields()){}
class Yield extends(node("Yield").fields()){}
class Debugger extends(node("Debugger").fields()){}
class CallExpression extends(node("CallExpression").fields()){}
class Modifier extends(node("Modifier").fields()){}
class InvokeBlock extends(node("InvokeBlock").fields()){}
class SplatAttr extends(node("SplatAttr").fields()){}
class PathExpression extends(node("PathExpression").fields()){}
class Missing extends(node("Missing").fields()){}
class InterpolateExpression extends(node("InterpolateExpression").fields()){}
class HasBlock extends(node("HasBlock").fields()){}
class HasBlockParams extends(node("HasBlockParams").fields()){}
class Curry extends(node("Curry").fields()){}
class Positional extends(node("Positional").fields()){}
class NamedArguments extends(node("NamedArguments").fields()){}
class NamedArgument extends(node("NamedArgument").fields()){}
class Args extends(node("Args").fields()){}
class Tail extends(node("Tail").fields()){}
class PresentList {
constructor(list) {
this.list = list;
}
toArray() {
return this.list;
}
map(callback) {
let result = mapPresentArray(this.list, callback);
return new PresentList(result);
}
filter(predicate) {
let out = [];
for (let item of this.list) predicate(item) && out.push(item);
return OptionalList(out);
}
toPresentArray() {
return this.list;
}
into({ifPresent: ifPresent}) {
return ifPresent(this);
}
}
class EmptyList {
map(_callback) {
return new EmptyList;
}
filter(_predicate) {
return new EmptyList;
}
toArray() {
return this.list;
}
toPresentArray() {
return null;
}
into({ifEmpty: ifEmpty}) {
return ifEmpty();
}
constructor() {
this.list = [];
}
}
// export type OptionalList<T> = PresentList<T> | EmptyList<T>;
function OptionalList(value) {
return isPresentArray(value) ? new PresentList(value) : new EmptyList;
}
class ResultImpl {
static all(...results) {
let out = [];
for (let result of results) {
if (result.isErr) return result.cast();
out.push(result.value);
}
return Ok(out);
}
}
const Result = ResultImpl;
class OkImpl extends ResultImpl {
constructor(value) {
super(), this.value = value, this.isOk = !0, this.isErr = !1;
}
expect(_message) {
return this.value;
}
ifOk(callback) {
return callback(this.value), this;
}
andThen(callback) {
return callback(this.value);
}
mapOk(callback) {
return Ok(callback(this.value));
}
ifErr(_callback) {
return this;
}
mapErr(_callback) {
return this;
}
}
class ErrImpl extends ResultImpl {
constructor(reason) {
super(), this.reason = reason, this.isOk = !1, this.isErr = !0;
}
expect(message) {
throw new Error(message || "expected an Ok, got Err");
}
andThen(_callback) {
return this.cast();
}
mapOk(_callback) {
return this.cast();
}
ifOk(_callback) {
return this;
}
mapErr(callback) {
return Err(callback(this.reason));
}
ifErr(callback) {
return callback(this.reason), this;
}
cast() {
return this;
}
}
function Ok(value) {
return new OkImpl(value);
}
function Err(reason) {
return new ErrImpl(reason);
}
class ResultArray {
constructor(items = []) {
this.items = items;
}
add(item) {
this.items.push(item);
}
toArray() {
let err = this.items.filter((item => item instanceof ErrImpl))[0];
return void 0 !== err ? err.cast() : Ok(this.items.map((item => item.value)));
}
toOptionalList() {
return this.toArray().mapOk((arr => OptionalList(arr)));
}
}
function convertPathToCallIfKeyword(path) {
return "Path" === path.type && "Free" === path.ref.type && path.ref.name in KEYWORDS_TYPES ? new ASTv2.CallExpression({
callee: path,
args: ASTv2.Args.empty(path.loc),
loc: path.loc
}) : path;
}
const VISIT_EXPRS = new class {
visit(node, state) {
switch (node.type) {
case "Literal":
return Ok(this.Literal(node));
case "Keyword":
return Ok(this.Keyword(node));
case "Interpolate":
return this.Interpolate(node, state);
case "Path":
return this.PathExpression(node);
case "Call":
{
let translated = CALL_KEYWORDS.translate(node, state);
return null !== translated ? translated : this.CallExpression(node, state);
}
}
}
visitList(nodes, state) {
return new ResultArray(nodes.map((e => VISIT_EXPRS.visit(e, state)))).toOptionalList();
}
/**
* Normalize paths into `hir.Path` or a `hir.Expr` that corresponds to the ref.
*
* TODO since keywords don't support tails anyway, distinguish PathExpression from
* VariableReference in ASTv2.
*/ PathExpression(path) {
let ref = this.VariableReference(path.ref), {tail: tail} = path;
if (isPresentArray(tail)) {
let tailLoc = tail[0].loc.extend((list = tail, 0 === list.length ? void 0 : list[list.length - 1]).loc);
return Ok(new PathExpression({
loc: path.loc,
head: ref,
tail: new Tail({
loc: tailLoc,
members: tail
})
}));
}
return Ok(ref);
var list;
}
VariableReference(ref) {
return ref;
}
Literal(literal) {
return literal;
}
Keyword(keyword) {
return keyword;
}
Interpolate(expr, state) {
let parts = expr.parts.map(convertPathToCallIfKeyword);
return VISIT_EXPRS.visitList(parts, state).mapOk((parts => new InterpolateExpression({
loc: expr.loc,
parts: parts
})));
}
CallExpression(expr, state) {
if ("Call" === expr.callee.type) throw new Error("unimplemented: subexpression at the head of a subexpression");
return Result.all(VISIT_EXPRS.visit(expr.callee, state), VISIT_EXPRS.Args(expr.args, state)).mapOk((([callee, args]) => new CallExpression({
loc: expr.loc,
callee: callee,
args: args
})));
}
Args({positional: positional, named: named, loc: loc}, state) {
return Result.all(this.Positional(positional, state), this.NamedArguments(named, state)).mapOk((([positional, named]) => new Args({
loc: loc,
positional: positional,
named: named
})));
}
Positional(positional, state) {
return VISIT_EXPRS.visitList(positional.exprs, state).mapOk((list => new Positional({
loc: positional.loc,
list: list
})));
}
NamedArguments(named, state) {
let pairs = named.entries.map((arg => {
let value = convertPathToCallIfKeyword(arg.value);
return VISIT_EXPRS.visit(value, state).mapOk((value => new NamedArgument({
loc: arg.loc,
key: arg.name,
value: value
})));
}));
return new ResultArray(pairs).toOptionalList().mapOk((pairs => new NamedArguments({
loc: named.loc,
entries: pairs
})));
}
};
class KeywordImpl {
constructor(keyword, type, delegate) {
this.keyword = keyword, this.delegate = delegate;
let nodes = new Set;
for (let nodeType of KEYWORD_NODES[type]) nodes.add(nodeType);
this.types = nodes;
}
match(node) {
if (!this.types.has(node.type)) return !1;
let path = getCalleeExpression(node);
return null !== path && "Path" === path.type && "Free" === path.ref.type && path.ref.name === this.keyword;
}
translate(node, state) {
if (this.match(node)) {
let path = getCalleeExpression(node);
return null !== path && "Path" === path.type && path.tail.length > 0 ? Err(generateSyntaxError(`The \`${this.keyword}\` keyword was used incorrectly. It was used as \`${path.loc.asString()}\`, but it cannot be used with additional path segments. \n\nError caused by`, node.loc)) : this.delegate.assert(node, state).andThen((param => this.delegate.translate({
node: node,
state: state
}, param)));
}
return null;
}
}
const KEYWORD_NODES = {
Call: [ "Call" ],
Block: [ "InvokeBlock" ],
Append: [ "AppendContent" ],
Modifier: [ "ElementModifier" ]
};
function getCalleeExpression(node) {
switch (node.type) {
// This covers the inside of attributes and expressions, as well as the callee
// of call nodes
case "Path":
return node;
case "AppendContent":
return getCalleeExpression(node.value);
case "Call":
case "InvokeBlock":
case "ElementModifier":
return node.callee;
default:
return null;
}
}
class Keywords {
constructor(type) {
this._keywords = [], this._type = type;
}
kw(name, delegate) {
return this._keywords.push(new KeywordImpl(name, this._type, delegate)), this;
}
translate(node, state) {
for (let keyword of this._keywords) {
let result = keyword.translate(node, state);
if (null !== result) return result;
}
let path = getCalleeExpression(node);
if (path && "Path" === path.type && "Free" === path.ref.type && isKeyword(path.ref.name)) {
let {name: name} = path.ref, usedType = this._type, validTypes = KEYWORDS_TYPES[name];
if (!validTypes.includes(usedType)) return Err(generateSyntaxError(`The \`${name}\` keyword was used incorrectly. It was used as ${typesToReadableName[usedType]}, but its valid usages are:\n\n${function(name, types) {
return types.map((type => {
switch (type) {
case "Append":
return `- As an append statement, as in: {{${name}}}`;
case "Block":
return `- As a block statement, as in: {{#${name}}}{{/${name}}}`;
case "Call":
return `- As an expression, as in: (${name})`;
case "Modifier":
return `- As a modifier, as in: <div {{${name}}}></div>`;
default:
return;
}
})).join("\n\n");
}
/**
* This function builds keyword definitions for a particular type of AST node (`KeywordType`).
*
* You can build keyword definitions for:
*
* - `Expr`: A `SubExpression` or `PathExpression`
* - `Block`: A `BlockStatement`
* - A `BlockStatement` is a keyword candidate if its head is a
* `PathExpression`
* - `Append`: An `AppendStatement`
*
* A node is a keyword candidate if:
*
* - A `PathExpression` is a keyword candidate if it has no tail, and its
* head expression is a `LocalVarHead` or `FreeVarHead` whose name is
* the keyword's name.
* - A `SubExpression`, `AppendStatement`, or `BlockStatement` is a keyword
* candidate if its head is a keyword candidate.
*
* The keyword infrastructure guarantees that:
*
* - If a node is not a keyword candidate, it is never passed to any keyword's
* `assert` method.
* - If a node is not the `KeywordType` for a particular keyword, it will not
* be passed to the keyword's `assert` method.
*
* `Expr` keywords are used in expression positions and should return HIR
* expressions. `Block` and `Append` keywords are used in statement
* positions and should return HIR statements.
*
* A keyword definition has two parts:
*
* - `match`, which determines whether an AST node matches the keyword, and can
* optionally return some information extracted from the AST node.
* - `translate`, which takes a matching AST node as well as the extracted
* information and returns an appropriate HIR instruction.
*
* # Example
*
* This keyword:
*
* - turns `(hello)` into `"hello"`
* - as long as `hello` is not in scope
* - makes it an error to pass any arguments (such as `(hello world)`)
*
* ```ts
* keywords('SubExpr').kw('hello', {
* assert(node: ExprKeywordNode): Result<void> | false {
* // we don't want to transform `hello` as a `PathExpression`
* if (node.type !== 'SubExpression') {
* return false;
* }
*
* // node.head would be `LocalVarHead` if `hello` was in scope
* if (node.head.type !== 'FreeVarHead') {
* return false;
* }
*
* if (node.params.length || node.hash) {
* return Err(generateSyntaxError(`(hello) does not take any arguments`), node.loc);
* } else {
* return Ok();
* }
* },
*
* translate(node: ASTv2.SubExpression): hir.Expression {
* return ASTv2.builders.literal("hello", node.loc)
* }
* })
* ```
*
* The keyword infrastructure checks to make sure that the node is the right
* type before calling `assert`, so you only need to consider `SubExpression`
* and `PathExpression` here. It also checks to make sure that the node passed
* to `assert` has the keyword name in the right place.
*
* Note the important difference between returning `false` from `assert`,
* which just means that the node didn't match, and returning `Err`, which
* means that the node matched, but there was a keyword-specific syntax
* error.
*/ (name, validTypes)}\n\nError caused by`, node.loc));
}
return null;
}
}
const typesToReadableName = {
Append: "an append statement",
Block: "a block statement",
Call: "a call expression",
Modifier: "a modifier"
};
function keywords(type) {
return new Keywords(type);
}
function toAppend({assert: assert, translate: translate}) {
return {
assert: assert,
translate: ({node: node, state: state}, value) => translate({
node: node,
state: state
}, value).mapOk((text => new AppendTextNode({
text: text,
loc: node.loc
})))
};
}
const CurriedTypeToReadableType = {
[CURRIED_COMPONENT]: "component",
[CURRIED_HELPER]: "helper",
[CURRIED_MODIFIER]: "modifier"
};
function assertCurryKeyword(curriedType) {
return (node, state) => {
let readableType = CurriedTypeToReadableType[curriedType], stringsAllowed = 0 === curriedType, {args: args} = node, definition = args.nth(0);
if (null === definition) return Err(generateSyntaxError(`(${readableType}) requires a ${readableType} definition or identifier as its first positional parameter, did not receive any parameters.`, args.loc));
if ("Literal" === definition.type) {
if (stringsAllowed && state.isStrict) return Err(generateSyntaxError(`(${readableType}) cannot resolve string values in strict mode templates`, node.loc));
if (!stringsAllowed) return Err(generateSyntaxError(`(${readableType}) cannot resolve string values, you must pass a ${readableType} definition directly`, node.loc));
}
return args = new ASTv2.Args({
positional: new ASTv2.PositionalArguments({
exprs: args.positional.exprs.slice(1),
loc: args.positional.loc
}),
named: args.named,
loc: args.loc
}), Ok({
definition: definition,
args: args
});
};
}
function translateCurryKeyword(curriedType) {
return ({node: node, state: state}, {definition: definition, args: args}) => {
let definitionResult = VISIT_EXPRS.visit(definition, state), argsResult = VISIT_EXPRS.Args(args, state);
return Result.all(definitionResult, argsResult).mapOk((([definition, args]) => new Curry({
loc: node.loc,
curriedType: curriedType,
definition: definition,
args: args
})));
};
}
function curryKeyword(curriedType) {
return {
assert: assertCurryKeyword(curriedType),
translate: translateCurryKeyword(curriedType)
};
}
const getDynamicVarKeyword = {
assert: function(node) {
let call = "AppendContent" === node.type ? node.value : node, named = "Call" === call.type ? call.args.named : null, positionals = "Call" === call.type ? call.args.positional : null;
if (named && !named.isEmpty()) return Err(generateSyntaxError("(-get-dynamic-vars) does not take any named arguments", node.loc));
let varName = positionals?.nth(0);
return varName ? positionals && positionals.size > 1 ? Err(generateSyntaxError("(-get-dynamic-vars) only receives one positional arg", node.loc)) : Ok(varName) : Err(generateSyntaxError("(-get-dynamic-vars) requires a var name to get", node.loc));
},
translate: function({node: node, state: state}, name) {
return VISIT_EXPRS.visit(name, state).mapOk((name => new GetDynamicVar({
name: name,
loc: node.loc
})));
}
};
function assertHasBlockKeyword(type) {
return node => {
let call = "AppendContent" === node.type ? node.value : node, named = "Call" === call.type ? call.args.named : null, positionals = "Call" === call.type ? call.args.positional : null;
if (named && !named.isEmpty()) return Err(generateSyntaxError(`(${type}) does not take any named arguments`, call.loc));
if (!positionals || positionals.isEmpty()) return Ok(SourceSlice.synthetic("default"));
if (1 === positionals.exprs.length) {
let positional = positionals.exprs[0];
return ASTv2.isLiteral(positional, "string") ? Ok(positional.toSlice()) : Err(generateSyntaxError(`(${type}) can only receive a string literal as its first argument`, call.loc));
}
return Err(generateSyntaxError(`(${type}) only takes a single positional argument`, call.loc));
};
}
function translateHasBlockKeyword(type) {
return ({node: node, state: {scope: scope}}, target) => Ok("has-block" === type ? new HasBlock({
loc: node.loc,
target: target,
symbol: scope.allocateBlock(target.chars)
}) : new HasBlockParams({
loc: node.loc,
target: target,
symbol: scope.allocateBlock(target.chars)
}));
}
function hasBlockKeyword(type) {
return {
assert: assertHasBlockKeyword(type),
translate: translateHasBlockKeyword(type)
};
}
function assertIfUnlessInlineKeyword(type) {
return originalNode => {
let inverted = "unless" === type, node = "AppendContent" === originalNode.type ? originalNode.value : originalNode, named = "Call" === node.type ? node.args.named : null, positional = "Call" === node.type ? 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), falsy = positional.nth(2);
return null === truthy ? 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)) : positional.size > 3 ? 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} parameters`, originalNode.loc)) : Ok({
condition: condition,
truthy: truthy,
falsy: falsy
});
};
}
function translateIfUnlessInlineKeyword(type) {
let inverted = "unless" === type;
return ({node: node, state: state}, {condition: condition, truthy: truthy, falsy: falsy}) => {
let conditionResult = VISIT_EXPRS.visit(condition, state), truthyResult = VISIT_EXPRS.visit(truthy, state), falsyResult = falsy ? VISIT_EXPRS.visit(falsy, state) : Ok(null);
return Result.all(conditionResult, truthyResult, falsyResult).mapOk((([condition, truthy, falsy]) => (inverted && (condition = new Not({
value: condition,
loc: node.loc
})), new IfInline({
loc: node.loc,
condition: condition,
truthy: truthy,
falsy: falsy
}))));
};
}
function ifUnlessInlineKeyword(type) {
return {
assert: assertIfUnlessInlineKeyword(type),
translate: translateIfUnlessInlineKeyword(type)
};
}
const logKeyword = {
assert: function(node) {
let {args: {named: named, positional: positional}} = node;
return named.isEmpty() ? Ok(positional) : Err(generateSyntaxError("(log) does not take any named arguments", node.loc));
},
translate: function({node: node, state: state}, positional) {
return VISIT_EXPRS.Positional(positional, state).mapOk((positional => new Log({
positional: positional,
loc: node.loc
})));
}
}, APPEND_KEYWORDS = keywords("Append").kw("has-block", toAppend(hasBlockKeyword("has-block"))).kw("has-block-params", toAppend(hasBlockKeyword("has-block-params"))).kw("-get-dynamic-var", toAppend(getDynamicVarKeyword)).kw("log", toAppend(logKeyword)).kw("if", toAppend(ifUnlessInlineKeyword("if"))).kw("unless", toAppend(ifUnlessInlineKeyword("unless"))).kw("yield", {
assert(node) {
let {args: args} = node;
if (args.named.isEmpty()) return Ok({
target: src.SourceS