-
Notifications
You must be signed in to change notification settings - Fork 0
/
gmlc_compiler.py
577 lines (471 loc) · 21.5 KB
/
gmlc_compiler.py
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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
import demjson
from gmlc_events import EVENT_TYPES, EVENT_NUMS, KEYS
from gmlc_utils import is_number, valid_varname
CHAR_SYMBOLS = frozenset([
':',
'{',
'}',
'(',
')',
',',
';'
])
KEYWORDS = frozenset([
'object',
'event',
'import',
'properties',
'room',
'script'
])
KEYWORDS_SE = frozenset(['#define']).union(KEYWORDS)
# A context class represents a general scope of expectation
class CtxType:
OBJ_DEC = 1
RM_DEC = 2
PROP = 3
IMPORT = 4
EVENT = 5
SCR_DEC = 6
SCR_DEC_GM = 7
class Ctx(object):
def __init__(self, typ, stage=0):
super(Ctx, self).__init__()
self.typ = typ
self.stage = stage
def advance(self):
self.stage += 1
def label(self):
if self.typ == CtxType.OBJ_DEC:
return "object declaration"
elif self.typ == CtxType.RM_DEC:
return "room declaration"
elif self.typ == CtxType.PROP:
return "resource property dict parsing"
elif self.typ == CtxType.IMPORT:
return "import event block"
elif self.typ == CtxType.EVENT:
return "event declaration"
elif self.typ == CtxType.SCR_DEC:
return "script declaration"
elif self.typ == CtxType.SCR_DEC_GM:
return "script declaration"
return ""
# The compiler is fed symbols and transpiles
class Compiler(object):
def __init__(self, allow_gmldef):
super(Compiler, self).__init__()
self.has_out = False
# The stack of contexts the the cursor has moved into
self.context_stack = []
# Whether the last output was import block
self.last_out_import = False
# Resource names
self.obj_names = set()
self.rm_names = set()
self.scr_names = set()
# The event type
self.event_type_name = None
self.allow_gmldef = allow_gmldef
# Script argument list
self.scr_args_current = None
# The running code block
self.codeblock = ""
# The indendation level of the code block
self.indent_level = 0
# Previous code block symbol added
self.prev_codeblock_symbols = [" "]
# Helper to add to data block
def codeblock_add(self, symbol):
# Allow `var name = value`
if len(self.prev_codeblock_symbols) >= 2:
if symbol == '=' and self.prev_codeblock_symbols[1] == "var":
symbol = ";" + self.prev_codeblock_symbols[0] + "="
# Determine whether padding is necessary
last_char = self.prev_codeblock_symbols[0][-1]
ended_with_name = last_char.isalnum() or last_char == '_' or last_char == "'" or last_char == '"'
starts_with_name = symbol[0].isalnum() or symbol[0] == '_' or symbol[0] == "'" or symbol[0] == '"'
if last_char == ';' or last_char == '}' or last_char == '{': self.codeblock += "\n"
if ended_with_name and starts_with_name: self.codeblock += " "
self.codeblock += symbol
self.prev_codeblock_symbols.insert(0, symbol);
def feed(self, symbol):
returned_output = ""
warnings = []
errors = []
# Helper function for appending output. Will dynamically prefix with a @import_evt line
def output(returned_output, string, is_import=False):
mod_string = None
if is_import and not self.last_out_import:
mod_string = "@import_evt\n" + string
elif not is_import and self.last_out_import:
mod_string = "@endimport\n" + string
else:
mod_string = string
returned_output += mod_string
self.last_out_import = is_import
self.prev_codeblock_symbols = [" "]
return returned_output
# Get context information
ctx = self.context_stack[-1] if self.context_stack else None
#print "S, C, I:", symbol, str(ctx.typ) + "s" + str(ctx.stage) if ctx else None, self.indent_level
# Parse global symbols
if not ctx:
if symbol == 'object':
self.context_stack.append(Ctx(CtxType.OBJ_DEC))
elif symbol == 'room':
self.context_stack.append(Ctx(CtxType.RM_DEC))
elif symbol == 'script':
self.context_stack.append(Ctx(CtxType.SCR_DEC))
elif symbol == '#define' and self.allow_gmldef:
self.context_stack.append(Ctx(CtxType.SCR_DEC_GM))
# Error cases
else:
erstr = "Unexpected symbol '" + symbol + "'."
if symbol in KEYWORDS:
erstr += " Keywords of this type belong in resource declarations."
errors.append(erstr)
# Parse object declaration symbols
elif ctx.typ == CtxType.OBJ_DEC:
# Name of object
if ctx.stage == 0:
errors.extend(validate_varname(symbol, ctx))
returned_output = output(returned_output, "@obj_declare " + symbol + "\n")
self.obj_names.add(symbol)
ctx.advance()
# Symbol following object name
elif ctx.stage == 1:
errors.extend(validate_symbol(symbol, ctx, [':', '{']))
if symbol == ':':
ctx.advance()
else:
ctx.stage = 4
# Name of parent
elif ctx.stage == 2:
errors.extend(validate_varname(symbol, ctx))
if symbol not in self.obj_names:
errors.append("No declared object matches name '" + symbol + "'.")
returned_output = output(returned_output, "object_set_parent(this_resource," + symbol + ");\n", True)
ctx.advance()
elif ctx.stage == 3:
errors.extend(validate_symbol(symbol, ctx, ['{']))
ctx.advance()
# Look for nested declarations
elif ctx.stage == 4:
errors.extend(validate_symbol(symbol, ctx, ['}', 'event', 'properties', 'import']))
if symbol == '}':
self.context_stack.pop()
elif symbol == 'event':
self.context_stack.append(Ctx(CtxType.EVENT))
elif symbol == 'properties':
self.context_stack.append(Ctx(CtxType.PROP))
elif symbol == 'import':
self.context_stack.append(Ctx(CtxType.IMPORT))
else:
raise NotImplementedError()
# Parse room declaration symbols
elif ctx.typ == CtxType.RM_DEC:
# Name of room
if ctx.stage == 0:
errors.extend(validate_varname(symbol, ctx))
returned_output = output(returned_output, "@rm_declare " + symbol + "\n")
self.rm_names.add(symbol)
ctx.advance()
# Curly following room name
elif ctx.stage == 1:
errors.extend(validate_symbol(symbol, ctx, ['{']))
ctx.advance()
# Look for nested declarations OR list of objects
elif ctx.stage == 2:
# Check if symbol is object
if symbol in self.obj_names:
returned_output = output(returned_output, symbol + "\n")
ctx.advance()
# Check nested declarations
elif symbol == '}':
self.context_stack.pop()
elif symbol == 'properties':
self.context_stack.append(Ctx(CtxType.PROP))
elif symbol == 'import':
self.context_stack.append(Ctx(CtxType.IMPORT))
# Error cases
else:
if valid_varname(symbol):
errors.append("Unknown object in room declaration: '" + symbol + "'.")
else:
errors.append("Unexpected symbol in room declaration: '" + symbol + "'.")
elif ctx.stage == 3:
# Check if symbol is object
if symbol in self.obj_names:
errors.append("Object names must be separated by commas")
# Check nested declarations
elif symbol == '}':
self.context_stack.pop()
elif symbol == 'properties':
self.context_stack.append(Ctx(CtxType.PROP))
elif symbol == 'import':
self.context_stack.append(Ctx(CtxType.IMPORT))
elif symbol == ',':
ctx.advance()
# Error cases
else:
errors.append("Unexpected symbol in room declaration: '" + symbol + "'.")
# Following a comma
elif ctx.stage == 4:
# Only GML object can follow commas
if symbol in self.obj_names:
returned_output = output(returned_output, symbol + "\n")
ctx.stage = 3
else:
errors.append("Expecting object name after ',' but instead found '" + symbol + "'.")
else:
raise NotImplementedError()
# Parse property symbols
elif ctx.typ == CtxType.PROP:
if ctx.stage == 0:
errors.extend(validate_symbol(symbol, ctx, ['{']))
self.codeblock = ""
ctx.advance()
# Add data to block
elif ctx.stage == 1:
if symbol == '}':
# Output parsed data
prefix = "object" if self.context_stack[-2].typ == CtxType.OBJ_DEC else "room"
import_block = parse_properties(self.codeblock, prefix, errors)
if import_block != None:
returned_output = output(returned_output, import_block, True)
self.context_stack.pop()
else:
self.codeblock_add(symbol)
# Parse import symbols
elif ctx.typ == CtxType.IMPORT:
if ctx.stage == 0:
errors.extend(validate_symbol(symbol, ctx, ['{']))
self.codeblock = ""
ctx.advance()
# Add data to block
elif ctx.stage == 1:
if symbol == '}' and self.indent_level == 0:
returned_output = output(returned_output, self.codeblock + "\n", True)
self.context_stack.pop()
else:
if symbol == '{': self.indent_level += 1
elif symbol == '}': self.indent_level -= 1
self.codeblock_add(symbol)
# Parse event symbols
elif ctx.typ == CtxType.EVENT:
if ctx.stage == 0:
# Prefix with ev_
mod_symbol = symbol if symbol[:3] == "ev_" else "ev_" + symbol
# Match with event type names
if mod_symbol not in EVENT_TYPES.keys():
errors.append("No valid event type matches name: '" + mod_symbol + "'.")
else:
self.event_type_name = mod_symbol
returned_output = output(returned_output, "@obj_evt " + str(EVENT_TYPES[mod_symbol]) + " ")
ctx.advance()
elif ctx.stage == 1:
errors.extend(validate_symbol(symbol, ctx, ['(']))
ctx.advance()
elif ctx.stage == 2:
if is_number(symbol):
returned_output = output(returned_output, symbol + "\n")
ctx.advance()
else:
# Check no arguments
if symbol == ')':
returned_output = output(returned_output, "0\n")
ctx.stage = 4
if not (self.event_type_name == "ev_create" or self.event_type_name == "ev_destroy" or self.event_type_name == "ev_draw"):
errors.append("Expecting event number argument for event of type " + self.event_type_name)
# Check if it is a single char for a keycode
elif self.event_type_name[:6] == "ev_key" and len(symbol) == 3 and symbol[0] == '"' and symbol[2] == '"':
returned_output = output(returned_output, str(ord(symbol[1].upper())) + "\n")
ctx.advance()
# Check for event constants
else:
prefix = symbol[:3]
if self.event_type_name == "ev_create" or self.event_type_name == "ev_destroy" or self.event_type_name == "ev_draw":
errors.append("Unexpected event number argument provided for event of type " + self.event_type_name)
elif self.event_type_name[:6] == "ev_key":
if prefix == "ev_":
errors.append("Unexpected event number constant beginning with 'ev_' for event type " + self.event_type_name)
elif prefix == "vk_":
returned_output = output(returned_output, str(KEYS[symbol]) + "\n")
else:
mod_symbol = "vk_" + symbol
if mod_symbol not in KEYS.keys():
errors.append("Unknown key constant " + mod_symbol)
else:
returned_output = output(returned_output, str(KEYS[mod_symbol]) + "\n")
else:
if prefix == "vk_":
errors.append("Unexpected event number constant beginning with 'vk_' for event type " + self.event_type_name)
elif prefix == "ev_":
returned_output = output(returned_output, str(EVENT_NUMS[symbol]) + "\n")
else:
mod_symbol = "ev_" + symbol
if mod_symbol not in EVENT_NUMS.keys():
errors.append("Unknown event number constant " + mod_symbol)
else:
returned_output = output(returned_output, str(EVENT_NUMS[mod_symbol]) + "\n")
ctx.advance()
elif ctx.stage == 3:
errors.extend(validate_symbol(symbol, ctx, [')']))
ctx.advance()
elif ctx.stage == 4:
errors.extend(validate_symbol(symbol, ctx, ['{']))
self.codeblock = ""
ctx.advance()
# Add data to block
elif ctx.stage == 5:
if symbol == '}' and self.indent_level == 0:
returned_output = output(returned_output, self.codeblock + "\n")
self.context_stack.pop()
else:
if symbol == '{': self.indent_level += 1
elif symbol == '}': self.indent_level -= 1
self.codeblock_add(symbol)
# Parse script declaration symbols
elif ctx.typ == CtxType.SCR_DEC:
# Name of script
if ctx.stage == 0:
errors.extend(validate_varname(symbol, ctx))
returned_output = output(returned_output, "@scr_declare " + symbol + "\n")
self.scr_names.add(symbol)
self.scr_args_current = []
ctx.advance()
# Opening parenthesis following script name
elif ctx.stage == 1:
errors.extend(validate_symbol(symbol, ctx, ['(']))
ctx.advance()
# Argument name or closing paren
elif ctx.stage == 2:
# Check if end of arguments
if symbol == ")":
ctx.stage = 5
else:
# Verify name
errors.extend(validate_varname(symbol, ctx))
self.scr_args_current.append(symbol)
ctx.advance()
# Comma or end paren
elif ctx.stage == 3:
errors.extend(validate_symbol(symbol, ctx, [')', ',']))
if symbol == ")":
ctx.stage = 5
elif symbol == ",":
ctx.advance()
# Argument name
elif ctx.stage == 4:
errors.extend(validate_varname(symbol, ctx))
self.scr_args_current.append(symbol)
ctx.stage = 3
# Open curly
elif ctx.stage == 5:
errors.extend(validate_symbol(symbol, ctx, ['{']))
self.codeblock = ""
var_declarations = ""
for index, argument in enumerate(self.scr_args_current):
var_declarations += "var " + argument + ";"
var_declarations += argument + "=argument" + str(index) + ";\n"
returned_output = output(returned_output, var_declarations)
ctx.advance()
# Actual code symbols
elif ctx.stage == 6:
if symbol == '}' and self.indent_level == 0:
returned_output = output(returned_output, self.codeblock + "\n")
self.context_stack.pop()
else:
if symbol == '{': self.indent_level += 1
elif symbol == '}': self.indent_level -= 1
else:
self.codeblock_add(symbol)
# Parse GML-style script declaration symbols
elif ctx.typ == CtxType.SCR_DEC_GM:
# Name of script
if ctx.stage == 0:
errors.extend(validate_varname(symbol, ctx))
returned_output = output(returned_output, "@scr_declare " + symbol + "\n")
self.scr_names.add(symbol)
self.scr_args_current = []
self.indent_level = 0
self.codeblock = ""
ctx.advance()
# Actual code symbols
elif ctx.stage == 1:
if symbol in KEYWORDS_SE and self.indent_level == 0:
returned_output = output(returned_output, self.codeblock + "\n")
self.context_stack.pop()
self.indent_level = 0
self.feed(symbol) # Feed the symbol again in the new context
else:
if symbol == '{': self.indent_level += 1
elif symbol == '}': self.indent_level -= 1
self.codeblock_add(symbol)
else:
raise NotImplementedError("Not all context types implemented in compiler")
# Prefix with initial if first time outputting
if not self.has_out:
returned_output = "\n@gml\n" + returned_output
self.has_out = True
return (returned_output, warnings, errors)
# Get lingering errors, warnings, compilation blocks
def feed_final(self):
errors = []
warnings = []
returned_output = ""
if self.indent_level != 0:
errors.append("Curly brace mismatch. Expecting '}'.");
if self.context_stack:
if self.context_stack[-1].typ == CtxType.SCR_DEC_GM:
self.last_out_import = False
self.prev_codeblock_symbols = [" "]
returned_output = self.codeblock + "\n"
self.context_stack.pop()
else:
errors.append("Source file ended in middle of " + self.context_stack[-1].label() + ".")
returned_output += "@end\n"
self.context_stack = []
return (returned_output, warnings, errors)
# Gets resource names (scripts are not counted)
def get_resource_names(self):
return self.obj_names.union(self.rm_names)
def get_script_names(self):
return self.scr_names
def validate_varname(name, ctx):
if not valid_varname(name):
erstr = "Invalid resource name in " + ctx.label() + "."
erstr += " Name '" + name + "' is not a valid GML name."
return [erstr]
return []
# Checks that the symbol matches one of the expectations in the provided list
def validate_symbol(symbol, ctx, expectations):
if symbol not in expectations:
erstr = "Unexpected symbol '" + symbol + "' in " + ctx.label() + "."
erstr += " Expected '" + "' or '".join(expectations) + "'."
return [erstr]
return []
# Parses a property data block into import statements. Prefix is "room" or "object"
def parse_properties(codeblock, prefix, errors):
evalstr = "{" + codeblock + "}"
prop_dict = None
try:
prop_dict = demjson.decode(evalstr)
except demjson.JSONDecodeError as err:
errors.append("Error while parsing " + prefix + " properties: " + str(err) + ".")
return None
importstr = ""
for key, value in prop_dict.iteritems():
mod_value = None
if isinstance(value, basestring):
mod_value = '"' + value + '"'
elif isinstance(value, bool):
mod_value = "true" if value else "false"
elif isinstance(value, (int, long, float)):
mod_value = str(value)
else:
errors.append("Unrecognized value type set for property '" + key + "' of " + prefix + ". Only number, boolean, and string literals are permitted.")
return None
importstr += prefix + "_set_" + key + "(this_resource," + mod_value + ");\n"
return importstr