aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Runs/Scripts/Condition.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Core/Runs/Scripts/Condition.cs')
-rw-r--r--src/Godosa.Core/Runs/Scripts/Condition.cs234
1 files changed, 234 insertions, 0 deletions
diff --git a/src/Godosa.Core/Runs/Scripts/Condition.cs b/src/Godosa.Core/Runs/Scripts/Condition.cs
new file mode 100644
index 0000000..346ee3b
--- /dev/null
+++ b/src/Godosa.Core/Runs/Scripts/Condition.cs
@@ -0,0 +1,234 @@
+// Provenance: own design — shared run scripts (spec §3.6)
+using System.Globalization;
+using System.Text;
+
+namespace Godosa.Core.Runs.Scripts;
+
+public interface IConditionContext
+{
+ bool HasProbe(string name);
+ RunValue Probe(string name, IReadOnlyList<string> args);
+ long? Constant(string name);
+}
+
+/// <summary>Condition AST: or / and / not / comparison / atom. A bare identifier is a probe without arguments when the
+/// game has one by that name, else a game constant.</summary>
+public abstract record Cond
+{
+ public abstract RunValue Eval(IConditionContext c);
+
+ public bool Test(IConditionContext c) => Truthy(Eval(c));
+
+ public abstract IEnumerable<(string Name, IReadOnlyList<string> Args)> Probes();
+
+ public abstract IEnumerable<string> Bare();
+
+ public static string Describe(Cond cond, IConditionContext c) =>
+ string.Join(" ", cond.Probes().Where(p => c.HasProbe(p.Name)).Distinct().Select(p =>
+ {
+ var label = p.Args.Count == 0 ? p.Name : $"{p.Name}({string.Join(",", p.Args)})";
+ return $"{label}={c.Probe(p.Name, p.Args)}";
+ }));
+
+ internal static bool Truthy(RunValue v) => v.IsNumber ? v.Number != 0 : !string.IsNullOrEmpty(v.Text);
+}
+
+public sealed record OrCond(Cond A, Cond B) : Cond
+{
+ public override RunValue Eval(IConditionContext c) => RunValue.Of(A.Test(c) || B.Test(c));
+ public override IEnumerable<(string, IReadOnlyList<string>)> Probes() => A.Probes().Concat(B.Probes());
+ public override IEnumerable<string> Bare() => A.Bare().Concat(B.Bare());
+}
+
+public sealed record AndCond(Cond A, Cond B) : Cond
+{
+ public override RunValue Eval(IConditionContext c) => RunValue.Of(A.Test(c) && B.Test(c));
+ public override IEnumerable<(string, IReadOnlyList<string>)> Probes() => A.Probes().Concat(B.Probes());
+ public override IEnumerable<string> Bare() => A.Bare().Concat(B.Bare());
+}
+
+public sealed record NotCond(Cond A) : Cond
+{
+ public override RunValue Eval(IConditionContext c) => RunValue.Of(!A.Test(c));
+ public override IEnumerable<(string, IReadOnlyList<string>)> Probes() => A.Probes();
+ public override IEnumerable<string> Bare() => A.Bare();
+}
+
+public sealed record CompareCond(Cond A, string Op, Cond B) : Cond
+{
+ public override RunValue Eval(IConditionContext c)
+ {
+ var (a, b) = (A.Eval(c), B.Eval(c));
+ if (a.IsNumber != b.IsNumber)
+ throw new ScriptFault($"cannot compare {a} with {b}");
+ var order = a.IsNumber ? a.Number!.Value.CompareTo(b.Number!.Value) : string.CompareOrdinal(a.Text, b.Text);
+ if (!a.IsNumber && Op is "<" or "<=" or ">" or ">=")
+ throw new ScriptFault($"cannot order strings {a} and {b}");
+ return RunValue.Of(Op switch
+ {
+ "=" => order == 0, "!=" => order != 0, "<" => order < 0, "<=" => order <= 0, ">" => order > 0, _ => order >= 0,
+ });
+ }
+
+ public override IEnumerable<(string, IReadOnlyList<string>)> Probes() => A.Probes().Concat(B.Probes());
+ public override IEnumerable<string> Bare() => A.Bare().Concat(B.Bare());
+}
+
+public sealed record LiteralCond(RunValue Value) : Cond
+{
+ public override RunValue Eval(IConditionContext c) => Value;
+ public override IEnumerable<(string, IReadOnlyList<string>)> Probes() => [];
+ public override IEnumerable<string> Bare() => [];
+}
+
+/// <summary>A probe call <c>name(args)</c>, or a bare <c>name</c> (probe if the game has it, else a constant).</summary>
+public sealed record NameCond(string Name, IReadOnlyList<string> Args, bool HasParens) : Cond
+{
+ public override RunValue Eval(IConditionContext c)
+ {
+ if (HasParens || c.HasProbe(Name))
+ return c.Probe(Name, Args);
+ return c.Constant(Name) is { } n ? RunValue.Of(n) : throw new ScriptFault($"unknown probe or constant {Name}");
+ }
+
+ public override IEnumerable<(string, IReadOnlyList<string>)> Probes() => [(Name, Args)];
+ public override IEnumerable<string> Bare() => HasParens ? [] : [Name];
+}
+
+public static class ConditionParser
+{
+ public static Cond Parse(string text)
+ {
+ var tokens = Lex(text);
+ var pos = 0;
+ var cond = Or(tokens, ref pos);
+ if (pos != tokens.Count)
+ throw new FormatException($"unexpected '{tokens[pos]}' in condition '{text}'");
+ return cond;
+ }
+
+ private static Cond Or(List<string> t, ref int pos)
+ {
+ var a = And(t, ref pos);
+ while (pos < t.Count && t[pos] == "or")
+ {
+ pos++;
+ a = new OrCond(a, And(t, ref pos));
+ }
+ return a;
+ }
+
+ private static Cond And(List<string> t, ref int pos)
+ {
+ var a = Not(t, ref pos);
+ while (pos < t.Count && t[pos] == "and")
+ {
+ pos++;
+ a = new AndCond(a, Not(t, ref pos));
+ }
+ return a;
+ }
+
+ private static Cond Not(List<string> t, ref int pos)
+ {
+ if (pos < t.Count && t[pos] == "not")
+ {
+ pos++;
+ return new NotCond(Not(t, ref pos));
+ }
+ return Compare(t, ref pos);
+ }
+
+ private static Cond Compare(List<string> t, ref int pos)
+ {
+ var a = Atom(t, ref pos);
+ if (pos < t.Count && t[pos] is "=" or "!=" or "<" or "<=" or ">" or ">=")
+ {
+ var op = t[pos++];
+ return new CompareCond(a, op, Atom(t, ref pos));
+ }
+ return a;
+ }
+
+ private static Cond Atom(List<string> t, ref int pos)
+ {
+ if (pos >= t.Count)
+ throw new FormatException("condition ends early");
+ var tok = t[pos++];
+ if (tok == "(")
+ {
+ var inner = Or(t, ref pos);
+ if (pos >= t.Count || t[pos] != ")")
+ throw new FormatException("missing ')'");
+ pos++;
+ return inner;
+ }
+ if (tok.StartsWith('"'))
+ return new LiteralCond(RunValue.Of(tok[1..^1]));
+ if (long.TryParse(tok, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var n))
+ return new LiteralCond(RunValue.Of(n));
+ if (!char.IsLetter(tok[0]) && tok[0] != '_')
+ throw new FormatException($"unexpected '{tok}'");
+ var paren = tok.IndexOf('(');
+ if (paren < 0)
+ return new NameCond(tok, [], false);
+ var args = tok[(paren + 1)..^1].Split(',', StringSplitOptions.TrimEntries).Where(a => a.Length > 0).ToArray();
+ return new NameCond(tok[..paren], args, true);
+ }
+
+ private static List<string> Lex(string text)
+ {
+ var tokens = new List<string>();
+ var i = 0;
+ while (i < text.Length)
+ {
+ var c = text[i];
+ if (c == ' ')
+ {
+ i++;
+ continue;
+ }
+ if (c is '(' or ')')
+ {
+ tokens.Add(c.ToString());
+ i++;
+ continue;
+ }
+ if (c == '"')
+ {
+ var end = text.IndexOf('"', i + 1);
+ if (end < 0)
+ throw new FormatException("unterminated quote");
+ tokens.Add(text[i..(end + 1)]);
+ i = end + 1;
+ continue;
+ }
+ if (c is '=' or '!' or '<' or '>')
+ {
+ var two = i + 1 < text.Length && text[i + 1] == '=';
+ var op = text.Substring(i, two ? 2 : 1);
+ if (op == "!")
+ throw new FormatException("use != or not");
+ tokens.Add(op);
+ i += op.Length;
+ continue;
+ }
+ var sb = new StringBuilder();
+ var depth = 0;
+ while (i < text.Length && (depth > 0 || (text[i] != ' ' && text[i] != ')' && !(text[i] is '=' or '!' or '<' or '>'))))
+ {
+ if (text[i] == '(' && sb.Length == 0)
+ break;
+ if (text[i] == '(')
+ depth++;
+ else if (text[i] == ')')
+ depth--;
+ sb.Append(text[i++]);
+ }
+ if (sb.Length == 0)
+ throw new FormatException($"unexpected '{text[i]}'");
+ tokens.Add(sb.ToString());
+ }
+ return tokens;
+ }
+}