diff --git a/kerboscript_tests/lex_suffix_test1.ks b/kerboscript_tests/lex_suffix_test1.ks new file mode 100644 index 000000000..035e57ab1 --- /dev/null +++ b/kerboscript_tests/lex_suffix_test1.ks @@ -0,0 +1,37 @@ +local my_lex is lexicon( + "Key0" , "A", + "Key1" , "B", + "Key2" , "C", + "Key3" , "D", + "Key4" , "E", + "Key5" , "F"). + +print "Expect: ABCDEF". +print "Actual: " + concat_lex(my_lex). + +set my_lex:key1 to "_". +set my_lex:key3 to "_". +set my_lex:key5 to "_". + +print "Expect: A_C_E_". +print "Actual: " + concat_lex(my_lex). + +set my_lex:key1 to { return "%". }. +set my_lex:key3 to { local a is 1. local b is 4. return a+b. }. +set my_lex:key5 to { return 3/2. }. + +print "Expect: A%C5E1.5". +print "Actual: " + concat_lex(my_lex). + +function concat_lex { + parameter the_lex. + + local str is "". + for key in the_lex:keys { + if the_lex[key]:istype("Delegate") + set str to str + the_lex[key](). + else + set str to str + the_lex[key]. + } + return str. +} diff --git a/kerboscript_tests/lex_suffix_test2.ks b/kerboscript_tests/lex_suffix_test2.ks new file mode 100644 index 000000000..bc3b99df2 --- /dev/null +++ b/kerboscript_tests/lex_suffix_test2.ks @@ -0,0 +1,39 @@ +print "Testing using Lex as a psuedo-class.". +print "------------------------------------". + + +print "Making 'fred', an instance of person'.". +local fred is construct_person("Fred", 23). +print "fred:greet() prints this:". +print fred:greet(). + +print "Making 'henri', an instance of frenchperson.". +local henri is construct_frenchperson("Henri", 19). +print henri:greet(). + +function construct_person { + parameter name, age. + + local myself is LEX(). + + set myself:name to name. + set myself:age to age. + set myself:greet to { + return "Hello, my name is " + myself:name +" and I am " + myself:age + " years old.". + }. + + return myself. +}. + +function construct_frenchperson { + parameter name, age. + + local myself is construct_person(name, age). + + // This is sort of like overriding a method: + set myself:greet to { + return "Bonjour, Je m'appelle " + myself:name + " et j'ai " + myself:age + " ans.". + }. + + return myself. +} diff --git a/kerboscript_tests/lex_suffix_test3.ks b/kerboscript_tests/lex_suffix_test3.ks new file mode 100644 index 000000000..b68893677 --- /dev/null +++ b/kerboscript_tests/lex_suffix_test3.ks @@ -0,0 +1,42 @@ +local empty_lex is lexicon(). +local populated_lex is lexicon( + "key0", 0, // valid suffix name + "key1", 10, // valid suffix name + "key2", 20, // valid suffix name + "KEY3", 30, // valid suffix name + // None of the following 3 should be valid suffix names because of the + // spaces: + " SpaceBefore", "----", + "SpaceAfter ", "----", + "Space Between", "----", + V(1,0,0), "----", // not a valid suffix name because Vectors aren't strings. + "zzzzzz", 9999 // valid suffix name + ). +print "Expect: True". +print "Actual: " + empty_lex:hassuffix("add"). // built-in suffix for all lex's. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:hassuffix("add"). // built-in suffix for all lex's. +print " ". +print "Expect: False". +print "Actual: " + empty_lex:hassuffix("key2"). // only exists if key2 is in the lex. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:hassuffix("key2"). // only exists if key2 is in the lex. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:hassuffix("kEy3"). // case insensitive check. +print " ". +print "Expect: True". +print "Actual: " + populated_lex:haskey("Space Between"). // key exists +print " ". +print "Expect: False". +print "Actual: " + populated_lex:hassuffix("Space Between"). // but isn't a valid suffix name + +// There should be 5 more suffixes in the populated list than in default lex's, +// because 5 of the keys in it form valid identifier strings: +// If this is more than 5, then keys that shouldn't be +// suffixes are getting into the list. +print "Expect: 5". +print "Actual: " + (populated_lex:suffixnames:length - empty_lex:suffixnames:length). + diff --git a/src/kOS.Safe.Test/Execution/Config.cs b/src/kOS.Safe.Test/Execution/Config.cs index 39c57a3f4..4294aeeac 100644 --- a/src/kOS.Safe.Test/Execution/Config.cs +++ b/src/kOS.Safe.Test/Execution/Config.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using System; using System.Collections.Generic; @@ -212,7 +212,7 @@ public IList GetConfigKeys() return new List(); } - public ISuffixResult GetSuffix(string suffixName) + public ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { throw new NotImplementedException(); } @@ -222,7 +222,7 @@ public void SaveConfig() throw new NotImplementedException(); } - public bool SetSuffix(string suffixName, object value) + public bool SetSuffix(string suffixName, object value, bool failOkay = false) { throw new NotImplementedException(); } diff --git a/src/kOS.Safe/Compilation/KS/Compiler.cs b/src/kOS.Safe/Compilation/KS/Compiler.cs index 827bdb339..1d7094273 100644 --- a/src/kOS.Safe/Compilation/KS/Compiler.cs +++ b/src/kOS.Safe/Compilation/KS/Compiler.cs @@ -1387,7 +1387,7 @@ private void VisitTernary(ParseNode node) /// Handles the short-circuit logic of boolean OR and boolean AND /// chains. It is like VisitExpressionChain (see elsewhere) but /// in this case it has the special logic to short circuit and skip - /// executing the righthand expression if it can. (The generic VisitExpressionXhain + /// executing the righthand expression if it can. (The generic VisitExpressionChain /// always evaluates both the left and right sides of the operator first, then /// does the operation). /// diff --git a/src/kOS.Safe/Compilation/KS/KSScript.cs b/src/kOS.Safe/Compilation/KS/KSScript.cs index 4fee5b92b..e719484fb 100644 --- a/src/kOS.Safe/Compilation/KS/KSScript.cs +++ b/src/kOS.Safe/Compilation/KS/KSScript.cs @@ -1,5 +1,6 @@ using kOS.Safe.Exceptions; using System.Collections.Generic; +using System.Text.RegularExpressions; using kOS.Safe.Persistence; namespace kOS.Safe.Compilation.KS diff --git a/src/kOS.Safe/Compilation/KS/kRISC.tpg b/src/kOS.Safe/Compilation/KS/kRISC.tpg index 7c4c10978..c3c6d098f 100644 --- a/src/kOS.Safe/Compilation/KS/kRISC.tpg +++ b/src/kOS.Safe/Compilation/KS/kRISC.tpg @@ -79,7 +79,12 @@ COLON -> @":"; IN -> @"in\b"; ARRAYINDEX -> @"#"; ALL -> @"all\b"; -IDENTIFIER -> @"[_\p{L}]\w*"; + +// WARNING - IF YOU EDIT THE REGEX FOR IDENTIFIER ON THE NEXT LINE, +// THEN ALSO EDIT kOS.Safe.Utilities.StringUtil.IsValidIdentifier() +// TO USE THE SAME REGEX !!!!! +IDENTIFIER -> @"[_\p{L}]\w*"; //<---- Important - see above Comment!!!!! + FILEIDENT -> @"[_\p{L}]\w*(\.[_\p{L}]\w*)*"; INTEGER -> @"\d[_\d]*"; DOUBLE -> @"(\d+(?:_\d*)*)?\.\d+(?:_\d*)*"; diff --git a/src/kOS.Safe/Encapsulation/ISuffixed.cs b/src/kOS.Safe/Encapsulation/ISuffixed.cs index 1f781550a..0cc669b82 100644 --- a/src/kOS.Safe/Encapsulation/ISuffixed.cs +++ b/src/kOS.Safe/Encapsulation/ISuffixed.cs @@ -1,10 +1,10 @@ -using kOS.Safe.Encapsulation.Suffixes; +using kOS.Safe.Encapsulation.Suffixes; namespace kOS.Safe.Encapsulation { public interface ISuffixed { - bool SetSuffix(string suffixName, object value); - ISuffixResult GetSuffix(string suffixName); + bool SetSuffix(string suffixName, object value, bool failOkay = false); + ISuffixResult GetSuffix(string suffixName, bool failOkay = false); } } \ No newline at end of file diff --git a/src/kOS.Safe/Encapsulation/Lexicon.cs b/src/kOS.Safe/Encapsulation/Lexicon.cs index 623a30857..e62128105 100644 --- a/src/kOS.Safe/Encapsulation/Lexicon.cs +++ b/src/kOS.Safe/Encapsulation/Lexicon.cs @@ -63,11 +63,13 @@ public int GetHashCode(TI obj) } private IDictionary internalDictionary; + private IDictionary> keySuffixes; private bool caseSensitive; public Lexicon() { internalDictionary = new Dictionary(new LexiconComparer()); + keySuffixes = new Dictionary>(new LexiconComparer()); caseSensitive = false; InitalizeSuffixes(); } @@ -137,6 +139,13 @@ private void SetCaseSensitivity(BooleanValue value) internalDictionary = newCase ? new Dictionary() : new Dictionary(new LexiconComparer()); + + // Regardless of whether or not the lexicon itself is case sensitive, + // the key Suffixes have to be IN-sensitive because they are getting + // values who's case got squashed by the compiler. This needs to + // be documented well in the user docs (i.e. using the suffix syntax + // cannot detect the difference between keys that differ only in case). + keySuffixes = new Dictionary>(new LexiconComparer()); } private BooleanValue HasValue(Structure value) @@ -293,6 +302,98 @@ public override string ToString() return new SafeSerializationMgr(null).ToString(this); } + // Try to call the normal SetSuffix that all structures do, but if that fails, + // then try to use this suffix name as a key and set the value in the lexicon + // at that key. This can insert new key values in the lexicon, just like + // doing `set x["foo"] to y.` can. + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) + { + if (base.SetSuffix(suffixName, value, true)) + return true; + + // If the above fails, then fallback on the key technique: + internalDictionary[new StringValue(suffixName)] = FromPrimitiveWithAssert(value); + return true; + } + + // Try to get the suffix the normal way that all structures do, but if + // that fails, then try to get the value in the lexicon who's key is + // this suffix name. (This implements using keys with the "colon" suffix + // syntax for issue #2551.) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) + { + ISuffixResult baseResult = base.GetSuffix(suffixName, true); + if (baseResult != null) + return baseResult; + + // If the above fails, but this suffix IS the name of a key in the + // dictionary, then try to use the key-suffix we made earlier + // (or make a new one and use it now) + // --------------------------------------------------------------- + + StringValue suffixAsStruct = new StringValue(suffixName); + + if (internalDictionary.ContainsKey(suffixAsStruct)) // even if keySuffixes has the value, it doesn't count if the key isn't there anymore. + { + SetSuffix theSuffix; + if (keySuffixes.TryGetValue(suffixAsStruct, out theSuffix)) + { + return theSuffix.Get(); + } + else // make a new suffix then since this is the first time it got mentioned this way: + { + theSuffix = new SetSuffix(() => internalDictionary[suffixAsStruct], value => internalDictionary[suffixAsStruct] = value); + keySuffixes.Add(suffixAsStruct, theSuffix); + return theSuffix.Get(); + } + } + else + { + // This will error out, but we may as well also remove this key + // from the list of suffixes: + keySuffixes.Remove(suffixAsStruct); + + if (failOkay) + return null; + else + throw new KOSSuffixUseException("get", suffixName, this); + } + } + + public override BooleanValue HasSuffix(StringValue suffixName) + { + if (base.HasSuffix(suffixName)) + return true; + if (internalDictionary.ContainsKey(suffixName)) + { + // It can only be a suffix if it is a valid identifier pattern, else the + // parser won't let the colon suffix syntax see it to pass it to GetSuffix() + // or SetSuffix(): + return StringUtil.IsValidIdentifier(suffixName); + } + return false; + } + + /// + /// Like normal Structure.GetSuffixNames except it also adds all + /// the keys that would validly work with the colon suffix syntax + /// to the list. + /// + /// + public override ListValue GetSuffixNames() + { + ListValue theList = base.GetSuffixNames(); + + foreach (Structure key in internalDictionary.Keys) + { + StringValue keyStr = key as StringValue; + if (keyStr != null && StringUtil.IsValidIdentifier(keyStr)) + { + theList.Add(keyStr); + } + } + return new ListValue(theList.OrderBy(item => item.ToString())); + } public override Dump Dump() { var result = new DumpWithHeader diff --git a/src/kOS.Safe/Encapsulation/Structure.cs b/src/kOS.Safe/Encapsulation/Structure.cs index fd8d61dec..302749158 100644 --- a/src/kOS.Safe/Encapsulation/Structure.cs +++ b/src/kOS.Safe/Encapsulation/Structure.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Text; using System.Collections.Generic; @@ -130,19 +130,28 @@ private static IDictionary GetStaticSuffixesForType(Type curren } } - public virtual bool SetSuffix(string suffixName, object value) + /// + /// Set a suffix of this structure that has suffixName to the given value. + /// If failOkay is false then it will throw exception if it fails to find the suffix. + /// If failOkay is true then it will continue happily if it fails to find the suffix. + /// + /// + /// + /// + /// false if failOkay was true and it failed to find the suffix + public virtual bool SetSuffix(string suffixName, object value, bool failOkay = false) { callInitializeSuffixes(); var suffixes = GetStaticSuffixesForType(GetType()); - if (!ProcessSetSuffix(suffixes, suffixName, value)) + if (!ProcessSetSuffix(suffixes, suffixName, value, failOkay)) { - return ProcessSetSuffix(instanceSuffixes, suffixName, value); + return ProcessSetSuffix(instanceSuffixes, suffixName, value, failOkay); } return false; } - private bool ProcessSetSuffix(IDictionary suffixes, string suffixName, object value) + private bool ProcessSetSuffix(IDictionary suffixes, string suffixName, object value, bool failOkay = false) { ISuffix suffix; if (suffixes.TryGetValue(suffixName, out suffix)) @@ -153,12 +162,22 @@ private bool ProcessSetSuffix(IDictionary suffixes, string suff settable.Set(value); return true; } - throw new KOSSuffixUseException("set", suffixName, this); + if (failOkay) + return false; + else + throw new KOSSuffixUseException("set", suffixName, this); } return false; } - public virtual ISuffixResult GetSuffix(string suffixName) + /// + /// Get the suffix with this name, or if it fails to find it, then either + /// throw exception or merely return null. (Will return null only if failOkay is true). + /// + /// + /// + /// + public virtual ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { callInitializeSuffixes(); ISuffix suffix; @@ -171,7 +190,10 @@ public virtual ISuffixResult GetSuffix(string suffixName) if (!suffixes.TryGetValue(suffixName, out suffix)) { - throw new KOSSuffixUseException("get",suffixName,this); + if (failOkay) + return null; + else + throw new KOSSuffixUseException("get",suffixName,this); } return suffix.Get(); } diff --git a/src/kOS.Safe/Utilities/StringUtil.cs b/src/kOS.Safe/Utilities/StringUtil.cs index 8602d1a89..c587a8ed4 100644 --- a/src/kOS.Safe/Utilities/StringUtil.cs +++ b/src/kOS.Safe/Utilities/StringUtil.cs @@ -1,4 +1,6 @@ -using System; +using System; +using System.Text.RegularExpressions; + namespace kOS.Safe { /// @@ -6,6 +8,11 @@ namespace kOS.Safe /// public static class StringUtil { + // The IDENTIFIER Regex Pattern is taken directly from kRISC.tpg - if it changes there, it should change here too. + // (It's messy to actually use the pattern directly from Scanner.cs because that requires an instance + // of SharedObjects to get an instance of the compiler.) + private static Regex identifierPattern = new Regex(@"\G(?:[_\p{L}]\w*)"); + public static bool EndsWith(string str, string suffix) { int strLen = str.Length; @@ -48,5 +55,16 @@ public static bool StartsWith(string str, string prefix) return true; } + + public static bool IsValidIdentifier(string str) + { + Match match = identifierPattern.Match(str); + + // Only counts as a valid identifier if the entire string matched without + // any leftover characters at the end of it: + if (match.Success && match.Length == str.Length) + return true; + return false; + } } } diff --git a/src/kOS/Suffixed/BodyTarget.cs b/src/kOS/Suffixed/BodyTarget.cs index d6df150dd..c13767410 100644 --- a/src/kOS/Suffixed/BodyTarget.cs +++ b/src/kOS/Suffixed/BodyTarget.cs @@ -246,10 +246,10 @@ public double GetDistance() return Vector3d.Distance(Shared.Vessel.CoMD, Body.position) - Body.Radius; } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay) { if (Target == null) throw new Exception("BODY structure appears to be empty!"); - return base.GetSuffix(suffixName); + return base.GetSuffix(suffixName, failOkay); } public override string ToString() diff --git a/src/kOS/Suffixed/Config.cs b/src/kOS/Suffixed/Config.cs index e132a59e2..b0156ac9a 100644 --- a/src/kOS/Suffixed/Config.cs +++ b/src/kOS/Suffixed/Config.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using kOS.Safe.Encapsulation; @@ -149,7 +149,7 @@ private void SaveConfigKey(ConfigKey key, PluginConfiguration config) config.SetValue(key.StringKey, keys[key.StringKey.ToUpper()].Value); } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { ConfigKey key = null; @@ -162,10 +162,18 @@ public override ISuffixResult GetSuffix(string suffixName) key = alias[suffixName]; } - return key != null ? new SuffixResult(FromPrimitiveWithAssert(key.Value)) : base.GetSuffix(suffixName); + return key != null ? new SuffixResult(FromPrimitiveWithAssert(key.Value)) : base.GetSuffix(suffixName, failOkay); } - public override bool SetSuffix(string suffixName, object value) + /// + /// same as Structure.SetSuffix, but it has the extra logic to alter the config keys + /// that the game auto-saves every so often. + /// + /// + /// + /// + /// + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) { ConfigKey key = null; @@ -178,7 +186,7 @@ public override bool SetSuffix(string suffixName, object value) key = alias[suffixName]; } - if (key == null) return base.SetSuffix(suffixName, value); + if (key == null) return base.SetSuffix(suffixName, value, failOkay); if (value.GetType() == key.ValType) { diff --git a/src/kOS/Suffixed/FlightControl.cs b/src/kOS/Suffixed/FlightControl.cs index a81a459a7..9af551a58 100644 --- a/src/kOS/Suffixed/FlightControl.cs +++ b/src/kOS/Suffixed/FlightControl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Collections.Generic; using kOS.AddOns.RemoteTech; @@ -54,7 +54,7 @@ public FlightControl(Vessel vessel) public Vessel Vessel { get; private set; } - public override bool SetSuffix(string suffixName, object value) + public override bool SetSuffix(string suffixName, object value, bool failOkay = false) { float floatValue = 0; Vector vectorValue = null; diff --git a/src/kOS/Suffixed/StageValues.cs b/src/kOS/Suffixed/StageValues.cs index a1303520f..964b61cbb 100644 --- a/src/kOS/Suffixed/StageValues.cs +++ b/src/kOS/Suffixed/StageValues.cs @@ -75,12 +75,12 @@ private Lexicon GetResourceDictionary() return resLex; } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { string fixedName; if (!Utils.IsResource(suffixName, out fixedName)) { - return base.GetSuffix(suffixName); + return base.GetSuffix(suffixName, failOkay); } double resourceAmount = GetResourceOfCurrentStage(fixedName); diff --git a/src/kOS/Suffixed/VesselTarget.cs b/src/kOS/Suffixed/VesselTarget.cs index c9d28054a..98b8482cd 100644 --- a/src/kOS/Suffixed/VesselTarget.cs +++ b/src/kOS/Suffixed/VesselTarget.cs @@ -400,7 +400,7 @@ private void StartTracking() } } - public override ISuffixResult GetSuffix(string suffixName) + public override ISuffixResult GetSuffix(string suffixName, bool failOkay = false) { // Most suffixes are handled by the newer AddSuffix system, except for the // resource levels, which have to use this older technique as a fallback because @@ -413,7 +413,7 @@ public override ISuffixResult GetSuffix(string suffixName) return new SuffixResult(ScalarValue.Create(dblValue)); } - return base.GetSuffix(suffixName); + return base.GetSuffix(suffixName, failOkay); } protected bool Equals(VesselTarget other)