diff --git a/AspectedRouting/AspectedRouting.csproj b/AspectedRouting/AspectedRouting.csproj index 700b7ec..5f918b6 100644 --- a/AspectedRouting/AspectedRouting.csproj +++ b/AspectedRouting/AspectedRouting.csproj @@ -12,6 +12,9 @@ PreserveNewest + + PreserveNewest + diff --git a/AspectedRouting/IO/LuaSkeleton/LuaParameterPrinter.Parameters.cs b/AspectedRouting/IO/LuaSkeleton/LuaParameterPrinter.Parameters.cs new file mode 100644 index 0000000..cc2db70 --- /dev/null +++ b/AspectedRouting/IO/LuaSkeleton/LuaParameterPrinter.Parameters.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using AspectedRouting.Language; +using AspectedRouting.Language.Expression; + +namespace AspectedRouting.IO.itinero1 +{ + public class LuaParameterPrinter + { + private ProfileMetaData _profile; + private LuaSkeleton.LuaSkeleton _skeleton; + + public LuaParameterPrinter(ProfileMetaData profile, LuaSkeleton.LuaSkeleton skeleton) + { + _profile = profile; + _skeleton = skeleton; + } + + + public string GenerateDefaultParameters() + { + var impl = new List() + { + "function default_parameters()", + " local parameters = {}", + DeclareParametersFor(_profile.DefaultParameters), + " return parameters", + "end" + }; + + return string.Join("\n",impl); + } + + /// + /// Generates a piece of code of the following format: + /// + /// parameters["x"] = a; + /// parameters["y"] = b: + /// ... + /// + /// Where x=a and y=b are defined in the profile + /// + /// Dependencies are added. + /// + /// Note that the caller should still add `local paramaters = default_parameters()` + /// + /// + /// + /// + public string DeclareParametersFor(Dictionary subParams) + { + var impl = ""; + foreach (var (paramName, value) in subParams) + { + if (paramName.Equals("description")) + { + continue; + } + + impl += $" parameters.{paramName.TrimStart('#').AsLuaIdentifier()} = {_skeleton.ToLua(value)}\n"; + } + + return impl; + } + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/Luaprinter.Expressions.cs b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs similarity index 95% rename from AspectedRouting/IO/itinero1/Luaprinter.Expressions.cs rename to AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs index 05befa5..aab59d6 100644 --- a/AspectedRouting/IO/itinero1/Luaprinter.Expressions.cs +++ b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Expressions.cs @@ -2,18 +2,20 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using AspectedRouting.IO.itinero1; using AspectedRouting.Language; using AspectedRouting.Language.Expression; using AspectedRouting.Language.Functions; using AspectedRouting.Language.Typ; using static AspectedRouting.Language.Deconstruct; -namespace AspectedRouting.IO.itinero1 +namespace AspectedRouting.IO.LuaSkeleton { - public partial class LuaPrinter + public partial class LuaSkeleton { - private string ToLua(IExpression bare, string key = "nil") + internal string ToLua(IExpression bare, string key = "nil") { + var collectedMapping = new List(); var order = new List(); @@ -63,6 +65,9 @@ namespace AspectedRouting.IO.itinero1 var func = new List(); + + + if ( UnApply( UnApply(IsFunc(Funcs.Dot), Assign(func)), @@ -155,7 +160,7 @@ namespace AspectedRouting.IO.itinero1 } AddFunction(called); - return $"{fc.CalledFunctionName.FunctionName()}(parameters, tags, result)"; + return $"{fc.CalledFunctionName.AsLuaIdentifier()}(parameters, tags, result)"; case Constant c: return ConstantToLua(c); case Mapping m: @@ -184,7 +189,7 @@ namespace AspectedRouting.IO.itinero1 return ToLua(collected.First(), key); case Parameter p: - return $"parameters[\"{p.ParamName.FunctionName()}\"]"; + return $"parameters[\"{p.ParamName.AsLuaIdentifier()}\"]"; default: throw new Exception("Could not convert " + bare + " to a lua expression"); } diff --git a/AspectedRouting/IO/itinero1/Luaprinter.Aspect.cs b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Function.cs similarity index 76% rename from AspectedRouting/IO/itinero1/Luaprinter.Aspect.cs rename to AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Function.cs index f9bd0e1..3e616db 100644 --- a/AspectedRouting/IO/itinero1/Luaprinter.Aspect.cs +++ b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.Function.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Linq; +using AspectedRouting.IO.itinero1; using AspectedRouting.Language; using AspectedRouting.Language.Expression; -namespace AspectedRouting.IO.itinero1 +namespace AspectedRouting.IO.LuaSkeleton { - public partial class LuaPrinter + public partial class LuaSkeleton { - private readonly HashSet _alreadyAddedFunctions = new HashSet(); public void AddFunction(AspectMetadata meta) { @@ -31,7 +31,7 @@ namespace AspectedRouting.IO.itinero1 { if (e is Function f && f.Name.Equals(Funcs.MemberOf.Name)) { - funcNameDeclaration = $"\n local funcName = \"{meta.Name.FunctionName()}\""; + funcNameDeclaration = $"\n local funcName = \"{meta.Name.AsLuaIdentifier()}\""; } return true; @@ -49,16 +49,12 @@ namespace AspectedRouting.IO.itinero1 "Number of combintations: " + numberOfCombinations, "Returns values: ", "]]", - "function " + meta.Name.FunctionName() + "(parameters, tags, result)" + funcNameDeclaration, + "function " + meta.Name.AsLuaIdentifier() + "(parameters, tags, result)" + funcNameDeclaration, " return " + ToLua(meta.ExpressionImplementation), "end" ); - _code.Add(impl); - foreach (var k in possibleTags.Keys) - { - _neededKeys.Add(k); // To generate a whitelist of OSM-keys that should be kept - } + _functionImplementations.Add(impl); } } } \ No newline at end of file diff --git a/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.cs b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.cs new file mode 100644 index 0000000..2a5707e --- /dev/null +++ b/AspectedRouting/IO/LuaSkeleton/LuaSkeleton.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.IO; +using AspectedRouting.Language; + +namespace AspectedRouting.IO.LuaSkeleton +{ + + /// + /// The 'LuaSkeleton' is a class which is used in Lua generation of profiles. + /// + /// The lua skeleton basically keeps track of dependencies, and added functions. + /// Once done, all these can be retrieved as code. + /// + /// E.g. if an expression is turned into lua with 'ToExpression', then the dependencies will be automatically added. + /// + /// + public partial class LuaSkeleton + { + private readonly Context _context; + + private readonly HashSet _dependencies = new HashSet(); + private readonly List _functionImplementations = new List(); + private readonly HashSet _alreadyAddedFunctions = new HashSet(); + + public LuaSkeleton(Context context) + { + _context = context; + } + + internal void AddDep(string name) + { + _dependencies.Add(name); + } + + public List GenerateFunctions() + { + return _functionImplementations; + } + + public void AddDependenciesFor(IExpression e) + { + var (_, functionNames) = e.InList().DirectlyAndInderectlyCalled(_context); + foreach (var functionName in functionNames) + { + + if (_context.DefinedFunctions.TryGetValue(functionName, out var aspectemeta)) + { + AddFunction(aspectemeta); + } + else + { + AddDep(functionName); + } + } + } + + public List GenerateDependencies() + { + var imps = new List(); + + foreach (var name in _dependencies) + { + var path = $"IO/lua/{name}.lua"; + if (File.Exists(path)) + { + imps.Add(File.ReadAllText(path)); + } + else + { + throw new FileNotFoundException(path); + } + } + + return imps; + } + + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/LuaStringExtensions.cs b/AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs similarity index 93% rename from AspectedRouting/IO/itinero1/LuaStringExtensions.cs rename to AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs index dc4e2b3..5f28130 100644 --- a/AspectedRouting/IO/itinero1/LuaStringExtensions.cs +++ b/AspectedRouting/IO/LuaSkeleton/LuaStringExtensions.cs @@ -23,7 +23,7 @@ namespace AspectedRouting.IO.itinero1 return "{" + string.Join(", ", contents) + "}"; } - public static string FunctionName(this string s) + public static string AsLuaIdentifier(this string s) { return s.Replace("$", "") .Replace("#", "") diff --git a/AspectedRouting/IO/LuaSkeleton/LuaTestPrinter.cs b/AspectedRouting/IO/LuaSkeleton/LuaTestPrinter.cs new file mode 100644 index 0000000..76f1f1d --- /dev/null +++ b/AspectedRouting/IO/LuaSkeleton/LuaTestPrinter.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using AspectedRouting.Tests; + +namespace AspectedRouting.IO.itinero1 +{ + public class LuaTestPrinter + { + private LuaSkeleton.LuaSkeleton _skeleton; + + public LuaTestPrinter(LuaSkeleton.LuaSkeleton skeleton, List unitTestRunners) + { + _skeleton = skeleton; + unitTestRunners.ForEach(_skeleton.AddDep); + } + + + public string GenerateFullTestSuite(List profileTests, List aspectTests) + { + + _skeleton.AddDep("inv"); + _skeleton.AddDep("double_compare"); + + + var aspectTestSuite = + string.Join("\n\n", aspectTests + .Where(x => x != null) + .Select( + GenerateAspectTestSuite + )); + + var profileTestSuite = + string.Join("\n\n", profileTests + .Where(x => x != null) + .Select( + GenerateProfileTestSuite + )); + + return string.Join("\n\n\n", new List + { + "function test_all()", + " " + aspectTestSuite.Indent(), + " -- Behaviour tests --", + " " + profileTestSuite.Indent(), + "end" + }); + } + + + private string GenerateProfileTestSuite(BehaviourTestSuite testSuite) + { + return string.Join("\n", + testSuite.Tests.Select((test, i) => GenerateProfileUnitTestCall(testSuite, i, test.Item1, test.tags)) + .ToList()); + } + + private string GenerateProfileUnitTestCall(BehaviourTestSuite testSuite, int index, ProfileResult expected, + Dictionary tags) + { + _skeleton.AddDep("debug_table"); + var parameters = new Dictionary(); + + + var keysToCheck = new List(); + foreach (var (key, value) in tags) + { + if (key.StartsWith("#")) + { + parameters[key.TrimStart('#')] = value; + } + + if (key.StartsWith("_relation:")) + { + keysToCheck.Add(key); + } + } + + + foreach (var key in keysToCheck) + { + var newKey = key.Replace(".", "_"); + tags[newKey] = tags[key]; + tags.Remove(key); + } + + foreach (var (paramName, _) in parameters) + { + tags.Remove("#" + paramName); + } + + // Generates something like: + // function unit_test_profile(profile_function, profile_name, index, expected, tags) + return + $"unit_test_profile(behaviour_{testSuite.Profile.Name.AsLuaIdentifier()}_{testSuite.BehaviourName.AsLuaIdentifier()}, " + + $"\"{testSuite.BehaviourName}\", " + + $"{index}, " + + $"{{access = \"{D(expected.Access)}\", speed = {expected.Speed}, oneway = \"{D(expected.Oneway)}\", priority = {expected.Priority} }}, " + + tags.ToLuaTable() + + ")"; + } + + + private string GenerateAspectTestSuite(AspectTestSuite testSuite) + { + var fName = testSuite.FunctionToApply.Name; + var tests = + testSuite.Tests + .Select((test, i) => GenerateAspectUnitTestCall(fName, i, test.expected, test.tags)) + .ToList(); + return string.Join("\n", tests); + } + + /// + /// Generate a unit test call + /// + private string GenerateAspectUnitTestCall(string functionToApplyName, int index, string expected, + Dictionary tags) + { + var parameters = new Dictionary(); + + foreach (var (key, value) in tags) + { + if (key.StartsWith("#")) + { + parameters[key.TrimStart('#')] = value; + } + } + + foreach (var (paramName, _) in parameters) + { + tags.Remove("#" + paramName); + } + + _skeleton.AddDep("unitTest"); + _skeleton.AddDep("debug_table"); + return + $"unit_test({functionToApplyName.AsLuaIdentifier()}, \"{functionToApplyName}\", {index}, \"{expected}\", {parameters.ToLuaTable()}, {tags.ToLuaTable()})"; + } + + + private string D(string s) + { + if (string.IsNullOrEmpty(s)) + { + return "0"; + } + + return s; + } + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/LuaPrinter.cs b/AspectedRouting/IO/itinero1/LuaPrinter.cs deleted file mode 100644 index 13f3dca..0000000 --- a/AspectedRouting/IO/itinero1/LuaPrinter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace AspectedRouting.IO.itinero1 -{ - public partial class LuaPrinter - { - private readonly HashSet _dependencies = new HashSet(); - private readonly HashSet _neededKeys = new HashSet(); - - private readonly List _code = new List(); - private readonly List _tests = new List(); - - /// - /// A dictionary containing the implementation of basic functions - /// - /// - private static IEnumerable LoadFunctions(IEnumerable names) - { - var imps = new List(); - - foreach (var name in names) - { - var path = $"IO/lua/{name}.lua"; - if (File.Exists(path)) - { - imps.Add(File.ReadAllText(path)); - } - else - { - throw new FileNotFoundException(path); - } - } - - return imps; - } - - private void AddDep(string name) - { - _dependencies.Add(name); - } - - - public string ToLua() - { - var deps = _dependencies.ToList(); - deps.Add("unitTestProfile"); - deps.Add("inv"); - deps.Add("double_compare"); - deps.Add("spoken_instructions"); - - var code = new List(); - - code.Add($"-- Itinero 1.0-profile, generated on {DateTime.Now:s}"); - code.Add("\n\n----------------------------- UTILS ---------------------------"); - code.AddRange(LoadFunctions(deps).ToList()); - - code.Add("\n\n----------------------------- PROFILE ---------------------------"); - var keys = _neededKeys.Select(key => "\"" + key + "\""); - code.Add("\n\nprofile_whitelist = {\n " + string.Join("\n , ", keys) + "}"); - - code.AddRange(_code); - - code.Add("\n\n ------------------------------- TESTS -------------------------"); - - code.Add("function test_all()"); - code.Add(string.Join("\n",_tests).Indent()); - code.Add("end"); - - var compatibility = string.Join("\n", - - - "", - "if (itinero == nil) then", - " itinero = {}", - " itinero.log = print", - "", - " -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended", - " runTests = true", - "", - "", - "else", - " print = itinero.log", - "end", - "", - "test_all()", - "if (not failed_tests and not failed_profile_tests) then", - " print(\"Tests OK\")", - "end" - ); - code.Add(compatibility); - - return string.Join("\n\n\n", code); - } - } -} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/LuaPrinter1.MainFunction.cs b/AspectedRouting/IO/itinero1/LuaPrinter1.MainFunction.cs new file mode 100644 index 0000000..ce10936 --- /dev/null +++ b/AspectedRouting/IO/itinero1/LuaPrinter1.MainFunction.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Linq; +using AspectedRouting.Language; +using AspectedRouting.Language.Functions; +using AspectedRouting.Language.Typ; + +namespace AspectedRouting.IO.itinero1 +{ + public partial class LuaPrinter1 + { + + + private string GenerateMainProfileFunction() + { + var impl = string.Join("\n", + "", + "", + "--[[", + _profile.Name, + "This is the main function called to calculate the access, oneway and speed.", + "Comfort is calculated as well, based on the parameters which are padded in", + "", + "Created by " + _profile.Author, + "Originally defined in " + _profile.Filename, + "]]", + "function " + _profile.Name + "(parameters, tags, result)", + "", + " -- initialize the result table on the default values", + " result.access = 0", + " result.speed = 0", + " result.factor = 1", + " result.direction = 0", + " result.canstop = true", + " result.attributes_to_keep = {}", + "", + " local access = " + _skeleton.ToLua(_profile.Access), + " if (access == nil or access == \"no\") then", + " return", + " end", + " tags.access = access", + " local oneway = " + _skeleton.ToLua(_profile.Oneway), + " tags.oneway = oneway", + " local speed = " + _skeleton.ToLua(_profile.Speed), + " tags.speed = speed", + " local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m", + ""); + + impl += + "\n local priority = 0\n "; + + foreach (var (parameterName, expression) in _profile.Priority) + { + var paramInLua = _skeleton.ToLua(new Parameter(parameterName)); + + + var exprInLua = _skeleton.ToLua(expression.Optimize()); + var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First()); + if (subs != null && subs.TryGetValue("$a", out var resultType) && + (resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String))) + { + _skeleton. AddDep("parse"); + exprInLua = "parse(" + exprInLua + ")"; + } + + impl += "\n " + string.Join("\n ", + $"if({paramInLua} ~= 0) then", + $" priority = priority + {paramInLua} * {exprInLua}", + "end" + ); + } + + + impl += string.Join("\n", + "", + "", + " if (priority <= 0) then", + " result.access = 0", + " return", + " end", + "", + " result.access = 1", + " result.speed = speed", + " result.factor = 1 / priority", + "", + " if (oneway == \"both\") then", + " result.direction = 0", + " elseif (oneway == \"with\") then", + " result.direction = 1", + " elseif (oneway == \"against\") then", + " result.direction = 2", + " else", + " error(\"Unexpected value for oneway: \"..oneway)", + " end", + "", + "end" + ); + + return impl; + } + + + private (string functionName, string implementation) GenerateBehaviourFunction( + string behaviourName, + Dictionary behaviourParameters) + { + var referenceName = _profile.Name + "_" + behaviourName; + var functionName = referenceName.AsLuaIdentifier(); + behaviourParameters.TryGetValue("description", out var description); + + _skeleton.AddDep("remove_relation_prefix"); + var impl = string.Join("\n", + "", + "--[[", + description, + "]]", + "function behaviour_" + functionName + "(tags, result)", + $" tags = remove_relation_prefix(tags, \"{behaviourName.AsLuaIdentifier()}\")", + " local parameters = default_parameters()", + " parameters.name = \"" + referenceName + "\"", + "" + ); + + impl += _parameterPrinter.DeclareParametersFor(behaviourParameters); + + impl += " " + _profile.Name + "(parameters, tags, result)\n"; + impl += "end\n"; + return (functionName, impl); + } + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/LuaPrinter1.RelationPreprocessor.cs b/AspectedRouting/IO/itinero1/LuaPrinter1.RelationPreprocessor.cs new file mode 100644 index 0000000..5fd6465 --- /dev/null +++ b/AspectedRouting/IO/itinero1/LuaPrinter1.RelationPreprocessor.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Linq; +using AspectedRouting.Language; +using AspectedRouting.Language.Expression; + +namespace AspectedRouting.IO.itinero1 +{ + public partial class LuaPrinter1 + { + + private (string implementation, HashSet extraKeys) GenerateMembershipPreprocessor() + { + // Extra keys are the names of introduced tag-keys, e.g. '_relation:bicycle_fastest:cycle_highway' + var extraKeys = new HashSet(); + var memberships = Analysis.MembershipMappingsFor(_profile, _context); + + foreach (var (calledInFunction, membership) in memberships) + { + var funcMetaData = new AspectMetadata( + membership, + "relation_preprocessing_for_" + calledInFunction.AsLuaIdentifier(), + "Function preprocessing needed for aspect " + calledInFunction + + ", called by the relation preprocessor", + "Generator", "", "NA" + ); + + + _skeleton.AddFunction(funcMetaData); + } + + + var func = new List + { + "", + "", + "-- Processes the relation. All tags which are added to result.attributes_to_keep will be copied to 'attributes' of each individual way", + "function relation_tag_processor(relation_tags, result)", + " local parameters = {}", + " local subresult = {}", + " local matched = false", + " result.attributes_to_keep = {}", + " ", + " -- Legacy to add colours to the bike networks", + " legacy_relation_preprocessor(relation_tags, result)" + }; + _skeleton.AddDep("legacy"); + + foreach (var (calledInFunction, expr) in memberships) + { + func.Add($"\n\n -- {calledInFunction} ---"); + + var usedParameters = expr.UsedParameters().Select(param => param.ParamName.TrimStart('#')).ToHashSet(); + + // First, we calculate the value for the default parameters + var preProcName = "relation_preprocessing_for_" + calledInFunction.AsLuaIdentifier(); + func.Add(""); + func.Add(""); + func.Add(" subresult.attributes_to_keep = {}"); + func.Add(" parameters = default_parameters()"); + func.Add($" matched = {preProcName}(parameters, relation_tags, subresult)"); + func.Add(" if (matched) then"); + var tagKey = "_relation:" + calledInFunction.AsLuaIdentifier(); + extraKeys.Add(tagKey); + func.Add( + " -- " + tagKey + + " is the default value, which will be overwritten in 'remove_relation_prefix' for behaviours having a different parameter settign"); + func.Add($" result.attributes_to_keep[\"{tagKey}\"] = \"yes\""); + func.Add(" end"); + + + if (usedParameters.Count() == 0) + { + // Every behaviour uses the default parameters for this one + func.Add(" -- No parameter dependence for aspect " + calledInFunction); + continue; + } + + foreach (var (behaviourName, parameters) in _profile.Behaviours) + { + if (usedParameters.Except(parameters.Keys.ToHashSet()).Any()) + { + // The parameters where the membership depends on, are not used here + // This is thus the same as the default. We don't have to calculate it + continue; + } + + func.Add(""); + func.Add(""); + func.Add(" subresult.attributes_to_keep = {}"); + func.Add(" parameters = default_parameters()"); + func.Add(_parameterPrinter.DeclareParametersFor(parameters.Where(kv => usedParameters.Contains(kv.Key)) + .ToDictionary(kv => kv.Key, kv => kv.Value))); + func.Add($" matched = {preProcName}(parameters, relation_tags, subresult)"); + func.Add(" if (matched) then"); + tagKey = "_relation:" + behaviourName.AsLuaIdentifier() + ":" + calledInFunction.AsLuaIdentifier(); + extraKeys.Add(tagKey); + func.Add($" result.attributes_to_keep[\"{tagKey}\"] = \"yes\""); + func.Add(" end"); + } + } + + func.Add("end"); + + return (string.Join("\n", func), extraKeys); + } + + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/LuaPrinter1.cs b/AspectedRouting/IO/itinero1/LuaPrinter1.cs new file mode 100644 index 0000000..55e4271 --- /dev/null +++ b/AspectedRouting/IO/itinero1/LuaPrinter1.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AspectedRouting.Language; +using AspectedRouting.Language.Expression; +using AspectedRouting.Tests; + +namespace AspectedRouting.IO.itinero1 +{ + public partial class LuaPrinter1 + { + private readonly ProfileMetaData _profile; + private readonly Context _context; + private readonly List _aspectTestSuites; + private readonly List _profileTests; + private readonly LuaSkeleton.LuaSkeleton _skeleton; + + + private readonly LuaParameterPrinter _parameterPrinter; + + + public LuaPrinter1(ProfileMetaData profile, Context context, + List aspectTestSuites, + List profileTests) + { + _profile = profile; + _context = context; + _aspectTestSuites = aspectTestSuites; + _profileTests = profileTests; + _skeleton = new LuaSkeleton.LuaSkeleton(context); + _parameterPrinter = new LuaParameterPrinter(profile, _skeleton); + } + + public string ToLua() + { + _skeleton.AddDep("spoken_instructions"); + + var (membershipFunction, extraKeys) = GenerateMembershipPreprocessor(); + var (profileOverview, behaviourFunctions) = GenerateProfileFunctions(); + var mainFunction = GenerateMainProfileFunction(); + var tests = new LuaTestPrinter(_skeleton, new List{"unitTest","unitTestProfile"}).GenerateFullTestSuite(_profileTests, _aspectTestSuites); + + + var keys = _profile.AllExpressions(_context).PossibleTags().Keys + .Concat(extraKeys) + .Select(key => "\"" + key + "\"") + .ToHashSet(); + + var header = new List + { + $"-- Itinero 1.0-profile, generated by AspectedRouting on {DateTime.Now:s}", + $"name = \"{_profile.Name}\"", + "normalize = false", + "vehicle_type = {" + string.Join(", ", _profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}", + // meta_whitelist is defined in the profile file, these are tags that are included in the generated route, but are not relevant for determining weights + "meta_whitelist = {\n \"cycle_network_colour\"," // cycle network colour is sneaked in here for legacy reasons + + string.Join("\n , ", _profile.Metadata.Select(s => "\"" + s + "\"")) + + " }", + "profile_whitelist = {\n " + string.Join("\n , ", keys) + "\n }", + "", + "", + "", + profileOverview, + "", + _parameterPrinter.GenerateDefaultParameters() + }; + + + // Add the aspect functions to the skeleton + var usedFunctions = _profile.CalledFunctionsRecursive(_context).Values.SelectMany(v => v).ToHashSet(); + foreach (var functionName in usedFunctions) + { + _skeleton.AddFunction(_context.GetAspect(functionName)); + } + + + // The dependencies should be generated after all the other functions are generated, to make sure all are added + var dependencies = _skeleton.GenerateDependencies(); + var functions = _skeleton.GenerateFunctions(); + + var allCode = new List> + { + header, + behaviourFunctions, + mainFunction.InList(), + membershipFunction.InList(), + "---------------------- ASPECTS ----------------------".InList(), + functions, + "---------------------- UTILS ------------------------".InList(), + dependencies, + "----------------------- TESTS ------------------------".InList(), + tests.InList(), + GenerateLegacyTail().InList() + }; + + + return string.Join("\n\n\n", allCode.Select(code => string.Join("\n", code))); + } + + + private (string profilesOverview, List behaviourImplementations) GenerateProfileFunctions() + { + // Profiles gets the contents for the 'profiles' field, part of the profile spec + var profiles = new List(); + var behaviourImplementations = new List(); + foreach (var (behaviourName, behaviourParameters) in _profile.Behaviours) + { + var (functionName, implementation ) = GenerateBehaviourFunction(behaviourName, behaviourParameters); + behaviourImplementations.Add(implementation); + profiles.Add( + string.Join(",\n ", + $" name = \"{behaviourName.AsLuaIdentifier()}\"", + " function_name = \"behaviour_" + functionName + "\"", + " metric = \"custom\"" + ) + ); + } + + var profilesOverview = "profiles = {\n {\n" + + string.Join("\n },\n {\n ", profiles) + "\n }\n}"; + return (profilesOverview, behaviourImplementations); + } + + + private string GenerateLegacyTail() + { + return string.Join("\n", + "", + "if (itinero == nil) then", + " itinero = {}", + " itinero.log = print", + "", + " -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended", + " runTests = true", + "", + "", + "else", + " print = itinero.log", + "end", + "", + "test_all()", + "if (not failed_tests and not failed_profile_tests) then", + " print(\"Tests OK\")", + "end" + ); + } + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/Luaprinter.Profile.cs b/AspectedRouting/IO/itinero1/Luaprinter.Profile.cs deleted file mode 100644 index 35c4ee9..0000000 --- a/AspectedRouting/IO/itinero1/Luaprinter.Profile.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AspectedRouting.Language; -using AspectedRouting.Language.Expression; -using AspectedRouting.Language.Functions; -using AspectedRouting.Language.Typ; - -namespace AspectedRouting.IO.itinero1 -{ - public partial class LuaPrinter - { - private readonly Context _context; - - public LuaPrinter(Context context) - { - _context = context; - } - - private void CreateMembershipPreprocessor(ProfileMetaData profile) - { - var memberships = Analysis.MembershipMappingsFor(profile, _context); - - foreach (var (calledInFunction, membership) in memberships) - { - var funcMetaData = new AspectMetadata( - membership, - "relation_preprocessing_for_" + calledInFunction.FunctionName(), - "Function preprocessing needed for aspect " + calledInFunction + - ", called by the relation preprocessor", - "Generator", "", "NA" - ); - - - AddFunction(funcMetaData); - } - - - var func = new List - { - "", - "", - "-- Processes the relation. All tags which are added to result.attributes_to_keep will be copied to 'attributes' of each individual way", - "function relation_tag_processor(relation_tags, result)", - " local parameters = {}", - " local subresult = {}", - " local matched = false", - " result.attributes_to_keep = {}" - }; - - foreach (var (calledInFunction, expr) in memberships) - { - func.Add($"\n\n -- {calledInFunction} ---"); - - var usedParameters = expr.UsedParameters().Select(param => param.ParamName.TrimStart('#')).ToHashSet(); - - // First, we calculate the value for the default parameters - var preProcName = "relation_preprocessing_for_" + calledInFunction.FunctionName(); - func.Add(""); - func.Add(""); - func.Add(" subresult.attributes_to_keep = {}"); - func.Add(" parameters = default_parameters()"); - func.Add($" matched = {preProcName}(parameters, relation_tags, subresult)"); - func.Add(" if (matched) then"); - var tagKey = "_relation:" + calledInFunction.FunctionName(); - _neededKeys.Add(tagKey); - func.Add( - " -- " + tagKey + - " is the default value, which will be overwritten in 'remove_relation_prefix' for behaviours having a different parameter settign"); - func.Add($" result.attributes_to_keep[\"{tagKey}\"] = \"yes\""); - func.Add(" end"); - - - if (usedParameters.Count() == 0) - { - // Every behaviour uses the default parameters for this one - func.Add(" -- No parameter dependence for aspect " + calledInFunction); - continue; - } - - foreach (var (behaviourName, parameters) in profile.Behaviours) - { - if (usedParameters.Except(parameters.Keys.ToHashSet()).Any()) - { - // The parameters where the membership depends on, are not used here - // This is thus the same as the default. We don't have to calculate it - continue; - } - - func.Add(""); - func.Add(""); - func.Add(" subresult.attributes_to_keep = {}"); - func.Add(" parameters = default_parameters()"); - func.Add(ParametersToLua(parameters.Where(kv => usedParameters.Contains(kv.Key)) - .ToDictionary(kv => kv.Key, kv => kv.Value))); - func.Add($" matched = {preProcName}(parameters, relation_tags, subresult)"); - func.Add(" if (matched) then"); - tagKey = "_relation:" + behaviourName.FunctionName() + ":" + calledInFunction.FunctionName(); - _neededKeys.Add(tagKey); - func.Add($" result.attributes_to_keep[\"{tagKey}\"] = \"yes\""); - func.Add(" end"); - } - } - - func.Add("end"); - - - _code.Add(string.Join("\n", func)); - } - - - /// - /// Adds the necessary called functions and the profile main entry point - /// - public void AddProfile(ProfileMetaData profile) - { - var defaultParameters = "\n"; - foreach (var (name, (types, inFunction)) in profile.UsedParameters(_context)) - { - defaultParameters += $"{name}: {string.Join(", ", types)}\n" + - $" Used in {inFunction}\n"; - } - - CreateMembershipPreprocessor(profile); - - - var impl = string.Join("\n", - "", - "", - $"name = \"{profile.Name}\"", - "normalize = false", - "vehicle_type = {" + string.Join(", ", profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}", - "meta_whitelist = {\n " + string.Join("\n , ", profile.Metadata.Select(s => "\"" + s + "\"")) + - "}", - "", - "", - "", - "--[[", - profile.Name, - "This is the main function called to calculate the access, oneway and speed.", - "Comfort is calculated as well, based on the parameters which are padded in", - "", - "Created by " + profile.Author, - "Originally defined in " + profile.Filename, - "Used parameters: " + defaultParameters.Indent(), - "]]", - "function " + profile.Name + "(parameters, tags, result)", - "", - " -- initialize the result table on the default values", - " result.access = 0", - " result.speed = 0", - " result.factor = 1", - " result.direction = 0", - " result.canstop = true", - " result.attributes_to_keep = {}", - "", - " local access = " + ToLua(profile.Access), - " if (access == nil or access == \"no\") then", - " return", - " end", - " tags.access = access", - " local oneway = " + ToLua(profile.Oneway), - " tags.oneway = oneway", - " local speed = " + ToLua(profile.Speed), - " tags.speed = speed", - " local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m", - ""); - - impl += - "\n local priority = 0\n "; - - foreach (var (parameterName, expression) in profile.Priority) - { - var paramInLua = ToLua(new Parameter(parameterName)); - - - var exprInLua = ToLua(expression); - var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First()); - if (subs != null && subs.TryGetValue("$a", out var resultType) && - (resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String))) - { - AddDep("parse"); - exprInLua = "parse(" + exprInLua + ")"; - } - - impl += "\n " + string.Join("\n ", - $"if({paramInLua} ~= 0) then", - $" priority = priority + {paramInLua} * {exprInLua}", - "end" - ); - } - - - impl += string.Join("\n", - "", - "", - " if (priority <= 0) then", - " result.access = 0", - " return", - " end", - "", - " result.access = 1", - " result.speed = speed", - " result.factor = 1 / priority", - "", - " if (oneway == \"both\") then", - " result.direction = 0", - " elseif (oneway == \"with\") then", - " result.direction = 1", - " elseif (oneway == \"against\") then", - " result.direction = 2", - " else", - " error(\"Unexpected value for oneway: \"..oneway)", - " end", - "", - "end", - "", - "", - "function default_parameters()", - " local parameters = {}", - ParametersToLua(profile.DefaultParameters), - " return parameters", - "end", - "", - "" - ); - - - var profiles = new List(); - foreach (var (name, subParams) in profile.Behaviours) - { - impl += BehaviourFunction(profile, name, subParams, profiles); - } - - impl += "\n\n\n"; - impl += "profiles = {\n {\n" + - string.Join("\n },\n {\n ", profiles) + "\n }\n}"; - - _code.Add(impl); - } - - private string BehaviourFunction(ProfileMetaData profile, - string name, - Dictionary subParams, List profiles) - { - var functionName = profile.Name + "_" + name; - - subParams.TryGetValue("description", out var description); - profiles.Add( - string.Join(",\n ", - $" name = \"{name.FunctionName()}\"", - " function_name = \"behaviour_" + functionName.FunctionName() + "\"", - " metric = \"custom\"" - ) - ); - - AddDep("remove_relation_prefix"); - var impl = string.Join("\n", - "", - "--[[", - description, - "]]", - "function behaviour_" + functionName.FunctionName() + "(tags, result)", - $" tags = remove_relation_prefix(tags, \"{name.FunctionName()}\")", - " local parameters = default_parameters()", - " parameters.name = \"" + functionName + "\"", - "" - ); - - impl += ParametersToLua(subParams); - - impl += " " + profile.Name + "(parameters, tags, result)\n"; - impl += "end\n"; - return impl; - } - - /// - /// `local parameters = default_parameters()` must still be invoked by caller! - /// - /// - /// - private string ParametersToLua(Dictionary subParams) - { - var impl = ""; - foreach (var (paramName, value) in subParams) - { - if (paramName.Equals("description")) - { - continue; - } - - impl += $" parameters.{paramName.TrimStart('#').FunctionName()} = {ToLua(value)}\n"; - } - - return impl; - } - } -} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero1/Luaprinter.TestSuites.cs b/AspectedRouting/IO/itinero1/Luaprinter.TestSuites.cs deleted file mode 100644 index 71d7522..0000000 --- a/AspectedRouting/IO/itinero1/Luaprinter.TestSuites.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using AspectedRouting.Tests; - -namespace AspectedRouting.IO.itinero1 -{ - public partial class LuaPrinter - { - public void AddTestSuite(ProfileTestSuite testSuite) - { - var tests = string.Join("\n", - testSuite.Tests.Select((test, i) => ToLua(testSuite, i, test.Item1, test.tags))); - _tests.Add(tests); - } - - private string ToLua(ProfileTestSuite testSuite, int index, ProfileResult expected, Dictionary tags) - { - AddDep("debug_table"); - var parameters = new Dictionary(); - - - var keysToCheck = new List(); - foreach (var (key, value) in tags) - { - if (key.StartsWith("#")) - { - parameters[key.TrimStart('#')] = value; - } - if(key.StartsWith("_relation:")) - { - keysToCheck.Add(key); - } - } - - - foreach (var key in keysToCheck) - { - var newKey = key.Replace(".", "_"); - tags[newKey] = tags[key]; - tags.Remove(key); - } - - foreach (var (paramName, _) in parameters) - { - tags.Remove("#" + paramName); - } - // function unit_test_profile(profile_function, profile_name, index, expected, tags) - - return $"unit_test_profile(behaviour_{testSuite.Profile.Name.FunctionName()}_{testSuite.BehaviourName.FunctionName()}, " + - $"\"{testSuite.BehaviourName}\", " + - $"{index}, " + - $"{{access = \"{D(expected.Access)}\", speed = {expected.Speed}, oneway = \"{D(expected.Oneway)}\", weight = {expected.Priority} }}, " + - tags.ToLuaTable() + - ")"; - } - - private string D(string s) - { - if (string.IsNullOrEmpty(s)) - { - return "0"; - } - - return s; - } - - - public void AddTestSuite(AspectTestSuite testSuite) - { - var fName = testSuite.FunctionToApply.Name; - var tests = string.Join("\n", - testSuite.Tests.Select((test, i) => ToLua(fName, i, test.expected, test.tags))); - _tests.Add(tests); - } - - private string ToLua(string functionToApplyName, int index, string expected, Dictionary tags) - { - var parameters = new Dictionary(); - - - foreach (var (key, value) in tags) - { - if (key.StartsWith("#")) - { - parameters[key.TrimStart('#')] = value; - } - } - - foreach (var (paramName, _) in parameters) - { - tags.Remove("#" + paramName); - } - - AddDep("unitTest"); - AddDep("debug_table"); - var funcName = functionToApplyName.Replace(" ", "_").Replace(".", "_"); - return - $"unit_test({funcName}, \"{functionToApplyName}\", {index}, \"{expected}\", {parameters.ToLuaTable()}, {tags.ToLuaTable()})"; - } - } -} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero2/LuaPrinter2.MainFunction.cs b/AspectedRouting/IO/itinero2/LuaPrinter2.MainFunction.cs new file mode 100644 index 0000000..92202d4 --- /dev/null +++ b/AspectedRouting/IO/itinero2/LuaPrinter2.MainFunction.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AspectedRouting.Language; +using AspectedRouting.Language.Typ; + +namespace AspectedRouting.IO.itinero2 +{ + public partial class LuaPrinter2 + { + private string GenerateFactorFunction() + { + var parameters = new Dictionary(); + foreach (var (name, value) in _profile.DefaultParameters) + { + parameters[name] = value; + } + + foreach (var (name, value) in _profile.Behaviours[_behaviourName]) + { + parameters[name] = value; + } + + + var aspects = new List(); + + foreach (var (paramName, expr) in _profile.Priority) + { + var paramExpr = parameters[paramName].Evaluate(_context); + if (!(paramExpr is double weight)) + { + continue; + } + + if (weight == 0) + { + continue; + } + + var expression = _profile.Priority[paramName]; + var exprInLua = _skeleton.ToLua(expression); + var subs = new Curry(Typs.Tags, new Var(("a"))).UnificationTable(expression.Types.First()); + if (subs != null && subs.TryGetValue("$a", out var resultType) && + (resultType.Equals(Typs.Bool) || resultType.Equals(Typs.String))) + { + _skeleton.AddDep("parse"); + exprInLua = "parse(" + exprInLua + ")"; + } + + aspects.Add(weight + " * " + exprInLua); + } + + Console.WriteLine(aspects.Lined()); + var code = new List() + { + "--[[", + "Generates the factor according to the priorities and the parameters for this behaviour", + "Note: 'result' is not actually used", + "]]", + "function calculate_priority(parameters, tags, result, access, oneway, speed)", + " local distance = 1", + " local priority = \n " + string.Join(" +\n ", aspects), + " return priority", + "end" + }; + return code.Lined(); + } + + private string GenerateMainFunction() + { + var parameters = _profile.Behaviours[_behaviourName]; + + _skeleton.AddDependenciesFor(_profile.Access); + _skeleton.AddDependenciesFor(_profile.Oneway); + _skeleton.AddDependenciesFor(_profile.Speed); + + _skeleton.AddDep("eq"); + var code = new List + { + "--[[", + _profile.Behaviours[_behaviourName]["description"].Evaluate(_context).ToString(), + "]]", + "function factor(tags, result)", + " ", + " -- initialize the result table on the default values", + " result.forward_speed = 0", + " result.backward_speed = 0", + " result.forward = 0", + " result.backward = 0", + " result.canstop = true", + " result.attributes_to_keep = {} -- not actually used anymore, but the code generation still uses this", + + "", + "", + " local parameters = default_parameters()", + _parameterPrinter.DeclareParametersFor(parameters), + "", + " local oneway = " + _skeleton.ToLua(_profile.Oneway), + " tags.oneway = oneway", + " -- forward calculation", + " tags[\"_direction\"] = \"with\"", + " local access_forward = " + _skeleton.ToLua(_profile.Access), + " if(oneway == \"against\") then", + " access_forward = \"no\"", + " end", + + " if(access_forward ~= nil and access_forward ~= \"no\") then", + " tags.access = access_forward", + " result.forward_speed = " + _skeleton.ToLua(_profile.Speed).Indent(), + " tags.speed = result.forward_speed", + " local priority = calculate_priority(parameters, tags, result, access_forward, oneway, result.forward_speed)", + " if (priority <= 0) then", + " result.forward_speed = 0", + " else", + " result.forward = 1 / priority", + " end", + " end", + "", + " -- backward calculation", + " tags[\"_direction\"] = \"against\"", + " local access_backward = " + _skeleton.ToLua(_profile.Access), + "", + " if(oneway == \"with\") then", + " access_backward = \"no\"", + " end", + "", + " if(access_backward ~= nil and access_backward ~= \"no\") then", + " tags.access = access_backward" + + " result.backward_speed = " + _skeleton.ToLua(_profile.Speed).Indent(), + " tags.speed = result.backward_speed", + " local priority = calculate_priority(parameters, tags, result, access_backward, oneway, result.backward_speed)", + " if (priority <= 0) then", + " result.backward_speed = 0", + " else", + " result.backward = 1 / priority", + " end", + " end", + + "end" + }; + + return code.Lined(); + } + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/itinero2/LuaPrinter2.cs b/AspectedRouting/IO/itinero2/LuaPrinter2.cs new file mode 100644 index 0000000..a7b2cc9 --- /dev/null +++ b/AspectedRouting/IO/itinero2/LuaPrinter2.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AspectedRouting.IO.itinero1; +using AspectedRouting.Language; +using AspectedRouting.Language.Expression; +using AspectedRouting.Tests; + +namespace AspectedRouting.IO.itinero2 +{ + /// + /// Lua printer for itinero2-lua format + /// + /// The itinero 2.0 lua profile is a whole lot simpler then the 1.0 format, + /// as a single profile there only describes a single behaviour of a vehicle: + /// + /// It has: + /// - name: string, e.g. 'bicycle.fastest' + /// - factor(attributes, result): void, where 'attributes' are all the tags of the way, + /// and result must contain (after calling): + /// - 'forward_speed', a double describing the forward speed (in km/h) + /// - 'backward_speed', the speed when travelling in the opposite direction (0 if not possible) + /// - 'forward', a double describing the forwardfactor + /// - 'backward', the backward factor + /// - 'canstop', a boolean indicating if stopping along the road is possible + /// + /// + public partial class LuaPrinter2 + { + private readonly ProfileMetaData _profile; + private readonly string _behaviourName; + private readonly Context _context; + private readonly List _aspectTests; + private readonly IEnumerable _behaviourTestSuite; + + private readonly LuaSkeleton.LuaSkeleton _skeleton; + private readonly LuaParameterPrinter _parameterPrinter; + + + public LuaPrinter2(ProfileMetaData profile, string behaviourName, + Context context, + List aspectTests, IEnumerable behaviourTestSuite) + { + _skeleton = new LuaSkeleton.LuaSkeleton(context); + _profile = profile; + _behaviourName = behaviourName; + _context = context; + _aspectTests = aspectTests; + _behaviourTestSuite = behaviourTestSuite; + _parameterPrinter = new LuaParameterPrinter(_profile, _skeleton); + } + + public string ToLua() + { + var header = + new List + { + $"name = \"{_profile.Name}.{_behaviourName}\"", + $"generationDate = \"{DateTime.Now:s}\"", + $"description = \"{_profile.Description}\"" + }; + + var tests = new LuaTestPrinter(_skeleton, new List() {"unitTestProfile2"}).GenerateFullTestSuite( + _behaviourTestSuite.ToList(), new List()); + var all = new List + { + header.Lined(), + "", + GenerateMainFunction(), + "", + GenerateFactorFunction(), + "", + _parameterPrinter.GenerateDefaultParameters(), + "", + "", + string.Join("\n\n", _skeleton.GenerateFunctions()), + "", + string.Join("\n\n", _skeleton.GenerateDependencies()), // Should be AFTER generating the main function! + "", + tests, + "", + + "if (itinero == nil) then", + " itinero = {}", + " itinero.log = print", + "", + " -- Itinero is not defined -> we are running from a lua interpreter -> the tests are intended", + " runTests = true", + "", + "", + "else", + " print = itinero.log", + "end", + "", + "test_all()", + "if (not failed_tests and not failed_profile_tests and print ~= nil) then", + " print(\"Tests OK\")", + "end" + }; + + return all.Lined(); + } + } +} \ No newline at end of file diff --git a/AspectedRouting/IO/lua/legacy.lua b/AspectedRouting/IO/lua/legacy.lua new file mode 100644 index 0000000..13265aa --- /dev/null +++ b/AspectedRouting/IO/lua/legacy.lua @@ -0,0 +1,17 @@ +--[[ + Legacy function to add cycle_colour +]] + +function legacy_relation_preprocessor(attributes, result) + if (attributes.route == "bicycle") then + -- This is a cycling network, the colour is copied + if (attributes.colour ~= nil) then + result.attributes_to_keep.cycle_network_colour = attributes.colour + end + + if (attributes.color ~= nil) then + -- for the americans! + result.attributes_to_keep.cycle_network_colour = attributes.color + end + end +end diff --git a/AspectedRouting/IO/lua/unitTestProfile.lua b/AspectedRouting/IO/lua/unitTestProfile.lua index bbf84a6..7d7e39b 100644 --- a/AspectedRouting/IO/lua/unitTestProfile.lua +++ b/AspectedRouting/IO/lua/unitTestProfile.lua @@ -1,20 +1,20 @@ failed_profile_tests = false --[[ -expected should be a table containing 'access', 'speed' and 'weight' +expected should be a table containing 'access', 'speed' and 'priority' ]] function unit_test_profile(profile_function, profile_name, index, expected, tags) local result = { attributes_to_keep = {} } local profile_failed = false profile_function(tags, result) - local accessCorrect = (result.access == 0 and (expected.access == "no" or expected.weight <= 0)) or result.access == 1 + local accessCorrect = (result.access == 0 and (expected.access == "no" or expected.priority <= 0)) or result.access == 1 if (not accessCorrect) then print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".access: expected " .. expected.access .. " but got " .. result.access) profile_failed = true failed_profile_tests = true end - if (expected.access == "no" or expected.weight <= 0) then + if (expected.access == "no" or expected.priority <= 0) then -- we cannot access this road, the other results are irrelevant if (profile_failed) then print("The used tags for test " .. tostring(index) .. " are:") @@ -46,8 +46,8 @@ function unit_test_profile(profile_function, profile_name, index, expected, tags end - if (not double_compare(result.factor, 1/expected.weight)) then - print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".factor: expected " .. expected.weight .. " but got " .. 1/result.factor) + if (not double_compare(result.factor, 1/expected.priority)) then + print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".factor: expected " .. expected.priority .. " but got " .. 1/result.factor) failed_profile_tests = true profile_failed = true end diff --git a/AspectedRouting/IO/lua/unitTestProfile2.lua b/AspectedRouting/IO/lua/unitTestProfile2.lua new file mode 100644 index 0000000..3593f04 --- /dev/null +++ b/AspectedRouting/IO/lua/unitTestProfile2.lua @@ -0,0 +1,67 @@ +failed_profile_tests = false +--[[ +Unit test of a behaviour function for an itinero 2.0 profile +]] + +function unit_test_profile(profile_function, profile_name, index, expected, tags) + -- Note: we don't actually use 'profile_function' + + local result = {} + local profile_failed = false + factor(tags, result) + + + local forward_access = result.forward_speed > 0 and result.forward > 0; + local backward_access = result.backward_speed > 0 and result.backward > 0; + + if (not forward_access and not backward_access) then + + if (expected.access == "no" or expected.speed <= 0 or expected.priority <= 0) then + -- All is fine, we can't access this thing anyway + return + end + + profile_failed = true + print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".access: expected " .. expected.access .. " but forward and backward are 0 (for either speed or factor)") + end + + if (expected.oneway == "with") then + if (backward_access) then + -- we can go against the direction, not good + print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but going against the direction is possible") + profile_failed = true; + end + if (not forward_access) then + print("Test " .. tostring(index) .. " warning for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but going with the direction is not possible") + end + end + + if (expected.oneway == "against") then + if (forward_access) then + -- we can go against the direction, not good + print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but going with the direction is possible") + end + if (not backward_access) then + print("Test " .. tostring(index) .. " warning for " .. profile_name .. ".oneway: expected " .. expected.oneway .. " but going against the direction is not possible") + end + end + + if (result.forward_speed ~= expected.speed and result.backward_speed ~= expected.speed) then + print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".speed: expected " .. expected.speed .. " but got " .. result.forward_speed .. " forward and " .. result.backward_speed .. " backward") + profile_failed = true; + end + + + if (result.forward ~= expected.priority and result.backward ~= expected.priority) then + print("Test " .. tostring(index) .. " failed for " .. profile_name .. ".priority: expected " .. expected.priority .. " but got " .. result.forward .. " forward and " .. result.backward .. " backward") + profile_failed = true; + end + + if(profile_failed) then + failed_profile_tests = true; + debug_table(tags, "tags: ") + debug_table(expected, "expected: ") + debug_table(result, "result: ") + end + +end \ No newline at end of file diff --git a/AspectedRouting/Language/Analysis.cs b/AspectedRouting/Language/Analysis.cs index 73c9781..88030a3 100644 --- a/AspectedRouting/Language/Analysis.cs +++ b/AspectedRouting/Language/Analysis.cs @@ -207,6 +207,8 @@ namespace AspectedRouting.Language { // Read as: this function calls the value-function var result = new Dictionary>(); + + var calledFunctions = new Queue(); void ScanExpression(IExpression e, string inFunction) @@ -252,6 +254,62 @@ namespace AspectedRouting.Language return result; } + public static (HashSet parameterName, HashSet calledFunctionNames) DirectlyAndInderectlyCalled( + this List exprs, Context ctx) + { + var parameters = new HashSet(); + var dependencies = new HashSet(); + + var queue = new Queue(); + exprs.ForEach(queue.Enqueue); + + while (queue.TryDequeue(out var next)) + { + var (p, deps) = next.DirectlyCalled(); + parameters.UnionWith(p); + var toCheck = deps.Except(dependencies); + dependencies.UnionWith(deps); + + foreach (var fName in toCheck) + { + queue.Enqueue(ctx.GetFunction(fName)); + } + } + + return (parameters, dependencies); + } + + + /// + /// Generates an overview of the dependencies of the expression, both which parameters it needs and what other functions (builtin or defined) it needs. + /// + /// + /// + public static (HashSet parameterName, HashSet calledFunctionNames) DirectlyCalled( + this IExpression expr) + { + var parameters = new HashSet(); + var dependencies = new HashSet(); + + expr.Visit(e => + { + if (e is FunctionCall fc) + { + dependencies.Add(fc.CalledFunctionName); + } + + if (e is Parameter p) + { + parameters.Add(p.ParamName); + } + + return true; + }); + + + return (parameters, dependencies); + } + public static string TypeBreakdown(this IExpression e) { var text = ""; @@ -404,12 +462,12 @@ namespace AspectedRouting.Language var usedTags = new Dictionary>(); foreach (var expr in exprs) { - var possible = expr.PossibleTags(); if (possible == null) { continue; } + foreach (var (key, values) in possible) { if (!usedTags.TryGetValue(key, out var collection)) diff --git a/AspectedRouting/Language/Context.cs b/AspectedRouting/Language/Context.cs index 01c427a..3563785 100644 --- a/AspectedRouting/Language/Context.cs +++ b/AspectedRouting/Language/Context.cs @@ -8,6 +8,7 @@ namespace AspectedRouting.Language public class Context { public readonly Dictionary Parameters = new Dictionary(); + public readonly Dictionary DefinedFunctions = new Dictionary(); public readonly string AspectName; @@ -48,6 +49,23 @@ namespace AspectedRouting.Language DefinedFunctions[name] = function; } + public AspectMetadata GetAspect(string name) + { + if (name.StartsWith("$")) + { + name = name.Substring(1); + } + + if (DefinedFunctions.ContainsKey(name)) + { + return DefinedFunctions[name]; + } + + throw new ArgumentException( + $"The aspect {name} is not a defined function. Known functions are " + + string.Join(", ", DefinedFunctions.Keys)); + } + public IExpression GetFunction(string name) { if (name.StartsWith("$")) diff --git a/AspectedRouting/Language/Expression/Apply.cs b/AspectedRouting/Language/Expression/Apply.cs index e1ddb1c..ada9cec 100644 --- a/AspectedRouting/Language/Expression/Apply.cs +++ b/AspectedRouting/Language/Expression/Apply.cs @@ -185,6 +185,8 @@ namespace AspectedRouting.Language.Expression return Funcs.Id; } + + if (Types.Count() > 1) { @@ -198,6 +200,18 @@ namespace AspectedRouting.Language.Expression return new Apply(_debugInfo, optimized); } + + { + var arg = new List(); + if ( + Deconstruct.UnApplyAny( + Deconstruct.IsFunc(Funcs.Id), + Deconstruct.Assign(arg) + ).Invoke(this)) + { + return arg.First(); + } + } { var (f, a) = FunctionApplications.Values.First(); @@ -274,6 +288,7 @@ namespace AspectedRouting.Language.Expression { return; } + foreach (var (_, (f, a)) in FunctionApplications) { f.Visit(visitor); diff --git a/AspectedRouting/Language/Expression/ProfileMetaData.cs b/AspectedRouting/Language/Expression/ProfileMetaData.cs index 5fc74b4..4e4f26d 100644 --- a/AspectedRouting/Language/Expression/ProfileMetaData.cs +++ b/AspectedRouting/Language/Expression/ProfileMetaData.cs @@ -35,9 +35,9 @@ namespace AspectedRouting.Language.Expression Author = author; Filename = filename; VehicleTyps = vehicleTyps; - Access = access; - Oneway = oneway; - Speed = speed; + Access = access.Optimize(); + Oneway = oneway.Optimize(); + Speed = speed.Optimize(); Priority = priority; Metadata = metadata; DefaultParameters = defaultParameters; diff --git a/AspectedRouting/Program.cs b/AspectedRouting/Program.cs index 2c726ef..6d132a4 100644 --- a/AspectedRouting/Program.cs +++ b/AspectedRouting/Program.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using AspectedRouting.IO; using AspectedRouting.IO.itinero1; +using AspectedRouting.IO.itinero2; using AspectedRouting.IO.jsonParser; using AspectedRouting.Language; using AspectedRouting.Language.Expression; @@ -13,7 +14,7 @@ namespace AspectedRouting { static class Program { - public static IEnumerable<(AspectMetadata aspect, AspectTestSuite tests)> ParseAspects( + public static List<(AspectMetadata aspect, AspectTestSuite tests)> ParseAspects( this IEnumerable jsonFileNames, List testFileNames, Context context) { var aspects = new List<(AspectMetadata aspect, AspectTestSuite tests)>(); @@ -55,42 +56,11 @@ namespace AspectedRouting return null; } - private static LuaPrinter GenerateLua(Context context, - IEnumerable<(AspectMetadata aspect, AspectTestSuite tests)> aspects, - ProfileMetaData profile, List profileTests) - { - var luaPrinter = new LuaPrinter(context); - var usedFunctions = profile.CalledFunctionsRecursive(context).Values.SelectMany(v => v).ToHashSet(); - - foreach (var (aspect, tests) in aspects) - { - if (!usedFunctions.Contains(aspect.Name)) - { - continue; - } - - luaPrinter.AddFunction(aspect); - if (tests != null) - { - luaPrinter.AddTestSuite(tests); - } - } - - luaPrinter.AddProfile(profile); - foreach (var testSuite in profileTests) - { - luaPrinter.AddTestSuite(testSuite); - } - - - return luaPrinter; - } - - private static IEnumerable<(ProfileMetaData profile, List profileTests)> ParseProfiles( + private static List<(ProfileMetaData profile, List profileTests)> ParseProfiles( IEnumerable jsonFiles, IReadOnlyCollection testFiles, Context context) { - var result = new List<(ProfileMetaData profile, List profileTests)>(); + var result = new List<(ProfileMetaData profile, List profileTests)>(); foreach (var jsonFile in jsonFiles) { try @@ -104,13 +74,13 @@ namespace AspectedRouting profile.SanityCheckProfile(context); - var profileTests = new List(); + var profileTests = new List(); foreach (var behaviourName in profile.Behaviours.Keys) { var path = testFiles.FindTest($"{profile.Name}.{behaviourName}.behaviour_test.csv"); if (path != null && File.Exists(path)) { - var test = ProfileTestSuite.FromString(context, profile, behaviourName, + var test = BehaviourTestSuite.FromString(context, profile, behaviourName, File.ReadAllText(path)); profileTests.Add(test); } @@ -201,6 +171,23 @@ namespace AspectedRouting Console.WriteLine($"Error in the file {file}:\n {msg}"); } + private static void PrintUsedTags(ProfileMetaData profile, Context context) + { + Console.WriteLine("\n\n\n---------- " + profile.Name + " --------------"); + foreach (var (key, values) in profile.AllExpressions(context).PossibleTags()) + { + var vs = "*"; + if (values.Any()) + { + vs = string.Join(", ", values); + } + + Console.WriteLine(key + ": " + vs); + } + + Console.WriteLine("\n\n\n------------------------"); + } + public static void Main(string[] args) { if (args.Length < 2) @@ -255,24 +242,26 @@ namespace AspectedRouting } - Console.WriteLine("\n\n\n---------- " + profile.Name + " --------------"); - foreach (var (key, values) in profile.AllExpressions(context).PossibleTags()) + PrintUsedTags(profile, context); + + var aspectTests = aspects.Select(a => a.tests).ToList(); + var luaProfile = new LuaPrinter1(profile, context, + aspectTests, + profileTests + ).ToLua(); + File.WriteAllText(outputDir + "/" + profile.Name + ".lua", luaProfile); + + foreach (var (behaviourName, behaviourParameters) in profile.Behaviours) { - var vs = "*"; - if (values.Any()) - { - vs = string.Join(", ", values); - } - - Console.WriteLine(key + ": " + vs); + var lua2behaviour = new LuaPrinter2( + profile, + behaviourName, + context, + aspectTests, + profileTests.Where(testsSuite => testsSuite.BehaviourName == behaviourName) + ).ToLua(); + File.WriteAllText($"{outputDir}/itinero2/{profile.Name}.{behaviourName}.lua", lua2behaviour); } - - Console.WriteLine("\n\n\n------------------------"); - - var luaPrinter = GenerateLua(context, aspects, profile, profileTests); - - - File.WriteAllText(outputDir + "/" + profile.Name + ".lua", luaPrinter.ToLua()); } Repl(context, diff --git a/AspectedRouting/Tests/ProfileTestSuite.cs b/AspectedRouting/Tests/ProfileTestSuite.cs index e524cb6..9da3385 100644 --- a/AspectedRouting/Tests/ProfileTestSuite.cs +++ b/AspectedRouting/Tests/ProfileTestSuite.cs @@ -8,13 +8,13 @@ using AspectedRouting.Language.Typ; namespace AspectedRouting.Tests { - public class ProfileTestSuite + public class BehaviourTestSuite { public readonly ProfileMetaData Profile; public readonly string BehaviourName; public readonly IEnumerable<(ProfileResult, Dictionary tags)> Tests; - public static ProfileTestSuite FromString(Context c, ProfileMetaData function, string profileName, + public static BehaviourTestSuite FromString(Context c, ProfileMetaData function, string behaviourName, string csvContents) { try @@ -109,21 +109,21 @@ namespace AspectedRouting.Tests } } - return new ProfileTestSuite(function, profileName, tests); + return new BehaviourTestSuite(function, behaviourName, tests); } catch (Exception e) { - throw new Exception("In the profile test file for " + profileName, e); + throw new Exception("In the profile test file for " + behaviourName, e); } } - public ProfileTestSuite( + public BehaviourTestSuite( ProfileMetaData profile, - string profileName, + string behaviourName, IEnumerable<(ProfileResult, Dictionary tags)> tests) { Profile = profile; - BehaviourName = profileName; + BehaviourName = behaviourName; Tests = tests; } diff --git a/AspectedRouting/Utils.cs b/AspectedRouting/Utils.cs index 7d505d6..80bf2d4 100644 --- a/AspectedRouting/Utils.cs +++ b/AspectedRouting/Utils.cs @@ -9,6 +9,16 @@ namespace AspectedRouting return s.Replace("\n", "\n "); } + public static List InList(this T t) + { + return new List {t}; + } + + public static string Lined(this IEnumerable lines) + { + return string.Join("\n", lines); + } + public static int Multiply(this IEnumerable ints) { var factor = 1;