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}"));
}
}
}
|