masak / alma

ALgoloid with MAcros -- a language with Algol-family syntax where macros take center stage
Artistic License 2.0
138 stars 15 forks source link

Implement +=, *= and all the other assignment operators #152

Open masak opened 8 years ago

masak commented 8 years ago

I'm thinking we first do this one behind a feature flag, then (when we have modules) as a pragma that flips on the feature flag, and finally as a pure-007 parser-changing module.

It'll need to be implemented using something like a "what are all the allowable infixes" hook, which adds all the assignment infixes in a dynamic-enough way that (say) defining a new ordinary infix op immediately makes the corresponding op= form available.

Also, remember that the parsing of op= is such that the entire rest of the expression has invisible parentheses. So, for example

my four = 4;
say(four * 10 + 3);    # 43
say(four *= 10 + 3);   # 52
masak commented 8 years ago

It'll need to be implemented using something like a "what are all the allowable infixes" hook, which adds all the assignment infixes in a dynamic-enough way that (say) defining a new ordinary infix op immediately makes the corresponding op= form available.

The cool thing is that is parsed gives us this for free. This is where "is parsed regexes are scoped to the (current language's) parser" pays off — no hook needed; we just parse and the infix is there.

macro postfix:<op=>(term, infixop, expr) is parsed(/ <infix> "=" <EXPR> /) {
    return quasi {
        {{{term}}} = {{{term}}} {{{infixop @ Q::Infix}}} {{{expr}}}
    }
}

Also, remember that the parsing of op= is such that the entire rest of the expression has invisible parentheses.

This also falls out of is parsed; the <EXPR> inside of it gobbles up everything, guaranteeing that there won't be an operator when it's done.

masak commented 6 years ago

If we go ahead and make assignment a statement form, I guess this one becomes a statement form too.

masak commented 6 years ago

Even here, lvalues get involved in a big way. Any side effect happening in the path of the lhs should happen once, which relates both to the Single Evaluation Rule and to the Assignment Protocol.

masak commented 6 years ago

Also, remember that the parsing of op= is such that the entire rest of the expression has invisible parentheses.

I think this is wrong, but without observable consequences, so to speak.

infix:<=> is of loosest precedence. op= would be an infix op of the same precence. So, in effect, the rest of the expression, being tighter or of equal precedence, would parse as if it had invisible parentheses.

New attempt at implementation:

macro infix:<op=>(lhs, rhs, infixop) is equiv(infix:<=>) is parsed(/ <infix> "=" /) {
    quasi {
        my L = lvalue({{{lhs}}});
        L.write(L.read() {{{Q.Infix @ infixop}}} {{{rhs}}});
    };
}
masak commented 6 years ago

Heh. The new implementation is left-recursive. Not a showstopper in itself, of course, but... interesting.

masak commented 6 years ago
  • There's also the question of how we allow += but disallow +==.

It's possible is parsed(/ <infix> <!after "="> "=" /) would be enough here. (Assuming the above-mentioned left recursion is handled.)

masak commented 6 years ago

That brings up another point. The "chaining operators" in Perl 6 (corresponding to all the comparison ops in 007) are "too diffy" to be assignmentopified.

Perl 6 has a precedence level called "structural infix" which 007 doesn't really have. (But might when it gets for example .. for ranges.) That one is diffy too.

Perhaps it would make sense to attach some metadata to precedence levels? That's a very unfinished thought, so I'll just leave that as it is.

masak commented 5 years ago

The new implementation is left-recursive. Not a showstopper in itself, of course, but... interesting.

Just noting in passing that this could be handled by adopting the infix:postfix:<=> idea from #430.

There's also the question of how we allow += but disallow +==. There's nothing wrong in principle with metaoperators acting twice; it's just in this case it doesn't make sense. This is where Perl 6 says that it can't make assignment out of += because += is too diffy.

I think we should adopt the diffy categorization wholesale. It works. Precedence levels aren't first-class, but we can put a @Diffy annotation on individual operators, as usual. I'm fine with only allowing @Diffy annotations on the first defined operator of a precedence level.

masak commented 5 years ago

The new implementation is left-recursive. Not a showstopper in itself, of course, but... interesting.

Just noting in passing that this could be handled by adopting the infix:postfix:<=> idea from #430.

I came here to make that same observation again, not knowing whether I'd made it before.

It is possible that infixes are "simple" enough (just an array of literal strings, right?) that a parsed infix:<op=> rule might just power through them all an extend the declarative prefix all the way to the =... but it feels like a dangerous game to play, and #430 feels like much better bet.

masak commented 3 years ago

Another thing. This blog post at 2ality explains how the new JavaScript logical-assignment operators (&&=, ||=, and ??=) do not follow the usual pattern of A op= B meaning A = A op B, but instead mean A op (A = B).

The reason for this (as explained in that post) is that the original logical operators are all short-circuiting, and the assignment versions should be too.

Interestingly, Raku does not go this route — a synopsis has a phrasing that amounts to "macroish operators lose their short-circuitingness as they are turned into assignment operators". That may be consistent, but it's not strangely consistent, if you see what I mean. I much prefer JavaScript's strategy here, even though it clearly means having two separate rules.

masak commented 1 year ago
  • There's also the question of how we allow += but disallow +==. There's nothing wrong in principle with metaoperators acting twice; it's just in this case it doesn't make sense. This is where Perl 6 says that it can't make assignment out of += because += is too diffy.

Actually, it's because it's too fiddly. (Duh!)

But this actually does solve the whole thing, like providing a base case against an infinite recursion; each thus created metaoperator only acts on operators which are not fiddly, but the created metaoperator itself is too fiddly and therefore not subject to further meta-ing.