commit 3365521283acc617934db8307349d03bfbc9a119 Author: redxef Date: Thu Jun 27 01:47:44 2024 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09e83c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/obj/ +**/bin/ +cache/ +config/ diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs new file mode 100644 index 0000000..1393ac6 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/Parser.cs @@ -0,0 +1,198 @@ +using System.Diagnostics; + +namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { + interface IAddable where T : IAddable { + static abstract T operator +(T left, T right); + } + + interface ISubtractable where T : ISubtractable { + static abstract T operator -(T left, T right); + } + + interface IMultiplicatable where T : IMultiplicatable { + static abstract T operator *(T left, T right); + } + + interface IDivisible where T : IDivisible { + static abstract T operator /(T left, T right); + static abstract T operator %(T left, T right); + } + + interface IComparable where T : IComparable { + static abstract E operator >(T left, T right); + static abstract E operator <(T left, T right); + static abstract E operator >=(T left, T right); + static abstract E operator <=(T left, T right); + static abstract E operator ==(T left, T right); + static abstract E operator !=(T left, T right); + } + + public abstract class Expression : IFormattable { + public abstract string ToString(string? format, IFormatProvider? provider); + } + public abstract class Atom : Expression {} + public class Symbol : Atom { + private readonly string _name; + public Symbol(string name) { + _name = name; + } + public string name { get => _name; } + public override string ToString(string? format, IFormatProvider? provider) { + return _name; + } + } + public class Boolean : Atom { + private readonly bool _value; + public Boolean(bool value) { + _value = value; + } + public bool value { get => _value; } + public override string ToString(string? format, IFormatProvider? provider) { + return _value? "t" : "nil"; + } + } + public class Integer : Atom, IAddable, ISubtractable, IMultiplicatable, IDivisible, IComparable { + private readonly int _value; + public Integer(int value) { + _value = value; + } + public int value { get => _value; } + public override string ToString(string? format, IFormatProvider? provider) { + return _value.ToString("0", provider); + } + public static Integer operator +(Integer a, Integer b) { + return new Integer(a.value + b.value); + } + public static Integer operator -(Integer a, Integer b) { + return new Integer(a.value - b.value); + } + public static Integer operator *(Integer a, Integer b) { + return new Integer(a.value * b.value); + } + public static Integer operator /(Integer a, Integer b) { + return new Integer(a.value / b.value); + } + public static Integer operator %(Integer a, Integer b) { + return new Integer(a.value % b.value); + } + public static Boolean operator >(Integer a, Integer b) { + return new Boolean(a.value > b.value); + } + public static Boolean operator <(Integer a, Integer b) { + return new Boolean(a.value < b.value); + } + public static Boolean operator >=(Integer a, Integer b) { + return new Boolean(a.value >= b.value); + } + public static Boolean operator <=(Integer a, Integer b) { + return new Boolean(a.value <= b.value); + } + public static Boolean operator ==(Integer a, Integer b) { + return new Boolean(a.value == b.value); + } + public static Boolean operator !=(Integer a, Integer b) { + return new Boolean(a.value != b.value); + } + } + public class String : Atom, IAddable { + private readonly string _value; + public String(string value) { + _value = value; + } + public string value { get => _value; } + public override string ToString(string? format, IFormatProvider? provider) { + return "\"" + _value + "\""; + } + public static String operator +(String a, String b) { + return new String (a.value + b.value); + } + } + public class List : Expression { + private IList _expressions; + public List(IList expressions) { + _expressions = expressions; + } + public IList expressions { get => _expressions; } + public override string ToString(string? format, IFormatProvider? provider) { + string r = "("; + foreach (var e in _expressions) { + r += " "; + r += e.ToString("0", provider); + } + return r + ")"; + } + public static List operator +(List a, List b) { + IList r = new List(); + r.Concat(a.expressions); + r.Concat(b.expressions); + return new List (r); + } + } + + public class Parser { + private StringTokenStream _sts; + public Parser(StringTokenStream tokens) { + _sts = tokens; + } + + public Expression parse() { + Token token = _sts.get(); + switch (token) { + case GroupingToken gt: + return parse_grouping(gt, gt.closing_value); + case AtomToken at: + return parse_atom(at); + case OperatorToken ot: + return new Symbol(ot.value); + case SpaceToken sp: + return parse(); + } + return parse(); + } + + Expression parse_string(GroupingToken start, GroupingToken end) { + Debug.Assert(start == end); + Debug.Assert("'\"".Contains(start.value)); + string r = ""; + while (_sts.available() > 0) { + Token t = _sts.get(); + if (t == end) { + break; + } + r += t.value; + } + _sts.commit(); + return new String(r); + } + + Expression parse_grouping(GroupingToken start, GroupingToken end) { + IList expressions = new List(); + while (_sts.available() > 0) { + Token t = _sts.get(); + if (t.Equals(end)) { + _sts.commit(); + break; + } + _sts.rewind(1); + expressions.Add(parse()); + } + return new List(expressions); + } + + Expression parse_atom(AtomToken at) { + int parsed_value; + if (int.TryParse(at.value, out parsed_value)) { + _sts.commit(); + return new Integer(parsed_value); + } + if (at.value.Equals("t")) { + return new Boolean(true); + } + if (at.value.Equals("nil")) { + return new Boolean(false); + } + _sts.commit(); + return new Symbol(at.value); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs new file mode 100644 index 0000000..58f5925 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Compiler/TokenStream.cs @@ -0,0 +1,144 @@ +using System.Reflection; +using Jellyfin.Plugin.SmartPlaylist.Util; + +namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler { + public interface IToken { + T value { get; } + abstract static IToken? take(E program); + } + + public abstract class Token: IToken , IEquatable> { + protected readonly T _value; + protected Token(T value) { + _value = value; + } + public T value { get => _value; } + public static IToken? take(E program) { + throw new NotImplementedException("Subclass this class"); + } + public bool Equals(Token? b) { + return b != null && _value != null && _value.Equals(b._value); + } + } + + class SpaceToken : Token { + private SpaceToken(string value) : base(value) {} + private static IToken? take(CharStream program) { + if (program.available() == 0) { + return null; + } + if (program.get() == ' ') { + return new SpaceToken(" "); + } + return null; + } + } + + class GroupingToken: Token { + private GroupingToken(string value) : base(value) {} + private static IToken? take(CharStream program) { + if (program.available() == 0) { + return null; + } + char t = program.get(); + if ("()\"'".Contains(t)) { + return new GroupingToken(t.ToString()); + } + return null; + } + private GroupingToken? _closing_value() { + if (_value == "(") { + return new GroupingToken(")"); + } else if (_value == ")") { + return null; + } + return new GroupingToken(_value); + } + public GroupingToken? closing_value { get => _closing_value(); } + } + + class AtomToken : Token { + private AtomToken(string value) : base(value) {} + private static IToken? take(CharStream program) { + string value = ""; + while (program.available() > 0) { + char t = program.get(); + if (!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Contains(t)) { + if (value.Equals("")) { + return null; + } + program.rewind(1); + return new AtomToken(value); + } + value += t; + } + return null; + } + } + + class OperatorToken : Token { + private OperatorToken(string value) : base(value) {} + private static IToken? take(CharStream program) { + if (program.available() == 0) { + return null; + } + return new OperatorToken(program.get().ToString()); + //char t = program.get(); + //if ("+-*/%".Contains(t)) { + // return new OperatorToken(t.ToString()); + //} + //return null; + } + } + + class CharStream: Stream { + public CharStream(IList items) : base(items) {} + public CharStream(string items) : base(items.ToCharArray().Cast().ToList()) {} + } + + public class StringTokenStream : Stream> { + private static readonly IList _classes = new List { + typeof(SpaceToken), + typeof(GroupingToken), + typeof(AtomToken), + typeof(OperatorToken), + }; + protected StringTokenStream(IList> tokens) : base(tokens) {} + private static StringTokenStream generate(CharStream program) { + IList> result = new List>(); + int prev_avail = 0; + while (true) { + if (prev_avail == program.available() && prev_avail == 0) { + break; + } else if (prev_avail == program.available()) { + throw new ApplicationException("Program is invalid"); + } + prev_avail = program.available(); + foreach (Type c in _classes) { + Token? t = (Token?) c.GetMethod( + "take", + BindingFlags.NonPublic | BindingFlags.Static, + null, + CallingConventions.Any, + new Type[] { typeof(CharStream) }, + null + )?.Invoke( + null, + new object[]{program} + ); + if (t == null) { + program.rewind(); + continue; + } + program.commit(); + result.Add(t); + break; + } + } + return new StringTokenStream(result); + } + public static StringTokenStream generate(string program) { + return StringTokenStream.generate(new CharStream(program)); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs new file mode 100644 index 0000000..70e3d06 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Lisp/Interpreter.cs @@ -0,0 +1,241 @@ +using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; + +namespace Jellyfin.Plugin.SmartPlaylist.Lisp { + public class EnvironmentEntry {}; + public class Entry: EnvironmentEntry { + private readonly Expression _expression; + public Entry(Expression expression) { + _expression = expression; + } + public Expression expression { get => _expression; } + }; + public class NOOPEntry: Entry { + public NOOPEntry() : base(new Compiler.Boolean(false)) {} + } + public class Function : EnvironmentEntry { + private readonly Func, Expression> _func; + public Function(Func, Expression> func) { + _func = func; + } + public Func, Expression> func { get => _func; } + } + + public class Environment : Dictionary { + public static Environment create() { + Environment e = new Environment(); + e.Add("+", new Function(op_add)); + e.Add("-", new Function(op_sub)); + e.Add("*", new Function(op_mul)); + e.Add("/", new Function(op_div)); + e.Add("%", new Function(op_rem)); + e.Add(">", new Function(op_gt)); + e.Add("<", new Function(op_lt)); + e.Add(">=", new Function(op_ge)); + e.Add("<=", new Function(op_le)); + e.Add("==", new Function(op_eq)); + e.Add("!=", new Function(op_ne)); + e.Add("abs", new Function(op_abs)); + e.Add("append", new Function(op_append)); + e.Add("apply", new Function(op_apply)); + e.Add("begin", new Function(op_begin)); + return e; + } + + private static T op_agg(Func op, IList args) { + T agg = args[0]; + foreach (var arg in args.Skip(1)) { + agg = op(agg, arg); + } + return agg; + } + + private static Expression op_add(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_agg((a, b) => a + b, args.Select(x => (Integer) x).ToList()); + case Compiler.String s: + return op_agg((a, b) => a + b, args.Select(x => (Compiler.String) x).ToList()); + //case Compiler.List: + // return op_agg((a, b) => a + b, args.Select(x => (Compiler.List) x).ToList()); + } + throw new ApplicationException("Don't know how to add these types"); + } + + private static Expression op_sub(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_agg((a, b) => a - b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_mul(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_agg((a, b) => a * b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_div(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_agg((a, b) => a / b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_rem(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_agg((a, b) => a % b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static E op_cmp(Func op, IList args) { + T first = args[0]; + T second = args[1]; + return op(first, second); + } + + private static Expression op_gt(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_cmp((a, b) => a > b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_lt(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_cmp((a, b) => a < b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_ge(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_cmp((a, b) => a >= b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_le(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_cmp((a, b) => a <= b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_eq(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_cmp((a, b) => a == b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_ne(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return op_cmp((a, b) => a != b, args.Select(x => (Integer) x).ToList()); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_abs(IList args) { + Expression first = args[0]; + switch (first) { + case Integer i: + return i.value >= 0 ? i : new Integer(-i.value); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_append(IList args) { + Expression first = args[0]; + switch (first) { + case List l: + return l + new List(args); + } + throw new ApplicationException("Don't know how to subtract these types"); + } + + private static Expression op_apply(IList args) { + IList e_list = (IList) args.Select(x => new Entry(x)).ToList(); + if (e_list.Count != 2) { + throw new ApplicationException("Expected exactly two arguments"); + } + if (e_list[0].GetType() != typeof(Function)) { + throw new ApplicationException("Expected first argument to be a function to apply"); + } + Function f = (Function) e_list[0]; + IList new_args = e_list.Skip(1).Select(x => ((Entry) x).expression).ToList(); + return f.func(args); + } + + private static Expression op_begin(IList args) { + return args.Last(); + } + + public EnvironmentEntry eval(Expression expression) { + switch (expression) { + case Symbol s: + return this[s.name]; + case Integer i: + return new Entry(i); + case Compiler.String s_: + return new Entry(s_); + case List list: + if (list.expressions[0].GetType() == typeof(Symbol)) { + if (((Symbol) list.expressions[0]).name.Equals("if")) { + Compiler.Boolean test = (Compiler.Boolean) ((Entry) eval(list.expressions[1])).expression; + return eval(list.expressions[2 + (test.value? 0 : 1)]); + } + if (((Symbol) list.expressions[0]).name.Equals("define")) { + Symbol test; + if (list.expressions[1].GetType() == typeof(Symbol)) { + test = (Symbol) list.expressions[1]; + } else { + test = (Symbol) ((Entry) eval(list.expressions[1])).expression; + } + this[test.name] = eval(list.expressions[2]); + return new NOOPEntry(); + } + } + IList e_list = list.expressions.Select(x => eval(x)).ToList(); + if (e_list.Count > 0 && e_list[0].GetType() == typeof(Function)) { + Function f = (Function) e_list[0]; + IList args = e_list.Skip(1).Select(x => ((Entry) x).expression).ToList(); + return new Entry(f.func(args)); + } + return new Entry(new List(list.expressions.Select(x => ((Entry) eval(x)).expression).ToList())); + } + throw new ApplicationException("Not handled case"); + } + public EnvironmentEntry eval(Parser p) { + return eval(p.parse()); + } + public EnvironmentEntry eval(StringTokenStream sts) { + return eval(new Parser(sts)); + } + public EnvironmentEntry eval(string p) { + return eval(StringTokenStream.generate(p)); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Plugin.cs b/Jellyfin.Plugin.SmartPlaylist/Plugin.cs new file mode 100644 index 0000000..c51caf5 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Plugin.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace Jellyfin.Plugin.SmartPlaylist { + public class Plugin : BasePlugin, IHasWebPages { + public Plugin( + IApplicationPaths applicationPaths, + IXmlSerializer xmlSerializer + ) : base (applicationPaths, xmlSerializer) { + Instance = this; + } + public static Plugin? Instance {get; private set; } + public override string Name => "Smart Playlist"; + public override Guid Id => Guid.Parse("dd2326e3-4d3e-4bfc-80e6-28502c1131df"); + public IEnumerable GetPages() { + return new[] { + new PluginPageInfo { + Name = this.Name, + EmbeddedResourcePath = string.Format(CultureInfo.InvariantCulture, "{0}.configPage.html", GetType().Namespace) + } + }; + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs b/Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs new file mode 100644 index 0000000..ee2e7f6 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/PluginConfiguration.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; + +namespace Jellyfin.Plugin.SmartPlaylist { + public class PluginConfiguration : BasePluginConfiguration { + public PluginConfiguration( + ) { + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs new file mode 100644 index 0000000..3f69d19 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/ScheduledTasks/GeneratePlaylist.cs @@ -0,0 +1,34 @@ +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; + +namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { + public class GeneratePlaylist : IScheduledTask { + private readonly ILogger _logger; + public GeneratePlaylist( + ILogger logger + ) { + _logger = logger; + _logger.LogInformation("Constructed Task"); + } + public string Category => "Library"; + public string Name => "(re)generate Smart Playlists"; + public string Description => "Generate or regenerate all Smart Playlists"; + public string Key => nameof(GeneratePlaylist); + public IEnumerable GetDefaultTriggers() { + return new[] { + new TaskTriggerInfo { + IntervalTicks = TimeSpan.FromMinutes(1).Ticks, + Type = TaskTriggerInfo.TriggerInterval, + } + }; + } + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { + _logger.LogInformation("This is a test"); + } + } +} diff --git a/Jellyfin.Plugin.SmartPlaylist/Util/Stream.cs b/Jellyfin.Plugin.SmartPlaylist/Util/Stream.cs new file mode 100644 index 0000000..81bb9d8 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/Util/Stream.cs @@ -0,0 +1,47 @@ +namespace Jellyfin.Plugin.SmartPlaylist.Util { + public interface IStream { + int available(); + T get(); + int commit(); + int rewind(); + int rewind(int n); + } + + public class Stream : IStream { + private readonly IList _items; + private int _cursor; + private int _ephemeral_cursor; + + protected Stream(IList items) { + _items = items; + _cursor = 0; + _ephemeral_cursor = 0; + } + + public int available() { + return _items.Count - _ephemeral_cursor; + } + public T get() { + return _items[_ephemeral_cursor++]; + } + public int commit() { + int diff = _ephemeral_cursor - _cursor; + _cursor = _ephemeral_cursor; + return diff; + } + public int rewind() { + int diff = _ephemeral_cursor - _cursor; + _ephemeral_cursor = _cursor; + return diff; + } + public int rewind(int n) { + int diff = _ephemeral_cursor - _cursor; + if (diff < n) { + n = diff; + } + _ephemeral_cursor -= n; + return n; + } + } +} + diff --git a/Jellyfin.Plugin.SmartPlaylist/configPage.html b/Jellyfin.Plugin.SmartPlaylist/configPage.html new file mode 100644 index 0000000..a90e708 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/configPage.html @@ -0,0 +1,79 @@ + + + + + Template + + +
+
+
+
+
+ + +
+
+ + +
A Description
+
+
+ +
+
+ + +
Another Description
+
+
+ +
+
+
+
+ +
+ + diff --git a/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj b/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj new file mode 100644 index 0000000..ae98220 --- /dev/null +++ b/Jellyfin.Plugin.SmartPlaylist/jellyfin-smart-playlist.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + jellyfin_smart_playlist + enable + enable + + + + + + + + diff --git a/Test/test.sh b/Test/test.sh new file mode 100755 index 0000000..19eefab --- /dev/null +++ b/Test/test.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -eu +cd "$(dirname "$0")" +pwd +( + cd .. + dotnet build +) +pwd +mkdir -p ./cache ./config/plugins/jellyfin-smart-playlist +cp ../bin/Debug/net8.0/jellyfin-smart-playlist.dll ./config/plugins/jellyfin-smart-playlist/ +docker run --rm --user "$(id -u):$(id -g)" \ + -v ./cache:/cache \ + -v ./config:/config \ + -p 8096:8096 \ + jellyfin/jellyfin diff --git a/Tests/GlobalUsings.cs b/Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Tests/Tests.cs b/Tests/Tests.cs new file mode 100644 index 0000000..d9efa0f --- /dev/null +++ b/Tests/Tests.cs @@ -0,0 +1,68 @@ +using Xunit; +using Lisp_Environment = Jellyfin.Plugin.SmartPlaylist.Lisp.Environment; +using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean; +using Jellyfin.Plugin.SmartPlaylist.Lisp; +using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler; + +namespace Tests +{ + public class Test { + [Fact] + public static void StringTokenStreamTest() { + StringTokenStream sts = StringTokenStream.generate("(\"some literal string\" def ghj +100 -+300 1)"); + Assert.Equal(sts.get().value, "("); + Assert.Equal(sts.get().value, "\""); + Assert.Equal(sts.get().value, "some"); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "literal"); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "string"); + Assert.Equal(sts.get().value, "\""); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "def"); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "ghj"); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "+"); + Assert.Equal(sts.get().value, "100"); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "-"); + Assert.Equal(sts.get().value, "+"); + Assert.Equal(sts.get().value, "300"); + Assert.Equal(sts.get().value, " "); + Assert.Equal(sts.get().value, "1"); + Assert.Equal(sts.get().value, ")"); + sts.commit(); + Assert.Equal(sts.available(), 0); + } + + [Fact] + public static void ParserTest() { + string program = "( + 1 ( * 2 3))"; + StringTokenStream sts = StringTokenStream.generate(program); + Parser p = new Parser(sts); + Assert.Equal(program, string.Format("{0}", p.parse())); + } + + [Fact] + public static void EvaluateTest() { + Expression p = new Parser(StringTokenStream.generate("(+ 5 (+ 1 2 3))")).parse(); + var e = Lisp_Environment.create(); + Entry r = (Entry) e.eval(p); + Assert.Equal(((Integer) r.expression).value, 11); + + r = (Entry) Lisp_Environment.create().eval("(> 1 2)"); + Assert.Equal(((Lisp_Boolean) r.expression).value, false); + + r = (Entry) Lisp_Environment.create().eval("(if (> 1 2) 3 4)"); + Assert.Equal(((Integer) r.expression).value, 4); + + r = (Entry) Lisp_Environment.create().eval("(begin (define x 1) 4)"); + Assert.Equal(((Integer) r.expression).value, 4); + + + r = (Entry) Lisp_Environment.create().eval("(apply + (1 2))"); + Assert.Equal(((Integer) r.expression).value, 3); + } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 0000000..c512b7f --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + +