diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core/Runs/Scripts/ScriptValidator.cs | |
| download | godosa-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.cs | 109 |
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}")); + } + } +} |
