I’m trying to understand the internals of what happens when using the dot notation.
There seems to be a few ways to make the call:
x = [1,2,3]; y=[0,2,4]
x .<= y #method 1
.<=(x,y) #method 2
(<=).(x,y) #method 3
What is happening under the hood in each of these methods? Are they all remapped to a broadcast call eventually?
I see that .<= does not exist as a function, but <= does. So I suspect that somewhere it is being parsed and reissued as a different call. Where (in which source file) does this remapping actually occur?
When I look at the AST, I see that method 1 and method 2 are the same, whereas method 3 is different. But I haven’t been able to trace it further.
Thanks folks! The post mentions that “the compiler notices these dots at parse time (or technically at “lowering” time, but in any case long before it knows the types of the variables etc.), and transforms them into calls to broadcast .” I was looking for exactly where this translation to broadcast calls happens and I couldn’t find it in Base. I think it happens either in the Lisp code (julia-parser.scm or julia-syntax.scm) or in the C code (interpreter.c) or a combination.
I believe there is a slight difference in how the methods are handled in the compiler. Method 1 (infix) and method 2 (dotted symbol) seem to get handled in julia-parser.scm as special symbols “prec-comparison”; whereas method 3 (dotted function) seem to get handled by dot fusion in julia-syntax.scm around line 1752.
That’s not quite the whole story.
Regarding your examples 1 and 2: To the parser, .=> is no different from any other infix operator, which just happens to have the same precedence as =>. All the add-dots function you see at the top of julia-syntax.jl does, is add both op and .op to the parser’s list of valid operators. The parser doesn’t have any clue that these are broadcasted calls, to it, they are just regular call expressions. (=>).(a, b) is parsed as (|.| => (tuple a b)), which is pretty much treated the same as getproperty, like a.b from the parser’s perspective.
Since just a couple of days ago on master, there’s also a 4th case, namely (.=>)(a, b), because to enable standalone .op syntax to represent the broadcasted version operators as in map(.*, a, b), this is parsed as (call (|.| =>) a b), i.e. a function call expression to the expression (|.| =>).
This means, that none of the actually interesting stuff about broadcasting syntax, like broadcast fusion or fused inplace broadcasts is actually handled by the parser, which is exactly as it should be, since the parser’s job is to just produce a tree representation of directly what you wrote and not really try to interpret what this means functionally. All of the cases above are then handled in expand-fuse-broadcast in julia-syntax.jl, as you wrote above, to actually implement all the broadcasting semantics and lowering to the proper Base functions in Base.Broadcast.
Yes, right, thanks for the info! I guess the part that had me confused was why they had different intermediate Expr representations and where things get merged back together to a broadcast call. I think I understand now. Method 1 and method 2 are parsed (by julia-parser.scm) as dotted symbols and the expression that is produced has a head of .<=. Method 3 is parsed as a broadcast function call and the expression that is produced has a head of .. Both of these cases are handled in expand-fuse-broadcast in julia-syntax.scm as you said. Method 1 and 2 are treated by dotop-named (operators that begin with a dot), which is defined in ast.scm. Method 3 is treated by dot-to-fuse. In the end, all cases are converted to broadcasted and broadcasted_kwsyntax calls, which are implemented in Base.Broadcast as you said.