1#!/usr/bin/env python
2#
3# Copyright (C) 2016 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17#
18# Using instructions from an architecture-specific config file, generate C
19# and assembly source files for the Dalvik interpreter.
20#
21
22import sys, string, re, time
23from string import Template
24
25interp_defs_file = "../../../libdexfile/dex/dex_instruction_list.h" # need opcode list
26kNumPackedOpcodes = 256
27
28splitops = False
29verbose = False
30handler_size_bits = -1000
31handler_size_bytes = -1000
32in_op_start = 0             # 0=not started, 1=started, 2=ended
33in_alt_op_start = 0         # 0=not started, 1=started, 2=ended
34default_op_dir = None
35default_alt_stub = None
36opcode_locations = {}
37alt_opcode_locations = {}
38asm_stub_text = []
39fallback_stub_text = []
40label_prefix = ".L"         # use ".L" to hide labels from gdb
41alt_label_prefix = ".L_ALT" # use ".L" to hide labels from gdb
42style = None                # interpreter style
43generate_alt_table = False
44function_type_format = ".type   %s, %%function"
45function_size_format = ".size   %s, .-%s"
46global_name_format = "%s"
47
48# Exception class.
49class DataParseError(SyntaxError):
50    "Failure when parsing data file"
51
52#
53# Set any omnipresent substitution values.
54#
55def getGlobalSubDict():
56    return { "handler_size_bits":handler_size_bits,
57             "handler_size_bytes":handler_size_bytes }
58
59#
60# Parse arch config file --
61# Set interpreter style.
62#
63def setHandlerStyle(tokens):
64    global style
65    if len(tokens) != 2:
66        raise DataParseError("handler-style requires one argument")
67    style = tokens[1]
68    if style != "computed-goto":
69        raise DataParseError("handler-style (%s) invalid" % style)
70
71#
72# Parse arch config file --
73# Set handler_size_bytes to the value of tokens[1], and handler_size_bits to
74# log2(handler_size_bytes).  Throws an exception if "bytes" is not 0 or
75# a power of two.
76#
77def setHandlerSize(tokens):
78    global handler_size_bits, handler_size_bytes
79    if style != "computed-goto":
80        print "Warning: handler-size valid only for computed-goto interpreters"
81    if len(tokens) != 2:
82        raise DataParseError("handler-size requires one argument")
83    if handler_size_bits != -1000:
84        raise DataParseError("handler-size may only be set once")
85
86    # compute log2(n), and make sure n is 0 or a power of 2
87    handler_size_bytes = bytes = int(tokens[1])
88    bits = -1
89    while bytes > 0:
90        bytes //= 2     # halve with truncating division
91        bits += 1
92
93    if handler_size_bytes == 0 or handler_size_bytes != (1 << bits):
94        raise DataParseError("handler-size (%d) must be power of 2" \
95                % orig_bytes)
96    handler_size_bits = bits
97
98#
99# Parse arch config file --
100# Copy a file in to asm output file.
101#
102def importFile(tokens):
103    if len(tokens) != 2:
104        raise DataParseError("import requires one argument")
105    source = tokens[1]
106    if source.endswith(".S"):
107        appendSourceFile(tokens[1], getGlobalSubDict(), asm_fp, None)
108    else:
109        raise DataParseError("don't know how to import %s (expecting .cpp/.S)"
110                % source)
111
112#
113# Parse arch config file --
114# Copy a file in to the C or asm output file.
115#
116def setAsmStub(tokens):
117    global asm_stub_text
118    if len(tokens) != 2:
119        raise DataParseError("import requires one argument")
120    try:
121        stub_fp = open(tokens[1])
122        asm_stub_text = stub_fp.readlines()
123    except IOError, err:
124        stub_fp.close()
125        raise DataParseError("unable to load asm-stub: %s" % str(err))
126    stub_fp.close()
127
128#
129# Parse arch config file --
130# Copy a file in to the C or asm output file.
131#
132def setFallbackStub(tokens):
133    global fallback_stub_text
134    if len(tokens) != 2:
135        raise DataParseError("import requires one argument")
136    try:
137        stub_fp = open(tokens[1])
138        fallback_stub_text = stub_fp.readlines()
139    except IOError, err:
140        stub_fp.close()
141        raise DataParseError("unable to load fallback-stub: %s" % str(err))
142    stub_fp.close()
143#
144# Parse arch config file --
145# Record location of default alt stub
146#
147def setAsmAltStub(tokens):
148    global default_alt_stub, generate_alt_table
149    if len(tokens) != 2:
150        raise DataParseError("import requires one argument")
151    default_alt_stub = tokens[1]
152    generate_alt_table = True
153#
154# Change the default function type format
155#
156def setFunctionTypeFormat(tokens):
157    global function_type_format
158    function_type_format = tokens[1]
159#
160# Change the default function size format
161#
162def setFunctionSizeFormat(tokens):
163    global function_size_format
164    function_size_format = tokens[1]
165#
166# Change the global name format
167#
168def setGlobalNameFormat(tokens):
169    global global_name_format
170    global_name_format = tokens[1]
171#
172# Parse arch config file --
173# Start of opcode list.
174#
175def opStart(tokens):
176    global in_op_start
177    global default_op_dir
178    if len(tokens) != 2:
179        raise DataParseError("opStart takes a directory name argument")
180    if in_op_start != 0:
181        raise DataParseError("opStart can only be specified once")
182    default_op_dir = tokens[1]
183    in_op_start = 1
184
185#
186# Parse arch config file --
187# Set location of a single alt opcode's source file.
188#
189def altEntry(tokens):
190    global generate_alt_table
191    if len(tokens) != 3:
192        raise DataParseError("alt requires exactly two arguments")
193    if in_op_start != 1:
194        raise DataParseError("alt statements must be between opStart/opEnd")
195    try:
196        index = opcodes.index(tokens[1])
197    except ValueError:
198        raise DataParseError("unknown opcode %s" % tokens[1])
199    if alt_opcode_locations.has_key(tokens[1]):
200        print "Note: alt overrides earlier %s (%s -> %s)" \
201                % (tokens[1], alt_opcode_locations[tokens[1]], tokens[2])
202    alt_opcode_locations[tokens[1]] = tokens[2]
203    generate_alt_table = True
204
205#
206# Parse arch config file --
207# Set location of a single opcode's source file.
208#
209def opEntry(tokens):
210    #global opcode_locations
211    if len(tokens) != 3:
212        raise DataParseError("op requires exactly two arguments")
213    if in_op_start != 1:
214        raise DataParseError("op statements must be between opStart/opEnd")
215    try:
216        index = opcodes.index(tokens[1])
217    except ValueError:
218        raise DataParseError("unknown opcode %s" % tokens[1])
219    if opcode_locations.has_key(tokens[1]):
220        print "Note: op overrides earlier %s (%s -> %s)" \
221                % (tokens[1], opcode_locations[tokens[1]], tokens[2])
222    opcode_locations[tokens[1]] = tokens[2]
223
224#
225# Parse arch config file --
226# End of opcode list; emit instruction blocks.
227#
228def opEnd(tokens):
229    global in_op_start
230    if len(tokens) != 1:
231        raise DataParseError("opEnd takes no arguments")
232    if in_op_start != 1:
233        raise DataParseError("opEnd must follow opStart, and only appear once")
234    in_op_start = 2
235
236    loadAndEmitOpcodes()
237    if splitops == False:
238        if generate_alt_table:
239            loadAndEmitAltOpcodes()
240
241def genaltop(tokens):
242    if in_op_start != 2:
243       raise DataParseError("alt-op can be specified only after op-end")
244    if len(tokens) != 1:
245        raise DataParseError("opEnd takes no arguments")
246    if generate_alt_table:
247        loadAndEmitAltOpcodes()
248
249#
250# Extract an ordered list of instructions from the VM sources.  We use the
251# "goto table" definition macro, which has exactly kNumPackedOpcodes
252# entries.
253#
254def getOpcodeList():
255    opcodes = []
256    opcode_fp = open(interp_defs_file)
257    opcode_re = re.compile(r"^\s*V\((....), (\w+),.*", re.DOTALL)
258    for line in opcode_fp:
259        match = opcode_re.match(line)
260        if not match:
261            continue
262        opcodes.append("op_" + match.group(2).lower())
263    opcode_fp.close()
264
265    if len(opcodes) != kNumPackedOpcodes:
266        print "ERROR: found %d opcodes in Interp.h (expected %d)" \
267                % (len(opcodes), kNumPackedOpcodes)
268        raise SyntaxError, "bad opcode count"
269    return opcodes
270
271def emitAlign():
272    if style == "computed-goto":
273        asm_fp.write("    .balign %d\n" % handler_size_bytes)
274
275#
276# Load and emit opcodes for all kNumPackedOpcodes instructions.
277#
278def loadAndEmitOpcodes():
279    sister_list = []
280    assert len(opcodes) == kNumPackedOpcodes
281    need_dummy_start = False
282    start_label = global_name_format % "artMterpAsmInstructionStart"
283    end_label = global_name_format % "artMterpAsmInstructionEnd"
284
285    # point MterpAsmInstructionStart at the first handler or stub
286    asm_fp.write("\n    .global %s\n" % start_label)
287    asm_fp.write("%s = " % start_label + label_prefix + "_op_nop\n")
288    asm_fp.write("    .text\n\n")
289
290    for i in xrange(kNumPackedOpcodes):
291        op = opcodes[i]
292
293        if opcode_locations.has_key(op):
294            location = opcode_locations[op]
295        else:
296            location = default_op_dir
297
298        if location == "FALLBACK":
299            emitFallback(i)
300        else:
301            loadAndEmitAsm(location, i, sister_list)
302
303    # For a 100% C implementation, there are no asm handlers or stubs.  We
304    # need to have the MterpAsmInstructionStart label point at op_nop, and it's
305    # too annoying to try to slide it in after the alignment psuedo-op, so
306    # we take the low road and just emit a dummy op_nop here.
307    if need_dummy_start:
308        emitAlign()
309        asm_fp.write(label_prefix + "_op_nop:   /* dummy */\n");
310
311    emitAlign()
312    asm_fp.write("    .global %s\n" % end_label)
313    asm_fp.write("%s:\n" % end_label)
314
315    if style == "computed-goto":
316        start_sister_label = global_name_format % "artMterpAsmSisterStart"
317        end_sister_label = global_name_format % "artMterpAsmSisterEnd"
318        emitSectionComment("Sister implementations", asm_fp)
319        asm_fp.write("    .global %s\n" % start_sister_label)
320        asm_fp.write("    .text\n")
321        asm_fp.write("    .balign 4\n")
322        asm_fp.write("%s:\n" % start_sister_label)
323        asm_fp.writelines(sister_list)
324        asm_fp.write("    .global %s\n" % end_sister_label)
325        asm_fp.write("%s:\n\n" % end_sister_label)
326
327#
328# Load an alternate entry stub
329#
330def loadAndEmitAltStub(source, opindex):
331    op = opcodes[opindex]
332    if verbose:
333        print " alt emit %s --> stub" % source
334    dict = getGlobalSubDict()
335    dict.update({ "opcode":op, "opnum":opindex })
336
337    emitAsmHeader(asm_fp, dict, alt_label_prefix)
338    appendSourceFile(source, dict, asm_fp, None)
339
340#
341# Load and emit alternate opcodes for all kNumPackedOpcodes instructions.
342#
343def loadAndEmitAltOpcodes():
344    assert len(opcodes) == kNumPackedOpcodes
345    start_label = global_name_format % "artMterpAsmAltInstructionStart"
346    end_label = global_name_format % "artMterpAsmAltInstructionEnd"
347
348    # point MterpAsmInstructionStart at the first handler or stub
349    asm_fp.write("\n    .global %s\n" % start_label)
350    asm_fp.write("    .text\n\n")
351    asm_fp.write("%s = " % start_label + label_prefix + "_ALT_op_nop\n")
352
353    for i in xrange(kNumPackedOpcodes):
354        op = opcodes[i]
355        if alt_opcode_locations.has_key(op):
356            source = "%s/alt_%s.S" % (alt_opcode_locations[op], op)
357        else:
358            source = default_alt_stub
359        loadAndEmitAltStub(source, i)
360
361    emitAlign()
362    asm_fp.write("    .global %s\n" % end_label)
363    asm_fp.write("%s:\n" % end_label)
364
365#
366# Load an assembly fragment and emit it.
367#
368def loadAndEmitAsm(location, opindex, sister_list):
369    op = opcodes[opindex]
370    source = "%s/%s.S" % (location, op)
371    dict = getGlobalSubDict()
372    dict.update({ "opcode":op, "opnum":opindex })
373    if verbose:
374        print " emit %s --> asm" % source
375
376    emitAsmHeader(asm_fp, dict, label_prefix)
377    appendSourceFile(source, dict, asm_fp, sister_list)
378
379#
380# Emit fallback fragment
381#
382def emitFallback(opindex):
383    op = opcodes[opindex]
384    dict = getGlobalSubDict()
385    dict.update({ "opcode":op, "opnum":opindex })
386    emitAsmHeader(asm_fp, dict, label_prefix)
387    for line in fallback_stub_text:
388        asm_fp.write(line)
389    asm_fp.write("\n")
390
391#
392# Output the alignment directive and label for an assembly piece.
393#
394def emitAsmHeader(outfp, dict, prefix):
395    outfp.write("/* ------------------------------ */\n")
396    # The alignment directive ensures that the handler occupies
397    # at least the correct amount of space.  We don't try to deal
398    # with overflow here.
399    emitAlign()
400    # Emit a label so that gdb will say the right thing.  We prepend an
401    # underscore so the symbol name doesn't clash with the Opcode enum.
402    outfp.write(prefix + "_%(opcode)s: /* 0x%(opnum)02x */\n" % dict)
403
404#
405# Output a generic instruction stub that updates the "glue" struct and
406# calls the C implementation.
407#
408def emitAsmStub(outfp, dict):
409    emitAsmHeader(outfp, dict, label_prefix)
410    for line in asm_stub_text:
411        templ = Template(line)
412        outfp.write(templ.substitute(dict))
413
414#
415# Append the file specified by "source" to the open "outfp".  Each line will
416# be template-replaced using the substitution dictionary "dict".
417#
418# If the first line of the file starts with "%" it is taken as a directive.
419# A "%include" line contains a filename and, optionally, a Python-style
420# dictionary declaration with substitution strings.  (This is implemented
421# with recursion.)
422#
423# If "sister_list" is provided, and we find a line that contains only "&",
424# all subsequent lines from the file will be appended to sister_list instead
425# of copied to the output.
426#
427# This may modify "dict".
428#
429def appendSourceFile(source, dict, outfp, sister_list):
430    outfp.write("/* File: %s */\n" % source)
431    infp = open(source, "r")
432    in_sister = False
433    for line in infp:
434        if line.startswith("%include"):
435            # Parse the "include" line
436            tokens = line.strip().split(' ', 2)
437            if len(tokens) < 2:
438                raise DataParseError("malformed %%include in %s" % source)
439
440            alt_source = tokens[1].strip("\"")
441            if alt_source == source:
442                raise DataParseError("self-referential %%include in %s"
443                        % source)
444
445            new_dict = dict.copy()
446            if len(tokens) == 3:
447                new_dict.update(eval(tokens[2]))
448            #print " including src=%s dict=%s" % (alt_source, new_dict)
449            appendSourceFile(alt_source, new_dict, outfp, sister_list)
450            continue
451
452        elif line.startswith("%default"):
453            # copy keywords into dictionary
454            tokens = line.strip().split(' ', 1)
455            if len(tokens) < 2:
456                raise DataParseError("malformed %%default in %s" % source)
457            defaultValues = eval(tokens[1])
458            for entry in defaultValues:
459                dict.setdefault(entry, defaultValues[entry])
460            continue
461
462        elif line.startswith("%break") and sister_list != None:
463            # allow more than one %break, ignoring all following the first
464            if style == "computed-goto" and not in_sister:
465                in_sister = True
466                sister_list.append("\n/* continuation for %(opcode)s */\n"%dict)
467            continue
468
469        # perform keyword substitution if a dictionary was provided
470        if dict != None:
471            templ = Template(line)
472            try:
473                subline = templ.substitute(dict)
474            except KeyError, err:
475                raise DataParseError("keyword substitution failed in %s: %s"
476                        % (source, str(err)))
477            except:
478                print "ERROR: substitution failed: " + line
479                raise
480        else:
481            subline = line
482
483        # write output to appropriate file
484        if in_sister:
485            sister_list.append(subline)
486        else:
487            outfp.write(subline)
488    outfp.write("\n")
489    infp.close()
490
491#
492# Emit a C-style section header comment.
493#
494def emitSectionComment(str, fp):
495    equals = "========================================" \
496             "==================================="
497
498    fp.write("\n/*\n * %s\n *  %s\n * %s\n */\n" %
499        (equals, str, equals))
500
501
502#
503# ===========================================================================
504# "main" code
505#
506
507#
508# Check args.
509#
510if len(sys.argv) != 3:
511    print "Usage: %s target-arch output-dir" % sys.argv[0]
512    sys.exit(2)
513
514target_arch = sys.argv[1]
515output_dir = sys.argv[2]
516
517#
518# Extract opcode list.
519#
520opcodes = getOpcodeList()
521#for op in opcodes:
522#    print "  %s" % op
523
524#
525# Open config file.
526#
527try:
528    config_fp = open("config_%s" % target_arch)
529except:
530    print "Unable to open config file 'config_%s'" % target_arch
531    sys.exit(1)
532
533#
534# Open and prepare output files.
535#
536try:
537    asm_fp = open("%s/mterp_%s.S" % (output_dir, target_arch), "w")
538except:
539    print "Unable to open output files"
540    print "Make sure directory '%s' exists and existing files are writable" \
541            % output_dir
542    # Ideally we'd remove the files to avoid confusing "make", but if they
543    # failed to open we probably won't be able to remove them either.
544    sys.exit(1)
545
546print "Generating %s" % (asm_fp.name)
547
548file_header = """/*
549 * This file was generated automatically by gen-mterp.py for '%s'.
550 *
551 * --> DO NOT EDIT <--
552 */
553
554""" % (target_arch)
555
556asm_fp.write(file_header)
557
558#
559# Process the config file.
560#
561failed = False
562try:
563    for line in config_fp:
564        line = line.strip()         # remove CRLF, leading spaces
565        tokens = line.split(' ')    # tokenize
566        #print "%d: %s" % (len(tokens), tokens)
567        if len(tokens[0]) == 0:
568            #print "  blank"
569            pass
570        elif tokens[0][0] == '#':
571            #print "  comment"
572            pass
573        else:
574            if tokens[0] == "handler-size":
575                setHandlerSize(tokens)
576            elif tokens[0] == "import":
577                importFile(tokens)
578            elif tokens[0] == "asm-stub":
579                setAsmStub(tokens)
580            elif tokens[0] == "asm-alt-stub":
581                setAsmAltStub(tokens)
582            elif tokens[0] == "op-start":
583                opStart(tokens)
584            elif tokens[0] == "op-end":
585                opEnd(tokens)
586            elif tokens[0] == "alt":
587                altEntry(tokens)
588            elif tokens[0] == "op":
589                opEntry(tokens)
590            elif tokens[0] == "handler-style":
591                setHandlerStyle(tokens)
592            elif tokens[0] == "alt-ops":
593                genaltop(tokens)
594            elif tokens[0] == "split-ops":
595                splitops = True
596            elif tokens[0] == "fallback-stub":
597               setFallbackStub(tokens)
598            elif tokens[0] == "function-type-format":
599               setFunctionTypeFormat(tokens)
600            elif tokens[0] == "function-size-format":
601               setFunctionSizeFormat(tokens)
602            elif tokens[0] == "global-name-format":
603               setGlobalNameFormat(tokens)
604            else:
605                raise DataParseError, "unrecognized command '%s'" % tokens[0]
606            if style == None:
607                print "tokens[0] = %s" % tokens[0]
608                raise DataParseError, "handler-style must be first command"
609except DataParseError, err:
610    print "Failed: " + str(err)
611    # TODO: remove output files so "make" doesn't get confused
612    failed = True
613    asm_fp.close()
614    asm_fp = None
615
616config_fp.close()
617
618#
619# Done!
620#
621if asm_fp:
622    asm_fp.close()
623
624sys.exit(failed)
625