Skip to content

Commit

Permalink
[inferpython] coupling LOAD_METHOD and CALL_METHOD bytecodes
Browse files Browse the repository at this point in the history
Summary:
the bytecode spec asserts that a CALL_METHOD instruction
should receive the result of a LOAD_METHOD. We trust this invariant and
provide a more explicit CallMethod instruction using this assumption.

Reviewed By: ngorogiannis

Differential Revision:
D63085765

Privacy Context Container: L1208441

fbshipit-source-id: 9f8a746b11aeda2c0ad08ff20ca303e30b75e527
  • Loading branch information
davidpichardie authored and facebook-github-bot committed Sep 26, 2024
1 parent 82508f1 commit 898d0f0
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 44 deletions.
18 changes: 14 additions & 4 deletions infer/src/python/PyIR.ml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ module Error = struct
| MakeFunction of string * Exp.t
| BuildConstKeyMapLength of int * int
| BuildConstKeyMapKeys of Exp.t
| LoadMethodExpected of Exp.t
| CompareOp of int
| CodeWithoutQualifiedName of FFI.Code.t
| UnpackSequence of int
Expand Down Expand Up @@ -396,6 +397,8 @@ module Error = struct
F.fprintf fmt "UNPACK_SEQUENCE: invalid count %d" n
| FormatValueSpec exp ->
F.fprintf fmt "FORMAT_VALUE: expected string literal or temporary, got %a" Exp.pp exp
| LoadMethodExpected exp ->
F.fprintf fmt "LOAD_METHOD_EXPECTED: expected a LOAD_METHOD result but got %a" Exp.pp exp
| NextOffsetMissing ->
F.fprintf fmt "Jump to next instruction detected, but next instruction is missing"
| MissingBackEdge (from, to_) ->
Expand Down Expand Up @@ -440,7 +443,7 @@ module Stmt = struct
| Store of {lhs: ScopedIdent.t; rhs: Exp.t}
| StoreSubscript of {lhs: Exp.t; index: Exp.t; rhs: Exp.t}
| Call of {lhs: SSA.t; exp: Exp.t; args: call_arg list; packed: bool}
| CallMethod of {lhs: SSA.t; call: Exp.t; args: Exp.t list}
| CallMethod of {lhs: SSA.t; name: string; self_if_needed: Exp.t; args: Exp.t list}
| BuiltinCall of {lhs: SSA.t; call: BuiltinCaller.t; args: Exp.t list}
| SetupAnnotations

Expand All @@ -456,8 +459,8 @@ module Stmt = struct
| Call {lhs; exp; args; packed} ->
F.fprintf fmt "%a <- %a(@[%a@])%s" SSA.pp lhs Exp.pp exp (Pp.seq ~sep:", " pp_call_arg) args
(if packed then " !packed" else "")
| CallMethod {lhs; call; args} ->
F.fprintf fmt "%a <- $CallMethod(%a, @[%a@])" SSA.pp lhs Exp.pp call
| CallMethod {lhs; name; self_if_needed; args} ->
F.fprintf fmt "%a <- %a.%s(@[%a@])" SSA.pp lhs Exp.pp self_if_needed name
(Pp.seq ~sep:", " Exp.pp) args
| BuiltinCall {lhs; call; args} ->
F.fprintf fmt "%a <- %s(@[%a@])" SSA.pp lhs (BuiltinCaller.show call)
Expand Down Expand Up @@ -1701,8 +1704,15 @@ let parse_bytecode st ({FFI.Code.co_consts; co_names; co_varnames} as code)
| "CALL_METHOD" ->
let* args, st = State.pop_n st arg in
let* call, st = State.pop st in
let* self_if_needed, name =
match call with
| Exp.LoadMethod (self_if_needed, name) ->
Ok (self_if_needed, name)
| _ ->
internal_error st (Error.LoadMethodExpected call)
in
let lhs, st = State.fresh_id st in
let stmt = Stmt.CallMethod {lhs; call; args} in
let stmt = Stmt.CallMethod {lhs; name; self_if_needed; args} in
let st = State.push_stmt st stmt in
let st = State.push st (Exp.Temp lhs) in
Ok (st, None)
Expand Down
138 changes: 138 additions & 0 deletions infer/src/python/unit/PyIRTest.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1415,3 +1415,141 @@ async def g():

b3:
return PYCNone |}]


let%expect_test _ =
let source = {|
def m(self, x, y, test):
return foo(self, x if test else y)
|} in
PyIR.test source ;
[%expect
{|
module dummy:

toplevel:
b0:
TOPLEVEL[m] <- $FuncObj(m, dummy.m, {})
return PYCNone


dummy.m:
b0:
n0 <- GLOBAL[foo]
n1 <- LOCAL[self]
n2 <- LOCAL[test]
if n2 then jmp b1(n1, n0) else jmp b2(n1, n0)

b1:
n7 <- LOCAL[x]
jmp b3(n7, n4, n3)

b2:
n11 <- LOCAL[y]
jmp b3(n11, n6, n5)

b3:
n12 <- n8(n9, n10)
return n12 |}]


let%expect_test _ =
let source = {|
def m(self, x, y, test):
return self.foo(x if test else y)
|} in
PyIR.test source ;
[%expect {| IR error: LOAD_METHOD_EXPECTED: expected a LOAD_METHOD result but got n5 |}]


let%expect_test _ =
let source = {|
def m(x, y, test):
return (x if test else y).foo()
|} in
PyIR.test source ;
[%expect
{|
module dummy:

toplevel:
b0:
TOPLEVEL[m] <- $FuncObj(m, dummy.m, {})
return PYCNone


dummy.m:
b0:
n0 <- LOCAL[test]
if n0 then jmp b1 else jmp b2

b1:
n1 <- LOCAL[x]
jmp b3(n1)

b2:
n3 <- LOCAL[y]
jmp b3(n3)

b3:
n4 <- n2.foo()
return n4 |}]


let%expect_test _ =
let source =
{|
class C:
def foo(self):
print('I am foo')

o = C()
o.foo()
o.foo = lambda : print('I am not foo')
o.foo()
#I am foo
#I am not foo
|}
in
PyIR.test source ;
[%expect
{|
module dummy:

toplevel:
b0:
n0 <- $BuildClass($FuncObj(C, dummy.C, {}), PYCString ("C"))
TOPLEVEL[C] <- n0
n1 <- TOPLEVEL[C]
n2 <- n1()
TOPLEVEL[o] <- n2
n3 <- TOPLEVEL[o]
n4 <- n3.foo()
n5 <- TOPLEVEL[o]
n5.foo <- $FuncObj(<lambda>, dummy.<lambda>, {})
n6 <- TOPLEVEL[o]
n7 <- n6.foo()
return PYCNone


dummy.<lambda>:
b0:
n0 <- GLOBAL[print]
n1 <- n0(PYCString ("I am not foo"))
return n1


dummy.C:
b0:
n0 <- TOPLEVEL[__name__]
TOPLEVEL[__module__] <- n0
TOPLEVEL[__qualname__] <- PYCString ("C")
TOPLEVEL[foo] <- $FuncObj(foo, dummy.C.foo, {})
return PYCNone


dummy.C.foo:
b0:
n0 <- GLOBAL[print]
n1 <- n0(PYCString ("I am foo"))
return PYCNone |}]
4 changes: 2 additions & 2 deletions infer/src/python/unit/PyIRTestBasic.ml
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ fp.write("yolo")
n1 <- n0(PYCString ("foo.txt"), PYCString ("wt"))
TOPLEVEL[fp] <- n1
n2 <- TOPLEVEL[fp]
n3 <- $CallMethod($LoadMethod(n2, write), PYCString ("yolo"))
n3 <- n2.write(PYCString ("yolo"))
return PYCNone |}]


Expand All @@ -334,7 +334,7 @@ with open("foo.txt", "wt") as fp:
n2 <- $LoadMethod(n1, __enter__)()
TOPLEVEL[fp] <- n2
n4 <- TOPLEVEL[fp]
n5 <- $CallMethod($LoadMethod(n4, write), PYCString ("yolo"))
n5 <- n4.write(PYCString ("yolo"))
jmp b1(CM(n1).__exit__)

b1:
Expand Down
6 changes: 3 additions & 3 deletions infer/src/python/unit/PyIRTestException.ml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ except (ValueError, AttributeError):
n0 <- $ImportName(os)(PYCNone, PYCInt (0))
TOPLEVEL[os] <- n0
n1 <- TOPLEVEL[os]
n2 <- $CallMethod($LoadMethod(n1, sysconf), PYCString ("SC_PAGESIZE"))
n2 <- n1.sysconf(PYCString ("SC_PAGESIZE"))
TOPLEVEL[page_size] <- n2
jmp b2

Expand Down Expand Up @@ -228,7 +228,7 @@ def f(x):
n5 <- $IterData(n3)
LOCAL[i] <- n5
n6 <- GLOBAL[foo]
n7 <- $CallMethod($LoadMethod(n6, Foo), )
n7 <- n6.Foo()
LOCAL[e] <- n7
n9 <- GLOBAL[print]
n10 <- n9(PYCString ("yolo"))
Expand All @@ -239,7 +239,7 @@ def f(x):

b4:
n12 <- LOCAL[e]
n13 <- $CallMethod($LoadMethod(n12, bar), )
n13 <- n12.bar()
jmp b1(n8) |}]


Expand Down
24 changes: 12 additions & 12 deletions infer/src/python/unit/PyIRTestImport.ml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ base.f(0)
n1 <- $ImportName(base)(PYCNone, PYCInt (0))
TOPLEVEL[base] <- n1
n2 <- TOPLEVEL[base]
n3 <- $CallMethod($LoadMethod(n2, f), PYCInt (0))
n3 <- n2.f(PYCInt (0))
return PYCNone |}]


Expand Down Expand Up @@ -192,9 +192,9 @@ if __name__ == '__main__':
n6 <- GLOBAL[sys]
n7 <- n6.argv
n8 <- n7[PYCInt (0)]
n9 <- $CallMethod($LoadMethod(n5, dirname), n8)
n10 <- $CallMethod($LoadMethod(n3, normpath), n9)
n11 <- $CallMethod($LoadMethod(n1, abspath), n10)
n9 <- n5.dirname(n8)
n10 <- n3.normpath(n9)
n11 <- n1.abspath(n10)
LOCAL[mydir] <- n11
n12 <- GLOBAL[len]
n13 <- GLOBAL[sys]
Expand All @@ -218,8 +218,8 @@ if __name__ == '__main__':
n24 <- n23.path
n25 <- LOCAL[i]
n26 <- n24[n25]
n27 <- $CallMethod($LoadMethod(n22, normpath), n26)
n28 <- $CallMethod($LoadMethod(n20, abspath), n27)
n27 <- n22.normpath(n26)
n28 <- n20.abspath(n27)
n29 <- LOCAL[mydir]
n30 <- $Compare.eq(n28, n29)
if n30 then jmp b4 else jmp b5
Expand All @@ -228,7 +228,7 @@ if __name__ == '__main__':
n33 <- GLOBAL[os]
n34 <- n33.path
n35 <- GLOBAL[__file__]
n36 <- $CallMethod($LoadMethod(n34, abspath), n35)
n36 <- n34.abspath(n35)
GLOBAL[__file__] <- n36
n37 <- GLOBAL[main]
n38 <- n37()
Expand Down Expand Up @@ -285,7 +285,7 @@ path.X()
n13 <- $ImportFrom(path)(n12)
TOPLEVEL[path] <- n13
n14 <- TOPLEVEL[path]
n15 <- $CallMethod($LoadMethod(n14, X), )
n15 <- n14.X()
return PYCNone |}]


Expand Down Expand Up @@ -388,7 +388,7 @@ class Test(unittest.TestCase):
n3 <- TOPLEVEL[hasattr]
n4 <- TOPLEVEL[signal]
n5 <- n3(n4, PYCString ("setitimer"))
n6 <- $CallMethod($LoadMethod(n2, skipUnless), n5, PYCString ("requires setitimer()"))
n6 <- n2.skipUnless(n5, PYCString ("requires setitimer()"))
n7 <- TOPLEVEL[unittest]
n8 <- n7.TestCase
n9 <- $BuildClass($FuncObj(Test, dummy.Test, {}), PYCString ("Test"), n8)
Expand Down Expand Up @@ -428,7 +428,7 @@ def f():
dummy.f:
b0:
n0 <- GLOBAL[foo]
n1 <- $CallMethod($LoadMethod(n0, bar), PYCInt (42))
n1 <- n0.bar(PYCInt (42))
throw n1 |}]


Expand Down Expand Up @@ -461,7 +461,7 @@ def f(ok):
dummy.f:
b0:
n0 <- GLOBAL[foo]
n1 <- $CallMethod($LoadMethod(n0, bar), )
n1 <- n0.bar()
jmp b2

b1:
Expand Down Expand Up @@ -530,7 +530,7 @@ def test_format_specifier_expressions(self):
LOCAL[width] <- PYCInt (10)
LOCAL[precision] <- PYCInt (4)
n0 <- GLOBAL[decimal]
n1 <- $CallMethod($LoadMethod(n0, Decimal), PYCString ("12.34567"))
n1 <- n0.Decimal(PYCString ("12.34567"))
LOCAL[value] <- n1
n2 <- GLOBAL[assertEqual]
n3 <- LOCAL[value]
Expand Down
Loading

0 comments on commit 898d0f0

Please sign in to comment.