UNPKG

@litexa/core

Version:

Litexa, a programming language for writing Alexa skills

358 lines (292 loc) 13.2 kB
### # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ### lib = module.exports.lib = {} { Function } = require('./function.coffee').lib { Intent, FilteredIntent } = require('./intent.coffee').lib { ParserError } = require("./errors.coffee").lib class lib.Transition constructor: (@name, @stop) -> toLambda: (output, indent, options) -> # this overrides any instruction to stop and return a response output.push "#{indent}delete context.shouldEndSession;" output.push "#{indent}delete context.shouldDropSession;" # queue the next state output.push "#{indent}context.nextState = '#{@name}';" if @stop output.push "#{indent}context.handoffState = '#{@name}';" output.push "#{indent}context.handoffIntent = true;" output.push "#{indent}return;" validateStateTransitions: (allStateNames) -> unless @name in allStateNames throw new ParserError @location, "Transition to non existant state: #{@name}" class lib.HandoffIntent constructor: (@name) -> toLambda: (output, indent, options) -> output.push "#{indent}context.handoffState = '#{@name}';" output.push "#{indent}context.handoffIntent = true;" validateStateTransitions: (allStateNames) -> unless @name in allStateNames throw new ParserError @location, "Handoff to non existant state: #{@name}" class lib.SetSkillEnd constructor: -> toLambda: (output, indent, options) -> # cancel any pending state transitions or handoffs output.push "#{indent}context.nextState = null;" output.push "#{indent}context.handoffState = null;" output.push "#{indent}context.handoffIntent = null;" output.push "#{indent}delete context.shouldDropSession;" # flag that we're exiting with this response output.push "#{indent}context.shouldEndSession = true;" class lib.SetSkillListen constructor: (@kinds) -> toLambda: (output, indent, options) -> # cancel any pending state transitions or handoffs output.push "#{indent}context.nextState = null;" output.push "#{indent}context.handoffState = null;" output.push "#{indent}context.handoffIntent = null;" # we don't want a session end output.push "#{indent}context.shouldEndSession = false;" unless 'microphone' in @kinds # but we're not listening for the microphone output.push "#{indent}context.shouldDropSession = true;" class lib.LogMessage constructor: (@contents) -> toLambda: (output, indent, options) -> output.push "#{indent}exports.Logging.log(JSON.stringify(#{@contents.toLambda(options)}));" class lib.State constructor: (@name) -> @intents = {} @languages = {} @parsePhase = 'start' @pushOrGetIntent null, '--default--', null @parsePhase = 'start' @locations = { default: null } isState: true prepareForLanguage: (location) -> return unless location?.language return if location.language == 'default' unless location.language of @languages @languages[location.language] = {} resetParsePhase: -> # used by the default constructors, pre parser @parsePhase = 'start' collectDefinedSlotTypes: (context, customSlotTypes) -> workingIntents = @collectIntentsForLanguage(context.language) for name, intent of workingIntents intent.collectDefinedSlotTypes context, customSlotTypes validateSlotTypes: (context, customSlotTypes) -> workingIntents = @collectIntentsForLanguage(context.language) for name, intent of workingIntents intent.validateSlotTypes customSlotTypes validateTransitions: (allStateNames, language) -> @startFunction?.validateStateTransitions(allStateNames, language) for name, intent of @intents intent.validateStateTransitions allStateNames, language for name, intent of @languages[language] intent.validateStateTransitions allStateNames, language hasIntent: (name, language) -> workingIntents = @collectIntentsForLanguage language for intentName, intent of workingIntents return true if name == intentName return false reportIntents: (language, output) -> workingIntents = @collectIntentsForLanguage language for name, intent of workingIntents continue if name == '--default--' report = intent.report() output[report] = true getIntentInLanguage: (language, intentName) -> if language == 'default' return @intents[intentName] return @languages[language]?[intentName] pushOrGetIntent: (location, utterance, intentInfo) -> switch @parsePhase when 'start' @parsePhase = 'intents' when 'intents' # fine else throw new ParserError location, "cannot add a new intent handler to the state `#{@name}` at this location. Have you already added state exit code before here? Check your indentation." try key = Intent.utteranceToName(location, utterance) catch err throw new ParserError location, "Cannot create intent name from `#{utterance}`: #{err}" language = location?.language ? 'default' collection = @intents if language != 'default' @languages[language] = @languages[language] ? {} collection = @languages[language] unless key of collection if intentInfo?.class? collection[key] = new intentInfo.class({ location, utterance }) else collection[key] = new Intent({ location, utterance }) else if !collection[key].defaultedResetOnGet and key != '--default--' # only allow repeat intents if they are events that can be filtered if collection[key] not instanceof FilteredIntent throw new ParserError location, "Not allowed to redefine intent `#{key}` in state `#{@name}`" intent = collection[key] if intent.defaultedResetOnGet intent.resetCode() intent.defaultedResetOnGet = undefined intent.allLocations.push location return intent pushCode: (line) -> switch @parsePhase when 'start' @startFunction = @startFunction ? new Function @startFunction.pushLine(line) when 'end', 'intents' @endFunction = @endFunction ? new Function @endFunction.pushLine(line) @parsePhase = 'end' else throw new ParserError line.location, "cannot add code to the state `#{@name}` here, you've already begun defining intents" collectIntentsForLanguage: (language) -> workingIntents = {} # for a given state, you will get the intents in that locale's # version of that state only (intents are not inherited from the parent state) if language of @languages for name, intent of @languages[language] workingIntents[name] = intent if @name == 'global' unless '--default--' of workingIntents workingIntents['--default--'] = @intents['--default--'] else if @intents? for name, intent of @intents workingIntents[name] = intent return workingIntents toLambda: (output, options) -> workingIntents = @collectIntentsForLanguage(options.language) options.scopeManager = new (require('./variableScope').VariableScopeManager)(@locations[options.language], @name) options.scopeManager.currentScope.referenceTester = options.referenceTester enterFunc = [] @startFunction?.toLambda(enterFunc, "", options) exitFunc = [] if @endFunction? @endFunction.toLambda(exitFunc, "", options) childIntentsEncountered = [] intentsFunc = [] intentsFunc.push "switch( context.intent ) {" for name, intent of workingIntents options.scopeManager.pushScope intent.location, "intent:#{name}" if name == '--default--' intentsFunc.push " default: {" if @name == 'global' intentsFunc.push " if (!runOtherwise) { return false; }" if @name != 'global' intentsFunc.push " if ( await processIntents.global(context, #{not intent.hasContent}) ) { return true; }" if intent.hasContent intent.toLambda(intentsFunc, options) else if intent.hasContent intent.toLambda(intentsFunc, options) else if options.strictMode intentsFunc.push " throw new Error('unhandled intent ' + context.intent + ' in state ' + context.handoffState);" else intentsFunc.push " console.error('unhandled intent ' + context.intent + ' in state ' + context.handoffState);" else # Child intents are registered to the state as handlers, but it is parent handlers that perform the logic # of adding them to the same switch case. Therefore, keep track of the ones already added to transformed code and # ignore them if they are encountered again. if childIntentsEncountered.includes intent.name options.scopeManager.popScope() continue for intentName in intent.childIntents intentsFunc.push " case '#{intentName}':" childIntentsEncountered.push intentName intentsFunc.push " case '#{intent.name}': {" if intent.code? for line in intent.code.split('\n') intentsFunc.push " " + line else intent.toLambda(intentsFunc, options) intentsFunc.push " break;\n }" options.scopeManager.popScope() intentsFunc.push "}" unless options.scopeManager.depth() == 1 throw new ParserError @locations[options.language], "scope imbalance: returned to state but scope has #{options.scopeManager.depth()} depth" # if we have local variables in the root scope that are accessed # after the enter function, then we need to persist those to the # database in a special state scope rootScope = options.scopeManager.currentScope if rootScope.hasDescendantAccess() names = [] # collect names for k, v of rootScope.variables when v.accesedByDescendant names.push k # unpack into local variables at the start of handlers, except # for the entry handler where they're initialized unpacker = "let {#{names.join ', '}} = context.db.read('__stateLocals') || {};" intentsFunc.unshift unpacker exitFunc.unshift unpacker # pack into database object and the end of handlers, except # for the exit state, where they're forgotten packer = "context.db.write('__stateLocals', {#{names.join ', '}} );" enterFunc.push packer intentsFunc.push packer output.push "enterState.#{@name} = async function(context) {" output.push " " + e for e in enterFunc output.push "};" intentsFunc.push "return true;" output.push "processIntents.#{@name} = async function(context, runOtherwise) {" output.push " " + e for e in intentsFunc output.push "};" output.push "exitState.#{@name} = async function(context) {" output.push " " + e for e in exitFunc output.push "};" output.push "" hasStatementsOfType: (types) -> if @startFunction? return true if @startFunction.hasStatementsOfType(types) if @intents? for name, intent of @intents return true if intent.hasStatementsOfType(types) return false collectRequiredAPIs: (apis) -> @startFunction?.collectRequiredAPIs(apis) @endFunction?.collectRequiredAPIs(apis) if @intents? for name, intent of @intents intent.collectRequiredAPIs apis toUtterances: (output) -> workingIntents = @collectIntentsForLanguage(output.language) for name, intent of workingIntents continue if intent.referenceIntent? intent.toUtterances(output) toModelV2: (output, context, extendedEventNames) -> workingIntents = @collectIntentsForLanguage(context.language) for name, intent of workingIntents continue if name == '--default--' continue if intent.referenceIntent? unless intent.hasUtterances unless (name in extendedEventNames) or name.includes('.') console.warn "`#{name}` does not have utterances; not adding to language model." continue try model = intent.toModelV2(context) catch err if err.location throw err # ParserErrors have location properties; propagate the error else throw new Error "failed to write language model for state `#{@name}`: #{err}" continue unless model? if model.name of context.intents console.error "duplicate `#{model.name}` intent found while writing model" else context.intents[model.name] = model output.languageModel.intents.push model toLocalization: (localization) -> @startFunction?.toLocalization(localization) for name, intent of @intents if !localization.intents[name]? and name != '--default--' # 'otherwise' handler -> no utterances # if this is a new intent, add it to the localization map localization.intents[name] = { default: [] } # add utterances mapped to the intent, and speech lines in the intent handler intent.toLocalization(localization)