Macros do not break referential transparency. The value of the expression given to the macro is the code/symbols themselves (not what they would return when executing). So if you have an outer macro that takes the result of an inner macro as its argument, and the inner macro does return the same string of symbols that you could have literally written in the code as an argument, then the outer macro do give the same result (i.e., does not break referential transparency). So macros are not inherently breaking referential transparency, they just take their arguments at another time, and so in the phrase “An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program’s behavior.” an “expression” must be another macro, and the “value of the expression” is a string of symbols that could have written directly into the code, and yes, those two are interchangeable to the outer macro (i.e., the program will run the same way) if they output the same string of symbols.
In Julia, the only convention related to “referential transparency” is that the name of methods that mutate their arguments should end with a !
. And this is not even followed by the IO functions of the standard library (it is assumed that all of them break “referential transparency”). Many languages do not have a strong convention about that, and do not make an effort to distinguish between functions that have referential transparency or not. Because in most languages, breaking referential transparency is exceedingly common, the programmer just needs to write a mutating procedure (which again, outside of functional languages, it is a very typical characteristic of a procedure).