// Provenance: own design — shared run scripts (spec §3.7) namespace Godosa.Core.Runs.Scripts; public static class ScriptValidator { public static List Validate(ScriptProgram p, IRunGame game, bool deterministic) { var errors = new List(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 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 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 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}")); } } }