Chose promise chose dûe. Voici une version Python…
Ou plutôt devrais-je dire une version CPython…
Ou plutôt devrais-je dire une version bytecode de CPython…
Ou plutot devrais-je dire une version qui s’assemble elle-même en du bytecode compatible avec CPython !
A priori, vu que j’utilise des opcodes assez simples, ça devrait marcher avec n’importe quelle version de Python 3, et peut-être même aussi avec Python 2.7, ou bien à vraiment très peu de choses près. Dans tous les cas je l’ai testée avec la distribution standard de python 3.5.
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 | import struct
import types
from dis import opmap as OPMAP
CODE = """
# Define sys.stdout.write as 'put' local function
LOAD_CONST 0
LOAD_CONST None
IMPORT_NAME sys
LOAD_ATTR stdout
LOAD_ATTR write
STORE_FAST put
# Now print the N first lines of the Christmas Tree
LOAD_CONST 0
STORE_FAST i
@begin_lines_loop
LOAD_FAST i
LOAD_FAST n
COMPARE_OP 0
POP_JUMP_IF_FALSE >end_lines_loop
# Print the n-i spaces
LOAD_FAST n
LOAD_FAST i
BINARY_SUBTRACT
STORE_FAST n_spaces
LOAD_CONST 0
STORE_FAST j
@begin_spaces_loop
LOAD_FAST j
LOAD_FAST n_spaces
COMPARE_OP 0
POP_JUMP_IF_FALSE >end_spaces_loop
LOAD_FAST put
LOAD_CONST ' '
CALL_FUNCTION 1
POP_TOP
LOAD_FAST j
LOAD_CONST 1
INPLACE_ADD
STORE_FAST j
JUMP_ABSOLUTE >begin_spaces_loop
@end_spaces_loop
# Print the i*2+1 stars
LOAD_FAST i
LOAD_CONST 2
BINARY_MULTIPLY
LOAD_CONST 1
BINARY_ADD
STORE_FAST n_stars
LOAD_CONST 0
STORE_FAST j
@begin_stars_loop
LOAD_FAST j
LOAD_FAST n_stars
COMPARE_OP 0
POP_JUMP_IF_FALSE >end_stars_loop
LOAD_FAST put
LOAD_CONST '*'
CALL_FUNCTION 1
POP_TOP
LOAD_FAST j
LOAD_CONST 1
INPLACE_ADD
STORE_FAST j
JUMP_ABSOLUTE >begin_stars_loop
@end_stars_loop
LOAD_FAST put
LOAD_CONST '\\n'
CALL_FUNCTION 1
POP_TOP
LOAD_FAST i
LOAD_CONST 1
INPLACE_ADD
STORE_FAST i
JUMP_ABSOLUTE >begin_lines_loop
@end_lines_loop
# Now print the foot of the Christmas Tree. (n spaces + '#' and a newline)
LOAD_CONST 0
STORE_FAST i
@begin_foot_loop
LOAD_FAST i
LOAD_FAST n
COMPARE_OP 0
POP_JUMP_IF_FALSE >end_foot_loop
LOAD_FAST put
LOAD_CONST ' '
CALL_FUNCTION 1
POP_TOP
LOAD_FAST i
LOAD_CONST 1
INPLACE_ADD
STORE_FAST i
JUMP_ABSOLUTE >begin_foot_loop
@end_foot_loop
LOAD_FAST put
LOAD_CONST '#\\n'
CALL_FUNCTION 1
RETURN_VALUE
"""
STACK_SIZE = 10
def get_index(lst, val):
"""Helper function: get the index of val in the lst.
If val is not in lst, it is appended to it and its index is returned.
"""
try:
return lst.index(val)
except ValueError:
lst.append(val)
return len(lst) - 1
def parse(code_lines, args):
"""Parse python pseudo-assembly code of a function.
Params:
code_lines: code as splitted lines.
args: tuple giving the positional parameter names of the function.
Returns:
statements: a list of (offset, opcode_name, arg) tuples.
var_names: the indexed list of local variable names used in the code.
names: the indexed list of 'global' names used in the code.
consts: the indexed list of constants used in the code.
labels: a label -> byte offset mapping.
"""
names = []
consts = [None]
var_names = list(args)
labels = {}
offset = 0
statements = []
for line in code_lines:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('@'):
labels[line[1:]] = offset
continue
opcode, *arg = line.split(maxsplit=1)
if arg:
arg = arg[0]
if opcode in {'LOAD_FAST', 'STORE_FAST'}:
arg = get_index(var_names, arg)
elif opcode in {'IMPORT_NAME', 'LOAD_ATTR'}:
arg = get_index(names, arg)
elif opcode in {'LOAD_CONST'}:
arg = get_index(consts, eval(arg))
elif opcode in {'CALL_FUNCTION', 'COMPARE_OP'}:
arg = eval(arg)
statements.append((offset, opcode, arg))
offset += 3
else:
statements.append((offset, opcode, None))
offset += 1
return statements, var_names, names, consts, labels
def resolve_addresses(statements, labels):
"""Replace :relative and >absolute args with their values."""
for idx, elt in enumerate(statements):
offset, opcode, arg = elt
if not isinstance(arg, str):
continue
if arg.startswith(':'):
rel_address = labels[arg[1:]] - offset - 3
statements[idx] = (offset, opcode, rel_address)
elif arg.startswith('>'):
abs_address = labels[arg[1:]]
statements[idx] = (offset, opcode, abs_address)
def dump_bytecode(statements):
"""Dump an list of (offset, opcode_name, arg) tuples to python bytecode."""
op_arg = struct.Struct('<BH')
op_no_arg = struct.Struct('B')
return b''.join(
(op_arg.pack(OPMAP[opcode], arg)
if arg is not None else
op_no_arg.pack(OPMAP[opcode])
for _, opcode, arg in statements)
)
def mk_func(code, name, args=(), stack_size=STACK_SIZE):
"""Assemble code to a Python function object."""
statements, lvars, names, consts, labels = parse(code.splitlines(), args)
resolve_addresses(statements, labels)
bytecode = dump_bytecode(statements)
code_obj = types.CodeType(
len(args), # argcount
0, # kwonlyargcount
len(lvars), # nlocals
STACK_SIZE, # stacksize
0, # flags
bytecode, # codestring
tuple(consts), # constants
tuple(names), # names
tuple(lvars), # varnames
'<bytecode>', # filename
name, # name (of the function)
1, # firstlineno
b'' # lnotab
# Too lazy to generate a valid lnotab, anyway the compiled function
# WOULD NEVER throw an exception, right? As hardcore elite python
# bytecode writers, we don't need this, as this is used only by the
# poor souls who happen to write code with bugs in it.
)
return types.FunctionType(code_obj, globals())
if __name__ == '__main__':
sapin = mk_func(CODE, 'sapin', ('n'))
sapin(5)
|
Concrètement, le code est écrit dans un pseudo-langage d’assemblage similaire à ce que nous sort le module standard dis
, sauf qu’il est un peu plus user-friendly dans le sens où les arguments des opcodes sont passés tels quels (noms de variables ou constantes littérales Python), et que j’ai rajouté un système de labels + adresses relatives (:label
) et absolues (>label
) pour les passer en argument des opcodes qui prennent des deltas ou des offsets comme paramètre.
Chose rigolote, le bytecode qui est écrit ici n’a pas d’équivalent Python. Il prend quelques raccourcis par rapport au standard (typiquement il ne génère/ne poppe pas de blocs quand il crée des boucles, d’ailleurs il n’utilise même pas les opcodes de Python qui servent à boucler, mais juste des jumps).
Je précise également que je me suis contenté du strict nécessaire pour faire marcher ce bytecode. Il y a des choses qui ne sont pas gérées automatiquement comme le calcul de la taille max de la stack (ça alourdirait beaucoup le code pour pas grand chose), ou encore le mapping interne qui associe à un opcode un numéro de ligne de code, pace que celui-ci a l’air relou à générer.
Voilà voilà.