diff --git a/doc/source/language/flow.rst b/doc/source/language/flow.rst index c6a0ec1eb..a1bc6a07c 100644 --- a/doc/source/language/flow.rst +++ b/doc/source/language/flow.rst @@ -61,6 +61,32 @@ Checks if the expression supplied returns true. If it does, ``IF`` executes the // syntax error - ELSE without IF. IF X > 10 { PRINT "Large". }. ELSE { PRINT "Small". }. +.. index:: CHOOSE +.. _choose: + +CHOOSE (Ternary operator) +------------------------- + +An expression that evalualtes to one of two choices depending on a +conditional check: + + CHOOSE expression1 IF condition ELSE expression2 + +Note this is NOT a statement. This is an expression that can be embedded +inside other statements, like so: + + SET X TO CHOOSE expression1 IF condition ELSE expression2. + PRINT CHOOSE "High" IF altitude > 20000 ELSE "Low". + +The reason to use the ``CHOOSE`` operator instead of an +IF/ELSE statement is that IF/ELSE won't return a value, while +this does, and thus this can be embedded inside other expressions. + +(This is similar to the ``?`` operator in languages like "C" and its +derivatives, except it puts the "true" choice first, then the +conditional check, then the "false" choice.) + + .. index:: LOCK .. _lock: diff --git a/kerboscript_tests/ternary.ks b/kerboscript_tests/ternary.ks new file mode 100644 index 000000000..e15b207f8 --- /dev/null +++ b/kerboscript_tests/ternary.ks @@ -0,0 +1,38 @@ +print "THESE are tests of the ternary CHOOSE operator.". + +Print "Basic case - Next line should print 'A':". +print choose "A" if true else "B". + +Print "Basic case - Next line should print 'B':". +print choose "A" if false else "B". + + +print "Testing nested CHOOSEs. Next line should be 'ABCDE*****':". +set str to "". +for i in range(0,10) { + set str to str + (choose "A" if i = 0 else choose "B" if i = 1 else choose "C" if i = 2 else choose "D" if i = 3 else choose "E" if i = 4 else "*"). + +} +print str. + + +print "Weird case - nesting choose in the boolean.". +set x to true. +set y to false. +set a to 1. +print " Next 2 lines should be 'A':". +print " " + (choose "A" if (choose x if a=1 else y) else "B"). +print " " + (choose "A" if choose x if a=1 else y else "B"). // same without parens should work. +print " Next 2 lines should be 'B':". +print " " + (choose "A" if (choose x if a=2 else y) else "B"). +print " " + (choose "A" if choose x if a=2 else y else "B"). // same without parens should work. + +print "Complex case - selecting delegate with choose.". +set a to 1. +set del1 to choose { print "trueDel". } if a = 1 else { print "falseDel.". }. +set a to 2. +set del2 to choose { print "trueDel". } if a = 1 else { print "falseDel.". }. +print "Next line should say 'trueDel':". +del1:call(). +print "Next line should say 'falseDel':". +del2:call(). diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 298e0d958..4692aba4c 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -282,6 +282,7 @@ private void IterateUserFunctions(ParseNode node, Action action) case TokenType.COMPARATOR: case TokenType.AND: case TokenType.OR: + case TokenType.CHOOSE: case TokenType.directive: doChildren = false; break; @@ -1149,6 +1150,9 @@ private void VisitNode(ParseNode node) case TokenType.expr: VisitExpr(node); break; + case TokenType.ternary_expr: + VisitTernary(node); + break; case TokenType.or_expr: case TokenType.and_expr: VisitShortCircuitBoolean(node); @@ -1352,6 +1356,33 @@ private void VisitExpr(ParseNode node) } } + private void VisitTernary(ParseNode node) + { + NodeStartHousekeeping(node); + + // Syntax pattern is: + // [0] = keyword CHOOSE + // [1] = expression returned if true + // [2] = keyword IF + // [3] = expression with boolean value + // [4] = keyword ELSE + // [5] = expression returned if false + + VisitNode(node.Nodes[3]); // eval the boolean clause, put on stack. + + Opcode bypassTrue = AddOpcode(new OpcodeBranchIfFalse()); + + VisitNode(node.Nodes[1]); // expression if true. + Opcode bypassFalse = AddOpcode(new OpcodeBranchJump()); + + bypassTrue.DestinationLabel = GetNextLabel(false); + + VisitNode(node.Nodes[5]); // expression if false. + + bypassFalse.DestinationLabel = GetNextLabel(false); + addBranchDestination = true; + } + /// /// Handles the short-circuit logic of boolean OR and boolean AND /// chains. It is like VisitExpressionChain (see elsewhere) but diff --git a/src/kOS.Safe/Compilation/KS/ParseTree.cs b/src/kOS.Safe/Compilation/KS/ParseTree.cs index 31c312b37..dce66ca99 100644 --- a/src/kOS.Safe/Compilation/KS/ParseTree.cs +++ b/src/kOS.Safe/Compilation/KS/ParseTree.cs @@ -319,6 +319,9 @@ internal object Eval(ParseTree tree, params object[] paramlist) case TokenType.expr: Value = Evalexpr(tree, paramlist); break; + case TokenType.ternary_expr: + Value = Evalternary_expr(tree, paramlist); + break; case TokenType.or_expr: Value = Evalor_expr(tree, paramlist); break; @@ -711,6 +714,13 @@ protected virtual object Evalexpr(ParseTree tree, params object[] paramlist) return null; } + protected virtual object Evalternary_expr(ParseTree tree, params object[] paramlist) + { + foreach (var node in Nodes) + node.Eval(tree, paramlist); + return null; + } + protected virtual object Evalor_expr(ParseTree tree, params object[] paramlist) { foreach (var node in Nodes) diff --git a/src/kOS.Safe/Compilation/KS/Parser.cs b/src/kOS.Safe/Compilation/KS/Parser.cs index ad6894a41..2dd1edfb9 100644 --- a/src/kOS.Safe/Compilation/KS/Parser.cs +++ b/src/kOS.Safe/Compilation/KS/Parser.cs @@ -1749,8 +1749,9 @@ private void Parsereturn_stmt(ParseNode parent) // NonTerminalSymbol: return_stm } // Concat Rule - tok = scanner.LookAhead(TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule - if (tok.Type == TokenType.PLUSMINUS + tok = scanner.LookAhead(TokenType.CHOOSE, TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule + if (tok.Type == TokenType.CHOOSE + || tok.Type == TokenType.PLUSMINUS || tok.Type == TokenType.NOT || tok.Type == TokenType.DEFINED || tok.Type == TokenType.INTEGER @@ -2674,9 +2675,12 @@ private void Parseexpr(ParseNode parent) // NonTerminalSymbol: expr ParseNode node = parent.CreateNode(scanner.GetToken(TokenType.expr), "expr"); parent.Nodes.Add(node); - tok = scanner.LookAhead(TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Choice Rule + tok = scanner.LookAhead(TokenType.CHOOSE, TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Choice Rule switch (tok.Type) { // Choice Rule + case TokenType.CHOOSE: + Parseternary_expr(node); // NonTerminal Rule: ternary_expr + break; case TokenType.PLUSMINUS: case TokenType.NOT: case TokenType.DEFINED: @@ -2693,13 +2697,63 @@ private void Parseexpr(ParseNode parent) // NonTerminalSymbol: expr Parseinstruction_block(node); // NonTerminal Rule: instruction_block break; default: - tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected PLUSMINUS, NOT, DEFINED, INTEGER, DOUBLE, TRUEFALSE, IDENTIFIER, FILEIDENT, BRACKETOPEN, STRING, or CURLYOPEN.", 0x0002, tok)); + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected CHOOSE, PLUSMINUS, NOT, DEFINED, INTEGER, DOUBLE, TRUEFALSE, IDENTIFIER, FILEIDENT, BRACKETOPEN, STRING, or CURLYOPEN.", 0x0002, tok)); break; } // Choice Rule parent.Token.UpdateRange(node.Token); } // NonTerminalSymbol: expr + private void Parseternary_expr(ParseNode parent) // NonTerminalSymbol: ternary_expr + { + Token tok; + ParseNode n; + ParseNode node = parent.CreateNode(scanner.GetToken(TokenType.ternary_expr), "ternary_expr"); + parent.Nodes.Add(node); + + + // Concat Rule + tok = scanner.Scan(TokenType.CHOOSE); // Terminal Rule: CHOOSE + n = node.CreateNode(tok, tok.ToString() ); + node.Token.UpdateRange(tok); + node.Nodes.Add(n); + if (tok.Type != TokenType.CHOOSE) { + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected " + TokenType.CHOOSE.ToString(), 0x1001, tok)); + return; + } + + // Concat Rule + Parseexpr(node); // NonTerminal Rule: expr + + // Concat Rule + tok = scanner.Scan(TokenType.IF); // Terminal Rule: IF + n = node.CreateNode(tok, tok.ToString() ); + node.Token.UpdateRange(tok); + node.Nodes.Add(n); + if (tok.Type != TokenType.IF) { + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected " + TokenType.IF.ToString(), 0x1001, tok)); + return; + } + + // Concat Rule + Parseexpr(node); // NonTerminal Rule: expr + + // Concat Rule + tok = scanner.Scan(TokenType.ELSE); // Terminal Rule: ELSE + n = node.CreateNode(tok, tok.ToString() ); + node.Token.UpdateRange(tok); + node.Nodes.Add(n); + if (tok.Type != TokenType.ELSE) { + tree.Errors.Add(new ParseError("Unexpected token '" + tok.Text.Replace("\n", "") + "' found. Expected " + TokenType.ELSE.ToString(), 0x1001, tok)); + return; + } + + // Concat Rule + Parseexpr(node); // NonTerminal Rule: expr + + parent.Token.UpdateRange(node.Token); + } // NonTerminalSymbol: ternary_expr + private void Parseor_expr(ParseNode parent) // NonTerminalSymbol: or_expr { Token tok; @@ -3103,8 +3157,9 @@ private void Parsefunction_trailer(ParseNode parent) // NonTerminalSymbol: funct } // Concat Rule - tok = scanner.LookAhead(TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule - if (tok.Type == TokenType.PLUSMINUS + tok = scanner.LookAhead(TokenType.CHOOSE, TokenType.PLUSMINUS, TokenType.NOT, TokenType.DEFINED, TokenType.INTEGER, TokenType.DOUBLE, TokenType.TRUEFALSE, TokenType.IDENTIFIER, TokenType.FILEIDENT, TokenType.BRACKETOPEN, TokenType.STRING, TokenType.CURLYOPEN); // Option Rule + if (tok.Type == TokenType.CHOOSE + || tok.Type == TokenType.PLUSMINUS || tok.Type == TokenType.NOT || tok.Type == TokenType.DEFINED || tok.Type == TokenType.INTEGER diff --git a/src/kOS.Safe/Compilation/KS/Scanner.cs b/src/kOS.Safe/Compilation/KS/Scanner.cs index 57f7dfaee..4b47fad08 100644 --- a/src/kOS.Safe/Compilation/KS/Scanner.cs +++ b/src/kOS.Safe/Compilation/KS/Scanner.cs @@ -280,6 +280,10 @@ public Scanner() Patterns.Add(TokenType.UNSET, regex); Tokens.Add(TokenType.UNSET); + regex = new Regex(@"\G(?:choose\b)"); + Patterns.Add(TokenType.CHOOSE, regex); + Tokens.Add(TokenType.CHOOSE); + regex = new Regex(@"\G(?:\()"); Patterns.Add(TokenType.BRACKETOPEN, regex); Tokens.Add(TokenType.BRACKETOPEN); @@ -576,109 +580,111 @@ public enum TokenType unset_stmt= 46, arglist = 47, expr = 48, - or_expr = 49, - and_expr= 50, - compare_expr= 51, - arith_expr= 52, - multdiv_expr= 53, - unary_expr= 54, - factor = 55, - suffix = 56, - suffix_trailer= 57, - suffixterm= 58, - suffixterm_trailer= 59, - function_trailer= 60, - array_trailer= 61, - atom = 62, - sci_number= 63, - number = 64, - varidentifier= 65, - identifier_led_stmt= 66, - identifier_led_expr= 67, + ternary_expr= 49, + or_expr = 50, + and_expr= 51, + compare_expr= 52, + arith_expr= 53, + multdiv_expr= 54, + unary_expr= 55, + factor = 56, + suffix = 57, + suffix_trailer= 58, + suffixterm= 59, + suffixterm_trailer= 60, + function_trailer= 61, + array_trailer= 62, + atom = 63, + sci_number= 64, + number = 65, + varidentifier= 66, + identifier_led_stmt= 67, + identifier_led_expr= 68, //Terminal tokens: - PLUSMINUS= 68, - MULT = 69, - DIV = 70, - POWER = 71, - E = 72, - NOT = 73, - AND = 74, - OR = 75, - TRUEFALSE= 76, - COMPARATOR= 77, - SET = 78, - TO = 79, - IS = 80, - IF = 81, - ELSE = 82, - UNTIL = 83, - STEP = 84, - DO = 85, - LOCK = 86, - UNLOCK = 87, - PRINT = 88, - AT = 89, - ON = 90, - TOGGLE = 91, - WAIT = 92, - WHEN = 93, - THEN = 94, - OFF = 95, - STAGE = 96, - CLEARSCREEN= 97, - ADD = 98, - REMOVE = 99, - LOG = 100, - BREAK = 101, - PRESERVE= 102, - DECLARE = 103, - DEFINED = 104, - LOCAL = 105, - GLOBAL = 106, - PARAMETER= 107, - FUNCTION= 108, - RETURN = 109, - SWITCH = 110, - COPY = 111, - FROM = 112, - RENAME = 113, - VOLUME = 114, - FILE = 115, - DELETE = 116, - EDIT = 117, - RUN = 118, - RUNPATH = 119, - RUNONCEPATH= 120, - ONCE = 121, - COMPILE = 122, - LIST = 123, - REBOOT = 124, - SHUTDOWN= 125, - FOR = 126, - UNSET = 127, - BRACKETOPEN= 128, - BRACKETCLOSE= 129, - CURLYOPEN= 130, - CURLYCLOSE= 131, - SQUAREOPEN= 132, - SQUARECLOSE= 133, - COMMA = 134, - COLON = 135, - IN = 136, - ARRAYINDEX= 137, - ALL = 138, - IDENTIFIER= 139, - FILEIDENT= 140, - INTEGER = 141, - DOUBLE = 142, - STRING = 143, - EOI = 144, - ATSIGN = 145, - LAZYGLOBAL= 146, - EOF = 147, - WHITESPACE= 148, - COMMENTLINE= 149 + PLUSMINUS= 69, + MULT = 70, + DIV = 71, + POWER = 72, + E = 73, + NOT = 74, + AND = 75, + OR = 76, + TRUEFALSE= 77, + COMPARATOR= 78, + SET = 79, + TO = 80, + IS = 81, + IF = 82, + ELSE = 83, + UNTIL = 84, + STEP = 85, + DO = 86, + LOCK = 87, + UNLOCK = 88, + PRINT = 89, + AT = 90, + ON = 91, + TOGGLE = 92, + WAIT = 93, + WHEN = 94, + THEN = 95, + OFF = 96, + STAGE = 97, + CLEARSCREEN= 98, + ADD = 99, + REMOVE = 100, + LOG = 101, + BREAK = 102, + PRESERVE= 103, + DECLARE = 104, + DEFINED = 105, + LOCAL = 106, + GLOBAL = 107, + PARAMETER= 108, + FUNCTION= 109, + RETURN = 110, + SWITCH = 111, + COPY = 112, + FROM = 113, + RENAME = 114, + VOLUME = 115, + FILE = 116, + DELETE = 117, + EDIT = 118, + RUN = 119, + RUNPATH = 120, + RUNONCEPATH= 121, + ONCE = 122, + COMPILE = 123, + LIST = 124, + REBOOT = 125, + SHUTDOWN= 126, + FOR = 127, + UNSET = 128, + CHOOSE = 129, + BRACKETOPEN= 130, + BRACKETCLOSE= 131, + CURLYOPEN= 132, + CURLYCLOSE= 133, + SQUAREOPEN= 134, + SQUARECLOSE= 135, + COMMA = 136, + COLON = 137, + IN = 138, + ARRAYINDEX= 139, + ALL = 140, + IDENTIFIER= 141, + FILEIDENT= 142, + INTEGER = 143, + DOUBLE = 144, + STRING = 145, + EOI = 146, + ATSIGN = 147, + LAZYGLOBAL= 148, + EOF = 149, + WHITESPACE= 150, + COMMENTLINE= 151 } public class Token diff --git a/src/kOS.Safe/Compilation/KS/kRISC.tpg b/src/kOS.Safe/Compilation/KS/kRISC.tpg index 654531f48..7c4c10978 100644 --- a/src/kOS.Safe/Compilation/KS/kRISC.tpg +++ b/src/kOS.Safe/Compilation/KS/kRISC.tpg @@ -65,6 +65,7 @@ REBOOT -> @"reboot\b"; SHUTDOWN -> @"shutdown\b"; FOR -> @"for\b"; UNSET -> @"unset\b"; +CHOOSE -> @"choose\b"; //Generic BRACKETOPEN -> @"\("; @@ -207,7 +208,8 @@ unset_stmt -> UNSET (IDENTIFIER | ALL) EOI; // ---------- expressions --------------------------- arglist -> expr (COMMA expr)*; -expr -> (or_expr|instruction_block); +expr -> (ternary_expr|or_expr|instruction_block); +ternary_expr -> CHOOSE expr IF expr ELSE expr; or_expr -> and_expr (OR and_expr)*; and_expr -> compare_expr (AND compare_expr)*; compare_expr -> arith_expr (COMPARATOR arith_expr)*;