Open olopierpa opened 6 months ago
R6RS specifies that each of the pieces of syntax in the else clause are an <expression>
, but I'm not aware of any way to reliably distinguish between a bit of syntax that is only valid in expression context (vs. definition context) in a syntax transformer. Using the example definition of the cond
syntax given in appendix B, the very first clause in the transformer turns (cond (else result1 result2 ...))
into (begin result1 result2 ...)
. When you couple that with the description of begin
as a "splicing" construct, it means that the contents of that clause can begin with definitions if the containing context is valid for definitions. I assume you are trying this out in the REPL, where any new form can be a definition.
Since the default environment for expand
is the interaction environment, I think you will always get that result from expand. However, if you try to actually run it in a context that is not valid for definitions then you will get different results; likewise if you run it in a context that has a different scope from top-level you will get different results.
> (let () (expand '(cond (else (define x 7) x))))
(begin (set! x 7) x)
> (let () (cond (else (define x 7) x)))
7
> x
Exception: variable x is not bound
> (expand '(let () (cond (else (define x 7) x))))
(letrec* ([#{x cqotkwzgeeepeo4i4c5k004i4-0} 7])
#{x cqotkwzgeeepeo4i4c5k004i4-0})
> (expand '(let () 23 (cond (else (define x 7) x))))
Exception: invalid context for definition (define x 7)
Got it. Thank you.
Indeed when the cond is not in a definition position it works as I expected: > (let () 42 (cond (else (define x 7) 7))) Exception: invalid context for definition (define x 7)
But, even understanding the cause, the following still "feels" wrong: > (let () (cond (else (define x 7) 7)) x) 7
If instead of expanding (cond (else result1 result2 ...)) into (begin result1 result2 ...) it expanded into (begin \<void> result1 result2 ...) would this problem be solved?
Like: > (let () (begin (define x 7) 7)) 7 > (let () (begin (void) (define x 7) 7)) Exception: invalid context for definition (define x 7) Type (debug) to enter the debugger.
Thanks!
It's worth noting that this behavior only appears for (cond [else ...])
forms. Having other branches for cond will always fail:
> (cond
[#f (define x 1234) x]
[else (define y 5678) y])
Exception: invalid context for definition (define x 1234)
> (cond
[#f (define x 1234) x])
Exception: invalid context for definition (define x 1234)
Though I'm not sure if that's an argument for making only-child else branches behave as if there were other branches in the cond, or just saying "well don't write code that way"
but this should not works: Chez Scheme Version 10.0.0 Copyright 1984-2024 Cisco Systems, Inc. ` (define (foo) (cond (else (define x 7) 3)) x)
(foo)
7 `
but I'm not aware of any way to reliably distinguish between a bit of syntax that is only valid in expression context (vs. definition context) in a syntax transformer.
Instead of detecting non-expression contexts, the macro could make sure that the result is an expression context. For example, (cond [else e1 e2 ...])
could expand to (if #t (begin e1 e2))
. Some form with a reliable expression context is needed (that allows any number of return values), and (if #t ....)
is the simplest option I see among the expander's core forms. We could also add a core form like $expression
, but I'd be inclined to stick with the available option.
It looks like (or (define x 7))
and (and (define x 7))
have the same issue — not because they use cond
, but because they have a similar expansion strategy.
Fixing cond
, or
, and and
with a wrapper like (if #t (begin ....))
seems like a good idea to me. It also seems possible that existing Chez Scheme code relies on the current behavior — but hopefully that kind of code is rare, and maybe better matching the standard is enough reason to break any programs like that.
I see that case
does have a similar difference from R6RS, after all: it puts the content of an else
clause into an internal-definition context, while other clauses of a case
form are not internal-definition contexts. Changing that behavior seems likely to break existing code, though, since internal-definition behavior is reasonable, and since it happens for else
even when there are other clauses before.
I remain unconvinced that anything is broken, even if it's a little strange. Both the r6rs and r7rs specs give definitions of cond
that work this way - as a "sample definition" in the r6rs spec, but in the chapter titled "Formal syntax and semantics" in r7rs. Similarly, syntax definitions for and
and or
are also given in both specs. On the other hand, the description of the semantics of cond
says that each element in the else clause is an <expression>
, and is silent on what an implementation may/should/must do if one of them is not (possibly because the authors assumed that it would be flagged as an error post-expansion). It would be ideal if this tension were resolved in the errata of the specs (but that seems unlikely).
I'm a bit hesitant about any non-backwards-compatible change that makes previously valid code no longer valid, absent a major version number change. It does seem rather unlikely that something is relying on this specific behavior, though.
On Fri, May 24, 2024 at 12:59 AM Jamie Taylor @.***> wrote:
I remain unconvinced that anything is broken, even if it's a little strange.
It may not be formally broken, but it leaves undetected an error which could be detected and reported at no cost.
Message ID: @.***>
Hello,
this expansion is surprising:
> (expand '(cond (else (define x 7) x))) (begin (set! x 7) x)
No difference between cond from chezscheme or from rnrs.
If I understand correctly the spec, in R6RS it's an error.
It would be better to either make all cond branches bodies, as Racket does, or raise an error.
As it is now:
> (cond (else (define x 7) x)) 7 > x 7
Cheers