1 #!/usr/bin/env python 2 3 """ 4 Attribute access plan translation. 5 6 Copyright (C) 2014-2018, 2023 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from encoders import encode_access_location 23 24 class AccessPlan: 25 26 "An attribute access plan." 27 28 def __init__(self, name, test, test_type, base, traversed, traversal_modes, 29 remaining, context, context_test, first_method, final_method, 30 origin, accessor_kinds): 31 32 "Initialise the plan." 33 34 # With instance attribute initialisers, the assignment below would be 35 # generated automatically. 36 37 ( 38 self.name, self.test, self.test_type, self.base, 39 self.traversed, self.traversal_modes, self.remaining, 40 self.context, self.context_test, 41 self.first_method, self.final_method, 42 self.origin, self.accessor_kinds) = ( 43 44 name, test, test_type, base, 45 traversed, traversal_modes, remaining, 46 context, context_test, 47 first_method, final_method, 48 origin, accessor_kinds) 49 50 # Define the first attribute access and subsequent accesses. 51 52 self.first_attrname = None 53 self.traversed_attrnames = traversed 54 self.traversed_attrname_modes = traversal_modes 55 self.remaining_attrnames = remaining 56 57 if traversed: 58 self.first_attrname = traversed[0] 59 self.traversed_attrnames = traversed[1:] 60 self.traversed_attrname_modes = traversal_modes[1:] 61 elif remaining: 62 self.first_attrname = remaining[0] 63 self.remaining_attrnames = remaining[1:] 64 65 def access_first_attribute(self): 66 67 "Return whether the first attribute is to be accessed." 68 69 return self.final_method in ("access", "access-invoke", "assign") or \ 70 self.all_subsequent_attributes() 71 72 def assigning_first_attribute(self): 73 74 "Return whether the first attribute access involves assignment." 75 76 return not self.all_subsequent_attributes() and self.final_method == "assign" 77 78 def get_first_attribute_name(self): 79 80 "Return any first attribute name to be used in an initial access." 81 82 return self.first_attrname 83 84 def all_subsequent_attributes(self): 85 86 "Return all subsequent attribute names involved in accesses." 87 88 return self.traversed_attrnames + self.remaining_attrnames 89 90 def attribute_traversals(self): 91 92 "Return a collection of (attribute name, traversal mode) tuples." 93 94 return zip(self.traversed_attrnames, self.traversed_attrname_modes) 95 96 def stored_accessor(self): 97 98 "Return the variable used to obtain the accessor." 99 100 return self.assigning_first_attribute() and "<target_accessor>" or "<accessor>" 101 102 def stored_accessor_modifier(self): 103 104 "Return the variable used to set the accessor." 105 106 return self.assigning_first_attribute() and "<set_target_accessor>" or "<set_accessor>" 107 108 def get_original_accessor(self): 109 110 "Return the original accessor details." 111 112 # Identify any static original accessor. 113 114 if self.base: 115 return self.base 116 117 # Employ names as contexts unless the context needs testing and 118 # potentially updating. In such cases, temporary context storage is 119 # used instead. 120 121 elif self.name and not (self.context_test == "test" and 122 self.final_method in ("access-invoke", "static-invoke")): 123 124 return "<name>" 125 126 # Use a generic placeholder representing the access expression in 127 # the general case. 128 129 else: 130 return "<expr>" 131 132 def get_instructions(self): 133 134 "Return a list of instructions corresponding to the plan." 135 136 # Emit instructions by appending them to a list. 137 138 instructions = [] 139 emit = instructions.append 140 141 # Set up any initial instructions. 142 143 accessor, context = self.process_initialisation(emit) 144 145 # Apply any test. 146 147 if self.test[0] == "test": 148 test_accessor = accessor = ("__%s_%s_%s" % self.test, accessor, self.test_type) 149 else: 150 test_accessor = None 151 152 # Perform the first or final access. 153 # The access only needs performing if the resulting accessor is used. 154 155 accessor = self.process_first_attribute(accessor, emit) 156 157 # Perform accesses for the traversed and remaining attributes. 158 159 accessor, context = self.process_traversed_attributes(accessor, context, emit) 160 accessor, context = self.process_remaining_attributes(accessor, context, emit) 161 162 # Make any accessor test available if not emitted. 163 164 test_accessor = not instructions and test_accessor or None 165 166 # Perform the access on the actual target. 167 168 accessor = self.process_attribute_access(accessor, context, test_accessor, emit) 169 170 # Produce an advisory instruction regarding the context. 171 172 self.process_context_identity(context, emit) 173 174 # Produce an advisory instruction regarding the final attribute. 175 176 if self.origin: 177 emit(("<final_identity>", self.origin)) 178 179 return instructions 180 181 def process_initialisation(self, emit): 182 183 """ 184 Use 'emit' to generate instructions for any initialisation of attribute 185 access. Return the potentially revised accessor and context indicators. 186 """ 187 188 # Identify any static original accessor. 189 190 original_accessor = self.get_original_accessor() 191 192 # Determine whether the first access involves assignment. 193 194 set_accessor = self.stored_accessor_modifier() 195 stored_accessor = self.stored_accessor() 196 197 # Set the context if already available. 198 199 context = None 200 201 if self.context == "base": 202 accessor = context = (self.base,) 203 elif self.context == "original-accessor": 204 205 # Prevent re-evaluation of any dynamic expression by storing it. 206 207 if original_accessor == "<expr>": 208 if self.final_method in ("access-invoke", "static-invoke"): 209 emit(("<set_context>", original_accessor)) 210 accessor = context = ("<context>",) 211 else: 212 emit((set_accessor, original_accessor)) 213 accessor = context = (stored_accessor,) 214 else: 215 accessor = context = (original_accessor,) 216 217 # Assigning does not set the context. 218 219 elif self.context in ("final-accessor", "unset") and self.access_first_attribute(): 220 221 # Prevent re-evaluation of any dynamic expression by storing it. 222 223 if original_accessor == "<expr>": 224 emit((set_accessor, original_accessor)) 225 accessor = (stored_accessor,) 226 else: 227 accessor = (original_accessor,) 228 else: 229 accessor = None 230 231 return accessor, context 232 233 def process_first_attribute(self, accessor, emit): 234 235 """ 236 Using 'accessor', use 'emit' to generate instructions for any first 237 attribute access. Return the potentially revised accessor. 238 """ 239 240 if self.access_first_attribute(): 241 attrname = self.get_first_attribute_name() 242 assigning = self.assigning_first_attribute() 243 244 if self.first_method == "relative-class": 245 if assigning: 246 emit(("__store_via_class", accessor, attrname, "<assexpr>")) 247 else: 248 accessor = ("__load_via_class", accessor, attrname) 249 250 elif self.first_method == "relative-object": 251 if assigning: 252 emit(("__store_via_object", accessor, attrname, "<assexpr>")) 253 else: 254 accessor = ("__load_via_object", accessor, attrname) 255 256 elif self.first_method == "relative-object-class": 257 if assigning: 258 emit(("__get_class_and_store", accessor, attrname, "<assexpr>")) 259 else: 260 accessor = ("__get_class_and_load", accessor, attrname) 261 262 elif self.first_method == "check-class": 263 if assigning: 264 emit(("__check_and_store_via_class", accessor, attrname, "<assexpr>")) 265 else: 266 accessor = ("__check_and_load_via_class", accessor, attrname) 267 268 elif self.first_method == "check-object": 269 if assigning: 270 emit(("__check_and_store_via_object", accessor, attrname, "<assexpr>")) 271 else: 272 accessor = ("__check_and_load_via_object", accessor, attrname) 273 274 elif self.first_method == "check-object-class": 275 if assigning: 276 emit(("__check_and_store_via_any", accessor, attrname, "<assexpr>")) 277 else: 278 accessor = ("__check_and_load_via_any", accessor, attrname) 279 280 return accessor 281 282 def process_traversed_attributes(self, accessor, context, emit): 283 284 """ 285 Using 'accessor' and 'context', use 'emit' to generate instructions 286 for the traversed attribute accesses. Return the potentially revised 287 accessor and context indicators. 288 """ 289 290 # Traverse attributes using the accessor. 291 292 num_remaining = len(self.all_subsequent_attributes()) 293 294 if self.traversed_attrnames: 295 for attrname, traversal_mode in self.attribute_traversals(): 296 assigning = num_remaining == 1 and self.final_method == "assign" 297 298 # Set the context, if appropriate. 299 300 if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor": 301 302 # Invoked attributes employ a separate context accessed 303 # during invocation. 304 305 if self.final_method in ("access-invoke", "static-invoke"): 306 emit(("<set_context>", accessor)) 307 accessor = context = "<context>" 308 309 # A private context within the access is otherwise 310 # retained. 311 312 else: 313 emit(("<set_private_context>", accessor)) 314 accessor = context = "<private_context>" 315 316 # Perform the access only if not achieved directly. 317 318 if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"): 319 320 if traversal_mode == "class": 321 if assigning: 322 emit(("__store_via_class", accessor, attrname, "<assexpr>")) 323 else: 324 accessor = ("__load_via_class", accessor, attrname) 325 else: 326 if assigning: 327 emit(("__store_via_object", accessor, attrname, "<assexpr>")) 328 else: 329 accessor = ("__load_via_object", accessor, attrname) 330 331 num_remaining -= 1 332 333 return accessor, context 334 335 def process_remaining_attributes(self, accessor, context, emit): 336 337 """ 338 Using 'accessor' and 'context', use 'emit' to generate instructions 339 for the remaining attribute accesses. Return the potentially revised 340 accessor and context indicators. 341 """ 342 343 remaining = self.remaining_attrnames 344 345 if remaining: 346 num_remaining = len(remaining) 347 348 for attrname in remaining: 349 assigning = num_remaining == 1 and self.final_method == "assign" 350 351 # Set the context, if appropriate. 352 353 if num_remaining == 1 and self.final_method != "assign" and self.context == "final-accessor": 354 355 # Invoked attributes employ a separate context accessed 356 # during invocation. 357 358 if self.final_method in ("access-invoke", "static-invoke"): 359 emit(("<set_context>", accessor)) 360 accessor = context = "<context>" 361 362 # A private context within the access is otherwise 363 # retained. 364 365 else: 366 emit(("<set_private_context>", accessor)) 367 accessor = context = "<private_context>" 368 369 # Perform the access only if not achieved directly. 370 371 if num_remaining > 1 or self.final_method in ("access", "access-invoke", "assign"): 372 373 # Constrain instructions involving certain special 374 # attribute names. 375 376 to_search = attrname == "__data__" and "object" or "any" 377 378 if assigning: 379 emit(("__check_and_store_via_%s" % to_search, accessor, attrname, "<assexpr>")) 380 else: 381 accessor = ("__check_and_load_via_%s" % to_search, accessor, attrname) 382 383 num_remaining -= 1 384 385 return accessor, context 386 387 def process_attribute_access(self, accessor, context, test_accessor, emit): 388 389 """ 390 Using 'accessor','context' and any 'test_accessor' operation, use 'emit' 391 to generate instructions for the final attribute access. Return the 392 potentially revised accessor. 393 """ 394 395 # Define or emit the means of accessing the actual target. 396 397 if self.final_method in ("static", "static-assign", "static-invoke"): 398 399 if test_accessor: 400 emit(test_accessor) 401 402 # Assignments to known attributes. 403 404 if self.final_method == "static-assign": 405 parent, attrname = self.origin.rsplit(".", 1) 406 emit(("__store_via_object", parent, attrname, "<assexpr>")) 407 408 # Invoked attributes employ a separate context. 409 410 elif self.final_method in ("static", "static-invoke"): 411 accessor = ("__load_static_ignore", self.origin) 412 413 # Wrap accesses in context operations. 414 415 if self.context_test == "test": 416 417 # Test and combine the context with static attribute details. 418 419 if self.final_method == "static": 420 emit(("__load_static_test", context, self.origin)) 421 422 # Test the context, storing it separately if required for the 423 # immediately invoked static attribute. 424 425 elif self.final_method == "static-invoke": 426 emit(("<test_context_static>", context, self.origin)) 427 428 # Test the context, storing it separately if required for an 429 # immediately invoked attribute. 430 431 elif self.final_method == "access-invoke": 432 emit(("<test_context_revert>", context, accessor)) 433 434 # Test the context and update the attribute details if 435 # appropriate. 436 437 else: 438 emit(("__test_context", context, accessor)) 439 440 elif self.context_test == "replace": 441 442 # Produce an object with updated context. 443 444 if self.final_method == "static": 445 emit(("__load_static_replace", context, self.origin)) 446 447 # Omit the context update operation where the target is static 448 # and the context is recorded separately. 449 450 elif self.final_method == "static-invoke": 451 pass 452 453 # If a separate context is used for an immediate invocation, 454 # produce the attribute details unchanged. 455 456 elif self.final_method == "access-invoke": 457 emit(accessor) 458 459 # Update the context in the attribute details. 460 461 else: 462 emit(("__update_context", context, accessor)) 463 464 # Omit the accessor for assignments and for invocations of static 465 # targets. Otherwise, emit the accessor which may involve the 466 # invocation of a test. 467 468 elif self.final_method not in ("assign", "static-assign", "static-invoke"): 469 emit(accessor) 470 471 return accessor 472 473 def process_context_identity(self, context, emit): 474 475 """ 476 Using 'context', use 'emit' to generate instructions to test the context 477 identity. 478 """ 479 480 if context: 481 482 # Only verify the context for invocation purposes if a suitable 483 # test has been performed. 484 485 if self.context_test in ("ignore", "replace") or \ 486 self.final_method in ("access-invoke", "static-invoke"): 487 488 emit(("<context_identity_verified>", context)) 489 else: 490 emit(("<context_identity>", context)) 491 492 def write(self, f, location): 493 494 "Write the plan to file 'f' with the given 'location' information." 495 496 print >>f, encode_access_location(location), \ 497 self.name or "{}", \ 498 self.test and "-".join(self.test) or "{}", \ 499 self.test_type or "{}", \ 500 self.base or "{}", \ 501 ".".join(self.traversed_attrnames) or "{}", \ 502 ".".join(self.traversed_attrname_modes) or "{}", \ 503 ".".join(self.remaining_attrnames) or "{}", \ 504 self.context, self.context_test, \ 505 self.first_method, self.final_method, self.origin or "{}", \ 506 ",".join(self.accessor_kinds) 507 508 # vim: tabstop=4 expandtab shiftwidth=4