UNPKG

@glimmer/compiler

Version:
1,487 lines (1,295 loc) 103 kB
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