aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Runs/Scripts/ScriptValidator.cs
blob: e67c48a9535c4bdb45077772f596f91c3536d261 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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}"));
        }
    }
}