diff --git a/README.md b/README.md index fa9a678c..530a01b5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ ![image|690x338](https://community.appinventor.mit.edu/uploads/default/original/3X/e/d/ed3b9d22ddaefffb4fd5ab71964b8816c56c63a1.png) -Falcon is a language designed for App Inventor to enable syntax-based programming and for incorporating agenting coding abilities. +Falcon is a language designed for App Inventor to enable syntax-based programming and for incorporating agentic coding abilities. ## Quirks 1. Falcon follows 1-based indexing. 2. Falcon variables are dynamically typed. Do not declare variables. 3. Lists and dictionaries are passed as references. 4. Falcon follows Kotlin's style of functional expressions. -5. Falcon does not have a return statement; the last expression in a body is returned. +5. Falcon does not have a traditional return statement. In result functions, the last expression is returned. Use `yield` inside a result function for an early return. 6. Falcon does NOT have a try-catch or a throw statement. 7. Only single-line comments using double slash `//` are supported. 8. Do not use `_` in place of unused variables @@ -179,15 +179,44 @@ func double(n) = { n * 2 } Or multiple expressions: ``` -func FibSum(n) = { +func Fib(n) = { if (n < 2) { n } else { - FibSum(n - 1) + FibSum(n - 2) + Fib(n - 1) + Fib(n - 2) } } ``` -Note that there is no `return` statement in Falcon. The last statement in a body is taken as the output of an expression. +Note that there is no `return` statement in Falcon. The last expression in a body is taken as the output of an expression. + +### yield + +`yield ` exits a result function early and returns the given value to the caller. It only works inside result functions (`= { ... }`). If no `yield` is reached, the last expression in the body is returned as usual. + +``` +func first_divisible(n, list) = { + for (i: 1..list.listLen()) { + if (list[i] % n == 0) { + yield list[i] + } + } + -1 +} + +println(first_divisible(7, [3, 10, 21, 44])) // Output: 21 +println(first_divisible(7, [1, 2, 3])) // Output: -1 +``` + +A common pattern is using `yield` as a guard clause: + +``` +func safe_div(a, b) = { + if (b == 0) { + yield 0 + } + a / b +} +``` ## Functions @@ -298,7 +327,7 @@ e.g. `"Hello ".trim()` ### Dictionary - `dictLen()` -- `get(key)` +- `get(key, notFound)` - `set(key, value)` - `delete(key)` - `getAtPath(path_list, notfound)` @@ -376,7 +405,7 @@ Usage `.min { m, n -> bool_m_preceeds_n }` and `.max { m, n -> bool_m_preceeds_n local names = ["Bob", "Alice", "John"] // Find the longest name local longestName = names - .max { m, n -> n.textLen() > m.textLen() } // use min { } for the shortest name + .max { m, n -> m.textLen() < n.textLen() } // use min { } for the shortest name println(longestName) ``` diff --git a/lang/code/ast/blockly.go b/lang/code/ast/blockly.go index fbf99169..2b9e4aa0 100644 --- a/lang/code/ast/blockly.go +++ b/lang/code/ast/blockly.go @@ -31,6 +31,31 @@ type Value struct { XMLName xml.Name `xml:"value"` Name string `xml:"name,attr"` Block Block `xml:"block"` + Shadow *Shadow `xml:"shadow"` +} + +type Shadow struct { + XMLName xml.Name `xml:"shadow"` + Type string `xml:"type,attr"` + Mutation *Mutation `xml:"mutation,omitempty"` + Fields []Field `xml:"field"` + Values []Value `xml:"value"` + Statements []Statement `xml:"statement"` + Next *Next `xml:"next"` +} + +func (s *Shadow) BlockValue() Block { + if s == nil { + return Block{} + } + return Block{ + Type: s.Type, + Mutation: s.Mutation, + Fields: s.Fields, + Values: s.Values, + Statements: s.Statements, + Next: s.Next, + } } type Mutation struct { @@ -116,7 +141,7 @@ func ValuesByPrefix(namePrefix string, operands []Expr) []Value { func ValueArgsByPrefix(on Expr, onName string, namePrefix string, operands []Expr) []Value { values := make([]Value, len(operands)+1) - values[0] = Value{Name: onName, Block: on.Blockly()} + values[0] = Value{Name: onName, Block: on.Blockly(false)} for i, operand := range operands { values[i+1] = Value{Name: namePrefix + strconv.Itoa(i), Block: operand.Blockly(false)} } @@ -139,7 +164,7 @@ func MakeValueArgs(on Expr, onName string, operands []Expr, names ...string) []V panic("len(operands) != len(names)") } values := make([]Value, len(operands)+1) - values[0] = Value{Name: onName, Block: on.Blockly()} + values[0] = Value{Name: onName, Block: on.Blockly(false)} for i, operand := range operands { values[i+1] = Value{Name: names[i], Block: operand.Blockly(false)} } @@ -173,9 +198,8 @@ func ensureStatement(expr Expr) Block { // First evaluate Blockly(). True indicates we expect a statement. // This gives time for if expressions to mutate to if statement. aBlock := expr.Blockly(true) - if expr.Consumable(true) { - // It's still consumable, wrap around evaluate but ignore result - return Block{Type: "controls_eval_but_ignore", Values: []Value{{Block: aBlock}}} + if expr.Consumable() { + panic("result of `" + expr.String() + "` is never used") } return aBlock } diff --git a/lang/code/ast/blockly_tooling.go b/lang/code/ast/blockly_tooling.go deleted file mode 100644 index bc3e7acd..00000000 --- a/lang/code/ast/blockly_tooling.go +++ /dev/null @@ -1,30 +0,0 @@ -package ast - -// DependsOnVariables checks if the expression references any of the variables in the list -func DependsOnVariables(e Expr, variables []string) bool { - var references []string - getReferencedVariables(e.Blockly(), &references) - - for _, reference := range references { - for _, match := range variables { - if reference == match { - return true - } - } - } - return false -} - -func getReferencedVariables(bky Block, currReferences *[]string) { - if bky.Type == "lexical_variable_get" { - *currReferences = append(*currReferences, bky.SingleField()) - } - - for _, value := range bky.Values { - getReferencedVariables(value.Block, currReferences) - } - - for _, statement := range bky.Statements { - getReferencedVariables(*statement.Block, currReferences) - } -} diff --git a/lang/code/ast/common/binary.go b/lang/code/ast/common/binary.go index 8937f68a..5a8e3670 100644 --- a/lang/code/ast/common/binary.go +++ b/lang/code/ast/common/binary.go @@ -15,16 +15,28 @@ type BinaryExpr struct { func (b *BinaryExpr) String() string { myPrecedence := lex.PrecedenceOf(b.Where.Flags[0]) + // For non-left-associative operators, right operands at the same precedence need parens + // to avoid changing semantics: e.g. 10-(3-2) must not become 10-3-2. + rightNeedsParensAtSamePrec := b.Operator == lex.Dash || b.Operator == lex.Slash || b.Operator == lex.Power stringified := make([]string, len(b.Operands)) for i, operand := range b.Operands { operandStr := operand.String() + needsParens := false // If operand is a BinaryExpr with lower precedence, wrap it if binExpr, ok := operand.(*BinaryExpr); ok { - if lex.PrecedenceOf(binExpr.Where.Flags[0]) < myPrecedence { - operandStr = "(" + operandStr + ")" + opPrec := lex.PrecedenceOf(binExpr.Where.Flags[0]) + if opPrec < myPrecedence { + needsParens = true + } else if i > 0 && rightNeedsParensAtSamePrec && opPrec == myPrecedence { + needsParens = true } + } else if !operand.Continuous() { + // Non-continuous expressions (e.g. if-else) need parens as binary operands + needsParens = true + } + if needsParens { + operandStr = "(" + operandStr + ")" } - stringified[i] = operandStr } return strings.Join(stringified, " "+*b.Where.Content+" ") @@ -39,7 +51,10 @@ func (b *BinaryExpr) CanRepeat(testOperator lex.Type) bool { return false } switch b.Operator { - case lex.Power, lex.Dash, lex.Slash: + case lex.Power, lex.Dash, lex.Slash, + lex.Equals, lex.NotEquals, lex.TextEquals, lex.TextNotEquals, + lex.LessThan, lex.LessThanEqual, lex.GreatThan, lex.GreaterThanEqual, + lex.TextLessThan, lex.TextGreaterThan: return false default: return true @@ -75,11 +90,23 @@ func (b *BinaryExpr) Continuous() bool { return false } -func (b *BinaryExpr) Consumable(flags ...bool) bool { +func (b *BinaryExpr) Consumable() bool { return true } func (b *BinaryExpr) Signature() []ast.Signature { + switch b.Operator { + case lex.Plus, lex.Times, lex.Dash, lex.Slash, lex.Power, lex.Remainder, lex.BitwiseAnd, lex.BitwiseOr, lex.BitwiseXor: + b.ensureSignature(ast.SignNumb, ast.SignText) + case lex.LogicAnd, lex.LogicOr: + b.ensureSignature(ast.SignBool, ast.SignText) + case lex.Underscore: + // _ auto-converts any operand type to text at runtime; no type enforcement here. + case lex.TextEquals, lex.TextNotEquals, lex.TextLessThan, lex.TextGreaterThan: + b.ensureSignature(ast.SignText, ast.SignNumb) + case lex.LessThan, lex.LessThanEqual, lex.GreatThan, lex.GreaterThanEqual: + b.ensureSignature(ast.SignNumb, ast.SignText) + } switch b.Operator { case lex.BitwiseAnd, lex.BitwiseOr, lex.BitwiseXor: return []ast.Signature{ast.SignNumb} @@ -95,6 +122,8 @@ func (b *BinaryExpr) Signature() []ast.Signature { return []ast.Signature{ast.SignText} case lex.LessThan, lex.LessThanEqual, lex.GreatThan, lex.GreaterThanEqual: return []ast.Signature{ast.SignBool} + case lex.Remainder: + return []ast.Signature{ast.SignNumb} case lex.TextEquals, lex.TextNotEquals, lex.TextLessThan, lex.TextGreaterThan: return []ast.Signature{ast.SignBool} default: @@ -103,6 +132,21 @@ func (b *BinaryExpr) Signature() []ast.Signature { } } +func (b *BinaryExpr) ensureSignature(acceptableSignature ...ast.Signature) { + if len(b.Operands) == 0 { + panic("BinaryExpr.ensureSignature: empty operands") + } + for _, op := range b.Operands { + opSigs := op.Signature() + for _, signature := range acceptableSignature { + if ast.HasSignature(opSigs, signature) { + return + } + } + } + b.Where.TypeError("Operator '%' requires % operand types", *b.Where.Content, ast.FormatSignatures(acceptableSignature)) +} + func (b *BinaryExpr) textCompare() ast.Block { var fieldOp string switch b.Operator { @@ -128,7 +172,7 @@ func (b *BinaryExpr) relationalExpr() ast.Block { case lex.LessThan: fieldOp = "LT" case lex.LessThanEqual: - fieldOp = "LT" + fieldOp = "LTE" case lex.GreatThan: fieldOp = "GT" case lex.GreaterThanEqual: diff --git a/lang/code/ast/common/drop.go b/lang/code/ast/common/drop.go new file mode 100644 index 00000000..b945c4f5 --- /dev/null +++ b/lang/code/ast/common/drop.go @@ -0,0 +1,43 @@ +package common + +import "strings" + +// dropTypeWords lists the type-name suffixes (and direct function names) that +// constitute a no-op type-conversion in Falcon's dynamic type system. +// ".toString()", ".toStr()", ".toInt()", ".toNumber()" etc. are all identity +// operations that should be silently dropped from the AST. +var dropTypeWords = map[string]bool{ + "string": true, "str": true, "text": true, + "int": true, "integer": true, + "num": true, "number": true, "float": true, "double": true, + "bool": true, "boolean": true, + "list": true, "array": true, "arr": true, + "dict": true, "map": true, "object": true, "obj": true, +} + +// IsDropMethod reports whether a 0-arg method call named name is a no-op +// type conversion that should be silently dropped. +// Matches the "to" camelCase prefix pattern (e.g. toString, toStr, +// toInt, toNumber, toList, toDict, toBoolean, …). +func IsDropMethod(name string) bool { + lower := strings.ToLower(name) + if !strings.HasPrefix(lower, "to") || len(lower) <= 2 { + return false + } + return dropTypeWords[lower[2:]] +} + +// IsDropFunction reports whether a 1-arg function call named name is a no-op +// type-cast wrapper that should be dropped, keeping only its argument. +// Matches both bare type names (string, int, number, …) and the "to" +// prefix form (toString, toInt, …). +func IsDropFunction(name string) bool { + lower := strings.ToLower(name) + if dropTypeWords[lower] { + return true + } + if strings.HasPrefix(lower, "to") && len(lower) > 2 { + return dropTypeWords[lower[2:]] + } + return false +} diff --git a/lang/code/ast/common/empty_socket.go b/lang/code/ast/common/empty_socket.go index 20bb1c05..82d50e03 100644 --- a/lang/code/ast/common/empty_socket.go +++ b/lang/code/ast/common/empty_socket.go @@ -21,10 +21,10 @@ func (e *EmptySocket) Continuous() bool { return true } -func (e *EmptySocket) Consumable(flags ...bool) bool { +func (e *EmptySocket) Consumable() bool { return false } func (e *EmptySocket) Signature() []ast.Signature { - return []ast.Signature{ast.SignText} + return []ast.Signature{ast.SignNumb} } diff --git a/lang/code/ast/common/func_call.go b/lang/code/ast/common/func_call.go index 6cccf249..0773abaf 100644 --- a/lang/code/ast/common/func_call.go +++ b/lang/code/ast/common/func_call.go @@ -4,9 +4,11 @@ import ( "Falcon/code/ast" "Falcon/code/ast/fundamentals" "Falcon/code/ast/variables" + "Falcon/code/fzf" "Falcon/code/lex" "Falcon/code/sugar" "strconv" + "strings" ) type FuncCall struct { @@ -42,8 +44,8 @@ var signatures = map[string]*FuncCallSignature{ "atan": makeSignature("atan", 1, ast.SignNumb), "degrees": makeSignature("degrees", 1, ast.SignNumb), "radians": makeSignature("radians", 1, ast.SignNumb), - "decToHex": makeSignature("decToHex", 1, ast.SignNumb), - "decToBin": makeSignature("decToBin", 1, ast.SignNumb), + "decToHex": makeSignature("decToHex", 1, ast.SignText), + "decToBin": makeSignature("decToBin", 1, ast.SignText), "hexToDec": makeSignature("hexToDec", 1, ast.SignNumb), "binToDec": makeSignature("binToDec", 1, ast.SignNumb), @@ -63,6 +65,13 @@ var signatures = map[string]*FuncCallSignature{ "geoMeanOf": makeSignature("geoMeanOf", 1, ast.SignNumb), "stdDevOf": makeSignature("stdDevOf", 1, ast.SignNumb), "stdErrOf": makeSignature("stdErrOf", 1, ast.SignNumb), + "modeOf": makeSignature("modeOf", 1, ast.SignList), + + "mod": makeSignature("mod", 2, ast.SignNumb), + "rem": makeSignature("rem", 2, ast.SignNumb), + "quot": makeSignature("quot", 2, ast.SignNumb), + "atan2": makeSignature("atan2", 2, ast.SignNumb), + "formatDecimal": makeSignature("formatDecimal", 2, ast.SignText), "println": makeSignature("println", 1, ast.SignVoid), "openScreen": makeSignature("openScreen", 1, ast.SignVoid), @@ -84,17 +93,77 @@ var signatures = map[string]*FuncCallSignature{ "get": makeSignature("get", 3, ast.SignAny), "call": makeSignature("call", -1-(3), ast.SignVoid), "vcall": makeSignature("vcall", -1-(3), ast.SignAny), - "every": makeSignature("every", 1, ast.SignAny), + "every": makeSignature("every", 1, ast.SignList), +} + +// argCountCorrections maps (funcName, argCount) → replacement function name for cases +// where a known built-in is called with the wrong number of arguments but there is a +// semantically equivalent function that accepts that count. +// Key is "name:argCount" formatted by FindArgCountCorrection. +var argCountCorrections = map[string]string{ + "round:2": "formatDecimal", // round(n, decimalPlaces) → formatDecimal(n, decimalPlaces) +} + +// FindArgCountCorrection returns the replacement function name when funcName is a known +// built-in called with argCount arguments that don't match its signature, or "" if none. +func FindArgCountCorrection(funcName string, argCount int) string { + return argCountCorrections[funcName+":"+strconv.Itoa(argCount)] } func MakeFuncCall(name string, args ...ast.Expr) ast.Expr { return &FuncCall{Where: lex.MakeFakeToken(lex.Func), Name: name, Args: args} } +// IsKnownFunction reports whether name is a registered built-in function. +func IsKnownFunction(name string) bool { + _, ok := signatures[name] + return ok +} + +// FindBestSuggestion returns the single highest-scoring built-in function name +// closest to funcName, or "" if no candidate clears the scoring threshold. +func FindBestSuggestion(funcName string) string { + candidates := make([]string, 0, len(signatures)) + for name := range signatures { + if name != funcName { + candidates = append(candidates, name) + } + } + if tops := fzf.Top(funcName, candidates, 1); len(tops) > 0 { + return tops[0] + } + return "" +} + +func joinOr(parts []string) string { + if len(parts) == 0 { + return "" + } + if len(parts) == 1 { + return parts[0] + } + if len(parts) == 2 { + return parts[0] + " or " + parts[1] + } + return strings.Join(parts[:len(parts)-1], ", ") + " or " + parts[len(parts)-1] +} + func TestSignature(funcName string, argsCount int) (string, *FuncCallSignature) { callSignature, ok := signatures[funcName] if !ok { - return sugar.Format("Cannot find function .%()", funcName), nil + candidates := make([]string, 0, len(signatures)) + for name := range signatures { + candidates = append(candidates, name) + } + suggestions := fzf.Top(funcName, candidates, 3) + if len(suggestions) > 0 { + parts := make([]string, len(suggestions)) + for i, s := range suggestions { + parts[i] = s + "()" + } + return "No function named " + funcName + "(). Did you mean " + joinOr(parts) + "?", nil + } + return "No function named " + funcName + "()", nil } if callSignature.ParamCount == -1 { if argsCount == 0 { @@ -131,7 +200,7 @@ func (f *FuncCall) String() string { func (f *FuncCall) Blockly(flags ...bool) ast.Block { errorMessage, signature := TestSignature(f.Name, len(f.Args)) if signature == nil { - panic(errorMessage) + f.Where.Error(errorMessage) } if len(flags) > 0 && !flags[0] && !f.Consumable() { f.Where.Error("Expected a consumable but got a statement") @@ -158,7 +227,7 @@ func (f *FuncCall) Blockly(flags ...bool) ast.Block { return f.modeOf() case "mod", "rem", "quot": return f.mathDivide() - case "aTan2": + case "atan2": return f.atan2() case "formatDecimal": return f.formatDecimal() @@ -211,7 +280,7 @@ func (f *FuncCall) Continuous() bool { return true } -func (f *FuncCall) Consumable(flags ...bool) bool { +func (f *FuncCall) Consumable() bool { if f.Name == "setRandSeed" || f.Name == "println" || f.Name == "openScreen" || f.Name == "openScreenWithValue" || f.Name == "closeScreen" || f.Name == "closeScreenWithValue" || @@ -223,9 +292,12 @@ func (f *FuncCall) Consumable(flags ...bool) bool { } func (f *FuncCall) Signature() []ast.Signature { + for _, arg := range f.Args { + arg.Signature() + } errorMessage, signature := TestSignature(f.Name, len(f.Args)) // signatures are already verified if signature == nil { - panic(errorMessage) + f.Where.Error(errorMessage) } return []ast.Signature{signature.Signature} } @@ -320,7 +392,7 @@ func (f *FuncCall) genericSet() ast.Block { func (f *FuncCall) splitColor() ast.Block { return ast.Block{ - Type: "color_make_color", + Type: "color_split_color", Values: ast.MakeValues(f.Args, "COLOR"), } } diff --git a/lang/code/ast/common/question.go b/lang/code/ast/common/question.go index 67f704d0..6111cb3b 100644 --- a/lang/code/ast/common/question.go +++ b/lang/code/ast/common/question.go @@ -3,14 +3,64 @@ package common import ( "Falcon/code/ast" "Falcon/code/ast/fundamentals" + "Falcon/code/fzf" "Falcon/code/lex" "Falcon/code/sugar" ) +var questionKeywords = []string{ + "number", "base10", "hexa", "bin", + "text", "list", "dict", + "emptyText", "emptyList", + "even", "odd", +} + +// IsKnownQuestion reports whether name is a valid question keyword. +func IsKnownQuestion(name string) bool { + for _, k := range questionKeywords { + if k == name { + return true + } + } + return false +} + +// FindBestQuestionSuggestion returns the closest question keyword for a method-call-style name. +// It strips a leading "isX" camelCase prefix (e.g. "isNumber" → "number") before fuzzy matching. +// Returns "" when no candidate clears the fzf threshold. +func FindBestQuestionSuggestion(methodName string) string { + base := stripIsPrefix(methodName) + if IsKnownQuestion(base) { + return base + } + if tops := fzf.Top(base, questionKeywords, 1); len(tops) > 0 { + return tops[0] + } + if base != methodName { + if tops := fzf.Top(methodName, questionKeywords, 1); len(tops) > 0 { + return tops[0] + } + } + return "" +} + +// stripIsPrefix removes a leading "is" + uppercase letter prefix from a camelCase name +// and lowercases the first character of the remainder (e.g. "isNumber" → "number"). +func stripIsPrefix(name string) string { + if len(name) > 2 && name[0] == 'i' && name[1] == 's' { + rest := name[2:] + if len(rest) > 0 && rest[0] >= 'A' && rest[0] <= 'Z' { + return string(rest[0]+'a'-'A') + rest[1:] + } + } + return name +} + type Question struct { - Where *lex.Token - On ast.Expr - Question string + Where *lex.Token + On ast.Expr + Question string + MethodCallSyntax bool // true when parsed from .name() or .isName() notation } func (q *Question) String() string { @@ -47,11 +97,12 @@ func (q *Question) Continuous() bool { return false } -func (q *Question) Consumable(flags ...bool) bool { +func (q *Question) Consumable() bool { return true } func (q *Question) Signature() []ast.Signature { + q.On.Signature() return []ast.Signature{ast.SignBool} } diff --git a/lang/code/ast/common/transform_call.go b/lang/code/ast/common/transform_call.go index ad010832..30789c25 100644 --- a/lang/code/ast/common/transform_call.go +++ b/lang/code/ast/common/transform_call.go @@ -39,10 +39,11 @@ func (t *Transform) Continuous() bool { return true } -func (t *Transform) Consumable(flags ...bool) bool { +func (t *Transform) Consumable() bool { return false } func (t *Transform) Signature() []ast.Signature { + t.On.Signature() return []ast.Signature{ast.SignText} } diff --git a/lang/code/ast/components/event.go b/lang/code/ast/components/event.go index c8715bd5..4e9acb8e 100644 --- a/lang/code/ast/components/event.go +++ b/lang/code/ast/components/event.go @@ -38,10 +38,13 @@ func (e *Event) Continuous() bool { return false } -func (e *Event) Consumable(flags ...bool) bool { +func (e *Event) Consumable() bool { return false } func (e *Event) Signature() []ast.Signature { + for _, expr := range e.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/components/every_component.go b/lang/code/ast/components/every_component.go index 42ff1075..b648cfb5 100644 --- a/lang/code/ast/components/every_component.go +++ b/lang/code/ast/components/every_component.go @@ -25,7 +25,7 @@ func (e *EveryComponent) Continuous() bool { return true } -func (e *EveryComponent) Consumable(flags ...bool) bool { +func (e *EveryComponent) Consumable() bool { return true } diff --git a/lang/code/ast/components/generic_event.go b/lang/code/ast/components/generic_event.go index 2720eec9..7eaae479 100644 --- a/lang/code/ast/components/generic_event.go +++ b/lang/code/ast/components/generic_event.go @@ -35,10 +35,13 @@ func (g *GenericEvent) Continuous() bool { return false } -func (g *GenericEvent) Consumable(flags ...bool) bool { +func (g *GenericEvent) Consumable() bool { return false } func (g *GenericEvent) Signature() []ast.Signature { + for _, expr := range g.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/components/generic_method_call.go b/lang/code/ast/components/generic_method_call.go index 415ebc5f..2cfcc06c 100644 --- a/lang/code/ast/components/generic_method_call.go +++ b/lang/code/ast/components/generic_method_call.go @@ -24,12 +24,17 @@ func (g *GenericMethodCall) String() string { } func (g *GenericMethodCall) Blockly(flags ...bool) ast.Block { + shape := "statement" + if g.Returning { + shape = "value" + } return ast.Block{ Type: "component_method", Mutation: &ast.Mutation{ MethodName: g.Method, IsGeneric: true, ComponentType: g.ComponentType, + Shape: shape, }, Values: ast.ValueArgsByPrefix(g.Component, "COMPONENT", "ARG", g.Args), } @@ -39,10 +44,14 @@ func (g *GenericMethodCall) Continuous() bool { return false } -func (g *GenericMethodCall) Consumable(flags ...bool) bool { - return false // play safe, may be consumable too +func (g *GenericMethodCall) Consumable() bool { + return g.Returning } func (g *GenericMethodCall) Signature() []ast.Signature { + g.Component.Signature() + for _, arg := range g.Args { + arg.Signature() + } return []ast.Signature{ast.SignAny} } diff --git a/lang/code/ast/components/generic_property_get.go b/lang/code/ast/components/generic_property_get.go index da35ea5d..896ec5f7 100644 --- a/lang/code/ast/components/generic_property_get.go +++ b/lang/code/ast/components/generic_property_get.go @@ -33,10 +33,11 @@ func (g *GenericPropertyGet) Continuous() bool { return false } -func (g *GenericPropertyGet) Consumable(flags ...bool) bool { +func (g *GenericPropertyGet) Consumable() bool { return true } func (g *GenericPropertyGet) Signature() []ast.Signature { + g.Component.Signature() return []ast.Signature{ast.SignAny} } diff --git a/lang/code/ast/components/generic_property_set.go b/lang/code/ast/components/generic_property_set.go index 4d2fdc35..8d2f913b 100644 --- a/lang/code/ast/components/generic_property_set.go +++ b/lang/code/ast/components/generic_property_set.go @@ -34,10 +34,12 @@ func (g *GenericPropertySet) Continuous() bool { return false } -func (g *GenericPropertySet) Consumable(flags ...bool) bool { +func (g *GenericPropertySet) Consumable() bool { return false } func (g *GenericPropertySet) Signature() []ast.Signature { + g.Component.Signature() + g.Value.Signature() return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/components/method_call.go b/lang/code/ast/components/method_call.go index 40d19933..735262aa 100644 --- a/lang/code/ast/components/method_call.go +++ b/lang/code/ast/components/method_call.go @@ -34,10 +34,13 @@ func (m *MethodCall) Continuous() bool { return false } -func (m *MethodCall) Consumable(flags ...bool) bool { - return false // may be consumable too +func (m *MethodCall) Consumable() bool { + return false } func (m *MethodCall) Signature() []ast.Signature { + for _, arg := range m.Args { + arg.Signature() + } return []ast.Signature{ast.SignAny} } diff --git a/lang/code/ast/components/property_get.go b/lang/code/ast/components/property_get.go index edb4020f..df01f383 100644 --- a/lang/code/ast/components/property_get.go +++ b/lang/code/ast/components/property_get.go @@ -36,7 +36,7 @@ func (p *PropertyGet) Continuous() bool { return false } -func (p *PropertyGet) Consumable(flags ...bool) bool { +func (p *PropertyGet) Consumable() bool { return true } diff --git a/lang/code/ast/components/property_set.go b/lang/code/ast/components/property_set.go index 6be11a32..996cab5b 100644 --- a/lang/code/ast/components/property_set.go +++ b/lang/code/ast/components/property_set.go @@ -43,10 +43,11 @@ func (p *PropertySet) Continuous() bool { return false } -func (p *PropertySet) Consumable(flags ...bool) bool { +func (p *PropertySet) Consumable() bool { return false } func (p *PropertySet) Signature() []ast.Signature { + p.Value.Signature() return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/control/break.go b/lang/code/ast/control/break.go index 81def234..2b76da43 100644 --- a/lang/code/ast/control/break.go +++ b/lang/code/ast/control/break.go @@ -20,7 +20,7 @@ func (b *Break) Continuous() bool { return true } -func (b *Break) Consumable(flags ...bool) bool { +func (b *Break) Consumable() bool { return false } diff --git a/lang/code/ast/control/do.go b/lang/code/ast/control/do.go index c36ff39e..4369bb2a 100644 --- a/lang/code/ast/control/do.go +++ b/lang/code/ast/control/do.go @@ -25,10 +25,13 @@ func (d *Do) Continuous() bool { return false } -func (d *Do) Consumable(flags ...bool) bool { +func (d *Do) Consumable() bool { return false } func (d *Do) Signature() []ast.Signature { - return []ast.Signature{ast.SignVoid} + for _, expr := range d.Body { + expr.Signature() + } + return d.Result.Signature() } diff --git a/lang/code/ast/control/each.go b/lang/code/ast/control/each.go index 4d36967a..45273f59 100644 --- a/lang/code/ast/control/each.go +++ b/lang/code/ast/control/each.go @@ -2,10 +2,12 @@ package control import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type Each struct { + Where *lex.Token IName string Iterable ast.Expr Body []ast.Expr @@ -28,10 +30,14 @@ func (e *Each) Continuous() bool { return false } -func (e *Each) Consumable(flags ...bool) bool { +func (e *Each) Consumable() bool { return false } func (e *Each) Signature() []ast.Signature { + e.Iterable.Signature() + for _, expr := range e.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/control/each_pair.go b/lang/code/ast/control/each_pair.go index ed1a5ff3..5c8c3174 100644 --- a/lang/code/ast/control/each_pair.go +++ b/lang/code/ast/control/each_pair.go @@ -2,10 +2,12 @@ package control import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type EachPair struct { + Where *lex.Token KeyName string ValueName string Iterable ast.Expr @@ -32,10 +34,14 @@ func (e *EachPair) Continuous() bool { return false } -func (e *EachPair) Consumable(flags ...bool) bool { +func (e *EachPair) Consumable() bool { return false } func (e *EachPair) Signature() []ast.Signature { + e.Iterable.Signature() + for _, expr := range e.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/control/for.go b/lang/code/ast/control/for.go index 6674445a..015aab75 100644 --- a/lang/code/ast/control/for.go +++ b/lang/code/ast/control/for.go @@ -2,10 +2,12 @@ package control import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type For struct { + Where *lex.Token IName string From ast.Expr To ast.Expr @@ -31,10 +33,16 @@ func (f *For) Continuous() bool { return false } -func (f *For) Consumable(flags ...bool) bool { +func (f *For) Consumable() bool { return false } func (f *For) Signature() []ast.Signature { + f.From.Signature() + f.To.Signature() + f.By.Signature() + for _, expr := range f.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/control/if.go b/lang/code/ast/control/if.go index 01f393ca..6415c747 100644 --- a/lang/code/ast/control/if.go +++ b/lang/code/ast/control/if.go @@ -2,17 +2,21 @@ package control import ( "Falcon/code/ast" + "Falcon/code/lex" "strings" ) type If struct { + Where *lex.Token Conditions []ast.Expr Bodies [][]ast.Expr ElseBody []ast.Expr + + mutated bool + mutation ast.Expr } func (i *If) String() string { - // TODO: accept flags here too var builder strings.Builder numConditions := len(i.Conditions) @@ -40,6 +44,21 @@ func (i *If) String() string { return builder.String() } +func (i *If) Decompose(nthBody int) *If { + nextIf := If{ + Where: i.Where, + Conditions: i.Conditions[nthBody:], + Bodies: i.Bodies[nthBody:], + ElseBody: i.ElseBody, + mutated: false, + mutation: nil, + } + i.Conditions = i.Conditions[:nthBody] + i.Bodies = i.Bodies[:nthBody] + i.ElseBody = nil + return &nextIf +} + func (i *If) Blockly(flags ...bool) ast.Block { if len(flags) > 0 && !flags[0] { // Default to an if expression @@ -76,6 +95,8 @@ func (i *If) createSimpleIf() ast.Block { simpleIf := MakeSimpleIf(condition, then, currElseBlock) currElseBlock = []ast.Expr{simpleIf} } + i.mutated = true + i.mutation = currElseBlock[0] return currElseBlock[0].Blockly() } @@ -83,10 +104,51 @@ func (i *If) Continuous() bool { return false } -func (i *If) Consumable(flags ...bool) bool { - return false +func (i *If) Consumable() bool { + if i.mutated { + return true + } + if i.ElseBody == nil || len(i.ElseBody) == 0 { + return false + } + for _, body := range i.Bodies { + if len(body) == 0 || !body[len(body)-1].Consumable() { + return false + } + } + if !i.ElseBody[len(i.ElseBody)-1].Consumable() { + return false + } + return true } func (i *If) Signature() []ast.Signature { - return []ast.Signature{ast.SignVoid} + for _, cond := range i.Conditions { + cond.Signature() + } + for _, body := range i.Bodies { + for _, expr := range body { + expr.Signature() + } + } + for _, expr := range i.ElseBody { + expr.Signature() + } + if i.ElseBody == nil || len(i.ElseBody) == 0 { + return []ast.Signature{ast.SignVoid} + } + for _, body := range i.Bodies { + if len(body) == 0 || !body[len(body)-1].Consumable() { + return []ast.Signature{ast.SignVoid} + } + } + if !i.ElseBody[len(i.ElseBody)-1].Consumable() { + return []ast.Signature{ast.SignVoid} + } + var result []ast.Signature + for _, body := range i.Bodies { + result = ast.CombineSignatures(result, body[len(body)-1].Signature()) + } + result = ast.CombineSignatures(result, i.ElseBody[len(i.ElseBody)-1].Signature()) + return result } diff --git a/lang/code/ast/control/if_expr.go b/lang/code/ast/control/if_expr.go index 1b87d2dd..26e99e56 100644 --- a/lang/code/ast/control/if_expr.go +++ b/lang/code/ast/control/if_expr.go @@ -17,6 +17,10 @@ type SimpleIf struct { normalElse []ast.Expr } +func (s *SimpleIf) Condition() ast.Expr { return s.condition } +func (s *SimpleIf) Then() []ast.Expr { return s.normalThen } +func (s *SimpleIf) Else() []ast.Expr { return s.normalElse } + func MakeSimpleIf(condition ast.Expr, then []ast.Expr, elze []ast.Expr) *SimpleIf { return &SimpleIf{ condition: condition, @@ -28,6 +32,9 @@ func MakeSimpleIf(condition ast.Expr, then []ast.Expr, elze []ast.Expr) *SimpleI } func (s *SimpleIf) String() string { + if len(s.normalThen) == 0 || len(s.normalElse) == 0 { + panic("SimpleIf.String: empty then or else body") + } var branches []string currIf := s var hasDiscontinuity = false @@ -44,8 +51,13 @@ func (s *SimpleIf) String() string { } var thenString string if len(currIf.normalThen) == 1 { - ifFormat += "if (%) % " thenString = currIf.normalThen[0].String() + if strings.ContainsRune(thenString, '\n') || strings.HasPrefix(thenString, "{") { + ifFormat += "if (%) {\n%} " + thenString = ast.PadBody(currIf.normalThen) + } else { + ifFormat += "if (%) % " + } } else { ifFormat += "if (%) {\n%} " thenString = ast.PadBody(currIf.normalThen) @@ -62,8 +74,13 @@ func (s *SimpleIf) String() string { var elseFormat string var elseString string if len(currIf.normalElse) == 1 { - elseFormat = "else %" elseString = currIf.normalElse[0].String() + if strings.ContainsRune(elseString, '\n') || strings.HasPrefix(elseString, "{") { + elseFormat = "else {\n%}" + elseString = ast.PadBody(currIf.normalElse) + } else { + elseFormat = "else %" + } } else { elseFormat = "else {\n%}" elseString = ast.PadBody(currIf.normalElse) @@ -80,6 +97,8 @@ func (s *SimpleIf) String() string { } func (s *SimpleIf) Blockly(flags ...bool) ast.Block { + // SimpleIf is always an expression (controls_choose). + // It should never appear in a statement position. if len(flags) > 0 && flags[0] { // Blockly expects a statement here, we have to mutate fullIf := If{ @@ -99,10 +118,17 @@ func (s *SimpleIf) Continuous() bool { return false } -func (s *SimpleIf) Consumable(flags ...bool) bool { - return !(len(flags) > 0 && flags[0]) +func (s *SimpleIf) Consumable() bool { + return true } func (s *SimpleIf) Signature() []ast.Signature { + s.condition.Signature() + for _, expr := range s.normalThen { + expr.Signature() + } + for _, expr := range s.normalElse { + expr.Signature() + } return ast.CombineSignatures(s.smartThen.Signature(), s.smartElse.Signature()) } diff --git a/lang/code/ast/control/while.go b/lang/code/ast/control/while.go index 643ccc69..998fa2b6 100644 --- a/lang/code/ast/control/while.go +++ b/lang/code/ast/control/while.go @@ -2,10 +2,12 @@ package control import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type While struct { + Where *lex.Token Condition ast.Expr Body []ast.Expr } @@ -26,10 +28,14 @@ func (w *While) Continuous() bool { return false } -func (w *While) Consumable(flags ...bool) bool { +func (w *While) Consumable() bool { return false } func (w *While) Signature() []ast.Signature { + w.Condition.Signature() + for _, expr := range w.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/control/yield.go b/lang/code/ast/control/yield.go new file mode 100644 index 00000000..a48b1a35 --- /dev/null +++ b/lang/code/ast/control/yield.go @@ -0,0 +1,32 @@ +package control + +import ( + "Falcon/code/ast" +) + +// Yield is a statement that immediately exits the enclosing function and +// returns the given value to the caller. It behaves like an early return. +type Yield struct { + Expr ast.Expr +} + +func (y *Yield) String() string { + return "yield " + y.Expr.String() +} + +func (y *Yield) Blockly(flags ...bool) ast.Block { + return ast.Block{Type: "controls_yield"} +} + +func (y *Yield) Continuous() bool { + return true +} + +func (y *Yield) Consumable() bool { + return false +} + +func (y *Yield) Signature() []ast.Signature { + y.Expr.Signature() + return []ast.Signature{ast.SignVoid} +} diff --git a/lang/code/ast/expr.go b/lang/code/ast/expr.go index c6ef9298..a92db21e 100644 --- a/lang/code/ast/expr.go +++ b/lang/code/ast/expr.go @@ -4,7 +4,7 @@ type Expr interface { String() string Blockly(flags ...bool) Block Continuous() bool - Consumable(flags ...bool) bool + Consumable() bool Signature() []Signature } diff --git a/lang/code/ast/fundamentals/boolean.go b/lang/code/ast/fundamentals/boolean.go index b85d43e0..2fe77f7a 100644 --- a/lang/code/ast/fundamentals/boolean.go +++ b/lang/code/ast/fundamentals/boolean.go @@ -33,7 +33,7 @@ func (b *Boolean) Continuous() bool { return true } -func (b *Boolean) Consumable(flags ...bool) bool { +func (b *Boolean) Consumable() bool { return true } @@ -60,10 +60,11 @@ func (n *Not) Continuous() bool { return false } -func (n *Not) Consumable(flags ...bool) bool { +func (n *Not) Consumable() bool { return true } func (n *Not) Signature() []ast.Signature { + n.Expr.Signature() return []ast.Signature{ast.SignBool} } diff --git a/lang/code/ast/fundamentals/color.go b/lang/code/ast/fundamentals/color.go index 66d30c39..6ed372fe 100644 --- a/lang/code/ast/fundamentals/color.go +++ b/lang/code/ast/fundamentals/color.go @@ -25,7 +25,7 @@ func (c *Color) Continuous() bool { return true } -func (c *Color) Consumable(flags ...bool) bool { +func (c *Color) Consumable() bool { return true } diff --git a/lang/code/ast/fundamentals/component.go b/lang/code/ast/fundamentals/component.go index 10d6412c..b94ba7ee 100644 --- a/lang/code/ast/fundamentals/component.go +++ b/lang/code/ast/fundamentals/component.go @@ -25,7 +25,7 @@ func (c *Component) Continuous() bool { return true } -func (c *Component) Consumable(flags ...bool) bool { +func (c *Component) Consumable() bool { return true } diff --git a/lang/code/ast/fundamentals/dictionary.go b/lang/code/ast/fundamentals/dictionary.go index 282dfca7..77e56af0 100644 --- a/lang/code/ast/fundamentals/dictionary.go +++ b/lang/code/ast/fundamentals/dictionary.go @@ -25,11 +25,14 @@ func (d *Dictionary) Continuous() bool { return true } -func (d *Dictionary) Consumable(flags ...bool) bool { +func (d *Dictionary) Consumable() bool { return true } func (d *Dictionary) Signature() []ast.Signature { + for _, elem := range d.Elements { + elem.Signature() + } return []ast.Signature{ast.SignDict} } @@ -53,11 +56,16 @@ func (p *Pair) Continuous() bool { return false } -func (p *Pair) Consumable(flags ...bool) bool { +func (p *Pair) Consumable() bool { return true } func (p *Pair) Signature() []ast.Signature { + if p.Key == nil || p.Value == nil { + panic("Pair.Signature: nil key or value") + } + p.Key.Signature() + p.Value.Signature() return []ast.Signature{ast.SignList} } @@ -76,10 +84,10 @@ func (w *WalkAll) Continuous() bool { return true } -func (w *WalkAll) Consumable(flags ...bool) bool { +func (w *WalkAll) Consumable() bool { return true } func (w *WalkAll) Signature() []ast.Signature { - return []ast.Signature{ast.SignText} + return []ast.Signature{ast.SignList} } diff --git a/lang/code/ast/fundamentals/helper.go b/lang/code/ast/fundamentals/helper.go index 025c2350..a57fd306 100644 --- a/lang/code/ast/fundamentals/helper.go +++ b/lang/code/ast/fundamentals/helper.go @@ -25,7 +25,7 @@ func (h *HelperDropdown) Continuous() bool { return true } -func (h *HelperDropdown) Consumable(flags ...bool) bool { +func (h *HelperDropdown) Consumable() bool { return true } diff --git a/lang/code/ast/fundamentals/list.go b/lang/code/ast/fundamentals/list.go index 5e308280..74b709e0 100644 --- a/lang/code/ast/fundamentals/list.go +++ b/lang/code/ast/fundamentals/list.go @@ -25,10 +25,13 @@ func (l *List) Continuous() bool { return true } -func (l *List) Consumable(flags ...bool) bool { +func (l *List) Consumable() bool { return true } func (l *List) Signature() []ast.Signature { + for _, elem := range l.Elements { + elem.Signature() + } return []ast.Signature{ast.SignList} } diff --git a/lang/code/ast/fundamentals/number.go b/lang/code/ast/fundamentals/number.go index b95aa458..ef570d72 100644 --- a/lang/code/ast/fundamentals/number.go +++ b/lang/code/ast/fundamentals/number.go @@ -23,7 +23,7 @@ func (n *Number) Continuous() bool { return true } -func (n *Number) Consumable(flags ...bool) bool { +func (n *Number) Consumable() bool { return true } diff --git a/lang/code/ast/fundamentals/smart_body.go b/lang/code/ast/fundamentals/smart_body.go index 55a6af06..f47b2c31 100644 --- a/lang/code/ast/fundamentals/smart_body.go +++ b/lang/code/ast/fundamentals/smart_body.go @@ -11,10 +11,21 @@ type SmartBody struct { } func (s *SmartBody) String() string { + if len(s.Body) == 1 { + if _, ok := s.Body[0].(*variables.VarResult); ok { + return s.Body[0].String() + } + if _, ok := s.Body[0].(*SmartBody); ok { + return s.Body[0].String() + } + } return sugar.Format("{\n%}", ast.PadBody(s.Body)) } func (s *SmartBody) Blockly(flags ...bool) ast.Block { + if len(s.Body) == 0 { + panic("SmartBody.Blockly: empty body") + } // a single expression, just inline it if v, ok := s.Body[0].(*variables.Var); ok { // it's a var body, but we want a var result! @@ -29,20 +40,7 @@ func (s *SmartBody) Blockly(flags ...bool) ast.Block { if len(s.Body) == 1 { return s.Body[0].Blockly(flags...) } - // prepare a do expression out of the then - doExpr := s.createDoSmt(s.Body[len(s.Body)-1], s.Body[:len(s.Body)-1]) - - var namesLocal = s.mutateVars() - if len(namesLocal) == 0 { - // no variables declared in the then, a do expression is enough - return doExpr - } - // We'd need to use a local result expression - var defaultLocalVals []ast.Expr - for k := range defaultLocalVals { - defaultLocalVals[k] = &Boolean{Value: false} - } - return s.createLocalResult(namesLocal, defaultLocalVals, doExpr) + return s.createDoSmt(s.Body[len(s.Body)-1], s.Body[:len(s.Body)-1]) } func (s *SmartBody) createLocalResult(names []string, values []ast.Expr, doExpr ast.Block) ast.Block { @@ -69,14 +67,33 @@ func (s *SmartBody) createDoSmt(doResult ast.Expr, doBody []ast.Expr) ast.Block } doExpr = doResult.Blockly(false) } else { - if !doResult.Consumable() { - panic("Cannot include a statement for the required variable result") - } - doExpr = ast.Block{ - Type: "controls_do_then_return", - Statements: ast.OptionalStatement("STM", doBody), - // TODO: we have set the flag to false, previously was true, verify effects - Values: []ast.Value{{Name: "VALUE", Block: doResult.Blockly(false)}}, + if v, ok := doResult.(*variables.Var); ok { + // The result is a var-body (non-consumable), but it contains a + // consumable result buried inside. Build a local_declaration_expression + // for the Var, then wrap the whole thing in a controls_do_then_return. + var innerDoExpr ast.Block + if len(v.Body) > 0 { + innerDoExpr = s.createDoSmt(v.Body[len(v.Body)-1], v.Body[:len(v.Body)-1]) + } else { + innerDoExpr = createEmptyDoSmt(v) + } + valueExpr := s.createLocalResult(v.Names, v.Values, innerDoExpr) + doExpr = ast.Block{ + Type: "controls_do_then_return", + Statements: ast.OptionalStatement("STM", doBody), + Values: []ast.Value{{Name: "VALUE", Block: valueExpr}}, + } + } else { + resultExpr := doResult.Blockly(false) + if !doResult.Consumable() { + panic("Cannot include a statement for the required variable result") + } + doExpr = ast.Block{ + Type: "controls_do_then_return", + Statements: ast.OptionalStatement("STM", doBody), + // TODO: we have set the flag to false, previously was true, verify effects + Values: []ast.Value{{Name: "VALUE", Block: resultExpr}}, + } } } return doExpr @@ -89,29 +106,20 @@ func createEmptyDoSmt(v *variables.Var) ast.Block { } } -// mutateVars returns a name list of declared variables, and the declarations are mutated to a set call. -// The variables will later be defined at the top. -func (s *SmartBody) mutateVars() []string { - var names []string - for k, expr := range s.Body { - // We only have simple variables - if e, ok := expr.(*variables.SimpleVar); ok { - names = append(names, e.Name) - // Mutate it to a set function - s.Body[k] = &variables.Set{Global: false, Name: e.Name, Expr: e.Value} - } - } - return names -} - func (s *SmartBody) Continuous() bool { return false } -func (s *SmartBody) Consumable(flags ...bool) bool { +func (s *SmartBody) Consumable() bool { return true } func (s *SmartBody) Signature() []ast.Signature { + if len(s.Body) == 0 { + panic("SmartBody.Signature: empty body") + } + for _, expr := range s.Body { + expr.Signature() + } return s.Body[len(s.Body)-1].Signature() } diff --git a/lang/code/ast/fundamentals/text.go b/lang/code/ast/fundamentals/text.go index bbcc412f..6050f51c 100644 --- a/lang/code/ast/fundamentals/text.go +++ b/lang/code/ast/fundamentals/text.go @@ -26,7 +26,7 @@ func (t *Text) Continuous() bool { return true } -func (t *Text) Consumable(flags ...bool) bool { +func (t *Text) Consumable() bool { return true } diff --git a/lang/code/ast/fundamentals/yield.go b/lang/code/ast/fundamentals/yield.go new file mode 100644 index 00000000..73b029c3 --- /dev/null +++ b/lang/code/ast/fundamentals/yield.go @@ -0,0 +1,51 @@ +package fundamentals + +import "Falcon/code/ast" + +type Yield struct { + Expr ast.Expr + TransformedExpr ast.Expr + UseTransformed bool +} + +func (y *Yield) GetExpr() ast.Expr { + if y.UseTransformed { + return y.TransformedExpr + } + return y.Expr +} + +func (y *Yield) String() string { + if y.UseTransformed { + return y.TransformedExpr.String() + } + return y.Expr.String() +} + +func (y *Yield) Blockly(flags ...bool) ast.Block { + if y.UseTransformed { + return y.TransformedExpr.Blockly(flags...) + } + return y.Expr.Blockly(flags...) +} + +func (y *Yield) Continuous() bool { + if y.UseTransformed { + return y.TransformedExpr.Continuous() + } + return y.Expr.Continuous() +} + +func (y *Yield) Consumable() bool { + if y.UseTransformed { + return y.TransformedExpr.Consumable() + } + return y.Expr.Consumable() +} + +func (y *Yield) Signature() []ast.Signature { + if y.UseTransformed { + return y.TransformedExpr.Signature() + } + return y.Expr.Signature() +} diff --git a/lang/code/ast/list/get.go b/lang/code/ast/list/get.go index 4a2ef504..1de38931 100644 --- a/lang/code/ast/list/get.go +++ b/lang/code/ast/list/get.go @@ -2,10 +2,12 @@ package list import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type Get struct { + Where *lex.Token List ast.Expr Index ast.Expr } @@ -29,10 +31,16 @@ func (g *Get) Continuous() bool { return true } -func (g *Get) Consumable(flags ...bool) bool { +func (g *Get) Consumable() bool { return true } func (g *Get) Signature() []ast.Signature { + g.List.Signature() + g.Index.Signature() + listSigs := g.List.Signature() + if !ast.HasSignature(listSigs, ast.SignList) { + g.Where.TypeError("List index access requires a list value, but got %", ast.FormatSignatures(listSigs)) + } return []ast.Signature{ast.SignAny} } diff --git a/lang/code/ast/list/set.go b/lang/code/ast/list/set.go index 96bf2756..5a3136e6 100644 --- a/lang/code/ast/list/set.go +++ b/lang/code/ast/list/set.go @@ -2,10 +2,12 @@ package list import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type Set struct { + Where *lex.Token List ast.Expr Index ast.Expr Value ast.Expr @@ -30,10 +32,17 @@ func (s *Set) Continuous() bool { return false } -func (s *Set) Consumable(flags ...bool) bool { +func (s *Set) Consumable() bool { return false } func (s *Set) Signature() []ast.Signature { + s.List.Signature() + s.Index.Signature() + s.Value.Signature() + listSigs := s.List.Signature() + if !ast.HasSignature(listSigs, ast.SignList) { + s.Where.TypeError("List index assignment requires a list value, but got %", ast.FormatSignatures(listSigs)) + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/list/transform.go b/lang/code/ast/list/transform.go index 7ad5d36b..7635b9ac 100644 --- a/lang/code/ast/list/transform.go +++ b/lang/code/ast/list/transform.go @@ -2,6 +2,9 @@ package list import ( "Falcon/code/ast" + "Falcon/code/ast/control" + "Falcon/code/ast/fundamentals" + "Falcon/code/ast/variables" "Falcon/code/lex" "Falcon/code/sugar" "strconv" @@ -36,6 +39,11 @@ var transformers = map[string]*TransformerSignature{ "max": makeSignature(0, 2), } +func IsTransformer(name string, argCount int) bool { + sig, ok := transformers[name] + return ok && sig.ArgSize == argCount +} + func TestSignature(transformerName string, argsCount int, namesCount int) (string, *TransformerSignature) { signature, ok := transformers[transformerName] if !ok { @@ -53,34 +61,72 @@ func TestSignature(transformerName string, argsCount int, namesCount int) (strin } func (t *Transformer) String() string { + switch t.Transformer.(type) { + case *control.Do, *variables.VarResult: + return t.bodyTransformerString(t.Transformer) + default: + if sb, ok := t.Transformer.(*fundamentals.SmartBody); ok && len(sb.Body) == 1 { + switch sb.Body[0].(type) { + case *variables.VarResult, *variables.Var: + return t.bodyTransformerString(sb.Body[0]) + } + } + } + return t.singleExprTransformerString() +} + +func (t *Transformer) singleExprTransformerString() string { if len(t.Args) == 0 { pFormat := "%\n .% { % -> % }" if !t.List.Continuous() { - pFormat = "(%)\n .% { % -> %} " + pFormat = "(%)\n .% { % -> % } " } return sugar.Format(pFormat, t.List.String(), t.Name, strings.Join(t.Names, ", "), t.Transformer.String()) - } else { - pFormat := "%\n .%(%) { % -> % }" + } + pFormat := "%\n .%(%) { % -> % }" + if !t.List.Continuous() { + pFormat = "(%)\n .%(%) { % -> % }" + } + return sugar.Format(pFormat, + t.List.String(), + t.Name, + ast.JoinExprs(", ", t.Args), + strings.Join(t.Names, ", "), + t.Transformer.String()) +} + +func (t *Transformer) bodyTransformerString(do ast.Expr) string { + if len(t.Args) == 0 { + pFormat := "%\n .% { % -> \n%}" if !t.List.Continuous() { - pFormat = "(%)\n .%(%) { % -> % }" + pFormat = "(%)\n .% { % -> \n%} " } return sugar.Format(pFormat, t.List.String(), t.Name, - ast.JoinExprs(", ", t.Args), strings.Join(t.Names, ", "), - t.Transformer.String()) + ast.PadDirect(ast.Pad(do.String()))) + } + pFormat := "%\n .%(%) { % -> \n%}" + if !t.List.Continuous() { + pFormat = "(%)\n .%(%) { % -> \n%}" } + return sugar.Format(pFormat, + t.List.String(), + t.Name, + ast.JoinExprs(", ", t.Args), + strings.Join(t.Names, ", "), + ast.PadDirect(ast.Pad(do.String()))) } func (t *Transformer) Blockly(flags ...bool) ast.Block { errorMessage, signature := TestSignature(t.Name, len(t.Args), len(t.Names)) if signature == nil { - panic(errorMessage) + t.Where.Error(errorMessage) } switch t.Name { case "map": @@ -107,14 +153,23 @@ func (t *Transformer) Continuous() bool { return true } -func (t *Transformer) Consumable(flags ...bool) bool { +func (t *Transformer) Consumable() bool { return true } func (t *Transformer) Signature() []ast.Signature { + t.List.Signature() + for _, arg := range t.Args { + arg.Signature() + } + t.Transformer.Signature() + listSigs := t.List.Signature() + if !ast.HasSignature(listSigs, ast.SignList) { + t.Where.TypeError("List transformer .% { } requires a list value, but got %", t.Name, ast.FormatSignatures(listSigs)) + } errorMessage, transformerSignature := TestSignature(t.Name, len(t.Args), len(t.Names)) if transformerSignature == nil { - panic(errorMessage) + t.Where.Error(errorMessage) } // TODO: this has to be improved when we are improving type safety if t.Name == "min" || t.Name == "max" || t.Name == "reduce" { diff --git a/lang/code/ast/method/autocorrect.go b/lang/code/ast/method/autocorrect.go new file mode 100644 index 00000000..d4cb1903 --- /dev/null +++ b/lang/code/ast/method/autocorrect.go @@ -0,0 +1,286 @@ +package method + +import ( + "Falcon/code/ast" + "Falcon/code/fzf" + "Falcon/code/lex" + "strings" +) + +// flattenChain flattens a nested method chain into left-to-right call order +// and returns the root receiver (the non-Call expression at the base). +// +// For s.a().b().c() (stored as c{b{a{s}}}), returns ([a, b, c], s). +func flattenChain(call *Call) ([]*Call, ast.Expr) { + var chain []*Call + curr := ast.Expr(call) + for { + if c, ok := curr.(*Call); ok { + chain = append(chain, c) + curr = c.On + } else { + break + } + } + for i, j := 0, len(chain)-1; i < j; i, j = i+1, j-1 { + chain[i], chain[j] = chain[j], chain[i] + } + return chain, curr +} + +// safeRootSignature returns the root expression's output signature, +// falling back to [SignAny] if Signature() panics. +func safeRootSignature(expr ast.Expr) (sigs []ast.Signature) { + defer func() { + if recover() != nil { + sigs = []ast.Signature{ast.SignAny} + } + }() + return expr.Signature() +} + +// moduleMatchesSig reports whether the given module ("text"/"list"/"dict") +// is compatible with the provided signatures. +func moduleMatchesSig(module string, sigs []ast.Signature) bool { + switch module { + case "text": + return ast.HasSignature(sigs, ast.SignText) + case "list": + return ast.HasSignature(sigs, ast.SignList) + case "dict": + return ast.HasSignature(sigs, ast.SignDict) + } + return false +} + +// signatureForModule converts a module name to its corresponding Signature constant. +func signatureForModule(module string) ast.Signature { + switch module { + case "text": + return ast.SignText + case "list": + return ast.SignList + case "dict": + return ast.SignDict + } + return ast.SignAny +} + +// minCorrectionScore is the minimum fzf.Score a candidate must reach to be +// considered as a correction. Using a lower threshold than fzf.Rank (0.2) so +// that prefix-match cases like "upper" → "uppercase" (score ≈ 0.18) are accepted. +const minCorrectionScore = 0.1 + +// argSigCompatible reports whether the provided argument signatures are compatible +// with the candidate method's declared parameter types. It checks only as many +// positions as are available in both slices; unmatched trailing params are ignored. +// A caller-side SignAny or a nil ParamSigs on the candidate means "any type OK". +func argSigCompatible(sig *CallSignature, argSigs [][]ast.Signature) bool { + if sig.ParamSigs == nil { + return true + } + for i := 0; i < len(sig.ParamSigs) && i < len(argSigs); i++ { + required := sig.ParamSigs[i] + if required == ast.SignAny { + continue + } + if !ast.HasSignature(argSigs[i], required) && !ast.HasSignature(argSigs[i], ast.SignAny) { + return false + } + } + return true +} + +// findBestCorrection returns the best replacement method name for wrongName such +// that the replacement accepts inputSigs, its param types are compatible with +// argSigs, and (when neededOutput is non-nil) it produces a compatible output type. +// Falls back to ignoring the output constraint if the constrained search yields nothing. +func findBestCorrection(wrongName string, inputSigs []ast.Signature, neededOutput *ast.Signature, argSigs [][]ast.Signature) string { + bestScore := -1.0 + bestName := "" + for name, sig := range signatures { + if name == wrongName { + continue + } + if !moduleMatchesSig(sig.Module, inputSigs) { + continue + } + if neededOutput != nil && sig.Signature != *neededOutput && sig.Signature != ast.SignAny { + continue + } + if !argSigCompatible(sig, argSigs) { + continue + } + if s := fzf.Score(wrongName, name); s > bestScore { + bestScore = s + bestName = name + } + } + if bestScore >= minCorrectionScore { + return bestName + } + if neededOutput != nil { + return findBestCorrection(wrongName, inputSigs, nil, argSigs) + } + return "" +} + +// tryDecomposeAnd attempts to split name into a sequence of valid method names +// using camelCase "And" (capital A) as the joiner delimiter. +// +// For example, "allButLastAndListLen" with inputSig=[SignList] returns +// ["allButLast", "listLen"] because the types chain correctly. +// Returns nil when no valid decomposition is found. +func tryDecomposeAnd(name string, inputSig []ast.Signature) []string { + // Base case: name is already a valid single method for this inputSig. + if sig, ok := signatures[name]; ok && moduleMatchesSig(sig.Module, inputSig) { + return []string{name} + } + // Try each "And" split point (capital A, preceded by a lowercase letter). + for i := 1; i+3 <= len(name); i++ { + if name[i] != 'A' || name[i+1] != 'n' || name[i+2] != 'd' { + continue + } + if name[i-1] < 'a' || name[i-1] > 'z' { + continue + } + rightRaw := name[i+3:] + if len(rightRaw) == 0 { + continue + } + left := name[:i] + // Lowercase the first character of the right part (join convention: + // "allButLast" + "listLen" → "allButLastAndListLen", so "ListLen" → "listLen"). + firstChar := rightRaw[0] + if firstChar >= 'A' && firstChar <= 'Z' { + firstChar += 'a' - 'A' + } + right := string(firstChar) + rightRaw[1:] + + leftSig, leftOk := signatures[left] + if !leftOk || !moduleMatchesSig(leftSig.Module, inputSig) { + continue + } + leftOutput := []ast.Signature{leftSig.Signature} + rest := tryDecomposeAnd(right, leftOutput) + if rest != nil { + return append([]string{left}, rest...) + } + } + return nil +} + +// applyDecomposition rewrites call c in-place to represent the last method in +// parts, inserting new Call nodes for all earlier parts between c.On and c. +// +// For parts = ["allButLast", "listLen"] and c originally being the merged call: +// +// c.On stays pointing to the original receiver +// a new Call{On: c.On, Name: "allButLast"} is created +// c.On is updated to the new inner call, c.Name = "listLen" +func applyDecomposition(c *Call, parts []string) { + inner := c.On + for _, part := range parts[:len(parts)-1] { + inner = &Call{Where: c.Where, On: inner, Name: part, Args: []ast.Expr{}} + } + c.On = inner + c.Name = parts[len(parts)-1] +} + +// Correction records a single name substitution made by CorrectChainAndCollect. +// Replacement is the text that replaces OldName at [Where.Row-len(OldName), Where.Row) +// in the source line. For AND-decompositions it is "a().b" style; for simple renames +// it is just the new method name. +type Correction struct { + Where *lex.Token + OldName string + Replacement string +} + +// CorrectChain inspects the full method chain rooted at call and rewrites any +// method names that are mismatched with their receiver types. Two strategies +// are applied in order for each invalid call: +// +// 1. And-decomposition: a merged name like "allButLastAndListLen" is split +// into two properly chained calls using "And" as the camelCase joiner. +// 2. Fuzzy rename: an unknown or wrong-module name like "upper" or "reverse" +// (on a list) is replaced with the closest matching valid method. +// +// Returns true if at least one correction was made. +func CorrectChain(call *Call) bool { + return CorrectChainAndCollect(call, nil) +} + +// CorrectChainAndCollect is like CorrectChain but also appends a Correction +// record for every name change it makes, enabling source-level reconstruction. +func CorrectChainAndCollect(call *Call, corrections *[]Correction) bool { + chain, root := flattenChain(call) + if len(chain) == 0 { + return false + } + + inputSig := safeRootSignature(root) + corrected := false + + for i, c := range chain { + sig, exists := signatures[c.Name] + isValid := exists && moduleMatchesSig(sig.Module, inputSig) + + if !isValid { + oldName := c.Name + // Strategy 1: try to split a merged "And"-joined name into two calls. + if parts := tryDecomposeAnd(c.Name, inputSig); len(parts) >= 2 { + applyDecomposition(c, parts) + sig = signatures[c.Name] + exists = sig != nil + corrected = true + if corrections != nil { + // Replacement text: "a().b" (last part keeps the original source parens) + *corrections = append(*corrections, Correction{ + Where: c.Where, + OldName: oldName, + Replacement: strings.Join(parts, "()."), + }) + } + } else { + // Strategy 2: fuzzy rename — find the closest valid single method. + // Scan forward past any consecutive invalid calls to find the first + // valid one; its input module constrains what this call must output. + var neededOutput *ast.Signature + for j := i + 1; j < len(chain); j++ { + if nextSig, ok := signatures[chain[j].Name]; ok { + needed := signatureForModule(nextSig.Module) + neededOutput = &needed + break + } + } + c.hintOutput = neededOutput // retained for error message generation if correction fails + argSigs := make([][]ast.Signature, len(c.Args)) + for i, arg := range c.Args { + argSigs[i] = safeRootSignature(arg) + } + if bestName := findBestCorrection(c.Name, inputSig, neededOutput, argSigs); bestName != "" { + c.Name = bestName + sig = signatures[bestName] + exists = true + corrected = true + if corrections != nil { + *corrections = append(*corrections, Correction{ + Where: c.Where, + OldName: oldName, + Replacement: bestName, + }) + } + } + } + } + + if exists && sig != nil { + inputSig = []ast.Signature{sig.Signature} + } else { + inputSig = []ast.Signature{ast.SignAny} + } + } + + return corrected +} diff --git a/lang/code/ast/method/call.go b/lang/code/ast/method/call.go index 1a5f7c91..b86b0238 100644 --- a/lang/code/ast/method/call.go +++ b/lang/code/ast/method/call.go @@ -2,30 +2,36 @@ package method import ( "Falcon/code/ast" + "Falcon/code/fzf" "Falcon/code/lex" "Falcon/code/sugar" "strconv" + "strings" ) type Call struct { - Where *lex.Token - On ast.Expr - Name string - Args []ast.Expr + Where *lex.Token + On ast.Expr + Name string + Args []ast.Expr + hintOutput *ast.Signature // nearest valid successor's input type, set by CorrectChain } type CallSignature struct { Module string BlocklyName string ParamCount int + Params string // human-readable parameter list, e.g. "key, notFound" Consumable bool Signature ast.Signature + ParamSigs []ast.Signature // nil = accept any; non-nil = required type per positional param } func makeSignature( module string, blocklyName string, paramCount int, + params string, consumable bool, signature ast.Signature, ) *CallSignature { @@ -33,81 +39,201 @@ func makeSignature( Module: module, BlocklyName: blocklyName, ParamCount: paramCount, + Params: params, Consumable: consumable, Signature: signature, } } +func makeSignatureTyped( + module string, + blocklyName string, + paramCount int, + params string, + consumable bool, + signature ast.Signature, + paramSigs []ast.Signature, +) *CallSignature { + cs := makeSignature(module, blocklyName, paramCount, params, consumable, signature) + cs.ParamSigs = paramSigs + return cs +} + var signatures = map[string]*CallSignature{ - "textLen": makeSignature("text", "text_length", 0, true, ast.SignNumb), - "trim": makeSignature("text", "text_trim", 0, true, ast.SignText), - "uppercase": makeSignature("text", "text_changeCase", 0, true, ast.SignText), - "lowercase": makeSignature("text", "text_changeCase", 0, true, ast.SignText), - "startsWith": makeSignature("text", "text_starts_at", 1, true, ast.SignBool), - "contains": makeSignature("text", "text_contains", 1, true, ast.SignBool), - "containsAny": makeSignature("text", "text_contains", 1, true, ast.SignBool), - "containsAll": makeSignature("text", "text_contains", 1, true, ast.SignBool), - "split": makeSignature("text", "text_split", 1, true, ast.SignList), - "splitAtFirst": makeSignature("text", "text_split", 1, true, ast.SignList), - "splitAtAny": makeSignature("text", "text_split", 1, true, ast.SignList), - "splitAtFirstOfAny": makeSignature("text", "text_split", 1, true, ast.SignList), - "splitAtSpaces": makeSignature("text", "text_split_at_spaces", 0, true, ast.SignList), - "reverse": makeSignature("text", "text_reverse", 0, true, ast.SignText), - "csvRowToList": makeSignature("text", "lists_from_csv_row", 0, true, ast.SignList), - "csvTableToList": makeSignature("text", "lists_from_csv_table", 0, true, ast.SignList), - "segment": makeSignature("text", "text_segment", 2, true, ast.SignText), - "replace": makeSignature("text", "text_replace_all", 2, true, ast.SignText), - "replaceFrom": makeSignature("text", "text_replace_mappings", 1, true, ast.SignText), - "replaceFromLongestFirst": makeSignature("text", "text_replace_mappings", 1, true, ast.SignText), - - "listLen": makeSignature("list", "lists_length", 0, true, ast.SignNumb), - "add": makeSignature("list", "lists_add_items", -1, false, ast.SignVoid), - "containsItem": makeSignature("list", "lists_is_in", 1, true, ast.SignBool), - "indexOf": makeSignature("list", "lists_position_in", 1, true, ast.SignNumb), - "insert": makeSignature("list", "lists_insert_item", 2, false, ast.SignVoid), - "remove": makeSignature("list", "lists_remove_item", 1, false, ast.SignVoid), - "appendList": makeSignature("list", "lists_append_list", 1, false, ast.SignVoid), - "lookupInPairs": makeSignature("list", "lists_lookup_in_pairs", 2, true, ast.SignAny), - "join": makeSignature("list", "lists_join_with_separator", 1, true, ast.SignText), - "slice": makeSignature("list", "lists_slice", 2, true, ast.SignList), - "random": makeSignature("list", "lists_pick_random_item", 0, true, ast.SignAny), - "reverseList": makeSignature("list", "lists_reverse", 0, true, ast.SignList), - "toCsvRow": makeSignature("list", "lists_to_csv_row", 0, true, ast.SignText), - "toCsvTable": makeSignature("list", "lists_to_csv_table", 0, true, ast.SignText), - "sort": makeSignature("list", "lists_sort", 0, true, ast.SignList), - "allButFirst": makeSignature("list", "lists_but_first", 0, true, ast.SignAny), - "allButLast": makeSignature("list", "lists_but_last", 0, true, ast.SignAny), - "pairsToDict": makeSignature("list", "dictionaries_alist_to_dict", 0, true, ast.SignDict), - - "dictLen": makeSignature("dict", "dictionaries_length", 0, true, ast.SignNumb), - "get": makeSignature("dict", "dictionaries_lookup", 2, true, ast.SignAny), - "set": makeSignature("dict", "dictionaries_set_pair", 2, false, ast.SignVoid), - "delete": makeSignature("dict", "dictionaries_delete_pair", 1, false, ast.SignVoid), - "getAtPath": makeSignature("dict", "dictionaries_recursive_lookup", 2, true, ast.SignAny), - "setAtPath": makeSignature("dict", "dictionaries_recursive_set", 2, false, ast.SignVoid), - "containsKey": makeSignature("dict", "dictionaries_is_key_in", 1, true, ast.SignBool), - "mergeInto": makeSignature("dict", "dictionaries_combine_dicts", 1, false, ast.SignDict), - "walkTree": makeSignature("dict", "dictionaries_walk_tree", 1, true, ast.SignAny), - "keys": makeSignature("dict", "dictionaries_getters", 0, true, ast.SignList), - "values": makeSignature("dict", "dictionaries_getters", 0, true, ast.SignList), - "toPairs": makeSignature("dict", "dictionaries_dict_to_alist", 0, true, ast.SignList), -} - -func TestSignature(methodName string, argsCount int) (string, *CallSignature) { + "textLen": makeSignature("text", "text_length", 0, "", true, ast.SignNumb), + "trim": makeSignature("text", "text_trim", 0, "", true, ast.SignText), + "uppercase": makeSignature("text", "text_changeCase", 0, "", true, ast.SignText), + "lowercase": makeSignature("text", "text_changeCase", 0, "", true, ast.SignText), + "startsWith": makeSignatureTyped("text", "text_starts_at", 1, "prefix", true, ast.SignBool, []ast.Signature{ast.SignText}), + "contains": makeSignatureTyped("text", "text_contains", 1, "piece", true, ast.SignBool, []ast.Signature{ast.SignText}), + "containsAny": makeSignatureTyped("text", "text_contains", 1, "pieces", true, ast.SignBool, []ast.Signature{ast.SignList}), + "containsAll": makeSignatureTyped("text", "text_contains", 1, "pieces", true, ast.SignBool, []ast.Signature{ast.SignList}), + "split": makeSignatureTyped("text", "text_split", 1, "separator", true, ast.SignList, []ast.Signature{ast.SignText}), + "splitAtFirst": makeSignatureTyped("text", "text_split", 1, "separator", true, ast.SignList, []ast.Signature{ast.SignText}), + "splitAtAny": makeSignature("text", "text_split", 1, "separators", true, ast.SignList), + "splitAtFirstOfAny": makeSignature("text", "text_split", 1, "separators", true, ast.SignList), + "splitAtSpaces": makeSignature("text", "text_split_at_spaces", 0, "", true, ast.SignList), + "reverse": makeSignature("text", "text_reverse", 0, "", true, ast.SignText), + "csvRowToList": makeSignature("text", "lists_from_csv_row", 0, "", true, ast.SignList), + "csvTableToList": makeSignature("text", "lists_from_csv_table", 0, "", true, ast.SignList), + "segment": makeSignatureTyped("text", "text_segment", 2, "start, length", true, ast.SignText, []ast.Signature{ast.SignNumb, ast.SignNumb}), + "replace": makeSignatureTyped("text", "text_replace_all", 2, "from, to", true, ast.SignText, []ast.Signature{ast.SignText, ast.SignText}), + "replaceFrom": makeSignatureTyped("text", "text_replace_mappings", 1, "mappingDict", true, ast.SignText, []ast.Signature{ast.SignDict}), + "replaceFromLongestFirst": makeSignatureTyped("text", "text_replace_mappings", 1, "mappingDict", true, ast.SignText, []ast.Signature{ast.SignDict}), + + "listLen": makeSignature("list", "lists_length", 0, "", true, ast.SignNumb), + "add": makeSignature("list", "lists_add_items", -1, "item...", false, ast.SignVoid), + "containsItem": makeSignature("list", "lists_is_in", 1, "item", true, ast.SignBool), + "indexOf": makeSignature("list", "lists_position_in", 1, "item", true, ast.SignNumb), + "insert": makeSignatureTyped("list", "lists_insert_item", 2, "index, item", false, ast.SignVoid, []ast.Signature{ast.SignNumb, ast.SignAny}), + "remove": makeSignatureTyped("list", "lists_remove_item", 1, "index", false, ast.SignVoid, []ast.Signature{ast.SignNumb}), + "appendList": makeSignatureTyped("list", "lists_append_list", 1, "other", false, ast.SignVoid, []ast.Signature{ast.SignList}), + "lookupInPairs": makeSignature("list", "lists_lookup_in_pairs", 2, "key, notFound", true, ast.SignAny), + "join": makeSignatureTyped("list", "lists_join_with_separator", 1, "separator", true, ast.SignText, []ast.Signature{ast.SignText}), + "slice": makeSignatureTyped("list", "lists_slice", 2, "from, to", true, ast.SignList, []ast.Signature{ast.SignNumb, ast.SignNumb}), + "random": makeSignature("list", "lists_pick_random_item", 0, "", true, ast.SignAny), + "reverseList": makeSignature("list", "lists_reverse", 0, "", true, ast.SignList), + "toCsvRow": makeSignature("list", "lists_to_csv_row", 0, "", true, ast.SignText), + "toCsvTable": makeSignature("list", "lists_to_csv_table", 0, "", true, ast.SignText), + "sort": makeSignature("list", "lists_sort", 0, "", true, ast.SignList), + "allButFirst": makeSignature("list", "lists_but_first", 0, "", true, ast.SignList), + "allButLast": makeSignature("list", "lists_but_last", 0, "", true, ast.SignList), + "pairsToDict": makeSignature("list", "dictionaries_alist_to_dict", 0, "", true, ast.SignDict), + + "dictLen": makeSignature("dict", "dictionaries_length", 0, "", true, ast.SignNumb), + "get": makeSignature("dict", "dictionaries_lookup", 2, "key, notFound", true, ast.SignAny), + "set": makeSignature("dict", "dictionaries_set_pair", 2, "key, value", false, ast.SignVoid), + "delete": makeSignature("dict", "dictionaries_delete_pair", 1, "key", false, ast.SignVoid), + "getAtPath": makeSignatureTyped("dict", "dictionaries_recursive_lookup", 2, "keys, notFound", true, ast.SignAny, []ast.Signature{ast.SignList, ast.SignAny}), + "setAtPath": makeSignatureTyped("dict", "dictionaries_recursive_set", 2, "keys, value", false, ast.SignVoid, []ast.Signature{ast.SignList, ast.SignAny}), + "containsKey": makeSignature("dict", "dictionaries_is_key_in", 1, "key", true, ast.SignBool), + "mergeInto": makeSignatureTyped("dict", "dictionaries_combine_dicts", 1, "other", true, ast.SignDict, []ast.Signature{ast.SignDict}), + "walkTree": makeSignature("dict", "dictionaries_walk_tree", 1, "procedure", true, ast.SignAny), + "keys": makeSignature("dict", "dictionaries_getters", 0, "", true, ast.SignList), + "values": makeSignature("dict", "dictionaries_getters", 0, "", true, ast.SignList), + "toPairs": makeSignature("dict", "dictionaries_dict_to_alist", 0, "", true, ast.SignList), +} + +// sigString returns a display string like ".get(key, notFound)" for error messages. +func sigString(name string, sig *CallSignature) string { + return "." + name + "(" + sig.Params + ")" +} + +// HintOutput returns the lookahead output constraint stored by CorrectChain, +// or nil if no valid successor was found in the chain. +func (c *Call) HintOutput() *ast.Signature { return c.hintOutput } + +// DeriveAllowedModules is exported for use in mistparser's checkPendingSymbols. +func DeriveAllowedModules(onSigs []ast.Signature) []string { + return deriveAllowedModules(onSigs) +} + +func deriveAllowedModules(onSigs []ast.Signature) []string { + var modules []string + if ast.HasSignature(onSigs, ast.SignText) { + modules = append(modules, "text") + } + if ast.HasSignature(onSigs, ast.SignList) { + modules = append(modules, "list") + } + if ast.HasSignature(onSigs, ast.SignDict) { + modules = append(modules, "dict") + } + return modules +} + +func joinOr(parts []string) string { + if len(parts) == 0 { + return "" + } + if len(parts) == 1 { + return parts[0] + } + if len(parts) == 2 { + return parts[0] + " or " + parts[1] + } + return strings.Join(parts[:len(parts)-1], ", ") + " or " + parts[len(parts)-1] +} + +// methodSuggestions returns a "Did you mean" suffix filtered by input module +// and optionally by neededOutput (the output type the caller expects). +// When neededOutput filtering leaves no candidates it falls back without it. +func methodSuggestions(methodName string, allowedModules []string, neededOutput *ast.Signature) string { + candidates := collectCandidates(methodName, allowedModules, neededOutput) + if len(candidates) == 0 && neededOutput != nil { + candidates = collectCandidates(methodName, allowedModules, nil) + } + suggestions := fzf.Top(methodName, candidates, 3) + if len(suggestions) > 0 { + parts := make([]string, len(suggestions)) + for i, s := range suggestions { + parts[i] = "." + s + "()" + } + return ". Did you mean " + joinOr(parts) + "?" + } + return "" +} + +func collectCandidates(methodName string, allowedModules []string, neededOutput *ast.Signature) []string { + candidates := make([]string, 0, len(signatures)) + for name, sig := range signatures { + if name == methodName { + continue + } + if len(allowedModules) > 0 { + moduleMatch := false + for _, mod := range allowedModules { + if sig.Module == mod { + moduleMatch = true + break + } + } + if !moduleMatch { + continue + } + } + if neededOutput != nil && sig.Signature != *neededOutput && sig.Signature != ast.SignAny { + continue + } + candidates = append(candidates, name) + } + return candidates +} + +// BuildSuggestions is exported for use in mistparser's checkPendingSymbols. +func BuildSuggestions(methodName string, allowedModules []string, neededOutput *ast.Signature) string { + return methodSuggestions(methodName, allowedModules, neededOutput) +} + +// FindBestSuggestion returns the single highest-scoring replacement name, or "" +// if no candidate clears the scoring threshold. +func FindBestSuggestion(methodName string, allowedModules []string, neededOutput *ast.Signature) string { + candidates := collectCandidates(methodName, allowedModules, neededOutput) + if len(candidates) == 0 && neededOutput != nil { + candidates = collectCandidates(methodName, allowedModules, nil) + } + if tops := fzf.Top(methodName, candidates, 1); len(tops) > 0 { + return tops[0] + } + return "" +} + +func TestSignature(methodName string, argsCount int, allowedModules ...string) (string, *CallSignature) { signature, ok := signatures[methodName] if !ok { - return sugar.Format("Cannot find method .%()", methodName), nil + return "No method named ." + methodName + "()" + methodSuggestions(methodName, allowedModules, nil), nil } + sig := sigString(methodName, signature) if signature.ParamCount >= 0 { if signature.ParamCount != argsCount { - return sugar.Format("Expected % args but got % for method .%()", - strconv.Itoa(signature.ParamCount), strconv.Itoa(argsCount), methodName), nil + return sugar.Format("% expects % arg(s) but got %", + sig, strconv.Itoa(signature.ParamCount), strconv.Itoa(argsCount)), nil } } else { minArgs := -signature.ParamCount if argsCount < minArgs { - return sugar.Format("Expected at least % args but got only % for method .%()", - strconv.Itoa(minArgs), strconv.Itoa(argsCount), methodName), nil + return sugar.Format("% expects at least % arg(s) but got %", + sig, strconv.Itoa(minArgs), strconv.Itoa(argsCount)), nil } } return "", signature @@ -122,9 +248,10 @@ func (c *Call) String() string { } func (c *Call) Blockly(flags ...bool) ast.Block { - errorMessage, signature := TestSignature(c.Name, len(c.Args)) + onSigs := c.On.Signature() + errorMessage, signature := TestSignature(c.Name, len(c.Args), deriveAllowedModules(onSigs)...) if signature == nil { - panic(errorMessage) + c.Where.Error(errorMessage) } switch signature.Module { case "text": @@ -134,7 +261,8 @@ func (c *Call) Blockly(flags ...bool) ast.Block { case "dict": return c.dictMethods(signature) default: - panic("Unknown module " + signature.Module) + c.Where.Error("Unknown method module: %", signature.Module) + panic("") } } @@ -142,18 +270,43 @@ func (c *Call) Continuous() bool { return true } -func (c *Call) Consumable(flags ...bool) bool { +func (c *Call) Consumable() bool { signature, ok := signatures[c.Name] if !ok { - c.Where.Error("Cannot find method .%()", c.Name) + return true } return signature.Consumable } func (c *Call) Signature() []ast.Signature { - errorMessage, signature := TestSignature(c.Name, len(c.Args)) + onSigs := c.On.Signature() + errorMessage, signature := TestSignature(c.Name, len(c.Args), deriveAllowedModules(onSigs)...) if signature == nil { - panic(errorMessage) + c.Where.Error(errorMessage) + } + for i, arg := range c.Args { + argSigs := arg.Signature() + if i < len(signature.ParamSigs) { + expected := signature.ParamSigs[i] + if expected != ast.SignAny && !ast.HasSignature(argSigs, expected) { + c.Where.TypeError(".%() argument % expects %, not %", c.Name, strconv.Itoa(i+1), expected.String(), ast.FormatSignatures(argSigs)) + } + } + } + intendedOutput := signature.Signature + switch signature.Module { + case "text": + if !ast.HasSignature(onSigs, ast.SignText) { + c.Where.TypeError(".%() operates on text, not %"+methodSuggestions(c.Name, deriveAllowedModules(onSigs), &intendedOutput), c.Name, ast.FormatSignatures(onSigs)) + } + case "list": + if !ast.HasSignature(onSigs, ast.SignList) { + c.Where.TypeError(".%() operates on lists, not %"+methodSuggestions(c.Name, deriveAllowedModules(onSigs), &intendedOutput), c.Name, ast.FormatSignatures(onSigs)) + } + case "dict": + if !ast.HasSignature(onSigs, ast.SignDict) { + c.Where.TypeError(".%() operates on dictionaries, not %"+methodSuggestions(c.Name, deriveAllowedModules(onSigs), &intendedOutput), c.Name, ast.FormatSignatures(onSigs)) + } } return []ast.Signature{signature.Signature} } diff --git a/lang/code/ast/method/dict.go b/lang/code/ast/method/dict.go index 58b08753..dd033f39 100644 --- a/lang/code/ast/method/dict.go +++ b/lang/code/ast/method/dict.go @@ -27,7 +27,8 @@ func (c *Call) dictMethods(signature *CallSignature) ast.Block { case "dictionaries_getters": return c.dictGetters() default: - panic("Unknown text method " + signature.BlocklyName) + c.Where.Error("Unknown dict method: %", signature.BlocklyName) + panic("") } } diff --git a/lang/code/ast/method/list.go b/lang/code/ast/method/list.go index adf9a62a..bf15b050 100644 --- a/lang/code/ast/method/list.go +++ b/lang/code/ast/method/list.go @@ -30,7 +30,8 @@ func (c *Call) listMethods(signature *CallSignature) ast.Block { case "lists_slice": return c.listSlice() default: - panic("Unknown list method " + signature.BlocklyName) + c.Where.Error("Unknown list method: %", signature.BlocklyName) + panic("") } } diff --git a/lang/code/ast/method/text.go b/lang/code/ast/method/text.go index 35a34834..dcd62f45 100644 --- a/lang/code/ast/method/text.go +++ b/lang/code/ast/method/text.go @@ -33,7 +33,8 @@ func (c *Call) textMethods(signature *CallSignature) ast.Block { case "text_replace_mappings": return c.textReplaceFrom() default: - panic("Unknown text method " + signature.BlocklyName) + c.Where.Error("Unknown text method: %", signature.BlocklyName) + panic("") } } @@ -72,9 +73,9 @@ func (c *Call) textReplace() ast.Block { return ast.Block{ Type: "text_replace_all", Values: []ast.Value{ - {Name: "TEXT", Block: c.On.Blockly()}, - {Name: "SEGMENT", Block: c.Args[0].Blockly()}, - {Name: "REPLACEMENT", Block: c.Args[1].Blockly()}, + {Name: "TEXT", Block: c.On.Blockly(false)}, + {Name: "SEGMENT", Block: c.Args[0].Blockly(false)}, + {Name: "REPLACEMENT", Block: c.Args[1].Blockly(false)}, }, } } @@ -83,9 +84,9 @@ func (c *Call) textSegment() ast.Block { return ast.Block{ Type: "text_segment", Values: []ast.Value{ - {Name: "TEXT", Block: c.On.Blockly()}, - {Name: "START", Block: c.Args[0].Blockly()}, - {Name: "LENGTH", Block: c.Args[1].Blockly()}, + {Name: "TEXT", Block: c.On.Blockly(false)}, + {Name: "START", Block: c.Args[0].Blockly(false)}, + {Name: "LENGTH", Block: c.Args[1].Blockly(false)}, }, } } @@ -107,8 +108,8 @@ func (c *Call) textSplit() ast.Block { Mutation: &ast.Mutation{Mode: fieldOp}, Fields: []ast.Field{{Name: "OP", Value: fieldOp}}, Values: []ast.Value{ - {Name: "TEXT", Block: c.On.Blockly()}, - {Name: "AT", Block: c.Args[0].Blockly()}, + {Name: "TEXT", Block: c.On.Blockly(false)}, + {Name: "AT", Block: c.Args[0].Blockly(false)}, }, } } @@ -128,8 +129,8 @@ func (c *Call) textContains() ast.Block { Mutation: &ast.Mutation{Mode: fieldOp}, Fields: []ast.Field{{Name: "OP", Value: fieldOp}}, Values: []ast.Value{ - {Name: "TEXT", Block: c.On.Blockly()}, - {Name: "PIECE", Block: c.Args[0].Blockly()}, + {Name: "TEXT", Block: c.On.Blockly(false)}, + {Name: "PIECE", Block: c.Args[0].Blockly(false)}, }, } } @@ -138,8 +139,8 @@ func (c *Call) textStartsAt() ast.Block { return ast.Block{ Type: "text_starts_at", Values: []ast.Value{ - {Name: "TEXT", Block: c.On.Blockly()}, - {Name: "PIECE", Block: c.Args[0].Blockly()}, + {Name: "TEXT", Block: c.On.Blockly(false)}, + {Name: "PIECE", Block: c.Args[0].Blockly(false)}, }, } } diff --git a/lang/code/ast/procedures/call.go b/lang/code/ast/procedures/call.go index f5ff0a69..8c6fe326 100644 --- a/lang/code/ast/procedures/call.go +++ b/lang/code/ast/procedures/call.go @@ -2,10 +2,12 @@ package procedures import ( "Falcon/code/ast" + "Falcon/code/lex" "Falcon/code/sugar" ) type Call struct { + Where *lex.Token Name string Parameters []string Arguments []ast.Expr @@ -35,11 +37,13 @@ func (v *Call) Continuous() bool { return true } -func (v *Call) Consumable(flags ...bool) bool { +func (v *Call) Consumable() bool { return v.Returning } func (v *Call) Signature() []ast.Signature { - // TODO: We'd have to lookup a procedure table to determine the signature. + for _, arg := range v.Arguments { + arg.Signature() + } return []ast.Signature{ast.SignAny} } diff --git a/lang/code/ast/procedures/returning_procedure.go b/lang/code/ast/procedures/returning_procedure.go index 7e56e405..ea364994 100644 --- a/lang/code/ast/procedures/returning_procedure.go +++ b/lang/code/ast/procedures/returning_procedure.go @@ -3,6 +3,8 @@ package procedures import ( "Falcon/code/ast" "Falcon/code/ast/control" + "Falcon/code/ast/fundamentals" + "Falcon/code/ast/variables" "Falcon/code/sugar" "strings" ) @@ -15,10 +17,20 @@ type RetProcedure struct { func (v *RetProcedure) String() string { var resultString string - if _, ok := v.Result.(*control.Do); !ok { - resultString = ast.Pad(v.Result.String()) - } else { + switch v.Result.(type) { + case *control.Do, *variables.VarResult: resultString = ast.Pad("{\n" + ast.Pad(v.Result.String()) + "}") + default: + if sb, ok := v.Result.(*fundamentals.SmartBody); ok && len(sb.Body) == 1 { + switch sb.Body[0].(type) { + case *variables.VarResult, *variables.Var: + resultString = ast.Pad("{\n" + ast.Pad(sb.Body[0].String()) + "}") + break + } + } + if resultString == "" { + resultString = ast.Pad(v.Result.String()) + } } return sugar.Format("func %(%) =\n%", v.Name, strings.Join(v.Parameters, ", "), resultString) } @@ -36,7 +48,7 @@ func (v *RetProcedure) Continuous() bool { return false } -func (v *RetProcedure) Consumable(flags ...bool) bool { +func (v *RetProcedure) Consumable() bool { return false } diff --git a/lang/code/ast/procedures/void_procedure.go b/lang/code/ast/procedures/void_procedure.go index 25e183d7..492e8a79 100644 --- a/lang/code/ast/procedures/void_procedure.go +++ b/lang/code/ast/procedures/void_procedure.go @@ -29,10 +29,13 @@ func (v *VoidProcedure) Continuous() bool { return false } -func (v *VoidProcedure) Consumable(flags ...bool) bool { +func (v *VoidProcedure) Consumable() bool { return false } func (v *VoidProcedure) Signature() []ast.Signature { + for _, expr := range v.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/signature.go b/lang/code/ast/signature.go index 6b8429d4..406c5244 100644 --- a/lang/code/ast/signature.go +++ b/lang/code/ast/signature.go @@ -1,5 +1,7 @@ package ast +import "strings" + //go:generate stringer -type=Signature type Signature int @@ -34,3 +36,25 @@ func CombineSignatures(first []Signature, second []Signature) []Signature { } return unique } + +// HasSignature reports whether signatures contain target or SignAny. +func HasSignature(signatures []Signature, target Signature) bool { + for _, s := range signatures { + if s == SignAny || s == target { + return true + } + } + return false +} + +// FormatSignatures returns a human-readable string for a slice of signatures. +func FormatSignatures(signatures []Signature) string { + if len(signatures) == 0 { + return "unknown" + } + parts := make([]string, len(signatures)) + for i, s := range signatures { + parts[i] = s.String() + } + return strings.Join(parts, " | ") +} diff --git a/lang/code/ast/signature_string.go b/lang/code/ast/signature_string.go new file mode 100644 index 00000000..3f6a61cf --- /dev/null +++ b/lang/code/ast/signature_string.go @@ -0,0 +1,28 @@ +package ast + +func (i Signature) String() string { + switch i { + case SignBool: + return "boolean" + case SignNumb: + return "number" + case SignText: + return "text" + case SignList: + return "list" + case SignDict: + return "dictionary" + case SignComponent: + return "component" + case SignHelper: + return "helper" + case SignAny: + return "any" + case SignOfEvent: + return "event" + case SignVoid: + return "void" + default: + return "unknown" + } +} diff --git a/lang/code/ast/variables/get.go b/lang/code/ast/variables/get.go index f57154f4..145a2a73 100644 --- a/lang/code/ast/variables/get.go +++ b/lang/code/ast/variables/get.go @@ -44,10 +44,13 @@ func (g *Get) Continuous() bool { return true } -func (g *Get) Consumable(flags ...bool) bool { +func (g *Get) Consumable() bool { return true } func (g *Get) Signature() []ast.Signature { + if len(g.ValueSignature) > 0 { + return g.ValueSignature + } return []ast.Signature{ast.SignAny} } diff --git a/lang/code/ast/variables/global.go b/lang/code/ast/variables/global.go index b690c779..854e83a3 100644 --- a/lang/code/ast/variables/global.go +++ b/lang/code/ast/variables/global.go @@ -25,10 +25,11 @@ func (g *Global) Continuous() bool { return false } -func (g *Global) Consumable(flags ...bool) bool { +func (g *Global) Consumable() bool { return false } func (g *Global) Signature() []ast.Signature { + g.Value.Signature() return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/variables/local_body.go b/lang/code/ast/variables/local_body.go index 496abc65..77b51f4a 100644 --- a/lang/code/ast/variables/local_body.go +++ b/lang/code/ast/variables/local_body.go @@ -37,10 +37,16 @@ func (v *Var) Continuous() bool { return false } -func (v *Var) Consumable(flags ...bool) bool { +func (v *Var) Consumable() bool { return false } func (v *Var) Signature() []ast.Signature { + for _, value := range v.Values { + value.Signature() + } + for _, expr := range v.Body { + expr.Signature() + } return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/ast/variables/local_result.go b/lang/code/ast/variables/local_result.go index 1d1b8e71..266e48ab 100644 --- a/lang/code/ast/variables/local_result.go +++ b/lang/code/ast/variables/local_result.go @@ -32,20 +32,17 @@ func (v *VarResult) String() string { } var builder strings.Builder - builder.WriteString("{\n") localLines := make([]string, len(combinedNames)) for k, name := range combinedNames { localLines[k] = "local " + name + " = " + combinedValues[k].String() } - builder.WriteString(ast.PadDirect(strings.Join(localLines, "\n"))) + builder.WriteString(strings.Join(localLines, "\n")) builder.WriteString("\n") - builder.WriteString(ast.PadDirect(result.String())) - builder.WriteString("\n}") + builder.WriteString(result.String()) return builder.String() } func (v *VarResult) Blockly(flags ...bool) ast.Block { - println("called!") return ast.Block{ Type: "local_declaration_expression", Mutation: &ast.Mutation{LocalNames: ast.MakeLocalNames(v.Names...)}, @@ -59,10 +56,13 @@ func (v *VarResult) Continuous() bool { return true } -func (v *VarResult) Consumable(flags ...bool) bool { +func (v *VarResult) Consumable() bool { return true } func (v *VarResult) Signature() []ast.Signature { + for _, value := range v.Values { + value.Signature() + } return v.Result.Signature() } diff --git a/lang/code/ast/variables/local_simple.go b/lang/code/ast/variables/local_simple.go deleted file mode 100644 index c74123fc..00000000 --- a/lang/code/ast/variables/local_simple.go +++ /dev/null @@ -1,45 +0,0 @@ -package variables - -import ( - "Falcon/code/ast" - "strings" -) - -type SimpleVar struct { - Name string - Value ast.Expr - Body []ast.Expr -} - -func (v *SimpleVar) String() string { - var builder strings.Builder - builder.WriteString("local ") - builder.WriteString(v.Name) - builder.WriteString(" = ") - builder.WriteString(v.Value.String()) - builder.WriteString("\n") - builder.WriteString(ast.JoinExprs("\n", v.Body)) - return builder.String() -} - -func (v *SimpleVar) Blockly(flags ...bool) ast.Block { - return ast.Block{ - Type: "local_declaration_statement", - Mutation: &ast.Mutation{LocalNames: ast.MakeLocalNames(v.Name)}, - Fields: []ast.Field{{Name: "VAR0", Value: v.Name}}, - Values: []ast.Value{{Name: "DECL0", Block: v.Value.Blockly(false)}}, - Statements: ast.OptionalStatement("STACK", v.Body), - } -} - -func (v *SimpleVar) Continuous() bool { - return false -} - -func (v *SimpleVar) Consumable(flags ...bool) bool { - return false -} - -func (v *SimpleVar) Signature() []ast.Signature { - return []ast.Signature{ast.SignVoid} -} diff --git a/lang/code/ast/variables/set.go b/lang/code/ast/variables/set.go index 12f68767..6ae338e4 100644 --- a/lang/code/ast/variables/set.go +++ b/lang/code/ast/variables/set.go @@ -33,10 +33,11 @@ func (s Set) Continuous() bool { return false } -func (s Set) Consumable(flags ...bool) bool { +func (s Set) Consumable() bool { return false } func (s Set) Signature() []ast.Signature { + s.Expr.Signature() return []ast.Signature{ast.SignVoid} } diff --git a/lang/code/context/code_context.go b/lang/code/context/code_context.go index 29b69edb..46ee0626 100644 --- a/lang/code/context/code_context.go +++ b/lang/code/context/code_context.go @@ -18,7 +18,72 @@ func (c *CodeContext) ReportError( message string, args ...string, ) { - panic(c.BuildError(true, column, row, highlightWordSize, message, args...)) + panic(c.BuildTracebackError(column, row, highlightWordSize, "CompileError", message, args...)) +} + +func (c *CodeContext) ReportTypeError( + column int, + row int, + highlightWordSize int, + message string, + args ...string, +) { + panic(c.BuildTracebackError(column, row, highlightWordSize, "TypeError", message, args...)) +} + +func (c *CodeContext) GetLine(lineNum int) string { + code := *c.SourceCode + beginOfLine := sugar.IndexAfterNthOccurrence(code, lineNum-1, '\n') + 1 + endOfLine := strings.Index(code[beginOfLine:], "\n") + if endOfLine == -1 { + endOfLine = len(code) - beginOfLine + } + return code[beginOfLine : beginOfLine+endOfLine] +} + +func (c *CodeContext) BuildCaret(endColumn, highlightSize int) string { + if highlightSize <= 0 { + highlightSize = 1 + } + start := endColumn - highlightSize + if start < 0 { + start = 0 + } + return strings.Repeat(" ", start) + strings.Repeat("^", highlightSize) +} + +// FormatTracebackFrame formats a single traceback frame in the style of +// Python tracebacks. It is the common ground shared by compile-time +// reporting and runtime.FormatRuntimeError. +func (c *CodeContext) FormatTracebackFrame( + fileName string, + line int, + column int, + highlightSize int, + funcName string, +) string { + var sb strings.Builder + sb.WriteString(" File \"" + fileName + "\", line " + strconv.Itoa(line) + ", in " + funcName + "\n") + sourceLine := c.GetLine(line) + sb.WriteString(" " + sourceLine + "\n") + sb.WriteString(" " + c.BuildCaret(column, highlightSize) + "\n") + return sb.String() +} + +// BuildTracebackError assembles a full traceback-style error message. +func (c *CodeContext) BuildTracebackError( + column int, + row int, + highlightWordSize int, + title string, + message string, + args ...string, +) string { + var sb strings.Builder + sb.WriteString("Traceback (most recent call last):\n") + sb.WriteString(c.FormatTracebackFrame(c.FileName, column, row, highlightWordSize, "")) + sb.WriteString(title + ": " + sugar.Format(message, args...) + "\n") + return sb.String() } func (c *CodeContext) BuildError( diff --git a/lang/code/fzf/score.go b/lang/code/fzf/score.go new file mode 100644 index 00000000..10024224 --- /dev/null +++ b/lang/code/fzf/score.go @@ -0,0 +1,99 @@ +package fzf + +import "strings" + +func TokenOverlap(a, b []string) float64 { + setA := make(map[string]struct{}) + for _, t := range a { + if len(t) <= 2 { + continue // skip short positional/preposition tokens ("at", "of", "is", …) + } + setA[Canonical(t)] = struct{}{} + } + setB := make(map[string]struct{}) + for _, t := range b { + if len(t) <= 2 { + continue + } + setB[Canonical(t)] = struct{}{} + } + + intersection := 0 + for t := range setA { + if _, ok := setB[t]; ok { + intersection++ + } + } + + union := len(setA) + len(setB) - intersection + if union == 0 { + return 0 + } + return float64(intersection) / float64(union) +} + +func FZFScore(input, candidate string) float64 { + inputLower := strings.ToLower(input) + candidateLower := strings.ToLower(candidate) + + if inputLower == "" { + return 1.0 + } + if len(inputLower) > len(candidateLower) { + return 0 + } + + var positions []int + j := 0 + for i := 0; i < len(candidateLower) && j < len(inputLower); i++ { + if candidateLower[i] == inputLower[j] { + positions = append(positions, i) + j++ + } + } + if len(positions) != len(inputLower) { + return 0 + } + + score := 0.0 + for idx, pos := range positions { + score += 1.0 + + if pos == 0 && idx == 0 { + score += 3.0 + } + + if pos > 0 && isBoundary(candidate[pos-1], candidate[pos]) { + score += 2.0 + } + + if idx > 0 && pos == positions[idx-1]+1 { + score += 2.0 + } + } + + score -= float64(len(candidateLower)-len(inputLower)) * 0.1 + + maxPossible := float64(len(inputLower)) * 7.0 + normalized := score / maxPossible + if normalized < 0 { + normalized = 0 + } + if normalized > 1 { + normalized = 1 + } + return normalized +} + +func isBoundary(prev, curr byte) bool { + return prev == '_' || prev == '-' || prev == ' ' || + (prev >= 'a' && prev <= 'z' && curr >= 'A' && curr <= 'Z') +} + +func Score(input, candidate string) float64 { + inputTokens := SplitCamel(input) + candTokens := SplitCamel(candidate) + overlap := TokenOverlap(inputTokens, candTokens) + fzf := FZFScore(input, candidate) + return 0.6*overlap + 0.4*fzf +} diff --git a/lang/code/fzf/split.go b/lang/code/fzf/split.go new file mode 100644 index 00000000..53c06825 --- /dev/null +++ b/lang/code/fzf/split.go @@ -0,0 +1,24 @@ +package fzf + +import "strings" + +func SplitCamel(s string) []string { + if s == "" { + return nil + } + var tokens []string + start := 0 + for i := 1; i < len(s); i++ { + if s[i] >= 'A' && s[i] <= 'Z' { + if s[i-1] >= 'a' && s[i-1] <= 'z' { + tokens = append(tokens, strings.ToLower(s[start:i])) + start = i + } else if i+1 < len(s) && s[i+1] >= 'a' && s[i+1] <= 'z' { + tokens = append(tokens, strings.ToLower(s[start:i])) + start = i + } + } + } + tokens = append(tokens, strings.ToLower(s[start:])) + return tokens +} diff --git a/lang/code/fzf/suggest.go b/lang/code/fzf/suggest.go new file mode 100644 index 00000000..438d29c7 --- /dev/null +++ b/lang/code/fzf/suggest.go @@ -0,0 +1,34 @@ +package fzf + +import "sort" + +type Suggestion struct { + Name string + Score float64 +} + +func Rank(input string, candidates []string) []Suggestion { + var result []Suggestion + for _, c := range candidates { + s := Score(input, c) + if s > 0.2 { + result = append(result, Suggestion{Name: c, Score: s}) + } + } + sort.Slice(result, func(i, j int) bool { + return result[i].Score > result[j].Score + }) + return result +} + +func Top(input string, candidates []string, n int) []string { + ranked := Rank(input, candidates) + if len(ranked) > n { + ranked = ranked[:n] + } + result := make([]string, len(ranked)) + for i, r := range ranked { + result[i] = r.Name + } + return result +} diff --git a/lang/code/fzf/synonyms.go b/lang/code/fzf/synonyms.go new file mode 100644 index 00000000..0823d241 --- /dev/null +++ b/lang/code/fzf/synonyms.go @@ -0,0 +1,312 @@ +package fzf + +import "strings" + +// synonymGraph maps a token to its direct synonym neighbours. +// The graph is intentionally sparse: clusters are connected through hub nodes +// (e.g. "length" links "len", "size", and "count") so that transitivity +// (size→length→len) is resolved by BFS in Canonical, never listed manually. +var synonymGraph = map[string][]string{ + // ── length / size / count ──────────────────────────────────────────────── + "length": {"len", "size", "count"}, + "len": {"length"}, + "size": {"length"}, + "count": {"length"}, + + // ── trim / strip ───────────────────────────────────────────────────────── + "trim": {"strip", "clean"}, + "strip": {"trim", "clean"}, + "clean": {"trim", "strip"}, + + // ── uppercase ───────────────────────────────────────────────────────────── + "uppercase": {"upper", "upcase", "toupper"}, + "upper": {"uppercase", "upcase", "toupper"}, + "upcase": {"upper", "uppercase"}, + "toupper": {"upper", "uppercase"}, + + // ── lowercase ───────────────────────────────────────────────────────────── + "lowercase": {"lower", "downcase", "tolower"}, + "lower": {"lowercase", "downcase", "tolower"}, + "downcase": {"lower", "lowercase"}, + "tolower": {"lower", "lowercase"}, + + // ── starts / begins / prefix (for startsWith) ─────────────────────────── + "starts": {"begins", "prefix"}, + "begins": {"starts", "prefix"}, + "prefix": {"starts", "begins"}, + + // ── contains / includes / has ───────────────────────────────────────────── + "contains": {"includes", "has", "include"}, + "includes": {"contains", "has", "include"}, + "has": {"contains", "includes", "include"}, + "include": {"contains", "includes", "has"}, + + // ── split / explode / divide / partition / tokenize ─────────────────────── + "split": {"explode", "divide", "partition", "tokenize"}, + "explode": {"split", "divide"}, + "divide": {"split", "explode", "partition"}, + "partition": {"split", "divide"}, + "tokenize": {"split"}, + + // ── reverse / flip / invert ─────────────────────────────────────────────── + "reverse": {"flip", "invert", "backward"}, + "flip": {"reverse", "invert"}, + "invert": {"reverse", "flip"}, + "backward": {"reverse"}, + "backwards": {"reverse"}, + + // ── segment / substring / substr / extract (text sub-range) ───────────── + "segment": {"substring", "substr", "extract"}, + "substring": {"segment", "substr", "extract"}, + "substr": {"segment", "substring"}, + "extract": {"segment", "substring"}, + + // ── slice / cut / range (list sub-range) ──────────────────────────────── + "slice": {"cut", "range"}, + "cut": {"slice", "range"}, + "range": {"slice", "cut"}, + + // ── replace / substitute / swap / subst ────────────────────────────────── + "replace": {"substitute", "swap", "subst"}, + "substitute": {"replace", "swap", "subst"}, + "swap": {"replace", "substitute"}, + "subst": {"replace", "substitute"}, + + // ── add / push / append ─────────────────────────────────────────────────── + "add": {"push", "append"}, + "push": {"add", "append"}, + "append": {"add", "push"}, + + // ── insert / prepend / unshift ──────────────────────────────────────────── + "insert": {"prepend", "unshift"}, + "prepend": {"insert", "unshift"}, + "unshift": {"insert", "prepend"}, + + // ── indexOf: index / find / position / search / locate / pos ───────────── + "index": {"find", "position", "search", "locate", "pos"}, + "find": {"index", "position", "search", "locate"}, + "position": {"index", "find", "locate", "pos"}, + "search": {"index", "find", "locate"}, + "locate": {"index", "find", "position"}, + "pos": {"index", "position"}, + + // ── remove / delete / erase / pop / del ────────────────────────────────── + "remove": {"delete", "erase", "pop", "del"}, + "delete": {"remove", "erase", "del"}, + "erase": {"remove", "delete"}, + "pop": {"remove"}, + "del": {"remove", "delete"}, + + // ── join / concat / concatenate / glue / implode ───────────────────────── + "join": {"concat", "concatenate", "glue", "implode", "connect"}, + "concat": {"join", "concatenate", "glue"}, + "concatenate": {"join", "concat"}, + "glue": {"join", "concat"}, + "implode": {"join"}, + "connect": {"join"}, + + // ── random / pick / sample / rand / choose ──────────────────────────────── + "random": {"pick", "sample", "rand", "choose"}, + "pick": {"random", "sample", "choose"}, + "sample": {"random", "pick", "choose"}, + "rand": {"random"}, + "choose": {"random", "pick", "sample"}, + + // ── sort / order / arrange / rank ───────────────────────────────────────── + "sort": {"order", "arrange", "rank"}, + "order": {"sort", "arrange", "rank"}, + "arrange": {"sort", "order"}, + "rank": {"sort", "order"}, + + // ── first / head / front ────────────────────────────────────────────────── + "first": {"head", "front"}, + "head": {"first", "front"}, + "front": {"first", "head"}, + + // ── last / tail / end / final ───────────────────────────────────────────── + "last": {"tail", "end", "final"}, + "tail": {"last", "end"}, + "end": {"last", "tail", "final"}, + "final": {"last", "end"}, + + // ── but / except / without / excluding (for allBut*) ──────────────────── + "but": {"except", "without", "excluding"}, + "except": {"but", "without", "excluding"}, + "without": {"but", "except"}, + "excluding": {"but", "except"}, + + // ── get / fetch / retrieve / lookup / read ──────────────────────────────── + "get": {"fetch", "retrieve", "lookup", "read"}, + "fetch": {"get", "retrieve", "lookup"}, + "retrieve": {"get", "fetch", "lookup"}, + "lookup": {"get", "fetch", "retrieve"}, + "read": {"get", "fetch"}, + + // ── set / put / store / assign / write ──────────────────────────────────── + "set": {"put", "store", "assign", "write"}, + "put": {"set", "store", "assign"}, + "store": {"set", "put"}, + "assign": {"set", "put"}, + "write": {"set", "store"}, + + // ── merge / combine / extend / union / mixin ────────────────────────────── + "merge": {"combine", "extend", "union", "mixin"}, + "combine": {"merge", "extend", "union"}, + "extend": {"merge", "combine"}, + "union": {"merge", "combine"}, + "mixin": {"merge"}, + + // ── walk / traverse / iterate / visit / scan ────────────────────────────── + "walk": {"traverse", "iterate", "visit", "scan"}, + "traverse": {"walk", "iterate", "visit"}, + "iterate": {"walk", "traverse"}, + "visit": {"walk", "traverse"}, + "scan": {"walk"}, + + // ── keys / keyset ──────────────────────────────────────────────────────── + "keys": {"keyset", "keynames"}, + "keyset": {"keys"}, + + // ── values / vals ──────────────────────────────────────────────────────── + "values": {"vals"}, + "vals": {"values"}, + + // ── pairs / entries / kvpairs (for toPairs, lookupInPairs, pairsToDict) ── + "pairs": {"entries", "kvpairs"}, + "entries": {"pairs", "kvpairs"}, + "kvpairs": {"pairs", "entries"}, + + // ── text / string / str ────────────────────────────────────────────────── + "text": {"string", "str"}, + "string": {"text", "str"}, + "str": {"text", "string"}, + + // ── list / array / arr / vec / vector ──────────────────────────────────── + "list": {"array", "arr", "vec", "vector"}, + "array": {"list", "arr", "vec"}, + "arr": {"list", "array"}, + "vec": {"list", "array", "vector"}, + "vector": {"list", "vec"}, + + // ── dict / map / hash / hashmap / object / obj ─────────────────────────── + "dict": {"map", "hash", "hashmap", "object", "obj"}, + "map": {"dict", "hash", "hashmap"}, + "hash": {"dict", "map", "hashmap"}, + "hashmap": {"dict", "map", "hash"}, + "object": {"dict", "obj"}, + "obj": {"dict", "object"}, + + // ── spaces / whitespace / ws / blanks ──────────────────────────────────── + "spaces": {"whitespace", "ws", "blanks"}, + "whitespace": {"spaces", "ws"}, + "ws": {"spaces", "whitespace"}, + "blanks": {"spaces"}, + + // ── separator / delimiter / sep / delim ────────────────────────────────── + "separator": {"delimiter", "sep", "delim"}, + "delimiter": {"separator", "sep", "delim"}, + "sep": {"separator", "delimiter"}, + "delim": {"separator", "delimiter"}, + + // ── item / element / elem ──────────────────────────────────────────────── + "item": {"element", "elem"}, + "element": {"item", "elem"}, + "elem": {"item", "element"}, + + // ── path / route / nested ──────────────────────────────────────────────── + "path": {"route", "nested"}, + "route": {"path"}, + "nested": {"path"}, + + // ── tree / graph / structure ────────────────────────────────────────────── + "tree": {"graph", "structure"}, + "graph": {"tree"}, + "structure": {"tree"}, + + // ── all / every / each (for containsAll, allButFirst, allButLast) ──────── + "all": {"every", "each"}, + "every": {"all", "each"}, + "each": {"all", "every"}, + + // ── any / some / either (for containsAny, splitAtAny) ─────────────────── + "any": {"some", "either"}, + "some": {"any", "either"}, + "either": {"any", "some"}, + + // ── csv / comma / separated (for csvRowToList, toCsvRow, etc.) ────────── + "csv": {"comma", "separated"}, + "comma": {"csv", "separated"}, + "separated": {"csv", "comma"}, + + // ── row / line / record (for csvRowToList, toCsvRow) ───────────────────── + "row": {"line", "record"}, + "line": {"row", "record"}, + "record": {"row", "line"}, + + // ── table / matrix / grid (for csvTableToList, toCsvTable) ────────────── + "table": {"matrix", "grid", "spreadsheet"}, + "matrix": {"table", "grid"}, + "grid": {"table", "matrix"}, + "spreadsheet": {"table"}, + + // ── key / prop / field / property / attr (for containsKey, getAtPath) ─── + "key": {"prop", "field", "property", "attr", "attribute"}, + "prop": {"key", "field", "property", "attr"}, + "field": {"key", "prop", "property", "attr"}, + "property": {"key", "prop", "field", "attr", "attribute"}, + "attr": {"key", "prop", "field", "property"}, + "attribute": {"key", "property"}, + + // ── longest / greedy / maximal (for replaceFromLongestFirst) ──────────── + "longest": {"greedy", "maximal"}, + "greedy": {"longest", "maximal"}, + "maximal": {"longest", "greedy"}, +} + +// Canonical returns the alphabetically smallest token reachable from word +// in the synonym cluster via BFS. Words absent from the graph return themselves. +// This provides a stable canonical form so that semantically equivalent tokens +// (e.g. "size", "len", "count") all collapse to the same representative. +func Canonical(word string) string { + word = strings.ToLower(word) + visited := make(map[string]bool) + queue := []string{word} + visited[word] = true + smallest := word + + for len(queue) > 0 { + curr := queue[0] + queue = queue[1:] + + if curr < smallest { + smallest = curr + } + + for _, neighbour := range synonymGraph[curr] { + n := strings.ToLower(neighbour) + if !visited[n] { + visited[n] = true + queue = append(queue, n) + } + } + } + + return smallest +} + +// CanonicalTokens splits identifier by camelCase boundaries (via SplitCamel), +// canonicalises each token, and deduplicates. This is the semantic-aware +// token set that TokenOverlap uses for scoring. +func CanonicalTokens(identifier string) []string { + tokens := SplitCamel(identifier) + seen := make(map[string]bool) + result := make([]string, 0, len(tokens)) + for _, tok := range tokens { + c := Canonical(strings.ToLower(tok)) + if !seen[c] { + seen[c] = true + result = append(result, c) + } + } + return result +} diff --git a/lang/code/lex/declarations.go b/lang/code/lex/declarations.go index 722a7fe2..e8ac69d5 100644 --- a/lang/code/lex/declarations.go +++ b/lang/code/lex/declarations.go @@ -63,10 +63,10 @@ var Keywords = map[string]StaticToken{ "walkAll": staticOf(WalkAll), "global": staticOf(Global), "local": staticOf(Local), - "compute": staticOf(Compute), "this": staticOf(This, Value), "func": staticOf(Func), "when": staticOf(When), "any": staticOf(Any), "undefined": staticOf(Undefined), + "yield": staticOf(Yield), } diff --git a/lang/code/lex/lexer.go b/lang/code/lex/lexer.go index f178ffb5..7bdcb1f9 100644 --- a/lang/code/lex/lexer.go +++ b/lang/code/lex/lexer.go @@ -130,6 +130,8 @@ func (l *Lexer) parse() { case '.': if l.consume('.') { l.createOp("..") + } else if l.isDigit() { + l.numericFraction() } else { l.createOp(".") } @@ -184,19 +186,25 @@ func (l *Lexer) createOp(op string) { func (l *Lexer) colorCode() { startIndex := l.currIndex - // Read up to 6 hex characters - for i := 0; i < 6 && l.notEOF(); i++ { + // Read up to 8 hex characters (6 for RGB, 8 for ARGB) + for i := 0; i < 8 && l.notEOF(); i++ { c := l.peek() if (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') { l.skip() } else { - l.error("Invalid color code character '%' in color literal", string(c)) + break } } length := l.currIndex - startIndex - if length != 6 { - l.error("Color code must be 6 hexadecimal characters, got %", strconv.Itoa(length)) + if length != 6 && length != 8 { + l.error("Color code must be 6 or 8 hexadecimal characters, got %", strconv.Itoa(length)) + } + if l.notEOF() { + c := l.peek() + if (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') { + l.error("Color code must be 6 or 8 hexadecimal characters, got more than 8") + } } content := l.source[startIndex-1 : l.currIndex] // include '#' l.appendToken(&Token{ @@ -213,21 +221,44 @@ func (l *Lexer) colorCode() { func (l *Lexer) text() { var writer strings.Builder for { + if !l.notEOF() { + l.error("Unterminated string literal") + } c := l.next() if c == '"' { break } + if c == '\n' { + l.currColumn++ + l.currRow = 0 + } if c == '\\' { - // Only handle escaping of (") e := l.peek() - if e == '"' || e == '\\' { + l.skip() + switch e { + case '"': + c = '"' + case '\\': + c = '\\' + case 'n': + writer.WriteByte('\n') + continue + case 'r': + writer.WriteByte('\r') + continue + case 't': + writer.WriteByte('\t') + continue + case 's': + writer.WriteByte(' ') + continue + default: + writer.WriteByte('\\') c = e - l.skip() } } writer.WriteByte(c) } - println(len(writer.String()) == 0) content := writer.String() l.appendToken(&Token{ Context: l.ctx, @@ -275,6 +306,19 @@ func (l *Lexer) numeric() { l.back() } } + // scientific notation: optional e/E followed by optional sign and digits + if l.notEOF() && (l.peek() == 'e' || l.peek() == 'E') { + l.skip() + numb.WriteByte('e') + if l.notEOF() && (l.peek() == '+' || l.peek() == '-') { + numb.WriteByte(l.next()) + } + exp := l.readNumeric() + if len(exp) == 0 { + l.error("Expected exponent digits after 'e' in numeric literal") + } + numb.WriteString(exp) + } content := numb.String() l.appendToken(&Token{ Context: l.ctx, @@ -288,10 +332,23 @@ func (l *Lexer) numeric() { } func (l *Lexer) appendToken(token *Token) { - println(token.Debug()) l.Tokens = append(l.Tokens, token) } +// numericFraction handles decimal literals that start with '.' (e.g. .5, .123). +// The leading '.' has already been consumed by the dispatch switch. +func (l *Lexer) numericFraction() { + content := "0." + l.readNumeric() + l.appendToken(&Token{ + Context: l.ctx, + Row: l.currRow, + Column: l.currColumn, + Type: Number, + Content: &content, + Flags: []Flag{Value, ConstantValue}, + }) +} + func (l *Lexer) readNumeric() string { startIndex := l.currIndex for l.notEOF() && l.isDigit() { @@ -326,6 +383,9 @@ func (l *Lexer) error(message string, args ...string) { } func (l *Lexer) consume(expect uint8) bool { + if !l.notEOF() { + return false + } if l.peek() == expect { l.currIndex++ l.currRow++ @@ -345,6 +405,9 @@ func (l *Lexer) skip() { } func (l *Lexer) peek() uint8 { + if l.isEOF() { + return 0 + } return l.source[l.currIndex] } diff --git a/lang/code/lex/token.go b/lang/code/lex/token.go index b830c199..96b7e55d 100644 --- a/lang/code/lex/token.go +++ b/lang/code/lex/token.go @@ -41,6 +41,14 @@ func (t *Token) Error(message string, args ...string) { } } +func (t *Token) TypeError(message string, args ...string) { + if t.Context != nil { + (*t.Context).ReportTypeError(t.Column, t.Row, len(*t.Content), message, args...) + } else { + panic(sugar.Format(message, args...)) + } +} + func (t *Token) BuildError(decorate bool, message string, args ...string) string { if t.Context != nil { return (*t.Context).BuildError(decorate, t.Column, t.Row, len(*t.Content), message, args...) @@ -49,6 +57,16 @@ func (t *Token) BuildError(decorate bool, message string, args ...string) string } } +func (t *Token) BuildErrorHighlight(decorate bool, highlightSize int, message string) string { + if t.Context != nil { + // row marks the end of the token (1-indexed). Shift it right so the caret + // still starts at this token's first character but spans highlightSize chars. + rowShifted := t.Row + (highlightSize - len(*t.Content)) + return (*t.Context).BuildError(decorate, t.Column, rowShifted, highlightSize, message) + } + return message +} + type StaticToken struct { Type Type Flags []Flag @@ -77,12 +95,13 @@ func (s *StaticToken) Normal( // TODO: (future) it'll point to something meaningful func MakeFakeToken(t Type) *Token { + empty := "" return &Token{ Column: -1, Row: -1, Context: nil, Type: t, Flags: make([]Flag, 0), - Content: nil, + Content: &empty, } } diff --git a/lang/code/lex/type.go b/lang/code/lex/type.go index d7e6e10c..d732bdbe 100644 --- a/lang/code/lex/type.go +++ b/lang/code/lex/type.go @@ -68,10 +68,10 @@ const ( WalkAll Global Local - Compute This Func When Any Undefined + Yield ) diff --git a/lang/code/lex/type_string.go b/lang/code/lex/type_string.go index 2e23baff..215189d9 100644 --- a/lang/code/lex/type_string.go +++ b/lang/code/lex/type_string.go @@ -63,21 +63,22 @@ func _() { _ = x[WalkAll-52] _ = x[Global-53] _ = x[Local-54] - _ = x[Compute-55] - _ = x[This-56] - _ = x[Func-57] - _ = x[When-58] - _ = x[Any-59] - _ = x[Undefined-60] + _ = x[This-55] + _ = x[Func-56] + _ = x[When-57] + _ = x[Any-58] + _ = x[Undefined-59] + _ = x[Yield-60] } -const _Type_name = "PlusDashTimesSlashPowerRemainderLogicOrLogicAndBitwiseOrBitwiseAndBitwiseXorEqualsNotEqualsLessThanLessThanEqualGreatThanGreaterThanEqualTextEqualsTextNotEqualsTextLessThanTextGreaterThanOpenCurveCloseCurveOpenSquareCloseSquareOpenCurlyCloseCurlyAssignDotCommaQuestionNotColonDoubleColonDoubleDotRightArrowUnderscoreAtTrueFalseTextNumberNameColorCodeIfElseForStepInWhileDoBreakWalkAllGlobalLocalComputeThisFuncWhenAnyUndefined" +const _Type_name = "PlusDashTimesSlashPowerRemainderLogicOrLogicAndBitwiseOrBitwiseAndBitwiseXorEqualsNotEqualsLessThanLessThanEqualGreatThanGreaterThanEqualTextEqualsTextNotEqualsTextLessThanTextGreaterThanOpenCurveCloseCurveOpenSquareCloseSquareOpenCurlyCloseCurlyAssignDotCommaQuestionNotColonDoubleColonDoubleDotRightArrowUnderscoreAtTrueFalseTextNumberNameColorCodeIfElseForStepInWhileDoBreakWalkAllGlobalLocalThisFuncWhenAnyUndefinedYield" -var _Type_index = [...]uint16{0, 4, 8, 13, 18, 23, 32, 39, 47, 56, 66, 76, 82, 91, 99, 112, 121, 137, 147, 160, 172, 187, 196, 206, 216, 227, 236, 246, 252, 255, 260, 268, 271, 276, 287, 296, 306, 316, 318, 322, 327, 331, 337, 341, 350, 352, 356, 359, 363, 365, 370, 372, 377, 384, 390, 395, 402, 406, 410, 414, 417, 426} +var _Type_index = [...]uint16{0, 4, 8, 13, 18, 23, 32, 39, 47, 56, 66, 76, 82, 91, 99, 112, 121, 137, 147, 160, 172, 187, 196, 206, 216, 227, 236, 246, 252, 255, 260, 268, 271, 276, 287, 296, 306, 316, 318, 322, 327, 331, 337, 341, 350, 352, 356, 359, 363, 365, 370, 372, 377, 384, 390, 395, 399, 403, 407, 410, 419, 424} func (i Type) String() string { - if i < 0 || i >= Type(len(_Type_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_Type_index)-1 { return "Type(" + strconv.FormatInt(int64(i), 10) + ")" } - return _Type_name[_Type_index[i]:_Type_index[i+1]] + return _Type_name[_Type_index[idx]:_Type_index[idx+1]] } diff --git a/lang/code/parsers/blocklytomist/parser.go b/lang/code/parsers/blocklytomist/parser.go index 15890ed7..136b69a6 100644 --- a/lang/code/parsers/blocklytomist/parser.go +++ b/lang/code/parsers/blocklytomist/parser.go @@ -75,7 +75,7 @@ func (p *Parser) decodeXML() []ast.Block { func (p *Parser) parseAllBlocks(allBlocks []ast.Block) []ast.Expr { var parsedBlocks []ast.Expr for i := range allBlocks { - parsedBlocks = append(parsedBlocks, p.parseBlock(allBlocks[i])) + parsedBlocks = append(parsedBlocks, p.recursiveParse(allBlocks[i])...) } return parsedBlocks } @@ -84,7 +84,7 @@ func (p *Parser) singleExpr(block ast.Block) ast.Expr { if len(block.Values) == 0 { return &common.EmptySocket{} } - return p.parseBlock(block.Values[0].Block) + return p.parseValue(block.Values[0]) } func (p *Parser) parseBlock(block ast.Block) ast.Expr { @@ -113,7 +113,8 @@ func (p *Parser) parseBlock(block ast.Block) ast.Expr { case "controls_openAnotherScreen": return common.MakeFuncCall("openScreen", p.singleExpr(block)) case "controls_openAnotherScreenWithStartValue": - return common.MakeFuncCall("openScreenWithValue", p.singleExpr(block)) + pVals := p.makeValueMap(block.Values) + return common.MakeFuncCall("openScreenWithValue", pVals.get("SCREENNAME"), pVals.get("STARTVALUE")) case "controls_getStartValue": return common.MakeFuncCall("getStartValue") case "controls_closeScreen": @@ -204,7 +205,7 @@ func (p *Parser) parseBlock(block ast.Block) ast.Expr { case "math_single": return p.mathSingle(block) case "math_atan2": - return common.MakeFuncCall("aTan2", p.fromVals(block.Values)...) + return common.MakeFuncCall("atan2", p.fromVals(block.Values)...) case "math_format_as_decimal": return common.MakeFuncCall("formatDecimal", p.fromMinVals(block.Values, 2)...) case "math_divide": @@ -677,12 +678,12 @@ func (p *Parser) dictLookupPath(block ast.Block) ast.Expr { func (p *Parser) dictRemove(block ast.Block) ast.Expr { pVals := p.makeValueMap(block.Values) - return p.makePropCall("remove", pVals.get("DICT"), pVals.get("KEY")) + return p.makePropCall("delete", pVals.get("DICT"), pVals.get("KEY")) } func (p *Parser) dictSet(block ast.Block) ast.Expr { pVals := p.makeValueMap(block.Values) - return p.makePropCall("set", pVals.get("KEY"), pVals.get("VALUE")) + return p.makePropCall("set", pVals.get("DICT"), pVals.get("KEY"), pVals.get("VALUE")) } func (p *Parser) dictLookup(block ast.Block) ast.Expr { @@ -1121,6 +1122,10 @@ func (p *Parser) optSingleBody(block ast.Block) []ast.Expr { func (p *Parser) makeStatementMap(allStatements []ast.Statement) StatementMap { statementMap := make(map[string][]ast.Expr, len(allStatements)) for _, stmt := range allStatements { + if stmt.Block == nil { + statementMap[stmt.Name] = []ast.Expr{} + continue + } statementMap[stmt.Name] = p.recursiveParse(*stmt.Block) } return StatementMap{statementMap: statementMap} @@ -1130,7 +1135,7 @@ func (p *Parser) recursiveParse(currBlock ast.Block) []ast.Expr { var pParsed []ast.Expr for { pParsed = append(pParsed, p.parseBlock(currBlock)) - if currBlock.Next == nil { + if currBlock.Next == nil || currBlock.Next.Block == nil { break } currBlock = *currBlock.Next.Block @@ -1149,7 +1154,7 @@ func (p *Parser) makeFieldMap(allFields []ast.Field) map[string]string { func (p *Parser) makeValueMap(allValues []ast.Value) ValueMap { valueMap := make(map[string]ast.Expr, len(allValues)) for _, val := range allValues { - valueMap[val.Name] = p.parseBlock(val.Block) + valueMap[val.Name] = p.parseValue(val) } return ValueMap{valueMap: valueMap} } @@ -1157,7 +1162,7 @@ func (p *Parser) makeValueMap(allValues []ast.Value) ValueMap { func (p *Parser) fromVals(allValues []ast.Value) []ast.Expr { arrBlocks := make([]ast.Expr, len(allValues)) for i := range allValues { - arrBlocks[i] = p.parseBlock(allValues[i].Block) + arrBlocks[i] = p.parseValue(allValues[i]) } return arrBlocks } @@ -1166,10 +1171,20 @@ func (p *Parser) fromMinVals(allValues []ast.Value, minCount int) []ast.Expr { size := max(minCount, len(allValues)) arrExprs := make([]ast.Expr, size) for i := range allValues { - arrExprs[i] = p.parseBlock(allValues[i].Block) + arrExprs[i] = p.parseValue(allValues[i]) } for i := len(allValues); i < size; i++ { arrExprs[i] = &common.EmptySocket{} } return arrExprs } + +func (p *Parser) parseValue(val ast.Value) ast.Expr { + if val.Block.Type != "" { + return p.parseBlock(val.Block) + } + if val.Shadow != nil { + return p.parseBlock(val.Shadow.BlockValue()) + } + return &common.EmptySocket{} +} diff --git a/lang/code/parsers/mistparser/autocorrect.go b/lang/code/parsers/mistparser/autocorrect.go new file mode 100644 index 00000000..fed04e16 --- /dev/null +++ b/lang/code/parsers/mistparser/autocorrect.go @@ -0,0 +1,226 @@ +package mistparser + +import ( + "Falcon/code/ast" + "Falcon/code/ast/common" + "Falcon/code/ast/components" + "Falcon/code/ast/control" + "Falcon/code/ast/fundamentals" + "Falcon/code/ast/list" + "Falcon/code/ast/method" + "Falcon/code/ast/procedures" + "Falcon/code/ast/variables" +) + +// safeSignature returns expr.Signature() without panicking; falls back to [SignAny]. +func safeSignature(expr ast.Expr) (sigs []ast.Signature) { + defer func() { + if recover() != nil { + sigs = []ast.Signature{ast.SignAny} + } + }() + return expr.Signature() +} + +// walkAndCorrect recursively visits every expression in the tree, corrects +// auto-correctable names in-place, and records each change as a SourcePatch +// so the original source can be reconstructed via ReconstructedSource(). +func (p *LangParser) walkAndCorrect(expr ast.Expr) { + if !p.autoCorrect || expr == nil { + return + } + switch e := expr.(type) { + + case *method.Call: + var corrections []method.Correction + method.CorrectChainAndCollect(e, &corrections) + for _, c := range corrections { + p.patches = append(p.patches, SourcePatch{ + Line: c.Where.Column, + Start: c.Where.Row - len(c.OldName), + End: c.Where.Row, + Text: c.Replacement, + }) + } + for _, arg := range e.Args { + p.walkAndCorrect(arg) + } + + // Variable declarations + case *variables.Var: + for _, v := range e.Values { + p.walkAndCorrect(v) + } + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *variables.VarResult: + for _, v := range e.Values { + p.walkAndCorrect(v) + } + p.walkAndCorrect(e.Result) + case *variables.Set: + p.walkAndCorrect(e.Expr) + case *variables.Global: + p.walkAndCorrect(e.Value) + + // Procedures + case *procedures.RetProcedure: + p.walkAndCorrect(e.Result) + case *procedures.VoidProcedure: + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *procedures.Call: + for _, arg := range e.Arguments { + p.walkAndCorrect(arg) + } + + // Common expressions + case *common.FuncCall: + if !common.IsKnownFunction(e.Name) { + if best := common.FindBestSuggestion(e.Name); best != "" { + oldName := e.Name + e.Name = best + p.patches = append(p.patches, SourcePatch{ + Line: e.Where.Column, + Start: e.Where.Row - len(oldName), + End: e.Where.Row, + Text: best, + }) + } + } + for _, arg := range e.Args { + p.walkAndCorrect(arg) + } + case *common.BinaryExpr: + for _, op := range e.Operands { + p.walkAndCorrect(op) + } + case *common.Question: + oldQuestion := e.Question + if !common.IsKnownQuestion(e.Question) { + if best := common.FindBestQuestionSuggestion(e.Question); best != "" { + e.Question = best + } + } + if e.MethodCallSyntax { + // Source had .name() or .isName() — patch replaces dot + name + () with ? keyword. + // The leading space preserves the visual gap that the dot occupied (e.g. s.isString() → s ? text). + p.patches = append(p.patches, SourcePatch{ + Line: e.Where.Column, + Start: e.Where.Row - len(oldQuestion) - 1, // include the dot + End: e.Where.Row + 2, // include () + Text: " ? " + e.Question, + }) + } else if e.Question != oldQuestion { + // Source used ? syntax but with wrong keyword — simple in-place rename. + p.patches = append(p.patches, SourcePatch{ + Line: e.Where.Column, + Start: e.Where.Row - len(oldQuestion), + End: e.Where.Row, + Text: e.Question, + }) + } + p.walkAndCorrect(e.On) + + // Control flow + case *control.If: + for _, cond := range e.Conditions { + p.walkAndCorrect(cond) + } + for _, body := range e.Bodies { + for _, b := range body { + p.walkAndCorrect(b) + } + } + for _, b := range e.ElseBody { + p.walkAndCorrect(b) + } + case *control.For: + p.walkAndCorrect(e.From) + p.walkAndCorrect(e.To) + p.walkAndCorrect(e.By) + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *control.While: + p.walkAndCorrect(e.Condition) + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *control.Each: + p.walkAndCorrect(e.Iterable) + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *control.EachPair: + p.walkAndCorrect(e.Iterable) + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *control.Do: + for _, b := range e.Body { + p.walkAndCorrect(b) + } + p.walkAndCorrect(e.Result) + + // List operations + case *list.Transformer: + p.walkAndCorrect(e.List) + for _, arg := range e.Args { + p.walkAndCorrect(arg) + } + p.walkAndCorrect(e.Transformer) + case *list.Get: + p.walkAndCorrect(e.List) + p.walkAndCorrect(e.Index) + case *list.Set: + p.walkAndCorrect(e.List) + p.walkAndCorrect(e.Index) + p.walkAndCorrect(e.Value) + + // Fundamentals that can contain sub-expressions + case *fundamentals.SmartBody: + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *fundamentals.List: + for _, item := range e.Elements { + p.walkAndCorrect(item) + } + case *fundamentals.Dictionary: + for _, item := range e.Elements { + p.walkAndCorrect(item) + } + case *fundamentals.Pair: + p.walkAndCorrect(e.Key) + p.walkAndCorrect(e.Value) + case *fundamentals.Not: + p.walkAndCorrect(e.Expr) + case *common.Transform: + p.walkAndCorrect(e.On) + + // Component event handlers + case *components.Event: + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *components.GenericEvent: + for _, b := range e.Body { + p.walkAndCorrect(b) + } + case *components.PropertySet: + p.walkAndCorrect(e.Value) + case *components.GenericPropertySet: + p.walkAndCorrect(e.Value) + case *components.MethodCall: + for _, arg := range e.Args { + p.walkAndCorrect(arg) + } + case *components.GenericMethodCall: + for _, arg := range e.Args { + p.walkAndCorrect(arg) + } + } +} diff --git a/lang/code/parsers/mistparser/error_rendering.go b/lang/code/parsers/mistparser/error_rendering.go new file mode 100644 index 00000000..79ef7e91 --- /dev/null +++ b/lang/code/parsers/mistparser/error_rendering.go @@ -0,0 +1,157 @@ +package mistparser + +import ( + "sort" + "strconv" + "strings" + + l "Falcon/code/lex" +) + +type pendingCallError struct { + token *l.Token + name string + suggestion string +} + +// renderCallErrorGroups groups bad call names by source line and renders +// each group as a single annotated block. All carets appear on one row +// and all hints appear on the row below that, both aligned to each bad name. +// For method chains, dots are expanded with surrounding spaces for readability. +// +// Example (autocorrect off, three bad names on one line): +// +// println(s . upperCase() . replaceAll(" ", "") . size()) +// ^^^^^^^^^ ^^^^^^^^^^ ^^^^ +// uppercase replace textLen ← correct names +// [line 2] +func renderCallErrorGroups(byLine map[int][]pendingCallError) []string { + lineNums := make([]int, 0, len(byLine)) + for ln := range byLine { + lineNums = append(lineNums, ln) + } + sort.Ints(lineNums) + + var blocks []string + for _, lineNum := range lineNums { + items := byLine[lineNum] + sort.Slice(items, func(i, j int) bool { + return items[i].token.Row < items[j].token.Row + }) + + ctx := items[0].token.Context + if ctx == nil { + for _, item := range items { + msg := "\nNo method named ." + item.name + "()" + if item.suggestion != "" { + msg += " → " + item.suggestion + } + msg += "\n[line " + strconv.Itoa(lineNum) + "]" + blocks = append(blocks, msg) + } + continue + } + + sourceLine := (*ctx).GetLine(lineNum) + formatted, offsets := expandDots(sourceLine) + + // Compute the rightmost extent so buffers are large enough. + maxEnd := len(formatted) + 20 + for _, item := range items { + origStart := item.token.Row - len(item.name) + newStart := offsetAt(offsets, origStart) + end := newStart + len(item.name) + if end > maxEnd { + maxEnd = end + 20 + } + } + + caretBuf := makeLine(maxEnd) + hintBuf := makeLine(maxEnd) + + hasSuggestion := false + for _, item := range items { + origStart := item.token.Row - len(item.name) + newStart := offsetAt(offsets, origStart) + + writeTo(caretBuf, newStart, strings.Repeat("^", len(item.name))) + + hint := item.suggestion + if hint == "" { + hint = "?" + } else { + hasSuggestion = true + } + writeTo(hintBuf, newStart, hint) + } + + hintLine := strings.TrimRight(string(hintBuf), " ") + if hasSuggestion { + hintLine += " ← correct names" + } + + var sb strings.Builder + sb.WriteByte('\n') + sb.WriteString(formatted) + sb.WriteByte('\n') + sb.WriteString(strings.TrimRight(string(caretBuf), " ")) + sb.WriteByte('\n') + sb.WriteString(hintLine) + sb.WriteByte('\n') + sb.WriteString("[line " + strconv.Itoa(lineNum) + "]") + blocks = append(blocks, sb.String()) + } + return blocks +} + +// expandDots returns a copy of line with a space inserted before and after +// every dot that is immediately followed by a letter (a method-call dot). +// It also returns an offsets slice where offsets[i] is the position in the +// formatted string that corresponds to position i in the original line. +func expandDots(line string) (formatted string, offsets []int) { + offsets = make([]int, len(line)+1) + var sb strings.Builder + pos := 0 + for i := 0; i < len(line); i++ { + offsets[i] = pos + c := line[i] + if c == '.' && i+1 < len(line) && isAlphaChar(line[i+1]) { + sb.WriteString(" . ") + pos += 3 + } else { + sb.WriteByte(c) + pos++ + } + } + offsets[len(line)] = pos + formatted = sb.String() + return +} + +func offsetAt(offsets []int, orig int) int { + if orig < 0 { + return 0 + } + if orig >= len(offsets) { + return offsets[len(offsets)-1] + } + return offsets[orig] +} + +func makeLine(size int) []byte { + b := make([]byte, size) + for i := range b { + b[i] = ' ' + } + return b +} + +func writeTo(buf []byte, pos int, text string) { + for i := 0; i < len(text) && pos+i < len(buf); i++ { + buf[pos+i] = text[i] + } +} + +func isAlphaChar(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} diff --git a/lang/code/parsers/mistparser/frame_type.go b/lang/code/parsers/mistparser/frame_type.go new file mode 100644 index 00000000..5083e649 --- /dev/null +++ b/lang/code/parsers/mistparser/frame_type.go @@ -0,0 +1,27 @@ +package mistparser + +import "Falcon/code/ast" + +//go:generate stringer -type=FrameType +type FrameType int + +const ( + FrameTypeIf FrameType = iota + FrameTypeLoop + FrameTypeSmartBody + FrameTypeVar + FrameTypeYield + FrameTypeNoYield +) + +type Frame struct { + FrameType FrameType + Expr ast.Expr +} + +func AppendFrame(frames []Frame, ft FrameType, expr ast.Expr) []Frame { + var newFrames []Frame + newFrames = append(newFrames, frames...) + newFrames = append(newFrames, Frame{FrameType: ft, Expr: expr}) + return newFrames +} diff --git a/lang/code/parsers/mistparser/lang_parser.go b/lang/code/parsers/mistparser/lang_parser.go index 8d45300b..f56e0880 100644 --- a/lang/code/parsers/mistparser/lang_parser.go +++ b/lang/code/parsers/mistparser/lang_parser.go @@ -18,25 +18,37 @@ import ( ) type LangParser struct { - Tokens []*l.Token - currIndex int - tokenSize int - currCheckpoint int + Tokens []*l.Token + currIndex int + tokenSize int - strict bool + strict bool + autoCorrect bool Resolver *NameResolver ScopeCursor *ScopeCursor aggregator *ErrorAggregator + patches []SourcePatch +} + +// EnableAutoCorrect turns on the auto-correction pass. Disabled by default. +func (p *LangParser) EnableAutoCorrect() { p.autoCorrect = true } + +// ReconstructedSource returns the original source code with all auto-corrections +// applied in-place. +func (p *LangParser) ReconstructedSource() string { + if len(p.Tokens) == 0 || p.Tokens[0].Context == nil { + return "" + } + return ApplyPatches(*p.Tokens[0].Context.SourceCode, p.patches) } func NewLangParser(strict bool, tokens []*l.Token) *LangParser { return &LangParser{ - Tokens: tokens, - tokenSize: len(tokens), - currIndex: 0, - currCheckpoint: 0, - strict: strict, + Tokens: tokens, + tokenSize: len(tokens), + currIndex: 0, + strict: strict, Resolver: &NameResolver{ Procedures: map[string]*Procedure{}, ComponentTypesMap: map[string]string{}, @@ -70,23 +82,63 @@ func (p *LangParser) ParseAll() []ast.Expr { e := p.parse() expressions = append(expressions, e) } - if p.strict { - p.checkPendingSymbols() + for _, e := range expressions { + p.walkAndCorrect(e) + } + p.checkPendingSymbols() + for _, e := range expressions { + e.Signature() } return expressions } func (p *LangParser) checkPendingSymbols() { var errorMessages []string + methodErrors := make(map[int][]pendingCallError) // keyed by line number + funcErrors := make(map[int][]pendingCallError) + questionErrors := make(map[int][]pendingCallError) + methodErrorCount := 0 + funcErrorCount := 0 + questionErrorCount := 0 + for token, parseError := range p.aggregator.Errors { // try resolve global variables again if get, ok := parseError.Owner.(*variables.Get); ok && get.Global { - signatures, resolved := p.ScopeCursor.ResolveVariable(get.Name) - println("Failed to resolve: " + get.String()) + signatures, resolved := p.ScopeCursor.ReferGlobalVariable(get.Name) if resolved { get.ValueSignature = signatures continue } + } else if mc, ok := parseError.Owner.(*method.Call); ok { + if _, sig := method.TestSignature(mc.Name, len(mc.Args)); sig != nil { + continue + } + inputSigs := safeSignature(mc.On) + allowedModules := method.DeriveAllowedModules(inputSigs) + suggestion := method.FindBestSuggestion(mc.Name, allowedModules, mc.HintOutput()) + methodErrors[token.Column] = append(methodErrors[token.Column], pendingCallError{token: token, name: mc.Name, suggestion: suggestion}) + methodErrorCount++ + continue + } else if q, ok := parseError.Owner.(*common.Question); ok { + if common.IsKnownQuestion(q.Question) { + continue + } + suggestion := common.FindBestQuestionSuggestion(q.Question) + hint := "" + if suggestion != "" { + hint = "? " + suggestion + } + questionErrors[token.Column] = append(questionErrors[token.Column], pendingCallError{token: token, name: q.Question, suggestion: hint}) + questionErrorCount++ + continue + } else if fc, ok := parseError.Owner.(*common.FuncCall); ok { + if _, sig := common.TestSignature(fc.Name, len(fc.Args)); sig != nil { + continue + } + suggestion := common.FindBestSuggestion(fc.Name) + funcErrors[token.Column] = append(funcErrors[token.Column], pendingCallError{token: token, name: fc.Name, suggestion: suggestion}) + funcErrorCount++ + continue } else if procCall, ok := parseError.Owner.(*procedures.Call); ok { // a late resolution of procedure calls procedureErrorMessage, procedureSignature := p.Resolver.ResolveProcedure(procCall.Name, len(procCall.Arguments)) @@ -99,9 +151,24 @@ func (p *LangParser) checkPendingSymbols() { } errorMessages = append(errorMessages, token.BuildError(false, parseError.ErrorMessage)) } - if len(errorMessages) > 0 { + + p.reportCompileErrors(errorMessages, methodErrors, funcErrors, questionErrors, methodErrorCount, funcErrorCount, questionErrorCount) +} + +func (p *LangParser) reportCompileErrors(errorMessages []string, methodErrors, funcErrors, questionErrors map[int][]pendingCallError, methodErrorCount, funcErrorCount, questionErrorCount int) { + methodBlocks := renderCallErrorGroups(methodErrors) + funcBlocks := renderCallErrorGroups(funcErrors) + questionBlocks := renderCallErrorGroups(questionErrors) + errorMessages = append(errorMessages, methodBlocks...) + errorMessages = append(errorMessages, funcBlocks...) + errorMessages = append(errorMessages, questionBlocks...) + + // Count each individual bad call, not each group block. + groupBlocks := len(methodBlocks) + len(funcBlocks) + len(questionBlocks) + totalErrors := len(errorMessages) - groupBlocks + methodErrorCount + funcErrorCount + questionErrorCount + if p.strict && totalErrors > 0 { var errorWriter strings.Builder - errorWriter.WriteString(sugar.Format("compile failed with % syntax errors", strconv.Itoa(len(errorMessages)))) + errorWriter.WriteString(sugar.Format("compile failed with % syntax errors", strconv.Itoa(totalErrors))) errorWriter.WriteString(strings.Join(errorMessages, "")) panic(errorWriter.String()) } @@ -136,12 +203,17 @@ func (p *LangParser) parse() ast.Expr { case l.While: return p.whileExpr() case l.Break: - p.skip() + tok := p.next() + if !p.ScopeCursor.In(ScopeLoop) { + tok.Error("break can only be used inside a loop") + } return &control.Break{} + case l.Yield: + return p.yieldSmt() case l.Local: - return p.varExpr() + return p.localSmt() case l.Global: - return p.globVar() + return p.globalSmt() case l.Func: return p.funcSmt() case l.When: @@ -151,11 +223,39 @@ func (p *LangParser) parse() ast.Expr { } return p.event() default: - // It cannot be consumable return p.expr(0) } } +func (p *LangParser) yieldSmt() ast.Expr { + tok := p.next() + if !p.ScopeCursor.In(ScopeRetProc) { + tok.Error("yield can only be used inside a returning procedure") + } + yieldName := "_result" + expr := p.parse() + if p.ScopeCursor.currScope.Type == ScopeRetProc && p.isNext(l.CloseCurly) { + // just return the expr as is + return expr + } + // _result = [ false, ] + transformedExpr := &variables.Set{ + Global: false, + Name: yieldName, + Expr: &fundamentals.List{ + Elements: []ast.Expr{ + &fundamentals.Boolean{Value: false}, + expr, + }, + }, + } + return &fundamentals.Yield{ + Expr: expr, + TransformedExpr: transformedExpr, + UseTransformed: false, + } +} + func (p *LangParser) genericEvent() ast.Expr { componentType := p.componentType() p.expect(l.Dot) @@ -164,14 +264,7 @@ func (p *LangParser) genericEvent() ast.Expr { if p.isNext(l.OpenCurve) { parameters = p.parameters() } - where := p.expect(l.OpenCurly) - p.ScopeCursor.Enter(where, ScopeEvent) - for _, param := range parameters { - p.ScopeCursor.DefineVariable(param, []ast.Signature{ast.SignOfEvent, ast.SignAny}) - } - body := p.bodyUntilCurly() - p.ScopeCursor.Exit(ScopeEvent) - p.expect(l.CloseCurly) + body := p.parseEventBody(parameters) return &components.GenericEvent{ComponentType: componentType, Event: eventName, Parameters: parameters, Body: body} } @@ -183,15 +276,7 @@ func (p *LangParser) event() ast.Expr { if p.isNext(l.OpenCurve) { parameters = p.parameters() } - where := p.expect(l.OpenCurly) - p.ScopeCursor.Enter(where, ScopeEvent) - for _, param := range parameters { - p.ScopeCursor.DefineVariable(param, []ast.Signature{ast.SignOfEvent, ast.SignAny}) - } - body := p.bodyUntilCurly() - p.ScopeCursor.Exit(ScopeEvent) - p.expect(l.CloseCurly) - + body := p.parseEventBody(parameters) return &components.Event{ ComponentName: component.Name, ComponentType: component.Type, @@ -201,39 +286,53 @@ func (p *LangParser) event() ast.Expr { } } +func (p *LangParser) parseEventBody(parameters []string) []ast.Expr { + vars := make([]ScopeVar, len(parameters)) + for i, param := range parameters { + vars[i] = scopeVar(param, ast.SignOfEvent, ast.SignAny) + } + return p.body(ScopeEvent, vars...) +} + func (p *LangParser) funcSmt() ast.Expr { where := p.next() name := p.name() - var parameters = p.parameters() + parameters := p.parameters() returning := p.consume(l.Assign) p.Resolver.Procedures[name] = &Procedure{Name: name, Parameters: parameters, Returning: returning} if returning { - p.ScopeCursor.Enter(where, ScopeSmartBody) - for _, parameter := range parameters { - p.ScopeCursor.DefineVariable(parameter, []ast.Signature{ast.SignAny}) - } - var result ast.Expr - if p.isNext(l.OpenCurly) { - result = p.smartBody() - } else { - result = p.parse() - } - p.ScopeCursor.Exit(ScopeSmartBody) - return &procedures.RetProcedure{Name: name, Parameters: parameters, Result: result} - } else { - where := p.expect(l.OpenCurly) - p.ScopeCursor.Enter(where, ScopeProc) - for _, parameter := range parameters { - p.ScopeCursor.DefineVariable(parameter, []ast.Signature{ast.SignAny}) - } - body := p.bodyUntilCurly() - p.ScopeCursor.Exit(ScopeProc) + return p.retProcedure(where, name, parameters) + } + return p.voidProcedure(name, parameters) +} + +func (p *LangParser) retProcedure(where *l.Token, name string, parameters []string) ast.Expr { + p.ScopeCursor.Enter(where, ScopeRetProc) + for _, parameter := range parameters { + p.ScopeCursor.DefineVariable(parameter, []ast.Signature{ast.SignAny}) + } + var result ast.Expr + if p.consume(l.OpenCurly) { + yieldParser := &YieldParser{Exprs: p.bodyUntilCurly()} + result = &fundamentals.SmartBody{Body: yieldParser.ParseYield()} p.expect(l.CloseCurly) - return &procedures.VoidProcedure{Name: name, Parameters: parameters, Body: body} + } else { + result = p.parse() } + p.ScopeCursor.Exit(ScopeRetProc) + return &procedures.RetProcedure{Name: name, Parameters: parameters, Result: result} } -func (p *LangParser) globVar() ast.Expr { +func (p *LangParser) voidProcedure(name string, parameters []string) ast.Expr { + vars := make([]ScopeVar, len(parameters)) + for i, param := range parameters { + vars[i] = scopeVar(param, ast.SignAny) + } + body := p.body(ScopeProc, vars...) + return &procedures.VoidProcedure{Name: name, Parameters: parameters, Body: body} +} + +func (p *LangParser) globalSmt() ast.Expr { where := p.next() if !p.ScopeCursor.AtRoot() { where.Error("Global variables can only be defined at the root.") @@ -245,23 +344,25 @@ func (p *LangParser) globVar() ast.Expr { return &variables.Global{Name: name, Value: value} } -func (p *LangParser) varExpr() ast.Expr { +func (p *LangParser) localSmt() ast.Expr { // a clean full scope variable var names []string var values []ast.Expr for { - p.createCheckpoint() + locCurrIndex := p.currIndex if !p.consume(l.Local) { break } name := p.name() p.expect(l.Assign) + preParseSumVarRefCount := p.GetSummatedVarRefCount(names) value := p.parse() + postParseSumVarRefCount := p.GetSummatedVarRefCount(names) - if ast.DependsOnVariables(value, names) { + if postParseSumVarRefCount > preParseSumVarRefCount { // Since this variable depends on the last variable, we cannot include // it in the current set. - p.backToPast() + p.currIndex = locCurrIndex break } @@ -270,57 +371,65 @@ func (p *LangParser) varExpr() ast.Expr { p.ScopeCursor.DefineVariable(name, value.Signature()) } // we have to parse rest of the body here - return &variables.Var{Names: names, Values: values, Body: p.bodyUntilCurly()} + body := p.bodyUntilCurly() + if len(body) == 1 && body[0].Consumable() { + return &variables.VarResult{Names: names, Values: values, Result: body[0]} + } + return &variables.Var{Names: names, Values: values, Body: body} + //return &variables.VarStack{Names: names, Values: values} +} + +func (p *LangParser) GetSummatedVarRefCount(names []string) int { + summatedCount := 0 + for _, name := range names { + refCount := p.ScopeCursor.GetVariableReferCount(name) + if refCount == -1 { + panic("Trying to query ref count of undeclared variable: " + name) + } + summatedCount += refCount + } + return summatedCount } func (p *LangParser) whileExpr() *control.While { - p.skip() + whileTok := p.next() p.expect(l.OpenCurve) condition := p.parse() p.expect(l.CloseCurve) body := p.body(ScopeLoop) - return &control.While{Condition: condition, Body: body} + return &control.While{Where: whileTok, Condition: condition, Body: body} } func (p *LangParser) forExpr() ast.Expr { - // TODO: - // We could refactor this later to reuse declaring variables inside body - p.skip() + forTok := p.next() p.expect(l.OpenCurve) firstName := p.name() if p.consume(l.Comma) { - // Dictionary For each loop - valueName := p.name() - p.expect(l.In) - iterable := p.parse() - p.expect(l.CloseCurve) - - where := p.expect(l.OpenCurly) - p.ScopeCursor.Enter(where, ScopeLoop) - p.ScopeCursor.DefineVariable(firstName, iterable.Signature()) - p.ScopeCursor.DefineVariable(valueName, iterable.Signature()) - body := p.bodyUntilCurly() - p.ScopeCursor.Exit(ScopeLoop) - p.expect(l.CloseCurly) - - return &control.EachPair{KeyName: firstName, ValueName: valueName, Iterable: iterable, Body: body} + return p.forEachPair(forTok, firstName) } else if p.consume(l.In) { - // For each loop - iterable := p.parse() - p.expect(l.CloseCurve) + return p.forEach(forTok, firstName) + } + return p.forRange(forTok, firstName) +} - where := p.expect(l.OpenCurly) - p.ScopeCursor.Enter(where, ScopeLoop) - p.ScopeCursor.DefineVariable(firstName, iterable.Signature()) - body := p.bodyUntilCurly() - p.ScopeCursor.Exit(ScopeLoop) - p.expect(l.CloseCurly) +func (p *LangParser) forEachPair(forTok *l.Token, keyName string) ast.Expr { + valueName := p.name() + p.expect(l.In) + iterable := p.parse() + p.expect(l.CloseCurve) + body := p.body(ScopeLoop, scopeVar(keyName, ast.SignAny), scopeVar(valueName, ast.SignAny)) + return &control.EachPair{Where: forTok, KeyName: keyName, ValueName: valueName, Iterable: iterable, Body: body} +} - return &control.Each{IName: firstName, Iterable: iterable, Body: body} - } - // For I loop +func (p *LangParser) forEach(forTok *l.Token, iName string) ast.Expr { + iterable := p.parse() + p.expect(l.CloseCurve) + body := p.body(ScopeLoop, scopeVar(iName, ast.SignAny)) + return &control.Each{Where: forTok, IName: iName, Iterable: iterable, Body: body} +} + +func (p *LangParser) forRange(forTok *l.Token, iName string) ast.Expr { p.expect(l.Colon) - // Earlier we were using p.element(), check the side effects from := p.parse() p.expect(l.DoubleDot) to := p.parse() @@ -331,53 +440,58 @@ func (p *LangParser) forExpr() ast.Expr { by = &fundamentals.Number{Content: "1"} } p.expect(l.CloseCurve) - - where := p.expect(l.OpenCurly) - p.ScopeCursor.Enter(where, ScopeLoop) - p.ScopeCursor.DefineVariable(firstName, []ast.Signature{ast.SignNumb}) - body := p.bodyUntilCurly() - p.ScopeCursor.Exit(ScopeLoop) - p.expect(l.CloseCurly) - - return &control.For{IName: firstName, From: from, To: to, By: by, Body: body} + body := p.body(ScopeLoop, scopeVar(iName, ast.SignNumb)) + return &control.For{Where: forTok, IName: iName, From: from, To: to, By: by, Body: body} } func (p *LangParser) ifSmt() ast.Expr { - p.skip() + where := p.next() var conditions []ast.Expr var bodies [][]ast.Expr + p.expect(l.OpenCurve) conditions = append(conditions, p.expr(0)) - if p.isNext(l.OpenCurly) { - bodies = append(bodies, p.body(ScopeIfBody)) - } else { - bodies = append(bodies, []ast.Expr{p.parse()}) - } + p.expect(l.CloseCurve) + bodies = append(bodies, p.parseConditionBody()) var elseBody []ast.Expr for p.notEOF() && p.consume(l.Else) { if p.consume(l.If) { + p.expect(l.OpenCurve) conditions = append(conditions, p.expr(0)) - if p.isNext(l.OpenCurly) { - bodies = append(bodies, p.body(ScopeIfBody)) - } else { - bodies = append(bodies, []ast.Expr{p.parse()}) - } + p.expect(l.CloseCurve) + bodies = append(bodies, p.parseConditionBody()) } else { - if p.isNext(l.OpenCurly) { - elseBody = p.body(ScopeIfBody) - } else { - elseBody = []ast.Expr{p.parse()} - } + elseBody = p.parseConditionBody() break } } - return &control.If{Conditions: conditions, Bodies: bodies, ElseBody: elseBody} + return &control.If{Where: where, Conditions: conditions, Bodies: bodies, ElseBody: elseBody} +} + +func (p *LangParser) parseConditionBody() []ast.Expr { + if p.isNext(l.OpenCurly) { + return p.body(ScopeIfBody) + } + return []ast.Expr{p.parse()} } -func (p *LangParser) body(scope ScopeType) []ast.Expr { +// ScopeVar declares a variable immediately after entering a new scope. +type ScopeVar struct { + Name string + Sigs []ast.Signature +} + +func scopeVar(name string, sigs ...ast.Signature) ScopeVar { + return ScopeVar{Name: name, Sigs: sigs} +} + +func (p *LangParser) body(scope ScopeType, vars ...ScopeVar) []ast.Expr { where := p.expect(l.OpenCurly) p.ScopeCursor.Enter(where, scope) + for _, v := range vars { + p.ScopeCursor.DefineVariable(v.Name, v.Sigs) + } expressions := p.bodyUntilCurly() p.ScopeCursor.Exit(scope) p.expect(l.CloseCurly) @@ -390,8 +504,24 @@ func (p *LangParser) bodyUntilCurly() []ast.Expr { return expressions } for p.notEOF() && !p.isNext(l.CloseCurly) { - expressions = append(expressions, p.parse()) + expr := p.parse() + expressions = append(expressions, expr) p.consume(l.Comma) + + // no statements allowed after `break` + switch expr.(type) { + case *control.Break, *fundamentals.Yield: + if p.notEOF() && !p.isNext(l.CloseCurly) { + p.peek().Error("unreachable code after '%'", expr.String()) + } + } + switch expr.(type) { + case *fundamentals.Yield: + if p.ScopeCursor.In(ScopeLoop) { + // we have to inject a break statement here + expressions = append(expressions, &control.Break{}) + } + } } return expressions } @@ -418,7 +548,7 @@ func (p *LangParser) expr(minPrecedence int) ast.Expr { if opToken.HasFlag(l.PreserveOrder) { right = p.element() } else { - right = p.expr(precedence) + right = p.expr(precedence + 1) } if rBinExpr, ok := right.(*common.BinaryExpr); ok && rBinExpr.CanRepeat(opToken.Type) { // for NoPreserveOrder: merge binary expr with same operator (towards right) @@ -446,10 +576,9 @@ func (p *LangParser) compoundOperator(opToken *l.Token, left ast.Expr) ast.Expr expr, done := p.assignSmt(left, binaryOperator) if done { return expr - } else { - opToken.Error("Unknown compound operator '%='", *opToken.Content) - panic("unreached") } + opToken.Error("Unknown compound operator '%='", *opToken.Content) + panic("unreached") } func (p *LangParser) makeBinary(opToken *l.Token, left ast.Expr, right ast.Expr) ast.Expr { @@ -472,7 +601,7 @@ func (p *LangParser) assignSmt(left ast.Expr, right ast.Expr) (ast.Expr, bool) { p.aggregator.MarkResolved(nameExpr.Where) return &variables.Set{Global: nameExpr.Global, Name: nameExpr.Name, Expr: right}, true } else if listGet, ok := left.(*list.Get); ok { - return &list.Set{List: listGet.List, Index: listGet.Index, Value: right}, true + return &list.Set{Where: listGet.Where, List: listGet.List, Index: listGet.Index, Value: right}, true } return nil, false } @@ -503,9 +632,12 @@ func (p *LangParser) element() ast.Expr { // constant value transformer left = &common.Transform{Where: p.next(), On: left, Name: p.name()} case l.OpenSquare: - p.skip() + if p.isOnNewLine() { + break // '[' is on a new line — new statement, not index access + } + openSquare := p.next() // an index element access - left = &list.Get{List: left, Index: p.parse()} + left = &list.Get{Where: openSquare, List: left, Index: p.parse()} p.expect(l.CloseSquare) continue } @@ -550,23 +682,43 @@ func (p *LangParser) objectCall(object ast.Expr) ast.Expr { p.skip() where := p.next() name := *where.Content - var args []ast.Expr if p.isNext(l.OpenCurve) { args = p.arguments() - if !p.isNext(l.OpenCurly) { - // he's a simple call! - errorMessage, signature := method.TestSignature(name, len(args)) - if signature == nil { - p.aggregator.EnqueueSymbol(where, object, errorMessage) - } else { - p.aggregator.MarkResolved(where) + // Only treat as a transformer if the name is a known transformer signature + // AND the arg count matches — otherwise the '{' belongs to the surrounding expression. + if !p.isNext(l.OpenCurly) || !list.IsTransformer(name, len(args)) { + return p.parseMethodCall(object, where, name, args) + } + } + return p.parseTransformer(object, where, name, args) +} + +func (p *LangParser) parseMethodCall(object ast.Expr, where *l.Token, name string, args []ast.Expr) ast.Expr { + call := &method.Call{Where: where, On: object, Name: name, Args: args} + errorMessage, signature := method.TestSignature(name, len(args)) + if signature == nil { + // Before treating as a bad method, check if it looks like a question (? keyword). + if len(args) == 0 { + if common.FindBestQuestionSuggestion(name) != "" { + q := &common.Question{Where: where, On: object, Question: name, MethodCallSyntax: true} + if common.IsKnownQuestion(name) { + p.aggregator.MarkResolved(where) + } else { + p.aggregator.EnqueueSymbol(where, q, "") + } + return q } - return &method.Call{Where: where, On: object, Name: name, Args: args} } + p.aggregator.EnqueueSymbol(where, call, errorMessage) + } else { + p.aggregator.MarkResolved(where) } + return call +} + +func (p *LangParser) parseTransformer(object ast.Expr, where *l.Token, name string, args []ast.Expr) ast.Expr { p.expect(l.OpenCurly) - // oh, no! he's a transformer >_> p.ScopeCursor.Enter(where, ScopeTypeTransform) var namesUsed []string if !p.consume(l.RightArrow) { @@ -578,11 +730,11 @@ func (p *LangParser) objectCall(object ast.Expr) ast.Expr { break } } - p.consume(l.RightArrow) + p.expect(l.RightArrow) } - transformer := p.parse() + transformer := p.exprOrSmartBody() p.ScopeCursor.Exit(ScopeTypeTransform) - p.consume(l.CloseCurly) + p.expect(l.CloseCurly) errorMessage, signature := list.TestSignature(name, len(args), len(namesUsed)) if signature == nil { p.aggregator.EnqueueSymbol(where, object, errorMessage) @@ -595,7 +747,22 @@ func (p *LangParser) objectCall(object ast.Expr) ast.Expr { Name: name, Args: args, Names: namesUsed, - Transformer: transformer} + Transformer: transformer, + } +} + +func (p *LangParser) exprOrSmartBody() ast.Expr { + locCurrIndex := p.currIndex + simpleExpr := p.parse() + if simpleExpr.Consumable() { + return simpleExpr + } + p.currIndex = locCurrIndex + // we gotta do a manual smart body here + p.ScopeCursor.Enter(p.peek(), ScopeSmartBody) + smartBody := &fundamentals.SmartBody{Body: p.bodyUntilCurly()} + p.ScopeCursor.Exit(ScopeSmartBody) + return smartBody } func (p *LangParser) term() ast.Expr { @@ -619,8 +786,6 @@ func (p *LangParser) term() ast.Expr { case l.If: p.back() return p.ifSmt() - case l.Compute: - return p.computeExpr() case l.WalkAll: return &fundamentals.WalkAll{} default: @@ -649,7 +814,7 @@ func (p *LangParser) smartBody() ast.Expr { func (p *LangParser) checkCall(token *l.Token) ast.Expr { value := p.value(token) - if nameExpr, ok := value.(*variables.Get); ok && !nameExpr.Global && p.isNext(l.OpenCurve) { + if nameExpr, ok := value.(*variables.Get); ok && !nameExpr.Global && p.isNext(l.OpenCurve) && !p.isOnNewLine() { arguments := p.arguments() // check for in-built function call _, funcCallSignature := common.TestSignature(nameExpr.Name, len(arguments)) @@ -657,50 +822,38 @@ func (p *LangParser) checkCall(token *l.Token) ast.Expr { p.aggregator.MarkResolved(nameExpr.Where) return &common.FuncCall{Where: nameExpr.Where, Name: nameExpr.Name, Args: arguments} } + if common.IsKnownFunction(nameExpr.Name) { + fc := &common.FuncCall{Where: nameExpr.Where, Name: nameExpr.Name, Args: arguments} + p.aggregator.EnqueueSymbol(nameExpr.Where, fc, "Bad call to function "+nameExpr.Name+"()") + return fc + } // check for a user defined procedure procedureErrorMessage, procedureSignature := p.Resolver.ResolveProcedure(nameExpr.Name, len(arguments)) - var funcCall *procedures.Call if procedureSignature != nil { - funcCall = &procedures.Call{ + p.aggregator.MarkResolved(nameExpr.Where) + return &procedures.Call{ + Where: nameExpr.Where, Name: nameExpr.Name, Parameters: procedureSignature.Parameters, Arguments: arguments, Returning: procedureSignature.Returning, } - p.aggregator.MarkResolved(nameExpr.Where) - } else { - // just fill in a template, could be resolved later - funcCall = &procedures.Call{Name: nameExpr.Name, Arguments: arguments} - p.aggregator.EnqueueSymbol(nameExpr.Where, funcCall, procedureErrorMessage) } + // Unknown calls may be forward-declared procedures. Keep them resolvable, but + // retain the built-in spelling hint if late resolution still fails. + if common.FindBestSuggestion(nameExpr.Name) != "" { + funcCall := &procedures.Call{Where: nameExpr.Where, Name: nameExpr.Name, Arguments: arguments} + p.aggregator.EnqueueSymbol(nameExpr.Where, funcCall, "No function named "+nameExpr.Name+"()") + return funcCall + } + // Unknown — fill in a template that may be resolved later (forward-declared procedure). + funcCall := &procedures.Call{Where: nameExpr.Where, Name: nameExpr.Name, Arguments: arguments} + p.aggregator.EnqueueSymbol(nameExpr.Where, funcCall, procedureErrorMessage) return funcCall } return value } -func (p *LangParser) computeExpr() *variables.VarResult { - var varNames []string - var varValues []ast.Expr - p.expect(l.OpenCurve) - - // a result local var - for p.notEOF() && !p.isNext(l.CloseCurve) { - name := p.name() - p.expect(l.Assign) - value := p.parse() - - varNames = append(varNames, name) - varValues = append(varValues, value) - - if !p.consume(l.Comma) { - break - } - } - p.expect(l.CloseCurve) - p.expect(l.RightArrow) - return &variables.VarResult{Names: varNames, Values: varValues, Result: p.parse()} -} - func (p *LangParser) dictionary() *fundamentals.Dictionary { var elements []ast.Expr if !p.consume(l.CloseCurly) { @@ -773,7 +926,7 @@ func (p *LangParser) value(t *l.Token) ast.Expr { return &fundamentals.Component{Name: *t.Content, Type: compType} } // May not be variable reference always. It could be a func or a method call. - signatures, found := p.ScopeCursor.ResolveVariable(*t.Content) + signatures, found := p.ScopeCursor.ReferVariable(*t.Content) get := &variables.Get{Where: t, Global: false, Name: *t.Content, ValueSignature: signatures} if !found { p.aggregator.EnqueueSymbol(t, get, "Cannot find symbol '"+*t.Content+"'") @@ -783,7 +936,7 @@ func (p *LangParser) value(t *l.Token) ast.Expr { p.expect(l.Dot) nameToken := p.expect(l.Name) name := *nameToken.Content - signatures, found := p.ScopeCursor.ResolveVariable(name) + signatures, found := p.ScopeCursor.ReferGlobalVariable(name) get := &variables.Get{Where: t, Global: true, Name: name, ValueSignature: signatures} if !found { p.aggregator.EnqueueSymbol(nameToken, get, "Cannot find symbol '"+*nameToken.Content+"'") @@ -869,14 +1022,6 @@ func (p *LangParser) next() *l.Token { return token } -func (p *LangParser) createCheckpoint() { - p.currCheckpoint = p.currIndex -} - -func (p *LangParser) backToPast() { - p.currIndex = p.currCheckpoint -} - func (p *LangParser) back() { p.currIndex-- } @@ -892,3 +1037,14 @@ func (p *LangParser) notEOF() bool { func (p *LangParser) isEOF() bool { return p.currIndex >= p.tokenSize } + +// isOnNewLine reports whether the current (peeked) token is on a different +// line than the last consumed token. Used to prevent cross-line postfix +// continuation — if '[' or '(' appears on a new line it is a new statement, +// not a continuation of the previous expression (mirrors Kotlin's rule). +func (p *LangParser) isOnNewLine() bool { + if p.currIndex == 0 { + return false + } + return p.peek().Column > p.Tokens[p.currIndex-1].Column +} diff --git a/lang/code/parsers/mistparser/scope.go b/lang/code/parsers/mistparser/scope.go index 44447931..f4e3a881 100644 --- a/lang/code/parsers/mistparser/scope.go +++ b/lang/code/parsers/mistparser/scope.go @@ -1,28 +1,79 @@ package mistparser -import "Falcon/code/ast" +import ( + "Falcon/code/ast" +) + +type VarEntry struct { + Signatures []ast.Signature + Count int +} type Scope struct { Type ScopeType Parent *Scope - Variables map[string][]ast.Signature + Variables map[string]*VarEntry } -func (s *Scope) DefineVariable(name string, signature []ast.Signature) { - s.Variables[name] = signature +func (s *Scope) DefineVariable(name string, signatures []ast.Signature) { + s.Variables[name] = &VarEntry{Signatures: signatures, Count: 0} } -func (s *Scope) ResolveVariable(name string) ([]ast.Signature, bool) { - signature, ok := s.Variables[name] +func (s *Scope) ReferVariable(name string) ([]ast.Signature, bool) { + variable, ok := s.Variables[name] if ok { - return signature, true + variable.Count += 1 + return variable.Signatures, true } if s.Parent != nil { - return s.Parent.ResolveVariable(name) + return s.Parent.ReferVariable(name) } return make([]ast.Signature, 0), false } +func (s *Scope) ReferGlobalVariable(name string) ([]ast.Signature, bool) { + variable, ok := s.Variables[name] + if ok { + variable.Count += 1 + return variable.Signatures, true + } + return make([]ast.Signature, 0), false +} + +func (s *Scope) GetVariableReferCount(name string) int { + variable, ok := s.Variables[name] + if ok { + return variable.Count + } + return -1 +} + +func (s *Scope) InLoop() bool { + var currScope = s + for { + if currScope.Type == ScopeLoop { + return true + } + currScope = currScope.Parent + if currScope == nil { + return false + } + } +} + +func (s *Scope) GetLoopScope() *Scope { + var currScope = s + for { + if currScope.Type == ScopeLoop { + return currScope + } + currScope = currScope.Parent + if currScope == nil { + return nil + } + } +} + func (s *Scope) IsRoot() bool { return s.Parent == nil } diff --git a/lang/code/parsers/mistparser/scope_cursor.go b/lang/code/parsers/mistparser/scope_cursor.go index 1b3baf42..7d2c37ae 100644 --- a/lang/code/parsers/mistparser/scope_cursor.go +++ b/lang/code/parsers/mistparser/scope_cursor.go @@ -27,13 +27,13 @@ type ScopeCursor struct { } func MakeScopeCursor() *ScopeCursor { - headScope := &Scope{Type: ScopeRoot, Parent: nil, Variables: map[string][]ast.Signature{}} + headScope := &Scope{Type: ScopeRoot, Parent: nil, Variables: map[string]*VarEntry{}} return &ScopeCursor{allScopes: []*Scope{headScope}, headScope: headScope, currScope: headScope} } func (s *ScopeCursor) Enter(where *lex.Token, t ScopeType) { s.checkScope(where, t) - newScope := &Scope{Type: t, Parent: s.currScope, Variables: map[string][]ast.Signature{}} + newScope := &Scope{Type: t, Parent: s.currScope, Variables: map[string]*VarEntry{}} s.allScopes = append(s.allScopes, newScope) s.currScope = newScope } @@ -68,8 +68,16 @@ func (s *ScopeCursor) DefineVariable(name string, signature []ast.Signature) { s.currScope.DefineVariable(name, signature) } -func (s *ScopeCursor) ResolveVariable(name string) ([]ast.Signature, bool) { - return s.currScope.ResolveVariable(name) +func (s *ScopeCursor) ReferVariable(name string) ([]ast.Signature, bool) { + return s.currScope.ReferVariable(name) +} + +func (s *ScopeCursor) ReferGlobalVariable(name string) ([]ast.Signature, bool) { + return s.headScope.ReferGlobalVariable(name) +} + +func (s *ScopeCursor) GetVariableReferCount(name string) int { + return s.currScope.GetVariableReferCount(name) } func (s *ScopeCursor) In(t ScopeType) bool { diff --git a/lang/code/parsers/mistparser/source_patch.go b/lang/code/parsers/mistparser/source_patch.go new file mode 100644 index 00000000..6191048d --- /dev/null +++ b/lang/code/parsers/mistparser/source_patch.go @@ -0,0 +1,54 @@ +package mistparser + +import ( + "sort" + "strings" +) + +// SourcePatch describes a single in-place text replacement within a source line. +// Start and End are 0-indexed positions within the line (End is exclusive). +// Line is the 1-indexed line number (token.Column). +type SourcePatch struct { + Line int + Start int + End int + Text string +} + +// ApplyPatches applies all patches to the original source string and returns +// the modified source with the same line structure. Patches on the same line +// are applied right-to-left so earlier positions are not invalidated. +func ApplyPatches(source string, patches []SourcePatch) string { + if len(patches) == 0 { + return source + } + + lines := strings.Split(source, "\n") + + byLine := make(map[int][]SourcePatch) + for _, p := range patches { + byLine[p.Line] = append(byLine[p.Line], p) + } + + for lineNum, linePatches := range byLine { + if lineNum < 1 || lineNum > len(lines) { + continue + } + line := lines[lineNum-1] + + sort.Slice(linePatches, func(i, j int) bool { + return linePatches[i].Start > linePatches[j].Start + }) + + for _, p := range linePatches { + if p.Start < 0 || p.End > len(line) || p.Start > p.End { + continue + } + line = line[:p.Start] + p.Text + line[p.End:] + } + + lines[lineNum-1] = line + } + + return strings.Join(lines, "\n") +} diff --git a/lang/code/parsers/mistparser/yield_parser.go b/lang/code/parsers/mistparser/yield_parser.go new file mode 100644 index 00000000..497bd2ac --- /dev/null +++ b/lang/code/parsers/mistparser/yield_parser.go @@ -0,0 +1,343 @@ +package mistparser + +import ( + "Falcon/code/ast" + "Falcon/code/ast/control" + "Falcon/code/ast/fundamentals" + "Falcon/code/ast/list" + "Falcon/code/ast/variables" + l "Falcon/code/lex" +) + +type path struct { + frames []Frame + yield *fundamentals.Yield +} + +func makePath(frames []Frame, yield *fundamentals.Yield) path { + p := path{} + p.frames = append(p.frames, frames...) + p.yield = yield + return p +} + +func (p *path) hasLoopInPath() bool { + for _, ft := range p.frames { + if ft.FrameType == FrameTypeLoop { + return true + } + } + return false +} + +type YieldParser struct { + Exprs []ast.Expr + + paths []path + pathIndex int + + localResultDeclared bool // if `_result = [true, false]` has been added +} + +func (y *YieldParser) ParseYield() []ast.Expr { + y.pathIndex = 0 + + y.mapRouteToYields(y.Exprs, []Frame{}) + //for _, path := range y.paths { + // for i, frameType := range path { + // print(frameType.String()) + // if i != len(path)-1 { + // print(" --> ") + // } + // println() + // } + //} + return y.edits(y.Exprs) +} + +func (y *YieldParser) edits(exprs []ast.Expr) []ast.Expr { + var newExprs []ast.Expr +outerLoop: + for k, expr := range exprs { + switch e := expr.(type) { + case *control.If: + allBodiesYield := true + for j := range e.Bodies { + currPath := y.nextPath() + var lastPath *path = nil + if j > 0 { + lastPath = &y.paths[y.pathIndex-2] + } + if currPath.yield == nil { + allBodiesYield = false + } + // a loop makes for a potential yield, not confirmed, so debranch If + if currPath.hasLoopInPath() && currPath.yield != nil { + currPath.yield.UseTransformed = true + requiresDeclaration := !y.localResultDeclared + if requiresDeclaration { + // this ensures the child edit() calls do not declare it themselves + y.localResultDeclared = true + } + var addedExprs []ast.Expr + if j+1 < len(e.Bodies) { + // decompose if branch + nextIf := e.Decompose(j + 1) + // wrap rest of the code in yield var check + nextIf.ElseBody = append(nextIf.ElseBody, y.yieldResultQuery(y.edits(exprs[k+1:]))) + addedExprs = append(addedExprs, y.addLoopBreaking(currPath, e)) + addedExprs = append(addedExprs, nextIf) + } else { + // already decomposed position, simply wrap rest of the code in yield var check + addedExprs = append(addedExprs, y.addLoopBreaking(currPath, e)) + addedExprs = append(addedExprs, y.yieldResultQuery(y.edits(exprs[k+1:]))) + } + if !requiresDeclaration { + newExprs = append(newExprs, addedExprs...) + } else { + newExprs = append(newExprs, y.declareLocalResult(addedExprs)) + } + break outerLoop + } + if lastPath != nil { + // last path didn't have yield, but this one does, so debranchIf + if lastPath.yield == nil && currPath.yield != nil { + newExprs = y.wrapInResultCheck(currPath.yield, exprs[k+1:], newExprs, e) + break outerLoop + } else if lastPath.yield != nil && currPath.yield == nil { + // OR last path had yield, but this one doesn't, so debranch to else + nextIf := e.Decompose(j) + e.ElseBody = []ast.Expr{nextIf} + e.ElseBody = append(e.ElseBody, exprs[k+1:]...) + y.pathIndex-- + e.ElseBody = y.edits(e.ElseBody) + newExprs = append(newExprs, e) + break outerLoop + } + } + if j+1 == len(e.Bodies) && currPath.yield == nil { + newExprs = append(newExprs, e) + continue outerLoop + } + } + // set rest of the body as else branch + if e.ElseBody == nil { + e.ElseBody = y.edits(exprs[k+1:]) + newExprs = append(newExprs, e) + break outerLoop + } + // else body exists + elsePath := y.nextPath() + // append rest of the body to else branch + if elsePath.yield == nil { + e.ElseBody = append(e.ElseBody, y.edits(exprs[k+1:])...) + newExprs = append(newExprs, e) + break outerLoop + } else if len(e.Bodies) == 1 && !allBodiesYield { + // only else has a yield, inverse it + thenBranch := e.Bodies[0] + e.Bodies[0] = e.ElseBody + // append rest of the body to else branch + e.ElseBody = append(thenBranch, y.edits(exprs[k+1:])...) + // invert the condition logic + switch c := e.Conditions[0].(type) { + case *fundamentals.Not: + e.Conditions[0] = c.Expr + break + case *fundamentals.Boolean: + e.Conditions[0] = &fundamentals.Boolean{Value: !c.Value} + break + default: + e.Conditions[0] = &fundamentals.Not{Expr: c} + } + newExprs = append(newExprs, e) + break outerLoop + } else { + // wrap rest of the body in result check + newExprs = y.wrapInResultCheck(elsePath.yield, exprs[k+1:], newExprs, e) + break outerLoop + } + case *control.For, *control.While, *control.Each, *control.EachPair: + // wrap rest of the body in a check + p := y.nextPath() + if p.yield != nil { + newExprs = y.wrapInResultCheck(p.yield, exprs[k+1:], newExprs, y.addLoopBreaking(p, e)) + break outerLoop + } + newExprs = append(newExprs, expr) + case *variables.Var: + println("yeahhhhhhhhhh") + e.Body = y.edits(e.Body) + newExprs = append(newExprs, e) + break outerLoop + default: + newExprs = append(newExprs, expr) + } + } + return newExprs +} + +func (y *YieldParser) addLoopBreaking(p path, currFor ast.Expr) ast.Expr { + totalLoops := 0 + currIndexMatch := 0 + for k, frame := range p.frames { + if frame.FrameType == FrameTypeLoop { + totalLoops++ + } + if frame.Expr == currFor { + currIndexMatch = k + } + } + loopIndex := 0 + // `if (!_result[1]) break` + conditionalBreaking := &control.If{ + Conditions: []ast.Expr{&fundamentals.Not{Expr: y.localResultQuery("1")}}, + Bodies: [][]ast.Expr{{&control.Break{}}}, + } + for _, frame := range p.frames { + if frame.FrameType != FrameTypeLoop { + continue + } + // add breaking for all loops except the most inner one (has a break) + if loopIndex != totalLoops-1 { + switch a := frame.Expr.(type) { + case *control.For: + a.Body = append(a.Body, conditionalBreaking) + break + case *control.While: + a.Body = append(a.Body, conditionalBreaking) + break + case *control.Each: + a.Body = append(a.Body, conditionalBreaking) + break + case *control.EachPair: + a.Body = append(a.Body, conditionalBreaking) + break + } + loopIndex++ + } + } + return p.frames[currIndexMatch].Expr +} + +func (y *YieldParser) wrapInResultCheck(yield *fundamentals.Yield, restOfTheBody []ast.Expr, newExprs []ast.Expr, currExpr ast.Expr) []ast.Expr { + yield.UseTransformed = true + yieldQuery := y.yieldResultQuery(y.edits(restOfTheBody)) + if y.localResultDeclared { + newExprs = append(newExprs, currExpr) + newExprs = append(newExprs, yieldQuery) + } else { + newExprs = append(newExprs, y.declareLocalResult([]ast.Expr{currExpr, yieldQuery})) + } + return newExprs +} + +func (y *YieldParser) yieldResultQuery(restOfTheBody []ast.Expr) ast.Expr { + // `if (_result[1]) { ... } else _result[2]` + return &control.If{ + Conditions: []ast.Expr{y.localResultQuery("1")}, + Bodies: [][]ast.Expr{restOfTheBody}, + ElseBody: []ast.Expr{y.localResultQuery("2")}, + } +} + +func (y *YieldParser) localResultQuery(index string) *list.Get { + // `_result[n]` + return &list.Get{ + Where: l.MakeFakeToken(l.OpenSquare), + List: &variables.Get{ + Where: l.MakeFakeToken(l.Name), + Global: false, + Name: "_result", + ValueSignature: []ast.Signature{ast.SignList}, + }, + Index: &fundamentals.Number{Content: index}, + } +} + +func (y *YieldParser) declareLocalResult(restOfTheBody []ast.Expr) ast.Expr { + y.localResultDeclared = true + // `_result = [true, false]`, + // where the first var indicates if unset, second var holds the value + return &variables.Var{ + Names: []string{"_result"}, + Values: []ast.Expr{&fundamentals.List{ + Elements: []ast.Expr{&fundamentals.Boolean{Value: true}, &fundamentals.Boolean{Value: false}}, + }}, + Body: restOfTheBody, + } +} + +func (y *YieldParser) mapRouteToYields(traverseExprs []ast.Expr, frames []Frame) { + if len(traverseExprs) == 0 { + return + } + // check if the last expression is yield + switch yield := traverseExprs[len(traverseExprs)-1].(type) { + case *fundamentals.Yield: + y.paths = append(y.paths, makePath(frames, yield)) + return + } + // or else the last second expression (in case of loop yield) + if len(traverseExprs) > 1 { + switch yield := traverseExprs[len(traverseExprs)-2].(type) { + case *fundamentals.Yield: + y.paths = append(y.paths, makePath(frames, yield)) + return + } + } + handled := false + for _, expr := range traverseExprs { + switch e := expr.(type) { + case *control.If: + { + handled = true + for _, body := range e.Bodies { + y.mapRouteToYields(body, AppendFrame(frames, FrameTypeIf, e)) + } + y.mapRouteToYields(e.ElseBody, AppendFrame(frames, FrameTypeIf, e)) + continue + } + case *control.For: + handled = true + y.mapRouteToYields(e.Body, AppendFrame(frames, FrameTypeLoop, e)) + continue + case *control.While: + handled = true + y.mapRouteToYields(e.Body, AppendFrame(frames, FrameTypeLoop, e)) + continue + case *control.Each: + handled = true + y.mapRouteToYields(e.Body, AppendFrame(frames, FrameTypeLoop, e)) + continue + case *control.EachPair: + handled = true + y.mapRouteToYields(e.Body, AppendFrame(frames, FrameTypeLoop, e)) + continue + case *fundamentals.SmartBody: + handled = true + y.mapRouteToYields(e.Body, AppendFrame(frames, FrameTypeSmartBody, e)) + continue + case *variables.VarResult: + handled = true + y.mapRouteToYields([]ast.Expr{e.Result}, AppendFrame(frames, FrameTypeVar, e)) + continue + case *variables.Var: + handled = true + y.mapRouteToYields(e.Body, AppendFrame(frames, FrameTypeVar, e)) + continue + } + } + if !handled { + y.paths = append(y.paths, makePath(frames, nil)) + } +} + +func (y *YieldParser) nextPath() path { + if y.pathIndex >= len(y.paths) { + panic("yield parser: no more paths available (index out of range)") + } + p := y.paths[y.pathIndex] + y.pathIndex += 1 + return p +} diff --git a/lang/code/runtime/csv_helper.go b/lang/code/runtime/csv_helper.go new file mode 100644 index 00000000..ccb7af53 --- /dev/null +++ b/lang/code/runtime/csv_helper.go @@ -0,0 +1,101 @@ +package runtime + +// csv.go provides minimal CSV parsing and formatting without encoding/csv. +// Supports quoted fields, escaped double-quotes (""), and multi-line tables. + +import "strings" + +// parseCSVRow parses a single CSV line into fields. +// Handles quoted fields and "" escape sequences. +func parseCSVRow(s string) []string { + var fields []string + var field strings.Builder + inQuote := false + + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case inQuote && c == '"': + // peek ahead for escaped quote + if i+1 < len(s) && s[i+1] == '"' { + field.WriteByte('"') + i++ + } else { + inQuote = false + } + case !inQuote && c == '"': + inQuote = true + case !inQuote && c == ',': + fields = append(fields, field.String()) + field.Reset() + case c == '\r': + // skip CR in CRLF sequences + default: + field.WriteByte(c) + } + } + fields = append(fields, field.String()) + return fields +} + +// parseCSVTable parses a multi-row CSV string, correctly handling quoted fields +// that span multiple lines (embedded newlines inside quoted fields are preserved). +func parseCSVTable(s string) [][]string { + var rows [][]string + var fields []string + var field strings.Builder + inQuote := false + + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case inQuote && c == '"': + if i+1 < len(s) && s[i+1] == '"' { + field.WriteByte('"') + i++ // skip escaped quote + } else { + inQuote = false + } + case !inQuote && c == '"': + inQuote = true + case !inQuote && c == ',': + fields = append(fields, field.String()) + field.Reset() + case !inQuote && c == '\n': + // end of row — skip blank lines + if field.Len() > 0 || len(fields) > 0 { + fields = append(fields, field.String()) + rows = append(rows, fields) + fields = nil + field.Reset() + } + case c == '\r': + // skip CR in CRLF sequences + default: + field.WriteByte(c) + } + } + // flush final row if any content remains + if field.Len() > 0 || len(fields) > 0 { + fields = append(fields, field.String()) + rows = append(rows, fields) + } + return rows +} + +// formatCSVField quotes a field if it contains a comma, double-quote, or newline. +func formatCSVField(s string) string { + if strings.ContainsAny(s, ",\"\n\r") { + return "\"" + strings.ReplaceAll(s, "\"", "\"\"") + "\"" + } + return s +} + +// formatCSVRow formats a slice of fields as a single CSV line (no trailing newline). +func formatCSVRow(fields []string) string { + parts := make([]string, len(fields)) + for i, f := range fields { + parts[i] = formatCSVField(f) + } + return strings.Join(parts, ",") +} \ No newline at end of file diff --git a/lang/code/runtime/def_funcs.go b/lang/code/runtime/def_funcs.go new file mode 100644 index 00000000..afa2a392 --- /dev/null +++ b/lang/code/runtime/def_funcs.go @@ -0,0 +1,188 @@ +package runtime + +// deffuncs.go dispatches built-in (default) function calls. +// Numeric specials (base conversions, colour, random, statistics) live in numops.go. + +import ( + "Falcon/code/ast/common" + "math" +) + +func (i *Interpreter) evalFuncCall(e *common.FuncCall) Value { + savedToken := i.lastToken + savedHighlight := i.lastHighlight + args := make([]Value, len(e.Args)) + for k, a := range e.Args { + args[k] = i.Eval(a) + } + i.lastToken = savedToken + i.lastHighlight = savedHighlight + + switch e.Name { + // --- Output --- + case "println": + i.printLine(args[0].AsStr()) + return VoidVal() + + // --- Math single-arg --- + case "sqrt": + return NumVal(math.Sqrt(args[0].AsNum())) + case "abs": + return NumVal(math.Abs(args[0].AsNum())) + case "neg": + return NumVal(-args[0].AsNum()) + case "log": + return NumVal(math.Log(args[0].AsNum())) + case "exp": + return NumVal(math.Exp(args[0].AsNum())) + case "round": + return NumVal(math.Round(args[0].AsNum())) + case "ceil": + return NumVal(math.Ceil(args[0].AsNum())) + case "floor": + return NumVal(math.Floor(args[0].AsNum())) + case "sin": + return NumVal(math.Sin(args[0].AsNum())) + case "cos": + return NumVal(math.Cos(args[0].AsNum())) + case "tan": + return NumVal(math.Tan(args[0].AsNum())) + case "asin": + return NumVal(math.Asin(args[0].AsNum())) + case "acos": + return NumVal(math.Acos(args[0].AsNum())) + case "atan": + return NumVal(math.Atan(args[0].AsNum())) + case "degrees": + return NumVal(args[0].AsNum() * 180 / math.Pi) + case "radians": + return NumVal(args[0].AsNum() * math.Pi / 180) + + // --- Base conversions (numops.go) --- + case "decToHex": + return evalDecToHex(args) + case "decToBin": + return evalDecToBin(args) + case "hexToDec": + return evalHexToDec(args) + case "binToDec": + return evalBinToDec(args) + case "dec": + return evalDec(args) + case "bin": + return evalBin(args) + case "octal": + return evalOctal(args) + case "hexa": + return evalHexa(args) + + // --- Random (numops.go) --- + case "randInt": + return evalRandInt(args) + case "randFloat": + return evalRandFloat(args) + case "setRandSeed": + return evalSetRandSeed(args) + + // --- Variadic min/max --- + case "min": + result := args[0].AsNum() + for _, v := range args[1:] { + if v.AsNum() < result { + result = v.AsNum() + } + } + return NumVal(result) + case "max": + result := args[0].AsNum() + for _, v := range args[1:] { + if v.AsNum() > result { + result = v.AsNum() + } + } + return NumVal(result) + + // --- List statistics (numops.go) --- + case "avgOf": + return evalAvgOf(args) + case "maxOf": + return evalMaxOf(args) + case "minOf": + return evalMinOf(args) + case "geoMeanOf": + return evalGeoMeanOf(args) + case "stdDevOf": + return evalStdDevOf(args) + case "stdErrOf": + return evalStdErrOf(args) + case "modeOf": + return evalModeOf(args) + + // --- Secondary math ops (numops.go) --- + case "mod": + return evalMod(args) + case "rem": + return evalRem(args) + case "quot": + return evalQuot(args) + case "atan2": + return evalAtan2(args) + case "formatDecimal": + return evalFormatDecimal(args) + + // --- List helpers --- + case "copyList": + if args[0].Type() != List { + panic("copyList requires a list, got " + args[0].TypeName()) + } + return deepCopyValue(args[0]) + case "copyDict": + if args[0].Type() != Dict { + panic("copyDict requires a dict, got " + args[0].TypeName()) + } + return deepCopyValue(args[0]) + + // --- Color (numops.go) --- + case "makeColor": + return evalMakeColor(args) + case "splitColor": + return evalSplitColor(args) + + // --- App Inventor screen stubs --- + case "openScreen": + i.stub("openScreen(" + args[0].AsStr() + ")") + return VoidVal() + case "openScreenWithValue": + i.stub("openScreenWithValue(" + args[0].AsStr() + ", ...)") + return VoidVal() + case "closeScreen": + i.stub("closeScreen()") + return VoidVal() + case "closeScreenWithValue": + i.stub("closeScreenWithValue(...)") + return VoidVal() + case "closeApp": + i.stub("closeApp()") + return VoidVal() + case "getPlainStartText": + i.stub("getPlainStartText()") + return StrVal("") + case "closeScreenWithPlainText": + i.stub("closeScreenWithPlainText(...)") + return VoidVal() + case "getStartValue": + i.stub("getStartValue()") + return StrVal("") + + // --- Generic component function stubs --- + case "set", "call", "vcall": + i.stub("component function '" + e.Name + "'") + return VoidVal() + case "get", "every": + i.stub("component function '" + e.Name + "'") + return NullVal() + + default: + panic("unknown built-in function: " + e.Name) + } +} diff --git a/lang/code/runtime/env.go b/lang/code/runtime/env.go new file mode 100644 index 00000000..9839ff33 --- /dev/null +++ b/lang/code/runtime/env.go @@ -0,0 +1,67 @@ +package runtime + +// Env is a single scope frame in the scope chain. +type Env struct { + vars map[string]*Value + parent *Env +} + +func NewEnv(parent *Env) *Env { + return &Env{vars: make(map[string]*Value), parent: parent} +} + +// Define creates a new variable in the current scope. +func (e *Env) Define(name string, val Value) { + e.vars[name] = &val +} + +// Get walks up the scope chain to find a variable. +func (e *Env) Get(name string) Value { + if v, ok := e.vars[name]; ok { + return *v + } + if e.parent != nil { + return e.parent.Get(name) + } + panic("undefined variable: " + name) +} + +// Set walks up the scope chain to update an existing variable. +func (e *Env) Set(name string, val Value) { + if _, ok := e.vars[name]; ok { + e.vars[name] = &val + return + } + if e.parent != nil { + e.parent.Set(name, val) + return + } + panic("assignment to undefined variable: " + name) +} + +// SetGlobal writes directly to the root scope. +func (e *Env) SetGlobal(name string, val Value) { + root := e.root() + if _, ok := root.vars[name]; ok { + root.vars[name] = &val + return + } + panic("assignment to undefined global: " + name) +} + +// GetGlobal reads from the root scope. +func (e *Env) GetGlobal(name string) Value { + root := e.root() + if v, ok := root.vars[name]; ok { + return *v + } + panic("undefined global variable: " + name) +} + +func (e *Env) root() *Env { + cur := e + for cur.parent != nil { + cur = cur.parent + } + return cur +} diff --git a/lang/code/runtime/methods.go b/lang/code/runtime/methods.go new file mode 100644 index 00000000..bab37f14 --- /dev/null +++ b/lang/code/runtime/methods.go @@ -0,0 +1,539 @@ +package runtime + +import ( + astlist "Falcon/code/ast/list" + astmethod "Falcon/code/ast/method" + "strconv" + "strings" +) + +// methodCall dispatches method calls on text, list, and dict values. +func (i *Interpreter) methodCall(e *astmethod.Call) Value { + savedToken := i.lastToken + savedHighlight := i.lastHighlight + on := i.Eval(e.On) + args := i.evalExprs(e.Args) + i.lastToken = savedToken + i.lastHighlight = savedHighlight + + switch e.Name { + // ============ Text methods ============ + case "textLen": + return NumVal(float64(len([]rune(on.AsStr())))) + case "trim": + return StrVal(strings.TrimSpace(on.AsStr())) + case "uppercase": + return StrVal(strings.ToUpper(on.AsStr())) + case "lowercase": + return StrVal(strings.ToLower(on.AsStr())) + case "startsWith": + return BoolVal(strings.HasPrefix(on.AsStr(), args[0].AsStr())) + case "contains": + return BoolVal(strings.Contains(on.AsStr(), args[0].AsStr())) + case "containsAny": + haystack := on.AsStr() + for _, v := range *args[0].AsList() { + if strings.Contains(haystack, v.AsStr()) { + return BoolVal(true) + } + } + return BoolVal(false) + case "containsAll": + haystack := on.AsStr() + for _, v := range *args[0].AsList() { + if !strings.Contains(haystack, v.AsStr()) { + return BoolVal(false) + } + } + return BoolVal(true) + case "split": + sep := args[0].AsStr() + parts := strings.Split(on.AsStr(), sep) + elems := make([]Value, len(parts)) + for k, p := range parts { + elems[k] = StrVal(p) + } + return ListVal(elems) + case "splitAtFirst": + sep := args[0].AsStr() + idx := strings.Index(on.AsStr(), sep) + if idx == -1 { + return ListVal([]Value{StrVal(on.AsStr()), StrVal("")}) + } + return ListVal([]Value{StrVal(on.AsStr()[:idx]), StrVal(on.AsStr()[idx+len(sep):])}) + case "splitAtAny": + haystack := on.AsStr() + seps := *args[0].AsList() + var result []Value + for len(haystack) > 0 { + earliest := len(haystack) + earliestLen := 0 + for _, v := range seps { + sep := v.AsStr() + if sep == "" { + continue + } + idx := strings.Index(haystack, sep) + if idx != -1 && idx < earliest { + earliest = idx + earliestLen = len(sep) + } + } + if earliestLen == 0 { + break + } + result = append(result, StrVal(haystack[:earliest])) + haystack = haystack[earliest+earliestLen:] + } + result = append(result, StrVal(haystack)) + return ListVal(result) + case "splitAtFirstOfAny": + haystack := on.AsStr() + earliest := len(haystack) + sep := "" + for _, v := range *args[0].AsList() { + s := v.AsStr() + if s == "" { + continue + } + idx := strings.Index(haystack, s) + if idx != -1 && idx < earliest { + earliest = idx + sep = s + } + } + if sep == "" { + return ListVal([]Value{StrVal(haystack), StrVal("")}) + } + return ListVal([]Value{StrVal(haystack[:earliest]), StrVal(haystack[earliest+len(sep):])}) + case "splitAtSpaces": + parts := strings.Fields(on.AsStr()) + elems := make([]Value, len(parts)) + for k, p := range parts { + elems[k] = StrVal(p) + } + return ListVal(elems) + case "reverse": + runes := []rune(on.AsStr()) + for a, b := 0, len(runes)-1; a < b; a, b = a+1, b-1 { + runes[a], runes[b] = runes[b], runes[a] + } + return StrVal(string(runes)) + case "csvRowToList": + records := parseCSVRow(on.AsStr()) + elems := make([]Value, len(records)) + for k, rec := range records { + elems[k] = StrVal(rec) + } + return ListVal(elems) + case "csvTableToList": + allRecords := parseCSVTable(on.AsStr()) + rows := make([]Value, len(allRecords)) + for k, rec := range allRecords { + elems := make([]Value, len(rec)) + for j, cell := range rec { + elems[j] = StrVal(cell) + } + rows[k] = ListVal(elems) + } + return ListVal(rows) + case "segment": + s := []rune(on.AsStr()) + from := coerceIndex(args[0], "segment start") - 1 // 1-based + length := coerceIndex(args[1], "segment length") + if from < 0 { + from = 0 + } + if length < 0 { + panic("segment: length must be non-negative, got " + strconv.Itoa(length)) + } + if length == 0 { + return StrVal("") + } + end := from + length + if end > len(s) { + end = len(s) + } + if from > end { + return StrVal("") + } + return StrVal(string(s[from:end])) + case "replace": + return StrVal(strings.ReplaceAll(on.AsStr(), args[0].AsStr(), args[1].AsStr())) + case "replaceFrom": + result := on.AsStr() + d := args[0].AsDict() + for _, entry := range d.entries { + result = strings.ReplaceAll(result, entry.Key, entry.Val.AsStr()) + } + return StrVal(result) + case "replaceFromLongestFirst": + result := on.AsStr() + d := args[0].AsDict() + keys := make([]string, len(d.entries)) + for k, entry := range d.entries { + keys[k] = entry.Key + } + insertionSort(len(keys), func(a, b int) bool { return len(keys[a]) > len(keys[b]) }, func(a, b int) { keys[a], keys[b] = keys[b], keys[a] }) + for _, key := range keys { + if val, ok := d.Get(key); ok { + result = strings.ReplaceAll(result, key, val.AsStr()) + } + } + return StrVal(result) + + // ============ List methods ============ + case "listLen": + return NumVal(float64(len(*on.AsList()))) + case "add": + list := on.AsList() + *list = append(*list, args...) + return VoidVal() + case "containsItem": + for _, v := range *on.AsList() { + if DeepEqual(v, args[0]) { + return BoolVal(true) + } + } + return BoolVal(false) + case "indexOf": + for k, v := range *on.AsList() { + if DeepEqual(v, args[0]) { + return NumVal(float64(k + 1)) // 1-based + } + } + return NumVal(0) + case "insert": + list := on.AsList() + idx := coerceIndex(args[0], "insert index") - 1 // 1-based + if idx < 0 || idx > len(*list) { + panic("insert: index " + args[0].String() + " out of bounds (list length " + strconv.Itoa(len(*list)) + ")") + } + val := args[1] + *list = append(*list, NullVal()) + copy((*list)[idx+1:], (*list)[idx:]) + (*list)[idx] = val + return VoidVal() + case "remove": + list := on.AsList() + idx := coerceIndex(args[0], "remove index") - 1 // 1-based + if idx < 0 || idx >= len(*list) { + panic("remove: index " + args[0].String() + " out of bounds (list length " + strconv.Itoa(len(*list)) + ")") + } + *list = append((*list)[:idx], (*list)[idx+1:]...) + return VoidVal() + case "appendList": + list := on.AsList() + other := args[0].AsList() + *list = append(*list, *other...) + return VoidVal() + case "lookupInPairs": + keyVal := args[0] + notFound := args[1] + for _, v := range *on.AsList() { + pair := *v.AsList() + if len(pair) >= 2 && DeepEqual(pair[0], keyVal) { + return pair[1] + } + } + return notFound + case "join": + parts := make([]string, len(*on.AsList())) + for k, v := range *on.AsList() { + parts[k] = v.AsStr() + } + return StrVal(strings.Join(parts, args[0].AsStr())) + case "slice": + if len(args) < 2 { + panic(".slice() requires 2 arguments (start, end)") + } + list := *on.AsList() + idx1 := coerceIndex(args[0], "slice start") - 1 // 1-based + idx2 := coerceIndex(args[1], "slice end") // 1-based inclusive → exclusive + if idx1 < 0 { + idx1 = 0 + } + if idx2 > len(list) { + idx2 = len(list) + } + if idx1 > idx2 { + return ListVal(nil) + } + return ListVal(list[idx1:idx2]) + case "random": + list := *on.AsList() + if len(list) == 0 { + panic("random() requires a non-empty list") + } + return list[rngIntn(len(list))] + case "reverseList": + list := *on.AsList() + cp := make([]Value, len(list)) + for k, v := range list { + cp[len(list)-1-k] = v + } + return ListVal(cp) + case "toCsvRow": + list := *on.AsList() + fields := make([]string, len(list)) + for k, v := range list { + fields[k] = v.AsStr() + } + return StrVal(formatCSVRow(fields)) + case "toCsvTable": + var sb strings.Builder + for k, row := range *on.AsList() { + rowList := *row.AsList() + fields := make([]string, len(rowList)) + for j, v := range rowList { + fields[j] = v.AsStr() + } + if k > 0 { + sb.WriteByte('\n') + } + sb.WriteString(formatCSVRow(fields)) + } + return StrVal(sb.String()) + case "sort": + list := *on.AsList() + cp := make([]Value, len(list)) + copy(cp, list) + sortValues(cp) + return ListVal(cp) + case "allButFirst": + list := *on.AsList() + if len(list) == 0 { + return ListVal(nil) + } + return ListVal(list[1:]) + case "allButLast": + list := *on.AsList() + if len(list) == 0 { + return ListVal(nil) + } + return ListVal(list[:len(list)-1]) + case "pairsToDict": + d := NewOrderedDict() + for _, v := range *on.AsList() { + pair := *v.AsList() + if len(pair) >= 2 { + d.Set(pair[0].AsStr(), pair[1]) + } + } + return DictVal(d) + + // ============ Dict methods ============ + case "dictLen": + return NumVal(float64(on.AsDict().Len())) + case "get": + d := on.AsDict() + if val, ok := d.Get(args[0].AsStr()); ok { + return val + } + return args[1] // notFound + case "set": + on.AsDict().Set(args[0].AsStr(), args[1]) + return VoidVal() + case "delete": + on.AsDict().Delete(args[0].AsStr()) + return VoidVal() + case "getAtPath": + return dictGetAtPath(on.AsDict(), args[0].AsList(), args[1]) + case "setAtPath": + dictSetAtPath(on.AsDict(), args[0].AsList(), args[1]) + return VoidVal() + case "containsKey": + return BoolVal(on.AsDict().ContainsKey(args[0].AsStr())) + case "mergeInto": + dst := on.AsDict() + src := args[0].AsDict() + for _, entry := range src.entries { + dst.Set(entry.Key, entry.Val) + } + return DictVal(dst) + case "walkTree": + // Simplified: treat path as a list of keys + path := args[0].AsList() + var cur Value = on + for _, key := range *path { + if cur.Type() == Dict { + if v, ok := cur.AsDict().Get(key.AsStr()); ok { + cur = v + } else { + return NullVal() + } + } else { + return NullVal() + } + } + return cur + case "keys": + return ListVal(on.AsDict().Keys()) + case "values": + return ListVal(on.AsDict().Values()) + case "toPairs": + d := on.AsDict() + pairs := make([]Value, len(d.entries)) + for k, entry := range d.entries { + pairs[k] = ListVal([]Value{StrVal(entry.Key), entry.Val}) + } + return ListVal(pairs) + + default: + panic("unknown method ." + e.Name + "() on " + on.TypeName() + " value") + } +} + +func dictGetAtPath(d *OrderedDict, path *[]Value, notFound Value) Value { + var cur Value = DictVal(d) + for _, key := range *path { + if cur.Type() != Dict { + return notFound + } + v, ok := cur.AsDict().Get(key.AsStr()) + if !ok { + return notFound + } + cur = v + } + return cur +} + +func dictSetAtPath(d *OrderedDict, path *[]Value, val Value) { + if len(*path) == 0 { + return + } + keys := *path + var cur Value = DictVal(d) + for _, key := range keys[:len(keys)-1] { + if cur.Type() != Dict { + panic("setAtPath: path segment '" + key.AsStr() + "' exists but is not a dict") + } + v, ok := cur.AsDict().Get(key.AsStr()) + if !ok { + nd := NewOrderedDict() + cur.AsDict().Set(key.AsStr(), DictVal(nd)) + cur = DictVal(nd) + } else { + cur = v + } + } + if cur.Type() != Dict { + panic("setAtPath: cannot set at path, intermediate value is not a dict") + } + cur.AsDict().Set(keys[len(keys)-1].AsStr(), val) +} + +// evalTransformer handles list lambda operations. +func (i *Interpreter) evalTransformer(e *astlist.Transformer) Value { + list := i.Eval(e.List).AsList() + outerEnv := i.currEnv + + switch e.Name { + case "map": + varName := e.Names[0] + result := make([]Value, len(*list)) + for k, elem := range *list { + lambdaEnv := NewEnv(outerEnv) + lambdaEnv.Define(varName, elem) + result[k] = i.inEnv(lambdaEnv, func() Value { return i.Eval(e.Transformer) }) + } + return ListVal(result) + + case "filter": + varName := e.Names[0] + var result []Value + for _, elem := range *list { + lambdaEnv := NewEnv(outerEnv) + lambdaEnv.Define(varName, elem) + if i.inEnv(lambdaEnv, func() Value { return i.Eval(e.Transformer) }).AsBool() { + result = append(result, elem) + } + } + return ListVal(result) + + case "reduce": + varName := e.Names[0] // element + accName := e.Names[1] // accumulator + acc := i.Eval(e.Args[0]) + for _, elem := range *list { + lambdaEnv := NewEnv(outerEnv) + lambdaEnv.Define(varName, elem) + lambdaEnv.Define(accName, acc) + acc = i.inEnv(lambdaEnv, func() Value { return i.Eval(e.Transformer) }) + } + return acc + + case "sort": + varM := e.Names[0] + varN := e.Names[1] + cp := make([]Value, len(*list)) + copy(cp, *list) + insertionSort(len(cp), func(a, b int) bool { + lambdaEnv := NewEnv(outerEnv) + lambdaEnv.Define(varM, cp[a]) + lambdaEnv.Define(varN, cp[b]) + return i.inEnv(lambdaEnv, func() Value { return i.Eval(e.Transformer) }).AsBool() + }, func(a, b int) { cp[a], cp[b] = cp[b], cp[a] }) + return ListVal(cp) + + case "sortByKey": + varName := e.Names[0] + cp := make([]Value, len(*list)) + copy(cp, *list) + insertionSort(len(cp), func(a, b int) bool { + envA := NewEnv(outerEnv) + envA.Define(varName, cp[a]) + keyA := i.inEnv(envA, func() Value { return i.Eval(e.Transformer) }) + envB := NewEnv(outerEnv) + envB.Define(varName, cp[b]) + keyB := i.inEnv(envB, func() Value { return i.Eval(e.Transformer) }) + na, aOk := CoerceNum(keyA) + nb, bOk := CoerceNum(keyB) + if aOk && bOk { + return na < nb + } + return keyA.AsStr() < keyB.AsStr() + }, func(a, b int) { cp[a], cp[b] = cp[b], cp[a] }) + return ListVal(cp) + + case "min": + varM := e.Names[0] + varN := e.Names[1] + if len(*list) == 0 { + panic("min() transformer requires a non-empty list") + } + best := (*list)[0] + for _, elem := range (*list)[1:] { + lambdaEnv := NewEnv(outerEnv) + // Swap: pass elem as m and best as n so the comparator + // returns true when the candidate (elem) beats current best. + lambdaEnv.Define(varM, elem) + lambdaEnv.Define(varN, best) + if i.inEnv(lambdaEnv, func() Value { return i.Eval(e.Transformer) }).AsBool() { + best = elem + } + } + return best + + case "max": + varM := e.Names[0] + varN := e.Names[1] + if len(*list) == 0 { + panic("max() transformer requires a non-empty list") + } + best := (*list)[0] + for _, elem := range (*list)[1:] { + lambdaEnv := NewEnv(outerEnv) + lambdaEnv.Define(varM, best) + lambdaEnv.Define(varN, elem) + if i.inEnv(lambdaEnv, func() Value { return i.Eval(e.Transformer) }).AsBool() { + best = elem + } + } + return best + + default: + panic("unknown list transformer ." + e.Name + "() — valid transformers: map, filter, sort, reduce, min, max") + } +} diff --git a/lang/code/runtime/num_helper.go b/lang/code/runtime/num_helper.go new file mode 100644 index 00000000..56c6f9ba --- /dev/null +++ b/lang/code/runtime/num_helper.go @@ -0,0 +1,480 @@ +package runtime + +// numops.go holds numeric operations that are less common or specialised: +// base conversions, colour manipulation, random numbers, and list statistics. +// Keeping them here lets deffuncs.go stay focused on the core built-in dispatch. + +import ( + "math" + "strconv" + "strings" + "time" +) + +// --- Base-format predicates (used by the ? questionnaire operator) --- + +func isBase10(s string) bool { + s = strings.TrimSpace(s) + if len(s) == 0 { + return false + } + if s[0] == '-' || s[0] == '+' { + s = s[1:] + if len(s) == 0 { + return false + } + } + for _, c := range s { + if c < '0' || c > '9' { + return false + } + } + return true +} + +func isHex(s string) bool { + s = strings.TrimSpace(strings.ToLower(s)) + if len(s) == 0 { + return false + } + for _, c := range s { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { + return false + } + } + return true +} + +func isBinary(s string) bool { + s = strings.TrimSpace(s) + if len(s) == 0 { + return false + } + for _, c := range s { + if c != '0' && c != '1' { + return false + } + } + return true +} + +// --- Base conversions --- + +func evalDecToHex(args []Value) Value { + n := args[0].AsNum() + if n != math.Trunc(n) { + panic("decToHex requires an integer, got " + formatNum(n)) + } + return StrVal(strings.ToUpper(strconv.FormatInt(int64(n), 16))) +} + +func evalDecToBin(args []Value) Value { + n := args[0].AsNum() + if n != math.Trunc(n) { + panic("decToBin requires an integer, got " + formatNum(n)) + } + return StrVal(strconv.FormatInt(int64(n), 2)) +} + +func evalHexToDec(args []Value) Value { + s := strings.TrimPrefix(args[0].AsStr(), "0x") + s = strings.TrimPrefix(s, "0X") + n, err := strconv.ParseInt(s, 16, 64) + if err != nil { + panic("invalid hex string: " + args[0].AsStr()) + } + return NumVal(float64(n)) +} + +func evalBinToDec(args []Value) Value { + s := strings.TrimPrefix(args[0].AsStr(), "0b") + s = strings.TrimPrefix(s, "0B") + n, err := strconv.ParseInt(s, 2, 64) + if err != nil { + panic("invalid binary string: " + args[0].AsStr()) + } + return NumVal(float64(n)) +} + +func evalDec(args []Value) Value { + n, err := strconv.ParseInt(args[0].AsStr(), 10, 64) + if err != nil { + panic("invalid decimal string: " + args[0].AsStr()) + } + return NumVal(float64(n)) +} + +func evalBin(args []Value) Value { + s := strings.TrimPrefix(args[0].AsStr(), "0b") + s = strings.TrimPrefix(s, "0B") + n, err := strconv.ParseInt(s, 2, 64) + if err != nil { + panic("invalid binary string: " + args[0].AsStr()) + } + return NumVal(float64(n)) +} + +func evalOctal(args []Value) Value { + s := strings.TrimPrefix(args[0].AsStr(), "0o") + s = strings.TrimPrefix(s, "0O") + n, err := strconv.ParseInt(s, 8, 64) + if err != nil { + panic("invalid octal string: " + args[0].AsStr()) + } + return NumVal(float64(n)) +} + +func evalHexa(args []Value) Value { + s := strings.TrimPrefix(args[0].AsStr(), "0x") + s = strings.TrimPrefix(s, "0X") + n, err := strconv.ParseInt(s, 16, 64) + if err != nil { + panic("invalid hex string: " + args[0].AsStr()) + } + return NumVal(float64(n)) +} + +// --- Color manipulation --- + +// hexByte formats a single byte (0–255) as a two-character uppercase hex string. +func hexByte(n int) string { + s := strconv.FormatInt(int64(n&0xFF), 16) + if len(s) < 2 { + return "0" + strings.ToUpper(s) + } + return strings.ToUpper(s) +} + +func clampByte(n int) int { + if n < 0 { + return 0 + } + if n > 255 { + return 255 + } + return n +} + +func evalMakeColor(args []Value) Value { + list := args[0].AsList() + if len(*list) != 3 && len(*list) != 4 { + panic("makeColor requires a list of 3 or 4 elements [r, g, b] or [r, g, b, a]") + } + r := clampByte(int((*list)[0].AsNum())) + g := clampByte(int((*list)[1].AsNum())) + b := clampByte(int((*list)[2].AsNum())) + a := 255 + if len(*list) == 4 { + a = clampByte(int((*list)[3].AsNum())) + } + return ColorVal("#" + hexByte(a) + hexByte(r) + hexByte(g) + hexByte(b)) +} + +func evalSplitColor(args []Value) Value { + var a, r, g, b int64 + // A numeric argument is treated as a 32-bit ARGB integer. + if args[0].Type() == Number { + nf := args[0].AsNum() + if nf < 0 { + panic("splitColor: color number must be non-negative, got " + formatNum(nf)) + } + n := uint64(int64(nf)) + a = int64((n >> 24) & 0xFF) + r = int64((n >> 16) & 0xFF) + g = int64((n >> 8) & 0xFF) + b = int64(n & 0xFF) + return ListVal([]Value{NumVal(float64(a)), NumVal(float64(r)), NumVal(float64(g)), NumVal(float64(b))}) + } + raw := args[0].AsStr() + hex := strings.TrimPrefix(raw, "#") + for _, c := range hex { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + panic("splitColor: invalid color value: " + raw) + } + } + if len(hex) == 8 { + a, _ = strconv.ParseInt(hex[0:2], 16, 64) + r, _ = strconv.ParseInt(hex[2:4], 16, 64) + g, _ = strconv.ParseInt(hex[4:6], 16, 64) + b, _ = strconv.ParseInt(hex[6:8], 16, 64) + } else if len(hex) == 6 { + a = 255 + r, _ = strconv.ParseInt(hex[0:2], 16, 64) + g, _ = strconv.ParseInt(hex[2:4], 16, 64) + b, _ = strconv.ParseInt(hex[4:6], 16, 64) + } else { + panic("splitColor: color must be 6 or 8 hex digits, got: " + raw) + } + // Return [a, r, g, b] to match App Inventor channel order + return ListVal([]Value{NumVal(float64(a)), NumVal(float64(r)), NumVal(float64(g)), NumVal(float64(b))}) +} + +// --- Random --- + +// --- RNG (xorshift64) --- + +var rngState uint64 = 0x9e3779b97f4a7c15 // non-zero default seed; overwritten by init + +func init() { + s := uint64(time.Now().UnixNano()) + if s == 0 { + s = 0x9e3779b97f4a7c15 + } + rngState = s +} + +func rngNext() uint64 { + rngState ^= rngState << 13 + rngState ^= rngState >> 7 + rngState ^= rngState << 17 + return rngState +} + +func rngIntn(n int) int { + return int(rngNext()>>1) % n +} + +func evalRandInt(args []Value) Value { + lo, hi := int(args[0].AsNum()), int(args[1].AsNum()) + if lo > hi { + lo, hi = hi, lo + } + return NumVal(float64(lo + rngIntn(hi-lo+1))) +} + +func evalRandFloat(_ []Value) Value { + // Use the high 53 bits for a uniform [0,1) float64. + return NumVal(float64(rngNext()>>11) / (1 << 53)) +} + +func evalSetRandSeed(args []Value) Value { + s := args[0].AsNum() + if s < 0 { + panic("setRandSeed requires a non-negative seed, got " + formatNum(s)) + } + seed := uint64(s) + if seed == 0 { + seed = 1 // xorshift64 must not be zero + } + rngState = seed + return NullVal() +} + +// --- List statistics --- + +func evalAvgOf(args []Value) Value { + list := args[0].AsList() + if len(*list) == 0 { + panic("avgOf() requires a non-empty list") + } + sum := 0.0 + for _, v := range *list { + sum += v.AsNum() + } + return NumVal(sum / float64(len(*list))) +} + +func evalMaxOf(args []Value) Value { + list := args[0].AsList() + if len(*list) == 0 { + panic("maxOf() requires a non-empty list") + } + mx := (*list)[0].AsNum() + for _, v := range (*list)[1:] { + n := v.AsNum() + if math.IsNaN(n) { + mx = n + break + } + if n > mx { + mx = n + } + } + return NumVal(mx) +} + +func evalMinOf(args []Value) Value { + list := args[0].AsList() + if len(*list) == 0 { + panic("minOf() requires a non-empty list") + } + mn := (*list)[0].AsNum() + for _, v := range (*list)[1:] { + n := v.AsNum() + if math.IsNaN(n) { + mn = n + break + } + if n < mn { + mn = n + } + } + return NumVal(mn) +} + +func evalGeoMeanOf(args []Value) Value { + list := args[0].AsList() + if len(*list) == 0 { + panic("geoMeanOf() requires a non-empty list") + } + product := 1.0 + for _, v := range *list { + n := v.AsNum() + if n < 0 { + panic("geoMeanOf() requires non-negative values") + } + product *= n + } + return NumVal(math.Pow(product, 1.0/float64(len(*list)))) +} + +func evalStdDevOf(args []Value) Value { + return NumVal(listStdDev(args[0].AsList(), false)) +} + +func evalStdErrOf(args []Value) Value { + list := args[0].AsList() + n := float64(len(*list)) + if n == 0 { + panic("stdErrOf() requires a non-empty list") + } + if n <= 1 { + return NumVal(0) + } + return NumVal(listStdDev(list, false) / math.Sqrt(n)) +} + +func evalModeOf(args []Value) Value { + list := args[0].AsList() + if len(*list) == 0 { + panic("modeOf() requires a non-empty list") + } + type entry struct { + val Value + count int + } + var entries []*entry +outer: + for _, v := range *list { + for _, e := range entries { + if DeepEqual(v, e.val) { + e.count++ + continue outer + } + } + entries = append(entries, &entry{val: v, count: 1}) + } + maxCount := 0 + for _, e := range entries { + if e.count > maxCount { + maxCount = e.count + } + } + var modes []Value + for _, e := range entries { + if e.count == maxCount { + modes = append(modes, e.val) + } + } + return ListVal(modes) +} + +// --- Secondary math ops --- + +func evalFormatDecimal(args []Value) Value { + n, rawPlaces := args[0].AsNum(), args[1].AsNum() + places := int(rawPlaces) + if rawPlaces != float64(places) || places < 0 { + panic("formatDecimal: places must be a non-negative integer, got " + formatNum(rawPlaces)) + } + if places > 100 { + panic("formatDecimal: places cannot exceed 100") + } + return StrVal(strconv.FormatFloat(n, 'f', places, 64)) +} + +func evalMod(args []Value) Value { + a, b := args[0].AsNum(), args[1].AsNum() + if b == 0 { + panic("modulus by zero") + } + // floor modulo: result has the same sign as the divisor + r := math.Mod(a, b) + if r != 0 && (r < 0) != (b < 0) { + r += b + } + return NumVal(r) +} + +func evalRem(args []Value) Value { + a, b := args[0].AsNum(), args[1].AsNum() + if b == 0 { + panic("remainder by zero") + } + return NumVal(math.Mod(a, b)) +} + +func evalQuot(args []Value) Value { + a, b := args[0].AsNum(), args[1].AsNum() + if b == 0 { + panic("quotient by zero") + } + return NumVal(math.Trunc(a / b)) +} + +func evalAtan2(args []Value) Value { + return NumVal(math.Atan2(args[0].AsNum(), args[1].AsNum())) +} + +// --- Shared helpers --- + +func listStdDev(list *[]Value, population bool) float64 { + n := float64(len(*list)) + if n == 0 { + panic("stdDevOf() / stdErrOf() requires a non-empty list") + } + sum := 0.0 + for _, v := range *list { + sum += v.AsNum() + } + mean := sum / n + variance := 0.0 + for _, v := range *list { + d := v.AsNum() - mean + variance += d * d + } + denom := n + if !population && n > 1 { + denom = n - 1 + } + return math.Sqrt(variance / denom) +} + +func sign(f float64) float64 { + if f < 0 { + return -1 + } + return 1 +} + +// insertionSort performs a stable in-place sort using the provided less function. +func insertionSort(n int, less func(i, j int) bool, swap func(i, j int)) { + for i := 1; i < n; i++ { + for j := i; j > 0 && less(j, j-1); j-- { + swap(j, j-1) + } + } +} + +// sortValues sorts a slice of Values numerically if possible, else lexicographically. +func sortValues(list []Value) { + insertionSort(len(list), func(a, b int) bool { + va, vb := list[a], list[b] + na, aOk := CoerceNum(va) + nb, bOk := CoerceNum(vb) + if aOk && bOk { + return na < nb + } + return va.AsStr() < vb.AsStr() + }, func(a, b int) { list[a], list[b] = list[b], list[a] }) +} diff --git a/lang/code/runtime/print.go b/lang/code/runtime/print.go new file mode 100644 index 00000000..33b21e48 --- /dev/null +++ b/lang/code/runtime/print.go @@ -0,0 +1,24 @@ +package runtime + +import "os" + +// printLine writes s followed by a newline. If the interpreter has an output +// callback (e.g. WASM streaming), it is called instead of writing to stdout. +func (i *Interpreter) printLine(s string) { + if i.outputCallback != nil { + i.outputCallback(s) + } else { + os.Stdout.WriteString(s) + os.Stdout.WriteString("\n") + } +} + +// stub prints a warning that the named feature is not supported outside App Inventor. +func (i *Interpreter) stub(feature string) { + msg := "[stub] " + feature + " is not supported outside App Inventor" + if i.outputCallback != nil { + i.outputCallback(msg) + } else { + os.Stdout.WriteString(msg + "\n") + } +} diff --git a/lang/code/runtime/runtime.go b/lang/code/runtime/runtime.go new file mode 100644 index 00000000..632f4f67 --- /dev/null +++ b/lang/code/runtime/runtime.go @@ -0,0 +1,767 @@ +package runtime + +import ( + "Falcon/code/ast" + "Falcon/code/ast/common" + "Falcon/code/ast/components" + "Falcon/code/ast/control" + "Falcon/code/ast/fundamentals" + astlist "Falcon/code/ast/list" + astmethod "Falcon/code/ast/method" + "Falcon/code/ast/procedures" + "Falcon/code/ast/variables" + "Falcon/code/lex" + "math" + "strconv" + "strings" +) + +type Procedure struct { + params []string + voidBody []ast.Expr // for void procedures + retExpr ast.Expr // for returning procedures +} + +type stackFrame struct { + token *lex.Token + name string +} + +// Interpreter implements Visitor and holds the runtime state. +type Interpreter struct { + globalEnv *Env + currEnv *Env + procedures map[string]*Procedure + lastToken *lex.Token // last source token seen during Eval — used for runtime error reporting + lastHighlight int // override highlight width (0 = use token's own content length) + stackTrace []stackFrame // populated as panics propagate up through procedure calls + outputCallback func(string) // if non-nil, receives each printed line instead of stdout +} + +func NewInterpreter() *Interpreter { + env := NewEnv(nil) + return &Interpreter{ + globalEnv: env, + currEnv: env, + procedures: make(map[string]*Procedure), + } +} + +// NewInterpreterWithOutput creates an Interpreter that calls callback for every +// printed line instead of writing to stdout. Used by the WASM runtime to stream +// output back to JavaScript. +func NewInterpreterWithOutput(callback func(string)) *Interpreter { + env := NewEnv(nil) + return &Interpreter{ + globalEnv: env, + currEnv: env, + procedures: make(map[string]*Procedure), + outputCallback: callback, + } +} + +// inEnv temporarily switches currEnv to env, calls fn, then restores currEnv. +// The restore happens even if fn panics, so scoping is always correct. +func (i *Interpreter) inEnv(env *Env, fn func() Value) Value { + prev := i.currEnv + i.currEnv = env + defer func() { i.currEnv = prev }() + return fn() +} + +// RunGetLast runs the program like Run but returns the value of the last +// non-definition top-level expression. Returns NullVal if none. +func (i *Interpreter) RunGetLast(exprs []ast.Expr) Value { + // Register all procedures first so globals can call them. + for _, e := range exprs { + switch n := e.(type) { + case *procedures.VoidProcedure: + i.procedures[n.Name] = &Procedure{params: n.Parameters, voidBody: n.Body} + case *procedures.RetProcedure: + i.procedures[n.Name] = &Procedure{params: n.Parameters, retExpr: n.Result} + } + } + for _, e := range exprs { + if n, ok := e.(*variables.Global); ok { + val := i.Eval(n.Value) + i.globalEnv.Define(n.Name, val) + } + } + var last = NullVal() + for _, e := range exprs { + switch e.(type) { + case *procedures.VoidProcedure, *procedures.RetProcedure, *variables.Global: + default: + last = i.Eval(e) + } + } + return last +} + +// Run executes a top-level list of expressions (a full program). +func (i *Interpreter) Run(exprs []ast.Expr) { + // Register all procedures first so global initializers can call them. + for _, e := range exprs { + switch n := e.(type) { + case *procedures.VoidProcedure: + i.procedures[n.Name] = &Procedure{params: n.Parameters, voidBody: n.Body} + case *procedures.RetProcedure: + i.procedures[n.Name] = &Procedure{params: n.Parameters, retExpr: n.Result} + } + } + // Evaluate globals after all procedures are registered. + for _, e := range exprs { + if n, ok := e.(*variables.Global); ok { + val := i.Eval(n.Value) + i.globalEnv.Define(n.Name, val) + } + } + // Execute non-definition top-level statements. + for _, e := range exprs { + switch e.(type) { + case *procedures.VoidProcedure, *procedures.RetProcedure, *variables.Global: + default: + i.Eval(e) + } + } +} + +func (i *Interpreter) Eval(expr ast.Expr) Value { + switch e := expr.(type) { + // fundamentals + case *fundamentals.Boolean: + return BoolVal(e.Value) + case *fundamentals.Not: + return BoolVal(!i.Eval(e.Expr).AsBool()) + case *fundamentals.Number: + f, err := strconv.ParseFloat(e.Content, 64) + if err != nil { + panic("invalid number literal: " + e.Content) + } + return NumVal(f) + case *fundamentals.Text: + return StrVal(e.Content) + case *fundamentals.Color: + return ColorVal(e.Hex) + case *fundamentals.List: + return ListVal(i.evalExprs(e.Elements)) + case *fundamentals.Dictionary: + return i.dictionary(e) + case *fundamentals.Pair: + // Bare pair evaluates to a two-element list [key, value]. + return ListVal([]Value{i.Eval(e.Key), i.Eval(e.Value)}) + case *fundamentals.SmartBody: + return i.execBody(e.Body) + case *fundamentals.Yield: + return i.Eval(e.GetExpr()) + case *fundamentals.Component: + i.stub("component reference @" + e.Name + " (" + e.Type + ")") + return NullVal() + + // falcon specific features + case *common.EmptySocket: + return NumVal(0) + case *common.Transform: + return i.Eval(e.On) // for "obfuscate" unwrapping + + case *common.BinaryExpr: + i.lastToken = e.Where + return i.binary(e) + case *common.FuncCall: + i.lastToken = e.Where + return i.evalFuncCall(e) + case *common.Question: + i.lastToken = e.Where + return i.question(e) + case *astmethod.Call: + i.lastToken = e.Where + return i.methodCall(e) + + // Control blocks + case *control.If: + return i.ifSmt(e) + case *control.SimpleIf: + return i.simpleIfExpr(e) + case *control.For: + i.lastToken = e.Where + return i.forSmt(e) + case *control.While: + i.lastToken = e.Where + return i.whileSmt(e) + case *control.Each: + i.lastToken = e.Where + return i.eachSmt(e) + case *control.EachPair: + i.lastToken = e.Where + return i.eachPairSmt(e) + case *control.Break: + panic(BreakSignal{}) + case *control.Yield: + panic(YieldSignal{Val: i.Eval(e.Expr)}) + case *control.Do: + i.execBody(e.Body) + return i.Eval(e.Result) + + // Variable blocks + case *variables.Get: + i.lastToken = e.Where + if e.Global { + return i.currEnv.GetGlobal(e.Name) + } + return i.currEnv.Get(e.Name) + case *variables.Set: + val := i.Eval(e.Expr) + if e.Global { + i.currEnv.SetGlobal(e.Name, val) + } else { + i.currEnv.Set(e.Name, val) + } + return VoidVal() + case *variables.Global: + val := i.Eval(e.Value) + i.currEnv.root().Define(e.Name, val) + return VoidVal() + case *variables.Var: + return i.evalVar(e) + case *variables.VarResult: + return i.evalVarResult(e) + + // Procedure definitions + case *procedures.VoidProcedure: + i.procedures[e.Name] = &Procedure{params: e.Parameters, voidBody: e.Body} + return VoidVal() + case *procedures.RetProcedure: + i.procedures[e.Name] = &Procedure{params: e.Parameters, retExpr: e.Result} + return VoidVal() + case *procedures.Call: + i.lastToken = e.Where + return i.evalProcedureCall(e) + + // List manipulation + case *astlist.Get: + listVal := i.Eval(e.List) + i.lastToken = e.Where + hl := 1 + len(e.Index.String()) + 1 // covers full [index] + i.lastHighlight = hl + if listVal.Type() == String { + panic("expected a list value but got " + listVal.errorStr() + " — use .segment(start, length) to extract characters from text") + } + list := listVal.AsList() + i.lastHighlight = 0 + idx := coerceIndex(i.Eval(e.Index), "list index") + if idx < 1 || idx > len(*list) { + i.lastToken = e.Where + i.lastHighlight = hl + panic("list index " + strconv.Itoa(idx) + " out of bounds (len=" + strconv.Itoa(len(*list)) + ")") + } + return (*list)[idx-1] + case *astlist.Set: + listVal := i.Eval(e.List) + i.lastToken = e.Where + hl := 1 + len(e.Index.String()) + 1 // covers full [index] + i.lastHighlight = hl + list := listVal.AsList() + i.lastHighlight = 0 + idx := coerceIndex(i.Eval(e.Index), "list index") + val := i.Eval(e.Value) + if idx < 1 || idx > len(*list) { + i.lastToken = e.Where + i.lastHighlight = hl + panic("list index " + strconv.Itoa(idx) + " out of bounds (len=" + strconv.Itoa(len(*list)) + ")") + } + (*list)[idx-1] = val + return VoidVal() + case *astlist.Transformer: + i.lastToken = e.Where + return i.evalTransformer(e) + + // Component blocks + case *components.Event: + i.stub("event handler " + e.ComponentName + "." + e.Event) + return VoidVal() + case *components.GenericEvent: + i.stub("generic event handler " + e.ComponentType + "." + e.Event) + return VoidVal() + case *components.MethodCall: + i.stub("component method " + e.ComponentName + "." + e.Method + "(...)") + return VoidVal() + case *components.GenericMethodCall: + i.stub("generic component method " + e.ComponentType + "." + e.Method + "(...)") + return VoidVal() + case *components.PropertyGet: + i.stub("property get " + e.ComponentName + "." + e.Property) + return NullVal() // property reads are expressions + case *components.GenericPropertyGet: + i.stub("generic property get " + e.ComponentType + "." + e.Property) + return NullVal() // property reads are expressions + case *components.PropertySet: + i.stub("property set " + e.ComponentName + "." + e.Property) + return VoidVal() + case *components.GenericPropertySet: + i.stub("generic property set " + e.ComponentType + "." + e.Property) + return VoidVal() + case *components.EveryComponent: + i.stub("every(" + e.Type + ")") + return EmptyList() + + default: + panic("unknown AST node type") + } +} + +func (i *Interpreter) dictionary(e *fundamentals.Dictionary) Value { + d := NewOrderedDict() + for _, el := range e.Elements { + pair := i.Eval(el).AsList() + if len(*pair) < 2 { + panic("dictionary entry must be a pair (two-element list)") + } + d.Set((*pair)[0].AsStr(), (*pair)[1]) + } + return DictVal(d) +} + +func (i *Interpreter) binary(e *common.BinaryExpr) Value { + if len(e.Operands) == 0 { + panic("binary expression has no operands") + } + // Short-circuit logical operators + switch e.Operator { + case lex.LogicAnd: + for _, op := range e.Operands { + if !i.Eval(op).AsBool() { + return BoolVal(false) + } + } + return BoolVal(true) + case lex.LogicOr: + for _, op := range e.Operands { + if i.Eval(op).AsBool() { + return BoolVal(true) + } + } + return BoolVal(false) + } + + vals := i.evalExprs(e.Operands) + switch e.Operator { + case lex.Plus: + result := vals[0].AsNum() + for _, v := range vals[1:] { + result += v.AsNum() + } + return NumVal(result) + case lex.Dash: + return NumVal(vals[0].AsNum() - vals[1].AsNum()) + case lex.Times: + result := vals[0].AsNum() + for _, v := range vals[1:] { + result *= v.AsNum() + } + return NumVal(result) + case lex.Slash: + b := vals[1].AsNum() + if b == 0 { + panic("division by zero") + } + return NumVal(vals[0].AsNum() / b) + case lex.Remainder: + a, b := vals[0].AsNum(), vals[1].AsNum() + if b == 0 { + panic("division by zero (remainder)") + } + return NumVal(math.Mod(a, b)) + case lex.Power: + return NumVal(math.Pow(vals[0].AsNum(), vals[1].AsNum())) + + case lex.BitwiseAnd: + result := int64(vals[0].AsNum()) + for _, v := range vals[1:] { + result &= int64(v.AsNum()) + } + return NumVal(float64(result)) + case lex.BitwiseOr: + result := int64(vals[0].AsNum()) + for _, v := range vals[1:] { + result |= int64(v.AsNum()) + } + return NumVal(float64(result)) + case lex.BitwiseXor: + result := int64(vals[0].AsNum()) + for _, v := range vals[1:] { + result ^= int64(v.AsNum()) + } + return NumVal(float64(result)) + + // runs deep comparison for logical equality + case lex.Equals: + return BoolVal(DeepEqual(vals[0], vals[1])) + case lex.NotEquals: + return BoolVal(!DeepEqual(vals[0], vals[1])) + + case lex.LessThan: + return BoolVal(vals[0].AsNum() < vals[1].AsNum()) + case lex.LessThanEqual: + return BoolVal(vals[0].AsNum() <= vals[1].AsNum()) + case lex.GreatThan: + return BoolVal(vals[0].AsNum() > vals[1].AsNum()) + case lex.GreaterThanEqual: + return BoolVal(vals[0].AsNum() >= vals[1].AsNum()) + + case lex.TextEquals: + return BoolVal(vals[0].AsStr() == vals[1].AsStr()) + case lex.TextNotEquals: + return BoolVal(vals[0].AsStr() != vals[1].AsStr()) + case lex.TextLessThan: + return BoolVal(vals[0].AsStr() < vals[1].AsStr()) + case lex.TextGreaterThan: + return BoolVal(vals[0].AsStr() > vals[1].AsStr()) + + // text join operation + case lex.Underscore: + var sb strings.Builder + for _, v := range vals { + sb.WriteString(v.AsStr()) + } + return StrVal(sb.String()) + + default: + panic("unknown binary operator: " + strconv.Itoa(int(e.Operator))) + } +} + +func (i *Interpreter) question(e *common.Question) Value { + v := i.Eval(e.On) + switch e.Question { + case "number": + _, ok := CoerceNum(v) + return BoolVal(ok) + case "base10": + return BoolVal(isBase10(v.AsStr())) + case "hexa": + return BoolVal(isHex(v.AsStr())) + case "bin": + return BoolVal(isBinary(v.AsStr())) + case "text": + return BoolVal(v.Type() == String) + case "list": + return BoolVal(v.Type() == List) + case "dict": + return BoolVal(v.Type() == Dict) + case "emptyText": + return BoolVal(v.Type() == String && v.strVal == "") + case "emptyList": + return BoolVal(v.Type() == List && len(*v.listVal) == 0) + case "even": + n := v.AsNum() + if n != math.Trunc(n) { + panic("? even requires an integer value, got " + formatNum(n)) + } + if math.Abs(n) > 9007199254740992 { + panic("? even: number is too large to determine parity") + } + return BoolVal(int64(n)%2 == 0) + case "odd": + n := v.AsNum() + if n != math.Trunc(n) { + panic("? odd requires an integer value, got " + formatNum(n)) + } + if math.Abs(n) > 9007199254740992 { + panic("? odd: number is too large to determine parity") + } + return BoolVal(int64(n)%2 != 0) + default: + panic("unknown ? question: " + e.Question) + } +} + +func (i *Interpreter) ifSmt(e *control.If) Value { + for k, cond := range e.Conditions { + if i.Eval(cond).AsBool() { + return i.execBody(e.Bodies[k]) + } + } + if e.ElseBody != nil { + return i.execBody(e.ElseBody) + } + return VoidVal() +} + +func (i *Interpreter) simpleIfExpr(e *control.SimpleIf) Value { + if i.Eval(e.Condition()).AsBool() { + return i.execBody(e.Then()) + } + if els := e.Else(); len(els) > 0 { + return i.execBody(els) + } + return NullVal() +} + +func (i *Interpreter) forSmt(e *control.For) Value { + from := i.Eval(e.From).AsNum() + to := i.Eval(e.To).AsNum() + by := i.Eval(e.By).AsNum() + if by == 0 { + panic("for loop step cannot be 0") + } + loopEnv := NewEnv(i.currEnv) + func() { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(BreakSignal); !ok { + panic(r) + } + } + }() + i.inEnv(loopEnv, func() Value { + step := 0 + for { + cur := from + float64(step)*by + if (by > 0 && cur > to) || (by < 0 && cur < to) { + break + } + loopEnv.Define(e.IName, NumVal(cur)) + i.execBody(e.Body) + step++ + } + return VoidVal() + }) + }() + return VoidVal() +} + +func (i *Interpreter) whileSmt(e *control.While) Value { + func() { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(BreakSignal); !ok { + panic(r) + } + } + }() + for i.Eval(e.Condition).AsBool() { + i.execBody(e.Body) + } + }() + return VoidVal() +} + +func (i *Interpreter) eachSmt(e *control.Each) Value { + list := i.Eval(e.Iterable).AsList() + loopEnv := NewEnv(i.currEnv) + loopEnv.Define(e.IName, NullVal()) + func() { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(BreakSignal); !ok { + panic(r) + } + } + }() + i.inEnv(loopEnv, func() Value { + for _, elem := range *list { + loopEnv.Define(e.IName, elem) + i.execBody(e.Body) + } + return VoidVal() + }) + }() + return VoidVal() +} + +func (i *Interpreter) eachPairSmt(e *control.EachPair) Value { + d := i.Eval(e.Iterable).AsDict() + loopEnv := NewEnv(i.currEnv) + loopEnv.Define(e.KeyName, NullVal()) + loopEnv.Define(e.ValueName, NullVal()) + func() { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(BreakSignal); !ok { + panic(r) + } + } + }() + i.inEnv(loopEnv, func() Value { + for _, entry := range d.entries { + loopEnv.Define(e.KeyName, StrVal(entry.Key)) + loopEnv.Define(e.ValueName, entry.Val) + i.execBody(e.Body) + } + return VoidVal() + }) + }() + return VoidVal() +} + +// --- Variables --- + +func (i *Interpreter) evalVar(e *variables.Var) Value { + childEnv := NewEnv(i.currEnv) + for k, name := range e.Names { + val := i.Eval(e.Values[k]) // values evaluated in parent scope + childEnv.Define(name, val) + } + return i.inEnv(childEnv, func() Value { return i.execBody(e.Body) }) +} + +func (i *Interpreter) evalVarResult(e *variables.VarResult) Value { + childEnv := NewEnv(i.currEnv) + for k, name := range e.Names { + val := i.Eval(e.Values[k]) + childEnv.Define(name, val) + } + return i.inEnv(childEnv, func() Value { return i.Eval(e.Result) }) +} + +// --- Procedure calls --- + +func (i *Interpreter) evalProcedureCall(e *procedures.Call) Value { + proc, ok := i.procedures[e.Name] + if !ok { + panic("undefined procedure: " + e.Name) + } + // Evaluate arguments in the current (caller) env before switching scope. + savedToken := i.lastToken + savedHighlight := i.lastHighlight + argVals := i.evalExprs(e.Arguments) + i.lastToken = savedToken + i.lastHighlight = savedHighlight + if len(argVals) != len(proc.params) { + panic("procedure " + e.Name + " expects " + strconv.Itoa(len(proc.params)) + " argument(s) but got " + strconv.Itoa(len(argVals))) + } + callEnv := NewEnv(i.globalEnv) + for k, param := range proc.params { + callEnv.Define(param, argVals[k]) + } + + var result Value + func() { + defer func() { + if r := recover(); r != nil { + i.stackTrace = append(i.stackTrace, stackFrame{e.Where, e.Name}) + panic(r) + } + }() + result = i.inEnv(callEnv, func() Value { + if proc.retExpr != nil { + // a returning procedure + var res Value + func() { + defer func() { + if r := recover(); r != nil { + switch rs := r.(type) { + case ReturnSignal: + res = rs.Val + case YieldSignal: + res = rs.Val + default: + panic(r) + } + } + }() + res = i.Eval(proc.retExpr) + }() + return res + } + // a void procedure + func() { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(ReturnSignal); !ok { + panic(r) + } + } + }() + i.execBody(proc.voidBody) + }() + return VoidVal() + }) + }() + return result +} + +// evaluates all expressions and returns the last expr result +func (i *Interpreter) execBody(body []ast.Expr) Value { + if len(body) == 0 { + return NullVal() + } + var last Value + for _, expr := range body { + last = i.Eval(expr) + } + return last +} + +// FormatRuntimeError formats a recovered panic value as a runtime error message. +// If a source token was recorded, the message includes the source line and a caret. +func (i *Interpreter) FormatRuntimeError(r any) string { + // If the panic value is already a fully-formatted traceback (e.g. from + // Token.TypeError / Token.Error), return it unchanged. + msg := "runtime error" + switch v := r.(type) { + case string: + if strings.HasPrefix(v, "Traceback (most recent call last):") { + return v + } + msg = v + case error: + msg = v.Error() + } + + var sb strings.Builder + sb.WriteString("Traceback (most recent call last):\n") + + // stackTrace is stored innermost -> outermost; print outermost -> innermost + for j := len(i.stackTrace) - 1; j >= 0; j-- { + frame := i.stackTrace[j] + var funcName string + if j == len(i.stackTrace)-1 { + funcName = "" + } else { + funcName = i.stackTrace[j+1].name + } + sb.WriteString(i.formatTraceFrame(frame.token, funcName)) + } + + // The actual error location + if i.lastToken != nil && i.lastToken.Column >= 0 { + var funcName string + if len(i.stackTrace) > 0 { + funcName = i.stackTrace[0].name + } else { + funcName = "" + } + sb.WriteString(i.formatTraceFrame(i.lastToken, funcName, true)) + } + + sb.WriteString("RuntimeError: " + msg + "\n") + return sb.String() +} +func (i *Interpreter) formatTraceFrame(token *lex.Token, funcName string, isLast ...bool) string { + last := len(isLast) > 0 && isLast[0] + fileName := "" + if token.Context != nil { + fileName = token.Context.FileName + } + hlSize := 1 + if token.Content != nil { + hlSize = len(*token.Content) + } + if last && i.lastHighlight > 0 { + hlSize = i.lastHighlight + } + if token.Context != nil { + return token.Context.FormatTracebackFrame(fileName, token.Column, token.Row, hlSize, funcName) + } + return " File \"" + fileName + "\", line " + strconv.Itoa(token.Column) + ", in " + funcName + "\n" +} + +// evaluates all exprs in a given list +func (i *Interpreter) evalExprs(exprs []ast.Expr) []Value { + vals := make([]Value, len(exprs)) + for k, expr := range exprs { + vals[k] = i.Eval(expr) + } + return vals +} diff --git a/lang/code/runtime/signals.go b/lang/code/runtime/signals.go new file mode 100644 index 00000000..92a6feda --- /dev/null +++ b/lang/code/runtime/signals.go @@ -0,0 +1,15 @@ +package runtime + +// BreakSignal is panicked by a break statement and recovered by loops. +type BreakSignal struct{} + +// ReturnSignal is panicked by a returning procedure and recovered by the caller. +type ReturnSignal struct { + Val Value +} + +// YieldSignal is panicked by a yield statement and recovered by the enclosing +// function call, which returns the carried value to the caller. +type YieldSignal struct { + Val Value +} diff --git a/lang/code/runtime/value.go b/lang/code/runtime/value.go new file mode 100644 index 00000000..f64284e0 --- /dev/null +++ b/lang/code/runtime/value.go @@ -0,0 +1,460 @@ +package runtime + +import ( + "math" + "strconv" + "strings" +) + +type ValueType int + +const ( + Null ValueType = iota + Bool + Number + String + List + Dict + Color + NonConsumable // result of a statement; consuming it is a runtime error +) + +type Value struct { + vtype ValueType + boolVal bool + numVal float64 + strVal string + listVal *[]Value + dictVal *OrderedDict +} + +// --- Constructors --- + +func NullVal() Value { return Value{vtype: Null} } +func BoolVal(b bool) Value { return Value{vtype: Bool, boolVal: b} } +func NumVal(n float64) Value { return Value{vtype: Number, numVal: n} } +func StrVal(s string) Value { return Value{vtype: String, strVal: s} } +func ColorVal(hex string) Value { return Value{vtype: Color, strVal: hex} } +func VoidVal() Value { return Value{vtype: NonConsumable} } + +func ListVal(elems []Value) Value { + cp := make([]Value, len(elems)) + copy(cp, elems) + return Value{vtype: List, listVal: &cp} +} + +func EmptyList() Value { + elems := make([]Value, 0) + return Value{vtype: List, listVal: &elems} +} + +func DictVal(d *OrderedDict) Value { + return Value{vtype: Dict, dictVal: d} +} + +func EmptyDict() Value { + return Value{vtype: Dict, dictVal: NewOrderedDict()} +} + +// --- Accessors --- + +func (v Value) Type() ValueType { return v.vtype } + +// TypeName returns a human-readable type name for use in error messages. +func (v Value) TypeName() string { + switch v.vtype { + case Null: + return "null" + case Bool: + return "boolean" + case Number: + return "number" + case String: + return "text" + case List: + return "list" + case Dict: + return "dict" + case Color: + return "color" + default: + return "unknown" + } +} + +// errorStr returns a short description of the value for use in error messages. +func (v Value) errorStr() string { + switch v.vtype { + case Number: + return "number " + v.String() + case Bool: + return "boolean " + v.String() + case String: + s := v.strVal + if len(s) > 24 { + s = s[:24] + "..." + } + return "text \"" + s + "\"" + case List: + return "list (length " + strconv.Itoa(len(*v.listVal)) + ")" + case Dict: + return "dict (length " + strconv.Itoa(v.dictVal.Len()) + ")" + case Null: + return "null" + case Color: + return "color " + v.strVal + default: + return "unknown" + } +} + +func (v Value) AsBool() bool { + if v.vtype == NonConsumable { + panic("expected a boolean value but got a statement result (void)") + } + if v.vtype != Bool { + panic("expected a boolean value but got " + v.errorStr()) + } + return v.boolVal +} + +func (v Value) AsNum() float64 { + if v.vtype == NonConsumable { + panic("expected a number value but got a statement result (void)") + } + switch v.vtype { + case Number: + return v.numVal + case String: + if f, err := strconv.ParseFloat(strings.TrimSpace(v.strVal), 64); err == nil { + if math.IsNaN(f) || math.IsInf(f, 0) { + panic("cannot convert text to finite number: \"" + v.strVal + "\"") + } + return f + } + panic("cannot convert text to number: \"" + v.strVal + "\"") + default: + panic("expected a number value but got " + v.errorStr()) + } +} + +func (v Value) AsStr() string { + if v.vtype == NonConsumable { + panic("cannot consume a statement result as a string") + } + switch v.vtype { + case String: + return v.strVal + case Color: + return v.strVal + case Number: + return formatNum(v.numVal) + case Bool: + if v.boolVal { + return "true" + } + return "false" + case Null: + return "" + case List: + return v.String() + case Dict: + return v.String() + default: + return v.String() + } +} + +func (v Value) AsList() *[]Value { + if v.vtype == NonConsumable { + panic("expected a list value but got a statement result (void)") + } + if v.vtype != List { + panic("expected a list value but got " + v.errorStr()) + } + return v.listVal +} + +func (v Value) AsDict() *OrderedDict { + if v.vtype == NonConsumable { + panic("expected a dict value but got a statement result (void)") + } + if v.vtype != Dict { + panic("expected a dict value but got " + v.errorStr()) + } + return v.dictVal +} + +func CoerceNum(v Value) (float64, bool) { + if v.vtype == Number { + return v.numVal, true + } + if v.vtype == String { + if f, err := strconv.ParseFloat(strings.TrimSpace(v.strVal), 64); err == nil { + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return f, true + } + } + } + return 0, false +} + +func coerceIndex(v Value, context string) int { + n := v.AsNum() + if math.IsNaN(n) { + panic(context + " is NaN") + } + if math.IsInf(n, 0) { + panic(context + " is infinite") + } + if math.Trunc(n) != n { + panic(context + " must be a whole number, got " + v.String()) + } + return int(n) +} + +func (v Value) String() string { + return v.stringDepth(0) +} + +func (v Value) stringDepth(depth int) string { + if depth > 50 { + return "..." + } + switch v.vtype { + case Null: + return "null" + case Bool: + if v.boolVal { + return "true" + } + return "false" + case Number: + return formatNum(v.numVal) + case String: + return v.strVal + case Color: + return v.strVal + case List: + parts := make([]string, len(*v.listVal)) + for i, e := range *v.listVal { + parts[i] = e.stringDepth(depth + 1) + } + return "[" + strings.Join(parts, ", ") + "]" + case Dict: + return v.dictVal.stringDepth(depth) + case NonConsumable: + return "" + default: + return "" + } +} + +func formatNum(f float64) string { + if math.IsInf(f, 1) { + return "Infinity" + } + if math.IsInf(f, -1) { + return "-Infinity" + } + if math.IsNaN(f) { + return "NaN" + } + if f == math.Trunc(f) && math.Abs(f) < 1e15 { + return strconv.FormatInt(int64(f), 10) + } + return strconv.FormatFloat(f, 'f', -1, 64) +} + +// deepCopyValue returns a fully independent deep copy of v. +// Primitive values (null, bool, number, string, color) are immutable and returned as-is. +// Lists and dicts are recursively cloned. +func deepCopyValue(v Value) Value { + return deepCopyValueMemo(v, make(map[*[]Value]*[]Value), make(map[*OrderedDict]*OrderedDict)) +} + +func deepCopyValueMemo(v Value, lists map[*[]Value]*[]Value, dicts map[*OrderedDict]*OrderedDict) Value { + switch v.vtype { + case List: + if cp, ok := lists[v.listVal]; ok { + return Value{vtype: List, listVal: cp} + } + src := *v.listVal + cp := make([]Value, len(src)) + lists[v.listVal] = &cp + for i, elem := range src { + cp[i] = deepCopyValueMemo(elem, lists, dicts) + } + return Value{vtype: List, listVal: &cp} + case Dict: + if nd, ok := dicts[v.dictVal]; ok { + return DictVal(nd) + } + nd := NewOrderedDict() + dicts[v.dictVal] = nd + for _, entry := range v.dictVal.entries { + nd.Set(entry.Key, deepCopyValueMemo(entry.Val, lists, dicts)) + } + return DictVal(nd) + default: + return v + } +} + +// DeepEqual checks structural equality of two values. +func DeepEqual(a, b Value) bool { + return deepEqualMemo(a, b, make(map[equalPair]bool)) +} + +type equalPair struct { + aList *[]Value + bList *[]Value + aDict *OrderedDict + bDict *OrderedDict +} + +func deepEqualMemo(a, b Value, seen map[equalPair]bool) bool { + if a.vtype != b.vtype { + return false + } + switch a.vtype { + case Null: + return true + case Bool: + return a.boolVal == b.boolVal + case Number: + return a.numVal == b.numVal + case String, Color: + return a.strVal == b.strVal + case List: + if a.listVal == b.listVal { + return true // same pointer — same list (handles self-referential equality) + } + pair := equalPair{aList: a.listVal, bList: b.listVal} + if seen[pair] { + return true + } + seen[pair] = true + la, lb := *a.listVal, *b.listVal + if len(la) != len(lb) { + return false + } + for i := range la { + if !deepEqualMemo(la[i], lb[i], seen) { + return false + } + } + return true + case Dict: + da, db := a.dictVal, b.dictVal + if da == db { + return true // same pointer — same dict + } + pair := equalPair{aDict: da, bDict: db} + if seen[pair] { + return true + } + seen[pair] = true + if da.Len() != db.Len() { + return false + } + for _, e := range da.entries { + bv, ok := db.Get(e.Key) + if !ok || !deepEqualMemo(e.Val, bv, seen) { + return false + } + } + return true + case NonConsumable: + return true + } + return false +} + +// --- OrderedDict --- + +type DictEntry struct { + Key string + Val Value +} + +type OrderedDict struct { + entries []DictEntry +} + +func NewOrderedDict() *OrderedDict { + return &OrderedDict{} +} + +func (d *OrderedDict) Len() int { return len(d.entries) } + +func (d *OrderedDict) Get(key string) (Value, bool) { + for _, e := range d.entries { + if e.Key == key { + return e.Val, true + } + } + return NullVal(), false +} + +func (d *OrderedDict) Set(key string, val Value) { + for i, e := range d.entries { + if e.Key == key { + d.entries[i].Val = val + return + } + } + d.entries = append(d.entries, DictEntry{Key: key, Val: val}) +} + +func (d *OrderedDict) Delete(key string) { + for i, e := range d.entries { + if e.Key == key { + d.entries = append(d.entries[:i], d.entries[i+1:]...) + return + } + } +} + +func (d *OrderedDict) ContainsKey(key string) bool { + _, ok := d.Get(key) + return ok +} + +func (d *OrderedDict) Keys() []Value { + keys := make([]Value, len(d.entries)) + for i, e := range d.entries { + keys[i] = StrVal(e.Key) + } + return keys +} + +func (d *OrderedDict) Values() []Value { + vals := make([]Value, len(d.entries)) + for i, e := range d.entries { + vals[i] = e.Val + } + return vals +} + +func (d *OrderedDict) Clone() *OrderedDict { + nd := NewOrderedDict() + nd.entries = make([]DictEntry, len(d.entries)) + copy(nd.entries, d.entries) + return nd +} + +func (d *OrderedDict) String() string { + return d.stringDepth(0) +} + +func (d *OrderedDict) stringDepth(depth int) string { + if depth > 50 { + return "{...}" + } + parts := make([]string, len(d.entries)) + for i, e := range d.entries { + parts[i] = e.Key + ": " + e.Val.stringDepth(depth+1) + } + return "{" + strings.Join(parts, ", ") + "}" +} diff --git a/lang/main.go b/lang/main.go index f71215e8..aab278ed 100644 --- a/lang/main.go +++ b/lang/main.go @@ -8,19 +8,389 @@ import ( "Falcon/code/lex" blocklyParser "Falcon/code/parsers/blocklytomist" mistParser "Falcon/code/parsers/mistparser" + "Falcon/code/runtime" designAnalysis "Falcon/design" + "bufio" "encoding/xml" + "fmt" + "io" "os" + "path/filepath" "strings" ) func main() { - println("Hello from Falcon!\n") - + if len(os.Args) > 1 { + switch os.Args[1] { + case "repl": + repl() + return + case "run": + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "usage: Falcon run ") + os.Exit(1) + } + runFile(os.Args[2]) + return + case "format": + formatStdin() + return + case "reformat": + reformatStdin() + return + case "roundtrip": + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "usage: Falcon roundtrip ") + os.Exit(1) + } + roundtripFile(os.Args[2]) + return + case "exec": + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "usage: Falcon exec ") + os.Exit(1) + } + execFile(os.Args[2]) + return + case "correct": + if len(os.Args) < 3 { + fmt.Fprintln(os.Stderr, "usage: Falcon correct ") + os.Exit(1) + } + correctFile(os.Args[2]) + return + } + } + //repl() //diffTest() - // analyzeSyntax() - // xmlTest() - designTest() + analyzeSyntax() + //xmlTest() + //designTest() + //runProgram() + //runFile("/home/kumaraswamy/Documents/falcon/testing/run.mist") +} + +func reformatStdin() { + codeBytes, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + source := string(codeBytes) + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "Error:", r) + os.Exit(1) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: ""} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + exprs := langParser.ParseAll() + blocks := make([]ast.Block, len(exprs)) + for i, expr := range exprs { + blocks[i] = expr.Blockly(true) + } + xmlBlock := ast.XmlRoot{ + Blocks: blocks, + XMLNS: "https://developers.google.com/blockly/xml", + } + xmlBytes, _ := xml.MarshalIndent(xmlBlock, "", " ") + roundtripped := blocklyParser.NewParser(string(xmlBytes)).GenerateAST() + var out strings.Builder + for i, expr := range roundtripped { + if i > 0 { + out.WriteRune('\n') + } + out.WriteString(expr.String()) + } + fmt.Print(out.String()) +} + +// roundtripFile round-trips a Falcon source file through: +// +// Stage 1 (exit 1): Falcon source → mist parser → AST +// Stage 2 (exit 2): AST → Blockly serializer → XML +// Stage 3 (exit 3): XML → Blockly parser → AST → Falcon source +func roundtripFile(path string) { + codeBytes, err := os.ReadFile(path) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + source := string(codeBytes) + fileName := filepath.Base(path) + + // Stage 1: Falcon source → AST + var exprs []ast.Expr + func() { + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "stage 1 (mist parser):", r) + os.Exit(1) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: fileName} + tokens := lex.NewLexer(codeContext).Lex() + exprs = mistParser.NewLangParser(false, tokens).ParseAll() + }() + + // Stage 2: AST → Blockly XML + var xmlBytes []byte + func() { + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "stage 2 (blockly serializer):", r) + os.Exit(2) + } + }() + blocks := make([]ast.Block, len(exprs)) + for i, expr := range exprs { + blocks[i] = expr.Blockly(true) + } + xmlBlock := ast.XmlRoot{ + Blocks: blocks, + XMLNS: "https://developers.google.com/blockly/xml", + } + xmlBytes, _ = xml.MarshalIndent(xmlBlock, "", " ") + }() + + // Stage 3: Blockly XML → AST → Falcon source + func() { + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "stage 3 (blockly parser):", r) + os.Exit(3) + } + }() + roundtripped := blocklyParser.NewParser(string(xmlBytes)).GenerateAST() + var out strings.Builder + for i, expr := range roundtripped { + if i > 0 { + out.WriteRune('\n') + } + out.WriteString(expr.String()) + } + fmt.Print(out.String()) + }() +} + +func formatStdin() { + codeBytes, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + source := string(codeBytes) + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "Error:", r) + os.Exit(1) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: ""} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + exprs := langParser.ParseAll() + var out strings.Builder + for i, expr := range exprs { + if i > 0 { + out.WriteRune('\n') + } + out.WriteString(expr.String()) + } + fmt.Print(out.String()) +} + +func runFile(path string) { + codeBytes, err := os.ReadFile(path) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + source := string(codeBytes) + fileName := filepath.Base(path) + interp := runtime.NewInterpreter() + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, interp.FormatRuntimeError(r)) + //fmt.Fprintln(os.Stderr, "\n--- Go stack trace ---") + //fmt.Fprintln(os.Stderr, string(debug.Stack())) + os.Exit(1) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: fileName} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + exprs := langParser.ParseAll() + //fmt.Println("--- corrected source ---") + //fmt.Println(langParser.ReconstructedSource()) + //fmt.Println("------------------------") + for _, e := range exprs { + e.Blockly() + } + interp.Run(exprs) +} + +// execFile runs a Falcon source file without the Blockly validation stage. +// Use this when the program uses features (e.g. yield) that are not yet +// representable in the Blockly XML serializer. +func execFile(path string) { + codeBytes, err := os.ReadFile(path) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + source := string(codeBytes) + fileName := filepath.Base(path) + interp := runtime.NewInterpreter() + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, interp.FormatRuntimeError(r)) + os.Exit(1) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: fileName} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + exprs := langParser.ParseAll() + interp.Run(exprs) +} + +func correctFile(path string) { + codeBytes, err := os.ReadFile(path) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + source := string(codeBytes) + fileName := filepath.Base(path) + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "Error:", r) + os.Exit(1) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: fileName} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + //langParser.EnableAutoCorrect() + langParser.ParseAll() + fmt.Print(langParser.ReconstructedSource()) +} + +func repl() { + fmt.Println("Falcon REPL (type :exit to quit, Ctrl+D to exit)") + fmt.Println() + + interp := runtime.NewInterpreter() + reader := bufio.NewReader(os.Stdin) + + var inputBuf strings.Builder + openBraces := 0 + + for { + if openBraces == 0 { + fmt.Print(">>>> ") + } else { + fmt.Print(".. ") + } + + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + fmt.Println() + fmt.Println("Goodbye!") + break + } + fmt.Fprintln(os.Stderr, "read error:", err) + continue + } + + trimmed := strings.TrimSpace(line) + if trimmed == ":exit" { + fmt.Println("Goodbye!") + break + } + + inString := false + escaped := false + for _, c := range line { + if escaped { + escaped = false + continue + } + if c == '\\' && inString { + escaped = true + continue + } + if c == '"' { + inString = !inString + } else if !inString && c == '{' { + openBraces++ + } else if !inString && c == '}' { + if openBraces > 0 { + openBraces-- + } + } + } + inputBuf.WriteString(line) + + if openBraces > 0 { + continue + } + openBraces = 0 + + source := inputBuf.String() + inputBuf.Reset() + + if strings.TrimSpace(source) == "" { + continue + } + + func() { + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "Error:", r) + } + }() + codeContext := &context.CodeContext{SourceCode: &source, FileName: ""} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + exprs := langParser.ParseAll() + for _, e := range exprs { + e.Blockly() + } + result := interp.RunGetLast(exprs) + if result.Type() != runtime.Null && result.Type() != runtime.NonConsumable { + fmt.Println("=", result.String()) + } + }() + } +} + +func runProgram() { + fileName := "run.mist" + filePath := "/home/kumaraswamy/Documents/falcon/testing/" + fileName + codeBytes, err := os.ReadFile(filePath) + if err != nil { + panic(err) + } + sourceCode := string(codeBytes) + codeContext := &context.CodeContext{SourceCode: &sourceCode, FileName: fileName} + + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistParser.NewLangParser(false, tokens) + exprs := langParser.ParseAll() + + interp := runtime.NewInterpreter() + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, interp.FormatRuntimeError(r)) + os.Exit(1) + } + }() + interp.Run(exprs) } func designTest() { @@ -46,7 +416,7 @@ func designTest() { func xmlTest() { xmlFile := "xml.txt" - xmlPath := "/home/ekina/Documents/Falcon/testing/" + xmlFile + xmlPath := "/home/kumaraswamy/Documents/falcon/testing/" + xmlFile codeBytes, err := os.ReadFile(xmlPath) if err != nil { panic(err) @@ -63,7 +433,7 @@ func xmlTest() { func analyzeSyntax() { fileName := "hi.mist" - filePath := "/home/ekina/Documents/Falcon/testing/" + fileName + filePath := "/home/kumaraswamy/Documents/falcon/testing/" + fileName codeBytes, err := os.ReadFile(filePath) if err != nil { panic(err) diff --git a/lang/wasm.go b/lang/wasm.go index 99f50d91..6b63f17a 100644 --- a/lang/wasm.go +++ b/lang/wasm.go @@ -9,7 +9,9 @@ import ( "Falcon/code/ast" "Falcon/code/context" "Falcon/code/lex" - "Falcon/code/parser" + blocklyParser "Falcon/code/parsers/blocklytomist" + "Falcon/code/parsers/mistparser" + "Falcon/code/runtime" "Falcon/design" "encoding/xml" "strings" @@ -66,7 +68,7 @@ func mistToXml(this js.Value, p []js.Value) any { codeContext := &context.CodeContext{SourceCode: &sourceCode, FileName: "appinventor.live"} tokens := lex.NewLexer(codeContext).Lex() - langParser := parser.NewLangParser(true, tokens) + langParser := mistparser.NewLangParser(true, tokens) langParser.SetComponentDefinitions(componentContextMap, reverseComponentMap) expressions := langParser.ParseAll() @@ -94,7 +96,7 @@ func xmlToMist(this js.Value, p []js.Value) any { return js.ValueOf("No XML content provided") } xmlContent := p[0].String() - exprs := parser.NewXMLParser(xmlContent).ParseBlockly() + exprs := blocklyParser.NewParser(xmlContent).GenerateAST() var builder strings.Builder for _, expr := range exprs { @@ -136,6 +138,44 @@ func convertXmlToSchema(this js.Value, p []js.Value) any { }) } +// runCode executes Falcon source code and streams each printed line to JS via +// the falconPrint(line) callback. Parse and runtime errors are sent to mistError(msg). +func runCode(this js.Value, p []js.Value) any { + if len(p) < 1 { + js.Global().Call("mistError", "runCode(sourceCode) not provided!") + return js.Undefined() + } + sourceCode := p[0].String() + + var interp *runtime.Interpreter + defer func() { + if r := recover(); r != nil { + var msg string + if interp != nil { + msg = interp.FormatRuntimeError(r) + } else if s, ok := r.(string); ok { + msg = s + } else if err, ok := r.(error); ok { + msg = err.Error() + } else { + msg = "unknown error" + } + js.Global().Call("mistError", msg) + } + }() + + codeContext := &context.CodeContext{SourceCode: &sourceCode, FileName: "wasm"} + tokens := lex.NewLexer(codeContext).Lex() + langParser := mistparser.NewLangParser(false, tokens) + expressions := langParser.ParseAll() + + interp = runtime.NewInterpreterWithOutput(func(line string) { + js.Global().Call("falconPrint", line) + }) + interp.Run(expressions) + return js.Undefined() +} + func main() { println("Hello from wasm.go!") @@ -144,5 +184,6 @@ func main() { js.Global().Set("xmlToMist", js.FuncOf(xmlToMist)) js.Global().Set("schemaToXml", js.FuncOf(convertSchemaToXml)) js.Global().Set("xmlToSchema", js.FuncOf(convertXmlToSchema)) + js.Global().Set("runCode", js.FuncOf(runCode)) <-c } diff --git a/lang/web/falcon.wasm b/lang/web/falcon.wasm index b876007d..61707b69 100755 Binary files a/lang/web/falcon.wasm and b/lang/web/falcon.wasm differ diff --git a/testing/diff0.mist b/testing/diff0.mist deleted file mode 100644 index 2a26f33a..00000000 --- a/testing/diff0.mist +++ /dev/null @@ -1,6 +0,0 @@ -@Button { Button1 } -@Notifier { Notifier1 } - -when Button1.Click() { - Notifier1.ShowAlert("Hello, World!") -} \ No newline at end of file diff --git a/testing/diff1.mist b/testing/diff1.mist deleted file mode 100644 index 051b61f0..00000000 --- a/testing/diff1.mist +++ /dev/null @@ -1,3 +0,0 @@ -when any Button.Click() { - Notifier1.ShowAlert("Hello, World!") -} \ No newline at end of file diff --git a/testing/hi.mist b/testing/hi.mist index 69bda836..6d59ac56 100644 --- a/testing/hi.mist +++ b/testing/hi.mist @@ -1,14 +1,12 @@ -@Button { Button1 } -@Label { Label1 } - -func main() { - println("Hello, World!") - println("Counter is currently: " _ this.counter) +func repro() = { + local i = 1 + while (i <= 3) { + if (i == 2) { + yield i + } + i = i + 1 + } + -1 } -global counter = 0 - -when Button1.Click { - this.counter = this.counter + 1 - Label1.Text = counter -} +println(repro()) diff --git a/testing/run.mist b/testing/run.mist new file mode 100644 index 00000000..6d59ac56 --- /dev/null +++ b/testing/run.mist @@ -0,0 +1,12 @@ +func repro() = { + local i = 1 + while (i <= 3) { + if (i == 2) { + yield i + } + i = i + 1 + } + -1 +} + +println(repro()) diff --git a/testing/xml.txt b/testing/xml.txt index 8be23560..94690dd2 100644 --- a/testing/xml.txt +++ b/testing/xml.txt @@ -1,83 +1,21 @@ -ballcolour#ffffffcheckcsvFALSEdoneFALSEdeletemenuletterwordto punctuationallstartwordsThe, It, He, She, They, This, That, There, I, We, A, An, In, As, At, If, When, While, After, AlthoughrightballsBall1Ball2Ball3Ball4Ball5leftballsBall6Ball7Ball8Ball9Ball10conditionalreplacer~conditionseparator\currentconversationmenutypemodelqueryseparator\partialsentencepartialwordsentencewordseparator\tagforwordsrequestvarseparator~ballposprefixpos_settingprefixset_charlistcurrentmenuitemsdatalistdigitslisterrorsgoodcombojunkmenuitemsmodelquerylistnotcsvpunctuationlistpunctuationchars.?!,;:-()[]{}'"recentwordslistsselectedmodelsselectedqueriessentencegroupsvarlistwordslistvardataleftballcolor100010050rightballcolor010010050draggedballindex0sentenceprefixfs_modelbedrock:us.meta.llama3-3-70b-instruct-v1:0startmenuwordslettersnumberspunctuationrepeatspeakdeleteaskqueryprefixq_wordsprefixw_providerbedrockmodel_providerchatgpt:gpt-4o-minichatgptchatgpt:o1-previewchatgptchatgpt:o1-minichatgptgoogle:geminigeminigoogle:gemini-1.0-progeminigoogle:gemini-1.5-progeminigoogle:gemini-2.0-flashgeminigoogle:gemini-2.5-flashgeminigoogle:gemini-2.0-flash-expgeminibedrock:anthropic.claude-sonnet-4-5-20250929-v1:0bedrockbedrock:meta.llama3-70b-instruct-v1:0bedrockbedrock:meta.llama3-70b-instruct-v1:0bedrockbedrock:us.meta.llama3-3-70b-instruct-v1:0bedrockbedrock:us.meta.llama4-maverick-17b-instruct-v1:0bedrockollama:gemma2ollamaollama:gemma2:2bollamaproviderschatgptchatgptchatgptchatgptmodelschatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-minichatgpt:gpt-4o-ministartingmodelschatgpt:gpt-4o-minichatgpt:gpt-4o-miniquerytemplategive me only 25 unique words ~word\\beginning with '[word]' ~ that are most likely to ~sentence\start a sentence\ follow the partial sentence '[sentence]'~ in .csv format. Your response should consist only of these 25 words alternating with commas.addkeyprefixtextk_textaddqueryprefixquerynameglobal queryprefixquerynameaddtoreporttexttbresultTexttbresultTextresponseclockresponseclockdd/MM/yyyy hh:mm:ss a\nChatBot1ModeltbresultTexttbresultText\ntext\n\nallballsballslistglobal leftballsballslistglobal rightballsballslistaskLLMglobal selectedmodelsNotifier1please select at least one modeltbdisplayTextNotifier1please first type what you want to ask the LLM, then select AskChatBot3Modelglobal selectedmodelsglobal selectedmodelsChatBot3Modelchatgpt:gpt-4o-miniChatBot3ProviderChatBot3Modelglobal model_providernot foundChatBot3ApiKeyTinyDB1addkeyprefixChatBot3ProviderChatBot3tbdisplayTextdebugSharing1tbresultTextScreen1TitlegetallpartialsentencesScreen1TitlegetrecentfirstwordsScreen1TitleSPLITas soon as\global sentencewordseparatorDo It Result: "\" ---- -1Screen1Titleitemglobal junkDo It Result: ["As soon as possible\", "As soon as\", "As soon\", "As\", "At the\", "At\", "In particular\", "In that\", "In\", "It is clear that they are w r\r", "It is clear that they are w\w", "It is clear that they are\", "It is clear that they\", "It is clear that\", "It is clear\", "It is clear that they are prepared\", "It is clear that they are\", "It is clear that they\", "It is\", "It\", "She had\", "She\", "That was\", "That\", "\"] ---- -SPLITitemglobal sentencewordseparatorDo It Result: "\" ---- -1global junkminusprefixtagsbeginningglobal wordsprefixDo It Result: "w_" ---- -TinyDB1w_After arriving\tagtagsbeginningglobal wordsprefixTinyDB1tagtagtagsbeginningglobal wordsprefixDo It Result: "w_" ---- -tbresultTexttbresultText\n\ntag\nTinyDB1tagScreen1Titleglobal partialsentencetbreplyTextglobal vardatatbreplyTextreplacevariablestbqueryTexttbreplyTextglobal varlisttbreplyTextglobal datalisttbreplyTextglobal checkcsvtbresultTexttbreplyTextglobal modelquerylisttbreplyTextChatBot1ModeltbresultTexttagsbeginningglobal wordsprefixaddtoreportgettagpartbetweenglobal wordsprefixAfterglobal sentencewordseparatorDo It Result: "\" ---- -tbqueryTextTinyDB1q_basicaskLLMTinyDB1TinyDB1_tbvarglobal partialsentencemyfirstwordssentenceswordssentencesentenceswordSPLITATFIRSTsentence 1wordwordswordswordwordsgetallpartialsentencespartialsentencespredicyiontagstagsbeginningglobal wordsprefixtagsminusprefixminusprefixpredicyiontagspartialsntencessentencepartstagsminusprefixpartialsntencesgetmenuitemitemglobal menuitemsitemglobal menuitems1global menuitemsglobal menuitemsitemgetrecentfirstwordsdicupdaterecentwordslistswordkeyswordslistglobal recentwordslistsDo It Result: [] ---- -wordslist1wordwordkeysworddic0KEYSdicgettagpartbetweenpretextposttextwordsliststagstagsbeginningpretextaddtoreportpretext starts tagstagswordslistsitemtagtagstagpretextitemaddtoreportpretextfollowed by : wordslistsitemitemwordslistssplitposttextSPLITitemposttextGTEsplitposttext1splitposttext1itemgetwordstextpartsSPLITtext:GTEparts2textparts2iscsvtextpartstextaddtoreportparts words. is csvpartstextremovenonalphatextGTEparts5partsgetwordsfromLLMglobal selectedmodelsNotifier1please select at least one modelChatBot2Modelglobal modelChatBot2Providerglobal providerChatBot2ApiKeyTinyDB1addkeyprefixChatBot2Providerqueryqueryfromtemplateglobal querytemplateglobal partialwordglobal partialsentencetbqueryTextquery\nsent to ChatBot2ModeladdtoreportChatBot2Model queried: queryChatBot2queryinitvarsvarlinesitemitemSPLITtbvarText\nitemitemGTEvarlines1global checkcsvEQUALvarlines1csvglobal checkcsvvarlinesvarlinesglobal varlistSPLITvarlines1global varseparatorDo It Result: "~" ---- -global datalistGTEvarlines1varlinesglobal checkcsvFALSEglobal vardataiscsvtextpartsisSPLITtext\nFALSEparapartswordsSPLITpara,GTEwords15global wordslistitemwordsremovenonalphaitemisTRUEisiscsv2textpartsSPLITtext:GTEparts2textparts2partsitemSPLITtext,itemGTEparts5global wordslistitempartsremovenonalphaitemGTEparts5makecharliststartcharendcharlstiAsciiConversion1startcharAsciiConversion1endchar1lstAsciiConversion1ilstmaketagpartialsentencepartialwordglobal wordsprefixpartialsentenceglobal sentencewordseparatorpartialwordminusprefixlistwithtagstaglistwithtagstagpartsSPLITtag_GTEtagparts2tagparts2tagnameinselectionselectionselection3selection2nextconversationglobal modelquerylistresponseclockTimerEnabledFALSEScreen1Titledoneglobal doneTRUEquerycompleteScreen1Title0global currentconversationglobal modelquerylistDo It Result: ["1\1\1", "2\1\1", "1\1\2", "2\1\2"] ---- -1mqdSPLITglobal currentconversationglobal modelqueryseparatorDo It Result: "\" ---- -global vardatadataindexmqd3varvalsLTEdataindexglobal datalistSPLITglobal datalistdataindexglobal varseparatori1global varlist1global varlistiglobal vardataLTEivarvalsvarvalsiglobal modelquerylist1tbqueryTextTinyDB1addqueryprefixglobal selectedqueriesmqd2tbqueryTextChatBot1Modelglobal selectedmodelsmqd1ChatBot1ProviderChatBot1Modelglobal model_providernot foundChatBot1ApiKeyTinyDB1addkeyprefixChatBot1ProviderqueryreplacevariablestbqueryTexttbqueryTextquery\nsent to ChatBot1ModelChatBot1queryaddtoreportprompt: query\nsent to model ChatBot1Model\nprocessdeletedeletehowmuchEQUALdeletehowmuchletterglobal partialwordGTEglobal partialsentence2global partialsentenceglobal partialsentence1global partialsentence1global partialsentenceGTEglobal partialword2global partialwordglobal partialword1global partialword1global partialwordEQUALdeletehowmuchwordglobal partialwordglobal partialsentencewordsglobal partialsentenceglobal partialsentence wordsglobal partialwordEQUALdeletehowmuchto punctuationglobal partialsentenceglobal partialsentence global partialwordglobal partialwordfoundpunctuationFALSEcharsSPLITglobal partialsentenceichars1-1charsiglobal punctuationlistglobal partialsentenceglobal partialsentence1ifoundpunctuationTRUEfoundpunctuationglobal partialsentenceEQUALdeletehowmuchallglobal partialsentenceglobal partialwordupdatetbdisplayquerycompletevasettingsVisibleFALSEvadoneVisibleTRUEglobal checkcsvreportresultsgood responsesglobal goodcomboreportresultsuseless responsesglobal notcsvreportscoresSCORESglobal goodcomboreportresultserrorsglobal errorsunselectbadmodelstbqueryVisibleFALSEtbqueryboxVisibleTRUEqueryfromtemplatequerypartialwordpartialsentencequeryquery[sentence]partialsentencequeryquery[word]partialwordpartsSPLITqueryglobal conditionalreplacerpartpartsconditionpartsSPLITpartglobal conditionseparatorEQconditionparts3itemconditionparts1replacerEQUALitemsentencepartialsentencepartialwordpartspartpartsEQreplacer0conditionparts2conditionparts3querypartsqueryreadfileFile1//sentences.txtremovenonalphatextallowedcharsglobal charlistSPLITtextallowed' charsitemcharsDOWNCASEitemallowedtextcharstextreplacevariablesqueryvarglobal varlistDo It Result: ["word", "sentence"] ---- -queryquery[var]varglobal vardataDo It Result: {"word":"","sentence":"w"} ---- -not foundpartsSPLITqueryglobal conditionalreplacerDo It Result: "\\" ---- -partpartsconditionpartsSPLITpartglobal conditionseparatorDo It Result: "\" ---- -EQconditionparts3varnameconditionparts1varnameKEYSglobal vardatapartspartpartsEQvarnameglobal vardatanot found0conditionparts2conditionparts3querypartsqueryreportresultscaptionresultslistresultslistreporttextitemresultslistmqdSPLITitemglobal modelqueryseparatorDo It Result: "\" ---- -reporttextreporttext\nglobal selectedmodelsmqd1__global selectedqueriesmqd2addtoreportcaption\n\nreporttextreportscorescaptionresultslistresultslistreporttextmodelcountquerycountModel scores,max global datalistDo It Result: "\" ---- -global selectedqueriesDo It Result: "\" ---- -\n Query scores,max global selectedmodelsDo It Result: "\" ---- -global datalistDo It Result: "\" ---- -itemresultslistmqdSPLITitemglobal modelqueryseparatorDo It Result: "\" ---- -modelqueryglobal selectedmodelsmqd1global selectedqueriesmqd2modelmodelcountmodelmodelcount01queryquerycountqueryquerycount01modelcountmodelcountreporttextreporttext\nmodel : countreporttextreporttext\n\nquerycountquerycountreporttextreporttext\nquery : counttbresultTextreporttext\n\ntbresultTextsaveblockblocklinesheadingSPLITblock\nlineslinelineslineheadinglines1lineslinesTinyDB1global sentenceprefixheadinglinesScreen1Titleheading updatedsavepredictionpartialsentencepartialwordpredictionTinyDB1maketagpartialsentencepartialwordpredictionsentencepartspartialsentencesandwordstagpartialsentencesandwordstagpartsSPLITtagglobal sentencewordseparatorDo It Result: "\" ---- -GTEtagparts1tagparts1tagbshareagainSharing1tbresultTextsentencesstartingalreadytypedsentencesgetallpartialsentencessentencesentencesEQsentencealreadytyped1setballcolorballglobal rightballsPaintColorballglobal rightballcolorballglobal leftballsPaintColorballglobal leftballcolortagsbeginningprefixtagTinyDB1EQtagprefix1tagscontainingpiecetagTinyDB1CONTAINStagpieceunselectbadmodelsbadmodelsitemglobal errorsmqdSPLITitemglobal modelqueryseparatorDo It Result: "\" ---- -modelindexmqd1LTEmodelindexglobal selectedmodelsDo It Result: ["gemini-2.0-flash", "gemini-2.0-flash-exp", "anthropic.claude-v2", "meta.llama3-70b-instruct-v1:0", "us.meta.llama3-3-70b-instruct-v1:0", "us.meta.llama4-maverick-17b-instruct-v1:0"] ---- -badmodelsglobal selectedmodelsDo It Result: ["gemini-2.0-flash", "gemini-2.0-flash-exp", "anthropic.claude-v2", "meta.llama3-70b-instruct-v1:0", "us.meta.llama3-3-70b-instruct-v1:0", "us.meta.llama4-maverick-17b-instruct-v1:0"] ---- -modelindexbadmodelbadmodelsindexbadmodelglobal selectedmodelsDo It Result: ["gemini-2.0-flash", "gemini-2.0-flash-exp", "anthropic.claude-v2", "meta.llama3-70b-instruct-v1:0", "us.meta.llama3-3-70b-instruct-v1:0", "us.meta.llama4-maverick-17b-instruct-v1:0"] ---- -GTindex0global selectedmodelsDo It Result: ["gemini-2.0-flash", "gemini-2.0-flash-exp", "anthropic.claude-v2", "meta.llama3-70b-instruct-v1:0", "us.meta.llama3-3-70b-instruct-v1:0", "us.meta.llama4-maverick-17b-instruct-v1:0"] ---- -indexupdateprovidermodelproviderglobal providersCONTAINSmodelproviderChatBot1ProviderproviderChatBot1ApiKeyTinyDB1addkeyprefixproviderupdateradiusandposradiusTinyDB1global settingprefixradius40postagtagsbeginningglobal ballposprefixtagpartsSPLITpostag_ballxyallballstagparts2SPLITTinyDB1postag\RadiusballradiusXballxy1Yballxy2updaterecentmenuitemsnextwordsgetrecentfirstwordsaddtoreportnextwordsEQnextwords1global partialsentenceglobal partialsentence nextwords1global partialwordnextwordsgetrecentfirstwordsglobal menuitemsnextwordsupdatetbdisplayupdaterecentwordslistssentencesgettagpartbetweenglobal wordsprefixglobal partialsentenceDo It Result: " After" ---- -global sentencewordseparatoraddtoreportsentencesglobal recentwordslistswordlistsentencesentencessentencewordlistupdateselectedmodelsglobal selectedmodelsTinyDB1selectedmodelsglobal startingmodelsDo It Result: ["us.meta.llama3-3-70b-instruct-v1:0", "gemma2:2b"] ---- -updatesentenceaddtexttbsentenceTexttbsentenceText TbwordTextaddtextupdatesentencegroupsglobal sentencegroupstagtagsbeginningglobal sentenceprefixtagglobal sentenceprefixupdatetbdisplaytbdisplayTextglobal partialsentence global partialwordupdatewordlistglobal tagforwordsrequestmaketagglobal partialsentenceglobal partialworddbcontentsTinyDB1global tagforwordsrequestdbcontentsglobal wordslistgetwordsfromLLMiscsvdbcontentsEQUALglobal menutypewordsglobal menuitemsglobal wordslistaddtoreportfrom local store: dbcontentsScreen1TitledbcontentslpsettingsVisibleballindexcomponentallballsLTEballindexglobal currentmenuitemsselecteditemglobal currentmenuitemsballindexglobal menutypeglobal menutypeselecteditemLTEballindexglobal sentencegroupsglobal menuitemsTinyDB1global sentenceprefixselecteditemEQUALselecteditemlettersglobal menuitemsglobal charlistEQUALselecteditemnumbersglobal menuitemsglobal digitslistEQUALselecteditempunctuationglobal menuitemsglobal punctuationlistEQUALselecteditemspeakTextToSpeech1tbdisplayTextEQUALselecteditemaskaskLLMEQUALselecteditemrepeatupdaterecentmenuitemsEQUALselecteditemwordsglobal menuitemsupdatewordlistEQUALselecteditemdeleteglobal menuitemsglobal deletemenuglobal menutypepunctuationnumbersglobal partialsentenceglobal partialsentence global partialwordselecteditemglobal partialwordupdatetbdisplayEQUALglobal menutyperepeatglobal menuitemsglobal partialsentenceglobal partialsentence selecteditemglobal partialwordupdatetbdisplayupdaterecentmenuitemsEQUALglobal menutypewordsglobal partialsentenceglobal partialsentence selecteditemglobal partialwordglobal menuitemsupdatewordlistupdatetbdisplayEQUALglobal menutypedeleteprocessdeleteselecteditemEQUALglobal menutypelettersEQUALselecteditem_global partialsentenceglobal partialsentence global partialwordglobal partialwordglobal partialwordglobal partialwordselecteditemupdatewordlistupdatetbdisplayglobal menutypeglobal sentencegroupsTextToSpeech1selecteditemlpsettingsVisibleindexcomponentallballsOREQglobal draggedballindex0EQglobal draggedballindexindexglobal draggedballindexindexcomponentcurrentXcurrentYPaintColorcomponentglobal ballcolourindexcomponentallballsEQindexglobal draggedballindexglobal draggedballindex0TinyDB1global ballposprefixindexXcomponent\Ycomponentglobal ballcolourPaintColorcomponentPaintColorcomponent#ffffffbbackvacanvasVisibleTRUEvasettingsVisibleFALSEbdonevadoneVisibleFALSEvasettingsVisibleTRUEbfreezeEQUALbfreezeTextfreezelpsettingsVisibleTRUEbfreezeTextdonemenuclockTimerEnabledFALSECanvas1menuclockTimerEnabledTRUElpsettingsVisibleFALSEbfreezeTextfreezebgoresponseclockTimerEnabledresponseclockTimerEnabledFALSEglobal doneTRUEinitvarsglobal modelquerylistglobal selectedqueriesDo It Result: ["basic"] ---- -Notifier1no queries selectedglobal selectedmodelsDo It Result: ["gemma2:2b", "us.meta.llama3-3-70b-instruct-v1:0"] ---- -Notifier1no models selectedTinyDB1addqueryprefixquery_boxtbqueryboxTextglobal doneFALSEtbresultTexttbreplyTexttbqueryVisibleTRUEtbqueryboxVisibleFALSEglobal errorsglobal goodcomboglobal notcsvdatasetnum1MAXglobal datalistDo It Result: [] ---- -iniinit11querynum1global selectedqueriesDo It Result: ["basic"] ---- -1modelnum1global selectedmodelsDo It Result: ["gemma2:2b", "us.meta.llama3-3-70b-instruct-v1:0"] ---- -1global modelquerylistDo It Result: ["us.meta.llama3-3-70b-instruct-v1:0\basic\my room is~n", "gemma2:2b\basic\~w", "us.meta.llama3-3-70b-instruct-v1:0\basic\~w"] ---- -modelnumglobal modelqueryseparatorquerynumglobal modelqueryseparatordatasetnumresponseclockTimerEnabledTRUEScreen1Title0nextconversationbsentencessaveblocktbreplyTextupdatesentencegroupsbshareSharing1tbresultTextbvartbvarVisibleTinyDB1_tbvartbvarTexttbvarVisibletbvarVisibleCanvas1ANDLTEy50touchedAnySpritevacanvasVisibleFALSEvasettingsVisibleTRUEreturnclockTimerEnabledTRUEChatBot1addtoreportresponseText\nglobal checkcsviscsvresponseTextglobal goodcomboglobal currentconversationdata okglobal notcsvglobal currentconversationdata not okglobal donenextconversationChatBot1tbreplyTextresponseText\ntbreplyTextaddtoreportresponseTextglobal errorsglobal currentconversationnextconversationChatBot2addtoreportresponseTextglobal wordslistgetwordsresponseTextiscsvresponseTextwordscsv,global wordslistTinyDB1maketagglobal partialsentenceglobal partialwordwordscsvglobal partialwordglobal wordslistitemglobal wordslistEQitemglobal partialword1Screen1Title,global wordslistChatBot2Notifier2ChatBot2Model\nChatBot2Provider\nresponseTextChatBot3TextToSpeech1responseTextChatBot3Notifier2ChatBot3Model\nChatBot3Provider\nresponseTextmenuclockglobal menuitemsglobal menuitemsglobal sentencegroupsDo It Result: ["Food", "General", "Help", "Leisure", "Places"] ---- -global menuitemsglobal startmenuDo It Result: ["words", "letters", "delete", "speak", "ask"] ---- -global menutypeglobal menutypetempliistglobal sentencegroupsDo It Result: ["Food", "General", "Help", "Leisure", "Places"] ---- -templiistglobal menuitemsglobal menuitemstempliistDo It Result: ["Food", "General", "Help", "Leisure", "Places"] ---- -Canvas1global currentmenuitemsballindex1allballs1menuitemgetmenuitemmenuitemglobal currentmenuitemsmenuitemCanvas1menuitemXallballsballindexYallballsballindexresponseclockScreen1TitleScreen1Title1GTEScreen1Title20tbreplyTexttbreplyText\nChatBot1Model timeoutaddtoreporttimeoutglobal errorsglobal currentconversationnextconversationreturnclockvasettingsVisibleFALSEreturnclockTimerEnabledFALSEvacanvasVisibleTRUEFile1blocksSPLITtext\n\nblockblockssaveblockblockupdatesentencegroupsScreen1TextToSpeech1hi Virajglobal querytemplateTinyDB1global settingprefixqueryglobal querytemplatemenuclockTimerIntervalTinyDB1global settingprefixmenutimer3000global modelTinyDB1global settingprefixmodelchatgpt:gpt-4o-miniglobal providerTinyDB1global settingprefixproviderchatgptsetballcolorsavepredictionglobal startwordsglobal partialsentenceglobal partialwordupdateselectedmodelslpkeysElementsglobal providersDo It Result: ["chatgpt", "chatgpt", "chatgpt", "chatgpt"] ---- -tbvarTextTinyDB1_tbvarcsv\nsentence~word\nmy room is~nTinyDB1addqueryprefixquery_boxglobal selectedqueriesquery_boxglobal digitslistmakecharlist09global punctuationlistSPLITglobal punctuationcharsglobal charlistmakecharlistazglobal charlist_tagsreadTinyDB1FALSEtagtagsCONTAINStagglobal sentenceprefixreadTRUEreadreadfileupdatesentencegroupsupdateradiusandposlpdelTinyDB1addqueryprefixlpdelSelectionlpdellpdelElementstagqnametagTinyDB1EQtagglobal queryprefix1nameinselectionqnameNEQtagquery_boxlpkeysNotifier2enter API key for lpkeysSelectionAPI key : TRUElploadlploadElementsqnametagTinyDB1EQtagglobal queryprefix1nameinselectionqnamelploadtbqueryboxTextTinyDB1addqueryprefixlploadSelectionlpmodelreturnclockTimerEnabledFALSElpmodelElementsmodelglobal modelsDo It Result: ["gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini", "gpt-4o-mini"] ---- -lpmodelElementsmodelglobal selectedmodelsDo It Result: ["gpt-4o-mini", "gpt-4o-mini"] ---- -x modellpmodelElementsalllpmodelEQUALlpmodelSelectionallglobal selectedmodelsglobal modelsselectedmodelglobal modelslpmodelSelectionIndexselectedmodelglobal selectedmodelsglobal selectedmodelsselectedmodelglobal selectedmodelsglobal selectedmodelsselectedmodelTinyDB1selectedmodelsglobal selectedmodelslpqueryEQUALlpquerySelectionsaveNotifier1name:please name the query...TRUEEQUALlpquerySelectiondelete...lpdelEQUALlpquerySelectionloadlploadEQlpquerySelectionx1global selectedqueriesnameinselectionlpquerySelectionglobal selectedqueriesglobal selectedquerieslpquerySelectionlpquerylpqueryElementssaveloaddelete...queriesitemTinyDB1CONTAINSitemglobal queryprefixqueryqueriesquerynamenameinselectionquerylpqueryElementsquerynameglobal selectedqueriesx querynamelpsettingsEQUALlpsettingsSelectionbutton radiusNotifier3enter fresh valuebutton radius = Ball1RadiusTRUEEQUALlpsettingsSelectionmenu timer delayNotifier4enter fresh valuemenu timer delay (ms) =menuclockTimerIntervalTRUEEQUALlpsettingsSelectionquery templateNotifier5global querytemplatepaste replacement for query template belowTRUEEQUALlpsettingsSelectionmodellpsinglemodelEQUALlpsettingsSelectionAPI keylpkeyslpsettingslpsettingsElementsmenu timer delayquery templatemodelAPI keylpsinglemodellpsinglemodelElementsKEYSglobal model_providerlpsinglemodelglobal modelKEYSglobal model_providerlpsinglemodelSelectionIndexTinyDB1global settingprefixmodelglobal modelglobal providerglobal modelglobal model_providerglobal providerTinyDB1global settingprefixproviderglobal providerNotifier1TinyDB1addqueryprefixresponsetbqueryboxTextNotifier2TinyDB1addkeyprefixlpkeysSelectionresponseNotifier3NUMBERresponseTinyDB1global settingprefixradiusresponseupdateradiusandposNotifier4NUMBERresponsemenuclockTimerIntervalresponseTinyDB1global settingprefixmenutimerresponseNotifier5NEQresponseCancelglobal querytemplateresponseTinyDB1global settingprefixqueryresponse \ No newline at end of file + --- Entry 779 --- + + + ROTATE_LEFT + + + + + 2 + 2 + 1 + 2 + 3 + 4 + + + + \ No newline at end of file