diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs index e093266..3b14a05 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs @@ -211,7 +211,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { public override string ToString() { return _value.ToString(); } - public static Expression FromBase(object o) { + public static Expression FromBase(object? o) { + if (o == null) { + return new Boolean(false); + } switch (o) { case bool b: return new Boolean(b); diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs index ec6447f..fdf69f5 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs @@ -24,11 +24,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { class SpaceToken : Token { private SpaceToken(string value) : base(value) {} private static IToken? take(CharStream program) { + char[] spaces = [' ', '\n']; if (program.Available() == 0) { return null; } - if (program.Get() == ' ') { - return new SpaceToken(" "); + var t = program.Get(); + if (spaces.Contains(t)) { + return new SpaceToken(t.ToString()); } return null; } diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs index 0cbc2ad..15f3df6 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs @@ -125,6 +125,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { this["car"] = _car; this["cdr"] = _cdr; this["cons"] = _cons; + this["list"] = _list; this["not"] = _not; this["length"] = _length; this["haskeys"] = _haskeys; @@ -260,6 +261,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } throw new ApplicationException(); } + private static Expression _list(IList args) { + return new Compiler.List(args); + } private static Expression _not(IList args) { if (args[0] == new Compiler.Boolean(false)) { return new Compiler.Boolean(true); @@ -337,7 +341,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } private static Expression _define(Executor e, IList args) { var refname = ((Symbol) args[0]).name; - e.environment.Set(refname, e.eval(args[1])); + e.environment.Set(refname, args[1]); return new Compiler.Boolean(false); // NOOP } private static Expression _lambda(Executor e, IList args) { @@ -397,9 +401,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { public BuiltinsLater builtinsLater { get => _builtinsLater; } public Expression EvalFunction(Symbol fcname, IList args) { - 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 (_environment.Find(fcname.name) is IEnvironment _e && _e != null) { + Expression first = _e.Get(fcname.name); + return new List((new []{first}).Concat(args.ToArray()).Select(x => eval(x)).ToList()); } if (_builtins.ContainsKey(fcname.name)) { Function fc = _builtins[fcname.name]; @@ -416,7 +420,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { public Expression eval(Expression expression) { switch (expression) { case Symbol s: - return _environment.Find(s.name).Get(s.name); + if (_environment.Find(s.name) is not IEnvironment env) { + throw new ApplicationException($"Could not find '{s.name}'"); + } + return env.Get(s.name); case Compiler.Boolean b: return b; case Integer i: @@ -431,16 +438,14 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { if (list.expressions.Count == 0) { return list; } - // do we really want to allow shadowing of builtins? - if (list.expressions[0].GetType() == typeof(Symbol)) { - return eval(EvalFunction((Symbol) list.expressions[0], list.expressions.Skip(1).ToList())); + if (list.expressions[0] is Symbol fc_symbol) { + return eval(EvalFunction(fc_symbol, list.expressions.Skip(1).ToList())); } - if (list.expressions[0].GetType() == typeof(Procedure)) { - Procedure procedure = (Procedure) list.expressions[0]; + if (list.expressions[0] is Procedure procedure) { 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)) { + var l = new Compiler.List(list.expressions.Select(x => eval(x)).ToList()); + if (l.expressions[0] is Symbol|| l.expressions[0] is Procedure) { return eval(l); } return l; diff --git a/Tests/Tests.cs b/Tests/Tests.cs index 23ea543..a0997b2 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -93,19 +93,19 @@ namespace Tests e = new Executor().eval("(apply + (1 2))"); Assert.Equal(((Integer) e).value, 3); - e = new Executor().eval("(car (10 20 30))"); + e = new Executor().eval("(car (list 10 20 30))"); Assert.Equal(((Integer) e).value, 10); - e = new Executor().eval("(cdr (10 20 30))"); + e = new Executor().eval("(cdr (list 10 20 30))"); Assert.Equal(string.Format("{0}", e), "(20 30)"); e = new Executor().eval("(cons 1 3)"); Assert.Equal(string.Format("{0}", e), "(1 3)"); - e = new Executor().eval("(cons 1 (2 3))"); + e = new Executor().eval("(cons 1 (list 2 3))"); Assert.Equal(string.Format("{0}", e), "(1 2 3)"); - e = new Executor().eval("(length (cons 1 (2 3)))"); + e = new Executor().eval("(length (cons 1 (list 2 3)))"); Assert.Equal(string.Format("{0}", e), "3"); e = new Executor().eval("(>= 2 2)"); @@ -126,10 +126,10 @@ namespace Tests e = new Executor().eval("(or nil 4)"); Assert.Equal("4", e.ToString()); - e = new Executor().eval("(= (1 2) (1 2))"); + e = new Executor().eval("(= (list 1 2) (list 1 2))"); Assert.Equal(e.ToString(), "t"); - e = new Executor().eval("(= (1 2 3) (1 2))"); + e = new Executor().eval("(= (list 1 2 3) (list 1 2))"); Assert.Equal(e.ToString(), "nil"); } [Fact] @@ -165,19 +165,28 @@ namespace Tests r = e.eval("(begin (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1)))))) (fact 10))"); Assert.Equal(string.Format("{0}", r), "3628800"); - r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 3 (1 2 3 4)))"); + r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 3 (list 1 2 3 4)))"); Assert.Equal(string.Format("{0}", r), "3"); e = new Executor(); - r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 0 (1 2 3 4)))"); + r = e.eval("(begin (define find (lambda (item list) (if (= list ()) nil (if (= item (car list)) (car list) (find item (cdr list)))))) (find 0 (list 1 2 3 4)))"); Assert.Equal(string.Format("{0}", r), "nil"); + + e = new Executor(); + r = e.eval(@" + (begin + (define map (lambda (fc l) (if (= l ()) () (cons (fc (car l)) (map fc (cdr l)))))) + (define multwo (lambda (x) (* 2 x))) + (map multwo (list 1 2 3))) + "); + Assert.Equal(string.Format("{0}", r), "(2 4 6)"); } [Fact] public static void DefaultEnvironmentTest() { Executor e = new Executor(new DefaultEnvironment()); - Assert.Equal("nil", e.eval("(find 0 (1 2 3 4))").ToString()); - Assert.Equal("3", e.eval("(find 3 (1 2 3 4))").ToString()); + Assert.Equal("nil", e.eval("(find 0 (list 1 2 3 4))").ToString()); + Assert.Equal("3", e.eval("(find 3 (list 1 2 3 4))").ToString()); } } }