AspectedRouting/AspectedRouting/IO/LuaPrinter.cs
Pieter Vander Vennet 2c2a28d30a Initial commit
2020-04-30 17:23:44 +02:00

535 lines
No EOL
19 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using AspectedRouting.Functions;
using AspectedRouting.Typ;
using static AspectedRouting.Deconstruct;
namespace AspectedRouting.IO
{
public class LuaPrinter
{
public static Dictionary<string, string> BasicFunctions = _basicFunctions();
private readonly HashSet<string> _deps = new HashSet<string>();
private readonly List<string> _code = new List<string>();
private readonly HashSet<string> _neededKeys = new HashSet<string>();
/// <summary>
/// A dictionary containing the implementation of basic functions
/// </summary>
/// <returns></returns>
private static Dictionary<string, string> _basicFunctions()
{
var imps = new Dictionary<string, string>();
var functionsToFetch = Funcs.BuiltinNames.ToList();
// These functions should be loaded from disk, but are not necessarily included
functionsToFetch.Add("table_to_list");
functionsToFetch.Add("debug_table");
functionsToFetch.Add("unitTest");
functionsToFetch.Add("unitTestProfile");
functionsToFetch.Add("double_compare");
foreach (var name in functionsToFetch)
{
var path = $"IO/lua/{name}.lua";
if (File.Exists(path))
{
imps[name] = File.ReadAllText(path);
}
}
return imps;
}
public LuaPrinter()
{
_deps.Add("debug_table");
}
private string ToLua(IExpression bare, Context context, string key = "nil")
{
var collectedMapping = new List<IExpression>();
var order = new List<IExpression>();
if (UnApply(
UnApply(
IsFunc(Funcs.FirstOf),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
_deps.Add(Funcs.FirstOf.Name);
return "first_match_of(tags, result, \n" +
" " + ToLua(order.First(), context, key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First(), context)).Indent().Indent() +
")";
}
if (UnApply(
UnApply(
IsFunc(Funcs.MustMatch),
Assign(order))
, UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))
).Invoke(bare))
{
_deps.Add(Funcs.MustMatch.Name);
return "must_match(tags, result, \n" +
" " + ToLua(order.First(), context, key) + "," +
("\n" + MappingToLua((Mapping) collectedMapping.First(), context)).Indent().Indent() +
")";
}
var collectedList = new List<IExpression>();
var func = new List<IExpression>();
if (
UnApply(
UnApply(IsFunc(Funcs.Dot), Assign(func)),
UnApply(IsFunc(Funcs.ListDot),
Assign(collectedList))).Invoke(bare))
{
var exprs = (IEnumerable<IExpression>) ((Constant) collectedList.First()).Evaluate(context);
var luaExprs = new List<string>();
var funcName = func.First().ToString().TrimStart('$');
_deps.Add(funcName);
foreach (var expr in exprs)
{
var c = new List<IExpression>();
if (UnApply(IsFunc(Funcs.Const), Assign(c)).Invoke(expr))
{
luaExprs.Add(ToLua(c.First(), context, key));
continue;
}
if (expr.Types.First() is Curry curry
&& curry.ArgType.Equals(Typs.Tags))
{
var lua = ToLua(expr, context, key);
luaExprs.Add(lua);
}
}
return "\n " + funcName + "({\n " + string.Join(",\n ", luaExprs) +
"\n })";
}
collectedMapping.Clear();
var dottedFunction = new List<IExpression>();
dottedFunction.Clear();
if (UnApply(
UnApply(
IsFunc(Funcs.Dot),
Assign(dottedFunction)
),
UnApply(
IsFunc(Funcs.StringStringToTags),
Assign(collectedMapping))).Invoke(bare)
)
{
var mapping = (Mapping) collectedMapping.First();
var baseFunc = (Function) dottedFunction.First();
_deps.Add(baseFunc.Name);
_deps.Add("table_to_list");
return baseFunc.Name +
"(table_to_list(tags, result, " +
("\n" + MappingToLua(mapping, context)).Indent().Indent() +
"))";
}
// The expression might be a function which still expects a string (the value from the tag) as argument
if (!(bare is Mapping) &&
bare.Types.First() is Curry curr &&
curr.ArgType.Equals(Typs.String))
{
var applied = new Apply(bare, new Constant(curr.ArgType, ("tags", "\"" + key + "\"")));
return ToLua(applied.Optimize(), context, key);
}
// The expression might consist of multiple nested functions
var fArgs = bare.DeconstructApply();
if (fArgs != null)
{
var (f, args) = fArgs.Value;
var baseFunc = (Function) f;
_deps.Add(baseFunc.Name);
return baseFunc.Name + "(" + string.Join(", ", args.Select(arg => ToLua(arg, context, key))) + ")";
}
var collected = new List<IExpression>();
switch (bare)
{
case FunctionCall fc:
var called = context.DefinedFunctions[fc.CalledFunctionName];
if (called.ProfileInternal)
{
return called.Name;
}
AddFunction(called, context);
return $"{fc.CalledFunctionName.FunctionName()}(parameters, tags, result)";
case Constant c:
return ConstantToLua(c, context);
case Mapping m:
return MappingToLua(m, context).Indent();
case Function f:
var fName = f.Name.TrimStart('$');
if (Funcs.Builtins.ContainsKey(fName))
{
_deps.Add(f.Name);
}
else
{
var definedFunc = context.DefinedFunctions[fName];
if (definedFunc.ProfileInternal)
{
return f.Name;
}
AddFunction(definedFunc, context);
}
return f.Name;
case Apply a when UnApply(IsFunc(Funcs.Const), Assign(collected)).Invoke(a):
return ToLua(collected.First(), context, key);
case Parameter p:
return $"parameters[\"{p.ParamName.FunctionName()}\"]";
default:
throw new Exception("Could not convert " + bare + " to a lua expression");
}
}
public static string ConstantToLua(Constant c, Context context)
{
var o = c.Evaluate(context);
switch (o)
{
case IExpression e:
return ConstantToLua(new Constant(e.Types.First(), e.Evaluate(null)), context);
case int i:
return "" + i;
case double d:
return "" + d;
case string s:
return '"' + s.Replace("\"", "\\\"") + '"';
case ValueTuple<string, string> unpack:
return unpack.Item1 + "[" + unpack.Item2 + "]";
case IEnumerable<object> ls:
var t = (c.Types.First() as ListType).InnerType;
return "{" + string.Join(", ", ls.Select(obj =>
{
var objInConstant = new Constant(t, obj);
if (obj is Constant asConstant)
{
objInConstant = asConstant;
}
return ConstantToLua(objInConstant, context);
})) + "}";
default:
return o.ToString();
}
}
public string MappingToLua(Mapping m, Context context)
{
var contents = m.StringToResultFunctions.Select(kv =>
{
var (key, expr) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return left + " = " + ToLua(expr, context, key);
}
);
return
"{\n " +
string.Join(",\n ", contents) +
"\n}";
}
private HashSet<string> addFunctions = new HashSet<string>();
public void AddFunction(AspectMetadata meta, Context context)
{
if (addFunctions.Contains(meta.Name))
{
// already added
return;
}
addFunctions.Add(meta.Name);
var possibleTags = meta.PossibleTags();
var usedParams = meta.UsedParameters();
var numberOfCombinations = possibleTags.Values.Select(lst => 1 + lst.Count).Multiply();
var impl = string.Join("\n",
"--[[",
meta.Description,
"",
"Unit: " + meta.Unit,
"Created by " + meta.Author,
"Originally defined in " + meta.Filepath,
"Uses tags: " + string.Join(", ", possibleTags.Keys),
"Used parameters: " + string.Join(", ", usedParams),
"Number of combintations: " + numberOfCombinations,
"Returns values: ",
"]]",
"function " + meta.Name.FunctionName() + "(parameters, tags, result)",
" return " + ToLua(meta.ExpressionImplementation, context),
"end"
);
_code.Add(impl);
foreach (var k in possibleTags.Keys)
{
_neededKeys.Add(k); // To generate a whitelist of OSM-keys that should be kept
}
}
private string CreateMembershipPreprocessor()
{
return "";
}
/// <summary>
/// Adds the necessary called functions and the profile main entry point
/// </summary>
public void CreateProfile(ProfileMetaData profile, Context context)
{
var defaultParameters = "\n";
foreach (var (name, (types, inFunction)) in profile.UsedParameters(context))
{
defaultParameters += $"{name}: {string.Join(", ", types)}\n" +
$" Used in {inFunction}\n";
}
var impl = string.Join("\n",
"",
"",
$"name = \"{profile.Name}\"",
"normalize = false",
"vehicle_type = {" + string.Join(", ", profile.VehicleTyps.Select(s => "\"" + s + "\"")) + "}",
"meta_whitelist = {" + string.Join(", ", 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, context),
" if (access == nil or access == \"no\") then",
" return",
" end",
" local oneway = " + ToLua(profile.Oneway, context),
" local speed = " + ToLua(profile.Speed, context),
" local distance = 1 -- the weight per meter for distance travelled is, well, 1m/m",
"");
impl +=
"\n local weight = \n ";
var weightParts = new List<string>();
foreach (var (parameterName, expression) in profile.Weights)
{
var weightPart = ToLua(new Parameter(parameterName), context) + " * ";
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)))
{
weightPart += "parse(" + ToLua(expression, context) + ")";
}
else
{
weightPart += ToLua(expression, context);
}
weightParts.Add(weightPart);
}
impl += string.Join(" + \n ", weightParts);
impl += string.Join("\n",
"",
"",
"",
" -- put all the values into the result-table, as needed for itinero",
" result.access = 1",
" result.speed = speed",
" result.factor = 1/weight",
"",
" if (oneway == \"both\") then",
" result.oneway = 0",
" elseif (oneway == \"with\") then",
" result.oneway = 1",
" else",
" result.oneway = 2",
" end",
"",
"end",
"",
"",
"function default_parameters()",
" return " + profile.DefaultParameters.ToLuaTable(),
"end",
"",
""
);
impl += "\n\n" + CreateMembershipPreprocessor() + "\n\n";
var profiles = new List<string>();
foreach (var (name, subParams) in profile.Profiles)
{
var functionName = profile.Name + "_" + name;
subParams.TryGetValue("description", out var description);
profiles.Add(
string.Join(",\n ",
$" name = \"{name}\"",
" function_name = \"profile_" + functionName + "\"",
" metric = \"custom\""
)
);
impl += string.Join("\n",
"",
"--[[",
description,
"]]",
"function profile_" + functionName + "(tags, result)",
" local parameters = default_parameters()",
""
);
foreach (var (paramName, value) in subParams)
{
impl += $" parameters.{paramName.TrimStart('#').FunctionName()} = {value.Pretty()}\n";
}
impl += " " + profile.Name + "(parameters, tags, result)\n";
impl += "end\n";
}
impl += "\n\n\n";
impl += "profiles = {\n {\n" +
string.Join("\n },\n {\n ", profiles) + "\n }\n}";
_code.Add(impl);
}
public string ToLua()
{
var deps = _deps.ToList();
deps.Add("unitTest");
deps.Add("unitTestProfile");
deps.Add("inv");
deps.Add("double_compare");
deps.Sort();
var code = deps.Select(d => BasicFunctions[d]).ToList();
var keys = _neededKeys.Select(key => "\"" + key + "\"");
code.Add("\n\nprofile_whitelist = {" + string.Join(", ", keys) + "}");
code.AddRange(_code);
return string.Join("\n\n\n", code);
}
}
public static class StringExtensions
{
public static string ToLuaTable(this Dictionary<string, string> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = \"{value}\"";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string ToLuaTable(this Dictionary<string, object> tags)
{
var contents = tags.Select(kv =>
{
var (key, value) = kv;
var left = "[\"" + key + "\"]";
if (Regex.IsMatch(key, "^[a-zA-Z][_a-zA-Z-9]*$"))
{
left = key;
}
return $"{left} = {value.Pretty()}";
});
return "{" + string.Join(", ", contents) + "}";
}
public static string FunctionName(this string s)
{
return s.Replace(" ", "_").Replace(".", "_").Replace("-", "_");
}
}
}