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/

Reply via email to