Skip to main content
Glama
emicklei

melrōse musical expression player

by emicklei
evaluator.go9.11 kB
package dsl import ( "errors" "fmt" "regexp" "strings" "github.com/emicklei/melrose/control" "github.com/emicklei/melrose/core" "github.com/emicklei/melrose/notify" "github.com/expr-lang/expr" ) type Evaluator struct { context core.Context funcs map[string]Function } func NewEvaluator(ctx core.Context) *Evaluator { return &Evaluator{ context: ctx, funcs: EvalFunctions(ctx), } } const fourSpaces = " " // Statements are separated by newlines and semicolons. // If a line is prefixed by one or more TABs then that line is appended to the previous. // If a line is prefixed by 4 SPACES then that line is appended to the previous. // Return the result of the last expression or statement. func (e *Evaluator) EvaluateProgram(source string) (any, error) { splitted := strings.Split(source, "\n") lines := make([]string, 0, len(splitted)) // indentation nrOfLastExpression := -1 for lineNr, line := range splitted { if strings.HasPrefix(line, "\t") || strings.HasPrefix(line, fourSpaces) { // append to previous if len(lines) == 0 { return nil, errors.New("syntax error, first line cannot start with TAB") } if nrOfLastExpression+1 != lineNr { return nil, fmt.Errorf("syntax error, line with TAB [%d] must be part of expression", lineNr+1) } lines[len(lines)-1] = withoutTrailingComment(lines[len(lines)-1]) + line // with TAB TODO nrOfLastExpression = lineNr continue } lines = append(lines, line) nrOfLastExpression = lineNr } // now, lines dont have leading tabs or 4 spaces var lastResult any for _, line := range lines { // replace all TABs line = strings.Replace(line, "\t", " ", -1) // whitespaces line = strings.TrimSpace(line) // empty if len(line) == 0 { continue } // comment line if strings.HasPrefix(line, "//") { continue } // trailing inline comment line = withoutTrailingComment(line) for _, statement := range strings.Split(line, ";") { // whitespaces statement = strings.TrimSpace(statement) // empty if len(statement) == 0 { continue } result, err := e.evaluateCleanStatement(statement) if err != nil { return nil, err } if result != nil { lastResult = result } } } return lastResult, nil } func (e *Evaluator) RecoveringEvaluateProgram(entry string) (any, error) { defer func() { if err := recover(); err != nil { notify.Errorf("%v", err) return } }() return e.EvaluateProgram(entry) } func (e *Evaluator) evaluateCleanStatement(entry string) (any, error) { if value, ok := e.context.Variables().Get(entry); ok { return value, nil } if varName, expression, ok := IsAssignment(entry); ok { // variable cannot be named after function if _, conflict := e.funcs[varName]; conflict { return nil, fmt.Errorf("cannot use variable [%s] because it is a defined function", varName) } r, err := e.EvaluateExpression(expression) if err != nil { return nil, err } return e.handleAssignment(varName, r) } // evaluate and print r, err := e.EvaluateExpression(entry) if err != nil { return nil, err } // special case for Loop,Listen,Recording if canStop, ok := r.(core.Stoppable); ok { varName := e.newSuggestedVariableName(canStop) if len(varName) == 0 { return nil, fmt.Errorf("this object must assigned to variable name, use e.g. var = %s", canStop.(core.Storable).Storex()) } return e.handleAssignment(varName, r) } // special case for Evals, put last because Stoppables can be also Evaluatable if theEval, ok := r.(core.Evaluatable); ok { if err := theEval.Evaluate(e.context); err != nil { // no condition return nil, err } } return r, nil } // The last expression returned a Stoppable and was not assigned to a variable. // Generate a name based on the combination of the file and the line (if both given). func (e *Evaluator) newSuggestedVariableName(stoppable core.Stoppable) string { var line int if v, ok := e.context.Environment().Load(core.EditorLineEnd); ok { line = v.(int) } else { return "" } return fmt.Sprintf("%s%d", shortTypeName(stoppable), line) } // *core.Loop => loop func shortTypeName(v any) string { if v == nil { return "nil" } parts := strings.Split(fmt.Sprintf("%T", v), ".") if len(parts) > 1 { return strings.ToLower(parts[len(parts)-1]) } return strings.ToLower(parts[0]) } func (e *Evaluator) handleAssignment(varName string, r any) (any, error) { // check delete if r == nil { e.context.Variables().Delete(varName) } else { // special case for Loop // if the value is a Loop // then if the variable refers to an existing loop // then change to Target of that loop // else store the loop // else store the result if theLoop, ok := r.(*core.Loop); ok { if storedValue, present := e.context.Variables().Get(varName); present { if otherLoop, replaceme := storedValue.(*core.Loop); replaceme { otherLoop.SetTarget(theLoop.Target()) r = otherLoop } else { // existing variable but not a Loop e.context.Variables().Put(varName, theLoop) } } else { // new variable for theLoop e.context.Variables().Put(varName, theLoop) } return r, nil } // special case for Listen // if the value is a Listen // then if the variable refers to an existing listen // then change to Target of that listen // else store the listen // else store the result if theListen, ok := r.(*control.Listen); ok { if storedValue, present := e.context.Variables().Get(varName); present { if otherListen, replaceme := storedValue.(*control.Listen); replaceme { otherListen.SetTarget(theListen.Target()) r = otherListen } else { // existing variable but not a Listen e.context.Variables().Put(varName, theListen) } } else { // new variable for theLoop e.context.Variables().Put(varName, theListen) } return r, nil } // special case for Recording // if the value is a Recording // then if the variable refers to an existing recording // then change the Target of that recording // else store the recording // else store the result if theRecording, ok := r.(*control.Recording); ok { if storedValue, present := e.context.Variables().Get(varName); present { if storedRecording, replaceme := storedValue.(*control.Recording); replaceme { storedRecording.GetTargetFrom(theRecording) r = storedRecording } else { // existing variable but not a Recording e.context.Variables().Put(varName, theRecording) } } else { // new variable for theRecording e.context.Variables().Put(varName, theRecording) } return r, nil } // not a Loop or Listen or Recording e.context.Variables().Put(varName, r) if aware, ok := r.(core.NameAware); ok { aware.VariableName(varName) } } return r, nil } // EvaluateExpression returns the result of an expression (entry) using a given store of variables. // The result is either FunctionResult or a "raw" Go object. func (e *Evaluator) EvaluateExpression(entry string) (any, error) { options := []expr.Option{} // since expr 1.14.3 for _, each := range []string{"join", "repeat", "trim", "replace", "duration", "map"} { options = append(options, expr.DisableBuiltin(each)) } env := map[string]any{} for k, f := range e.funcs { env[k] = f.Func } for k := range e.context.Variables().Variables() { env[k] = variable{Name: k, store: e.context.Variables()} } options = append(options, expr.Env(env)) options = append(options, expr.Patch(new(indexedAccessPatcher))) addOperatorsTo(env) program, err := expr.Compile(entry, append(options, exprOperators()...)...) if err != nil { // try parsing the entry as a sequence or chord // this can be requested from the editor to listen to a part of a sequence,chord,note,progression if strings.Contains(entry, "/") { if subchord, suberr := core.ParseChord(entry); suberr == nil { if notify.IsDebug() { notify.Debugf("dsl.evaluate:%s", subchord.Storex()) } return subchord, nil } } // try parsing the entry as a sequence if subseq, suberr := core.ParseSequence(entry); suberr == nil { if notify.IsDebug() { notify.Debugf("dsl.evaluate:%s", subseq.Storex()) } return subseq, nil } // give up return nil, err } return expr.Run(program, env) } // https://regex101.com/ var assignmentRegex = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$`) // [ ]a[]=[]note('c') func IsAssignment(entry string) (varname string, expression string, ok bool) { sanitized := strings.TrimSpace(entry) res := assignmentRegex.FindAllStringSubmatch(sanitized, -1) if len(res) != 1 { return "", "", false } if len(res[0]) != 3 { return "", "", false } return res[0][1], res[0][2], true } func (e *Evaluator) LookupFunction(fn string) (Function, bool) { for name, each := range e.funcs { if name == fn { return each, true } } return Function{}, false } func withoutTrailingComment(s string) string { if slashes := strings.Index(s, "//"); slashes != -1 { return s[0:slashes] } return s }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/emicklei/melrose'

If you have feedback or need assistance with the MCP directory API, please join our Discord server