| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404 |
- """A "low-level" IR builder class.
- LowLevelIRBuilder provides core abstractions we use for constructing
- IR as well as a number of higher-level ones (accessing attributes,
- calling functions and methods, and coercing between types, for
- example). The core principle of the low-level IR builder is that all
- of its facilities operate solely on the IR level and not the AST
- level---it has *no knowledge* of mypy types or expressions.
- """
- from __future__ import annotations
- from typing import Callable, Final, Optional, Sequence, Tuple
- from mypy.argmap import map_actuals_to_formals
- from mypy.nodes import ARG_POS, ARG_STAR, ARG_STAR2, ArgKind
- from mypy.operators import op_methods
- from mypy.types import AnyType, TypeOfAny
- from mypyc.common import (
- BITMAP_BITS,
- FAST_ISINSTANCE_MAX_SUBCLASSES,
- MAX_LITERAL_SHORT_INT,
- MAX_SHORT_INT,
- MIN_LITERAL_SHORT_INT,
- MIN_SHORT_INT,
- PLATFORM_SIZE,
- use_method_vectorcall,
- use_vectorcall,
- )
- from mypyc.errors import Errors
- from mypyc.ir.class_ir import ClassIR, all_concrete_classes
- from mypyc.ir.func_ir import FuncDecl, FuncSignature
- from mypyc.ir.ops import (
- ERR_FALSE,
- ERR_NEVER,
- NAMESPACE_MODULE,
- NAMESPACE_STATIC,
- NAMESPACE_TYPE,
- Assign,
- AssignMulti,
- BasicBlock,
- Box,
- Branch,
- Call,
- CallC,
- Cast,
- ComparisonOp,
- Extend,
- Float,
- FloatComparisonOp,
- FloatNeg,
- FloatOp,
- GetAttr,
- GetElementPtr,
- Goto,
- Integer,
- IntOp,
- KeepAlive,
- LoadAddress,
- LoadErrorValue,
- LoadLiteral,
- LoadMem,
- LoadStatic,
- MethodCall,
- Op,
- RaiseStandardError,
- Register,
- SetMem,
- Truncate,
- TupleGet,
- TupleSet,
- Unbox,
- Unreachable,
- Value,
- float_comparison_op_to_id,
- float_op_to_id,
- int_op_to_id,
- )
- from mypyc.ir.rtypes import (
- PyListObject,
- PyObject,
- PySetObject,
- PyVarObject,
- RArray,
- RInstance,
- RPrimitive,
- RTuple,
- RType,
- RUnion,
- bit_rprimitive,
- bitmap_rprimitive,
- bool_rprimitive,
- bytes_rprimitive,
- c_int_rprimitive,
- c_pointer_rprimitive,
- c_pyssize_t_rprimitive,
- c_size_t_rprimitive,
- check_native_int_range,
- dict_rprimitive,
- float_rprimitive,
- int_rprimitive,
- is_bit_rprimitive,
- is_bool_rprimitive,
- is_bytes_rprimitive,
- is_dict_rprimitive,
- is_fixed_width_rtype,
- is_float_rprimitive,
- is_int16_rprimitive,
- is_int32_rprimitive,
- is_int64_rprimitive,
- is_int_rprimitive,
- is_list_rprimitive,
- is_none_rprimitive,
- is_set_rprimitive,
- is_short_int_rprimitive,
- is_str_rprimitive,
- is_tagged,
- is_tuple_rprimitive,
- is_uint8_rprimitive,
- list_rprimitive,
- none_rprimitive,
- object_pointer_rprimitive,
- object_rprimitive,
- optional_value_type,
- pointer_rprimitive,
- short_int_rprimitive,
- str_rprimitive,
- )
- from mypyc.irbuild.mapper import Mapper
- from mypyc.irbuild.util import concrete_arg_kind
- from mypyc.options import CompilerOptions
- from mypyc.primitives.bytes_ops import bytes_compare
- from mypyc.primitives.dict_ops import (
- dict_build_op,
- dict_new_op,
- dict_ssize_t_size_op,
- dict_update_in_display_op,
- )
- from mypyc.primitives.exc_ops import err_occurred_op, keep_propagating_op
- from mypyc.primitives.float_ops import copysign_op, int_to_float_op
- from mypyc.primitives.generic_ops import (
- generic_len_op,
- generic_ssize_t_len_op,
- py_call_op,
- py_call_with_kwargs_op,
- py_getattr_op,
- py_method_call_op,
- py_vectorcall_method_op,
- py_vectorcall_op,
- )
- from mypyc.primitives.int_ops import (
- int16_divide_op,
- int16_mod_op,
- int16_overflow,
- int32_divide_op,
- int32_mod_op,
- int32_overflow,
- int64_divide_op,
- int64_mod_op,
- int64_to_int_op,
- int_comparison_op_mapping,
- int_to_int32_op,
- int_to_int64_op,
- ssize_t_to_int_op,
- uint8_overflow,
- )
- from mypyc.primitives.list_ops import list_build_op, list_extend_op, new_list_op
- from mypyc.primitives.misc_ops import bool_op, fast_isinstance_op, none_object_op
- from mypyc.primitives.registry import (
- ERR_NEG_INT,
- CFunctionDescription,
- binary_ops,
- method_call_ops,
- unary_ops,
- )
- from mypyc.primitives.set_ops import new_set_op
- from mypyc.primitives.str_ops import str_check_if_true, str_ssize_t_size_op, unicode_compare
- from mypyc.primitives.tuple_ops import list_tuple_op, new_tuple_op, new_tuple_with_length_op
- from mypyc.rt_subtype import is_runtime_subtype
- from mypyc.sametype import is_same_type
- from mypyc.subtype import is_subtype
- DictEntry = Tuple[Optional[Value], Value]
- # If the number of items is less than the threshold when initializing
- # a list, we would inline the generate IR using SetMem and expanded
- # for-loop. Otherwise, we would call `list_build_op` for larger lists.
- # TODO: The threshold is a randomly chosen number which needs further
- # study on real-world projects for a better balance.
- LIST_BUILDING_EXPANSION_THRESHOLD = 10
- # From CPython
- PY_VECTORCALL_ARGUMENTS_OFFSET: Final = 1 << (PLATFORM_SIZE * 8 - 1)
- FIXED_WIDTH_INT_BINARY_OPS: Final = {
- "+",
- "-",
- "*",
- "//",
- "%",
- "&",
- "|",
- "^",
- "<<",
- ">>",
- "+=",
- "-=",
- "*=",
- "//=",
- "%=",
- "&=",
- "|=",
- "^=",
- "<<=",
- ">>=",
- }
- # Binary operations on bools that are specialized and don't just promote operands to int
- BOOL_BINARY_OPS: Final = {"&", "&=", "|", "|=", "^", "^=", "==", "!=", "<", "<=", ">", ">="}
- class LowLevelIRBuilder:
- def __init__(
- self, current_module: str, errors: Errors, mapper: Mapper, options: CompilerOptions
- ) -> None:
- self.current_module = current_module
- self.errors = errors
- self.mapper = mapper
- self.options = options
- self.args: list[Register] = []
- self.blocks: list[BasicBlock] = []
- # Stack of except handler entry blocks
- self.error_handlers: list[BasicBlock | None] = [None]
- # Values that we need to keep alive as long as we have borrowed
- # temporaries. Use flush_keep_alives() to mark the end of the live range.
- self.keep_alives: list[Value] = []
- def set_module(self, module_name: str, module_path: str) -> None:
- """Set the name and path of the current module."""
- self.module_name = module_name
- self.module_path = module_path
- # Basic operations
- def add(self, op: Op) -> Value:
- """Add an op."""
- assert not self.blocks[-1].terminated, "Can't add to finished block"
- self.blocks[-1].ops.append(op)
- return op
- def goto(self, target: BasicBlock) -> None:
- """Add goto to a basic block."""
- if not self.blocks[-1].terminated:
- self.add(Goto(target))
- def activate_block(self, block: BasicBlock) -> None:
- """Add a basic block and make it the active one (target of adds)."""
- if self.blocks:
- assert self.blocks[-1].terminated
- block.error_handler = self.error_handlers[-1]
- self.blocks.append(block)
- def goto_and_activate(self, block: BasicBlock) -> None:
- """Add goto a block and make it the active block."""
- self.goto(block)
- self.activate_block(block)
- def push_error_handler(self, handler: BasicBlock | None) -> None:
- self.error_handlers.append(handler)
- def pop_error_handler(self) -> BasicBlock | None:
- return self.error_handlers.pop()
- def self(self) -> Register:
- """Return reference to the 'self' argument.
- This only works in a method.
- """
- return self.args[0]
- def flush_keep_alives(self) -> None:
- if self.keep_alives:
- self.add(KeepAlive(self.keep_alives.copy()))
- self.keep_alives = []
- # Type conversions
- def box(self, src: Value) -> Value:
- if src.type.is_unboxed:
- if isinstance(src, Integer) and is_tagged(src.type):
- return self.add(LoadLiteral(src.value >> 1, rtype=object_rprimitive))
- return self.add(Box(src))
- else:
- return src
- def unbox_or_cast(
- self, src: Value, target_type: RType, line: int, *, can_borrow: bool = False
- ) -> Value:
- if target_type.is_unboxed:
- return self.add(Unbox(src, target_type, line))
- else:
- if can_borrow:
- self.keep_alives.append(src)
- return self.add(Cast(src, target_type, line, borrow=can_borrow))
- def coerce(
- self,
- src: Value,
- target_type: RType,
- line: int,
- force: bool = False,
- *,
- can_borrow: bool = False,
- ) -> Value:
- """Generate a coercion/cast from one type to other (only if needed).
- For example, int -> object boxes the source int; int -> int emits nothing;
- object -> int unboxes the object. All conversions preserve object value.
- If force is true, always generate an op (even if it is just an assignment) so
- that the result will have exactly target_type as the type.
- Returns the register with the converted value (may be same as src).
- """
- src_type = src.type
- if src_type.is_unboxed and not target_type.is_unboxed:
- # Unboxed -> boxed
- return self.box(src)
- if (src_type.is_unboxed and target_type.is_unboxed) and not is_runtime_subtype(
- src_type, target_type
- ):
- if (
- isinstance(src, Integer)
- and is_short_int_rprimitive(src_type)
- and is_fixed_width_rtype(target_type)
- ):
- value = src.numeric_value()
- if not check_native_int_range(target_type, value):
- self.error(f'Value {value} is out of range for "{target_type}"', line)
- return Integer(src.value >> 1, target_type)
- elif is_int_rprimitive(src_type) and is_fixed_width_rtype(target_type):
- return self.coerce_int_to_fixed_width(src, target_type, line)
- elif is_fixed_width_rtype(src_type) and is_int_rprimitive(target_type):
- return self.coerce_fixed_width_to_int(src, line)
- elif is_short_int_rprimitive(src_type) and is_fixed_width_rtype(target_type):
- return self.coerce_short_int_to_fixed_width(src, target_type, line)
- elif (
- isinstance(src_type, RPrimitive)
- and isinstance(target_type, RPrimitive)
- and src_type.is_native_int
- and target_type.is_native_int
- and src_type.size == target_type.size
- and src_type.is_signed == target_type.is_signed
- ):
- # Equivalent types
- return src
- elif (is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type)) and is_tagged(
- target_type
- ):
- shifted = self.int_op(
- bool_rprimitive, src, Integer(1, bool_rprimitive), IntOp.LEFT_SHIFT
- )
- return self.add(Extend(shifted, target_type, signed=False))
- elif (
- is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type)
- ) and is_fixed_width_rtype(target_type):
- return self.add(Extend(src, target_type, signed=False))
- elif isinstance(src, Integer) and is_float_rprimitive(target_type):
- if is_tagged(src_type):
- return Float(float(src.value // 2))
- return Float(float(src.value))
- elif is_tagged(src_type) and is_float_rprimitive(target_type):
- return self.int_to_float(src, line)
- elif (
- isinstance(src_type, RTuple)
- and isinstance(target_type, RTuple)
- and len(src_type.types) == len(target_type.types)
- ):
- # Coerce between two tuple types by coercing each item separately
- values = []
- for i in range(len(src_type.types)):
- v = None
- if isinstance(src, TupleSet):
- item = src.items[i]
- # We can't reuse register values, since they can be modified.
- if not isinstance(item, Register):
- v = item
- if v is None:
- v = TupleGet(src, i)
- self.add(v)
- values.append(v)
- return self.add(
- TupleSet(
- [self.coerce(v, t, line) for v, t in zip(values, target_type.types)], line
- )
- )
- # To go between any other unboxed types, we go through a boxed
- # in-between value, for simplicity.
- tmp = self.box(src)
- return self.unbox_or_cast(tmp, target_type, line)
- if (not src_type.is_unboxed and target_type.is_unboxed) or not is_subtype(
- src_type, target_type
- ):
- return self.unbox_or_cast(src, target_type, line, can_borrow=can_borrow)
- elif force:
- tmp = Register(target_type)
- self.add(Assign(tmp, src))
- return tmp
- return src
- def coerce_int_to_fixed_width(self, src: Value, target_type: RType, line: int) -> Value:
- assert is_fixed_width_rtype(target_type), target_type
- assert isinstance(target_type, RPrimitive)
- res = Register(target_type)
- fast, slow, end = BasicBlock(), BasicBlock(), BasicBlock()
- check = self.check_tagged_short_int(src, line)
- self.add(Branch(check, fast, slow, Branch.BOOL))
- self.activate_block(fast)
- size = target_type.size
- if size < int_rprimitive.size:
- # Add a range check when the target type is smaller than the source tyoe
- fast2, fast3 = BasicBlock(), BasicBlock()
- upper_bound = 1 << (size * 8 - 1)
- if not target_type.is_signed:
- upper_bound *= 2
- check2 = self.add(ComparisonOp(src, Integer(upper_bound, src.type), ComparisonOp.SLT))
- self.add(Branch(check2, fast2, slow, Branch.BOOL))
- self.activate_block(fast2)
- if target_type.is_signed:
- lower_bound = -upper_bound
- else:
- lower_bound = 0
- check3 = self.add(ComparisonOp(src, Integer(lower_bound, src.type), ComparisonOp.SGE))
- self.add(Branch(check3, fast3, slow, Branch.BOOL))
- self.activate_block(fast3)
- tmp = self.int_op(
- c_pyssize_t_rprimitive,
- src,
- Integer(1, c_pyssize_t_rprimitive),
- IntOp.RIGHT_SHIFT,
- line,
- )
- tmp = self.add(Truncate(tmp, target_type))
- else:
- if size > int_rprimitive.size:
- tmp = self.add(Extend(src, target_type, signed=True))
- else:
- tmp = src
- tmp = self.int_op(target_type, tmp, Integer(1, target_type), IntOp.RIGHT_SHIFT, line)
- self.add(Assign(res, tmp))
- self.goto(end)
- self.activate_block(slow)
- if is_int64_rprimitive(target_type) or (
- is_int32_rprimitive(target_type) and size == int_rprimitive.size
- ):
- # Slow path calls a library function that handles more complex logic
- ptr = self.int_op(
- pointer_rprimitive, src, Integer(1, pointer_rprimitive), IntOp.XOR, line
- )
- ptr2 = Register(c_pointer_rprimitive)
- self.add(Assign(ptr2, ptr))
- if is_int64_rprimitive(target_type):
- conv_op = int_to_int64_op
- else:
- conv_op = int_to_int32_op
- tmp = self.call_c(conv_op, [ptr2], line)
- self.add(Assign(res, tmp))
- self.add(KeepAlive([src]))
- self.goto(end)
- elif is_int32_rprimitive(target_type):
- # Slow path just always generates an OverflowError
- self.call_c(int32_overflow, [], line)
- self.add(Unreachable())
- elif is_int16_rprimitive(target_type):
- # Slow path just always generates an OverflowError
- self.call_c(int16_overflow, [], line)
- self.add(Unreachable())
- elif is_uint8_rprimitive(target_type):
- # Slow path just always generates an OverflowError
- self.call_c(uint8_overflow, [], line)
- self.add(Unreachable())
- else:
- assert False, target_type
- self.activate_block(end)
- return res
- def coerce_short_int_to_fixed_width(self, src: Value, target_type: RType, line: int) -> Value:
- if is_int64_rprimitive(target_type):
- return self.int_op(target_type, src, Integer(1, target_type), IntOp.RIGHT_SHIFT, line)
- # TODO: i32
- assert False, (src.type, target_type)
- def coerce_fixed_width_to_int(self, src: Value, line: int) -> Value:
- if (
- (is_int32_rprimitive(src.type) and PLATFORM_SIZE == 8)
- or is_int16_rprimitive(src.type)
- or is_uint8_rprimitive(src.type)
- ):
- # Simple case -- just sign extend and shift.
- extended = self.add(Extend(src, c_pyssize_t_rprimitive, signed=src.type.is_signed))
- return self.int_op(
- int_rprimitive,
- extended,
- Integer(1, c_pyssize_t_rprimitive),
- IntOp.LEFT_SHIFT,
- line,
- )
- assert is_fixed_width_rtype(src.type)
- assert isinstance(src.type, RPrimitive)
- src_type = src.type
- res = Register(int_rprimitive)
- fast, fast2, slow, end = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()
- c1 = self.add(ComparisonOp(src, Integer(MAX_SHORT_INT, src_type), ComparisonOp.SLE))
- self.add(Branch(c1, fast, slow, Branch.BOOL))
- self.activate_block(fast)
- c2 = self.add(ComparisonOp(src, Integer(MIN_SHORT_INT, src_type), ComparisonOp.SGE))
- self.add(Branch(c2, fast2, slow, Branch.BOOL))
- self.activate_block(slow)
- if is_int64_rprimitive(src_type):
- conv_op = int64_to_int_op
- elif is_int32_rprimitive(src_type):
- assert PLATFORM_SIZE == 4
- conv_op = ssize_t_to_int_op
- else:
- assert False, src_type
- x = self.call_c(conv_op, [src], line)
- self.add(Assign(res, x))
- self.goto(end)
- self.activate_block(fast2)
- if int_rprimitive.size < src_type.size:
- tmp = self.add(Truncate(src, c_pyssize_t_rprimitive))
- else:
- tmp = src
- s = self.int_op(int_rprimitive, tmp, Integer(1, tmp.type), IntOp.LEFT_SHIFT, line)
- self.add(Assign(res, s))
- self.goto(end)
- self.activate_block(end)
- return res
- def coerce_nullable(self, src: Value, target_type: RType, line: int) -> Value:
- """Generate a coercion from a potentially null value."""
- if src.type.is_unboxed == target_type.is_unboxed and (
- (target_type.is_unboxed and is_runtime_subtype(src.type, target_type))
- or (not target_type.is_unboxed and is_subtype(src.type, target_type))
- ):
- return src
- target = Register(target_type)
- valid, invalid, out = BasicBlock(), BasicBlock(), BasicBlock()
- self.add(Branch(src, invalid, valid, Branch.IS_ERROR))
- self.activate_block(valid)
- coerced = self.coerce(src, target_type, line)
- self.add(Assign(target, coerced, line))
- self.goto(out)
- self.activate_block(invalid)
- error = self.add(LoadErrorValue(target_type))
- self.add(Assign(target, error, line))
- self.goto_and_activate(out)
- return target
- # Attribute access
- def get_attr(
- self, obj: Value, attr: str, result_type: RType, line: int, *, borrow: bool = False
- ) -> Value:
- """Get a native or Python attribute of an object."""
- if (
- isinstance(obj.type, RInstance)
- and obj.type.class_ir.is_ext_class
- and obj.type.class_ir.has_attr(attr)
- ):
- op = GetAttr(obj, attr, line, borrow=borrow)
- # For non-refcounted attribute types, the borrow might be
- # disabled even if requested, so don't check 'borrow'.
- if op.is_borrowed:
- self.keep_alives.append(obj)
- return self.add(op)
- elif isinstance(obj.type, RUnion):
- return self.union_get_attr(obj, obj.type, attr, result_type, line)
- else:
- return self.py_get_attr(obj, attr, line)
- def union_get_attr(
- self, obj: Value, rtype: RUnion, attr: str, result_type: RType, line: int
- ) -> Value:
- """Get an attribute of an object with a union type."""
- def get_item_attr(value: Value) -> Value:
- return self.get_attr(value, attr, result_type, line)
- return self.decompose_union_helper(obj, rtype, result_type, get_item_attr, line)
- def py_get_attr(self, obj: Value, attr: str, line: int) -> Value:
- """Get a Python attribute (slow).
- Prefer get_attr() which generates optimized code for native classes.
- """
- key = self.load_str(attr)
- return self.call_c(py_getattr_op, [obj, key], line)
- # isinstance() checks
- def isinstance_helper(self, obj: Value, class_irs: list[ClassIR], line: int) -> Value:
- """Fast path for isinstance() that checks against a list of native classes."""
- if not class_irs:
- return self.false()
- ret = self.isinstance_native(obj, class_irs[0], line)
- for class_ir in class_irs[1:]:
- def other() -> Value:
- return self.isinstance_native(obj, class_ir, line)
- ret = self.shortcircuit_helper("or", bool_rprimitive, lambda: ret, other, line)
- return ret
- def get_type_of_obj(self, obj: Value, line: int) -> Value:
- ob_type_address = self.add(GetElementPtr(obj, PyObject, "ob_type", line))
- ob_type = self.add(LoadMem(object_rprimitive, ob_type_address))
- self.add(KeepAlive([obj]))
- return ob_type
- def type_is_op(self, obj: Value, type_obj: Value, line: int) -> Value:
- typ = self.get_type_of_obj(obj, line)
- return self.add(ComparisonOp(typ, type_obj, ComparisonOp.EQ, line))
- def isinstance_native(self, obj: Value, class_ir: ClassIR, line: int) -> Value:
- """Fast isinstance() check for a native class.
- If there are three or fewer concrete (non-trait) classes among the class
- and all its children, use even faster type comparison checks `type(obj)
- is typ`.
- """
- concrete = all_concrete_classes(class_ir)
- if concrete is None or len(concrete) > FAST_ISINSTANCE_MAX_SUBCLASSES + 1:
- return self.call_c(fast_isinstance_op, [obj, self.get_native_type(class_ir)], line)
- if not concrete:
- # There can't be any concrete instance that matches this.
- return self.false()
- type_obj = self.get_native_type(concrete[0])
- ret = self.type_is_op(obj, type_obj, line)
- for c in concrete[1:]:
- def other() -> Value:
- return self.type_is_op(obj, self.get_native_type(c), line)
- ret = self.shortcircuit_helper("or", bool_rprimitive, lambda: ret, other, line)
- return ret
- # Calls
- def _construct_varargs(
- self,
- args: Sequence[tuple[Value, ArgKind, str | None]],
- line: int,
- *,
- has_star: bool,
- has_star2: bool,
- ) -> tuple[Value | None, Value | None]:
- """Construct *args and **kwargs from a collection of arguments
- This is pretty complicated, and almost all of the complication here stems from
- one of two things (but mostly the second):
- * The handling of ARG_STAR/ARG_STAR2. We want to create as much of the args/kwargs
- values in one go as we can, so we collect values until our hand is forced, and
- then we emit creation of the list/tuple, and expand it from there if needed.
- * Support potentially nullable argument values. This has very narrow applicability,
- as this will never be done by our compiled Python code, but is critically used
- by gen_glue_method when generating glue methods to mediate between the function
- signature of a parent class and its subclasses.
- For named-only arguments, this is quite simple: if it is
- null, don't put it in the dict.
- For positional-or-named arguments, things are much more complicated.
- * First, anything that was passed as a positional arg
- must be forwarded along as a positional arg. It *must
- not* be converted to a named arg. This is because mypy
- does not enforce that positional-or-named arguments
- have the same name in subclasses, and it is not
- uncommon for code to have different names in
- subclasses (a bunch of mypy's visitors do this, for
- example!). This is arguably a bug in both mypy and code doing
- this, and they ought to be using positional-only arguments, but
- positional-only arguments are new and ugly.
- * On the flip side, we're willing to accept the
- infelicity of sometimes turning an argument that was
- passed by keyword into a positional argument. It's wrong,
- but it's very marginal, and avoiding it would require passing
- a bitmask of which arguments were named with every function call,
- or something similar.
- (See some discussion of this in testComplicatedArgs)
- Thus, our strategy for positional-or-named arguments is to
- always pass them as positional, except in the one
- situation where we can not, and where we can be absolutely
- sure they were passed by name: when an *earlier*
- positional argument was missing its value.
- This means that if we have a method `f(self, x: int=..., y: object=...)`:
- * x and y present: args=(x, y), kwargs={}
- * x present, y missing: args=(x,), kwargs={}
- * x missing, y present: args=(), kwargs={'y': y}
- To implement this, when we have multiple optional
- positional arguments, we maintain a flag in a register
- that tracks whether an argument has been missing, and for
- each such optional argument (except the first), we check
- the flag to determine whether to append the argument to
- the *args list or add it to the **kwargs dict. What a
- mess!
- This is what really makes everything here such a tangle;
- otherwise the *args and **kwargs code could be separated.
- The arguments has_star and has_star2 indicate whether the target function
- takes an ARG_STAR and ARG_STAR2 argument, respectively.
- (These will always be true when making a pycall, and be based
- on the actual target signature for a native call.)
- """
- star_result: Value | None = None
- star2_result: Value | None = None
- # We aggregate values that need to go into *args and **kwargs
- # in these lists. Once all arguments are processed (in the
- # happiest case), or we encounter an ARG_STAR/ARG_STAR2 or a
- # nullable arg, then we create the list and/or dict.
- star_values: list[Value] = []
- star2_keys: list[Value] = []
- star2_values: list[Value] = []
- seen_empty_reg: Register | None = None
- for value, kind, name in args:
- if kind == ARG_STAR:
- if star_result is None:
- star_result = self.new_list_op(star_values, line)
- self.call_c(list_extend_op, [star_result, value], line)
- elif kind == ARG_STAR2:
- if star2_result is None:
- star2_result = self._create_dict(star2_keys, star2_values, line)
- self.call_c(dict_update_in_display_op, [star2_result, value], line=line)
- else:
- nullable = kind.is_optional()
- maybe_pos = kind.is_positional() and has_star
- maybe_named = kind.is_named() or (kind.is_optional() and name and has_star2)
- # If the argument is nullable, we need to create the
- # relevant args/kwargs objects so that we can
- # conditionally modify them.
- if nullable:
- if maybe_pos and star_result is None:
- star_result = self.new_list_op(star_values, line)
- if maybe_named and star2_result is None:
- star2_result = self._create_dict(star2_keys, star2_values, line)
- # Easy cases: just collect the argument.
- if maybe_pos and star_result is None:
- star_values.append(value)
- continue
- if maybe_named and star2_result is None:
- assert name is not None
- key = self.load_str(name)
- star2_keys.append(key)
- star2_values.append(value)
- continue
- # OK, anything that is nullable or *after* a nullable arg needs to be here
- # TODO: We could try harder to avoid creating basic blocks in the common case
- new_seen_empty_reg = seen_empty_reg
- out = BasicBlock()
- if nullable:
- # If this is the first nullable positional arg we've seen, create
- # a register to track whether anything has been null.
- # (We won't *check* the register until the next argument, though.)
- if maybe_pos and not seen_empty_reg:
- new_seen_empty_reg = Register(bool_rprimitive)
- self.add(Assign(new_seen_empty_reg, self.false(), line))
- skip = BasicBlock() if maybe_pos else out
- keep = BasicBlock()
- self.add(Branch(value, skip, keep, Branch.IS_ERROR))
- self.activate_block(keep)
- # If this could be positional or named and we /might/ have seen a missing
- # positional arg, then we need to compile *both* a positional and named
- # version! What a pain!
- if maybe_pos and maybe_named and seen_empty_reg:
- pos_block, named_block = BasicBlock(), BasicBlock()
- self.add(Branch(seen_empty_reg, named_block, pos_block, Branch.BOOL))
- else:
- pos_block = named_block = BasicBlock()
- self.goto(pos_block)
- if maybe_pos:
- self.activate_block(pos_block)
- assert star_result
- self.translate_special_method_call(
- star_result, "append", [value], result_type=None, line=line
- )
- self.goto(out)
- if maybe_named and (not maybe_pos or seen_empty_reg):
- self.activate_block(named_block)
- assert name is not None
- key = self.load_str(name)
- assert star2_result
- self.translate_special_method_call(
- star2_result, "__setitem__", [key, value], result_type=None, line=line
- )
- self.goto(out)
- if nullable and maybe_pos and new_seen_empty_reg:
- assert skip is not out
- self.activate_block(skip)
- self.add(Assign(new_seen_empty_reg, self.true(), line))
- self.goto(out)
- self.activate_block(out)
- seen_empty_reg = new_seen_empty_reg
- assert not (star_result or star_values) or has_star
- assert not (star2_result or star2_values) or has_star2
- if has_star:
- # If we managed to make it this far without creating a
- # *args list, then we can directly create a
- # tuple. Otherwise create the tuple from the list.
- if star_result is None:
- star_result = self.new_tuple(star_values, line)
- else:
- star_result = self.call_c(list_tuple_op, [star_result], line)
- if has_star2 and star2_result is None:
- star2_result = self._create_dict(star2_keys, star2_values, line)
- return star_result, star2_result
- def py_call(
- self,
- function: Value,
- arg_values: list[Value],
- line: int,
- arg_kinds: list[ArgKind] | None = None,
- arg_names: Sequence[str | None] | None = None,
- ) -> Value:
- """Call a Python function (non-native and slow).
- Use py_call_op or py_call_with_kwargs_op for Python function call.
- """
- if use_vectorcall(self.options.capi_version):
- # More recent Python versions support faster vectorcalls.
- result = self._py_vector_call(function, arg_values, line, arg_kinds, arg_names)
- if result is not None:
- return result
- # If all arguments are positional, we can use py_call_op.
- if arg_kinds is None or all(kind == ARG_POS for kind in arg_kinds):
- return self.call_c(py_call_op, [function] + arg_values, line)
- # Otherwise fallback to py_call_with_kwargs_op.
- assert arg_names is not None
- pos_args_tuple, kw_args_dict = self._construct_varargs(
- list(zip(arg_values, arg_kinds, arg_names)), line, has_star=True, has_star2=True
- )
- assert pos_args_tuple and kw_args_dict
- return self.call_c(py_call_with_kwargs_op, [function, pos_args_tuple, kw_args_dict], line)
- def _py_vector_call(
- self,
- function: Value,
- arg_values: list[Value],
- line: int,
- arg_kinds: list[ArgKind] | None = None,
- arg_names: Sequence[str | None] | None = None,
- ) -> Value | None:
- """Call function using the vectorcall API if possible.
- Return the return value if successful. Return None if a non-vectorcall
- API should be used instead.
- """
- # We can do this if all args are positional or named (no *args or **kwargs, not optional).
- if arg_kinds is None or all(
- not kind.is_star() and not kind.is_optional() for kind in arg_kinds
- ):
- if arg_values:
- # Create a C array containing all arguments as boxed values.
- coerced_args = [self.coerce(arg, object_rprimitive, line) for arg in arg_values]
- arg_ptr = self.setup_rarray(object_rprimitive, coerced_args, object_ptr=True)
- else:
- arg_ptr = Integer(0, object_pointer_rprimitive)
- num_pos = num_positional_args(arg_values, arg_kinds)
- keywords = self._vectorcall_keywords(arg_names)
- value = self.call_c(
- py_vectorcall_op,
- [function, arg_ptr, Integer(num_pos, c_size_t_rprimitive), keywords],
- line,
- )
- if arg_values:
- # Make sure arguments won't be freed until after the call.
- # We need this because RArray doesn't support automatic
- # memory management.
- self.add(KeepAlive(coerced_args))
- return value
- return None
- def _vectorcall_keywords(self, arg_names: Sequence[str | None] | None) -> Value:
- """Return a reference to a tuple literal with keyword argument names.
- Return null pointer if there are no keyword arguments.
- """
- if arg_names:
- kw_list = [name for name in arg_names if name is not None]
- if kw_list:
- return self.add(LoadLiteral(tuple(kw_list), object_rprimitive))
- return Integer(0, object_rprimitive)
- def py_method_call(
- self,
- obj: Value,
- method_name: str,
- arg_values: list[Value],
- line: int,
- arg_kinds: list[ArgKind] | None,
- arg_names: Sequence[str | None] | None,
- ) -> Value:
- """Call a Python method (non-native and slow)."""
- if use_method_vectorcall(self.options.capi_version):
- # More recent Python versions support faster vectorcalls.
- result = self._py_vector_method_call(
- obj, method_name, arg_values, line, arg_kinds, arg_names
- )
- if result is not None:
- return result
- if arg_kinds is None or all(kind == ARG_POS for kind in arg_kinds):
- # Use legacy method call API
- method_name_reg = self.load_str(method_name)
- return self.call_c(py_method_call_op, [obj, method_name_reg] + arg_values, line)
- else:
- # Use py_call since it supports keyword arguments (and vectorcalls).
- method = self.py_get_attr(obj, method_name, line)
- return self.py_call(method, arg_values, line, arg_kinds=arg_kinds, arg_names=arg_names)
- def _py_vector_method_call(
- self,
- obj: Value,
- method_name: str,
- arg_values: list[Value],
- line: int,
- arg_kinds: list[ArgKind] | None,
- arg_names: Sequence[str | None] | None,
- ) -> Value | None:
- """Call method using the vectorcall API if possible.
- Return the return value if successful. Return None if a non-vectorcall
- API should be used instead.
- """
- if arg_kinds is None or all(
- not kind.is_star() and not kind.is_optional() for kind in arg_kinds
- ):
- method_name_reg = self.load_str(method_name)
- coerced_args = [
- self.coerce(arg, object_rprimitive, line) for arg in [obj] + arg_values
- ]
- arg_ptr = self.setup_rarray(object_rprimitive, coerced_args, object_ptr=True)
- num_pos = num_positional_args(arg_values, arg_kinds)
- keywords = self._vectorcall_keywords(arg_names)
- value = self.call_c(
- py_vectorcall_method_op,
- [
- method_name_reg,
- arg_ptr,
- Integer((num_pos + 1) | PY_VECTORCALL_ARGUMENTS_OFFSET, c_size_t_rprimitive),
- keywords,
- ],
- line,
- )
- # Make sure arguments won't be freed until after the call.
- # We need this because RArray doesn't support automatic
- # memory management.
- self.add(KeepAlive(coerced_args))
- return value
- return None
- def call(
- self,
- decl: FuncDecl,
- args: Sequence[Value],
- arg_kinds: list[ArgKind],
- arg_names: Sequence[str | None],
- line: int,
- *,
- bitmap_args: list[Register] | None = None,
- ) -> Value:
- """Call a native function.
- If bitmap_args is given, they override the values of (some) of the bitmap
- arguments used to track the presence of values for certain arguments. By
- default, the values of the bitmap arguments are inferred from args.
- """
- # Normalize args to positionals.
- args = self.native_args_to_positional(
- args, arg_kinds, arg_names, decl.sig, line, bitmap_args=bitmap_args
- )
- return self.add(Call(decl, args, line))
- def native_args_to_positional(
- self,
- args: Sequence[Value],
- arg_kinds: list[ArgKind],
- arg_names: Sequence[str | None],
- sig: FuncSignature,
- line: int,
- *,
- bitmap_args: list[Register] | None = None,
- ) -> list[Value]:
- """Prepare arguments for a native call.
- Given args/kinds/names and a target signature for a native call, map
- keyword arguments to their appropriate place in the argument list,
- fill in error values for unspecified default arguments,
- package arguments that will go into *args/**kwargs into a tuple/dict,
- and coerce arguments to the appropriate type.
- """
- sig_args = sig.args
- n = sig.num_bitmap_args
- if n:
- sig_args = sig_args[:-n]
- sig_arg_kinds = [arg.kind for arg in sig_args]
- sig_arg_names = [arg.name for arg in sig_args]
- concrete_kinds = [concrete_arg_kind(arg_kind) for arg_kind in arg_kinds]
- formal_to_actual = map_actuals_to_formals(
- concrete_kinds,
- arg_names,
- sig_arg_kinds,
- sig_arg_names,
- lambda n: AnyType(TypeOfAny.special_form),
- )
- # First scan for */** and construct those
- has_star = has_star2 = False
- star_arg_entries = []
- for lst, arg in zip(formal_to_actual, sig_args):
- if arg.kind.is_star():
- star_arg_entries.extend([(args[i], arg_kinds[i], arg_names[i]) for i in lst])
- has_star = has_star or arg.kind == ARG_STAR
- has_star2 = has_star2 or arg.kind == ARG_STAR2
- star_arg, star2_arg = self._construct_varargs(
- star_arg_entries, line, has_star=has_star, has_star2=has_star2
- )
- # Flatten out the arguments, loading error values for default
- # arguments, constructing tuples/dicts for star args, and
- # coercing everything to the expected type.
- output_args: list[Value] = []
- for lst, arg in zip(formal_to_actual, sig_args):
- if arg.kind == ARG_STAR:
- assert star_arg
- output_arg = star_arg
- elif arg.kind == ARG_STAR2:
- assert star2_arg
- output_arg = star2_arg
- elif not lst:
- if is_fixed_width_rtype(arg.type):
- output_arg = Integer(0, arg.type)
- elif is_float_rprimitive(arg.type):
- output_arg = Float(0.0)
- else:
- output_arg = self.add(LoadErrorValue(arg.type, is_borrowed=True))
- else:
- base_arg = args[lst[0]]
- if arg_kinds[lst[0]].is_optional():
- output_arg = self.coerce_nullable(base_arg, arg.type, line)
- else:
- output_arg = self.coerce(base_arg, arg.type, line)
- output_args.append(output_arg)
- for i in reversed(range(n)):
- if bitmap_args and i < len(bitmap_args):
- # Use override provided by caller
- output_args.append(bitmap_args[i])
- continue
- # Infer values of bitmap args
- bitmap = 0
- c = 0
- for lst, arg in zip(formal_to_actual, sig_args):
- if arg.kind.is_optional() and arg.type.error_overlap:
- if i * BITMAP_BITS <= c < (i + 1) * BITMAP_BITS:
- if lst:
- bitmap |= 1 << (c & (BITMAP_BITS - 1))
- c += 1
- output_args.append(Integer(bitmap, bitmap_rprimitive))
- return output_args
- def gen_method_call(
- self,
- base: Value,
- name: str,
- arg_values: list[Value],
- result_type: RType | None,
- line: int,
- arg_kinds: list[ArgKind] | None = None,
- arg_names: list[str | None] | None = None,
- can_borrow: bool = False,
- ) -> Value:
- """Generate either a native or Python method call."""
- # If we have *args, then fallback to Python method call.
- if arg_kinds is not None and any(kind.is_star() for kind in arg_kinds):
- return self.py_method_call(base, name, arg_values, base.line, arg_kinds, arg_names)
- # If the base type is one of ours, do a MethodCall
- if (
- isinstance(base.type, RInstance)
- and base.type.class_ir.is_ext_class
- and not base.type.class_ir.builtin_base
- ):
- if base.type.class_ir.has_method(name):
- decl = base.type.class_ir.method_decl(name)
- if arg_kinds is None:
- assert arg_names is None, "arg_kinds not present but arg_names is"
- arg_kinds = [ARG_POS for _ in arg_values]
- arg_names = [None for _ in arg_values]
- else:
- assert arg_names is not None, "arg_kinds present but arg_names is not"
- # Normalize args to positionals.
- assert decl.bound_sig
- arg_values = self.native_args_to_positional(
- arg_values, arg_kinds, arg_names, decl.bound_sig, line
- )
- return self.add(MethodCall(base, name, arg_values, line))
- elif base.type.class_ir.has_attr(name):
- function = self.add(GetAttr(base, name, line))
- return self.py_call(
- function, arg_values, line, arg_kinds=arg_kinds, arg_names=arg_names
- )
- elif isinstance(base.type, RUnion):
- return self.union_method_call(
- base, base.type, name, arg_values, result_type, line, arg_kinds, arg_names
- )
- # Try to do a special-cased method call
- if not arg_kinds or arg_kinds == [ARG_POS] * len(arg_values):
- target = self.translate_special_method_call(
- base, name, arg_values, result_type, line, can_borrow=can_borrow
- )
- if target:
- return target
- # Fall back to Python method call
- return self.py_method_call(base, name, arg_values, line, arg_kinds, arg_names)
- def union_method_call(
- self,
- base: Value,
- obj_type: RUnion,
- name: str,
- arg_values: list[Value],
- return_rtype: RType | None,
- line: int,
- arg_kinds: list[ArgKind] | None,
- arg_names: list[str | None] | None,
- ) -> Value:
- """Generate a method call with a union type for the object."""
- # Union method call needs a return_rtype for the type of the output register.
- # If we don't have one, use object_rprimitive.
- return_rtype = return_rtype or object_rprimitive
- def call_union_item(value: Value) -> Value:
- return self.gen_method_call(
- value, name, arg_values, return_rtype, line, arg_kinds, arg_names
- )
- return self.decompose_union_helper(base, obj_type, return_rtype, call_union_item, line)
- # Loading various values
- def none(self) -> Value:
- """Load unboxed None value (type: none_rprimitive)."""
- return Integer(1, none_rprimitive)
- def true(self) -> Value:
- """Load unboxed True value (type: bool_rprimitive)."""
- return Integer(1, bool_rprimitive)
- def false(self) -> Value:
- """Load unboxed False value (type: bool_rprimitive)."""
- return Integer(0, bool_rprimitive)
- def none_object(self) -> Value:
- """Load Python None value (type: object_rprimitive)."""
- return self.add(LoadAddress(none_object_op.type, none_object_op.src, line=-1))
- def load_int(self, value: int) -> Value:
- """Load a tagged (Python) integer literal value."""
- if value > MAX_LITERAL_SHORT_INT or value < MIN_LITERAL_SHORT_INT:
- return self.add(LoadLiteral(value, int_rprimitive))
- else:
- return Integer(value)
- def load_float(self, value: float) -> Value:
- """Load a float literal value."""
- return Float(value)
- def load_str(self, value: str) -> Value:
- """Load a str literal value.
- This is useful for more than just str literals; for example, method calls
- also require a PyObject * form for the name of the method.
- """
- return self.add(LoadLiteral(value, str_rprimitive))
- def load_bytes(self, value: bytes) -> Value:
- """Load a bytes literal value."""
- return self.add(LoadLiteral(value, bytes_rprimitive))
- def load_complex(self, value: complex) -> Value:
- """Load a complex literal value."""
- return self.add(LoadLiteral(value, object_rprimitive))
- def load_static_checked(
- self,
- typ: RType,
- identifier: str,
- module_name: str | None = None,
- namespace: str = NAMESPACE_STATIC,
- line: int = -1,
- error_msg: str | None = None,
- ) -> Value:
- if error_msg is None:
- error_msg = f'name "{identifier}" is not defined'
- ok_block, error_block = BasicBlock(), BasicBlock()
- value = self.add(LoadStatic(typ, identifier, module_name, namespace, line=line))
- self.add(Branch(value, error_block, ok_block, Branch.IS_ERROR, rare=True))
- self.activate_block(error_block)
- self.add(RaiseStandardError(RaiseStandardError.NAME_ERROR, error_msg, line))
- self.add(Unreachable())
- self.activate_block(ok_block)
- return value
- def load_module(self, name: str) -> Value:
- return self.add(LoadStatic(object_rprimitive, name, namespace=NAMESPACE_MODULE))
- def get_native_type(self, cls: ClassIR) -> Value:
- """Load native type object."""
- fullname = f"{cls.module_name}.{cls.name}"
- return self.load_native_type_object(fullname)
- def load_native_type_object(self, fullname: str) -> Value:
- module, name = fullname.rsplit(".", 1)
- return self.add(LoadStatic(object_rprimitive, name, module, NAMESPACE_TYPE))
- # Other primitive operations
- def binary_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value:
- """Perform a binary operation.
- Generate specialized operations based on operand types, with a fallback
- to generic operations.
- """
- ltype = lreg.type
- rtype = rreg.type
- # Special case tuple comparison here so that nested tuples can be supported
- if isinstance(ltype, RTuple) and isinstance(rtype, RTuple) and op in ("==", "!="):
- return self.compare_tuples(lreg, rreg, op, line)
- # Special case == and != when we can resolve the method call statically
- if op in ("==", "!="):
- value = self.translate_eq_cmp(lreg, rreg, op, line)
- if value is not None:
- return value
- # Special case various ops
- if op in ("is", "is not"):
- return self.translate_is_op(lreg, rreg, op, line)
- # TODO: modify 'str' to use same interface as 'compare_bytes' as it avoids
- # call to PyErr_Occurred()
- if is_str_rprimitive(ltype) and is_str_rprimitive(rtype) and op in ("==", "!="):
- return self.compare_strings(lreg, rreg, op, line)
- if is_bytes_rprimitive(ltype) and is_bytes_rprimitive(rtype) and op in ("==", "!="):
- return self.compare_bytes(lreg, rreg, op, line)
- if is_tagged(ltype) and is_tagged(rtype) and op in int_comparison_op_mapping:
- return self.compare_tagged(lreg, rreg, op, line)
- if is_bool_rprimitive(ltype) and is_bool_rprimitive(rtype) and op in BOOL_BINARY_OPS:
- if op in ComparisonOp.signed_ops:
- return self.bool_comparison_op(lreg, rreg, op, line)
- else:
- return self.bool_bitwise_op(lreg, rreg, op[0], line)
- if isinstance(rtype, RInstance) and op in ("in", "not in"):
- return self.translate_instance_contains(rreg, lreg, op, line)
- if is_fixed_width_rtype(ltype):
- if op in FIXED_WIDTH_INT_BINARY_OPS:
- if op.endswith("="):
- op = op[:-1]
- if op != "//":
- op_id = int_op_to_id[op]
- else:
- op_id = IntOp.DIV
- if is_bool_rprimitive(rtype) or is_bit_rprimitive(rtype):
- rreg = self.coerce(rreg, ltype, line)
- rtype = ltype
- if is_fixed_width_rtype(rtype) or is_tagged(rtype):
- return self.fixed_width_int_op(ltype, lreg, rreg, op_id, line)
- if isinstance(rreg, Integer):
- return self.fixed_width_int_op(
- ltype, lreg, self.coerce(rreg, ltype, line), op_id, line
- )
- elif op in ComparisonOp.signed_ops:
- if is_int_rprimitive(rtype):
- rreg = self.coerce_int_to_fixed_width(rreg, ltype, line)
- elif is_bool_rprimitive(rtype) or is_bit_rprimitive(rtype):
- rreg = self.coerce(rreg, ltype, line)
- op_id = ComparisonOp.signed_ops[op]
- if is_fixed_width_rtype(rreg.type):
- return self.comparison_op(lreg, rreg, op_id, line)
- if isinstance(rreg, Integer):
- return self.comparison_op(lreg, self.coerce(rreg, ltype, line), op_id, line)
- elif is_fixed_width_rtype(rtype):
- if op in FIXED_WIDTH_INT_BINARY_OPS:
- if op.endswith("="):
- op = op[:-1]
- if op != "//":
- op_id = int_op_to_id[op]
- else:
- op_id = IntOp.DIV
- if isinstance(lreg, Integer):
- return self.fixed_width_int_op(
- rtype, self.coerce(lreg, rtype, line), rreg, op_id, line
- )
- if is_tagged(ltype):
- return self.fixed_width_int_op(rtype, lreg, rreg, op_id, line)
- if is_bool_rprimitive(ltype) or is_bit_rprimitive(ltype):
- lreg = self.coerce(lreg, rtype, line)
- return self.fixed_width_int_op(rtype, lreg, rreg, op_id, line)
- elif op in ComparisonOp.signed_ops:
- if is_int_rprimitive(ltype):
- lreg = self.coerce_int_to_fixed_width(lreg, rtype, line)
- elif is_bool_rprimitive(ltype) or is_bit_rprimitive(ltype):
- lreg = self.coerce(lreg, rtype, line)
- op_id = ComparisonOp.signed_ops[op]
- if isinstance(lreg, Integer):
- return self.comparison_op(self.coerce(lreg, rtype, line), rreg, op_id, line)
- if is_fixed_width_rtype(lreg.type):
- return self.comparison_op(lreg, rreg, op_id, line)
- # Mixed int comparisons
- if op in ("==", "!="):
- op_id = ComparisonOp.signed_ops[op]
- if is_tagged(ltype) and is_subtype(rtype, ltype):
- rreg = self.coerce(rreg, int_rprimitive, line)
- return self.comparison_op(lreg, rreg, op_id, line)
- if is_tagged(rtype) and is_subtype(ltype, rtype):
- lreg = self.coerce(lreg, int_rprimitive, line)
- return self.comparison_op(lreg, rreg, op_id, line)
- elif op in op in int_comparison_op_mapping:
- if is_tagged(ltype) and is_subtype(rtype, ltype):
- rreg = self.coerce(rreg, short_int_rprimitive, line)
- return self.compare_tagged(lreg, rreg, op, line)
- if is_tagged(rtype) and is_subtype(ltype, rtype):
- lreg = self.coerce(lreg, short_int_rprimitive, line)
- return self.compare_tagged(lreg, rreg, op, line)
- if is_float_rprimitive(ltype) or is_float_rprimitive(rtype):
- if isinstance(lreg, Integer):
- lreg = Float(float(lreg.numeric_value()))
- elif isinstance(rreg, Integer):
- rreg = Float(float(rreg.numeric_value()))
- elif is_int_rprimitive(lreg.type):
- lreg = self.int_to_float(lreg, line)
- elif is_int_rprimitive(rreg.type):
- rreg = self.int_to_float(rreg, line)
- if is_float_rprimitive(lreg.type) and is_float_rprimitive(rreg.type):
- if op in float_comparison_op_to_id:
- return self.compare_floats(lreg, rreg, float_comparison_op_to_id[op], line)
- if op.endswith("="):
- base_op = op[:-1]
- else:
- base_op = op
- if base_op in float_op_to_id:
- return self.float_op(lreg, rreg, base_op, line)
- call_c_ops_candidates = binary_ops.get(op, [])
- target = self.matching_call_c(call_c_ops_candidates, [lreg, rreg], line)
- assert target, "Unsupported binary operation: %s" % op
- return target
- def check_tagged_short_int(self, val: Value, line: int, negated: bool = False) -> Value:
- """Check if a tagged integer is a short integer.
- Return the result of the check (value of type 'bit').
- """
- int_tag = Integer(1, c_pyssize_t_rprimitive, line)
- bitwise_and = self.int_op(c_pyssize_t_rprimitive, val, int_tag, IntOp.AND, line)
- zero = Integer(0, c_pyssize_t_rprimitive, line)
- op = ComparisonOp.NEQ if negated else ComparisonOp.EQ
- check = self.comparison_op(bitwise_and, zero, op, line)
- return check
- def compare_tagged(self, lhs: Value, rhs: Value, op: str, line: int) -> Value:
- """Compare two tagged integers using given operator (value context)."""
- # generate fast binary logic ops on short ints
- if is_short_int_rprimitive(lhs.type) and is_short_int_rprimitive(rhs.type):
- return self.comparison_op(lhs, rhs, int_comparison_op_mapping[op][0], line)
- op_type, c_func_desc, negate_result, swap_op = int_comparison_op_mapping[op]
- result = Register(bool_rprimitive)
- short_int_block, int_block, out = BasicBlock(), BasicBlock(), BasicBlock()
- check_lhs = self.check_tagged_short_int(lhs, line)
- if op in ("==", "!="):
- check = check_lhs
- else:
- # for non-equality logical ops (less/greater than, etc.), need to check both sides
- check_rhs = self.check_tagged_short_int(rhs, line)
- check = self.int_op(bit_rprimitive, check_lhs, check_rhs, IntOp.AND, line)
- self.add(Branch(check, short_int_block, int_block, Branch.BOOL))
- self.activate_block(short_int_block)
- eq = self.comparison_op(lhs, rhs, op_type, line)
- self.add(Assign(result, eq, line))
- self.goto(out)
- self.activate_block(int_block)
- if swap_op:
- args = [rhs, lhs]
- else:
- args = [lhs, rhs]
- call = self.call_c(c_func_desc, args, line)
- if negate_result:
- # TODO: introduce UnaryIntOp?
- call_result = self.unary_op(call, "not", line)
- else:
- call_result = call
- self.add(Assign(result, call_result, line))
- self.goto_and_activate(out)
- return result
- def compare_tagged_condition(
- self, lhs: Value, rhs: Value, op: str, true: BasicBlock, false: BasicBlock, line: int
- ) -> None:
- """Compare two tagged integers using given operator (conditional context).
- Assume lhs and rhs are tagged integers.
- Args:
- lhs: Left operand
- rhs: Right operand
- op: Operation, one of '==', '!=', '<', '<=', '>', '<='
- true: Branch target if comparison is true
- false: Branch target if comparison is false
- """
- is_eq = op in ("==", "!=")
- if (is_short_int_rprimitive(lhs.type) and is_short_int_rprimitive(rhs.type)) or (
- is_eq and (is_short_int_rprimitive(lhs.type) or is_short_int_rprimitive(rhs.type))
- ):
- # We can skip the tag check
- check = self.comparison_op(lhs, rhs, int_comparison_op_mapping[op][0], line)
- self.flush_keep_alives()
- self.add(Branch(check, true, false, Branch.BOOL))
- return
- op_type, c_func_desc, negate_result, swap_op = int_comparison_op_mapping[op]
- int_block, short_int_block = BasicBlock(), BasicBlock()
- check_lhs = self.check_tagged_short_int(lhs, line, negated=True)
- if is_eq or is_short_int_rprimitive(rhs.type):
- self.flush_keep_alives()
- self.add(Branch(check_lhs, int_block, short_int_block, Branch.BOOL))
- else:
- # For non-equality logical ops (less/greater than, etc.), need to check both sides
- rhs_block = BasicBlock()
- self.add(Branch(check_lhs, int_block, rhs_block, Branch.BOOL))
- self.activate_block(rhs_block)
- check_rhs = self.check_tagged_short_int(rhs, line, negated=True)
- self.flush_keep_alives()
- self.add(Branch(check_rhs, int_block, short_int_block, Branch.BOOL))
- # Arbitrary integers (slow path)
- self.activate_block(int_block)
- if swap_op:
- args = [rhs, lhs]
- else:
- args = [lhs, rhs]
- call = self.call_c(c_func_desc, args, line)
- if negate_result:
- self.add(Branch(call, false, true, Branch.BOOL))
- else:
- self.flush_keep_alives()
- self.add(Branch(call, true, false, Branch.BOOL))
- # Short integers (fast path)
- self.activate_block(short_int_block)
- eq = self.comparison_op(lhs, rhs, op_type, line)
- self.add(Branch(eq, true, false, Branch.BOOL))
- def compare_strings(self, lhs: Value, rhs: Value, op: str, line: int) -> Value:
- """Compare two strings"""
- compare_result = self.call_c(unicode_compare, [lhs, rhs], line)
- error_constant = Integer(-1, c_int_rprimitive, line)
- compare_error_check = self.add(
- ComparisonOp(compare_result, error_constant, ComparisonOp.EQ, line)
- )
- exception_check, propagate, final_compare = BasicBlock(), BasicBlock(), BasicBlock()
- branch = Branch(compare_error_check, exception_check, final_compare, Branch.BOOL)
- branch.negated = False
- self.add(branch)
- self.activate_block(exception_check)
- check_error_result = self.call_c(err_occurred_op, [], line)
- null = Integer(0, pointer_rprimitive, line)
- compare_error_check = self.add(
- ComparisonOp(check_error_result, null, ComparisonOp.NEQ, line)
- )
- branch = Branch(compare_error_check, propagate, final_compare, Branch.BOOL)
- branch.negated = False
- self.add(branch)
- self.activate_block(propagate)
- self.call_c(keep_propagating_op, [], line)
- self.goto(final_compare)
- self.activate_block(final_compare)
- op_type = ComparisonOp.EQ if op == "==" else ComparisonOp.NEQ
- return self.add(ComparisonOp(compare_result, Integer(0, c_int_rprimitive), op_type, line))
- def compare_bytes(self, lhs: Value, rhs: Value, op: str, line: int) -> Value:
- compare_result = self.call_c(bytes_compare, [lhs, rhs], line)
- op_type = ComparisonOp.EQ if op == "==" else ComparisonOp.NEQ
- return self.add(ComparisonOp(compare_result, Integer(1, c_int_rprimitive), op_type, line))
- def compare_tuples(self, lhs: Value, rhs: Value, op: str, line: int = -1) -> Value:
- """Compare two tuples item by item"""
- # type cast to pass mypy check
- assert isinstance(lhs.type, RTuple) and isinstance(rhs.type, RTuple)
- equal = True if op == "==" else False
- result = Register(bool_rprimitive)
- # empty tuples
- if len(lhs.type.types) == 0 and len(rhs.type.types) == 0:
- self.add(Assign(result, self.true() if equal else self.false(), line))
- return result
- length = len(lhs.type.types)
- false_assign, true_assign, out = BasicBlock(), BasicBlock(), BasicBlock()
- check_blocks = [BasicBlock() for _ in range(length)]
- lhs_items = [self.add(TupleGet(lhs, i, line)) for i in range(length)]
- rhs_items = [self.add(TupleGet(rhs, i, line)) for i in range(length)]
- if equal:
- early_stop, final = false_assign, true_assign
- else:
- early_stop, final = true_assign, false_assign
- for i in range(len(lhs.type.types)):
- if i != 0:
- self.activate_block(check_blocks[i])
- lhs_item = lhs_items[i]
- rhs_item = rhs_items[i]
- compare = self.binary_op(lhs_item, rhs_item, op, line)
- # Cast to bool if necessary since most types uses comparison returning a object type
- # See generic_ops.py for more information
- if not is_bool_rprimitive(compare.type):
- compare = self.call_c(bool_op, [compare], line)
- if i < len(lhs.type.types) - 1:
- branch = Branch(compare, early_stop, check_blocks[i + 1], Branch.BOOL)
- else:
- branch = Branch(compare, early_stop, final, Branch.BOOL)
- # if op is ==, we branch on false, else branch on true
- branch.negated = equal
- self.add(branch)
- self.activate_block(false_assign)
- self.add(Assign(result, self.false(), line))
- self.goto(out)
- self.activate_block(true_assign)
- self.add(Assign(result, self.true(), line))
- self.goto_and_activate(out)
- return result
- def translate_instance_contains(self, inst: Value, item: Value, op: str, line: int) -> Value:
- res = self.gen_method_call(inst, "__contains__", [item], None, line)
- if not is_bool_rprimitive(res.type):
- res = self.call_c(bool_op, [res], line)
- if op == "not in":
- res = self.bool_bitwise_op(res, Integer(1, rtype=bool_rprimitive), "^", line)
- return res
- def bool_bitwise_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value:
- if op == "&":
- code = IntOp.AND
- elif op == "|":
- code = IntOp.OR
- elif op == "^":
- code = IntOp.XOR
- else:
- assert False, op
- return self.add(IntOp(bool_rprimitive, lreg, rreg, code, line))
- def bool_comparison_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value:
- op_id = ComparisonOp.signed_ops[op]
- return self.comparison_op(lreg, rreg, op_id, line)
- def unary_not(self, value: Value, line: int) -> Value:
- mask = Integer(1, value.type, line)
- return self.int_op(value.type, value, mask, IntOp.XOR, line)
- def unary_op(self, value: Value, expr_op: str, line: int) -> Value:
- typ = value.type
- if is_bool_rprimitive(typ) or is_bit_rprimitive(typ):
- if expr_op == "not":
- return self.unary_not(value, line)
- if expr_op == "+":
- return value
- if is_fixed_width_rtype(typ):
- if expr_op == "-":
- # Translate to '0 - x'
- return self.int_op(typ, Integer(0, typ), value, IntOp.SUB, line)
- elif expr_op == "~":
- if typ.is_signed:
- # Translate to 'x ^ -1'
- return self.int_op(typ, value, Integer(-1, typ), IntOp.XOR, line)
- else:
- # Translate to 'x ^ 0xff...'
- mask = (1 << (typ.size * 8)) - 1
- return self.int_op(typ, value, Integer(mask, typ), IntOp.XOR, line)
- elif expr_op == "+":
- return value
- if is_float_rprimitive(typ):
- if expr_op == "-":
- return self.add(FloatNeg(value, line))
- elif expr_op == "+":
- return value
- if isinstance(value, Integer):
- # TODO: Overflow? Unsigned?
- num = value.value
- if is_short_int_rprimitive(typ):
- num >>= 1
- return Integer(-num, typ, value.line)
- if is_tagged(typ) and expr_op == "+":
- return value
- if isinstance(value, Float):
- return Float(-value.value, value.line)
- if isinstance(typ, RInstance):
- if expr_op == "-":
- method = "__neg__"
- elif expr_op == "+":
- method = "__pos__"
- elif expr_op == "~":
- method = "__invert__"
- else:
- method = ""
- if method and typ.class_ir.has_method(method):
- return self.gen_method_call(value, method, [], None, line)
- call_c_ops_candidates = unary_ops.get(expr_op, [])
- target = self.matching_call_c(call_c_ops_candidates, [value], line)
- assert target, "Unsupported unary operation: %s" % expr_op
- return target
- def make_dict(self, key_value_pairs: Sequence[DictEntry], line: int) -> Value:
- result: Value | None = None
- keys: list[Value] = []
- values: list[Value] = []
- for key, value in key_value_pairs:
- if key is not None:
- # key:value
- if result is None:
- keys.append(key)
- values.append(value)
- continue
- self.translate_special_method_call(
- result, "__setitem__", [key, value], result_type=None, line=line
- )
- else:
- # **value
- if result is None:
- result = self._create_dict(keys, values, line)
- self.call_c(dict_update_in_display_op, [result, value], line=line)
- if result is None:
- result = self._create_dict(keys, values, line)
- return result
- def new_list_op_with_length(self, length: Value, line: int) -> Value:
- """This function returns an uninitialized list.
- If the length is non-zero, the caller must initialize the list, before
- it can be made visible to user code -- otherwise the list object is broken.
- You might need further initialization with `new_list_set_item_op` op.
- Args:
- length: desired length of the new list. The rtype should be
- c_pyssize_t_rprimitive
- line: line number
- """
- return self.call_c(new_list_op, [length], line)
- def new_list_op(self, values: list[Value], line: int) -> Value:
- length: list[Value] = [Integer(len(values), c_pyssize_t_rprimitive, line)]
- if len(values) >= LIST_BUILDING_EXPANSION_THRESHOLD:
- return self.call_c(list_build_op, length + values, line)
- # If the length of the list is less than the threshold,
- # LIST_BUILDING_EXPANSION_THRESHOLD, we directly expand the
- # for-loop and inline the SetMem operation, which is faster
- # than list_build_op, however generates more code.
- result_list = self.call_c(new_list_op, length, line)
- if not values:
- return result_list
- args = [self.coerce(item, object_rprimitive, line) for item in values]
- ob_item_ptr = self.add(GetElementPtr(result_list, PyListObject, "ob_item", line))
- ob_item_base = self.add(LoadMem(pointer_rprimitive, ob_item_ptr, line))
- for i in range(len(values)):
- if i == 0:
- item_address = ob_item_base
- else:
- offset = Integer(PLATFORM_SIZE * i, c_pyssize_t_rprimitive, line)
- item_address = self.add(
- IntOp(pointer_rprimitive, ob_item_base, offset, IntOp.ADD, line)
- )
- self.add(SetMem(object_rprimitive, item_address, args[i], line))
- self.add(KeepAlive([result_list]))
- return result_list
- def new_set_op(self, values: list[Value], line: int) -> Value:
- return self.call_c(new_set_op, values, line)
- def setup_rarray(
- self, item_type: RType, values: Sequence[Value], *, object_ptr: bool = False
- ) -> Value:
- """Declare and initialize a new RArray, returning its address."""
- array = Register(RArray(item_type, len(values)))
- self.add(AssignMulti(array, list(values)))
- return self.add(
- LoadAddress(object_pointer_rprimitive if object_ptr else c_pointer_rprimitive, array)
- )
- def shortcircuit_helper(
- self,
- op: str,
- expr_type: RType,
- left: Callable[[], Value],
- right: Callable[[], Value],
- line: int,
- ) -> Value:
- # Having actual Phi nodes would be really nice here!
- target = Register(expr_type)
- # left_body takes the value of the left side, right_body the right
- left_body, right_body, next_block = BasicBlock(), BasicBlock(), BasicBlock()
- # true_body is taken if the left is true, false_body if it is false.
- # For 'and' the value is the right side if the left is true, and for 'or'
- # it is the right side if the left is false.
- true_body, false_body = (right_body, left_body) if op == "and" else (left_body, right_body)
- left_value = left()
- self.add_bool_branch(left_value, true_body, false_body)
- self.activate_block(left_body)
- left_coerced = self.coerce(left_value, expr_type, line)
- self.add(Assign(target, left_coerced))
- self.goto(next_block)
- self.activate_block(right_body)
- right_value = right()
- right_coerced = self.coerce(right_value, expr_type, line)
- self.add(Assign(target, right_coerced))
- self.goto(next_block)
- self.activate_block(next_block)
- return target
- def bool_value(self, value: Value) -> Value:
- """Return bool(value).
- The result type can be bit_rprimitive or bool_rprimitive.
- """
- if is_bool_rprimitive(value.type) or is_bit_rprimitive(value.type):
- result = value
- elif is_runtime_subtype(value.type, int_rprimitive):
- zero = Integer(0, short_int_rprimitive)
- result = self.comparison_op(value, zero, ComparisonOp.NEQ, value.line)
- elif is_fixed_width_rtype(value.type):
- zero = Integer(0, value.type)
- result = self.add(ComparisonOp(value, zero, ComparisonOp.NEQ))
- elif is_same_type(value.type, str_rprimitive):
- result = self.call_c(str_check_if_true, [value], value.line)
- elif is_same_type(value.type, list_rprimitive) or is_same_type(
- value.type, dict_rprimitive
- ):
- length = self.builtin_len(value, value.line)
- zero = Integer(0)
- result = self.binary_op(length, zero, "!=", value.line)
- elif (
- isinstance(value.type, RInstance)
- and value.type.class_ir.is_ext_class
- and value.type.class_ir.has_method("__bool__")
- ):
- # Directly call the __bool__ method on classes that have it.
- result = self.gen_method_call(value, "__bool__", [], bool_rprimitive, value.line)
- elif is_float_rprimitive(value.type):
- result = self.compare_floats(value, Float(0.0), FloatComparisonOp.NEQ, value.line)
- else:
- value_type = optional_value_type(value.type)
- if value_type is not None:
- not_none = self.translate_is_op(value, self.none_object(), "is not", value.line)
- always_truthy = False
- if isinstance(value_type, RInstance):
- # check whether X.__bool__ is always just the default (object.__bool__)
- if not value_type.class_ir.has_method(
- "__bool__"
- ) and value_type.class_ir.is_method_final("__bool__"):
- always_truthy = True
- if always_truthy:
- result = not_none
- else:
- # "X | None" where X may be falsey and requires a check
- result = Register(bit_rprimitive)
- true, false, end = BasicBlock(), BasicBlock(), BasicBlock()
- branch = Branch(not_none, true, false, Branch.BOOL)
- self.add(branch)
- self.activate_block(true)
- # unbox_or_cast instead of coerce because we want the
- # type to change even if it is a subtype.
- remaining = self.unbox_or_cast(value, value_type, value.line)
- as_bool = self.bool_value(remaining)
- self.add(Assign(result, as_bool))
- self.goto(end)
- self.activate_block(false)
- self.add(Assign(result, Integer(0, bit_rprimitive)))
- self.goto(end)
- self.activate_block(end)
- else:
- result = self.call_c(bool_op, [value], value.line)
- return result
- def add_bool_branch(self, value: Value, true: BasicBlock, false: BasicBlock) -> None:
- opt_value_type = optional_value_type(value.type)
- if opt_value_type is None:
- bool_value = self.bool_value(value)
- self.add(Branch(bool_value, true, false, Branch.BOOL))
- else:
- # Special-case optional types
- is_none = self.translate_is_op(value, self.none_object(), "is not", value.line)
- branch = Branch(is_none, true, false, Branch.BOOL)
- self.add(branch)
- always_truthy = False
- if isinstance(opt_value_type, RInstance):
- # check whether X.__bool__ is always just the default (object.__bool__)
- if not opt_value_type.class_ir.has_method(
- "__bool__"
- ) and opt_value_type.class_ir.is_method_final("__bool__"):
- always_truthy = True
- if not always_truthy:
- # Optional[X] where X may be falsey and requires a check
- branch.true = BasicBlock()
- self.activate_block(branch.true)
- # unbox_or_cast instead of coerce because we want the
- # type to change even if it is a subtype.
- remaining = self.unbox_or_cast(value, opt_value_type, value.line)
- self.add_bool_branch(remaining, true, false)
- def call_c(
- self,
- desc: CFunctionDescription,
- args: list[Value],
- line: int,
- result_type: RType | None = None,
- ) -> Value:
- """Call function using C/native calling convention (not a Python callable)."""
- # Handle void function via singleton RVoid instance
- coerced = []
- # Coerce fixed number arguments
- for i in range(min(len(args), len(desc.arg_types))):
- formal_type = desc.arg_types[i]
- arg = args[i]
- arg = self.coerce(arg, formal_type, line)
- coerced.append(arg)
- # Reorder args if necessary
- if desc.ordering is not None:
- assert desc.var_arg_type is None
- coerced = [coerced[i] for i in desc.ordering]
- # Coerce any var_arg
- var_arg_idx = -1
- if desc.var_arg_type is not None:
- var_arg_idx = len(desc.arg_types)
- for i in range(len(desc.arg_types), len(args)):
- arg = args[i]
- arg = self.coerce(arg, desc.var_arg_type, line)
- coerced.append(arg)
- # Add extra integer constant if any
- for item in desc.extra_int_constants:
- val, typ = item
- extra_int_constant = Integer(val, typ, line)
- coerced.append(extra_int_constant)
- error_kind = desc.error_kind
- if error_kind == ERR_NEG_INT:
- # Handled with an explicit comparison
- error_kind = ERR_NEVER
- target = self.add(
- CallC(
- desc.c_function_name,
- coerced,
- desc.return_type,
- desc.steals,
- desc.is_borrowed,
- error_kind,
- line,
- var_arg_idx,
- )
- )
- if desc.is_borrowed:
- # If the result is borrowed, force the arguments to be
- # kept alive afterwards, as otherwise the result might be
- # immediately freed, at the risk of a dangling pointer.
- for arg in coerced:
- if not isinstance(arg, (Integer, LoadLiteral)):
- self.keep_alives.append(arg)
- if desc.error_kind == ERR_NEG_INT:
- comp = ComparisonOp(target, Integer(0, desc.return_type, line), ComparisonOp.SGE, line)
- comp.error_kind = ERR_FALSE
- self.add(comp)
- if desc.truncated_type is None:
- result = target
- else:
- truncate = self.add(Truncate(target, desc.truncated_type))
- result = truncate
- if result_type and not is_runtime_subtype(result.type, result_type):
- if is_none_rprimitive(result_type):
- # Special case None return. The actual result may actually be a bool
- # and so we can't just coerce it.
- result = self.none()
- else:
- result = self.coerce(target, result_type, line, can_borrow=desc.is_borrowed)
- return result
- def matching_call_c(
- self,
- candidates: list[CFunctionDescription],
- args: list[Value],
- line: int,
- result_type: RType | None = None,
- can_borrow: bool = False,
- ) -> Value | None:
- matching: CFunctionDescription | None = None
- for desc in candidates:
- if len(desc.arg_types) != len(args):
- continue
- if all(
- is_subtype(actual.type, formal) for actual, formal in zip(args, desc.arg_types)
- ) and (not desc.is_borrowed or can_borrow):
- if matching:
- assert matching.priority != desc.priority, "Ambiguous:\n1) {}\n2) {}".format(
- matching, desc
- )
- if desc.priority > matching.priority:
- matching = desc
- else:
- matching = desc
- if matching:
- target = self.call_c(matching, args, line, result_type)
- return target
- return None
- def int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int = -1) -> Value:
- """Generate a native integer binary op.
- Use native/C semantics, which sometimes differ from Python
- semantics.
- Args:
- type: Either int64_rprimitive or int32_rprimitive
- op: IntOp.* constant (e.g. IntOp.ADD)
- """
- return self.add(IntOp(type, lhs, rhs, op, line))
- def float_op(self, lhs: Value, rhs: Value, op: str, line: int) -> Value:
- """Generate a native float binary arithmetic operation.
- This follows Python semantics (e.g. raise exception on division by zero).
- Add a FloatOp directly if you want low-level semantics.
- Args:
- op: Binary operator (e.g. '+' or '*')
- """
- op_id = float_op_to_id[op]
- if op_id in (FloatOp.DIV, FloatOp.MOD):
- if not (isinstance(rhs, Float) and rhs.value != 0.0):
- c = self.compare_floats(rhs, Float(0.0), FloatComparisonOp.EQ, line)
- err, ok = BasicBlock(), BasicBlock()
- self.add(Branch(c, err, ok, Branch.BOOL, rare=True))
- self.activate_block(err)
- if op_id == FloatOp.DIV:
- msg = "float division by zero"
- else:
- msg = "float modulo"
- self.add(RaiseStandardError(RaiseStandardError.ZERO_DIVISION_ERROR, msg, line))
- self.add(Unreachable())
- self.activate_block(ok)
- if op_id == FloatOp.MOD:
- # Adjust the result to match Python semantics (FloatOp follows C semantics).
- return self.float_mod(lhs, rhs, line)
- else:
- return self.add(FloatOp(lhs, rhs, op_id, line))
- def float_mod(self, lhs: Value, rhs: Value, line: int) -> Value:
- """Perform x % y on floats using Python semantics."""
- mod = self.add(FloatOp(lhs, rhs, FloatOp.MOD, line))
- res = Register(float_rprimitive)
- self.add(Assign(res, mod))
- tricky, adjust, copysign, done = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()
- is_zero = self.add(FloatComparisonOp(res, Float(0.0), FloatComparisonOp.EQ, line))
- self.add(Branch(is_zero, copysign, tricky, Branch.BOOL))
- self.activate_block(tricky)
- same_signs = self.is_same_float_signs(lhs, rhs, line)
- self.add(Branch(same_signs, done, adjust, Branch.BOOL))
- self.activate_block(adjust)
- adj = self.float_op(res, rhs, "+", line)
- self.add(Assign(res, adj))
- self.add(Goto(done))
- self.activate_block(copysign)
- # If the remainder is zero, CPython ensures the result has the
- # same sign as the denominator.
- adj = self.call_c(copysign_op, [Float(0.0), rhs], line)
- self.add(Assign(res, adj))
- self.add(Goto(done))
- self.activate_block(done)
- return res
- def compare_floats(self, lhs: Value, rhs: Value, op: int, line: int) -> Value:
- return self.add(FloatComparisonOp(lhs, rhs, op, line))
- def fixed_width_int_op(
- self, type: RPrimitive, lhs: Value, rhs: Value, op: int, line: int
- ) -> Value:
- """Generate a binary op using Python fixed-width integer semantics.
- These may differ in overflow/rounding behavior from native/C ops.
- Args:
- type: Either int64_rprimitive or int32_rprimitive
- op: IntOp.* constant (e.g. IntOp.ADD)
- """
- lhs = self.coerce(lhs, type, line)
- rhs = self.coerce(rhs, type, line)
- if op == IntOp.DIV:
- if isinstance(rhs, Integer) and rhs.value not in (-1, 0):
- if not type.is_signed:
- return self.int_op(type, lhs, rhs, IntOp.DIV, line)
- else:
- # Inline simple division by a constant, so that C
- # compilers can optimize more
- return self.inline_fixed_width_divide(type, lhs, rhs, line)
- if is_int64_rprimitive(type):
- prim = int64_divide_op
- elif is_int32_rprimitive(type):
- prim = int32_divide_op
- elif is_int16_rprimitive(type):
- prim = int16_divide_op
- elif is_uint8_rprimitive(type):
- self.check_for_zero_division(rhs, type, line)
- return self.int_op(type, lhs, rhs, op, line)
- else:
- assert False, type
- return self.call_c(prim, [lhs, rhs], line)
- if op == IntOp.MOD:
- if isinstance(rhs, Integer) and rhs.value not in (-1, 0):
- if not type.is_signed:
- return self.int_op(type, lhs, rhs, IntOp.MOD, line)
- else:
- # Inline simple % by a constant, so that C
- # compilers can optimize more
- return self.inline_fixed_width_mod(type, lhs, rhs, line)
- if is_int64_rprimitive(type):
- prim = int64_mod_op
- elif is_int32_rprimitive(type):
- prim = int32_mod_op
- elif is_int16_rprimitive(type):
- prim = int16_mod_op
- elif is_uint8_rprimitive(type):
- self.check_for_zero_division(rhs, type, line)
- return self.int_op(type, lhs, rhs, op, line)
- else:
- assert False, type
- return self.call_c(prim, [lhs, rhs], line)
- return self.int_op(type, lhs, rhs, op, line)
- def check_for_zero_division(self, rhs: Value, type: RType, line: int) -> None:
- err, ok = BasicBlock(), BasicBlock()
- is_zero = self.binary_op(rhs, Integer(0, type), "==", line)
- self.add(Branch(is_zero, err, ok, Branch.BOOL))
- self.activate_block(err)
- self.add(
- RaiseStandardError(
- RaiseStandardError.ZERO_DIVISION_ERROR, "integer division or modulo by zero", line
- )
- )
- self.add(Unreachable())
- self.activate_block(ok)
- def inline_fixed_width_divide(self, type: RType, lhs: Value, rhs: Value, line: int) -> Value:
- # Perform floor division (native division truncates)
- res = Register(type)
- div = self.int_op(type, lhs, rhs, IntOp.DIV, line)
- self.add(Assign(res, div))
- same_signs = self.is_same_native_int_signs(type, lhs, rhs, line)
- tricky, adjust, done = BasicBlock(), BasicBlock(), BasicBlock()
- self.add(Branch(same_signs, done, tricky, Branch.BOOL))
- self.activate_block(tricky)
- mul = self.int_op(type, res, rhs, IntOp.MUL, line)
- mul_eq = self.add(ComparisonOp(mul, lhs, ComparisonOp.EQ, line))
- self.add(Branch(mul_eq, done, adjust, Branch.BOOL))
- self.activate_block(adjust)
- adj = self.int_op(type, res, Integer(1, type), IntOp.SUB, line)
- self.add(Assign(res, adj))
- self.add(Goto(done))
- self.activate_block(done)
- return res
- def inline_fixed_width_mod(self, type: RType, lhs: Value, rhs: Value, line: int) -> Value:
- # Perform floor modulus
- res = Register(type)
- mod = self.int_op(type, lhs, rhs, IntOp.MOD, line)
- self.add(Assign(res, mod))
- same_signs = self.is_same_native_int_signs(type, lhs, rhs, line)
- tricky, adjust, done = BasicBlock(), BasicBlock(), BasicBlock()
- self.add(Branch(same_signs, done, tricky, Branch.BOOL))
- self.activate_block(tricky)
- is_zero = self.add(ComparisonOp(res, Integer(0, type), ComparisonOp.EQ, line))
- self.add(Branch(is_zero, done, adjust, Branch.BOOL))
- self.activate_block(adjust)
- adj = self.int_op(type, res, rhs, IntOp.ADD, line)
- self.add(Assign(res, adj))
- self.add(Goto(done))
- self.activate_block(done)
- return res
- def is_same_native_int_signs(self, type: RType, a: Value, b: Value, line: int) -> Value:
- neg1 = self.add(ComparisonOp(a, Integer(0, type), ComparisonOp.SLT, line))
- neg2 = self.add(ComparisonOp(b, Integer(0, type), ComparisonOp.SLT, line))
- return self.add(ComparisonOp(neg1, neg2, ComparisonOp.EQ, line))
- def is_same_float_signs(self, a: Value, b: Value, line: int) -> Value:
- neg1 = self.add(FloatComparisonOp(a, Float(0.0), FloatComparisonOp.LT, line))
- neg2 = self.add(FloatComparisonOp(b, Float(0.0), FloatComparisonOp.LT, line))
- return self.add(ComparisonOp(neg1, neg2, ComparisonOp.EQ, line))
- def comparison_op(self, lhs: Value, rhs: Value, op: int, line: int) -> Value:
- return self.add(ComparisonOp(lhs, rhs, op, line))
- def builtin_len(self, val: Value, line: int, use_pyssize_t: bool = False) -> Value:
- """Generate len(val).
- Return short_int_rprimitive by default.
- Return c_pyssize_t if use_pyssize_t is true (unshifted).
- """
- typ = val.type
- size_value = None
- if is_list_rprimitive(typ) or is_tuple_rprimitive(typ) or is_bytes_rprimitive(typ):
- elem_address = self.add(GetElementPtr(val, PyVarObject, "ob_size"))
- size_value = self.add(LoadMem(c_pyssize_t_rprimitive, elem_address))
- self.add(KeepAlive([val]))
- elif is_set_rprimitive(typ):
- elem_address = self.add(GetElementPtr(val, PySetObject, "used"))
- size_value = self.add(LoadMem(c_pyssize_t_rprimitive, elem_address))
- self.add(KeepAlive([val]))
- elif is_dict_rprimitive(typ):
- size_value = self.call_c(dict_ssize_t_size_op, [val], line)
- elif is_str_rprimitive(typ):
- size_value = self.call_c(str_ssize_t_size_op, [val], line)
- if size_value is not None:
- if use_pyssize_t:
- return size_value
- offset = Integer(1, c_pyssize_t_rprimitive, line)
- return self.int_op(short_int_rprimitive, size_value, offset, IntOp.LEFT_SHIFT, line)
- if isinstance(typ, RInstance):
- # TODO: Support use_pyssize_t
- assert not use_pyssize_t
- length = self.gen_method_call(val, "__len__", [], int_rprimitive, line)
- length = self.coerce(length, int_rprimitive, line)
- ok, fail = BasicBlock(), BasicBlock()
- self.compare_tagged_condition(length, Integer(0), ">=", ok, fail, line)
- self.activate_block(fail)
- self.add(
- RaiseStandardError(
- RaiseStandardError.VALUE_ERROR, "__len__() should return >= 0", line
- )
- )
- self.add(Unreachable())
- self.activate_block(ok)
- return length
- # generic case
- if use_pyssize_t:
- return self.call_c(generic_ssize_t_len_op, [val], line)
- else:
- return self.call_c(generic_len_op, [val], line)
- def new_tuple(self, items: list[Value], line: int) -> Value:
- size: Value = Integer(len(items), c_pyssize_t_rprimitive)
- return self.call_c(new_tuple_op, [size] + items, line)
- def new_tuple_with_length(self, length: Value, line: int) -> Value:
- """This function returns an uninitialized tuple.
- If the length is non-zero, the caller must initialize the tuple, before
- it can be made visible to user code -- otherwise the tuple object is broken.
- You might need further initialization with `new_tuple_set_item_op` op.
- Args:
- length: desired length of the new tuple. The rtype should be
- c_pyssize_t_rprimitive
- line: line number
- """
- return self.call_c(new_tuple_with_length_op, [length], line)
- def int_to_float(self, n: Value, line: int) -> Value:
- return self.call_c(int_to_float_op, [n], line)
- # Internal helpers
- def decompose_union_helper(
- self,
- obj: Value,
- rtype: RUnion,
- result_type: RType,
- process_item: Callable[[Value], Value],
- line: int,
- ) -> Value:
- """Generate isinstance() + specialized operations for union items.
- Say, for Union[A, B] generate ops resembling this (pseudocode):
- if isinstance(obj, A):
- result = <result of process_item(cast(A, obj)>
- else:
- result = <result of process_item(cast(B, obj)>
- Args:
- obj: value with a union type
- rtype: the union type
- result_type: result of the operation
- process_item: callback to generate op for a single union item (arg is coerced
- to union item type)
- line: line number
- """
- # TODO: Optimize cases where a single operation can handle multiple union items
- # (say a method is implemented in a common base class)
- fast_items = []
- rest_items = []
- for item in rtype.items:
- if isinstance(item, RInstance):
- fast_items.append(item)
- else:
- # For everything but RInstance we fall back to C API
- rest_items.append(item)
- exit_block = BasicBlock()
- result = Register(result_type)
- for i, item in enumerate(fast_items):
- more_types = i < len(fast_items) - 1 or rest_items
- if more_types:
- # We are not at the final item so we need one more branch
- op = self.isinstance_native(obj, item.class_ir, line)
- true_block, false_block = BasicBlock(), BasicBlock()
- self.add_bool_branch(op, true_block, false_block)
- self.activate_block(true_block)
- coerced = self.coerce(obj, item, line)
- temp = process_item(coerced)
- temp2 = self.coerce(temp, result_type, line)
- self.add(Assign(result, temp2))
- self.goto(exit_block)
- if more_types:
- self.activate_block(false_block)
- if rest_items:
- # For everything else we use generic operation. Use force=True to drop the
- # union type.
- coerced = self.coerce(obj, object_rprimitive, line, force=True)
- temp = process_item(coerced)
- temp2 = self.coerce(temp, result_type, line)
- self.add(Assign(result, temp2))
- self.goto(exit_block)
- self.activate_block(exit_block)
- return result
- def translate_special_method_call(
- self,
- base_reg: Value,
- name: str,
- args: list[Value],
- result_type: RType | None,
- line: int,
- can_borrow: bool = False,
- ) -> Value | None:
- """Translate a method call which is handled nongenerically.
- These are special in the sense that we have code generated specifically for them.
- They tend to be method calls which have equivalents in C that are more direct
- than calling with the PyObject api.
- Return None if no translation found; otherwise return the target register.
- """
- call_c_ops_candidates = method_call_ops.get(name, [])
- call_c_op = self.matching_call_c(
- call_c_ops_candidates, [base_reg] + args, line, result_type, can_borrow=can_borrow
- )
- return call_c_op
- def translate_eq_cmp(self, lreg: Value, rreg: Value, expr_op: str, line: int) -> Value | None:
- """Add a equality comparison operation.
- Args:
- expr_op: either '==' or '!='
- """
- ltype = lreg.type
- rtype = rreg.type
- if not (isinstance(ltype, RInstance) and ltype == rtype):
- return None
- class_ir = ltype.class_ir
- # Check whether any subclasses of the operand redefines __eq__
- # or it might be redefined in a Python parent class or by
- # dataclasses
- cmp_varies_at_runtime = (
- not class_ir.is_method_final("__eq__")
- or not class_ir.is_method_final("__ne__")
- or class_ir.inherits_python
- or class_ir.is_augmented
- )
- if cmp_varies_at_runtime:
- # We might need to call left.__eq__(right) or right.__eq__(left)
- # depending on which is the more specific type.
- return None
- if not class_ir.has_method("__eq__"):
- # There's no __eq__ defined, so just use object identity.
- identity_ref_op = "is" if expr_op == "==" else "is not"
- return self.translate_is_op(lreg, rreg, identity_ref_op, line)
- return self.gen_method_call(lreg, op_methods[expr_op], [rreg], ltype, line)
- def translate_is_op(self, lreg: Value, rreg: Value, expr_op: str, line: int) -> Value:
- """Create equality comparison operation between object identities
- Args:
- expr_op: either 'is' or 'is not'
- """
- op = ComparisonOp.EQ if expr_op == "is" else ComparisonOp.NEQ
- lhs = self.coerce(lreg, object_rprimitive, line)
- rhs = self.coerce(rreg, object_rprimitive, line)
- return self.add(ComparisonOp(lhs, rhs, op, line))
- def _create_dict(self, keys: list[Value], values: list[Value], line: int) -> Value:
- """Create a dictionary(possibly empty) using keys and values"""
- # keys and values should have the same number of items
- size = len(keys)
- if size > 0:
- size_value: Value = Integer(size, c_pyssize_t_rprimitive)
- # merge keys and values
- items = [i for t in list(zip(keys, values)) for i in t]
- return self.call_c(dict_build_op, [size_value] + items, line)
- else:
- return self.call_c(dict_new_op, [], line)
- def error(self, msg: str, line: int) -> None:
- self.errors.error(msg, self.module_path, line)
- def num_positional_args(arg_values: list[Value], arg_kinds: list[ArgKind] | None) -> int:
- if arg_kinds is None:
- return len(arg_values)
- num_pos = 0
- for kind in arg_kinds:
- if kind == ARG_POS:
- num_pos += 1
- return num_pos
|