@litexa/core
Version:
Litexa, a programming language for writing Alexa skills
604 lines (464 loc) • 17.3 kB
text/coffeescript
###
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
###
lib = module.exports.lib = {}
{ ParserError } = require("./errors.coffee").lib
operatorMap =
'+': '+'
'-': '-'
'*': '*'
'/': '/'
'==': '==='
'===': '==='
'!=': '!=='
'!==': '!=='
'<': '<'
'<=': '<='
'>': '>'
'>=': '>='
'else': 'else'
'expr': 'expr'
'regex': 'regex'
'and': '&&'
'&&': '&&'
'or': '||'
'||': '||'
'not': '!'
isStaticValue = (v) ->
switch typeof(v)
when 'string' then return true
when 'number' then return true
when 'boolean' then return true
when 'object' then return v.isStatic?()
return false
evaluateStaticValue = (v, context, location) ->
switch typeof(v)
when 'string'
if v[0] == '"' and v[v.length-1] == '"'
return v[1...v.length-1]
else
return v
when 'number' then return v
when 'boolean' then return v
when 'object'
unless v.evaluateStatic?
throw new ParserError location, "missing evaluateStatic for #{JSON.stringify(v)}"
try
return v.evaluateStatic(context)
catch err
throw new ParserError location, "Error in static evaluation: #{err}"
throw "don't know how to static evaluate #{JSON.stringify(v)}"
class lib.EvaluateExpression
constructor: (@expression) ->
toLambda: (output, indent, options) ->
output.push "#{indent}#{@expression.toLambda(options)}"
toString: ->
@expression.toString()
class lib.Expression
constructor: (@location, @root) ->
unless @root?
throw new ParserError @location, "expression with no root?"
isStatic: ->
return isStaticValue(@root)
evaluateStatic: (context) ->
evaluateStaticValue @root, context, @location
toLambda: (options, keepRootParentheses) ->
if @root.toLambda?
@root.skipParentheses = not ( keepRootParentheses ? false )
return @root.toLambda(options)
return @root
toString: ->
@root.toString()
class lib.UnaryExpression
constructor: (@location, @op, @val) ->
unless @op of operatorMap
throw new ParserError @location, "unrecognized operator #{@op}"
isStatic: ->
return isStaticValue(@val)
evaluateStatic: (context) ->
val = evaluateStaticValue @val, context, @location
op = operatorMap[@op]
eval "#{op}#{JSON.stringify val}"
toLambda: (options) ->
val = @val
if @val.toLambda?
val = @val.toLambda(options)
op = operatorMap[@op]
if @skipParentheses
"#{op}#{val}"
else
"(#{op}#{val})"
toString: ->
"#{@op}#{@val}"
class lib.BinaryExpression
constructor: (@location, @left, @op, @right) ->
unless @op of operatorMap
throw new ParserError @location, "unrecognized operator #{@op}"
isStatic: ->
return isStaticValue(@left) and isStaticValue(@right)
evaluateStatic: (context) ->
left = evaluateStaticValue @left, context, @location
right = evaluateStaticValue @right, context, @location
op = operatorMap[@op]
eval "#{JSON.stringify left} #{op} #{JSON.stringify right}"
toLambda: (options) ->
left = @left
if @left.toLambda?
left = @left.toLambda(options)
right = @right
if @right.toLambda?
right = @right.toLambda(options)
op = operatorMap[@op]
if @skipParentheses
"#{left} #{op} #{right}"
else
"(#{left} #{op} #{right})"
toString: ->
return "#{@left.toString()} #{@op} #{@right.toString()}"
class lib.LocalExpressionCall
constructor: (@location, @name, @arguments) ->
toLambda: (options) ->
args = []
for a in @arguments
if a.toLambda?
args.push a.toLambda(options)
else
args.push a
options.scopeManager.checkAccess @location, @name.base
"await #{@name}(#{args.join(', ')})"
toString: ->
args = []
for a in @arguments
args.push a.toString()
return "#{@name}(#{args.join(', ')})"
class lib.DBExpressionCall
constructor: (@location, @name, @arguments) ->
toLambda: (options) ->
args = []
for a in @arguments
if a.toLambda?
args.push a.toLambda(options)
else
args.push a
"await context.db.read('#{@name.base}')#{@name.toLambdaTail(options)}(#{args.join(', ')})"
toString: ->
args = []
for a in @arguments
args.push a.toString()
return "@#{@name}(#{args.join(', ')})"
class lib.IfCondition
constructor: (@expression, @negated) ->
pushCode: (line) ->
@startFunction = @startFunction ? new lib.Function
@startFunction.pushLine(line)
validateStateTransitions: (allStateNames, language) ->
@startFunction?.validateStateTransitions?(allStateNames, language)
toLambda: (output, indent, options) ->
unless options.language
throw "missing language in if"
if @negated
output.push "#{indent}if (!(#{@expression.toLambda(options)})) {"
else
output.push "#{indent}if (#{@expression.toLambda(options)}) {"
@startFunction?.toLambda(output, indent + " ", options)
output.push "#{indent}}"
hasStatementsOfType: (types) ->
if @startFunction?
return @startFunction.hasStatementsOfType(types)
return false
collectRequiredAPIs: (apis) ->
@startFunction?.collectRequiredAPIs?(apis)
toLocalization: (localization) ->
@startFunction?.toLocalization(localization)
class lib.ElseCondition
constructor: (@expression, @negated) ->
pushCode: (line) ->
@startFunction = @startFunction ? new lib.Function
@startFunction.pushLine(line)
validateStateTransitions: (allStateNames, language) ->
@startFunction?.validateStateTransitions?(allStateNames, language)
toLambda: (output, indent, options) ->
if @expression
if @negated
output.push "#{indent}else if (!(#{@expression.toLambda(options)})) {"
else
output.push "#{indent}else if (#{@expression.toLambda(options)}) {"
else
output.push "#{indent}else {"
@startFunction?.toLambda(output, indent + " ", options)
output.push "#{indent}}"
hasStatementsOfType: (types) ->
if @startFunction?
return @startFunction.hasStatementsOfType(types)
return false
collectRequiredAPIs: (apis) ->
@startFunction?.collectRequiredAPIs?(apis)
toLocalization: (localization) ->
@startFunction?.toLocalization(localization)
class lib.ForStatement
constructor: (@keyName, @valueName, @sourceName) ->
pushCode: (line) ->
@startFunction = @startFunction ? new lib.Function
@startFunction.pushLine(line)
validateStateTransitions: (allStateNames, language) ->
@startFunction?.validateStateTransitions?(allStateNames, language)
toLambda: (output, indent, options) ->
tempKey = options.scopeManager.newTemporary(@location)
code = []
sourceName = @sourceName.toLambda(options)
# lexically scope the block
options.scopeManager.pushScope @location, 'for'
code.push "for (let #{tempKey} in #{sourceName}){"
if @valueName?
options.scopeManager.allocate @location, @valueName
code.push " let #{@valueName} = #{sourceName}[#{tempKey}];"
if @keyName?
options.scopeManager.allocate @location, @keyName
code.push " let #{@keyName} = #{tempKey};"
@startFunction?.toLambda(code, " ", options)
code.push "}"
options.scopeManager.popScope()
for l in code
output.push indent + l
hasStatementsOfType: (types) ->
if @startFunction?
return @startFunction.hasStatementsOfType(types)
return false
collectRequiredAPIs: (apis) ->
@startFunction?.collectRequiredAPIs?(apis)
toLocalization: (localization) ->
@startFunction?.toLocalization(localization)
class lib.SwitchStatement
constructor: (@assignments) ->
@cases = []
pushCase: (switchCase) ->
@cases.push switchCase
validateStateTransitions: (allStateNames, language) ->
for c in @cases
c.validateStateTransitions?(allStateNames, language)
toLambda: (output, indent, options) ->
# switch statements are turned into cascading if/else statements
# as we allow a variety of switching scenarios, while JavaScript
# only supports jumping on integers.
# if we have local assignments, then our scoping promise is
# they won't be visible after the switch statement, which
# means we'll need an extra block scope to contain them.
needWrap = @assignments[0]?.needsScope()
# either way, switch blocks are lexical scopes to us
options.scopeManager.pushScope @location, "switch"
if needWrap
output.push "#{indent}{"
childIndent = indent + " "
else
childIndent = indent
# each assignment becomes a local variable
for a in @assignments
a.toLambda(output, childIndent, options)
# if we have at least one assignment, then they
# become the implicit variable in case comparisons.
# if it's non trivial, then we cache it in a local variable
implicit = @assignments[0]?.stringName
# let each case generate their chunk
for c, idx in @cases
c.toLambda(output, childIndent, options, idx==0, implicit)
if needWrap
output.push "#{indent}}"
options.scopeManager.popScope()
toLocalization: (localization) ->
@cases.forEach((c) -> c.startFunction?.toLocalization(localization))
class lib.SwitchAssignment
constructor: (@location, @name, @value) ->
needsScope: ->
@value? or @name?.toLambda?
toLambda: (output, indent, options) ->
@stringName = @name
if @name?.toLambda?
@stringName = @name.toLambda(options)
if @stringName? and @value?
# if we're assigning a value, this needs to be a new var
options.scopeManager.allocate @location, @stringName
unless @stringName
# if no name, then it's the implicit, and we'll make this a temporary
@stringName = options.scopeManager.newTemporary(@location)
if @value?
# if there isn't a value, then this is just importing the implicit
output.push "#{indent}let #{@stringName} = #{@value.toLambda(options)};"
class lib.SwitchCase
constructor: (@location, @operator, @value) ->
unless @operator of operatorMap
throw new ParserError @location, "Unrecognized operator #{@operator}"
pushCode: (line) ->
@startFunction = @startFunction ? new lib.Function
@startFunction.pushLine(line)
validateStateTransitions: (allStateNames, language) ->
@startFunction?.validateStateTransitions?(allStateNames, language)
toLambda: (output, indent, options, first, implicit) ->
if @operator == 'else'
output.push "#{indent}else {"
else
cmd = if first then 'if' else 'else if'
if @operator == 'expr'
val = @value?.toLambda?(options, false)
output.push "#{indent}#{cmd} (#{val}) {"
else if @operator == 'regex'
output.push "#{indent}#{cmd} (/#{@value.expression}/#{@value.flags}.test(#{implicit})) {"
else
val = @value?.toLambda?(options, true)
op = operatorMap[@operator]
output.push "#{indent}#{cmd} (#{implicit} #{op} #{val}) {"
@startFunction?.toLambda(output, indent + " ", options)
output.push "#{indent}}"
toLocalization: (localization) ->
@startFunction?.toLocalization(localization)
class lib.SetSetting
constructor: (@variable, @value) ->
toLambda: (output, indent, options) ->
output.push "#{indent}context.settings['#{@variable}'] = #{@value};"
class lib.DBAssignment
constructor: (@name, @expression) ->
toLambda: (output, indent, options) ->
tail = @name.toLambdaTail()
if tail == ""
output.push "#{indent}context.db.write('#{@name.base}', #{@expression.toLambda(options)});"
else
output.push "#{indent}context.db.read('#{@name.base}')#{tail} = #{@expression.toLambda(options)};"
class lib.WrapClass
constructor: (@className, @variableName, @source) ->
toLambda: (output, indent, options) ->
options.scopeManager.allocate @location, @variableName
output.push "#{indent}var #{@variableName} = new #{@className}(context.db.read('#{@source}', true), context);"
class lib.DBTypeDefinition
constructor: (@location, @name, @type) ->
class lib.LocalDeclaration
constructor: (@name, @expression) ->
toLambda: (output, indent, options) ->
options.scopeManager.allocate @location, @name
output.push "#{indent}let #{@name} = #{@expression.toLambda(options)};"
class lib.LocalVariableAssignment
constructor: (@name, @expression) ->
toLambda: (output, indent, options) ->
options.scopeManager.checkAccess @location, @name
output.push "#{indent}#{@name} = #{@expression.toLambda(options)};"
class lib.LocalVariableReference
constructor: (@location, @name) ->
toLambda: (options) ->
options.scopeManager.checkAccess @location, @name.base
@name.toLambda(options)
toString: (options) ->
return @name
class lib.SlotVariableAssignment
constructor: (@location, @name, @expression) ->
if @name.base in ['request', 'event']
throw new ParserError @location, "cannot assign to the reserved variable name `$#{@name}`"
toLambda: (output, indent, options) ->
output.push "#{indent}context.slots.#{@name} = #{@expression.toLambda(options)};"
class lib.Directive
constructor: (@expression) ->
toLambda: (output, indent, options) ->
expression = @expression.toLambda(options)
code = """
var __directives = #{expression};
if (!__directives) { throw new Error('directive expression at line #{@location?.start?.line} did not return an array of directives'); }
if (!Array.isArray(__directives)) {
__directives = [__directives];
}
for(var i=0; i<__directives.length; ++i) {
if (typeof(__directives[i]) == 'object') {
context.directives.push(__directives[i]);
} else {
throw new Error('directive expression at line #{@location?.start?.line} produced item ' + i + ' that was not an object');
}
} """
for line in code.split '\n'
output.push indent + line
class lib.RecordMetric
constructor: (@name) ->
toLambda: (output, indent, options) ->
output.push "#{indent}reportValueMetric('#{@name}', 1);"
class lib.SetResponseSpacing
constructor: (@milliseconds) ->
toLambda: (output, indent, options) ->
output.push "#{indent}context.db.responseMinimumDelay = #{@milliseconds};"
class lib.Function
constructor: ->
@languages = {}
pushLine: (line) ->
unless line.location?.language
throw "Missing language in line #{line.constructor?.name}"
language = line.location.language
unless language of @languages
@languages[language] = []
@languages[language].push line
validateStateTransitions: (allStateNames, language) ->
return unless @languages[language]?
for line in @languages[language]
line.validateStateTransitions?(allStateNames, language)
toLambda: (output, indent, options) ->
unless options.language
console.error options
throw "no language in toLambda"
lines = @languages['default']
if options.language of @languages
lines = @languages[options.language]
if lines?
for line in lines
unless line?.toLambda?
console.error line
throw "missing toLambda for #{line.constructor.name}"
line.toLambda(output, indent, options)
if @shouldEndSession
output.push("context.shouldEndSession = true;")
toLocalization: (localization) ->
return unless 'default' of @languages
for line, idx in @languages.default
if line.toLocalization?
line.toLocalization(localization)
forEachPart: (language, cb) ->
return unless @languages[language]
for line in @languages[language]
cb(line)
hasStatementsOfType: (types) ->
for lang, lines of @languages
for line in lines
if line.hasStatementsOfType
return true if line.hasStatementsOfType(types)
return false
collectRequiredAPIs: (apis) ->
for lang, lines of @languages
for line in lines
line.collectRequiredAPIs?(apis)
class lib.FunctionMap
###
Interface compatible with Function, this is a
convenience object for collecting named blocks of
alternative functions.
###
constructor: ->
@currentName = '__'
@functions = {}
@functions[@currentName] = new lib.Function
setCurrentName: (name) ->
unless name of @functions
@functions[name] = new lib.Function
@currentName = name
pushLine: (line) ->
@functions[@currentName].pushLine line
validateStateTransitions: (allStateNames, language) ->
toLambda: (output, indent, options, name) ->
return unless name?
return unless name of @functions
return @functions[name].toLambda output, indent, options
toLocalization: (localization) ->
forEachPart: (language, cb) ->
for n, f of @functions
f.forEachPart language, cb
hasStatementsOfType: (types) ->
for n, f of @functions
return true if f.hasStatementsOfType types
return false
collectRequiredAPIs: (apis) ->
for n, f of @functions
f.collectRequiredAPIs?(apis)