From 396384fd71a4a4bb05dd69a550a019ae07f698da Mon Sep 17 00:00:00 2001 From: redxef Date: Fri, 8 Nov 2024 03:08:29 +0100 Subject: [PATCH] feat(lisp): more builtins instead of derived. --- .../Lisp/Expression.cs | 21 +++++- .../Lisp/Interpreter.cs | 75 ++++++++++++++----- Tests/Tests.cs | 14 ++-- 3 files changed, 84 insertions(+), 26 deletions(-) diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Expression.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Expression.cs index 28d37ea..b27f686 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Expression.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Expression.cs @@ -29,6 +29,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { E Equals(T other); } + interface IInner { + public object Inner(); + } + public abstract class Expression: IComparable { public override abstract string? ToString(); public abstract override int GetHashCode(); @@ -48,7 +52,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } public abstract class Atom : Expression {} - public class Scalar : Atom where V : notnull { + public class Scalar : Atom, IInner where V : notnull { protected V _value; public Scalar(V value) { _value = value; @@ -68,6 +72,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { public V Value() { return _value; } + public object Inner() { + return _value; + } } public class Symbol : Atom { private string _name; @@ -276,18 +283,24 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { Executor new_e = new Executor(new SubEnvironment(e.environment), e.builtins, e.builtinsLater); var _params = _parameters.Select(x => x.Name()).ToArray(); var idx_rest = -1; + + IList<(string, Expression)> name_args = new List<(string, Expression)>(); for (var i = 0; i < _parameters.Count(); i++) { var name = _params[i]; if (name.Equals(".")) { idx_rest = i + 1; break; } - new_e.environment.Set(name, _eval(e, args[i])); + name_args.Add((name, _eval(e, args[i]))); } if (idx_rest > 0) { - new_e.environment.Set(_params[idx_rest], Cons.FromList(args.Skip(idx_rest - 1).Select(x => _eval(e, x)))); + name_args.Add((_params[idx_rest], Cons.FromList(args.Skip(idx_rest - 1).Select(x => _eval(e, x))))); } - return new_e.eval(_body); + foreach (var na in name_args) { + new_e.environment.Set(na.Item1, na.Item2); + } + var r = new_e.eval(_body); + return r; } } diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs index 371fbbd..1a1211d 100644 --- a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs @@ -39,15 +39,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { public class DefaultEnvironment: Environment { public DefaultEnvironment() { var e = new Executor(); - this["if"] = e.eval("(lambda* (condition a b) ( cond ((eval condition) (eval a)) (t (eval b))))"); this["null"] = new Symbol("not"); this["list"] = e.eval("(lambda (. args) args)"); this["find"] = e.eval("(lambda (item list_) (if (null list_) nil (if (= item (car list_)) (car list_) (find item (cdr list_)))))"); this["map"] = e.eval("(lambda (fc l) (if (null l) nil (cons (fc (car l)) (map fc (cdr l)))))"); - this["and"] = e.eval("(lambda (l) (if (null l) t (if (car l) (and (cdr l)) nil)))"); - this["or"] = e.eval("(lambda (l) (if (null l) nil (if (car l) t (or (cdr l)))))"); - this["any"] = e.eval("(lambda (fc l) (or (map fc l)))"); - this["all"] = e.eval("(lambda (fc l) (and (map fc l)))"); + this["fold"] = e.eval("(lambda (fc i l) (if (null l) i (fold fc (fc i (car l)) (cdr l))))"); + this["any"] = e.eval("(lambda (fc l) (apply or (map fc l)))"); + this["all"] = e.eval("(lambda (fc l) (apply and (map fc l)))"); } } @@ -104,7 +102,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { this[">"] = (x) => _cmp((Integer a, Integer b) => a > b, x); this[">="] = (x) => _cmp((Integer a, Integer b) => a >= b, x); this["!="] = (x) => _cmp((Integer a, Integer b) => a != b, x); - this["not"] = (x) => (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE; + this["not"] = (x) => { + return (x.First() == Boolean.FALSE) ? Boolean.TRUE : Boolean.FALSE; + }; + this["haskeys"] = _haskeys; this["getitems"] = _getitems; @@ -160,7 +161,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { return Boolean.TRUE; } private static Expression _getitems(IEnumerable args) { - Object o = (Object) args.First(); + Object o = new Object(((IInner) args.First()).Inner()); IList r = new List(); foreach (var e in args.Skip(1)) { String s = (String) e; @@ -180,7 +181,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } private static Expression _invoke(IEnumerable args) { - Object o = (Object) args.First(); + Object o = new Object(((IInner) args.First()).Inner()); String s = (String) args.Skip(1).First(); IEnumerable l; if (args.Skip(2).First() is Boolean lb && lb == Boolean.FALSE) { @@ -190,25 +191,31 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } else { throw new ApplicationException($"Expected a list of arguments, got {args.Skip(2).First()}"); } - - IList r = new List(); - MethodInfo? mi = o.Value().GetType().GetMethod(s.Value()); - if (mi == null) { - throw new ApplicationException($"{o.Value()} has not method {s.Value()}"); - } - object?[]? l_ = l.Select(x => { + object[]? l_ = l.Select(x => { switch (x) { case Integer s: return s.Value(); case Boolean b: return b.Value(); + case String s: + return s.Value(); case Object o: return o.Value(); case Cons c: return c.ToList().ToList(); } - return null; + throw new ApplicationException($"Unhandled value {x} (type {x.GetType()})"); }).ToArray(); + Type[] l_types = l_.Select( x => { + return x.GetType(); + }).ToArray(); + + IList r = new List(); + MethodInfo? mi = o.Value().GetType().GetMethod(s.Value(), l_types); + if (mi == null) { + throw new ApplicationException($"{o.Value()} has not method {s.Value()}"); + } + return Object.FromBase(mi.Invoke(o.Value(), l_)); } } @@ -218,11 +225,36 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { this["quote"] = _quote; this["eval"] = _eval; this["cond"] = _cond; + this["if"] = _if; this["define"] = _define; this["let"] = _let; this["let*"] = _let_star; this["lambda"] = _lambda; this["lambda*"] = _lambda_star; + this["apply"] = _apply; + + this["and"] = (e, x) => { + Expression? r = null; + foreach (var xi in x) { + r = e.eval(xi); + if (r == Boolean.FALSE) { + return r; + } + } + if (r is null) { + return Boolean.FALSE; + } + return r; + }; + this["or"] = (e, x) => { + foreach (var xi in x) { + var r = e.eval(xi); + if (r != Boolean.FALSE) { + return r; + } + } + return Boolean.FALSE; + }; } private static Expression _quote(Executor e, IEnumerable args) { return args.First(); @@ -243,6 +275,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } return Boolean.FALSE; } + private static Expression _if(Executor e, IEnumerable args) { + if (e.eval(args.First()).Equals(Boolean.FALSE)) { + return e.eval(args.Skip(2).First()); + } + return e.eval(args.Skip(1).First()); + } private static Expression _define(Executor e, IEnumerable args) { Symbol refname = (Symbol) args.First(); e.environment.Parent(true).Set(refname.Name(), args.Skip(1).Select(x => e.eval(x)).First()); @@ -269,7 +307,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } Symbol refname = (Symbol) pair_cons.Item1; Expression exp_ = ((Cons) pair_cons.Item2).Item1; - vars.Add((refname, exp_)); + vars.Add((refname, e.eval(exp_))); } foreach (var pair in vars) { new_e.environment.Set(pair.Item1.Name(), pair.Item2); @@ -294,6 +332,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp { } return new Procedure(proc_args, args.Skip(1).First(), false); } + private static Expression _apply(Executor e, IEnumerable args) { + return e.eval(new Cons(args.First(), e.eval(args.Skip(1).First()))); + } } public class Executor { diff --git a/Tests/Tests.cs b/Tests/Tests.cs index 0f370c6..439d934 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -192,7 +192,6 @@ namespace Tests (define null (lambda* (x) (cond ((eval x) nil) (t t)))) (null (quote (1 2)))) """).ToString()); - } [Fact] public static void ObjectTest() { @@ -212,18 +211,23 @@ namespace Tests Executor e = new Executor(new DefaultEnvironment()); Assert.Equal("1", e.eval("(if nil 0 1)").ToString()); Assert.Equal("0", e.eval("(if t 0 1)").ToString()); + Assert.Equal("5", e.eval("(if t (if t 5 nil) nil)").ToString()); + Assert.Equal("nil", e.eval("(if t (if nil 5 nil) nil)").ToString()); Assert.Equal("(1 2 3)", e.eval("(list 1 2 3)").ToString()); Assert.Equal("3", e.eval("(find 3 (list 1 2 3 4))").ToString()); Assert.Equal("nil", e.eval("(find 0 (list 1 2 3 4))").ToString()); Assert.Equal("(2 4 6)", e.eval("(map (lambda (x) (* x 2)) (quote (1 2 3)))").ToString()); - Assert.Equal("nil", e.eval("(and (quote (1 2 3 nil)))").ToString()); - Assert.Equal("t", e.eval("(and (quote (1 2 3 4)))").ToString()); - Assert.Equal("t", e.eval("(or (quote (nil nil 1 nil)))").ToString()); - Assert.Equal("nil", e.eval("(or (quote (nil nil nil nil)))").ToString()); + Assert.Equal("nil", e.eval("(and 1 2 3 nil)").ToString()); + Assert.Equal("t", e.eval("(and t t t t)").ToString()); + Assert.Equal("t", e.eval("(or nil nil t nil)").ToString()); + Assert.Equal("nil", e.eval("(or nil nil nil nil)").ToString()); Assert.Equal("t", e.eval("(any (lambda (x) (= x 2)) (list 1 2 3 4 5 6))").ToString()); Assert.Equal("nil", e.eval("(any (lambda (x) (= x 2)) (list 1 3 4 5 6))").ToString()); + Assert.Equal("nil", e.eval("(any (lambda (x) (= x 2)) nil)").ToString()); Assert.Equal("t", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 5))").ToString()); Assert.Equal("nil", e.eval("(all (lambda (x) (= 1 (% x 2))) (list 1 3 4 5))").ToString()); + Assert.Equal("nil", e.eval("(all (lambda (x) (= x 2)) nil)").ToString()); + Assert.Equal("10", e.eval("(fold (lambda (a b) (+ a b)) 0 (list 1 2 3 4))").ToString()); } } }