feat: allow procedures.
This commit is contained in:
parent
6662c2e1e5
commit
7dc3dd01ae
4 changed files with 138 additions and 15 deletions
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
||||||
|
@ -7,7 +6,99 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
using Function = Func<IList<Expression>, Expression>;
|
using Function = Func<IList<Expression>, Expression>;
|
||||||
using FunctionLater = Func<Executor, IList<Expression>, Expression>;
|
using FunctionLater = Func<Executor, IList<Expression>, Expression>;
|
||||||
|
|
||||||
public class Environment : Dictionary<string, Expression> {}
|
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<T1, T2>(IEnumerable<T1> a, IEnumerable<T2> 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<Expression> 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<string, Expression>(p, args)) {
|
||||||
|
new_e.environment.Set(tuple.Item1, tuple.Item2);
|
||||||
|
}
|
||||||
|
return new_e.eval(_body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IEnvironment<K, V> {
|
||||||
|
public V Get(K k);
|
||||||
|
public void Set(K k, V v);
|
||||||
|
public IEnvironment<K, V>? Find(K k);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Environment : Dictionary<string, Expression>, IEnvironment<string, Expression> {
|
||||||
|
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<string, Expression>? Find(string k) {
|
||||||
|
if (ContainsKey(k)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubEnvironment : Dictionary<string, Expression>, IEnvironment<string, Expression> {
|
||||||
|
private IEnvironment<string, Expression> _super;
|
||||||
|
public SubEnvironment(IEnvironment<string, Expression> 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<string, Expression>? Find(string k) {
|
||||||
|
if (ContainsKey(k)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return _super.Find(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class Builtins : Dictionary<string, Function> {
|
public class Builtins : Dictionary<string, Function> {
|
||||||
public Builtins() : base() {
|
public Builtins() : base() {
|
||||||
this["+"] = _add;
|
this["+"] = _add;
|
||||||
|
@ -237,6 +328,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
public BuiltinsLater() : base() {
|
public BuiltinsLater() : base() {
|
||||||
this["if"] = _if;
|
this["if"] = _if;
|
||||||
this["define"] = _define;
|
this["define"] = _define;
|
||||||
|
this["lambda"] = _lambda;
|
||||||
this["apply"] = _apply;
|
this["apply"] = _apply;
|
||||||
this["and"] = _and;
|
this["and"] = _and;
|
||||||
this["or"] = _or;
|
this["or"] = _or;
|
||||||
|
@ -246,9 +338,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
return e.eval(args[1 + (test ? 0 : 1)]);
|
return e.eval(args[1 + (test ? 0 : 1)]);
|
||||||
}
|
}
|
||||||
private static Expression _define(Executor e, IList<Expression> args) {
|
private static Expression _define(Executor e, IList<Expression> 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
|
return new Compiler.Boolean(false); // NOOP
|
||||||
}
|
}
|
||||||
|
private static Expression _lambda(Executor e, IList<Expression> args) {
|
||||||
|
return new Procedure((Compiler.List) args[0], args[1]);
|
||||||
|
}
|
||||||
private static Expression _apply(Executor e, IList<Expression> args) {
|
private static Expression _apply(Executor e, IList<Expression> args) {
|
||||||
if (args[0].GetType() != typeof(Symbol)) {
|
if (args[0].GetType() != typeof(Symbol)) {
|
||||||
throw new ApplicationException();
|
throw new ApplicationException();
|
||||||
|
@ -279,10 +375,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Executor {
|
public class Executor {
|
||||||
Environment _environment;
|
IEnvironment<string, Expression> _environment;
|
||||||
Builtins _builtins;
|
Builtins _builtins;
|
||||||
BuiltinsLater _builtinsLater;
|
BuiltinsLater _builtinsLater;
|
||||||
public Executor(Environment environment, Builtins builtins, BuiltinsLater builtinsLater) {
|
public Executor(IEnvironment<string, Expression> environment, Builtins builtins, BuiltinsLater builtinsLater) {
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
_builtins = builtins;
|
_builtins = builtins;
|
||||||
_builtinsLater = builtinsLater;
|
_builtinsLater = builtinsLater;
|
||||||
|
@ -293,13 +389,13 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
_builtinsLater = new BuiltinsLater();
|
_builtinsLater = new BuiltinsLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Environment environment { get => _environment; }
|
public IEnvironment<string, Expression> environment { get => _environment; }
|
||||||
public Builtins builtins { get => _builtins; }
|
public Builtins builtins { get => _builtins; }
|
||||||
public BuiltinsLater builtinsLater { get => _builtinsLater; }
|
public BuiltinsLater builtinsLater { get => _builtinsLater; }
|
||||||
|
|
||||||
public Expression EvalFunction(Symbol fcname, IList<Expression> args) {
|
public Expression EvalFunction(Symbol fcname, IList<Expression> args) {
|
||||||
if (_environment.ContainsKey(fcname.name)) {
|
if (_environment.Find(fcname.name) is IEnvironment<string, Expression> _e) {
|
||||||
Expression first = environment[fcname.name];
|
Expression? first = _e.Get(fcname.name);
|
||||||
return new List(new []{first}.ToList()) + new List(args.Select(x => eval(x)).ToList());
|
return new List(new []{first}.ToList()) + new List(args.Select(x => eval(x)).ToList());
|
||||||
}
|
}
|
||||||
if (_builtins.ContainsKey(fcname.name)) {
|
if (_builtins.ContainsKey(fcname.name)) {
|
||||||
|
@ -317,21 +413,31 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
public Expression eval(Expression expression) {
|
public Expression eval(Expression expression) {
|
||||||
switch (expression) {
|
switch (expression) {
|
||||||
case Symbol s:
|
case Symbol s:
|
||||||
return _environment[s.name];
|
return _environment.Find(s.name).Get(s.name);
|
||||||
case Compiler.Boolean b:
|
case Compiler.Boolean b:
|
||||||
return b;
|
return b;
|
||||||
case Integer i:
|
case Integer i:
|
||||||
return i;
|
return i;
|
||||||
case Compiler.String s:
|
case Compiler.String s:
|
||||||
return s;
|
return s;
|
||||||
|
case Procedure p:
|
||||||
|
return p;
|
||||||
case List list:
|
case List list:
|
||||||
// do we really want to allow shadowing of builtins?
|
// do we really want to allow shadowing of builtins?
|
||||||
if (list.expressions[0].GetType() == typeof(Symbol)) {
|
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) {
|
public Expression eval(Parser p) {
|
||||||
return eval(p.parse());
|
return eval(p.parse());
|
||||||
|
|
|
@ -105,9 +105,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
List<Guid> results = new List<Guid>();
|
List<Guid> results = new List<Guid>();
|
||||||
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
||||||
Executor executor = new Executor();
|
Executor executor = new Executor();
|
||||||
executor.environment["user"] = new Lisp_Object(user);
|
executor.environment.Set("user", new Lisp_Object(user));
|
||||||
foreach (var i in items) {
|
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);
|
var r = executor.eval(expression);
|
||||||
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
||||||
if (r is Lisp_Boolean r_bool) {
|
if (r is Lisp_Boolean r_bool) {
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Controller" Version="10.9.7" />
|
<PackageReference Include="Jellyfin.Controller" Version="10.9.7" />
|
||||||
<PackageReference Include="Jellyfin.Model" Version="10.9.7" />
|
<PackageReference Include="Jellyfin.Model" Version="10.9.7" />
|
||||||
|
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -113,11 +113,27 @@ namespace Tests
|
||||||
public static void ObjectTest() {
|
public static void ObjectTest() {
|
||||||
Executor e = new Executor();
|
Executor e = new Executor();
|
||||||
Expression r;
|
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')");
|
r = e.eval("(haskeys o 'i' 'b')");
|
||||||
Assert.Equal(((Lisp_Boolean)r).value, true);
|
Assert.Equal(((Lisp_Boolean)r).value, true);
|
||||||
r = e.eval("(getitems o 'i' 'b')");
|
r = e.eval("(getitems o 'i' 'b')");
|
||||||
Assert.Equal(string.Format("{0}", r), "(5 nil)");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue