29
78
Fork
You've already forked r7rs
2

Make mutating literals undefined behavior #373

Open
opened 2026年07月03日 18:35:16 +02:00 by phm · 16 comments
Member
Copy link

This concerns the discussion in #74, #80, #192, #278, and #296.

In the R6RS, literal constants should raise an exception on modificiation:

It is desirable for constants(i.e. the values of literal expressions) to reside in read-only memory. To express this, it is convenient to imagine that every object that refers to locations is associated with a flag telling whether that object is mutableor immutable. Literal constants, the strings returned by symbol->string, records with no mutable fields, and other values explicitly designated as immutable are immutable objects, while all objects created by the other procedures listed in this report are mutable. An attempt to store a new value into a location referred to by an immutable object should raise an exception with condition type &assertion.

In the R7RS, it is an error:

Every object that denotes locations is either mutable or immutable. Literal constants, the strings returned by symbol->string, and possibly the environment returned by scheme-report-environment are immutable objects. All objects created by the other procedures listed in this report are mutable. It is an error to attempt to store a new value into a location that is denoted by an immutable object.

[...]

Rationale: In many systems it is desirable for constants (i.e. the values of literal expressions) to reside in read-only memory. Making it an error to alter constants permits this implementation strategy, while not requiring other systems to distinguish between mutable and immutable objects.

On the Stygian Blue docket, enforcement of immutability of literal values was voted down. SRFI 116, immutable lists, was removed in the Stygian Blue docket, and SRFI 101 may be removed at a later point in time. So immutable variants (or possibly disjoint types) of standard Scheme objects are mostly out.

I propose we keep the status quo of R6RS/R7RS, where mutation of an immutable object is undefined behavior which should raise an exception, and there is no way of differentiating mutable and immutable objects. I think this is the only sensible way forward, given the constraints of the votes. It will also address all of the immutability issues above (since we go with the status quo).


Proposed wording (new wording in bold, based off of R7RS):

Every object that denotes locations is either mutable or immutable. Literal constants, (削除) the strings returned by symbol->string, (削除ここまで) records with no mutable fields and (削除) possibly the environment returned by scheme-report-environment (削除ここまで) other values explicitly designated as immutable are immutable objects. All objects created by the other procedures listed in this report are mutable. It is (削除) an error (削除ここまで) undefined behavior to attempt to store a new value into a location that is denoted by an immutable object. An attempt to store a new value into a location referred to by an immutable object should raise an exception.

[...]

Rationale: In many systems it is desirable for constants (i.e. the values of literal expressions) to reside in read-only memory. Making it (削除) an error (削除ここまで) undefined behavior to alter constants permits this implementation strategy, while not requiring other systems to distinguish between mutable and immutable objects.

  1. The part about symbol->string and scheme-report-environment is covered by "other values explicitly designated as immutable."
  2. The part about the exception will depend on what form the exception system will take.
This concerns the discussion in #74, #80, #192, #278, and #296. In the R6RS, [literal constants should raise an exception on modificiation](https://r6rs.org/final/html/r6rs/r6rs-Z-H-8.html#node_sec_5.10): > It is desirable for constants(i.e. the values of literal expressions) to reside in read-only memory. To express this, it is convenient to imagine that every object that refers to locations is associated with a flag telling whether that object is mutableor immutable. Literal constants, the strings returned by symbol->string, records with no mutable fields, and other values explicitly designated as immutable are immutable objects, while all objects created by the other procedures listed in this report are mutable. An attempt to store a new value into a location referred to by an immutable object should raise an exception with condition type &assertion. In the R7RS, [it is an error](https://standards.scheme.org/r7rs-html5/index.html#3.4): > Every object that denotes locations is either mutable or immutable. Literal constants, the strings returned by symbol->string, and possibly the environment returned by scheme-report-environment are immutable objects. All objects created by the other procedures listed in this report are mutable. It is an error to attempt to store a new value into a location that is denoted by an immutable object. > > [...] > > Rationale: In many systems it is desirable for constants (i.e. the values of literal expressions) to reside in read-only memory. Making it an error to alter constants permits this implementation strategy, while not requiring other systems to distinguish between mutable and immutable objects. On the Stygian Blue docket, [enforcement of immutability of literal values](https://codeberg.org/scheme/r7rs/src/branch/main/ballot-results/dpk/2025-03-stygian-blue-reddish-green-report.md#1-should-r7rs-large-require-implementations-to-enforce-the-immutability-of-literal-values) was voted down. [SRFI 116](https://srfi.schemers.org/srfi-116/), immutable lists, was removed in the Stygian Blue docket, and [SRFI 101](https://srfi.schemers.org/srfi-101/) may be removed [at a later point in time](https://codeberg.org/scheme/r7rs/src/branch/main/ballot-results/dpk/2025-03-stygian-blue-reddish-green-report.md#5-should-r7rs-large-ultimately-include-only-one-of-list-queues-a-k-a-tconcs-srfi-117-scheme-list-queue-from-the-red-edition-random-access-lists-srfi-101-scheme-rlist-from-the-red-edition-flexvectors-srfi-214-persistent-growable-vectors-no-srfi-yet-or-finger-trees-no-srfi-yet). So immutable variants (or possibly disjoint types) of standard Scheme objects are mostly out. I propose we keep the status quo of R6RS/R7RS, where mutation of an immutable object is undefined behavior which should raise an exception, and there is no way of differentiating mutable and immutable objects. I think this is the only sensible way forward, given the constraints of the votes. It will also address all of the immutability issues above (since we go with the status quo). ________________________ Proposed wording (new wording in bold, based off of R7RS): > Every object that denotes locations is either mutable or immutable. Literal constants, ~~the strings returned by symbol->string,~~ **records with no mutable fields** and ~~possibly the environment returned by scheme-report-environment~~ **other values explicitly designated as immutable** are immutable objects. All objects created by the other procedures listed in this report are mutable. It is ~~an error~~ **undefined behavior** to attempt to store a new value into a location that is denoted by an immutable object. **An attempt to store a new value into a location referred to by an immutable object should raise an exception.** > > [...] > > Rationale: In many systems it is desirable for constants (i.e. the values of literal expressions) to reside in read-only memory. Making it ~~an error~~ **undefined behavior** to alter constants permits this implementation strategy, while not requiring other systems to distinguish between mutable and immutable objects. 1. The part about `symbol->string` and `scheme-report-environment` is covered by "other values explicitly designated as immutable." 2. The part about the exception will depend on what form the exception system will take.

I agree with this. However, I think it would be good to have a procedure that accepts a representable value and returns a literal constant which is the same (in the sense of equal?) as the argument without sharing with mutable objects. This can be portably but inefficiently implemented using write, two string ports, and read, but most implementations can do better.

In addition, another purpose for immutable objects is to prevent mutation from spreading across abstraction boundaries. This involves copying only once if the implementation can non-portably detect immutability, as it must do if it is going to raise an exception.

I agree with this. However, I think it would be good to have a procedure that accepts a representable value and returns a literal constant which is the same (in the sense of `equal?`) as the argument without sharing with mutable objects. This can be portably but inefficiently implemented using `write`, two string ports, and `read`, but most implementations can do better. In addition, another purpose for immutable objects is to prevent mutation from spreading across abstraction boundaries. This involves copying only once if the implementation can non-portably detect immutability, as it must do if it is going to raise an exception.
Member
Copy link

I'm in favor. And yes, at some point we need to pin down what kind of condition should be raised. (I like R6RS's &assertion here.)

Why do we need to mention "records with no mutable fields"? How could one even attempt to mutate such a record?

I'm in favor. And yes, at some point we need to pin down what kind of condition should be raised. (I like R6RS's `&assertion` here.) Why do we need to mention "records with no mutable fields"? How could one even attempt to mutate such a record?
Author
Member
Copy link

@johnwcowan wrote in #373 (comment):

I agree with this. However, I think it would be good to have a procedure that accepts a representable value and returns a literal constant which is the same (in the sense of equal?) as the argument without sharing with mutable objects. This can be portably but inefficiently implemented using write, two string ports, and read, but most implementations can do better.

This is basically a deep-copy procedure, which in many cases a programmer would want to be extended to other contains (sets, maps, record types, etc). I think one can write their own version of this specific to the types they know they are using (if you aren't using a value of a certain type, then you aren't going to be mutating it).

Since we don't have generic procedures, we can't really add the more general version. So I don't think we should add it.

In addition, another purpose for immutable objects is to prevent mutation from spreading across abstraction boundaries. This involves copying only once if the implementation can non-portably detect immutability, as it must do if it is going to raise an exception.

I will add that.

Why do we need to mention "records with no mutable fields"? How could one even attempt to mutate such a record?

I don't know, it's there because it was in the R6RS's description. (It makes about as much sense as saying that numbers are immutable objects.)

@johnwcowan wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18556688: > I agree with this. However, I think it would be good to have a procedure that accepts a representable value and returns a literal constant which is the same (in the sense of `equal?`) as the argument without sharing with mutable objects. This can be portably but inefficiently implemented using `write`, two string ports, and `read`, but most implementations can do better. This is basically a deep-copy procedure, which in many cases a programmer would want to be extended to other contains (sets, maps, record types, etc). I think one can write their own version of this specific to the types they know they are using (if you aren't using a value of a certain type, then you aren't going to be mutating it). Since we don't have generic procedures, we can't really add the more general version. So I don't think we should add it. > In addition, another purpose for immutable objects is to prevent mutation from spreading across abstraction boundaries. This involves copying only once if the implementation can non-portably detect immutability, as it must do if it is going to raise an exception. I will add that. > Why do we need to mention "records with no mutable fields"? How could one even attempt to mutate such a record? I don't know, it's there because it was in the R6RS's description. (It makes about as much sense as saying that numbers are immutable objects.)

Read and write are precisely generic procedures, limited to types that have external representations. And the difference between this procedure and ordinary deep copy is that it is an error to mutate the result. It lets us programmatically create literal constants.

Note that we can also use eval to implement this procedure, being careful to wrap the argument in quote if it is a symbol or pair.

`Read` and `write` are precisely generic procedures, limited to types that have external representations. And the difference between this procedure and ordinary deep copy is that it is an error to mutate the result. It lets us programmatically create literal constants. Note that we can also use `eval` to implement this procedure, being careful to wrap the argument in `quote` if it is a symbol or pair.
Author
Member
Copy link

@johnwcowan wrote in #373 (comment):

Read and write are precisely generic procedures

I meant user-extensible generic procedures.

It lets us programmatically create literal constants.

I don't think we should add more procedures where operations are undefined behavior (with no way to detect it before mutation) unless there is more precedent for it. Especially if it's possible to implement it portably.

@johnwcowan wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18589520: > `Read` and `write` are precisely generic procedures I meant user-extensible generic procedures. > It lets us programmatically create literal constants. I don't think we should add more procedures where operations are undefined behavior (with no way to detect it before mutation) unless there is more precedent for it. Especially if it's possible to implement it portably.
Member
Copy link

I would revisit the question of whether we really want to have "undefined behaviour" in the report. We know the plagues of C.

If we can't agree on making raising an exception mandatory, we should at least make it "implementation-defined" by giving a precise set of options that an implementation may follow to prevent "spooky actions at a distance". (E.g. an implementation may silently ignore the attempt to mutate literals, or an implementation may allow this, but then it may not fold literals in one library with literals in another library, etc. In no case should the attempt of mutating an immutable string make the internal symbol table invalid, etc.)

I would revisit the question of whether we really want to have "undefined behaviour" in the report. We know the plagues of C. If we can't agree on making raising an exception mandatory, we should at least make it "implementation-defined" by giving a precise set of options that an implementation may follow to prevent "spooky actions at a distance". (E.g. an implementation may silently ignore the attempt to mutate literals, or an implementation may allow this, but then it may not fold literals in one library with literals in another library, etc. In no case should the attempt of mutating an immutable string make the internal symbol table invalid, etc.)
Member
Copy link

I regard each occurrence of "UB" in our final Report as a black mark against us. I would prefer a statement such as "It is an error to mutate an immutable value, and the non-continuable exception &mutation SHOULD BE raised. It is understood that an implementation may be unable to identify some or all such cases; the implementation SHOULD document any such cases."

I regard each occurrence of "UB" in our final Report as a black mark against us. I would prefer a statement such as "It is an error to mutate an immutable value, and the non-continuable exception &mutation SHOULD BE raised. It is understood that an implementation may be unable to identify some or all such cases; the implementation SHOULD document any such cases."
Member
Copy link

@vmanis wrote in #373 (comment):

I regard each occurrence of "UB" in our final Report as a black mark against us. I would prefer a statement such as "It is an error to mutate an immutable value, and the non-continuable exception &mutation SHOULD BE raised. It is understood that an implementation may be unable to identify some or all such cases; the implementation SHOULD document any such cases."

It seems to me that such a statement would effectively allow UB, even if not formulated in this way.

@vmanis wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18746750: > I regard each occurrence of "UB" in our final Report as a black mark against us. I would prefer a statement such as "It is an error to mutate an immutable value, and the non-continuable exception &mutation SHOULD BE raised. It is understood that an implementation may be unable to identify some or all such cases; the implementation SHOULD document any such cases." It seems to me that such a statement would effectively allow UB, even if not formulated in this way.
Author
Member
Copy link

@mnw wrote in #373 (comment):

I would revisit the question of whether we really want to have "undefined behaviour" in the report. We know the plagues of C.

I don't know if anyone wants undefined behavior. Although "undefined behavior" allows an implementation to do nefarious things like the GCC, there is no reason that they should do so, and I don't know how many popular R7RS-Small implementations go to such extremes (for the equivalent "it is an error").

As far as I know, much of a C compiler’s crazy behavior on undefined behavior is motivated by speed. Although Scheme can be fast, if your absolute, number-one priority is how fast your code is, you’re likely not reaching for a garbage collected mostly-functional language.

@mnw wrote in #373 (comment):

If we can't agree on making raising an exception mandatory, we should at least make it "implementation-defined" by giving a precise set of options that an implementation may follow to prevent "spooky actions at a distance". (E.g. an implementation may silently ignore the attempt to mutate literals, or an implementation may allow this, but then it may not fold literals in one library with literals in another library, etc. In no case should the attempt of mutating an immutable string make the internal symbol table invalid, etc.)

We could allow one of 1) raising an exception or 2) act as-if the object was freshly allocated (I think that is what most other implementations do), although

  1. Some implementations do a mix (Chez allows mutating quoted literals, but doesn't allow mutating the return values of symbol->string).
  2. Adding a set of options means we set in stone the various ways implementations currently handle constant data. Then we might have code that only works for implementations that implement variant N of immutable object semantics, and then we're stuck with it.
  3. I don't know if any implementations document what they do with immutable objects, if the behavior isn’t raising an exception.

I think if we can't do the correct thing (which in my opinion is to require raising an exception) then we should just leave it for a future Report to specify.

@mnw wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18730190: > I would revisit the question of whether we really want to have "undefined behaviour" in the report. We know the plagues of C. I don't know if anyone *wants* undefined behavior. Although "undefined behavior" allows an implementation to do nefarious things like the GCC, there is no reason that they should do so, and I don't know how many popular R7RS-Small implementations go to such extremes (for the equivalent "it is an error"). As far as I know, much of a C compiler’s crazy behavior on undefined behavior is motivated by speed. Although Scheme can be fast, if your absolute, number-one priority is how fast your code is, you’re likely not reaching for a garbage collected mostly-functional language. @mnw wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18730190: > If we can't agree on making raising an exception mandatory, we should at least make it "implementation-defined" by giving a precise set of options that an implementation may follow to prevent "spooky actions at a distance". (E.g. an implementation may silently ignore the attempt to mutate literals, or an implementation may allow this, but then it may not fold literals in one library with literals in another library, etc. In no case should the attempt of mutating an immutable string make the internal symbol table invalid, etc.) We could allow one of 1) raising an exception or 2) act as-if the object was freshly allocated (I think that is what most other implementations do), although 1. Some implementations do a mix (Chez allows mutating quoted literals, but doesn't allow mutating the return values of `symbol->string`). 2. Adding a set of options means we set in stone the various ways implementations currently handle constant data. Then we might have code that only works for implementations that implement variant *N* of immutable object semantics, and then we're stuck with it. 3. I don't know if any implementations document what they do with immutable objects, if the behavior isn’t raising an exception. I think if we can't do the correct thing (which in my opinion is to require raising an exception) then we should just leave it for a future Report to specify.
Member
Copy link

@phm wrote in #373 (comment):

@mnw wrote in #373 (Kommentar):

I would revisit the question of whether we really want to have "undefined behaviour" in the report. We know the plagues of C.

I don't know if anyone wants undefined behavior. Although "undefined behavior" allows an implementation to do nefarious things like the GCC, there is no reason that they should do so, and I don't know how many popular R7RS-Small implementations go to such extremes (for the equivalent "it is an error").

As far as I know, much of a C compiler’s crazy behavior on undefined behavior is motivated by speed. Although Scheme can be fast, if your absolute, number-one priority is how fast your code is, you’re likely not reaching for a garbage collected mostly-functional language.

A C compiler's "crazy" behaviour is not as crazy as it seems. The problem is that a C compiler cannot, in general, guess what you mean. And it is not a particular feature of C and its implementations. It will, more or less, always be present as soon as a language has UB.

Coming back to the particular case of mutating literals: if some part of a program mutates a literal, it can happen that in some unrelated library (even in a core library or in the core expander if these are coded in Scheme), a literal datum suddenly changes because of some late compiler pass that folds literals (which is desirable). The Scheme implementation has no way to control how much the other library will run berserk when assumed invariants suddenly no longer hold.

The same is true if the symbol table of the Scheme system becomes corrupted.

Note that an implementation doesn't have "to do nefarious things" here.

@mnw wrote in #373 (Kommentar):

If we can't agree on making raising an exception mandatory, we should at least make it "implementation-defined" by giving a precise set of options that an implementation may follow to prevent "spooky actions at a distance". (E.g. an implementation may silently ignore the attempt to mutate literals, or an implementation may allow this, but then it may not fold literals in one library with literals in another library, etc. In no case should the attempt of mutating an immutable string make the internal symbol table invalid, etc.)

We could allow one of 1) raising an exception or 2) act as-if the object was freshly allocated (I think that is what most other implementations do), although

1. Some implementations do a mix (Chez allows mutating quoted literals, but doesn't allow mutating the return values of `symbol->string`).
2. Adding a set of options means we set in stone the various ways implementations currently handle constant data. Then we might have code that only works for implementations that implement variant _N_ of immutable object semantics, and then we're stuck with it.
3. I don't know if any implementations document what they do with immutable objects, if the behavior isn’t raising an exception.

I think if we can't do the correct thing (which in my opinion is to require raising an exception) then we should just leave it for a future Report to specify.

Chez, for example, has been moving towards raising exceptions; for example, mutating the return value of symbol->string was possible in version 9. I think we should specify the right thing here and give implementations an incentive to move towards a better world. (And option 2 is not that bad; later reports can further restrict it to 1.)

@phm wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18748985: > @mnw wrote in #373 (Kommentar): > > > I would revisit the question of whether we really want to have "undefined behaviour" in the report. We know the plagues of C. > > I don't know if anyone _wants_ undefined behavior. Although "undefined behavior" allows an implementation to do nefarious things like the GCC, there is no reason that they should do so, and I don't know how many popular R7RS-Small implementations go to such extremes (for the equivalent "it is an error"). > > As far as I know, much of a C compiler’s crazy behavior on undefined behavior is motivated by speed. Although Scheme can be fast, if your absolute, number-one priority is how fast your code is, you’re likely not reaching for a garbage collected mostly-functional language. A C compiler's "crazy" behaviour is not as crazy as it seems. The problem is that a C compiler cannot, in general, guess what you mean. And it is not a particular feature of C and its implementations. It will, more or less, always be present as soon as a language has UB. Coming back to the particular case of mutating literals: if some part of a program mutates a literal, it can happen that in some unrelated library (even in a core library or in the core expander if these are coded in Scheme), a literal datum suddenly changes because of some late compiler pass that folds literals (which is desirable). The Scheme implementation has no way to control how much the other library will run berserk when assumed invariants suddenly no longer hold. The same is true if the symbol table of the Scheme system becomes corrupted. Note that an implementation doesn't have "to do nefarious things" here. > > @mnw wrote in #373 (Kommentar): > > > If we can't agree on making raising an exception mandatory, we should at least make it "implementation-defined" by giving a precise set of options that an implementation may follow to prevent "spooky actions at a distance". (E.g. an implementation may silently ignore the attempt to mutate literals, or an implementation may allow this, but then it may not fold literals in one library with literals in another library, etc. In no case should the attempt of mutating an immutable string make the internal symbol table invalid, etc.) > > We could allow one of 1) raising an exception or 2) act as-if the object was freshly allocated (I think that is what most other implementations do), although > > 1. Some implementations do a mix (Chez allows mutating quoted literals, but doesn't allow mutating the return values of `symbol->string`). > > 2. Adding a set of options means we set in stone the various ways implementations currently handle constant data. Then we might have code that only works for implementations that implement variant _N_ of immutable object semantics, and then we're stuck with it. > > 3. I don't know if any implementations document what they do with immutable objects, if the behavior isn’t raising an exception. > > > I think if we can't do the correct thing (which in my opinion is to require raising an exception) then we should just leave it for a future Report to specify. Chez, for example, has been moving towards raising exceptions; for example, mutating the return value of `symbol->string` was possible in version 9. I think we should specify the right thing here and give implementations an incentive to move towards a better world. (And option 2 is not that bad; later reports can further restrict it to 1.)
Member
Copy link

I'm inclined to replace my earlier SHOULD with MUST, and recognize that some/many/most implementations will not meet this criterion. (I also support implementations having a safety parameter or setting.) And there is no need for an implementer to document what they do in cases where an exception should be raised, only that they do not check in certain or all cases.

I'm inclined to replace my earlier SHOULD with MUST, and recognize that some/many/most implementations will not meet this criterion. (I also support implementations having a safety parameter or setting.) And there is no need for an implementer to document what they do in cases where an exception should be raised, only that they do not check in certain or all cases.
Author
Member
Copy link

@mnw wrote in #373 (comment):

A C compiler's "crazy" behaviour is not as crazy as it seems. The problem is that a C compiler cannot, in general, guess what you mean. And it is not a particular feature of C and its implementations. It will, more or less, always be present as soon as a language has UB.

It cannot correctly guess what I mean, but there are many sensible ways to define undefined behavior (for example, making signed arithmetic wrap with -fwrapv, modify aliasing rules with -fno-strict-aliasing, etc).

Coming back to the particular case of mutating literals: if some part of a program mutates a literal, it can happen that in some unrelated library (even in a core library or in the core expander if these are coded in Scheme), a literal datum suddenly changes because of some late compiler pass that folds literals (which is desirable). The Scheme implementation has no way to control how much the other library will run berserk when assumed invariants suddenly no longer hold.

If an implementation cannot guarantee that attempting to mutate a constant will not compromise the implementation in such an extreme way, then it shouldn't fold mutable constants. Or at the very least it should have some flag that disables doing such a thing.

I think we should specify the right thing here and give implementations an incentive to move towards a better world.

The whole issue here is that the right thing (well, what I think is the right thing) was voted down. Many of the "no" votes cited implementation burden as a reason.

(And option 2 is not that bad; later reports can further restrict it to 1.)

Removing something from the Report is a lot harder than adding it.

@mnw wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18750032: > A C compiler's "crazy" behaviour is not as crazy as it seems. The problem is that a C compiler cannot, in general, guess what you mean. And it is not a particular feature of C and its implementations. It will, more or less, always be present as soon as a language has UB. It cannot correctly guess what I mean, but there are many sensible ways to define undefined behavior (for example, making signed arithmetic wrap with `-fwrapv`, modify aliasing rules with `-fno-strict-aliasing`, etc). > Coming back to the particular case of mutating literals: if some part of a program mutates a literal, it can happen that in some unrelated library (even in a core library or in the core expander if these are coded in Scheme), a literal datum suddenly changes because of some late compiler pass that folds literals (which is desirable). The Scheme implementation has no way to control how much the other library will run berserk when assumed invariants suddenly no longer hold. If an implementation cannot guarantee that attempting to mutate a constant will not compromise the implementation in such an extreme way, then it shouldn't fold mutable constants. Or at the very least it should have some flag that disables doing such a thing. > I think we should specify the right thing here and give implementations an incentive to move towards a better world. The whole issue here is that [the right thing](https://codeberg.org/scheme/r7rs/src/branch/main/ballot-results/dpk/2025-03-stygian-blue-reddish-green-report.md#1-should-r7rs-large-require-implementations-to-enforce-the-immutability-of-literal-values) (well, what I think is the right thing) was voted down. Many of the ["no" votes](https://codeberg.org/scheme/r7rs/src/branch/main/ballot-results/dpk/2025-03-stygian-blue-reddish-green.csv) cited implementation burden as a reason. > (And option 2 is not that bad; later reports can further restrict it to 1.) Removing something from the Report is a lot harder than adding it.
Member
Copy link

This is why I hope we can put the correct behaviour in our Report, albeit with a SHOULD. This in no way invalidates any previously-valid program, but it replaces UB with a desired behaviour that some (perhaps all) implementations might not be able to comply with in some case.

This is why I hope we can put the correct behaviour in our Report, albeit with a SHOULD. This in no way invalidates any previously-valid program, but it replaces UB with a desired behaviour that some (perhaps all) implementations might not be able to comply with in some case.
Member
Copy link

@phm wrote in #373 (comment):

@mnw wrote in #373 (Kommentar):

A C compiler's "crazy" behaviour is not as crazy as it seems. The problem is that a C compiler cannot, in general, guess what you mean. And it is not a particular feature of C and its implementations. It will, more or less, always be present as soon as a language has UB.

It cannot correctly guess what I mean, but there are many sensible ways to define undefined behavior (for example, making signed arithmetic wrap with -fwrapv, modify aliasing rules with -fno-strict-aliasing, etc).

And there are equally good reasons why, in particular, these things are UB in C.

But we are not trying to create a new C standard, so this is likely moot.

Coming back to the particular case of mutating literals: if some part of a program mutates a literal, it can happen that in some unrelated library (even in a core library or in the core expander if these are coded in Scheme), a literal datum suddenly changes because of some late compiler pass that folds literals (which is desirable). The Scheme implementation has no way to control how much the other library will run berserk when assumed invariants suddenly no longer hold.

If an implementation cannot guarantee that attempting to mutate a constant will not compromise the implementation in such an extreme way, then it shouldn't fold mutable constants. Or at the very least it should have some flag that disables doing such a thing.

Try to formulate precisely where the boundary of acceptable behaviour is.

I think we should specify the right thing here and give implementations an incentive to move towards a better world.

The whole issue here is that the right thing (well, what I think is the right thing) was voted down. Many of the "no" votes cited implementation burden as a reason.

It is likely implementation-laziness...

Anyway, if we follow option 2, it will make life easier for implementers, compared to option 1.

(And option 2 is not that bad; later reports can further restrict it to 1.)

Removing something from the Report is a lot harder than adding it.

Here, it is the other way round because we would not be removing options offered to the user, but rather those offered to the implementation. In fact, this is what the difference between a report and its successor usually amounts to if we have backward compatibility. 😃

@phm wrote in https://codeberg.org/scheme/r7rs/issues/373#issuecomment-18771182: > @mnw wrote in #373 (Kommentar): > > > A C compiler's "crazy" behaviour is not as crazy as it seems. The problem is that a C compiler cannot, in general, guess what you mean. And it is not a particular feature of C and its implementations. It will, more or less, always be present as soon as a language has UB. > > It cannot correctly guess what I mean, but there are many sensible ways to define undefined behavior (for example, making signed arithmetic wrap with `-fwrapv`, modify aliasing rules with `-fno-strict-aliasing`, etc). And there are equally good reasons why, in particular, these things are UB in C. But we are not trying to create a new C standard, so this is likely moot. > > Coming back to the particular case of mutating literals: if some part of a program mutates a literal, it can happen that in some unrelated library (even in a core library or in the core expander if these are coded in Scheme), a literal datum suddenly changes because of some late compiler pass that folds literals (which is desirable). The Scheme implementation has no way to control how much the other library will run berserk when assumed invariants suddenly no longer hold. > > If an implementation cannot guarantee that attempting to mutate a constant will not compromise the implementation in such an extreme way, then it shouldn't fold mutable constants. Or at the very least it should have some flag that disables doing such a thing. Try to formulate precisely where the boundary of acceptable behaviour is. > > I think we should specify the right thing here and give implementations an incentive to move towards a better world. > > The whole issue here is that [the right thing](https://codeberg.org/scheme/r7rs/src/branch/main/ballot-results/dpk/2025-03-stygian-blue-reddish-green-report.md#1-should-r7rs-large-require-implementations-to-enforce-the-immutability-of-literal-values) (well, what I think is the right thing) was voted down. Many of the ["no" votes](https://codeberg.org/scheme/r7rs/src/branch/main/ballot-results/dpk/2025-03-stygian-blue-reddish-green.csv) cited implementation burden as a reason. It is likely implementation-laziness... Anyway, if we follow option 2, it will make life easier for implementers, compared to option 1. > > (And option 2 is not that bad; later reports can further restrict it to 1.) > > Removing something from the Report is a lot harder than adding it. Here, it is the other way round because we would not be removing options offered to the user, but rather those offered to the implementation. In fact, this is what the difference between a report and its successor usually amounts to if we have backward compatibility. 😃
Author
Member
Copy link

Try to formulate precisely where the boundary of acceptable behaviour is.

If I could do so, I wouldn't label it undefined behavior. And even if I could formulate some precise definition of what I think is acceptable/unacceptable, its clear other people have different tradeoffs for this. (Otherwise, they would have voted for what I voted for.) So elaborating my idea on what a reasonable implementation does doesn't help us here.

Here, it is the other way round because we would not be removing options offered to the user, but rather those offered to the implementation. In fact, this is what the difference between a report and its successor usually amounts to if we have backward compatibility. 😃

If we offer an alternative, motiviated by the fact that implementations don't want to change their behavior, then in the future they will stick with the lazy option and not move over. That's the issue. (I don't see why there's any difference between the user and the implementation here. We offer two disjoint semantics, so a user can write code assuming one or the other.)

And there are equally good reasons why, in particular, these things are UB in C.

But we are not trying to create a new C standard, so this is likely moot.

I don't think the reasons are moot. For example, one extension (on x86_64) is to make signed integer overflow twos-compliment overflow (-fwrapv). But some platforms might make signed integer overflow saturate. Or trap. Instead of exhaustively enumerating what implementations can do (which is not necessarily complete) the standard leaves it undefined for implementations to implement according to the needs and requirements of their users.

> Try to formulate precisely where the boundary of acceptable behaviour is. If I could do so, I wouldn't label it undefined behavior. And even if I could formulate some precise definition of what I think is acceptable/unacceptable, its clear other people have different tradeoffs for this. (Otherwise, they would have voted for what I voted for.) So elaborating my idea on what a reasonable implementation does doesn't help us here. > Here, it is the other way round because we would not be removing options offered to the user, but rather those offered to the implementation. In fact, this is what the difference between a report and its successor usually amounts to if we have backward compatibility. 😃 If we offer an alternative, motiviated by the fact that implementations don't want to change their behavior, then in the future they will stick with the lazy option and not move over. That's the issue. (I don't see why there's any difference between the user and the implementation here. We offer two disjoint semantics, so a user can write code assuming one or the other.) > And there are equally good reasons why, in particular, these things are UB in C. > But we are not trying to create a new C standard, so this is likely moot. I don't think the reasons are moot. For example, one extension (on x86_64) is to make signed integer overflow twos-compliment overflow (`-fwrapv`). But some platforms might make signed integer overflow saturate. Or trap. Instead of exhaustively enumerating what implementations can do (which is not necessarily complete) the standard leaves it undefined for implementations to implement according to the needs and requirements of their users.
Owner
Copy link

Continuing the discussion of expunging UB entirely in #375

Continuing the discussion of expunging UB entirely in #375
Sign in to join this conversation.
No Branch/Tag specified
main
pages
proc-draft
c-trichotomy
global-id-prop-ref
allow-van-tonder-like-expanders
No results found.
Labels
Clear labels
Batteries
Issues for the Batteries volume of the report, providing a standard library
Cleanup
Issues where inconsistencies or other infelicities have arisen
Consent Docket
Items which will be automatically be given a certain resolution if no objection is made by a certain date
Environments
Issues for the Environments volume of the report, providing OS interfaces and similar
Foundations
Issues for the Foundations volume of the report, related to the core language semantics
Macrological Fascicle
Meta
Procedural and organisational issues
Multiple proposals exist
Multiple competing, concrete proposals to resolve this issue exist (whether they are SRFI or still only pre-SRFI)
Pie-in-the-sky ideas
Ideas currently lacking any form of worked-out proposal
Problems without solutions
Problems we *must* solve but haven’t worked out any solution for yet
Procedural Fascicle
Public Comment
Publications
Issues related to the practicalities of making the report available
Question
For information and clarification
R6RS Compatibility
Issue touches on questions of R6RS compatibility
R7RS-small
Issues with the Small language Report
Resolution in draft
A resolution (not necessarily final) is specified in the current draft of the relevant fascicle
Specification exists
Proposals for which a viable, but incomplete specification text exists
SRFI/RnRS spec exists
A SRFI for this proposal exists, or a solution from another RnRS
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
6 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
scheme/r7rs#373
Reference in a new issue
scheme/r7rs
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?