diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs index 8dd89a9..e2a4e74 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; @@ -7,7 +6,99 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { using Function = Func, Expression>; using FunctionLater = Func, Expression>; - public class Environment : Dictionary {} + public class Procedure : Expression { + private Compiler.List _parameters; + private Expression _body; + public Procedure(Compiler.List parameters, Expression body) { + _parameters = parameters; + _body = body; + } + private static IEnumerable<(T1, T2)> Zip(IEnumerable a, IEnumerable b) { + using (var e1 = a.GetEnumerator()) using (var e2 = b.GetEnumerator()) { + while (e1.MoveNext() && e2.MoveNext()) { + yield return (e1.Current, e2.Current); + } + } + } + public override int GetHashCode() { + int hash = 17; + hash *= 23; + hash += _parameters.GetHashCode(); + hash *= 23; + hash += _body.GetHashCode(); + return hash; + } + public override bool Equals(Expression? other) { + if (other is Procedure other_p) { + return _parameters == other_p._parameters && _body == other_p._body; + } + return false; + } + public override object Inner() { + throw new ApplicationException("This is not sensible"); + } + public override string ToString() { + return $"(lambda {_parameters} {_body})"; + } + + public Expression Call(Executor e, IList args) { + var p = _parameters.expressions.Select(x => x.Inner().ToString()).ToList(); + Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater); + foreach (var tuple in Zip(p, args)) { + new_e.environment.Set(tuple.Item1, tuple.Item2); + } + return new_e.eval(_body); + } + } + + public interface IEnvironment { + public V Get(K k); + public void Set(K k, V v); + public IEnvironment? Find(K k); + } + + public class Environment : Dictionary, IEnvironment { + public Expression? Get(string k) { + if (TryGetValue(k, out Expression v)) { + return v; + } + return null; + } + public void Set(string k, Expression v) { + Add(k, v); + } + + public IEnvironment? Find(string k) { + if (ContainsKey(k)) { + return this; + } + return null; + } + } + + public class SubEnvironment : Dictionary, IEnvironment { + private IEnvironment _super; + public SubEnvironment(IEnvironment super) { + _super = super; + } + public Expression? Get(string k) { + if (TryGetValue(k, out Expression v)) { + return v; + } + return null; + } + public void Set(string k, Expression v) { + Add(k, v); + } + + public IEnvironment? Find(string k) { + if (ContainsKey(k)) { + return this; + } + return _super.Find(k); + } + } + public class Builtins : Dictionary { public Builtins() : base() { this["+"] = _add; @@ -237,6 +328,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { public BuiltinsLater() : base() { this["if"] = _if; this["define"] = _define; + this["lambda"] = _lambda; this["apply"] = _apply; this["and"] = _and; this["or"] = _or; @@ -246,9 +338,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { return e.eval(args[1 + (test ? 0 : 1)]); } private static Expression _define(Executor e, IList args) { - e.environment[((Symbol) args[0]).name] = e.eval(args[1]); + var refname = ((Symbol) args[0]).name; + e.environment.Set(refname, e.eval(args[1])); return new Compiler.Boolean(false); // NOOP } + private static Expression _lambda(Executor e, IList args) { + return new Procedure((Compiler.List) args[0], args[1]); + } private static Expression _apply(Executor e, IList args) { if (args[0].GetType() != typeof(Symbol)) { throw new ApplicationException(); @@ -279,10 +375,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } public class Executor { - Environment _environment; + IEnvironment _environment; Builtins _builtins; BuiltinsLater _builtinsLater; - public Executor(Environment environment, Builtins builtins, BuiltinsLater builtinsLater) { + public Executor(IEnvironment environment, Builtins builtins, BuiltinsLater builtinsLater) { _environment = environment; _builtins = builtins; _builtinsLater = builtinsLater; @@ -293,13 +389,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { _builtinsLater = new BuiltinsLater(); } - public Environment environment { get => _environment; } + public IEnvironment environment { get => _environment; } public Builtins builtins { get => _builtins; } public BuiltinsLater builtinsLater { get => _builtinsLater; } public Expression EvalFunction(Symbol fcname, IList args) { - if (_environment.ContainsKey(fcname.name)) { - Expression first = environment[fcname.name]; + if (_environment.Find(fcname.name) is IEnvironment _e) { + Expression? first = _e.Get(fcname.name); return new List(new []{first}.ToList()) + new List(args.Select(x => eval(x)).ToList()); } if (_builtins.ContainsKey(fcname.name)) { @@ -317,21 +413,31 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { public Expression eval(Expression expression) { switch (expression) { case Symbol s: - return _environment[s.name]; + return _environment.Find(s.name).Get(s.name); case Compiler.Boolean b: return b; case Integer i: return i; case Compiler.String s: return s; + case Procedure p: + return p; case List list: // do we really want to allow shadowing of builtins? if (list.expressions[0].GetType() == typeof(Symbol)) { - return EvalFunction((Symbol) list.expressions[0], list.expressions.Skip(1).ToList()); + return eval(EvalFunction((Symbol) list.expressions[0], list.expressions.Skip(1).ToList())); } - return new List(list.expressions.Select(x => eval(x)).ToList()); + if (list.expressions[0].GetType() == typeof(Procedure)) { + Procedure procedure = (Procedure) list.expressions[0]; + return eval(procedure.Call(this, list.expressions.Skip(1).ToList())); + } + var l = new List(list.expressions.Select(x => eval(x)).ToList()); + if (l.expressions[0].GetType() == typeof(Procedure)) { + return eval(l); + } + return l; } - throw new ApplicationException("Not handled case"); + throw new ApplicationException($"Not handled case '{expression}'"); } public Expression eval(Parser p) { return eval(p.parse()); diff --git a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs index 34925d6..8405967 100644 --- a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs +++ b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs @@ -105,9 +105,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { List results = new List(); Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); Executor executor = new Executor(); - executor.environment["user"] = new Lisp_Object(user); + executor.environment.Set("user", new Lisp_Object(user)); foreach (var i in items) { - executor.environment["item"] = new Lisp_Object(i); + executor.environment.Set("item", new Lisp_Object(i)); var r = executor.eval(expression); _logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString()); if (r is Lisp_Boolean r_bool) { diff --git a/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj b/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj index ae98220..604ef49 100644 --- a/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj +++ b/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj @@ -10,6 +10,7 @@ + diff --git a/Tests/Tests.cs b/Tests/Tests.cs index e102eb3..622f376 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -113,11 +113,27 @@ namespace Tests public static void ObjectTest() { Executor e = new Executor(); Expression r; - e.environment["o"] = new Lisp_Object(new O(5, false)); + e.environment.Set("o", new Lisp_Object(new O(5, false))); r = e.eval("(haskeys o 'i' 'b')"); Assert.Equal(((Lisp_Boolean)r).value, true); r = e.eval("(getitems o 'i' 'b')"); Assert.Equal(string.Format("{0}", r), "(5 nil)"); } + + [Fact] + public static void ProcedureTest() { + Executor e = new Executor(); + Expression r; + r = e.eval("((lambda (a) (* a a)) 2)"); + Assert.Equal(string.Format("{0}", r), "4"); + + r = e.eval("(begin (define mull (lambda (a) (* a a))) (mull 3))"); + Assert.Equal(string.Format("{0}", r), "9"); + + //r = e.eval(""" + //(begin (define pi 3.1415) 1) + //"""); + //Assert.Equal(string.Format("{0}", r), "1"); + } } }