aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Runs/Scripts/ScriptValidator.cs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core/Runs/Scripts/ScriptValidator.cs
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core/Runs/Scripts/ScriptValidator.cs')
-rw-r--r--src/Godosa.Core/Runs/Scripts/ScriptValidator.cs109
1 files changed, 109 insertions, 0 deletions
diff --git a/src/Godosa.Core/Runs/Scripts/ScriptValidator.cs b/src/Godosa.Core/Runs/Scripts/ScriptValidator.cs
new file mode 100644
index 0000000..e67c48a
--- /dev/null
+++ b/src/Godosa.Core/Runs/Scripts/ScriptValidator.cs
@@ -0,0 +1,109 @@
+// Provenance: own design — shared run scripts (spec §3.7)
+namespace Godosa.Core.Runs.Scripts;
+
+public static class ScriptValidator
+{
+ public static List<ScriptError> Validate(ScriptProgram p, IRunGame game, bool deterministic)
+ {
+ var errors = new List<ScriptError>(p.Errors);
+ foreach (var (name, action) in game.Actions)
+ {
+ if (LibVerbs.Find(name) is { } lib && lib.Kind != action.Kind)
+ errors.Add(new ScriptError(new SourceRef(p.Entry, 1), $"game verb {name} shadows a lib verb with another kind"));
+ else if (LibVerbs.Find(name) is { GameHook: false })
+ errors.Add(new ScriptError(new SourceRef(p.Entry, 1), $"game verb {name} shadows a lib verb"));
+ }
+ var inSetup = false;
+ foreach (var i in p.Code)
+ {
+ switch (i.Op)
+ {
+ case Op.SetupBegin:
+ inSetup = true;
+ break;
+ case Op.SetupEnd:
+ inSetup = false;
+ break;
+ case Op.TryBegin when deterministic:
+ errors.Add(new ScriptError(i.At, "try needs a non-deterministic run"));
+ break;
+ case Op.Verb:
+ Verb(errors, i, game, p.Mode, inSetup);
+ break;
+ case Op.Wait or Op.Expect:
+ Condition(errors, i, game);
+ break;
+ }
+ }
+ return errors;
+ }
+
+ private static void Verb(List<ScriptError> errors, Instr i, IRunGame game, RunMode mode, bool inSetup)
+ {
+ var lib = LibVerbs.Find(i.A);
+ var gameVerb = lib == null ? game.Actions.GetValueOrDefault(i.A) : null;
+ if (lib == null && gameVerb == null)
+ {
+ errors.Add(new ScriptError(i.At, $"unknown verb {i.A}"));
+ return;
+ }
+ if ((lib is { GameHook: true } && !game.Actions.ContainsKey(i.A)) || (lib?.Name == "rng" && game.Rng == null))
+ {
+ errors.Add(new ScriptError(i.At, $"verb {i.A} not supported by game {game.Name}"));
+ return;
+ }
+ var (kind, min, max) = lib != null ? (lib.Kind, lib.MinArgs, lib.MaxArgs) : (gameVerb!.Kind, gameVerb.MinArgs, gameVerb.MaxArgs);
+ List<Token> tokens;
+ try
+ {
+ tokens = StepLexer.Tokens(i.B);
+ }
+ catch (FormatException e)
+ {
+ errors.Add(new ScriptError(i.At, e.Message));
+ return;
+ }
+ if (tokens.Count < min || tokens.Count > max)
+ errors.Add(new ScriptError(i.At, $"verb {i.A} takes {min}..{max} arguments, got {tokens.Count}"));
+ if (kind == VerbKind.Debug && mode == RunMode.Player && !inSetup)
+ errors.Add(new ScriptError(i.At, $"debug verb {i.A} outside setup in player mode"));
+ if (i.A == "output" && tokens.Count == 1 && !LibVerbs.OutputLevels.Contains(tokens[0].Text))
+ errors.Add(new ScriptError(i.At, "output level must be quiet|checkpoints|labels|steps|trace"));
+ foreach (var t in tokens.Where(t => !t.Quoted && t.Text.StartsWith('@')))
+ {
+ var name = t.Text[1..(t.Text.IndexOf('(') is var p and > 0 ? p : t.Text.Length)];
+ if (!game.Locators.ContainsKey(name))
+ errors.Add(new ScriptError(i.At, $"unknown locator @{name}"));
+ }
+ }
+
+ private static void Condition(List<ScriptError> errors, Instr i, IRunGame game)
+ {
+ if (i.A.Contains("${", StringComparison.Ordinal))
+ return; // expanded at run time; checked then
+ Cond cond;
+ try
+ {
+ cond = ConditionParser.Parse(i.A);
+ }
+ catch (FormatException e)
+ {
+ errors.Add(new ScriptError(i.At, e.Message));
+ return;
+ }
+ foreach (var bare in cond.Bare())
+ {
+ if (!game.Probes.ContainsKey(bare) && !game.Constants.ContainsKey(bare))
+ errors.Add(new ScriptError(i.At, $"unknown probe or constant {bare}"));
+ }
+ foreach (var (name, args) in cond.Probes())
+ {
+ if (cond.Bare().Contains(name) && args.Count == 0)
+ continue;
+ if (!game.Probes.TryGetValue(name, out var probe))
+ errors.Add(new ScriptError(i.At, $"unknown probe {name}"));
+ else if (args.Count < probe.MinArgs || args.Count > probe.MaxArgs)
+ errors.Add(new ScriptError(i.At, $"probe {name} takes {probe.MinArgs}..{probe.MaxArgs} arguments, got {args.Count}"));
+ }
+ }
+}