Hello, On Wed, 16 Dec 2020 00:50:27 +1300 Greg Ewing <greg.ew...@canterbury.ac.nz> wrote:
> On 16/12/20 12:24 am, Paul Sokolovsky wrote: > > > That's good answer, thanks. But... it doesn't correspond to the > > implementation reality. > > Why are we talking about implementation? You said you wanted > to keep to the conceptual level. At that level, there is NO > difference at all. I'm not sure how well I was able to convey, but even in the initial message I tried to ask for an asymptotically coherent theory which would apply across layers and would allow to explain all of the following: 1. Surface syntax, and intuitive-semantics difference between "a.b()" and "(a.b)()". (Can add just "a.b" for completeness.) 2. Deeper (i.e. abstract) syntax difference between the above. 3. Codegeneration difference for the above, or in more formal terms, small-step semantics for the above. I specifically write "asymptotically coherent", because we already know that a number of parts are missing (e.g. level 2 above is completely missing so far), and there can be random cases (mostly on bytecode codegeneration end) which don't fit into theory, and to explain which we'd need to apply to outside means like: "oh, we just didn't think about that" or "oh, that's bytecode optimization". Ok, so here's my theory: Which still starts with setting the stage. Besides those simple operators they teach in school, there're others, less simple. Some are still pretty familiar to programmers. For example, C's ternary conditional "?:" operator. It's indeed usually named "?:", but that doesn't tell us much about its actual syntax: expr1 ? expr2 : expr3 So, it's a ternary operator, unlike common unary or binary ones. And it's not prefix, postfix, or infix. Linguistics has a term for a generalized prefix/suffix/infix concept, so let's call such operators "affix". Python also has ternary conditional operator: expr2 if expr1 else expr3 Which shows: a) operator lexics doesn't have to consist of punctuation, letters work too; b) it has different order of expression comparing to C's version. Despite those striking differences, nobody even gets confused. Which shows human ability to see deeper similarity across surface differences, on which ability we're going to rely in the rest of our discussion. And we're almost there. The only intermediate step to consider is the call operator, "()". It's much older than conditional operator in Python, but always was a special one: expr(args) So, it's binary, and it's also affix, as we can't ignore that closing paren. A note about "binary": a *function* may take multiple arguments. However, a *call operator*, in its abstract syntax, takes just a single 2nd arg, of special type "args" (and syntax of that includes zero or more args, positional and keywords, starred and double-starred at trailing positions). With all the above in mind, Python3.7, in a strange twist of fate, and without much ado, has acquired a new operator: the method call, ".()". It's a ternary operator with the following syntax: expr.name(args) It's an affix operator, with its 3 constituent characters nicely spread around the expression it forms. Now, everything falls into its place: An expression like: expr.name is an "attribute access operator" which gets compiled to LOAD_ATTR instruction. expr() is a "call operator", which gets compied to CALL_FUNCTION and: expr.name() is a "method call operator", which gets compiled into LOAD_METHOD and complementary CALL_METHOD opcodes. CPython3.6 and below didn't have ".()" operator, and compiled it as "attr access" + "function call", but CPython3.7 got the new operator, and compiles it as such (the single operator). The ".()" operator is interesting, because it's compounded from existing operators. It thus can be "sliced" using parenthesis into individual operators. And the meaning of (a.b)() is absolutely clear - it says "first compute 'a.b', and then call it", just the same as "a + (b + c)" says "first compute 'b + c', and then add that to 'a'". So, why CPython3.7+ still compiles "(a.b)()" using LOAD_METHOD. The currently proposed explanation in this thread was "optimization", and let's just agree with it ;-). The real reason is of course different, and it would be nice to discuss it further. But still, are there Python implementations which compile "(a.b)()" faithfully, with its baseline semantic meaning? Of course there're. E.g., MicroPython-derived Python implementations compile it in the full accordance with the theory presented here: obj.meth() (obj.meth)() $ pycopy -v -v objmeth.py [] 00 LOAD_NAME obj (cache=0) 04 LOAD_METHOD meth 07 CALL_METHOD n=0 nkw=0 09 POP_TOP 10 LOAD_NAME obj (cache=0) 14 LOAD_ATTR meth (cache=0) 18 CALL_FUNCTION n=0 nkw=0 20 POP_TOP 21 LOAD_CONST_NONE 22 RETURN_VALUE Discussion? Criticism? Concerns? > > -- > Greg -- Best regards, Paul mailto:pmis...@gmail.com _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-le...@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/JRC6UR3YT4XTN2YMSSWHPIPBOG43HPLT/ Code of Conduct: http://python.org/psf/codeofconduct/