This repository was archived by the owner on Oct 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathtm_format.py
More file actions
428 lines (391 loc) · 16.2 KB
/
tm_format.py
File metadata and controls
428 lines (391 loc) · 16.2 KB
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
#!/usr/bin/env python3
import json
import os.path
import pprint
import shlex
from task_maker.args import Arch, UIS
from task_maker.config import Config
from task_maker.formats import ioi_format, IOITask, \
Subtask, Generator, Validator, Constraint, ScoreMode, TestCase, \
parse_variable, get_write_input_file, \
get_write_output_file, TaskFormat, Task
from task_maker.formats.ioi_format.parsing import get_generator, get_validator, \
get_task_without_testcases, get_task_solutions
from task_maker.source_file import SourceFile
from task_maker.task_maker_frontend import Frontend
from typing import List, IO, Dict, Tuple
from typing import Optional
def parse_cases(gen: IO, task: IOITask, copy_compiled: bool) -> List[Subtask]:
"""
Parse the cases.gen file and build a list of subtasks. If there is an error
in the file, an exception is raised.
"""
lines = [l.strip() for l in gen.readlines()]
subtasks = [] # type: List[Subtask]
generators = dict() # type: Dict[str, Generator]
validators = dict() # type: Dict[str, Validator]
constraints = [] # type: List[Constraint]
current_gen = None # type: Optional[Generator]
current_val = None # type: Optional[Validator]
default_gen = None # type: Optional[Generator]
default_val = None # type: Optional[Validator]
tc_num = 0
st_num = -1 # will be incremented at the first : SUBTASK
guessed_gen = get_generator()
if guessed_gen:
default_gen = Generator(
"default",
SourceFile.from_file(guessed_gen, task.name, copy_compiled,
"bin/gen_default", Arch.DEFAULT, {}), [])
task.default_gen = default_gen
guessed_val = get_validator()
if guessed_val:
default_val = Validator(
"default",
SourceFile.from_file(guessed_val, task.name, copy_compiled,
"bin/val_default", Arch.DEFAULT, {}), [])
task.default_val = default_val
def is_float(s):
try:
float(s)
return True
except ValueError:
return False
def parse_command(line: str):
return shlex.split(line[1:])
def process_GEN(args: List[str]):
nonlocal default_gen, current_gen
# global GEN definitions
if not subtasks:
if len(args) < 2:
raise ValueError(
"The GEN command needs al least 2 arguments: "
"name path [args [args ...]] (line %d)" % lineno)
name = args[0]
if name in generators:
raise ValueError(
"Duplicate GEN definition at line %d" % lineno)
generator = Generator(
name,
SourceFile.from_file(args[1], task.name, copy_compiled,
"bin/gen_" + name, Arch.DEFAULT, {}),
args[2:])
generators[name] = generator
if name == "default":
default_gen = generator
task.default_gen = default_gen
# subtask local GEN
else:
if len(args) != 1:
raise ValueError(
"The GEN command for overriding the generator "
"needs only one parameter (line %d)" % lineno)
name = args[0]
if name not in generators:
raise ValueError(
"Generator '%s' not declared (line %d)" % lineno)
current_gen = generators[name]
def process_VAL(args: List[str]):
nonlocal default_val, current_val
# global VAL definitions
if not subtasks:
if len(args) < 2:
raise ValueError(
"The VAL command needs al least 2 arguments: "
"name path [args [args ...]] (line %d)" % lineno)
name = args[0]
if name in validators:
raise ValueError(
"Duplicate VAL definition at line %d" % lineno)
validator = Validator(
name,
SourceFile.from_file(args[1], task.name, copy_compiled,
"bin/val_" + name, Arch.DEFAULT, {}),
args[2:])
validators[name] = validator
if name == "default":
default_val = validator
task.default_val = default_val
# subtask local VAL
else:
if len(args) != 1:
raise ValueError(
"The VAL command for overriding the validator "
"needs only one parameter (line %d)" % lineno)
name = args[0]
if name not in validators:
raise ValueError(
"Validator '%s' not declared (line %d)" % lineno)
current_val = validators[name]
def process_CONSTRAINT(args: List[str]):
# there are 4 cases:
# a) 42 < $XXX
# b) $XXX < 123
# c) 42 < $XXX < 123
# d) $XXX > 42
if len(args) not in [3, 5]:
raise ValueError("Invalid number of arguments passed to "
"CONSTRAINT (line %d)" % lineno)
if args[1] not in ["<", "<=", ">", ">="] or \
(len(args) == 5 and args[3] not in ["<", "<="]):
raise ValueError(
"Invalid operator passed to CONSTRAINT (line %d)" % lineno)
if args[1][0] == "<":
more_or_equal = args[1] == "<="
else:
more_or_equal = args[1] == ">="
less_or_equal = args[3] == "<=" if len(args) == 5 else False
# case a
if len(args) == 3 and is_float(args[0]):
if args[2][0] != "$":
raise ValueError("Expecting variable name in CONSTRAINT "
"(line %d)" % lineno)
var = args[2][1:]
constraint = Constraint(var, float(args[0]), None, more_or_equal,
False)
# case b
elif len(args) == 3 and is_float(args[2]) and args[1][0] == "<":
if args[0][0] != "$":
raise ValueError("Expecting variable name in CONSTRAINT "
"(line %d)" % lineno)
var = args[0][1:]
constraint = Constraint(var, None, float(args[2]), False,
more_or_equal)
# case c
elif len(args) == 5 and is_float(args[0]) and is_float(args[4]):
if args[2][0] != "$":
raise ValueError("Expecting variable name in CONSTRAINT "
"(line %d)" % lineno)
lowest_ok = float(args[0]) if more_or_equal else float(args[0]) + 1
hiest_ok = float(args[4]) if less_or_equal else float(args[4]) - 1
var = args[2][1:]
if lowest_ok > hiest_ok:
raise ValueError(
"CONSTRAINT is always false (line %d)" % lineno)
constraint = Constraint(var, float(args[0]), float(args[4]),
more_or_equal, less_or_equal)
# case d
elif len(args) == 3 and is_float(args[2]) and args[1][0] == ">":
if args[0][0] != "$":
raise ValueError("Expecting variable name in CONSTRAINT "
"(line %d)" % lineno)
var = args[0][1:]
constraint = Constraint(var, float(args[2]), None, more_or_equal,
False)
else:
raise ValueError(
"Invalid format for CONSTRAINT (line %d)" % lineno)
# global constraints
if not subtasks:
constraints.append(constraint)
# subtask constraints
else:
subtasks[-1].constraints.append(constraint)
def process_SUBTASK(args: List[str]):
nonlocal current_gen, current_val, st_num
if len(args) < 1:
raise ValueError("Invalid arguments to SUBTASK: max_score [name] "
"(line %d)" % lineno)
if not is_float(args[0]):
raise ValueError(
"Invalid SUBTASK score '%s' (line %d)" % (args[0], lineno))
st_num += 1
name = " ".join(args[1:])
subtask = Subtask(name, "", ScoreMode.MIN, float(args[0]), {},
constraints.copy())
subtasks.append(subtask)
current_gen = default_gen
current_val = default_val
def process_DESCRIPTION(args: List[str]):
if not subtasks:
raise ValueError(
"Cannot DESCRIPTION without subtasks (line %d)" % lineno)
if not args:
raise ValueError("No description provided (line %s)" % lineno)
desc = " ".join(args)
subtasks[-1].description = desc
def process_COPY(args: List[str]):
nonlocal tc_num
if not subtasks:
raise ValueError("Cannot COPY without subtasks (line %d)" % lineno)
if len(args) != 1:
raise ValueError(
"Invalid number of arguments to COPY (line %d)" % lineno)
if not current_val:
raise ValueError("No VAL available (line %d)" % lineno)
testcase = TestCase(None, current_val, [], [], args[0], None,
get_write_input_file(tc_num),
get_write_output_file(tc_num))
subtasks[-1].testcases[tc_num] = testcase
tc_num += 1
def add_testcase(args: List[str], generator: Generator,
validator: Validator):
nonlocal tc_num
testcase = TestCase(generator, validator, args, [], None, None,
get_write_input_file(tc_num),
get_write_output_file(tc_num))
if generator.args_spec:
if len(generator.args_spec) != len(args):
raise ValueError("Number of params mismatch the definition "
"(line %d)" % lineno)
for index, (name, value) in enumerate(
zip(generator.args_spec, args)):
if value.startswith("$"):
value = parse_variable(value, testcase, subtasks[-1],
tc_num, st_num)
testcase.generator_args[index] = value
testcase.matched_params[name] = value
for constraint in subtasks[-1].constraints:
if name != constraint.name or not is_float(value):
continue
if not constraint.accept(float(value)):
raise ValueError("Constraint not met: %s when %s=%f "
"(line %d)" % (constraint, name,
float(value), lineno))
subtasks[-1].testcases[tc_num] = testcase
tc_num += 1
def process_RUN(args: List[str]):
if not subtasks:
raise ValueError("Cannot RUN without subtasks (line %d)" % lineno)
if len(args) < 1:
raise ValueError(
"RUN needs al least an argument (line %d)" % lineno)
name = args[0]
if name not in generators:
raise ValueError("Generator '%s' not declared (line %d)" % lineno)
add_testcase(args[1:], generators[name], current_val)
def process_TESTCASE(args):
if not subtasks:
raise ValueError(
"Cannot add a testcase without subtasks (line %d)" % lineno)
if not current_gen:
raise ValueError("No GEN available (line %d)" % lineno)
if not current_val:
raise ValueError("No VAL available (line %d)" % lineno)
add_testcase(args, current_gen, current_val)
for lineno, line in enumerate(lines, 1):
# skip empty lines
if not line:
continue
# skip the comments
if line.startswith("#"):
continue
# a command
if line.startswith(":"):
cmd, *args = parse_command(line)
if cmd == "GEN":
process_GEN(args)
elif cmd == "VAL":
process_VAL(args)
elif cmd == "CONSTRAINT":
process_CONSTRAINT(args)
elif cmd == "SUBTASK":
process_SUBTASK(args)
elif cmd == "DESCRIPTION":
process_DESCRIPTION(args)
elif cmd == "COPY":
process_COPY(args)
elif cmd == "RUN":
process_RUN(args)
else:
raise ValueError("Unknown command '%s' in '%s' (line %d)" %
(cmd, line, lineno))
# a simple testcase
else:
process_TESTCASE(shlex.split(line))
return subtasks
def generate_gen_GEN(subtasks: List[Subtask]):
"""
cmsImportTask still uses gen/GEN to get the list of test cases and subtasks,
this function will return a fake gen/GEN with enough information for
importing the built task.
This generated file should be considered temporary, tm-allow-delete is
putted at the top of the file, without that string the file cannot be
deleted automatically.
"""
GEN = "# Generated by task-maker. Do not edit!\n"
GEN += "# tm-allow-delete\n"
for subtask in subtasks:
GEN += "\n#ST: %d\n" % int(subtask.max_score)
name = ""
if subtask.name:
name += " " + subtask.name
if subtask.description:
name += " " + subtask.description
if name:
GEN += "#%s\n" % name
for constraint in subtask.constraints:
GEN += "# %s\n" % str(constraint)
for testcase in subtask.testcases.values():
if testcase.generator:
# TODO add a custom wrapper to make this works with cmsMake
GEN += "%s %s\n" % (testcase.generator.name, " ".join(
[shlex.quote(a) for a in testcase.generator_args]))
else:
GEN += "#COPY: %s\n" % testcase.input_file
return GEN
def write_gen_GEN(task: IOITask):
"""
Check if gen/GEN can be removed and then write a fake one for cmsImportTask.
"""
if os.path.exists("gen/GEN"):
with open("gen/GEN") as f:
if "tm-allow-delete" not in f.read(1024):
return
with open("gen/GEN", "w") as f:
f.write(generate_gen_GEN(task.subtasks.values()))
def get_task(config: Config):
"""
Get the task from the config
"""
task = get_task_without_testcases(config)
with open("gen/cases.gen", "r") as gen:
subtasks = parse_cases(gen, task, config.copy_exe)
for st_num, subtask in enumerate(subtasks):
task.subtasks[st_num] = subtask
return task
class TMFormat(TaskFormat):
"""
Entry point for task-maker format
"""
@staticmethod
def clean():
"""
Perform the cleanup for a task-maker task. Removes all the files of a
ioi-format task, and removes also gen/GEN only if auto-generated.
"""
ioi_format.IOIFormat.clean(has_generator=True)
if os.path.exists("gen/GEN"):
with open("gen/GEN") as f:
if "tm-allow-delete" not in f.read():
print("Kept non task-maker gen/GEN")
else:
os.remove("gen/GEN")
@staticmethod
def task_info(config: Config):
task = get_task(config)
if config.ui == UIS.JSON:
print(json.dumps(task.to_dict()))
elif config.ui != UIS.SILENT:
pprint.pprint(task.to_dict())
@staticmethod
def get_task(config: Config) -> Task:
return get_task(config)
@staticmethod
def evaluate_task(frontend: Frontend, config: Config):
"""
Evaluates the task by building the structure and using the ioi-format
interface
"""
task = get_task(config)
solutions = get_task_solutions(config, task)
if not config.dry_run:
write_gen_GEN(task)
return ioi_format.evaluate_task(frontend, task, solutions, config)
@staticmethod
def make_booklet(frontend: Frontend, config: Config,
tasks: List[Tuple[str, IOITask]]) -> int:
return ioi_format.IOIFormat.make_booklet(frontend, config, tasks)
@staticmethod
def fuzz_checker(config: Config):
ioi_format.IOIFormat.fuzz_checker(config)