• Re: Optimization by programmers

    From Tim Rentsch@[email protected] to comp.arch on Tue Sep 29 15:44:56 2026
    From Newsgroup: comp.arch

    Michael S <[email protected]> writes:

    On Mon, 28 Sep 2026 02:59:06 -0700
    Tim Rentsch <[email protected]> wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index
    variables.

    In Rust, the only acceptable array index is of type "usize", which
    is effectively the same as u64 on most platforms these days, but
    does not need to be so: On a 32-bit target, it would be the same
    as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    That tells me a lot. Thank you.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Sep 30 13:57:24 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    [snip]
    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    Beginning Rust programmers tend to go through a few phases,
    particularly if coming from a language like C. "Fighting the
    Borrow Checker" is well-known; "traits are awesome; let's use
    them everywhere!" is another.

    When I started programming in Rust, most of my immediate
    colleagues and I had terribly little experience in the language.
    We were excited about the its potential benefits, but realized
    it would take a few months to build up a sufficient level of
    familiarity to use it skillfully. One quipped, "this language
    has a near-vertical learning curve."

    At one point a coworker (self-deprecatingly) said, "look, I'm
    writing crust! C in Rust syntax!" Everyone thought this was a
    great play on words, but he was correct: the programs we were
    writing were essentially C programs, just expressed in Rust. We
    quickly adopted that to describe our still naive code: it wasn't
    an insult, just an expression that we were still coming to terms
    with a new way of doing things. Some of the hallmarks of that
    included using the wrong paradigms (and types!) for lots of our
    code.

    I'd be curious to know more about your program. I've written
    several hundred kloc of Rust at this point, and while what I
    write may be terribly specialized, I rarely find myself indexing
    an array; often, I'm using iterators or some other abstraction
    for that kind of thing. If that was the one program you have
    written in Rust, my _guess_ is that you were using paradigms
    appropriate to a different language, which is why you felt you
    needed to reach for casts to index an array so frequently.

    Again, that's not a comment on your program, so much as
    suggesting an impedence mismatch between what you wrote and how
    one would generally expect to use the language.

    In general, I think a person's experience writing a single
    program tells us very little.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Michael S@[email protected] to comp.arch on Wed Sep 30 17:50:06 2026
    From Newsgroup: comp.arch

    On Wed, 30 Sep 2026 13:57:24 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    [snip]
    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    Beginning Rust programmers tend to go through a few phases,
    particularly if coming from a language like C. "Fighting the
    Borrow Checker" is well-known; "traits are awesome; let's use
    them everywhere!" is another.


    I didn't reach the second stage. Not sure about first.

    When I started programming in Rust, most of my immediate
    colleagues and I had terribly little experience in the language.
    We were excited about the its potential benefits, but realized
    it would take a few months to build up a sufficient level of
    familiarity to use it skillfully. One quipped, "this language
    has a near-vertical learning curve."


    IMHO, he is correct.
    Rust is certainly the hardest to learn out of computer languges that I
    ever tried that I would not call idiotic.

    At one point a coworker (self-deprecatingly) said, "look, I'm
    writing crust! C in Rust syntax!" Everyone thought this was a
    great play on words, but he was correct: the programs we were
    writing were essentially C programs, just expressed in Rust. We
    quickly adopted that to describe our still naive code: it wasn't
    an insult, just an expression that we were still coming to terms
    with a new way of doing things. Some of the hallmarks of that
    included using the wrong paradigms (and types!) for lots of our
    code.

    I'd be curious to know more about your program.

    The program was an implementation scalable flood fill algorithm (i.e.
    one with much better than typical worst case BigO properties, both for
    speed and for space) together with test benches for correctness and
    speed. As you could guess, I didn't really wrote it in Rust. It was a non-literal port from C. The purpose of translation was two-fold:
    improvement of my knowledge of Rust and getting better feel for cost of mandatory bound checking for one particular sort of data structures.
    Before doing Rust port I did port to Golang. Unlike for Rust, where I
    only read The Book and went trough part of tutorial, I had small
    previous practical experience in Go, but it was not very recent. Still,
    the difference in my productivity was huge. In favor of Go, of course.

    I've written
    several hundred kloc of Rust at this point, and while what I
    write may be terribly specialized, I rarely find myself indexing
    an array; often, I'm using iterators or some other abstraction
    for that kind of thing. If that was the one program you have
    written in Rust, my _guess_ is that you were using paradigms
    appropriate to a different language, which is why you felt you
    needed to reach for casts to index an array so frequently.

    Again, that's not a comment on your program, so much as
    suggesting an impedence mismatch between what you wrote and how
    one would generally expect to use the language.

    In general, I think a person's experience writing a single
    program tells us very little.


    I never pretended otherwise. Although I don't see how one can avoid
    cast if one wants to use indices that originate in signed domain. Or,
    as was often a case in this particular program, in domain of narrower
    unsigned integers.

    - Dan C.



    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Wed Sep 30 18:03:46 2026
    From Newsgroup: comp.arch

    Dan Cross wrote:
    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    [snip]
    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    Beginning Rust programmers tend to go through a few phases,
    particularly if coming from a language like C. "Fighting the
    Borrow Checker" is well-known; "traits are awesome; let's use
    them everywhere!" is another.

    When I started programming in Rust, most of my immediate
    colleagues and I had terribly little experience in the language.
    We were excited about the its potential benefits, but realized
    it would take a few months to build up a sufficient level of
    familiarity to use it skillfully. One quipped, "this language
    has a near-vertical learning curve."

    At one point a coworker (self-deprecatingly) said, "look, I'm
    writing crust! C in Rust syntax!" Everyone thought this was a
    great play on words, but he was correct: the programs we were
    writing were essentially C programs, just expressed in Rust. We
    quickly adopted that to describe our still naive code: it wasn't
    an insult, just an expression that we were still coming to terms
    with a new way of doing things. Some of the hallmarks of that
    included using the wrong paradigms (and types!) for lots of our
    code.

    I have definitely noted this in my own code, crust is a good name for it.

    Quite often, when "fighting the borrow checker", typically because I'm updating a complicated data structure in two locations at the same time,
    I'll fall back to "write Fortran in Rust", with every access indexed.

    This is BTW one of those problems the brand new upcoming borrow checker
    is supposed to be far better at figuring out: If I'm updating totally
    separate parts of the same larger structure, then it should be OK to
    mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this type of
    issue, they just don't come naturally to me yet.

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Wed Sep 30 17:49:34 2026
    From Newsgroup: comp.arch


    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    [snip]
    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    Beginning Rust programmers tend to go through a few phases,
    particularly if coming from a language like C. "Fighting the
    Borrow Checker" is well-known; "traits are awesome; let's use
    them everywhere!" is another.

    When I started programming in Rust, most of my immediate
    colleagues and I had terribly little experience in the language.
    We were excited about the its potential benefits, but realized
    it would take a few months to build up a sufficient level of
    familiarity to use it skillfully. One quipped, "this language
    has a near-vertical learning curve."

    At one point a coworker (self-deprecatingly) said, "look, I'm
    writing crust! C in Rust syntax!" Everyone thought this was a
    great play on words, but he was correct: the programs we were
    writing were essentially C programs, just expressed in Rust. We
    quickly adopted that to describe our still naive code: it wasn't
    an insult, just an expression that we were still coming to terms
    with a new way of doing things. Some of the hallmarks of that
    included using the wrong paradigms (and types!) for lots of our
    code.

    I have definitely noted this in my own code, crust is a good name for it.

    Quite often, when "fighting the borrow checker", typically because I'm updating a complicated data structure in two locations at the same time, I'll fall back to "write Fortran in Rust", with every access indexed.

    Wouldn't that be FRustrating ?!?

    This is BTW one of those problems the brand new upcoming borrow checker
    is supposed to be far better at figuring out: If I'm updating totally separate parts of the same larger structure, then it should be OK to
    mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this type of
    issue, they just don't come naturally to me yet.

    Terje

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Thu Oct 1 06:58:09 2026
    From Newsgroup: comp.arch

    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Thu Oct 1 07:14:06 2026
    From Newsgroup: comp.arch

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables.

    In Rust, the only acceptable array index is of type "usize", which is
    effectively the same as u64 on most platforms these days, but does not
    need to be so: On a 32-bit target, it would be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial underpinnings of Rust safety. Even more importantly, all such indices
    are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    But given that the checks are put in by the compiler, it seems
    rather silly to insist that index expressions have type usize,
    especially when a common remedy is just to add a cast. It's like
    saying the car won't start unless you take off your seatbelt.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From antispam@[email protected] (Waldek Hebisch) to comp.arch on Fri Oct 2 04:10:15 2026
    From Newsgroup: comp.arch

    Stephen Fuld <[email protected]d> wrote:

    It is obvious that you are right; the the existing choices as documented
    in the standard and your post above (thanks for that) don't permit what
    I want. I apologize for not recognizing that up front. But I maintain
    that there should be a category that essentially says "You can do what
    you want, but you must document what you did." Furthermore, if such a choice existed, most instances of undefined should be "reclassified"
    into that new choice. I think that would aid programmers understanding.

    In case of gcc 'ureachable()' can be given simple definition:
    optimizer will assume that 'ureachable()' will never be
    executed and do program transformations based on that knowledge.

    Consder the following program fragment:

    void foo1(int i) {
    if (i < 0) {
    unreachable();
    }
    foo2(i);
    }

    void foo2(int i) {
    if (i < 0) {
    foo3(i);
    } else {
    foo4(i);
    }
    }

    In foo1 compiler could infer that 'i >= 0'. It could also inline
    'foo2' into 'foo1'. After inlining it could infer that 'foo4'
    will be called. So compiler could transform 'foo1' into

    void foo1(int i) {
    foo4(i);
    }

    If ever 'foo1' gets called with negative argument you would get
    call to 'foo4' with negative argument.

    Some people have mental model that C is compiled on statement by
    statement basis. In compiler that is doing simple statement by
    statement compilation 'if' in 'foo2' would prevent call to
    'foo4' with negative argument. If compiler simply replaced
    'unreachable()' by a trap instruction, then program would
    trap in ;foo1'. But undefined behaviour opens essentially
    unlimied possibilites for program transformation, based on
    surrouding code.

    BTW: I can image code using a macro

    #define ASSERT_FACT(x) {if (x) unreachable();}

    and codebase sprinkled with statements like:

    ASSERT_FACT(i >= 0 && i < 100)

    So 'unreachable()' essentially allows passing arbitrary assertions
    to the compiler.

    Let me add that given enough facts compiler should be able to
    prove that all of them are true. It would be nice to have an
    option so that compiler warns if it can not prove that some
    'unreachable()' is really unreachable.

    And note that false statement implies everthing, so if your
    'unreachable()' is wrong then there are unlimited possibilities
    for unexpected behaviour.
    --
    Waldek Hebisch
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From antispam@[email protected] (Waldek Hebisch) to comp.arch on Fri Oct 2 05:31:48 2026
    From Newsgroup: comp.arch

    David Brown <[email protected]> wrote:
    On 26/09/2026 16:25, Stefan Monnier wrote:
    David Brown [2026-09-25 19:00:26] wrote:

    A halfway point might be for the compiler to emit a warning when it
    can't show that `unreachable()` is indeed unreachable.

    I can certainly agree that the compiler could give a warning if the unreachable() is definitely reached. And if compiler writers can figure
    out some decent heuristics about "unreachable()" being inevitably
    reached after some useful code path, then a warning would be nice.
    (That can't be given specifications in the standards, because you could
    not define clear semantics here - but it's fine to have optional
    compiler warnings on things that appear to be wrong according to some
    kind of pattern recognition.)

    But it would be virtually useless to have a warning any time the
    compiler cannot prove that the "unreachable()" is indeed unreachable.
    The whole point of its use is when the programmer knows more than the compiler, and is giving the compiler additional information. If the compiler knows that point in the code cannot be reached, then it already knows it is unreachable, and will already optimise on the assumption
    that the point is not reached!

    I do not think that such warning would be useful to Stefan. But
    that does not make it useless. Namely, difficultly of proof
    decrease sharply if you have enough knowledge. Namely, for
    seqential code current gcc is reasonably adept at propagating
    conditions. It can recognize _some_ looping patterns. But
    in general for loops and recursion one needs appropriate
    invariants and variants. It is hard to automatically
    infer them. But with enough programmer supplied information
    compiler in principle could verify its correctness by
    simple sequential-like reasoning. Failure to verify
    information provided by programmer indicates that this
    info is either wrong or incomplete. Both are reasons to
    change it, either by removing info that failed to verify
    or adding more info in hope that this will lead to
    successful proof.

    [ I generally like the idea of programmers being able to request
    specific optimizations and to be warned by the compiler when those
    requests can't be satisfied. So we don't have to go dig in the
    assembly output to confirm whether the compiler did the right thing or
    not. ]


    I agree on that principle - I'm a big fan of warnings when my code
    appears to be wrong. But this is not a situation where warnings would
    be appropriate.

    Lisp compiler 'sbcl' at high optimization level emits a lot of
    warnings about constructs that it can not optimize, giving
    information that is needed to enable optimization. In C similar
    warnings are less needed, because basic C construct already have low
    level ("optimized") form. But C programmers may expect some
    optimizations and it would be good for compiler to report if
    optimizations did not apply.

    Of course, most people using '-Werror' does not want optimization
    warning to halt the build. So clearly they should go in different
    category than normal warnings.
    --
    Waldek Hebisch
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Fri Oct 2 10:05:59 2026
    From Newsgroup: comp.arch

    On 02/10/2026 06:10, Waldek Hebisch wrote:
    Stephen Fuld <[email protected]d> wrote:

    It is obvious that you are right; the the existing choices as documented
    in the standard and your post above (thanks for that) don't permit what
    I want. I apologize for not recognizing that up front. But I maintain
    that there should be a category that essentially says "You can do what
    you want, but you must document what you did." Furthermore, if such a
    choice existed, most instances of undefined should be "reclassified"
    into that new choice. I think that would aid programmers understanding.

    In case of gcc 'ureachable()' can be given simple definition:
    optimizer will assume that 'ureachable()' will never be
    executed and do program transformations based on that knowledge.


    Yes.

    That is equivalent to saying that if the execution reaches
    "unreachable()", the behaviour is undefined. In C, it is the same
    thing. (Some people don't seem to like that, and think that "undefined behaviour" should really mean "something vague and nebulous - I can't
    explain it to other people, never mind to a compiler, but I am sure I
    know what I mean".)

    In gcc and clang, the function "unreachable()" is the same as __builtin_unreachable(), and is translated directly by the front-end
    into the "UB" code in the internal intermediary language of the compiler.


    Consder the following program fragment:

    void foo1(int i) {
    if (i < 0) {
    unreachable();
    }
    foo2(i);
    }

    void foo2(int i) {
    if (i < 0) {
    foo3(i);
    } else {
    foo4(i);
    }
    }

    In foo1 compiler could infer that 'i >= 0'. It could also inline
    'foo2' into 'foo1'. After inlining it could infer that 'foo4'
    will be called. So compiler could transform 'foo1' into

    void foo1(int i) {
    foo4(i);
    }

    If ever 'foo1' gets called with negative argument you would get
    call to 'foo4' with negative argument.

    Yes, that is exactly the kind of thing "unreachable()" is useful for.


    Some people have mental model that C is compiled on statement by
    statement basis. In compiler that is doing simple statement by
    statement compilation 'if' in 'foo2' would prevent call to
    'foo4' with negative argument. If compiler simply replaced
    'unreachable()' by a trap instruction, then program would
    trap in ;foo1'. But undefined behaviour opens essentially
    unlimied possibilites for program transformation, based on
    surrouding code.

    I think it is quite common to wrap "unreachable()" in a macro so that
    you can quickly make such a change before rebuilding your code.
    Something like :

    #ifdef DEBUGGING
    #define Unreachable() __builtin_trap()
    #else
    #define Unreachable() unreachable()
    #endif



    BTW: I can image code using a macro

    #define ASSERT_FACT(x) {if (x) unreachable();}

    Or maybe even :

    #define ASSERT_FACT(x) {if (!(x)) unreachable();}


    Again, support for compile-time choices of run-time checks can be
    useful. And you can get a compile-time error if the compiler is able to
    see that the asserted "fact" is never true.

    extern void __attribute__((error("Alternative fact asserted"))) assertFactFailed(void);

    #define Assert_fact(x) \
    do { \
    if (__builtin_constant_p(!(x))) { \
    if (!(x)) { \
    assertFactFailed(); \
    } \
    } \
    #ifdef DEBUGGING
    if (!(x)) __builtin_trap(); \
    #else
    if (!(x)) __builtin_unreachable(); \
    #endif
    } while (0)



    and codebase sprinkled with statements like:

    ASSERT_FACT(i >= 0 && i < 100)

    So 'unreachable()' essentially allows passing arbitrary assertions
    to the compiler.

    C++23 also has the [[assume(x)]] attribute doing exactly that, but it
    has not made it into C as yet.


    Let me add that given enough facts compiler should be able to
    prove that all of them are true. It would be nice to have an
    option so that compiler warns if it can not prove that some
    'unreachable()' is really unreachable.


    I disagree on that one - or at least, I am very sceptical. You add an "unreachable()" to the code precisely because you know more than the
    compiler can realistically know. If the compiler can figure out that
    "(i >= 0 && i < 100)", then there would be no need to tell it.

    But I suppose you might still use it to help inform other programmers,
    or to silence some warnings (like missing return values on code paths
    that are never reached).

    extern void __attribute__((error("Alternative fact confirmed"))) confirmIncorrectFact(void);

    extern void __attribute__((error("Fact not confirmed"))) confirmFactFailed(void);

    #define Confirm_fact(x) \
    do { \
    if (__builtin_constant_p(!(x))) { \
    if (!(x)) { \
    confirmIncorrectFact(); \
    } \
    } else { \
    confirmFactFailed(); \
    } \
    } while (0)

    Note that whether or not the compiler can confirm a claimed fact depends
    on things like optimisation flags - that makes it all a bit fragile.
    But you might find use for it.

    (Of course __builtin_constant_p() is not portable beyond gcc, clang, and perhaps a few other compilers.)

    And note that false statement implies everthing, so if your
    'unreachable()' is wrong then there are unlimited possibilities
    for unexpected behaviour.


    Yes.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Fri Oct 2 20:38:00 2026
    From Newsgroup: comp.arch

    Tim Rentsch wrote:
    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.

    My personal Rust journey has been driven by a desire to match or beat
    the fastest known times for the Advent of Code puzzles, from 2015 to 2025.

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Fri Oct 2 21:23:03 2026
    From Newsgroup: comp.arch

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.

    My personal Rust journey has been driven by a desire to match or
    beat the fastest known times for the Advent of Code puzzles, from
    2015 to 2025.

    That's good to know but what is a good resource to learn the Rust
    language? I've looked at the Rust book several times and each
    time come away rather frustrated.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Fri Oct 2 22:04:43 2026
    From Newsgroup: comp.arch

    [email protected] (Waldek Hebisch) writes:

    Stephen Fuld <[email protected]d> wrote:

    It is obvious that you are right; the the existing choices as
    documented in the standard and your post above (thanks for that)
    don't permit what I want. I apologize for not recognizing that up
    front. But I maintain that there should be a category that
    essentially says "You can do what you want, but you must document
    what you did." Furthermore, if such a choice existed, most
    instances of undefined should be "reclassified" into that new
    choice. I think that would aid programmers understanding.

    In case of gcc 'ureachable()' can be given simple definition:
    optimizer will assume that 'ureachable()' will never be
    executed and do program transformations based on that knowledge.

    Consder the following program fragment:

    void foo1(int i) {
    if (i < 0) {
    unreachable();
    }
    foo2(i);
    }

    void foo2(int i) {
    if (i < 0) {
    foo3(i);
    } else {
    foo4(i);
    }
    }

    In foo1 compiler could infer that 'i >= 0'. It could also inline
    'foo2' into 'foo1'. After inlining it could infer that 'foo4'
    will be called. So compiler could transform 'foo1' into

    void foo1(int i) {
    foo4(i);
    }

    If ever 'foo1' gets called with negative argument you would get
    call to 'foo4' with negative argument.

    Some people have mental model that C is compiled on statement by
    statement basis. In compiler that is doing simple statement by
    statement compilation 'if' in 'foo2' would prevent call to
    'foo4' with negative argument. If compiler simply replaced
    'unreachable()' by a trap instruction, then program would
    trap in ;foo1'. But undefined behaviour opens essentially
    unlimied possibilites for program transformation, based on
    surrouding code.

    BTW: I can image code using a macro

    #define ASSERT_FACT(x) {if (x) unreachable();}

    and codebase sprinkled with statements like:

    ASSERT_FACT(i >= 0 && i < 100)

    So 'unreachable()' essentially allows passing arbitrary assertions
    to the compiler.

    Let me add that given enough facts compiler should be able to
    prove that all of them are true. It would be nice to have an
    option so that compiler warns if it can not prove that some
    'unreachable()' is really unreachable.

    And note that false statement implies everthing, so if your
    'unreachable()' is wrong then there are unlimited possibilities
    for unexpected behaviour.

    How is this reasoning affected by the change in definition of
    undefined behavior in C23 (as compared for example with the
    definitions in C11 or C99)?
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From antispam@[email protected] (Waldek Hebisch) to comp.arch on Sat Oct 3 05:28:42 2026
    From Newsgroup: comp.arch

    Tim Rentsch <[email protected]> wrote:
    Thomas Koenig <[email protected]> writes:

    Bubblesort is the worst of non-joke sorting algorithms, see
    the quote from Numerical Recipes...

    For people who don't like bubble sort, or for inclusion in a set
    of benchmarks, I suggest the following recently discovered
    sorting algorithm (written in C-ish pseudocode):

    void
    baffle_sort( unsigned n, int *elements ){
    for( unsigned i = 0; i < n; i++ ){
    for( unsigned j = 0; j < n; j++ ){
    if( elements[i] < elements[j] ){
    /* exchange elements[i] and elements[j] */
    }
    }
    }
    }

    (Disclaimer: not original.)

    AFAICS this is very old "sorting by choice" algortim, just written
    in inefficient way (using swaps and keeping running max in the
    array instead of using extra variable).
    --
    Waldek Hebisch
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Sat Oct 3 11:12:14 2026
    From Newsgroup: comp.arch

    Tim Rentsch <[email protected]> writes:
    That's good to know but what is a good resource to learn the Rust
    language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    If you mean "The Rust Programming Language", I came away from it
    disillusioned.

    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker.

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference
    counting after all. However, the problem in this case is the usual
    thing with type inference: There is so much going on that happens
    automagically (in particular automatic dereferencing in Rust), and the
    Rust compiler does not know where you went wrong, so the error message
    is unhelpful (while for the simpler cases the compiler usually guesses
    right and gives very helpful, to-the-point error messages).

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Sat Oct 3 04:38:18 2026
    From Newsgroup: comp.arch

    [email protected] (Waldek Hebisch) writes:

    Tim Rentsch <[email protected]> wrote:

    Thomas Koenig <[email protected]> writes:

    Bubblesort is the worst of non-joke sorting algorithms, see
    the quote from Numerical Recipes...

    For people who don't like bubble sort, or for inclusion in a set
    of benchmarks, I suggest the following recently discovered
    sorting algorithm (written in C-ish pseudocode):

    void
    baffle_sort( unsigned n, int *elements ){
    for( unsigned i = 0; i < n; i++ ){
    for( unsigned j = 0; j < n; j++ ){
    if( elements[i] < elements[j] ){
    /* exchange elements[i] and elements[j] */
    }
    }
    }
    }

    (Disclaimer: not original.)

    AFAICS this is very old "sorting by choice" algortim,

    I suggest you look again. It is not a selection sort.

    just written
    in inefficient way (using swaps and keeping running max in the
    array instead of using extra variable).

    I am making no claim that this sort would be a good choice if
    one wanted an efficient algorithm. Surely no one would choose
    it on that basis. That's not what makes it interesting.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Sat Oct 3 14:25:05 2026
    From Newsgroup: comp.arch

    Tim Rentsch wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.

    My personal Rust journey has been driven by a desire to match or
    beat the fastest known times for the Advent of Code puzzles, from
    2015 to 2025.

    That's good to know but what is a good resource to learn the Rust
    language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    I don't have any suggestions here, I tend to google any questions I
    have, then follow those links that point into the Rust std lib.

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From antispam@[email protected] (Waldek Hebisch) to comp.arch on Sat Oct 3 13:13:34 2026
    From Newsgroup: comp.arch

    Tim Rentsch <[email protected]> wrote:
    [email protected] (Waldek Hebisch) writes:

    Stephen Fuld <[email protected]d> wrote:

    It is obvious that you are right; the the existing choices as
    documented in the standard and your post above (thanks for that)
    don't permit what I want. I apologize for not recognizing that up
    front. But I maintain that there should be a category that
    essentially says "You can do what you want, but you must document
    what you did." Furthermore, if such a choice existed, most
    instances of undefined should be "reclassified" into that new
    choice. I think that would aid programmers understanding.

    In case of gcc 'ureachable()' can be given simple definition:
    optimizer will assume that 'ureachable()' will never be
    executed and do program transformations based on that knowledge.

    Consder the following program fragment:

    void foo1(int i) {
    if (i < 0) {
    unreachable();
    }
    foo2(i);
    }

    void foo2(int i) {
    if (i < 0) {
    foo3(i);
    } else {
    foo4(i);
    }
    }

    In foo1 compiler could infer that 'i >= 0'. It could also inline
    'foo2' into 'foo1'. After inlining it could infer that 'foo4'
    will be called. So compiler could transform 'foo1' into

    void foo1(int i) {
    foo4(i);
    }

    If ever 'foo1' gets called with negative argument you would get
    call to 'foo4' with negative argument.

    Some people have mental model that C is compiled on statement by
    statement basis. In compiler that is doing simple statement by
    statement compilation 'if' in 'foo2' would prevent call to
    'foo4' with negative argument. If compiler simply replaced
    'unreachable()' by a trap instruction, then program would
    trap in ;foo1'. But undefined behaviour opens essentially
    unlimied possibilites for program transformation, based on
    surrouding code.

    BTW: I can image code using a macro

    #define ASSERT_FACT(x) {if (x) unreachable();}

    and codebase sprinkled with statements like:

    ASSERT_FACT(i >= 0 && i < 100)

    So 'unreachable()' essentially allows passing arbitrary assertions
    to the compiler.

    Let me add that given enough facts compiler should be able to
    prove that all of them are true. It would be nice to have an
    option so that compiler warns if it can not prove that some
    'unreachable()' is really unreachable.

    And note that false statement implies everthing, so if your
    'unreachable()' is wrong then there are unlimited possibilities
    for unexpected behaviour.

    How is this reasoning affected by the change in definition of
    undefined behavior in C23 (as compared for example with the
    definitions in C11 or C99)?

    I am not aware of any change that would affect reasoning above.
    AFAICS wording defining undefined behaviour is essentially the
    same. But there are many scattered changes mentioning undefined
    behaviour, so I may have missed something important. Still,
    the reasong above is valid even with quite limited definition
    of undefined behaviour, namely that up to the point of
    encountering 'unreachable()' observable behaviour agrees with
    abstract machine. Once 'unreachable()' is encountered,
    behaviour of abstract machine is no longer defined, so
    program may continue doing anything. And if 'unreachable()'
    is not encountered, then propagating conditional info is
    valid. Of course, time travel gives additional possibilities
    for optimization, but the example above respects casuality.
    --
    Waldek Hebisch
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Sat Oct 3 21:08:28 2026
    From Newsgroup: comp.arch

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:
    [...]
    My personal Rust journey has been driven by a desire to match or
    beat the fastest known times for the Advent of Code puzzles, from
    2015 to 2025.

    That's good to know but what is a good resource to learn the Rust
    language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    I don't have any suggestions here, I tend to google any questions I
    have, then follow those links that point into the Rust std lib.

    Okay. Kind of disappointing but thank you for the followup.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From aph@[email protected] to comp.arch on Sun Oct 4 08:15:48 2026
    From Newsgroup: comp.arch

    David Brown <[email protected]> wrote:
    On 25/09/2026 07:39, Anton Ertl wrote:
    David Brown <[email protected]> writes:
    On 21/09/2026 19:17, Anton Ertl wrote:
    I referred to a specific paradoxical effect, not completely avoiding
    all of them: The effect of programmers who, instead of working on
    making the code faster through source-level changes, have to invest
    time into avoiding getting it miscompiled.


    By "miscompiled", do you mean incorrect object code, or object code that >>> did not have the performance the programmer expected or hoped for?

    Code that behaves differently than intended, e.g., where a bounds check
    is "optimized" away.


    The trouble with that is determining precisely what was intended.
    Compilers are limited - the programmer has to express their intention in
    the restricted form of the programming language (C or whatever else).
    And the /only/ things that can be relied on to have a clear meaning are things that have clear defined behaviour as documented by the language standards or the compiler documentation.

    A program is a contract between the programmer and the compiler - the programmer promises to provide good code with a clearly defined meaning,
    and the compiler promises to give object code that implements that
    defined meaning. If the programmer breaks their side of the contract by writing code that does not have a clear meaning (at compile-time or run-time), the compiler /cannot/ fulfil its side of the contract. It is unable to generate "correct" object code from incorrect source code.

    If the programmer writes "a = b + c;" (all signed int), what is the intention? Does the programmer mean "generate an ADD assembly
    instruction"? Does he/she mean "I don't care how it happens, but 'a'
    should be the sum of 'b' and 'c'"? Do they mean "give me a two's
    complement wrapped addition"? Do they mean "I promise there will be no overflow" ? Do they mean "I /hope/ there will be no overflow" ? Or
    "While I am debugging, let me know if there is an overflow" ?

    The programmer might have intended lots of subtly different things from
    the same bit of code. But compilers cannot work by mind-reading. The
    only thing that the /code/ intends is "Add these two numbers. I promise there will be no overflow". Whatever the programmer imagines, that is
    the intention of the code. The compiler's job is to follow that - and
    if the programmer breaks his promise, it is garbage in, garbage out.


    I find the whole concept of optimising based on "what the programmer intended", rather than what the code they wrote means, somewhat hypocritical. The idea that "a = b + c;" should "obviously" wrap on a
    two's complement machine, while "a = b + 10 + c - 10;" should
    "obviously" optimise away the constants is clearly contradictory. It is
    an attitude that is denying responsibility - claiming "the compiler
    broke my code" rather than understanding "my code had mistakes but previously gave the right results by luck".

    I am not underestimating the difficulty of writing correct code here, or
    the frustration and costs involved when the behaviour of real-world code changes with different compilers or flags. But it is critical that programmers understand the origin of problems in order to deal with the issues instead of passing the blame around. Baring compiler bugs (even
    my favourite compiler has one or two outstanding bugs), what you term "miscompiling" here is actually errors in the programmers' source code.

    The way to improve the situation is not by blaming compiler writers, or inventing some kind of vague notion of what programmers "intended" or
    "do what some old compiler did", or limiting optimisations in compilers.
    Instead, it is by improving compilers, run-time checkers and static analysis tools to help identify the errors in the source code, improving teaching of programming to reduce the risk of such errors, improving languages to support lower-risk constructs, changing to programming languages with different characteristics, etc. Indeed, I find that
    better compilers with more powerful optimisations means fewer bugs in my code because I can write the code in clearer ways and still get
    efficient results in the end.


    Performance regressions can be nasty for performance-sensitive code,
    and consume time for finding a workaround (if one can be found), but
    is a different thing.


    Agreed. And correctness trumps speed every time.

    But as
    for the problems you mention w.r.t UB, I think it's just the result of >>>>> poor semantics, for which I guess we (language semanticists) are partly >>>>> to blame: we have developed fairly good tools to design sane language >>>>> specs in general,

    I violently disagree. The C89 standard (just to name one) is a
    partial specification not because the original C standards people were >>>> poor at specifying semantics, but because given the differences
    between the compilers out there and the hardware out there, the
    easiest way to reach a consensus is to leave some parts unspecified.

    I have never spoken to the C standards committee or writers, either of
    current standard versions or the original C89 standard, or writers of
    pre-standard C specifications. So I cannot in any way claim to know
    their thoughts or motivations. I also have not seen any documentation
    that suggests what they might have thought about "optimisation on the
    assumption that undefined behaviour does not occur" - in either
    direction.

    When C89 was developed, the dominating C compiler in the Unix world
    was PCC. GCC came on the scene late in the game (1988), without such
    assumptions and produced a significant performance advantage. Only
    after several years of heavy gcc development assumptions like
    -fstrict-aliasing were introduced around gcc-2.7 (which was initially
    disabled by default);

    ("-fstrict-aliasing" is still not enabled by default - you have to
    actively enable at least "-O2".)

    I don't know when the assumption that signed
    integer operations do not overflow was introduced, and initially it
    could not be disabled (-fwrapv was added around gcc-3.4), so obviously
    by that time people had taken over the gcc project who had a different
    view about how undefined behaviour should be treated.

    I completely disagree that this is an "obvious" conclusion. While I
    cannot say anything for sure here, the explanation that comes first to
    my mind is that it was part of the natural progression of gcc towards
    more optimisations.

    "-fwrapv" would have been added as a result of complaints about
    problems with old code that made invalid assumptions that happened
    to "work" because older gcc did not have as many optimisations.

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it
    was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv"
    would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    In hindsight, though, it would have been easy enough to use unsigned
    arithmetic within GCC to support gcj.

    I see absolutely no reason to assume that anyone at any time thought
    such optimisations were not valid - they simply had not yet been
    implemented.

    I agree.

    Andrew.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Sun Oct 4 12:15:31 2026
    From Newsgroup: comp.arch

    On 04/10/2026 10:15, [email protected]d wrote:
    David Brown <[email protected]> wrote:
    On 25/09/2026 07:39, Anton Ertl wrote:

    <snip>

    I don't know when the assumption that signed
    integer operations do not overflow was introduced, and initially it
    could not be disabled (-fwrapv was added around gcc-3.4), so obviously
    by that time people had taken over the gcc project who had a different
    view about how undefined behaviour should be treated.

    I completely disagree that this is an "obvious" conclusion. While I
    cannot say anything for sure here, the explanation that comes first to
    my mind is that it was part of the natural progression of gcc towards
    more optimisations.

    "-fwrapv" would have been added as a result of complaints about
    problems with old code that made invalid assumptions that happened
    to "work" because older gcc did not have as many optimisations.

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it
    was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv"
    would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.


    Thanks for that historical information - I had no idea that the
    implementation of Java was behind that feature of gcc. There are plenty
    of features (compiler flags, and extensions) of gcc that came about as
    the result of support of languages other than C and C++. But I did not
    know about this one.

    In hindsight, though, it would have been easy enough to use unsigned arithmetic within GCC to support gcj.


    Yes - there's nothing that I can think of where the effect of "-fwrapv"
    could not be achieved with equal run-time efficiency by using unsigned
    types and some casts or other conversions. But if you need wrapping
    signed integer overflow semantics, then it is obviously neater in the
    source code to have it directly than to go via unsigned types.

    I see absolutely no reason to assume that anyone at any time thought
    such optimisations were not valid - they simply had not yet been
    implemented.

    I agree.

    Andrew.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Sun Oct 4 14:16:55 2026
    From Newsgroup: comp.arch

    [email protected]d <[email protected]d> schrieb:

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it
    was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv"
    would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    Linear congruential random number generators, in common use when
    the dinosaurs roamed the computer centers, depend on wraparound
    semantics. In gfortran, they stopped working at one time because
    gcc assuming that this would not happen. -fwrap is a workaround
    (without changing the source), but of course using the RANDOM_NUMBER
    intrinsic is better - it also uses a reasonable algorithm.

    So, some people should probably be grateful to you :-)
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Sun Oct 4 16:36:29 2026
    From Newsgroup: comp.arch

    On 04/10/2026 16:16, Thomas Koenig wrote:
    [email protected]d <[email protected]d> schrieb:

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it
    was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv"
    would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    Linear congruential random number generators, in common use when
    the dinosaurs roamed the computer centers, depend on wraparound
    semantics. In gfortran, they stopped working at one time because
    gcc assuming that this would not happen. -fwrap is a workaround
    (without changing the source), but of course using the RANDOM_NUMBER intrinsic is better - it also uses a reasonable algorithm.

    So, some people should probably be grateful to you :-)


    Do unsigned types in Fortran not have wrapping semantics? Or are there
    no unsigned integer types in Fortran? I know not every language has the
    same distinction as C here.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Sun Oct 4 17:46:26 2026
    From Newsgroup: comp.arch

    David Brown <[email protected]> schrieb:
    On 04/10/2026 16:16, Thomas Koenig wrote:
    [email protected]d <[email protected]d> schrieb:

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it
    was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv"
    would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    Linear congruential random number generators, in common use when
    the dinosaurs roamed the computer centers, depend on wraparound
    semantics. In gfortran, they stopped working at one time because
    gcc assuming that this would not happen. -fwrap is a workaround
    (without changing the source), but of course using the RANDOM_NUMBER
    intrinsic is better - it also uses a reasonable algorithm.

    So, some people should probably be grateful to you :-)


    Do unsigned types in Fortran not have wrapping semantics? Or are there
    no unsigned integer types in Fortran? I know not every language has the same distinction as C here.

    The Fortran language has no unsigned types. Two compilers, gfortran
    and flang, implement a suggestion that I made to J3 (much improved
    by some people on the committee) and was accepted, but not taken
    up by WG5. At the time, I had already started implementing it,
    so I went ahead and completed it anyway, see

    https://gcc.gnu.org/onlinedocs/gfortran/Unsigned-integers.html

    (You can probably tell that this is still a sore point.)

    But what the pseudo-random number generators did was use *signed*
    overflow, which had been prohibited by the standard forever, but
    because people needed something and it worked, they went ahead
    and did it anyway.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Sun Oct 4 20:04:49 2026
    From Newsgroup: comp.arch

    On 04/10/2026 19:46, Thomas Koenig wrote:
    David Brown <[email protected]> schrieb:
    On 04/10/2026 16:16, Thomas Koenig wrote:
    [email protected]d <[email protected]d> schrieb:

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it
    was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv"
    would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    Linear congruential random number generators, in common use when
    the dinosaurs roamed the computer centers, depend on wraparound
    semantics. In gfortran, they stopped working at one time because
    gcc assuming that this would not happen. -fwrap is a workaround
    (without changing the source), but of course using the RANDOM_NUMBER
    intrinsic is better - it also uses a reasonable algorithm.

    So, some people should probably be grateful to you :-)


    Do unsigned types in Fortran not have wrapping semantics? Or are there
    no unsigned integer types in Fortran? I know not every language has the
    same distinction as C here.

    The Fortran language has no unsigned types. Two compilers, gfortran
    and flang, implement a suggestion that I made to J3 (much improved
    by some people on the committee) and was accepted, but not taken
    up by WG5. At the time, I had already started implementing it,
    so I went ahead and completed it anyway, see

    https://gcc.gnu.org/onlinedocs/gfortran/Unsigned-integers.html

    (You can probably tell that this is still a sore point.)

    But what the pseudo-random number generators did was use *signed*
    overflow, which had been prohibited by the standard forever, but
    because people needed something and it worked, they went ahead
    and did it anyway.


    I would guess you'd have trouble writing something that was efficient
    here, and avoided UB when you don't have language support for wrapping overflow (whether it be for signed or unsigned types).

    Thanks for the explanation. I can't imagine I'll ever work with
    Fortran, but it's nice to know more about it anyway.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 5 11:36:59 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables. >>>>
    In Rust, the only acceptable array index is of type "usize", which is
    effectively the same as u64 on most platforms these days, but does not >>>> need to be so: On a 32-bit target, it would be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such indices
    are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.

    But given that the checks are put in by the compiler, it seems
    rather silly to insist that index expressions have type usize,
    especially when a common remedy is just to add a cast. It's like
    saying the car won't start unless you take off your seatbelt.

    I don't know why you would say that. The point is that Rust is
    strongly, statically typed and the type of an arrray index is
    well-defined as `usize`. Being intentional in what one writes
    is an important aspect of being a proficient Rust programmer.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 5 11:47:56 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    On Wed, 30 Sep 2026 13:57:24 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    [snip]
    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    Beginning Rust programmers tend to go through a few phases,
    particularly if coming from a language like C. "Fighting the
    Borrow Checker" is well-known; "traits are awesome; let's use
    them everywhere!" is another.

    I didn't reach the second stage. Not sure about first.

    When I started programming in Rust, most of my immediate
    colleagues and I had terribly little experience in the language.
    We were excited about the its potential benefits, but realized
    it would take a few months to build up a sufficient level of
    familiarity to use it skillfully. One quipped, "this language
    has a near-vertical learning curve."

    IMHO, he is correct.
    Rust is certainly the hardest to learn out of computer languges that I
    ever tried that I would not call idiotic.

    She, actually, but yes. I guess it depends on one's background,
    but in general if you are coming from many years of experience
    in languages like C or C++, Rust can be a real slog. I've seen
    some anecdotal reports that teaching it to programming neophytes
    as a first language is actually quite productive.

    At one point a coworker (self-deprecatingly) said, "look, I'm
    writing crust! C in Rust syntax!" Everyone thought this was a
    great play on words, but he was correct: the programs we were
    writing were essentially C programs, just expressed in Rust. We
    quickly adopted that to describe our still naive code: it wasn't
    an insult, just an expression that we were still coming to terms
    with a new way of doing things. Some of the hallmarks of that
    included using the wrong paradigms (and types!) for lots of our
    code.

    I'd be curious to know more about your program.

    The program was an implementation scalable flood fill algorithm (i.e.
    one with much better than typical worst case BigO properties, both for
    speed and for space) together with test benches for correctness and
    speed. As you could guess, I didn't really wrote it in Rust. It was a >non-literal port from C. The purpose of translation was two-fold:
    improvement of my knowledge of Rust and getting better feel for cost of >mandatory bound checking for one particular sort of data structures.

    So mandatory bounds checking will usually be compiled out in
    optimized builds; you can choose to keep it in, but most don't.

    Before doing Rust port I did port to Golang. Unlike for Rust, where I
    only read The Book and went trough part of tutorial, I had small
    previous practical experience in Go, but it was not very recent. Still,
    the difference in my productivity was huge. In favor of Go, of course.

    Indeed. Sometimes I find Go far more productive, and other times
    Rust. It really depends on the category of problem I am working
    on.

    I've written
    several hundred kloc of Rust at this point, and while what I
    write may be terribly specialized, I rarely find myself indexing
    an array; often, I'm using iterators or some other abstraction
    for that kind of thing. If that was the one program you have
    written in Rust, my _guess_ is that you were using paradigms
    appropriate to a different language, which is why you felt you
    needed to reach for casts to index an array so frequently.

    Again, that's not a comment on your program, so much as
    suggesting an impedence mismatch between what you wrote and how
    one would generally expect to use the language.

    In general, I think a person's experience writing a single
    program tells us very little.

    I never pretended otherwise.

    No I know, but at least one other reader appeared to.

    Although I don't see how one can avoid
    cast if one wants to use indices that originate in signed domain. Or,
    as was often a case in this particular program, in domain of narrower >unsigned integers.

    Well, for narrower to wider unsigned types, there is `into()`.
    Depending on what you were using for the underlying collection,
    there is also the `std::ops::Index` trait that you could `impl`
    for index types other than `usize`, and container types other
    than primitive arrays and slices.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 5 12:29:00 2026
    From Newsgroup: comp.arch

    In article <119jbt5$cn3a$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    [snip]
    My only Rust program was full of casts of various integer types to
    usize. Exactly for the purpose of array indexing.

    Beginning Rust programmers tend to go through a few phases,
    particularly if coming from a language like C. "Fighting the
    Borrow Checker" is well-known; "traits are awesome; let's use
    them everywhere!" is another.

    When I started programming in Rust, most of my immediate
    colleagues and I had terribly little experience in the language.
    We were excited about the its potential benefits, but realized
    it would take a few months to build up a sufficient level of
    familiarity to use it skillfully. One quipped, "this language
    has a near-vertical learning curve."

    At one point a coworker (self-deprecatingly) said, "look, I'm
    writing crust! C in Rust syntax!" Everyone thought this was a
    great play on words, but he was correct: the programs we were
    writing were essentially C programs, just expressed in Rust. We
    quickly adopted that to describe our still naive code: it wasn't
    an insult, just an expression that we were still coming to terms
    with a new way of doing things. Some of the hallmarks of that
    included using the wrong paradigms (and types!) for lots of our
    code.

    I have definitely noted this in my own code, crust is a good name for it.

    Quite often, when "fighting the borrow checker", typically because I'm >updating a complicated data structure in two locations at the same time, >I'll fall back to "write Fortran in Rust", with every access indexed.

    I myself have done this on more than one occasion; it's a quick
    way to get going. When I wrote the rxv64 operating system, for
    instance, and got to the part where I was implementing a 6th Ed
    Unix-style buffer cache, and needed a doubly linked list, I
    ended up using an array as a proxy, with next and prev indices
    instead of pointers. The cache in simple systems is fixed sized
    anyway, so it wasn't a major imposition.

    This is BTW one of those problems the brand new upcoming borrow checker
    is supposed to be far better at figuring out: If I'm updating totally >separate parts of the same larger structure, then it should be OK to
    mutate both at the same time.

    Yes, Polonius (named for the character from Hamlet). It is now
    in Alpha in nightly rustc builds.

    https://blog.rust-lang.org/2026/08/04/enabling-polonius-alpha-on-nightly/

    I do realize that there are far more Rusty ways to solve this type of
    issue, they just don't come naturally to me yet.

    Things like Cell, RefCell, etc, supporting interior mutability
    are the usual solutions to things like that.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 5 12:29:52 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.

    Without knowing what methods you have tried, what books you have
    already explored, what you found dissatisfying about them, and
    why, it is impossible to give you a good answer.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 5 12:45:54 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    Tim Rentsch <[email protected]> writes:
    That's good to know but what is a good resource to learn the Rust
    language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    If you mean "The Rust Programming Language", I came away from it >disillusioned.

    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    I think what you mean to say is that you couldn't find a way to
    implement that in the manner you were accustomed to thinking
    about the problem, given your prior experience. But that is
    separate from whether it can be done or not.

    In fact, in Rust you can use the approximately the same
    techniques you would use in Pascal, C, or (probably) Forth: raw
    pointers are a part of the language, and you can simply use them
    with `unsafe` code, and it would be no more "unsafe" than in C
    or Pascal. One might argue that that defeats the purpose of the
    language, and you'd be right to a point. The difference is that
    in C or Pascal, the entire program is what Rust would call
    "unsafe", whereas generally this can be restricted to just a few
    lines of carefully audited code in Rust.

    FWIW, for things like containers of various kinds (trees, hash
    tables, linked-lists, growable arrays, etc), production programs
    usually use an existing library or crate instead of rolling
    their own.

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker.

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference >counting after all. However, the problem in this case is the usual
    thing with type inference: There is so much going on that happens >automagically (in particular automatic dereferencing in Rust), and the
    Rust compiler does not know where you went wrong, so the error message
    is unhelpful (while for the simpler cases the compiler usually guesses
    right and gives very helpful, to-the-point error messages).

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    Sounds like you're in what we'd call the, "fighting the borrow
    checker" stage. It may be that you end up really not liking
    Rust or feeling that other languages are better, but I wager
    that just about anyone who has seriously used the language has
    gone through similar things.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Niklas Holsti@[email protected] to comp.arch on Mon Oct 5 16:48:11 2026
    From Newsgroup: comp.arch

    On 2026-10-05 14:36, Dan Cross wrote:
    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables. >>>>>
    In Rust, the only acceptable array index is of type "usize", which is >>>>> effectively the same as u64 on most platforms these days, but does not >>>>> need to be so: On a 32-bit target, it would be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such indices
    are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.

    But given that the checks are put in by the compiler, it seems
    rather silly to insist that index expressions have type usize,
    especially when a common remedy is just to add a cast. It's like
    saying the car won't start unless you take off your seatbelt.

    I don't know why you would say that. The point is that Rust is
    strongly, statically typed and the type of an arrray index is
    well-defined as `usize`. Being intentional in what one writes
    is an important aspect of being a proficient Rust programmer.

    The Ada language and its relatives are, arguably, even more strongly
    typed in the array-indexing area, because different array types can have different index types, and the declaration of an array type also
    specifies the type of the index. When combined with range-constrained
    types and subtypes, this means that the bounds check can often be done statically, at compile time, and the remaining run-time checks (if any)
    are usually not significant for performance.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 5 13:55:07 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    [email protected] (Waldek Hebisch) writes:

    Tim Rentsch <[email protected]> wrote:

    Thomas Koenig <[email protected]> writes:

    Bubblesort is the worst of non-joke sorting algorithms, see
    the quote from Numerical Recipes...

    For people who don't like bubble sort, or for inclusion in a set
    of benchmarks, I suggest the following recently discovered
    sorting algorithm (written in C-ish pseudocode):

    void
    baffle_sort( unsigned n, int *elements ){
    for( unsigned i = 0; i < n; i++ ){
    for( unsigned j = 0; j < n; j++ ){
    if( elements[i] < elements[j] ){
    /* exchange elements[i] and elements[j] */
    }
    }
    }
    }

    (Disclaimer: not original.)

    AFAICS this is very old "sorting by choice" algortim,

    I suggest you look again. It is not a selection sort.

    just written
    in inefficient way (using swaps and keeping running max in the
    array instead of using extra variable).

    I am making no claim that this sort would be a good choice if
    one wanted an efficient algorithm. Surely no one would choose
    it on that basis. That's not what makes it interesting.

    This whole thread is probably better in something like
    comp.programming.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From antispam@[email protected] (Waldek Hebisch) to comp.arch on Mon Oct 5 22:54:41 2026
    From Newsgroup: comp.arch

    Tim Rentsch <[email protected]> wrote:
    [email protected] (Waldek Hebisch) writes:

    Tim Rentsch <[email protected]> wrote:

    Thomas Koenig <[email protected]> writes:

    Bubblesort is the worst of non-joke sorting algorithms, see
    the quote from Numerical Recipes...

    For people who don't like bubble sort, or for inclusion in a set
    of benchmarks, I suggest the following recently discovered
    sorting algorithm (written in C-ish pseudocode):

    void
    baffle_sort( unsigned n, int *elements ){
    for( unsigned i = 0; i < n; i++ ){
    for( unsigned j = 0; j < n; j++ ){
    if( elements[i] < elements[j] ){
    /* exchange elements[i] and elements[j] */
    }
    }
    }
    }

    (Disclaimer: not original.)

    AFAICS this is very old "sorting by choice" algortim,

    I suggest you look again. It is not a selection sort.

    Yes, you are right. First pass seclecting maximal element is
    like selection sort, the other passes work as insertion sort.

    just written
    in inefficient way (using swaps and keeping running max in the
    array instead of using extra variable).

    I am making no claim that this sort would be a good choice if
    one wanted an efficient algorithm. Surely no one would choose
    it on that basis. That's not what makes it interesting.

    Probably the most interestiong thing about it is that it actually
    sorts. As a benchmork it could be useful to assess quality of
    compiler removal of useless code. I suspect that currently no
    compiler is able to remove useless half of this code, also
    probably can not optimize sequence of swaps to array shift. But
    those are not far from what is alreade done.
    --
    Waldek Hebisch
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From antispam@[email protected] (Waldek Hebisch) to comp.arch on Tue Oct 6 02:17:57 2026
    From Newsgroup: comp.arch

    David Brown <[email protected]> wrote:
    On 04/10/2026 19:46, Thomas Koenig wrote:
    David Brown <[email protected]> schrieb:
    On 04/10/2026 16:16, Thomas Koenig wrote:
    [email protected]d <[email protected]d> schrieb:

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it >>>>> was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv" >>>>> would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    Linear congruential random number generators, in common use when
    the dinosaurs roamed the computer centers, depend on wraparound
    semantics. In gfortran, they stopped working at one time because
    gcc assuming that this would not happen. -fwrap is a workaround
    (without changing the source), but of course using the RANDOM_NUMBER
    intrinsic is better - it also uses a reasonable algorithm.

    So, some people should probably be grateful to you :-)


    Do unsigned types in Fortran not have wrapping semantics? Or are there
    no unsigned integer types in Fortran? I know not every language has the >>> same distinction as C here.

    The Fortran language has no unsigned types. Two compilers, gfortran
    and flang, implement a suggestion that I made to J3 (much improved
    by some people on the committee) and was accepted, but not taken
    up by WG5. At the time, I had already started implementing it,
    so I went ahead and completed it anyway, see

    https://gcc.gnu.org/onlinedocs/gfortran/Unsigned-integers.html

    (You can probably tell that this is still a sore point.)

    But what the pseudo-random number generators did was use *signed*
    overflow, which had been prohibited by the standard forever, but
    because people needed something and it worked, they went ahead
    and did it anyway.


    I would guess you'd have trouble writing something that was efficient
    here, and avoided UB when you don't have language support for wrapping overflow (whether it be for signed or unsigned types).

    If language supports large enough integers and with reasonable
    optimizer code like:

    bigint a = 123;
    bigint b = 7;
    bigint c = 17;
    long f() {
    a = 0xffffffffffffffff &(a*c + b);
    return a;
    }

    will be compiled using wrapping 64-bit arithmetic. If additionaly
    you make sure that a is local to random number module so compiler
    knows that it has seen all updates to a, and updates stay within
    64 bits than compiler can use 64-bit variables.

    But having explicit wrapping types is more direct and easier to
    implement, which mattered for dinosaurs.
    --
    Waldek Hebisch
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Tue Oct 6 06:35:05 2026
    From Newsgroup: comp.arch

    [email protected] (Waldek Hebisch) writes:
    Tim Rentsch <[email protected]> wrote:
    [email protected] (Waldek Hebisch) writes:

    Tim Rentsch <[email protected]> wrote:

    Thomas Koenig <[email protected]> writes:
    void
    baffle_sort( unsigned n, int *elements ){
    for( unsigned i = 0; i < n; i++ ){
    for( unsigned j = 0; j < n; j++ ){
    if( elements[i] < elements[j] ){
    /* exchange elements[i] and elements[j] */
    }
    }
    }
    }
    ...
    Probably the most interestiong thing about it is that it actually
    sorts. As a benchmork it could be useful to assess quality of
    compiler removal of useless code. I suspect that currently no
    compiler is able to remove useless half of this code, also
    probably can not optimize sequence of swaps to array shift. But
    those are not far from what is alreade done.

    Given that gcc and clang do not optimize the sequence of swaps in the
    bubble sort component of John Hennesseys stan.c benchmark collection
    from the early 1980s into array rotation, I would not expect it to
    happen for the above, especially given that elements[i] and
    elements[j] can and regularly will alias. Ok, the compiler can infer
    that no swap happens in this case, and split the inner loop into a
    0..i-1 one and an i+1..n-1 one, but doing that is more involved than I
    expect and have experienced from compiler optimizations for
    "irrelevant" code.

    For comparison, the inner loop of the bubble-sort benchmark in stan.c
    is:

    i=1;
    while ( i<top ) {
    if ( sortlist[i] > sortlist[i+1] ) {
    j = sortlist[i];
    sortlist[i] = sortlist[i+1];
    sortlist[i+1] = j;
    };
    i=i+1;
    };

    I see fewer hurdles in optimizing that than the inner loop of
    baffle_sort(), but it does not happen.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Tue Oct 6 10:05:49 2026
    From Newsgroup: comp.arch

    On 06/10/2026 04:17, Waldek Hebisch wrote:
    David Brown <[email protected]> wrote:
    On 04/10/2026 19:46, Thomas Koenig wrote:
    David Brown <[email protected]> schrieb:
    On 04/10/2026 16:16, Thomas Koenig wrote:
    [email protected]d <[email protected]d> schrieb:

    "-fwrapv" didn't exist when we started gcj, which used GCC to
    implement Java. After some discussion with other GCC maintainers, it >>>>>> was agreed to add wrapping signed integer semantics to GCC's IR.
    "-fwrapv" provides that to C-derived languages too. Whether "-fwrapv" >>>>>> would have happened without the pull from gcj I can't say. It's
    impossible to rerun history.

    Linear congruential random number generators, in common use when
    the dinosaurs roamed the computer centers, depend on wraparound
    semantics. In gfortran, they stopped working at one time because
    gcc assuming that this would not happen. -fwrap is a workaround
    (without changing the source), but of course using the RANDOM_NUMBER >>>>> intrinsic is better - it also uses a reasonable algorithm.

    So, some people should probably be grateful to you :-)


    Do unsigned types in Fortran not have wrapping semantics? Or are there >>>> no unsigned integer types in Fortran? I know not every language has the >>>> same distinction as C here.

    The Fortran language has no unsigned types. Two compilers, gfortran
    and flang, implement a suggestion that I made to J3 (much improved
    by some people on the committee) and was accepted, but not taken
    up by WG5. At the time, I had already started implementing it,
    so I went ahead and completed it anyway, see

    https://gcc.gnu.org/onlinedocs/gfortran/Unsigned-integers.html

    (You can probably tell that this is still a sore point.)

    But what the pseudo-random number generators did was use *signed*
    overflow, which had been prohibited by the standard forever, but
    because people needed something and it worked, they went ahead
    and did it anyway.


    I would guess you'd have trouble writing something that was efficient
    here, and avoided UB when you don't have language support for wrapping
    overflow (whether it be for signed or unsigned types).

    If language supports large enough integers and with reasonable
    optimizer code like:

    bigint a = 123;
    bigint b = 7;
    bigint c = 17;
    long f() {
    a = 0xffffffffffffffff &(a*c + b);
    return a;
    }

    will be compiled using wrapping 64-bit arithmetic. If additionaly
    you make sure that a is local to random number module so compiler
    knows that it has seen all updates to a, and updates stay within
    64 bits than compiler can use 64-bit variables.

    But having explicit wrapping types is more direct and easier to
    implement, which mattered for dinosaurs.


    Begin direct and easier to write matters to us non-dinosaurs too!

    Yes, such "bigint" code should give you correct answers, but
    optimisation is not going to be guaranteed. It is likely that you are
    going to need a lot of mask operations in your code - its going to be
    ugly, or have to use inline functions where infix operators would be
    nicer. (Does Fortran have classes with overloaded operators, like C++?)
    There are certainly good optimising Fortran compilers.



    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Tue Oct 6 08:29:19 2026
    From Newsgroup: comp.arch

    [email protected] (Dan Cross) writes:
    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    Tim Rentsch <[email protected]> writes:
    That's good to know but what is a good resource to learn the Rust >>>language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    If you mean "The Rust Programming Language", I came away from it >>disillusioned.

    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    I think what you mean to say is that you couldn't find a way to
    implement that in the manner you were accustomed to thinking
    about the problem, given your prior experience.

    My goal was to implement search tree insertion as efficiently and
    compactly as in Pascal, C or Forth (and in contrast to Java), ideally
    in safe Rust. Here it is in C (untested):

    struct tree {
    int key; /* or some other key type */
    int val; /* or some other value type */
    struct tree *left;
    struct tree *right;
    };

    struct tree **search(struct tree **rootp, int key)
    {
    while ((root=*rootp)!=NULL && key != root->key) {
    if (key < root->key)
    rootp = &(root->left);
    else
    rootp = &(root->right);
    }
    return rootp;
    }

    void insert(struct tree **rootp, int key, int val)
    {
    struct tree **nodep = **search(rootp, key);
    struct tree *node = *nodep;
    if (node == NULL) {
    node = malloc(sizeof(tree));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    *nodep = node;
    }
    node->val = val;
    }

    Passing a reference to a mutable field is what is done in these
    languages, and I don't know an efficient and similarly compact
    technique that does not pass a reference to a mutable field.

    I also knew Java solutions to the problem:

    1) Special-case the root (which you typically have to do in Java
    anyway, and otherwise always keep a pointer to the last tree node
    around, and also keep the knowledge about the last left-or-right
    decision around (or take it again). This all leads to quite a bit of
    code that is duplicated relative to the code above.

    2) Recurse into the tree and rewrite all the links to the subtrees
    along the spine of the search. This is not quite as duplicative as 1)
    (but the root still has to be handled separately), but it performs
    several writes instead of just one, and the recursion is not a tail
    recursion, so is relatively expensive.

    I wanted something with the benefits of the Pascal/C/Forth variant,
    not something like the Java variants. I don't know if that counts as
    "manner you were accustomed to thinking about the problem".

    And I learned this technique in Pascal or Modula-2 (probably from one
    of Wirth's books), and had no problem transferring the knowledge into
    C or Forth, on the contrary: In C and Forth I could turn search into a
    loop (in Pascal it has to be a tail-recursive function, because you
    can only produce references to variables and fields through reference parameters).

    For a language that aspires to take over from C for systems
    programming, I expect that one can use this technique, too.

    But that is separate from whether it can be done or not.

    Sure, and as I found out later, it can be done.

    In fact, in Rust you can use the approximately the same
    techniques you would use in Pascal, C, or (probably) Forth: raw
    pointers are a part of the language, and you can simply use them
    with `unsafe` code, and it would be no more "unsafe" than in C
    or Pascal. One might argue that that defeats the purpose of the
    language, and you'd be right to a point. The difference is that
    in C or Pascal, the entire program is what Rust would call
    "unsafe", whereas generally this can be restricted to just a few
    lines of carefully audited code in Rust.

    Given that I do not understand the automagic stuff done by the Rust
    compiler enough to get this kind of tree insertion past the borrow
    checker, even though it is possible, I don't trust my capabilities to
    audit unsafe Rust.

    FWIW, for things like containers of various kinds (trees, hash
    tables, linked-lists, growable arrays, etc), production programs
    usually use an existing library or crate instead of rolling
    their own.

    When I saw that this technique cannot be implemented in Java, I
    wondered why Java was not having references to fields (answer:
    probably because that would make garbage collection more costly and/or
    less precise), that this technique is needed for relatively few
    things, mostly stuff that's in libraries. And I wondered how these
    library methos are implemented (at one point I guessed that they
    implemented these parts of their libraries in C, but when I actually
    look, the implementation of what I looked at was in Java, IIRC along
    the lines of 1)).

    For a language that aspires to replace C for systems programming,
    giving the Java answer is not good enough, though.

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker.

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference >>counting after all. However, the problem in this case is the usual
    thing with type inference: There is so much going on that happens >>automagically (in particular automatic dereferencing in Rust), and the
    Rust compiler does not know where you went wrong, so the error message
    is unhelpful (while for the simpler cases the compiler usually guesses >>right and gives very helpful, to-the-point error messages).

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    Sounds like you're in what we'd call the, "fighting the borrow
    checker" stage. It may be that you end up really not liking
    Rust or feeling that other languages are better, but I wager
    that just about anyone who has seriously used the language has
    gone through similar things.

    I wonder about that. From the books I read, I get the impression that
    Rust used to have less automagic stuff, and much of the automagic was
    added later, as a convenience for the programmers who already knew the language. So the Rust programmers of the early days formed a mental
    model based on "Pedestrian Rust" (i.e., with less automagic), and when
    they see a program with more automagic, and need to understand some conveniently short piece of Rust code in detail, they form a mental
    model of the code in Pedestrian Rust. But how can a Rust programmer
    who starts now learn Pedestrian Rust and how it relates to the modern
    language. There are hints in the books I read, but not enough to get
    a firm foundation.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Tue Oct 6 02:53:26 2026
    From Newsgroup: comp.arch

    [email protected] (Waldek Hebisch) writes:

    Tim Rentsch <[email protected]> wrote:

    [email protected] (Waldek Hebisch) writes:

    Tim Rentsch <[email protected]> wrote:

    Thomas Koenig <[email protected]> writes:

    Bubblesort is the worst of non-joke sorting algorithms, see
    the quote from Numerical Recipes...

    For people who don't like bubble sort, or for inclusion in a set
    of benchmarks, I suggest the following recently discovered
    sorting algorithm (written in C-ish pseudocode):

    void
    baffle_sort( unsigned n, int *elements ){
    for( unsigned i = 0; i < n; i++ ){
    for( unsigned j = 0; j < n; j++ ){
    if( elements[i] < elements[j] ){
    /* exchange elements[i] and elements[j] */
    }
    }
    }
    }

    (Disclaimer: not original.)

    [...]

    I am making no claim that this sort would be a good choice if
    one wanted an efficient algorithm. Surely no one would choose
    it on that basis. That's not what makes it interesting.

    Probably the most interestiong thing about it is that it actually
    sorts.

    That it actually sorts is I think the most surprising thing.

    What I found most interesting is how it sorts or why it sorts. It
    took me a while to understand what makes it work. It is difficult,
    for example, to write invariants for this code that can be used to
    establish the conclusion that sorting actually takes place. A
    better name than baffle_sort() might be least_transparent_sort().
    But that would ruin the surprise. ;)
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Tue Oct 6 05:01:32 2026
    From Newsgroup: comp.arch

    [email protected] (Anton Ertl) writes:

    Tim Rentsch <[email protected]> writes:

    That's good to know but what is a good resource to learn the Rust
    language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    If you mean "The Rust Programming Language", I came away from it disillusioned.

    Yes, that's the book I meant.

    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker. [discovered later not to be necessary]

    I think this test case is a good one. Somewhat disappointing that
    Rust makes it non-obvious. Do you know if what you learned would
    carry over to, for example, AVL trees?

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference counting after all.

    Thank you for this reference. I am just starting to look it over.
    Do you know if there is a PDF version somewhere rather than the endless-series-of-webpages format shown on its website?

    However, the problem in this case is the usual
    thing with type inference: There is so much going on that happens automagically (in particular automatic dereferencing in Rust), and the
    Rust compiler does not know where you went wrong, so the error message
    is unhelpful (while for the simpler cases the compiler usually guesses
    right and gives very helpful, to-the-point error messages).

    Hmm. That might be worth my while doing some web research for.

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    Can I ask how much experience you have with ML-style languages
    (e.g., ML, OCaml, Haskell)? I'm wondering how the type inference in
    Rust compares to the type inference done in those languages.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Tue Oct 6 15:21:09 2026
    From Newsgroup: comp.arch

    Tim Rentsch <[email protected]> writes: >[email protected] (Anton Ertl) writes:
    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker. [discovered later not to be necessary]

    I think this test case is a good one. Somewhat disappointing that
    Rust makes it non-obvious. Do you know if what you learned would
    carry over to, for example, AVL trees?

    What I learned? No. I did not really learn it. I saw how I could
    modify an example from the book to achieve what I want, and understood
    the explanation of that example, but I do not know how to use this
    stuff to build code from scratch.

    Whether it is possible to do AVL trees in safe Rust without resorting
    to reference-counting and such? Maybe, but I guess there are too many
    mutable references around in AVL tree operations at the same time.

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference
    counting after all.
    ...
    Do you know if there is a PDF version somewhere rather than the >endless-series-of-webpages format shown on its website?

    I don't know, but I would expect it. The way it looks, it seems to
    use some software for books in multiple formats.

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    Can I ask how much experience you have with ML-style languages
    (e.g., ML, OCaml, Haskell)? I'm wondering how the type inference in
    Rust compares to the type inference done in those languages.

    A little with Miranda and Haskell (Hugs), and the latter is the reason
    why I have this opinion of type inference: You get a long error
    message that tells you that there is some inconsistency in the
    inferred types; because the types are inferred from a significant
    stretch of code, the problem could be anywhere in this code.

    My impression is that in Rust you tend to nail down things more often
    than in Haskell, so the stretches are shorter, but there is more
    automagic going on in Rust, with automatic defererencing and
    referencing, automatic dropping, etc., and the borrow checker
    interacts with the automagic, which makes it at least as hard to deal
    with as th Haskell type inference.

    For Haskell I can at least make types explicit at various places to
    reduce the area where the error might hide, but in Rust I have no way
    to disable the automagic, and no way (or at least I don't know it) to
    tell the borrow checker what I think it should be seeing, such that it
    can tell me where my error is.

    I have investigated some tool that IIRC rewrite the Rust program to
    give more information about how it understands the program, but at
    least for my test case, it was not enlightening.

    As for the type system, a friend told me that he likes Rust because it
    gives him what he likes about Haskell's type system in an imperative
    language.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Tue Oct 6 23:16:39 2026
    From Newsgroup: comp.arch

    Dan Cross wrote:
    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables. >>>>>
    In Rust, the only acceptable array index is of type "usize", which is >>>>> effectively the same as u64 on most platforms these days, but does not >>>>> need to be so: On a 32-bit target, it would be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such indices
    are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.

    That's what I assumed, except I check my assumptions by godbolt'ing my
    code, using -O -C "target_cpu=native" to get a bleeding edge optimized
    build, and that's where I saw a loop which started with

    while pos+7 < input.len() {
    ... here a bunch of accesses to input[p], input[p+1]...input[p+6]
    }

    Every one of those secodnary accesses were very obviously tested against
    the input length (stored in a register for the duration)

    I considered reporting this as a very clear performance bug!

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From George Neuner@[email protected] to comp.arch on Tue Oct 6 21:18:07 2026
    From Newsgroup: comp.arch

    On Tue, 06 Oct 2026 05:01:32 -0700, Tim Rentsch
    <[email protected]> wrote:

    [email protected] (Anton Ertl) writes:

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference
    counting after all.

    Thank you for this reference. I am just starting to look it over.
    Do you know if there is a PDF version somewhere rather than the >endless-series-of-webpages format shown on its website?

    At the upper right are controls to go to the GIT repository or "print"
    the book as rendered. BUt it seems that if you print/save you'll lose
    the table of contents.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Tue Oct 6 22:17:49 2026
    From Newsgroup: comp.arch

    George Neuner <[email protected]> writes:

    On Tue, 06 Oct 2026 05:01:32 -0700, Tim Rentsch
    <[email protected]> wrote:

    [email protected] (Anton Ertl) writes:

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference
    counting after all.

    Thank you for this reference. I am just starting to look it over.
    Do you know if there is a PDF version somewhere rather than the
    endless-series-of-webpages format shown on its website?

    At the upper right are controls to go to the GIT repository or "print"
    the book as rendered. BUt it seems that if you print/save you'll lose
    the table of contents.

    Okay great. Thank you.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Stefan Monnier@[email protected] to comp.arch on Tue Oct 6 14:55:05 2026
    From Newsgroup: comp.arch

    What I found most interesting is how it sorts or why it sorts. It
    took me a while to understand what makes it work. It is difficult,
    for example, to write invariants for this code that can be used to
    establish the conclusion that sorting actually takes place.

    Indeed, mostly because the first iteration (i=0) is completely different
    from the other ones: the swaps that take place happen only when

    i > j or i == 0

    So only the first iteration of the outer loop really works on the whole
    array. The subsequent ones do nothing once j > i.


    === Stefan
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Wed Oct 7 05:41:23 2026
    From Newsgroup: comp.arch

    [email protected] (Anton Ertl) writes:

    Tim Rentsch <[email protected]> writes:

    [email protected] (Anton Ertl) writes:

    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker. [discovered later not to be necessary]

    I think this test case is a good one. Somewhat disappointing that
    Rust makes it non-obvious. Do you know if what you learned would
    carry over to, for example, AVL trees?

    What I learned? No. I did not really learn it. I saw how I could
    modify an example from the book to achieve what I want, and understood
    the explanation of that example, but I do not know how to use this
    stuff to build code from scratch.

    Whether it is possible to do AVL trees in safe Rust without resorting
    to reference-counting and such? Maybe, but I guess there are too many mutable references around in AVL tree operations at the same time.

    A simple search tree can do insertion only at the leaves. Other
    binary tree insertion methods typically modify interior nodes, and
    certainly AVL trees fall in this category. Except for the difference
    of leaf-node-only modification and interior-node modification, I
    don't see why they should be any different. Is there something
    about the Rust-safe code that you wrote that depends on modifying
    only leaf nodes?

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference
    counting after all.

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    Can I ask how much experience you have with ML-style languages
    (e.g., ML, OCaml, Haskell)? I'm wondering how the type inference in
    Rust compares to the type inference done in those languages.

    A little with Miranda and Haskell (Hugs), and the latter is the reason
    why I have this opinion of type inference: You get a long error
    message that tells you that there is some inconsistency in the
    inferred types; because the types are inferred from a significant
    stretch of code, the problem could be anywhere in this code.

    My impression is that in Rust you tend to nail down things more often
    than in Haskell, so the stretches are shorter, but there is more
    automagic going on in Rust, with automatic defererencing and
    referencing, automatic dropping, etc., and the borrow checker
    interacts with the automagic, which makes it at least as hard to deal
    with as th Haskell type inference.

    For Haskell I can at least make types explicit at various places to
    reduce the area where the error might hide, but in Rust I have no way
    to disable the automagic, and no way (or at least I don't know it) to
    tell the borrow checker what I think it should be seeing, such that it
    can tell me where my error is.

    I have investigated some tool that IIRC rewrite the Rust program to
    give more information about how it understands the program, but at
    least for my test case, it was not enlightening.

    As for the type system, a friend told me that he likes Rust because it
    gives him what he likes about Haskell's type system in an imperative language.

    Very interesting. Thank you for the long explanation. I am curious to
    know more but comp.arch is not the right place for that discussion so
    I will stop here (and perhaps continue in a better choice of forum).
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Oct 7 13:28:05 2026
    From Newsgroup: comp.arch

    In article <11a3ofn$2ak3s$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables. >>>>>>
    In Rust, the only acceptable array index is of type "usize", which is >>>>>> effectively the same as u64 on most platforms these days, but does not >>>>>> need to be so: On a 32-bit target, it would be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such indices >>>> are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.

    That's what I assumed, except I check my assumptions by godbolt'ing my
    code, using -O -C "target_cpu=native" to get a bleeding edge optimized >build, and that's where I saw a loop which started with

    while pos+7 < input.len() {
    ... here a bunch of accesses to input[p], input[p+1]...input[p+6]
    }

    Every one of those secodnary accesses were very obviously tested against
    the input length (stored in a register for the duration)

    I considered reporting this as a very clear performance bug!

    I wonder if what you saw was actually loop unrolling. I just
    tried something similar, albeit with a very simple sum in the
    loop body, and don't see a bunch of extraneous bounds checks.

    I do wonder, though, whether you'd get better object code using `input.iter().chunks(n)` or `.windows(n)`.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Oct 7 14:23:21 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    [snip]
    My impression is that in Rust you tend to nail down things more often
    than in Haskell, so the stretches are shorter, but there is more
    automagic going on in Rust, with automatic defererencing and
    referencing, automatic dropping, etc., and the borrow checker
    interacts with the automagic, which makes it at least as hard to deal
    with as th Haskell type inference.

    The rules for when references are taken, when dereferences
    happen, and when things are dropped are well-defined; there's no
    magic there.

    To a first order approximation, a references is taken using the
    `&` and `&mut` operators, or when an object is the target of a
    method call that takes `&self` or `&mut self`.

    An object is dereferenced using `*` operator, the `.deref()`
    operator for something that impl's the `Deref` trait, or when
    using the `.` operator on product types.

    An object is dropped either when it goes out of scope, or when
    subject to the `Drop` operator.

    Rust objects and references have lifetimes, which are a
    first-class part of the type system. The borrow checker
    enforces rules relating the lifetimes of objects and their
    references.

    Specifically:

    * A reference to an object cannot outlive the object itself (no
    dangling references).
    * If a _mutable_ reference to an object exists at some point in
    time, then that reference is the only reference to that
    object.
    * Any number if _immutable_ references may exist to an object at
    any given time, mutually exclusive with a mutable reference to
    that object.

    Rust references are non-nullable, and the borrow checker rules
    enforce that they always point to a valid object; the language
    will not allow you to take a reference to an uninitialized
    object.

    There are a couple of fine points here; if you have a mutable
    reference to something, it's ok to call a method on it that
    takes `&self`, for example, but for the most part the above is a
    decent mental model.

    There are also so-called "raw" pointers in Rust that are similar
    to pointers in C or Pascal in that they are nullable, can
    dangle, and don't necessarily have to point to a valid,
    initialized object. One can create a raw pointer using the
    `&raw` or `&raw mut` operators, by assignment from a reference,
    or by an explicit function/cast. Creating a raw pointer is
    considered safe, since they do not have to be valid. However,
    dereferencing a raw pointer is `unsafe`, meaning that it is the
    programmer's responsibility to ensure that it points to a valid
    object before dereferencing.

    Raw pointers also have a notion of "provenance", which tracks
    their origin (think capabilities in the CHERI sense here; indeed
    CHERI was the motivation for the "strict provenance" APIs: https://doc.rust-lang.org/std/ptr/index.html#strict-provenance).

    For Haskell I can at least make types explicit at various places to
    reduce the area where the error might hide, but in Rust I have no way
    to disable the automagic, and no way (or at least I don't know it) to
    tell the borrow checker what I think it should be seeing, such that it
    can tell me where my error is.

    This is classic, "fighting the borrow checker." The checker
    will tell you exactly where it thinks your error is, as far as
    saying, "you use a reference at this point, when the object it
    points to went out of scope at this other point..." or "you
    attempted to take a mutable reference to an object while another
    live reference to it already existed..." or "you tried to take
    *any* reference to an object while a live mutable reference
    exists". There's no magic here, and generally the error
    messages are pretty good.

    The `unsafe` keyword is the escape hatch that tells the compiler
    that I, the programmer, am doing something weird that it has no
    insight into, and so it cannot automatically enforce its checks,
    and that I am therefore taking on the responsibility of ensuring
    that the language's rules with respect to memory safety and
    concurrency are respected. Note that `unsafe` does not allow a
    programmer to _violate_ those rules, it merely tells the
    compiler that they've been checked manually.

    Once you get used to it, the borrow checker is unintrusive, and
    it really does catch errors that would lead to crashes or
    mangled data in other languages. But if you are trying to write
    a Rust program the same way you would in a language that is not
    type- and memory-safe, like C or (I gather) Forth, you will
    almost certainly find it very frustrating.

    I have investigated some tool that IIRC rewrite the Rust program to
    give more information about how it understands the program, but at
    least for my test case, it was not enlightening.

    Perhaps you are referring to Miri? That is a simulator for the
    IR, that can be used to find bugs in raw pointer usage in
    `unsafe` code.

    As for the type system, a friend told me that he likes Rust because it
    gives him what he likes about Haskell's type system in an imperative >language.

    Yes. The type system allows you to make aggressive use of
    things like "typestates" and "newtypes" to eliminate entire
    categories of errors. The "Parse, don't validate" metaphor
    introduced by Alexis King on her blog post (https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) translates almost directly from Haskell to Rust; my colleague
    Cliff Biffle has written on typestates in Rust here: https://cliffle.com/blog/rust-typestate/

    When I wrote the host OS boot loader for our machines (the thing
    we run from the reset vector) I used newtypes to describe page
    mappings so that virtaul page addresses of a certain size _had_
    to be canonical and properly aligned and _had_ to point to a
    correctly sized and aligned physical page frame that was
    similarly valid. Once one has parsed a virtual page address or
    physical frame address into its representative type, there's no
    need to revalidate it at point of use. The existence of an
    instance of those types is a proof that the addresses are valid: https://github.com/oxidecomputer/phbl/blob/main/src/mmu.rs

    These techniques apply to all sorts of things. Making a
    erroneous combination unrepresentable at the type level is an
    incredibly powerful tool.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Wed Oct 7 16:43:34 2026
    From Newsgroup: comp.arch

    Dan Cross wrote:
    In article <11a3ofn$2ak3s$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables. >>>>>>>
    In Rust, the only acceptable array index is of type "usize", which is >>>>>>> effectively the same as u64 on most platforms these days, but does not >>>>>>> need to be so: On a 32-bit target, it would be the same as u32. >>>>>>>
    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such indices >>>>> are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.

    That's what I assumed, except I check my assumptions by godbolt'ing my
    code, using -O -C "target_cpu=native" to get a bleeding edge optimized
    build, and that's where I saw a loop which started with

    while pos+7 < input.len() {
    ... here a bunch of accesses to input[p], input[p+1]...input[p+6]
    }

    Every one of those secodnary accesses were very obviously tested against
    the input length (stored in a register for the duration)

    I considered reporting this as a very clear performance bug!

    I wonder if what you saw was actually loop unrolling. I just
    tried something similar, albeit with a very simple sum in the
    loop body, and don't see a bunch of extraneous bounds checks.

    Not unrolling, the code is here:

    https://godbolt.org/z/YnY5G1K49

    The interesting part starts at line 28/29 .LBB0_02 in the asm window
    where you can see (starting at line 37) a bunch of cmp r8,rsi / jae
    <panic> sequences.


    I do wonder, though, whether you'd get better object code using `input.iter().chunks(n)` or `.windows(n)`.

    I guess I can try this, I'm guessing chunks(7) will error out if less
    than 7 elements are available?

    The code is from Advent of Code 2024 day 3, https://github.com/TerjeWiigMathisen/aoc/tree/master/2024/day03

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch,comp.programming on Wed Oct 7 18:29:26 2026
    From Newsgroup: comp.arch

    [Crossposted and and Followup-To: comp.programming]

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote: >[email protected] (Dan Cross) writes:
    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    Tim Rentsch <[email protected]> writes:
    That's good to know but what is a good resource to learn the Rust >>>>language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    If you mean "The Rust Programming Language", I came away from it >>>disillusioned.

    The reason was that I could not get a search-tree insertion technique
    to work that can be implemented in e.g., Pascal, C, and Forth, but not
    in Java (because in Java I cannot produce references to a field inside
    an object).

    I think what you mean to say is that you couldn't find a way to
    implement that in the manner you were accustomed to thinking
    about the problem, given your prior experience.

    My goal was to implement search tree insertion as efficiently and
    compactly as in Pascal, C or Forth (and in contrast to Java), ideally
    in safe Rust. Here it is in C (untested):

    struct tree {
    int key; /* or some other key type */
    int val; /* or some other value type */
    struct tree *left;
    struct tree *right;
    };

    struct tree **search(struct tree **rootp, int key)
    {
    while ((root=*rootp)!=NULL && key != root->key) {
    if (key < root->key)
    rootp = &(root->left);
    else
    rootp = &(root->right);
    }
    return rootp;
    }

    An intersting way to write it; I understand you're trying to
    reuse `search` in `insert`, below, and thus why you want to
    return `rootp`.

    void insert(struct tree **rootp, int key, int val)
    {
    struct tree **nodep = **search(rootp, key);

    I know you said this was untested; clearly you should not
    indirect the result of `search` here.

    struct tree *node = *nodep;
    if (node == NULL) {
    node = malloc(sizeof(tree));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    *nodep = node;
    }
    node->val = val;
    }

    Passing a reference to a mutable field is what is done in these
    languages, and I don't know an efficient and similarly compact
    technique that does not pass a reference to a mutable field.

    I also knew Java solutions to the problem:

    1) Special-case the root (which you typically have to do in Java
    anyway, and otherwise always keep a pointer to the last tree node
    around, and also keep the knowledge about the last left-or-right
    decision around (or take it again). This all leads to quite a bit of
    code that is duplicated relative to the code above.

    2) Recurse into the tree and rewrite all the links to the subtrees
    along the spine of the search. This is not quite as duplicative as 1)
    (but the root still has to be handled separately), but it performs
    several writes instead of just one, and the recursion is not a tail >recursion, so is relatively expensive.

    Or, 3) Simply reimplement the comparison in `insert`; it's a
    trivial amount of code.

    I chose a variant of (1) for my implementation, but only because
    it was easy, not because it was required. No unsafe code or
    fancy reference counting here.

    BEGIN `tree.rs`<---
    //! Simple binary search tree.

    use std::cmp::Ordering;
    use std::fmt::Debug;

    /// A binary search tree over any key type that implements a
    /// total ordering.
    pub struct Tree<K: Debug + Ord, V: Debug> {
    root: Option<Box<Node<K, V>>>,
    }

    impl<K: Debug + Ord, V: Debug> Tree<K, V> {
    /// Creates a new, empty tree.
    pub fn new() -> Tree<K, V> {
    Tree { root: None }
    }

    /// Searches the tree for the given key, and returns
    /// `Some(&value)` if it's here, otherwise returns None.
    ///
    /// # Example
    ///
    /// ```
    /// let mut tree = tree::Tree::<i32, i32>::new();
    /// tree.insert(2, 200);
    /// tree.insert(1, 100);
    /// assert_eq!(tree.search(&0), None);
    /// assert_eq!(tree.search(&1), Some(&100));
    /// assert_eq!(tree.search(&2), Some(&200));
    /// ```
    pub fn search(&self, key: &K) -> Option<&V> {
    let mut root = &self.root;
    while let Some(node) = root {
    match key.cmp(&node.key) {
    Ordering::Less => root = &node.left,
    Ordering::Equal => return Some(&node.value),
    Ordering::Greater => root = &node.right,
    }
    }
    None
    }

    /// Inserts the given `value` into the tree with the given
    /// `key`. Returns true if the node was inserted, or false
    /// if the key was already in the tree.
    ///
    /// # Example
    ///
    /// ```
    /// let mut tree = tree::Tree::<i32, i32>::new();
    /// assert_eq!(tree.insert(1, 100), true);
    /// assert_eq!(tree.insert(1, 200), false);
    /// assert_eq!(tree.insert(2, 100), true);
    /// ```
    pub fn insert(&mut self, key: K, value: V) -> bool {
    let mut root = &mut self.root;
    loop {
    let Some(node) = root else {
    root.replace(Node::new(key, value));
    return true;
    };
    match key.cmp(&node.key) {
    Ordering::Less => root = &mut node.left,
    Ordering::Equal => return false,
    Ordering::Greater => root = &mut node.right,
    }
    }
    }

    /// Print the tree, if it is not empty.
    pub fn print(&self) {
    if let Some(node) = &self.root {
    node.print();
    }
    }
    }

    // A node in the tree owns its key, value, and left and right
    // children.
    struct Node<K: Debug + Ord, V: Debug> {
    key: K,
    value: V,
    left: Option<Box<Node<K, V>>>,
    right: Option<Box<Node<K, V>>>,
    }

    impl<K: Debug + Ord, V: Debug> Node<K, V> {
    /// Creates a new boxed node owning the given key and value.
    pub fn new(key: K, value: V) -> Box<Node<K, V>> {
    Box::new(Node {
    key,
    value,
    left: None,
    right: None,
    })
    }

    /// Pre-order print of this node and its children.
    pub fn print(&self) {
    if let Some(left) = &self.left {
    left.print();
    }
    println!("key = {:?}, value = {:?}", self.key, self.value);
    if let Some(right) = &self.right {
    right.print();
    }
    }
    }
    END `tree.rs`<---

    Note that there's more code here than in your C code, but this
    also does a lot more; this is generic over any key and value
    types where the key type implements a total ordering, and where
    both types implement the `Debug` trait (so they can be printed).

    Just to be fancy I added some comments, and note that the
    examples in thosee comments are actually unit tests that are run
    with `cargo test`.

    There is also a pre-order `print` routine. With a trivial
    driver program, this prints:

    term% cat src/main.rs
    use tree::Tree;

    pub fn main() {
    let mut tree = Tree::<i32, i32>::new();
    tree.insert(2, 200);
    tree.insert(1, 100);
    tree.insert(3, 300);
    tree.print();
    println!("tree.search(0) = {:?}", tree.search(&0));
    println!("tree.search(1) = {:?}", tree.search(&1));
    }
    term% cargo run
    Compiling tree v0.1.0 (/Users/cross/xfer/tree)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    Running `target/debug/tree`
    key = 1, value = 100
    key = 2, value = 200
    key = 3, value = 300
    tree.search(0) = None
    tree.search(1) = Some(100)
    term%

    I wanted something with the benefits of the Pascal/C/Forth variant,
    not something like the Java variants. I don't know if that counts as
    "manner you were accustomed to thinking about the problem".

    I would say that it does. You wanted to do something that is
    familiar from Pascal or Modula-2, but that is annoying to
    express in Rust. However, that doesn't mean that there is not a
    similar way to effect the same thing in Rust.

    And I learned this technique in Pascal or Modula-2 (probably from one
    of Wirth's books), and had no problem transferring the knowledge into
    C or Forth, on the contrary: In C and Forth I could turn search into a
    loop (in Pascal it has to be a tail-recursive function, because you
    can only produce references to variables and fields through reference >parameters).

    For a language that aspires to take over from C for systems
    programming, I expect that one can use this technique, too.

    See above. The core of the `search` and `insert` algorithms are
    similar to the ones you sketched, though the pointer-to-pointer
    to a node is not returned by `search`. If that is what you mean
    by "technique" then you're right; doing that in Rust is
    inconvenient because it violates the borrow checker's rules
    about the existence of multiple simultaneous mutable references
    to an object. But I claim that is just a detail, and not a
    terribly interesting one at that; certainly not an impediment to
    substituting Rust for C in systems programming.

    Even so, as I said, one _can_ use the same technique using a
    smallish-bit of `unsafe` code. This version is more faithful to
    your original:

    BEGIN `unsafe_tree.rs`<---
    //! Simple binary search tree.

    use std::cmp::Ordering;
    use std::fmt::Debug;

    /// A binary search tree over any key type that implements a
    /// total ordering.
    pub struct Tree<K: Debug + Ord, V: Debug> {
    root: *mut Node<K, V>,
    }

    impl<K: Debug + Ord, V: Debug> Tree<K, V> {
    /// Creates a new, empty tree.
    pub fn new() -> Tree<K, V> {
    Tree {
    root: std::ptr::null_mut(),
    }
    }

    fn search(&mut self, key: &K) -> &mut *mut Node<K, V> {
    let mut nodep = &mut self.root;
    while let Some(node) = unsafe { nodep.as_mut() } {
    match key.cmp(&node.key) {
    Ordering::Less => nodep = &mut node.left,
    Ordering::Equal => break,
    Ordering::Greater => nodep = &mut node.right,
    }
    }
    nodep
    }

    pub fn insert(&mut self, key: K, value: V) {
    let inodep = self.search(&key);
    if let Some(node) = unsafe { inodep.as_mut() } {
    node.value = value;
    } else {
    let node = Node::new(key, value);
    *inodep = std::boxed::Box::leak(node);
    }
    }

    pub fn print(&self) {
    unsafe { self.root.as_ref() }.map(|root| root.print());
    }
    }

    impl<K: Debug + Ord, V: Debug> Drop for Tree<K, V> {
    fn drop(&mut self) {
    let _root = unsafe { std::boxed::Box::from_raw(self.root) };
    }
    }

    // A node in the tree owns its key, value, and left and right
    // children.
    struct Node<K: Debug + Ord, V: Debug> {
    key: K,
    value: V,
    left: *mut Node<K, V>,
    right: *mut Node<K, V>,
    }

    impl<K: Debug + Ord, V: Debug> Node<K, V> {
    /// Creates a new boxed node owning the given key and value.
    pub fn new(key: K, value: V) -> Box<Node<K, V>> {
    Box::new(Node {
    key,
    value,
    left: std::ptr::null_mut(),
    right: std::ptr::null_mut(),
    })
    }

    /// Pre-order print of this node and its children.
    pub fn print(&self) {
    unsafe { self.left.as_ref() }.map(|left| left.print());
    println!("key = {:?}, value = {:?}", self.key, self.value);
    unsafe { self.right.as_ref() }.map(|right| right.print());
    }
    }

    impl<K: Debug + Ord, V: Debug> Drop for Node<K, V> {
    fn drop(&mut self) {
    if !self.left.is_null() {
    let _left = unsafe { std::boxed::Box::from_raw(self.left) };
    }
    if !self.right.is_null() {
    let _right = unsafe { std::boxed::Box::from_raw(self.right) };
    }
    }
    }

    #[cfg(test)]
    mod tests {
    use super::*;
    #[test]
    fn raw_insert_works() {
    let mut tree = Tree::new();
    tree.insert(1, 100);
    tree.insert(3, 300);
    tree.insert(2, 200);
    drop(tree);
    }
    }
    END `unsafe_tree.rs`<---

    I stripped comments for brevity, but added explicit `Drop` impls
    to avoid leaking memory. The seemingly useless test here allows
    me to exercise this under `miri` to check for memory safety
    violations manipulating raw pointers with `unsafe`:

    term% cargo +nightly miri test
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
    Running unittests src/lib.rs (target/miri/aarch64-apple-darwin/debug/build/tree/6797a5ad52527de9/out/tree-6797a5ad52527de9)

    running 1 test
    test otree::tests::raw_insert_works ... ok

    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s

    Running unittests src/main.rs (target/miri/aarch64-apple-darwin/debug/build/tree/6121d361cfb78277/out/tree-6121d361cfb78277)

    running 0 tests

    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s

    Doc-tests tree

    running 2 tests
    test src/lib.rs - Tree<K,V>::insert (line 51) ... ok
    test src/lib.rs - Tree<K,V>::search (line 25) ... ok

    test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s

    all doctests ran in 0.14s; merged doctests compilation took 0.03s
    term%

    So, we can see that it is possible to use the same technique you
    are familiar with.

    The difference is that, with Rust, you usually don't _want_ to
    do it this way, since there's a completely safe alternative that
    leverages the type system to prohibit memory leaks, dangling
    pointers, and so on, by construction.

    Frankly, unless I was doing something very odd, I wouldn't write
    my own binary search tree at all; the standard library's
    collections implementations are pretty good, and then there are
    third-party libraries like Hashbrown and Crossbeam that are
    best-in-class across the industry.

    But that is separate from whether it can be done or not.

    Sure, and as I found out later, it can be done.

    Indeed; see above.

    In fact, in Rust you can use approximately the same
    techniques you would use in Pascal, C, or (probably) Forth: raw
    pointers are a part of the language, and you can simply use them
    with `unsafe` code, and it would be no more "unsafe" than in C
    or Pascal. One might argue that that defeats the purpose of the
    language, and you'd be right to a point. The difference is that
    in C or Pascal, the entire program is what Rust would call
    "unsafe", whereas generally this can be restricted to just a few
    lines of carefully audited code in Rust.

    Given that I do not understand the automagic stuff done by the Rust
    compiler enough to get this kind of tree insertion past the borrow
    checker, even though it is possible, I don't trust my capabilities to
    audit unsafe Rust.

    Is it significantly different than doing so in C, Pascal, or
    Forth? See the second program, above; the use of `unsafe` in
    the bits that correspond to your sketch in C is constrained to
    two places that indirect raw pointers.

    It is true that I think it is more difficult to write _correct_
    `unsafe` code in Rust than in, say, C, because the language is
    more demanding of the programmer. But in return for that
    investment in effort, you get substantally more type and memory
    safety.

    FWIW, for things like containers of various kinds (trees, hash
    tables, linked-lists, growable arrays, etc), production programs
    usually use an existing library or crate instead of rolling
    their own.

    When I saw that this technique cannot be implemented in Java, I
    wondered why Java was not having references to fields (answer:
    probably because that would make garbage collection more costly and/or
    less precise), that this technique is needed for relatively few
    things, mostly stuff that's in libraries. And I wondered how these
    library methos are implemented (at one point I guessed that they
    implemented these parts of their libraries in C, but when I actually
    look, the implementation of what I looked at was in Java, IIRC along
    the lines of 1)).

    For a language that aspires to replace C for systems programming,
    giving the Java answer is not good enough, though.

    Fortunately for Rust, that's not what it does.

    At some point I thought that one would have to use the more
    heavy-weight reference-counting stuff to get through the borrow
    checker.

    Then I found the book "Learn Rust With Entirely Too Many Linked
    Lists", and I found that the problem could be solved without reference >>>counting after all. However, the problem in this case is the usual
    thing with type inference: There is so much going on that happens >>>automagically (in particular automatic dereferencing in Rust), and the >>>Rust compiler does not know where you went wrong, so the error message
    is unhelpful (while for the simpler cases the compiler usually guesses >>>right and gives very helpful, to-the-point error messages).

    Even after reading the second book, I am still disillusioned with
    Rust. The Rust fans tend to say: "If it compiles, it runs." The
    problem is that it can be harder to get a Rust program to compile than
    to debug a program in, say, Forth (which has barely any checking).
    For debugging, I at least have a method on how to proceed. When I get
    an unhelpful compiler error, I am left to trying various things,
    blindly.

    Sounds like you're in what we'd call the, "fighting the borrow
    checker" stage. It may be that you end up really not liking
    Rust or feeling that other languages are better, but I wager
    that just about anyone who has seriously used the language has
    gone through similar things.

    I wonder about that.

    It's so common that "fighting the borrow checker" is the
    community's slogan to describe it. Another common phenomenon
    for those who put in the work and come out the other side of
    that is that one finds the discipline it imposes on how one
    thinks of one's programs makes one a better programmers in
    unsafe languages, like C.

    From the books I read, I get the impression that
    Rust used to have less automagic stuff, and much of the automagic was
    added later, as a convenience for the programmers who already knew the >language. So the Rust programmers of the early days formed a mental
    model based on "Pedestrian Rust" (i.e., with less automagic), and when
    they see a program with more automagic, and need to understand some >conveniently short piece of Rust code in detail, they form a mental
    model of the code in Pedestrian Rust. But how can a Rust programmer
    who starts now learn Pedestrian Rust and how it relates to the modern >language. There are hints in the books I read, but not enough to get
    a firm foundation.

    Early Rust was very different, and much closer to a managed
    language with garbage collection, green threads, a heavy-weight
    runtime, etc. Put another way, the trend has been less and less
    "automagic" (as you put it) and making things more explicit over
    time.

    You keep conflating things you are not familiar with in a
    language you are not familiar with, with the idea that those
    things are "automagic." But really there is no magic here, and
    I suspect the answer to your question about how one learns to
    adapt their mental model of Rust is to first stop thinking of it
    as magical.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Michael S@[email protected] to comp.arch on Wed Oct 7 22:25:39 2026
    From Newsgroup: comp.arch

    On Mon, 5 Oct 2026 11:36:59 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index
    variables.

    In Rust, the only acceptable array index is of type "usize",
    which is effectively the same as u64 on most platforms these
    days, but does not need to be so: On a 32-bit target, it would
    be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such
    indices are verified against the actual array upper boundary.
    [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.


    Are you sure?
    Methinks The Book states otherwise: https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access





    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Oct 7 19:57:20 2026
    From Newsgroup: comp.arch

    In article <11a5lqo$2v5bq$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    In article <11a3ofn$2ak3s$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index variables. >>>>>>>>
    In Rust, the only acceptable array index is of type "usize", which is >>>>>>>> effectively the same as u64 on most platforms these days, but does not >>>>>>>> need to be so: On a 32-bit target, it would be the same as u32. >>>>>>>>
    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of >>>>>>> the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial >>>>>> underpinnings of Rust safety. Even more importantly, all such indices >>>>>> are verified against the actual array upper boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.

    That's what I assumed, except I check my assumptions by godbolt'ing my
    code, using -O -C "target_cpu=native" to get a bleeding edge optimized
    build, and that's where I saw a loop which started with

    while pos+7 < input.len() {
    ... here a bunch of accesses to input[p], input[p+1]...input[p+6]
    }

    Every one of those secodnary accesses were very obviously tested against >>> the input length (stored in a register for the duration)

    I considered reporting this as a very clear performance bug!

    I wonder if what you saw was actually loop unrolling. I just
    tried something similar, albeit with a very simple sum in the
    loop body, and don't see a bunch of extraneous bounds checks.

    Not unrolling, the code is here:

    https://godbolt.org/z/YnY5G1K49

    The interesting part starts at line 28/29 .LBB0_02 in the asm window
    where you can see (starting at line 37) a bunch of cmp r8,rsi / jae
    <panic> sequences.

    So you're not actually indexing into an _array_ here, but rather
    into a _slice_, which is always bounds-checked in safe Rust
    code.

    I do wonder, though, whether you'd get better object code using
    `input.iter().chunks(n)` or `.windows(n)`.

    I guess I can try this, I'm guessing chunks(7) will error out if less
    than 7 elements are available?

    But really what you want is to find all subsequences matching
    the regular expression , `mul([0-9]+,[0-9]+)`, and then extract
    the two numeric fields, multiple them, and sum the results.
    That's a reduction over a sequence of products, which is itself
    a reduction over a sequence of patches that yield pairs of
    integers.

    I understand you want raw speed; a way to get that is to
    implement the DFA corresponding to that regexp by hand, and use
    something like Boyer Moore to handle the fixed part.

    The code is from Advent of Code 2024 day 3, >https://github.com/TerjeWiigMathisen/aoc/tree/master/2024/day03

    Oh, I don't do Advent of Code, but looking briefly at the
    problem description I don't see why you couldn't use `&str` to
    represent the input data. Nothing says it's not UTF-8, and from
    what I understand, the guy who runs it usually isn't a jerk
    about that kind of thing. I haven't tried to benchmark your
    code against, e.g., this:

    BEGIN<---
    pub fn next(mut s: &str) -> Option<(i32, &str)> {
    while !s.is_empty() {
    let pos = s.find("mul(")?;
    let (_, rest) = s.split_at(pos + 4);
    s = rest;
    let comma = s.find(|c: char| !c.is_ascii_digit())?;
    let (adigits, rest) = s.split_at(comma);
    let Some(rest) = rest.strip_prefix(",") else {
    s = rest;
    continue;
    };
    let a = adigits.parse::<i32>().ok()?;
    let rparen = rest.find(|c: char| !c.is_ascii_digit())?;
    let (bdigits, rest) = rest.split_at(rparen);
    let Some(rest) = rest.strip_prefix(")") else {
    s = rest;
    continue;
    };
    let b = bdigits.parse::<i32>().ok()?;
    return Some((a * b, rest));
    }
    None
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn invalids() {
    assert_eq!(next("aaa"), None);
    assert_eq!(next(""), None);
    assert_eq!(next("mul(3,5"), None);
    assert_eq!(next("mul(3, 5"), None);
    assert_eq!(next("mul(3,"), None);
    assert_eq!(next("mul(3"), None);
    assert_eq!(next("mul("), None);
    }

    #[test]
    fn aproduct() {
    let s = "abmul[1,2]mul(3,5)a";
    assert_eq!(next(s), Some((15, "a")));
    }

    #[test]
    fn mproducts() {
    let s = "mul(1,2)mul(3,4)";
    let aopt = next(s);
    assert_eq!(aopt, Some((2, "mul(3,4)")));
    let (a, s) = aopt.unwrap();
    let bopt = next(s);
    assert_eq!(bopt, Some((12, "")));
    let (b, s) = bopt.unwrap();
    assert_eq!(next(s), None);
    assert_eq!(a * b, 24);
    }
    }
    END<---

    I suspect yours may be somewhat faster, though this likely does
    more error checking. YMMV.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Oct 7 20:05:50 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    On Mon, 5 Oct 2026 11:36:59 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index
    variables.

    In Rust, the only acceptable array index is of type "usize",
    which is effectively the same as u64 on most platforms these
    days, but does not need to be so: On a 32-bit target, it would
    be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such
    indices are verified against the actual array upper boundary.
    [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.


    Are you sure?
    Methinks The Book states otherwise: >https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access

    If the array length and index are known ahead of time, or can be
    statically proven to be valid, then yes, the checks are (likely)
    removed in release mode as part of the ordinary optimization
    passes. In the case where one has already asserted that
    `p + n < arr.len()`, then I would expect any index `arr[p + k]`
    to omit the bounds check, for k < n.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Michael S@[email protected] to comp.arch on Wed Oct 7 23:25:31 2026
    From Newsgroup: comp.arch

    On Wed, 7 Oct 2026 20:05:50 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    On Mon, 5 Oct 2026 11:36:59 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index
    variables.

    In Rust, the only acceptable array index is of type "usize",
    which is effectively the same as u64 on most platforms these
    days, but does not need to be so: On a 32-bit target, it
    would be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99%
    of the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more
    crucial underpinnings of Rust safety. Even more importantly,
    all such indices are verified against the actual array upper
    boundary. [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.


    Are you sure?
    Methinks The Book states otherwise: >https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access


    If the array length and index are known ahead of time, or can be
    statically proven to be valid, then yes, the checks are (likely)
    removed in release mode as part of the ordinary optimization
    passes.

    That is not different from any other bound-checked language that has
    optimizing compiler.

    In the case where one has already asserted that
    `p + n < arr.len()`, then I would expect any index `arr[p + k]`
    to omit the bounds check, for k < n.

    - Dan C.



    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Wed Oct 7 22:31:12 2026
    From Newsgroup: comp.arch

    Dan Cross wrote:
    In article <11a5lqo$2v5bq$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Not unrolling, the code is here:

    https://godbolt.org/z/YnY5G1K49

    The interesting part starts at line 28/29 .LBB0_02 in the asm window
    where you can see (starting at line 37) a bunch of cmp r8,rsi / jae
    <panic> sequences.

    So you're not actually indexing into an _array_ here, but rather
    into a _slice_, which is always bounds-checked in safe Rust
    code.

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable
    array.

    Even if it isn't, the fact that I start by verifying that the slice
    extends at least 7 bytes past the p index means that all of p,
    p+1,p+2...p+6 must be valid, right?


    I do wonder, though, whether you'd get better object code using
    `input.iter().chunks(n)` or `.windows(n)`.

    I guess I can try this, I'm guessing chunks(7) will error out if less
    than 7 elements are available?

    But really what you want is to find all subsequences matching
    the regular expression , `mul([0-9]+,[0-9]+)`, and then extract
    the two numeric fields, multiple them, and sum the results.
    That's a reduction over a sequence of products, which is itself
    a reduction over a sequence of patches that yield pairs of
    integers.

    Yes, doing it that way is easy!

    I understand you want raw speed; a way to get that is to
    implement the DFA corresponding to that regexp by hand, and use
    something like Boyer Moore to handle the fixed part.

    Collectively we've tried all sorts of stuff in order to come up with the absolute fastest solution, without having to use unsafe code.


    The code is from Advent of Code 2024 day 3,
    https://github.com/TerjeWiigMathisen/aoc/tree/master/2024/day03

    Oh, I don't do Advent of Code, but looking briefly at the
    problem description I don't see why you couldn't use `&str` to

    I typically pass the input to my process(inp:&str) function that way,
    but by turning it into a naked byte array (slice?), indexing is better
    defined with no need to iterate over a potential UTF8 sequence (even
    though we know there won't be any).

    represent the input data. Nothing says it's not UTF-8, and from
    what I understand, the guy who runs it usually isn't a jerk
    about that kind of thing. I haven't tried to benchmark your
    code against, e.g., this:

    What you implement here is just Part1: After solving that you get to
    read Part2 where you have to also detect "do()" and "don't()" sequences,
    so that the multiplication result only adds to the part2 result if we're
    not following a don't().

    BEGIN<---
    pub fn next(mut s: &str) -> Option<(i32, &str)> {
    while !s.is_empty() {
    let pos = s.find("mul(")?;
    let (_, rest) = s.split_at(pos + 4);
    s = rest;
    let comma = s.find(|c: char| !c.is_ascii_digit())?;
    let (adigits, rest) = s.split_at(comma);
    let Some(rest) = rest.strip_prefix(",") else {
    s = rest;
    continue;
    };
    let a = adigits.parse::<i32>().ok()?;
    let rparen = rest.find(|c: char| !c.is_ascii_digit())?;
    let (bdigits, rest) = rest.split_at(rparen);
    let Some(rest) = rest.strip_prefix(")") else {
    s = rest;
    continue;
    };
    let b = bdigits.parse::<i32>().ok()?;
    return Some((a * b, rest));
    }
    None
    }

    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn invalids() {
    assert_eq!(next("aaa"), None);
    assert_eq!(next(""), None);
    assert_eq!(next("mul(3,5"), None);
    assert_eq!(next("mul(3, 5"), None);
    assert_eq!(next("mul(3,"), None);
    assert_eq!(next("mul(3"), None);
    assert_eq!(next("mul("), None);
    }

    #[test]
    fn aproduct() {
    let s = "abmul[1,2]mul(3,5)a";
    assert_eq!(next(s), Some((15, "a")));
    }

    #[test]
    fn mproducts() {
    let s = "mul(1,2)mul(3,4)";
    let aopt = next(s);
    assert_eq!(aopt, Some((2, "mul(3,4)")));
    let (a, s) = aopt.unwrap();
    let bopt = next(s);
    assert_eq!(bopt, Some((12, "")));
    let (b, s) = bopt.unwrap();
    assert_eq!(next(s), None);
    assert_eq!(a * b, 24);
    }
    }
    END<---

    I suspect yours may be somewhat faster, though this likely does
    more error checking. YMMV.


    My guess is that your code (after adding the simultaneous scans for the do/don't markers) will take about twice as long (or more?) as the 7 us
    both my own and u/maneatingape (reddit handle of the guy with the
    fastest Rust solutions to all the 500+ puzzles) needs to deliver the result.

    He is famous for solving every one of them in less than half a second:

    His current total is 462 milliseconds for 524 stars.

    I've managed to improve slightly on his code a few times, but more often
    he is ahead of the best I've been able to come up with.

    Collectively, the group discussing this on reddit have tried many
    different search approaches, including unaligned load of 4 bytes as a
    u32, then compare that directly against "mul(", "do()" and "don'" in a
    match statement. For me, this was slower in Rust, but a C programmer
    gained a us or two this way.

    Setting up a combined Boyer-Moore would be doable:

    If the fourth byte is '(', ')' or "'", check the preceeding bytes, if
    not a match use a pre-computed skip table (maybe even one for each
    initial match?) to increment the pointer. Hmmm... maybe I'll try this!

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Wed Oct 7 22:36:30 2026
    From Newsgroup: comp.arch

    Dan Cross wrote:
    In article <[email protected]>,
    Michael S <[email protected]> wrote:
    On Mon, 5 Oct 2026 11:36:59 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index
    variables.

    In Rust, the only acceptable array index is of type "usize",
    which is effectively the same as u64 on most platforms these
    days, but does not need to be so: On a 32-bit target, it would
    be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such
    indices are verified against the actual array upper boundary.
    [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.


    Are you sure?
    Methinks The Book states otherwise:
    https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access

    If the array length and index are known ahead of time, or can be
    statically proven to be valid, then yes, the checks are (likely)
    removed in release mode as part of the ordinary optimization
    passes. In the case where one has already asserted that
    `p + n < arr.len()`, then I would expect any index `arr[p + k]`
    to omit the bounds check, for k < n.

    That's exactly what I expected when I wrote my code the way I did. I
    know that Java specifically handles this situation, lifting any array
    bounds checking out of inner loops if possible.

    As that Godbolt link shows, my expectation was wrong.

    OTOH, for such a small program having 6 extra branches, none of them
    ever taken, is not likely to add any cycles to the program latency since current CPUs have more than enough issue ports to be able to handle
    these branches while processing the real code.

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Wed Oct 7 16:49:52 2026
    From Newsgroup: comp.arch

    On 2026-Oct-07 15:25, Michael S wrote:
    https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access

    Does Rust allow zero length arrays, vectors, strings and slices?
    Ada calls these a null slice.
    In DEC Fortran 77 a zero length slice was a bounds check error,
    which was really irritating because in string manipulation they happen a lot. If so, how does one specify a null slice in Rust?
    I couldn't find any definition in that doc.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Wed Oct 7 22:13:50 2026
    From Newsgroup: comp.arch

    On 2026-Oct-07 16:49, EricP wrote:
    On 2026-Oct-07 15:25, Michael S wrote:
    https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access

    Does Rust allow zero length arrays, vectors, strings and slices?
    Ada calls these a null slice.
    In DEC Fortran 77 a zero length slice was a bounds check error,
    which was really irritating because in string manipulation they happen a lot. If so, how does one specify a null slice in Rust?
    I couldn't find any definition in that doc.



    I asked the Google AI "does programming language rust support
    zero length array slices" and it said:

    Yes, Rust fully supports zero-length (empty) array slices.
    A slice in Rust ([T]) represents a dynamically-sized view into
    a contiguous sequence of elements. Because the length is evaluated
    at runtime, a slice can safely have a length of 0.

    How to Create an Empty Slice

    You can create or obtain a zero-length slice in Rust in several ways:

    From an empty array or vector:

    let empty_array: [i32; 0] = [];
    let slice: &[i32] = &empty_array; // A zero-length slice

    Using a range that yields no elements:

    let numbers = [1, 2, 3, 4];
    let empty_slice = &numbers[2..2]; // Length is 0

    Directly via standard library constants:

    let empty_slice: &[i32] = &[];

    Memory Layout of a Zero-Length Slice

    Under the hood, a slice reference (&[T]) is a fat pointer
    consisting of two components:

    1. A pointer to the data.
    2. The length of the slice (usize).

    For a zero-length slice, the length component is 0. To avoid using
    a null pointer (which Rust references cannot be), the data pointer
    component points to a well-aligned, non-null "dangling" pointer
    address (specifically NonNull::dangling()), making it completely
    safe and overhead-free.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Thu Oct 8 05:46:50 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> schrieb:
    On 2026-Oct-07 15:25, Michael S wrote:
    https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access

    Does Rust allow zero length arrays, vectors, strings and slices?
    Ada calls these a null slice.
    In DEC Fortran 77 a zero length slice was a bounds check error,

    Modern Fortran does allow it, and yes, it is very convenient.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Thu Oct 8 12:05:31 2026
    From Newsgroup: comp.arch

    On 2026-Oct-07 15:25, Michael S wrote:
    On Mon, 5 Oct 2026 11:36:59 -0000 (UTC)
    [email protected] (Dan Cross) wrote:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:

    Terje Mathisen <[email protected]> writes:

    Tim Rentsch wrote:
    [...]
    I favor a type equivalent to size_t for use as array index
    variables.

    In Rust, the only acceptable array index is of type "usize",
    which is effectively the same as u64 on most platforms these
    days, but does not need to be so: On a 32-bit target, it would
    be the same as u32.

    So effectively very similar to size_t.

    Maybe that limitation works okay in Rust. In C sometimes it's
    important to allow a signed value for indexing. Probably 99% of
    the time size_t is good, but not always.

    The fact that array indices are unsigned is one of the more crucial
    underpinnings of Rust safety. Even more importantly, all such
    indices are verified against the actual array upper boundary.
    [...]

    It's nice that array index values are checked against the array
    bounds.

    Terje was actually incorrect here; bounds checks are added in
    debug mode, but are (usually) removed in optimized builds.


    Are you sure?
    Methinks The Book states otherwise: https://doc.rust-lang.org/book/ch03-02-data-types.html?highlight=array#invalid-array-element-access

    The reference is less see-spot-run

    https://doc.rust-lang.org/stable/reference/

    Looking at the rules for type casting:

    https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#numeric-cast

    Some initial thoughts:

    There are no value range checks between cast types, just bit-wise reinterpretation
    to the new type with possible sign/zero extension or truncation for size change.

    They don't define the order in which those two operations, type and size change, occur.
    If one casts an i8 into a u16 it doesn't say whether the value is sign extended to i16 first then type changed to u16, or type changed to u8 first then zero extended to u16.

    Since an array index is defined as type usize, if the index expression is signed
    then it will be bit-wise reinterpreted as unsigned, and possibly turn a negative
    value into a large positive usize value. If bounds checks are disabled for production builds then you get exactly the same array overflow as in C.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.programming,comp.arch on Thu Oct 8 21:28:55 2026
    From Newsgroup: comp.arch

    [Cross-post to comp.programming, Followup-To: comp.programming]

    In article <11a6a6h$37i1n$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    In article <11a5lqo$2v5bq$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Not unrolling, the code is here:

    https://godbolt.org/z/YnY5G1K49

    The interesting part starts at line 28/29 .LBB0_02 in the asm window
    where you can see (starting at line 37) a bunch of cmp r8,rsi / jae
    <panic> sequences.

    So you're not actually indexing into an _array_ here, but rather
    into a _slice_, which is always bounds-checked in safe Rust
    code.

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable >array.

    Sadly, though, it's not. :-/

    Even if it isn't, the fact that I start by verifying that the slice
    extends at least 7 bytes past the p index means that all of p,
    p+1,p+2...p+6 must be valid, right?

    One would think!

    I do wonder, though, whether you'd get better object code using
    `input.iter().chunks(n)` or `.windows(n)`.

    I guess I can try this, I'm guessing chunks(7) will error out if less
    than 7 elements are available?

    But really what you want is to find all subsequences matching
    the regular expression , `mul([0-9]+,[0-9]+)`, and then extract
    the two numeric fields, multiple them, and sum the results.
    That's a reduction over a sequence of products, which is itself
    a reduction over a sequence of patches that yield pairs of
    integers.

    Yes, doing it that way is easy!

    Well, I mean as a conceptual thing; not necessary using a regex
    library. That is, mentally write the patterns down as a DFA and
    then write that out as code+state tables.

    [snip]
    represent the input data. Nothing says it's not UTF-8, and from
    what I understand, the guy who runs it usually isn't a jerk
    about that kind of thing. I haven't tried to benchmark your
    code against, e.g., this:

    What you implement here is just Part1: After solving that you get to
    read Part2 where you have to also detect "do()" and "don't()" sequences,
    so that the multiplication result only adds to the part2 result if we're
    not following a don't().

    Ah. Yeah, I'm going to refrain; it's cool that folks are excited
    about that, but it's not my favorite biscuit.

    [snip]
    I suspect yours may be somewhat faster, though this likely does
    more error checking. YMMV.

    My guess is that your code (after adding the simultaneous scans for the >do/don't markers) will take about twice as long (or more?) as the 7 us
    both my own and u/maneatingape (reddit handle of the guy with the
    fastest Rust solutions to all the 500+ puzzles) needs to deliver the result.

    Maybe? I dunno; the library is probably optimized to use pretty
    efficient algorithms, and/or simd and all that jazz.

    [snip]
    Setting up a combined Boyer-Moore would be doable:

    If the fourth byte is '(', ')' or "'", check the preceeding bytes, if
    not a match use a pre-computed skip table (maybe even one for each
    initial match?) to increment the pointer. Hmmm... maybe I'll try this!

    Right. This lets you potentially go sub-linear looking for the
    tokens you care about. I wonder how it would stack up against
    using something like `memchr::memmem`, though, since that'll get
    you hardware acceleration using SIMD instructions and goofy
    stuff like that.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Thu Oct 8 22:05:40 2026
    From Newsgroup: comp.arch

    In article <11a6agf$37m70$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    [snip]
    If the array length and index are known ahead of time, or can be
    statically proven to be valid, then yes, the checks are (likely)
    removed in release mode as part of the ordinary optimization
    passes. In the case where one has already asserted that
    `p + n < arr.len()`, then I would expect any index `arr[p + k]`
    to omit the bounds check, for k < n.

    That's exactly what I expected when I wrote my code the way I did. I
    know that Java specifically handles this situation, lifting any array
    bounds checking out of inner loops if possible.

    As that Godbolt link shows, my expectation was wrong.

    OTOH, for such a small program having 6 extra branches, none of them
    ever taken, is not likely to add any cycles to the program latency since >current CPUs have more than enough issue ports to be able to handle
    these branches while processing the real code.

    I thought it was worth a small experiment; here's a silly pair
    of functions:

    ```rust
    #[inline(never)]
    pub fn sum_sub_array(arr: [u32; 4], k: usize) -> u32 {
    if k + 3 > arr.len() {
    return 0;
    }
    arr[k] + arr[k + 1] + arr[k + 2]
    }

    #[inline(never)]
    pub fn sum_sub_slice(sl: &[u32], k: usize) -> u32 {
    if k + 3 > sl.len() {
    return 0;
    }
    sl[k] + sl[k + 1] + sl[k + 2]
    }
    ```

    If I run these in such a way that they're not totally optimized
    away and do a release build, they generate the exact same code:

    ```
    0000000000015cb0 <rust_indices::sum_sub_array>:
    15cb0: 31 c0 xor %eax,%eax
    15cb2: 48 83 fe 01 cmp $0x1,%rsi
    15cb6: 77 0b ja 15cc3 <rust_indices::sum_sub_array+0x13>
    15cb8: 8b 44 b7 04 mov 0x4(%rdi,%rsi,4),%eax
    15cbc: 03 04 b7 add (%rdi,%rsi,4),%eax
    15cbf: 03 44 b7 08 add 0x8(%rdi,%rsi,4),%eax
    15cc3: c3 ret
    15cc4: cc int3
    ...
    15ccf: cc int3

    0000000000015cd0 <rust_indices::sum_sub_slice>:
    15cd0: 31 c0 xor %eax,%eax
    15cd2: 48 83 fe 01 cmp $0x1,%rsi
    15cd6: 77 0b ja 15ce3 <rust_indices::sum_sub_slice+0x13>
    15cd8: 8b 44 b7 04 mov 0x4(%rdi,%rsi,4),%eax
    15cdc: 03 04 b7 add (%rdi,%rsi,4),%eax
    15cdf: 03 44 b7 08 add 0x8(%rdi,%rsi,4),%eax
    15ce3: c3 ret
    15ce4: cc int3
    ...
    15cef: cc int3
    ```

    Perhaps it's something about the complexity of your function
    that exceed some sort of threshold for the optimizer.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Thu Oct 8 22:55:00 2026
    From Newsgroup: comp.arch

    In article <jbPxS.600$[email protected]>,
    EricP <[email protected]> wrote:
    [snip]
    The reference is less see-spot-run

    https://doc.rust-lang.org/stable/reference/

    Looking at the rules for type casting:

    https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#numeric-cast

    Some initial thoughts:

    There are no value range checks between cast types, just bit-wise reinterpretation
    to the new type with possible sign/zero extension or truncation for size change.

    They don't define the order in which those two operations, type and size change, occur.
    If one casts an i8 into a u16 it doesn't say whether the value is sign extended
    to i16 first then type changed to u16, or type changed to u8 first then zero >extended to u16.

    They do, but it requires looking at _two_ rules from that list.

    The first,

    |Casting between two integers of the same size (e.g. i32 -> u32)
    |is a no-op (Rust uses 2’s complement for negative values of
    |fixed integers)

    and the third,

    |Casting from a smaller integer to a larger integer (e.g. u8 ->
    |u32) will
    |
    | * zero-extend if the source is unsigned
    | * sign-extend if the source is signed

    Note that the second of these, the third rule on the list,
    mentions sign extension if the _source_ type is signed; the
    signedness of the destination is not relevant.

    Taken together, these imply that the source will be
    sign-extended and then the conversion to unsigned will be
    treated as a noop: rule #1 does not apply first because it is
    specific to conversions between sign/unsigned integers of the
    _same width_; so we start with rule #3, sign-extend, and _then_
    we may apply rule #1.

    For a negative value of a source type that is smaller than an
    unsigned destination type, the result will be an unsigned value
    in the upper half of the destination type's range.

    term% cat cast.rs
    fn main() {
    let n: i8 = -2;
    println!("-2 as u16 = {}", n as u16);
    }
    term% rustc cast.rs
    term% ./cast
    -2 as u16 = 65534
    term%

    If you wanted to force specific behavior, you could use multiple
    casts (e.g., `n as u8 as u16`).

    Since an array index is defined as type usize, if the index expression
    is signed then it will be bit-wise reinterpreted as unsigned,

    No, if the index expression is signed, it will be a compile-time
    error since usize is, by definiton, unsigned, and usize is the
    only permissible index type. Rust is statically typed, and
    there are no implicit conversions that factor here.

    and possibly turn a negative value into a large positive usize
    value.

    In order for this to happen, one would have to use an explicit
    cast to make the negative value a `usize` first.

    A fair followup question is, "suppose I do a comparison against
    a signed type, but don't account for zero, and then convert it
    to usize; could the optimizer remove the bounds checks?" For
    example, this:

    ```rust
    #[inline(never)]
    pub fn get_a_number(k: i32) -> i32 {
    const ARR: [i32; 10] = [500, 1, 200, 3, 410, 5, 37, 7, 8, -5];
    if k < 10 { ARR[k as usize] } else { 0 }
    }
    ```

    When built with optimization enabled and linked with a simple
    driver, panic's at runtime trying to use a negative value cast
    to a (large) positive value as an index. On the other hand, if
    I modify it to check the lower bound on the array, that is
    optimized away. So this version,

    ```rust
    #[inline(never)]
    pub fn get_a_number(k: i32) -> i32 {
    const ARR: [i32; 10] = [500, 1, 200, 3, 410, 5, 37, 7, 8, -5];
    if 0 <= k && k < 10 { ARR[k as usize] } else { 0 }
    }
    ```
    yields this object code on x86_64:
    ```
    0000000000014ad0 <rbounds::get_a_number>:
    14ad0: 31 c0 xor %eax,%eax
    14ad2: 83 ff 0a cmp $0xa,%edi
    14ad5: 73 0c jae 14ae3 <rbounds::get_a_number+0x13>
    14ad7: 89 f8 mov %edi,%eax
    14ad9: 48 8d 0d 04 19 ff ff lea -0xe6fc(%rip),%rcx # 63e4 <__abi_tag+0x60e8>
    14ae0: 8b 04 81 mov (%rcx,%rax,4),%eax
    14ae3: c3 ret
    14ae4: cc int3
    ...
    14aef: cc int3
    ```

    If bounds checks are disabled for
    production builds then you get exactly the same array overflow as in C.

    See above.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Fri Oct 9 16:36:48 2026
    From Newsgroup: comp.arch

    Dan Cross wrote:
    In article <11a6agf$37m70$[email protected]>,
    Terje Mathisen <[email protected]> wrote:
    Dan Cross wrote:
    [snip]
    If the array length and index are known ahead of time, or can be
    statically proven to be valid, then yes, the checks are (likely)
    removed in release mode as part of the ordinary optimization
    passes. In the case where one has already asserted that
    `p + n < arr.len()`, then I would expect any index `arr[p + k]`
    to omit the bounds check, for k < n.

    That's exactly what I expected when I wrote my code the way I did. I
    know that Java specifically handles this situation, lifting any array
    bounds checking out of inner loops if possible.

    As that Godbolt link shows, my expectation was wrong.

    OTOH, for such a small program having 6 extra branches, none of them
    ever taken, is not likely to add any cycles to the program latency since
    current CPUs have more than enough issue ports to be able to handle
    these branches while processing the real code.

    I thought it was worth a small experiment; here's a silly pair
    of functions:

    Thanks for testing!

    ```rust
    #[inline(never)]
    pub fn sum_sub_array(arr: [u32; 4], k: usize) -> u32 {
    if k + 3 > arr.len() {
    return 0;
    }
    arr[k] + arr[k + 1] + arr[k + 2]
    }

    #[inline(never)]
    pub fn sum_sub_slice(sl: &[u32], k: usize) -> u32 {
    if k + 3 > sl.len() {
    return 0;
    }
    sl[k] + sl[k + 1] + sl[k + 2]
    }
    ```

    If I run these in such a way that they're not totally optimized
    away and do a release build, they generate the exact same code:

    That's actually what I would expect, re my post about a read-only array
    and ditto slice being the same.

    ```
    0000000000015cb0 <rust_indices::sum_sub_array>:
    15cb0: 31 c0 xor %eax,%eax
    15cb2: 48 83 fe 01 cmp $0x1,%rsi
    15cb6: 77 0b ja 15cc3 <rust_indices::sum_sub_array+0x13>
    15cb8: 8b 44 b7 04 mov 0x4(%rdi,%rsi,4),%eax
    15cbc: 03 04 b7 add (%rdi,%rsi,4),%eax
    15cbf: 03 44 b7 08 add 0x8(%rdi,%rsi,4),%eax
    15cc3: c3 ret
    15cc4: cc int3
    ...
    15ccf: cc int3

    0000000000015cd0 <rust_indices::sum_sub_slice>:
    15cd0: 31 c0 xor %eax,%eax
    15cd2: 48 83 fe 01 cmp $0x1,%rsi
    15cd6: 77 0b ja 15ce3 <rust_indices::sum_sub_slice+0x13>
    15cd8: 8b 44 b7 04 mov 0x4(%rdi,%rsi,4),%eax
    15cdc: 03 04 b7 add (%rdi,%rsi,4),%eax
    15cdf: 03 44 b7 08 add 0x8(%rdi,%rsi,4),%eax
    15ce3: c3 ret
    15ce4: cc int3
    ...
    15cef: cc int3
    ```

    Perhaps it's something about the complexity of your function
    that exceed some sort of threshold for the optimizer.


    Quite possibly so...

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch,comp.programming on Sat Oct 10 14:18:00 2026
    From Newsgroup: comp.arch

    [email protected] (Dan Cross) writes:
    [Crossposted and and Followup-To: comp.programming]

    Given that I don't subscribe to c.p, I kept comp.arch in the
    Newsgroups.

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    My goal was to implement search tree insertion as efficiently and
    compactly as in Pascal, C or Forth (and in contrast to Java), ideally
    in safe Rust. Here it is in C (untested):

    struct tree {
    int key; /* or some other key type */
    int val; /* or some other value type */
    struct tree *left;
    struct tree *right;
    };

    struct tree **search(struct tree **rootp, int key)
    {
    while ((root=*rootp)!=NULL && key != root->key) {
    if (key < root->key)
    rootp = &(root->left);
    else
    rootp = &(root->right);
    }
    return rootp;
    }

    An intersting way to write it; I understand you're trying to
    reuse `search` in `insert`, below, and thus why you want to
    return `rootp`.

    void insert(struct tree **rootp, int key, int val)
    {
    struct tree **nodep = **search(rootp, key);

    I know you said this was untested; clearly you should not
    indirect the result of `search` here.

    struct tree *node = *nodep;
    if (node == NULL) {
    node = malloc(sizeof(tree));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    *nodep = node;
    }
    node->val = val;
    }

    Passing a reference to a mutable field is what is done in these
    languages, and I don't know an efficient and similarly compact
    technique that does not pass a reference to a mutable field.

    I also knew Java solutions to the problem:

    1) Special-case the root (which you typically have to do in Java
    anyway, and otherwise always keep a pointer to the last tree node
    around, and also keep the knowledge about the last left-or-right
    decision around (or take it again). This all leads to quite a bit of
    code that is duplicated relative to the code above.

    2) Recurse into the tree and rewrite all the links to the subtrees
    along the spine of the search. This is not quite as duplicative as 1)
    (but the root still has to be handled separately), but it performs
    several writes instead of just one, and the recursion is not a tail >>recursion, so is relatively expensive.

    Or, 3) Simply reimplement the comparison in `insert`

    That goes along with 1), but I forgot to mention it.

    it's a trivial amount of code.

    There's the rub. I don't consider a factor of ~2 trivial, even if it
    is only 20 lines of code. And in particular, "simply reimplement"
    means that the same thing occurs several times in the code, and in
    maintainance one must remember to change all of the occurences.

    Fortunately, that's not necessary in Rust. But the fact that you did
    not find it tells me that Rust is hard even for experienced Rust
    programmers.

    I chose a variant of (1) for my implementation, but only because
    it was easy, not because it was required. No unsafe code or
    fancy reference counting here.

    BEGIN `tree.rs`<---
    //! Simple binary search tree.

    use std::cmp::Ordering;
    use std::fmt::Debug;

    /// A binary search tree over any key type that implements a
    /// total ordering.
    pub struct Tree<K: Debug + Ord, V: Debug> {
    root: Option<Box<Node<K, V>>>,
    }

    impl<K: Debug + Ord, V: Debug> Tree<K, V> {
    /// Creates a new, empty tree.
    pub fn new() -> Tree<K, V> {
    Tree { root: None }
    }

    /// Searches the tree for the given key, and returns
    /// `Some(&value)` if it's here, otherwise returns None.
    ///
    /// # Example
    ///
    /// ```
    /// let mut tree = tree::Tree::<i32, i32>::new();
    /// tree.insert(2, 200);
    /// tree.insert(1, 100);
    /// assert_eq!(tree.search(&0), None);
    /// assert_eq!(tree.search(&1), Some(&100));
    /// assert_eq!(tree.search(&2), Some(&200));
    /// ```
    pub fn search(&self, key: &K) -> Option<&V> {
    let mut root = &self.root;
    while let Some(node) = root {
    match key.cmp(&node.key) {
    Ordering::Less => root = &node.left,
    Ordering::Equal => return Some(&node.value),
    Ordering::Greater => root = &node.right,
    }
    }
    None
    }

    /// Inserts the given `value` into the tree with the given
    /// `key`. Returns true if the node was inserted, or false
    /// if the key was already in the tree.
    ///
    /// # Example
    ///
    /// ```
    /// let mut tree = tree::Tree::<i32, i32>::new();
    /// assert_eq!(tree.insert(1, 100), true);
    /// assert_eq!(tree.insert(1, 200), false);
    /// assert_eq!(tree.insert(2, 100), true);
    /// ```
    pub fn insert(&mut self, key: K, value: V) -> bool {
    let mut root = &mut self.root;
    loop {
    let Some(node) = root else {
    root.replace(Node::new(key, value));
    return true;
    };
    match key.cmp(&node.key) {
    Ordering::Less => root = &mut node.left,
    Ordering::Equal => return false,
    Ordering::Greater => root = &mut node.right,
    }
    }
    }

    /// Print the tree, if it is not empty.
    pub fn print(&self) {
    if let Some(node) = &self.root {
    node.print();
    }
    }
    }

    // A node in the tree owns its key, value, and left and right
    // children.
    struct Node<K: Debug + Ord, V: Debug> {
    key: K,
    value: V,
    left: Option<Box<Node<K, V>>>,
    right: Option<Box<Node<K, V>>>,
    }

    impl<K: Debug + Ord, V: Debug> Node<K, V> {
    /// Creates a new boxed node owning the given key and value.
    pub fn new(key: K, value: V) -> Box<Node<K, V>> {
    Box::new(Node {
    key,
    value,
    left: None,
    right: None,
    })
    }

    /// Pre-order print of this node and its children.
    pub fn print(&self) {
    if let Some(left) = &self.left {
    left.print();
    }
    println!("key = {:?}, value = {:?}", self.key, self.value);
    if let Some(right) = &self.right {
    right.print();
    }
    }
    }
    END `tree.rs`<---

    Note that there's more code here than in your C code, but this
    also does a lot more; this is generic over any key and value
    types where the key type implements a total ordering, and where
    both types implement the `Debug` trait (so they can be printed).

    If I don't need any of that, does it get smaller? I don't think so.

    Here's my attempt, and I expect that experienced Rust programmers will
    find ways to improve on it. It includes a lookup function:

    -------------------
    use std::cmp::Ordering;

    pub struct Tree<K: Ord, V> {
    root: Subtree<K, V>,
    }

    type Subtree<K,V> = Option<Box<Node<K,V>>>;

    struct Node<K: Ord, V> {
    key: K,
    val: V,
    left: Subtree<K, V>,
    right: Subtree<K, V>,
    }

    impl<K: Ord,V> Tree<K,V> {
    pub fn new() -> Self {
    Tree { root: None }
    }

    pub fn insert(&mut self, key: K, val: V) {
    let mut t = &mut self.root;
    while let Some(node)=t {
    match key.cmp(&node.key) {
    Ordering::Equal => {node.val = val; return;},
    Ordering::Less => t = &mut node.left,
    Ordering::Greater => t = &mut node.right,
    };
    };
    *t = Some(Box::new(Node {key, val, left: None, right: None}))
    }

    pub fn lookup(&self, key: K) -> Option<&V> {
    let mut t = &self.root;
    while let Some(node)=t {
    match key.cmp(&node.key) {
    Ordering::Equal => return Some(&node.val),
    Ordering::Less => t = &node.left,
    Ordering::Greater => t = &node.right,
    };
    };
    None
    }
    }

    fn main() {
    let mut q1 = Tree::new();
    q1.insert(2,"abc".to_string());
    q1.insert(3,"def".to_string());
    q1.insert(1,"ghi".to_string());
    q1.insert(3,"jkl".to_string());
    println!("{:?}",q1.lookup(1));
    println!("{:?}",q1.lookup(2));
    println!("{:?}",q1.lookup(3));
    }
    ----------------------------

    Like your code and Java code, this has a separate type Tree with a
    field root and with its own new function, while in C a "struct tree *"
    variable can serve as root pointer; and this leads to an additional
    line each in insert and lookup. Can we do without most of that
    (havibf a constructor is useful) and just use Subtree instead of Tree?

    insert now integrates search, because I could not satisfy the compiler
    with a separate search; maybe a more experience Rust programmer can
    separate it out again. The advantage is that there is no need to
    combine the found and notfound cases in one return value in search,
    only to handle them as separate cases in insert. But at least there
    is no duplication of the comparison code.

    The integration of search in insert led to a duplication of most of
    the search code in lookup. However, requiring a mutable reference is
    not necessary for the lookup code, so a search that takes a mutable
    reference and returns a mutable reference is too restrictive for
    lookup. But maybe a more experienced Rust programmer can write a
    generic search that takes an &mut tree for insert, and a & for lookup.

    Of course, if search is factored out, both clients have to explicitly
    check for both outcomes and have code for both. But that's probably
    preferable to duplicating the search.

    though the pointer-to-pointer
    to a node is not returned by `search`. If that is what you mean
    by "technique" then you're right; doing that in Rust is
    inconvenient because it violates the borrow checker's rules
    about the existence of multiple simultaneous mutable references
    to an object.

    Yes, that's what I meant by "technique". And it's possible in safe
    Rust, but I had a very hard time finding it (and for a while thought
    that it is not possible). The fact that you thought it is not
    possible in safe Rust means that it is not just my inexperience at
    fault here.

    But I claim that is just a detail, and not a
    terribly interesting one at that; certainly not an impediment to
    substituting Rust for C in systems programming.

    I think that, in order to be the rightful heir, safe Rust should be
    able to do everything one can do in the other language that's not
    inherently unsafe, and do it without needing to duplicate code of
    other nastyness. You may wonder about "inherently unsafe": I have no definition for that yet, but I would not kno why inserting a node into
    a search tree would ever fall into that category,

    Even so, as I said, one _can_ use the same technique using a
    smallish-bit of `unsafe` code.

    That's cool, but given that I have difficulty understanding why the
    compiler complained about my version with separate search, I feel very uncomfortable in asserting to the compiler that my use of unsafe
    features is safe once we leave the unsafe block.

    Frankly, unless I was doing something very odd, I wouldn't write
    my own binary search tree at all; the standard library's
    collections implementations are pretty good

    And I actually don't use binary search trees anyway, neither
    self-written nor from libraries, because hash tables tend to be the
    better alternative, except when I want to know the next-higher element
    or somesuch. But being able to have a reference to a field is also
    useful when implementing hash tables.

    But anyway, the libraries have to be written in some language, and
    given that Rust fans tell us to rewrite all C code in Rust, I would
    expect that the library should be written in Rust, and, in particular
    in safe Rust (otherwise, why rewrite it in Rust?).

    And actually, we teach our students how to write this stuff (even in
    Java), because we think it teaches them something useful. And at
    least one Rust fan thinks so, too, and wrote "Learn Rust With Entirely
    Too Many Linked Lists", where I found out how to implement the
    mentioned technique in safe Rust.

    Given that I do not understand the automagic stuff done by the Rust >>compiler enough to get this kind of tree insertion past the borrow
    checker, even though it is possible, I don't trust my capabilities to
    audit unsafe Rust.

    Is it significantly different than doing so in C, Pascal, or
    Forth? See the second program, above; the use of `unsafe` in
    the bits that correspond to your sketch in C is constrained to
    two places that indirect raw pointers.

    I may be able to shut up the compiler and get an executable with
    "unsafe", but will the resulting program work? And I can have the
    same assurance in C or Forth, so why should I switch to Rust to get
    that?

    It's so common that "fighting the borrow checker" is the
    community's slogan to describe it.

    Sounds to me like "the community" has found a convenient cliche to
    avoid thinking about why people have difficulties with Rust, and why
    these difficulties may increase as Rust advances.

    From the books I read, I get the impression that
    Rust used to have less automagic stuff, and much of the automagic was
    added later, as a convenience for the programmers who already knew the >>language. So the Rust programmers of the early days formed a mental
    model based on "Pedestrian Rust" (i.e., with less automagic), and when
    they see a program with more automagic, and need to understand some >>conveniently short piece of Rust code in detail, they form a mental
    model of the code in Pedestrian Rust. But how can a Rust programmer
    who starts now learn Pedestrian Rust and how it relates to the modern >>language. There are hints in the books I read, but not enough to get
    a firm foundation.

    Early Rust was very different, and much closer to a managed
    language with garbage collection, green threads, a heavy-weight
    runtime, etc. Put another way, the trend has been less and less
    "automagic" (as you put it) and making things more explicit over
    time.

    What I mean is the automatic dropping, and the fact that the Box stuff
    only occurs in the type declarations, but is automatically
    dereferenced or otherwise automatically handled; "let x = ..."
    produces a type automatically (but I guess you could put a type
    declaration there, though).

    You keep conflating things you are not familiar with in a
    language you are not familiar with, with the idea that those
    things are "automagic." But really there is no magic here, and
    I suspect the answer to your question about how one learns to
    adapt their mental model of Rust is to first stop thinking of it
    as magical.

    The language hides a number of things with automation, and given that
    I have no good mental model for that, following Clarke's third law
    ("Any sufficiently advanced technology is indistinguishable from
    magic."), I refer to that automatic stuff as "automagic stuff". I
    don't think it's magic, it just tries to be indistinguishable. It
    shouldn't.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch,comp.programming on Sat Oct 10 17:31:03 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote: >[email protected] (Dan Cross) writes:
    [Crossposted and and Followup-To: comp.programming]

    Given that I don't subscribe to c.p, I kept comp.arch in the
    Newsgroups.

    Ok. If it doesn't bother the regulars, then I guess that's
    fine. It seems off-topic, but sure.

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    My goal was to implement search tree insertion as efficiently and >>>compactly as in Pascal, C or Forth (and in contrast to Java), ideally
    in safe Rust. Here it is in C (untested):

    struct tree {
    int key; /* or some other key type */
    int val; /* or some other value type */
    struct tree *left;
    struct tree *right;
    };

    struct tree **search(struct tree **rootp, int key)
    {
    while ((root=*rootp)!=NULL && key != root->key) {
    if (key < root->key)
    rootp = &(root->left);
    else
    rootp = &(root->right);
    }
    return rootp;
    }

    An intersting way to write it; I understand you're trying to
    reuse `search` in `insert`, below, and thus why you want to
    return `rootp`.

    void insert(struct tree **rootp, int key, int val)
    {
    struct tree **nodep = **search(rootp, key);

    I know you said this was untested; clearly you should not
    indirect the result of `search` here.

    struct tree *node = *nodep;
    if (node == NULL) {
    node = malloc(sizeof(tree));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    *nodep = node;
    }
    node->val = val;
    }

    Passing a reference to a mutable field is what is done in these >>>languages, and I don't know an efficient and similarly compact
    technique that does not pass a reference to a mutable field.

    I also knew Java solutions to the problem:

    1) Special-case the root (which you typically have to do in Java
    anyway, and otherwise always keep a pointer to the last tree node
    around, and also keep the knowledge about the last left-or-right
    decision around (or take it again). This all leads to quite a bit of >>>code that is duplicated relative to the code above.

    2) Recurse into the tree and rewrite all the links to the subtrees
    along the spine of the search. This is not quite as duplicative as 1) >>>(but the root still has to be handled separately), but it performs >>>several writes instead of just one, and the recursion is not a tail >>>recursion, so is relatively expensive.

    Or, 3) Simply reimplement the comparison in `insert`

    That goes along with 1), but I forgot to mention it.

    It is an important distinction.

    it's a trivial amount of code.

    There's the rub. I don't consider a factor of ~2 trivial, even if it
    is only 20 lines of code.

    Repeating the conditional is 5 lines, including the closing
    brace on a ine by itself. The `while` surrounding it is another
    2, again including the closing brace.

    And in particular, "simply reimplement"
    means that the same thing occurs several times in the code, and in >maintainance one must remember to change all of the occurences.

    It's a loop around a match over a conditional expression:

    while let Some(node) = root {
    root = match key.cmp(&node.key) {
    Ordering::Less => &mut node.left,
    Ordering::Equal => return false,
    Ordering::Greater => &mut node.right,
    };
    }

    And in fact it _has_ to be different in a way, because the
    reference must be mutable, whereas for searching as a lookup,
    that is not necessary.

    You could wrap that in a macro if you really wanted to.

    Fortunately, that's not necessary in Rust. But the fact that you did
    not find it tells me that Rust is hard even for experienced Rust
    programmers.

    I do not know what you are referring to when you write, the
    "fact that you did not find it"; did you not notice that I gave
    you _two_ programs, one of which was pretty much the same as the
    C program you gave? What, exactly, are you referring to?

    I chose a variant of (1) for my implementation, but only because
    it was easy, not because it was required. No unsafe code or
    fancy reference counting here.

    BEGIN `tree.rs`<---
    //! Simple binary search tree.

    use std::cmp::Ordering;
    use std::fmt::Debug;

    /// A binary search tree over any key type that implements a
    /// total ordering.
    pub struct Tree<K: Debug + Ord, V: Debug> {
    root: Option<Box<Node<K, V>>>,
    }

    impl<K: Debug + Ord, V: Debug> Tree<K, V> {
    /// Creates a new, empty tree.
    pub fn new() -> Tree<K, V> {
    Tree { root: None }
    }

    /// Searches the tree for the given key, and returns
    /// `Some(&value)` if it's here, otherwise returns None.
    ///
    /// # Example
    ///
    /// ```
    /// let mut tree = tree::Tree::<i32, i32>::new();
    /// tree.insert(2, 200);
    /// tree.insert(1, 100);
    /// assert_eq!(tree.search(&0), None);
    /// assert_eq!(tree.search(&1), Some(&100));
    /// assert_eq!(tree.search(&2), Some(&200));
    /// ```
    pub fn search(&self, key: &K) -> Option<&V> {
    let mut root = &self.root;
    while let Some(node) = root {
    match key.cmp(&node.key) {
    Ordering::Less => root = &node.left,
    Ordering::Equal => return Some(&node.value),
    Ordering::Greater => root = &node.right,
    }
    }
    None
    }

    /// Inserts the given `value` into the tree with the given
    /// `key`. Returns true if the node was inserted, or false
    /// if the key was already in the tree.
    ///
    /// # Example
    ///
    /// ```
    /// let mut tree = tree::Tree::<i32, i32>::new();
    /// assert_eq!(tree.insert(1, 100), true);
    /// assert_eq!(tree.insert(1, 200), false);
    /// assert_eq!(tree.insert(2, 100), true);
    /// ```
    pub fn insert(&mut self, key: K, value: V) -> bool {
    let mut root = &mut self.root;
    loop {
    let Some(node) = root else {
    root.replace(Node::new(key, value));
    return true;
    };
    match key.cmp(&node.key) {
    Ordering::Less => root = &mut node.left,
    Ordering::Equal => return false,
    Ordering::Greater => root = &mut node.right,
    }
    }
    }

    /// Print the tree, if it is not empty.
    pub fn print(&self) {
    if let Some(node) = &self.root {
    node.print();
    }
    }
    }

    // A node in the tree owns its key, value, and left and right
    // children.
    struct Node<K: Debug + Ord, V: Debug> {
    key: K,
    value: V,
    left: Option<Box<Node<K, V>>>,
    right: Option<Box<Node<K, V>>>,
    }

    impl<K: Debug + Ord, V: Debug> Node<K, V> {
    /// Creates a new boxed node owning the given key and value.
    pub fn new(key: K, value: V) -> Box<Node<K, V>> {
    Box::new(Node {
    key,
    value,
    left: None,
    right: None,
    })
    }

    /// Pre-order print of this node and its children.
    pub fn print(&self) {
    if let Some(left) = &self.left {
    left.print();
    }
    println!("key = {:?}, value = {:?}", self.key, self.value);
    if let Some(right) = &self.right {
    right.print();
    }
    }
    }
    END `tree.rs`<---

    Note that there's more code here than in your C code, but this
    also does a lot more; this is generic over any key and value
    types where the key type implements a total ordering, and where
    both types implement the `Debug` trait (so they can be printed).

    If I don't need any of that, does it get smaller? I don't think so.

    Here's my attempt, and I expect that experienced Rust programmers will
    find ways to improve on it. It includes a lookup function:

    -------------------
    use std::cmp::Ordering;

    pub struct Tree<K: Ord, V> {
    root: Subtree<K, V>,
    }

    type Subtree<K,V> = Option<Box<Node<K,V>>>;

    struct Node<K: Ord, V> {
    key: K,
    val: V,
    left: Subtree<K, V>,
    right: Subtree<K, V>,
    }

    impl<K: Ord,V> Tree<K,V> {
    pub fn new() -> Self {
    Tree { root: None }
    }

    pub fn insert(&mut self, key: K, val: V) {
    let mut t = &mut self.root;
    while let Some(node)=t {
    match key.cmp(&node.key) {
    Ordering::Equal => {node.val = val; return;},
    Ordering::Less => t = &mut node.left,
    Ordering::Greater => t = &mut node.right,
    };
    };
    *t = Some(Box::new(Node {key, val, left: None, right: None}))
    }

    pub fn lookup(&self, key: K) -> Option<&V> {
    let mut t = &self.root;
    while let Some(node)=t {
    match key.cmp(&node.key) {
    Ordering::Equal => return Some(&node.val),
    Ordering::Less => t = &node.left,
    Ordering::Greater => t = &node.right,
    };
    };
    None
    }
    }

    This is essentially the same program I just presented to you;
    you added a type alias, changed `insert` to set the value, and
    used `*t` instead of `.replace` for inserting a new node, used
    ersatz formatting (ie, not `rustfmt`), and omitted the comments.

    I find it strange that you claim this as different from what I
    presented to you, except in trivial details, while also telling
    me that I "did not find" something.

    Are you sure you actually ready my post, and the programs in it?

    fn main() {
    let mut q1 = Tree::new();
    q1.insert(2,"abc".to_string());
    q1.insert(3,"def".to_string());
    q1.insert(1,"ghi".to_string());
    q1.insert(3,"jkl".to_string());
    println!("{:?}",q1.lookup(1));
    println!("{:?}",q1.lookup(2));
    println!("{:?}",q1.lookup(3));
    }
    ----------------------------

    Like your code and Java code, this has a separate type Tree with a
    field root and with its own new function, while in C a "struct tree *" >variable can serve as root pointer; and this leads to an additional
    line each in insert and lookup. Can we do without most of that
    (havibf a constructor is useful) and just use Subtree instead of Tree?

    Sure. I do not think it is an interesting exercise, though.

    insert now integrates search, because I could not satisfy the compiler
    with a separate search; maybe a more experience Rust programmer can
    separate it out again. The advantage is that there is no need to
    combine the found and notfound cases in one return value in search,
    only to handle them as separate cases in insert. But at least there
    is no duplication of the comparison code.

    I fail to see how your program is very different from mine,
    aside from using the type alias for `Subtree`. The comparison
    is clearly duplicated in your code. Perhaps you are commenting
    on a hypothetical where that does not happen, as in my second
    example?

    The integration of search in insert led to a duplication of most of
    the search code in lookup. However, requiring a mutable reference is
    not necessary for the lookup code, so a search that takes a mutable
    reference and returns a mutable reference is too restrictive for
    lookup. But maybe a more experienced Rust programmer can write a
    generic search that takes an &mut tree for insert, and a & for lookup.

    If it offends you that much, you could use a macro.

    By analogy, in your C, how would it affect things if you wanted
    to `const` qualify the tree pointer for lookups?

    Of course, if search is factored out, both clients have to explicitly
    check for both outcomes and have code for both. But that's probably >preferable to duplicating the search.

    See my second program in the post you responded to.

    though the pointer-to-pointer
    to a node is not returned by `search`. If that is what you mean
    by "technique" then you're right; doing that in Rust is
    inconvenient because it violates the borrow checker's rules
    about the existence of multiple simultaneous mutable references
    to an object.

    Yes, that's what I meant by "technique". And it's possible in safe
    Rust, but I had a very hard time finding it (and for a while thought
    that it is not possible). The fact that you thought it is not
    possible in safe Rust means that it is not just my inexperience at
    fault here.

    I did not say that it was "not possible"; indeed, I can think of
    any number of ways to do it, using interior mutability, arcs,
    etc. I said it was inconvenient, and I would argue not very
    interesting.

    You seem to be claiming you have a solution to this, but have
    yet to present that code; your program above was basically a
    copy of my program, and your commentary suggests you couldn't
    figure out a different way to write it yourself.

    But I claim that is just a detail, and not a
    terribly interesting one at that; certainly not an impediment to >>substituting Rust for C in systems programming.

    I think that, in order to be the rightful heir, safe Rust should be
    able to do everything one can do in the other language that's not
    inherently unsafe, and do it without needing to duplicate code of
    other nastyness.

    I think that criteria is both silly and arbitrary, and has no
    basis in actual practice. You are asserting that an arbitrary
    implementation detail of this one program is a useful yardstick
    by which to measure a language's applicability to an entire
    problem domain, and then defined your criteria so that it
    excludes the most obvious translation.

    You may wonder about "inherently unsafe": I have no
    definition for that yet, but I would not kno why inserting a node into
    a search tree would ever fall into that category,

    Actually, I think perhaps it's the other way around: you do not
    seem to know what "unsafe" means in a Rust context. It is
    merely a statement that the compiler cannot infer that the
    "unsafe" code meets the language's memory safety criteria, and
    that the programmer is asserting that it does; that is exactly
    the same as every line in a C program.

    Even so, as I said, one _can_ use the same technique using a
    smallish-bit of `unsafe` code.

    That's cool, but given that I have difficulty understanding why the
    compiler complained about my version with separate search, I feel very >uncomfortable in asserting to the compiler that my use of unsafe
    features is safe once we leave the unsafe block.

    I'm afraid that sounds like a problem for you to deal with, not
    a problem with Rust.

    Frankly, unless I was doing something very odd, I wouldn't write
    my own binary search tree at all; the standard library's
    collections implementations are pretty good

    And I actually don't use binary search trees anyway, neither
    self-written nor from libraries, because hash tables tend to be the
    better alternative, except when I want to know the next-higher element
    or somesuch. But being able to have a reference to a field is also
    useful when implementing hash tables.

    But anyway, the libraries have to be written in some language, and
    given that Rust fans tell us to rewrite all C code in Rust, I would
    expect that the library should be written in Rust, and, in particular
    in safe Rust (otherwise, why rewrite it in Rust?).

    I'm not sure how useful it is to try to explain this, since your
    thoughts on the matter appear to have already calcified.

    But the reason is that locality of reasoning is a powerful tool
    when building reuseable components. Does the Rust standard
    library use `unsafe` code? Yes, it does. However, it wraps
    that in a safe facade, and competent Rust programmers seek to
    minimize it. The `unsafe` code is then heavily audited; this
    does not guarantee bug freedom, but no one serious ever claimed
    that it did. However, it _does_ make debugging dramatically
    easier: if a program dumps core at some point down the road,
    it's likely that the crash originated in `unsafe` code in a
    library, and that becomes the focus for looking for the bug.

    And actually, we teach our students how to write this stuff (even in
    Java), because we think it teaches them something useful. And at
    least one Rust fan thinks so, too, and wrote "Learn Rust With Entirely
    Too Many Linked Lists", where I found out how to implement the
    mentioned technique in safe Rust.

    Yes. Gankra was also the driving force behind the strict
    pointer provenance work. You may enjoy her writing on, "the
    tower of weakenings."

    Given that I do not understand the automagic stuff done by the Rust >>>compiler enough to get this kind of tree insertion past the borrow >>>checker, even though it is possible, I don't trust my capabilities to >>>audit unsafe Rust.

    Is it significantly different than doing so in C, Pascal, or
    Forth? See the second program, above; the use of `unsafe` in
    the bits that correspond to your sketch in C is constrained to
    two places that indirect raw pointers.

    I may be able to shut up the compiler and get an executable with
    "unsafe", but will the resulting program work? And I can have the
    same assurance in C or Forth, so why should I switch to Rust to get
    that?

    Because with C and Forth you can't just wrap the unsafe bits
    behind a safe interface that consumers can rely on, placing the
    burden on all users to ensure safety, whereas in Rust, you can.

    Consider the arguments in in your Tree example; the tree itself
    is a pointer to a pointer to the root. What if that pointer is
    garbage? What if it's dangling? In Rust, `&self` is guaranteed
    to be a valid, non-nil reference to a live object, and moreover
    you are guaranteed that no other thread is trying to mutate that
    object while you're holding that ref. Neither C nor Forth can
    give you any such guarantee. Similarly, `&mut self` guarantees
    that no other reference to the object exists. Visibility
    similarly gives you control; by wrapping, say, a raw pointer
    inside of an `struct` as a non-public member, you can guarantee
    that the same pointer is the one used in multiple, subsequent
    function calls: as written, your C code has two functions that
    share no relationship save that one calls the other. I could
    cal `insert`, and then go and muck with the tree myself, so that
    its ordering constraints are violated, and then call `insert`
    again and have a tree that didn't respect the BST properties. I
    could pass a random garbage `root` pointer and cause the program
    to crash. And so on.

    Do I really need to go on, here?

    It's so common that "fighting the borrow checker" is the
    community's slogan to describe it.

    Sounds to me like "the community" has found a convenient cliche to
    avoid thinking about why people have difficulties with Rust, and why
    these difficulties may increase as Rust advances.

    Bluntly, I think you are not really in a position to make that
    claim, and are inventing excuses to justify your preconceptions
    and biases.

    From the books I read, I get the impression that
    Rust used to have less automagic stuff, and much of the automagic was >>>added later, as a convenience for the programmers who already knew the >>>language. So the Rust programmers of the early days formed a mental >>>model based on "Pedestrian Rust" (i.e., with less automagic), and when >>>they see a program with more automagic, and need to understand some >>>conveniently short piece of Rust code in detail, they form a mental
    model of the code in Pedestrian Rust. But how can a Rust programmer
    who starts now learn Pedestrian Rust and how it relates to the modern >>>language. There are hints in the books I read, but not enough to get
    a firm foundation.

    Early Rust was very different, and much closer to a managed
    language with garbage collection, green threads, a heavy-weight
    runtime, etc. Put another way, the trend has been less and less >>"automagic" (as you put it) and making things more explicit over
    time.

    What I mean is the automatic dropping, and the fact that the Box stuff
    only occurs in the type declarations, but is automatically
    dereferenced or otherwise automatically handled; "let x = ..."
    produces a type automatically (but I guess you could put a type
    declaration there, though).

    Drop and box as a smart pointer is simply "RAII", which has been
    in C++ since the 1980s. Hindley and Milner created the type
    inference rules you are referring to here in the 1970s.

    You keep conflating things you are not familiar with in a
    language you are not familiar with, with the idea that those
    things are "automagic." But really there is no magic here, and
    I suspect the answer to your question about how one learns to
    adapt their mental model of Rust is to first stop thinking of it
    as magical.

    The language hides a number of things with automation, and given that

    No, it really doesn't.

    I have no good mental model for that, following Clarke's third law
    ("Any sufficiently advanced technology is indistinguishable from
    magic."), I refer to that automatic stuff as "automagic stuff". I
    don't think it's magic, it just tries to be indistinguishable. It
    shouldn't.

    Sorry, but I see no evidence that you have taken the time to
    actually understand that the things you attribute to automation
    are actually rather explicit; you're a smart guy, and I'm quite
    sure you could do it if you wanted; it's really all rather
    obvious once you dig into it a little bit. Given that, and that
    you continue to assert that it's "automagic" I can tell that you
    simply don't want to, perhaps because it is different from what
    you are used to.

    Hint: when an object goes out of scope, it is "dropped." A type
    can register a "drop handler" by implementing the `Drop` trait.
    For a smart pointer like `Box`, that simply frees the thing it
    points to. Same with a Mutex: the `lock()` operation returns a
    `MutexGuard` object that holds the lock. When it goes out of
    scope, its `drop()` method unlocks the mutex. Cf, https://cliffle.com/blog/rust-mutexes/

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Michael S@[email protected] to comp.arch on Sat Oct 10 22:28:54 2026
    From Newsgroup: comp.arch

    On Tue, 06 Oct 2026 08:29:19 GMT
    [email protected] (Anton Ertl) wrote:

    [email protected] (Dan Cross) writes:
    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote:
    Tim Rentsch <[email protected]> writes:
    That's good to know but what is a good resource to learn the Rust >>>language? I've looked at the Rust book several times and each
    time come away rather frustrated.

    If you mean "The Rust Programming Language", I came away from it >>disillusioned.

    The reason was that I could not get a search-tree insertion
    technique to work that can be implemented in e.g., Pascal, C, and
    Forth, but not in Java (because in Java I cannot produce references
    to a field inside an object).

    I think what you mean to say is that you couldn't find a way to
    implement that in the manner you were accustomed to thinking
    about the problem, given your prior experience.

    My goal was to implement search tree insertion as efficiently and
    compactly as in Pascal,

    I suppose, you mean some extended Pascal dialect here, because I can not imagine how you code an example below in Standard Pascal.
    Then again, I learned Pascal ~40 years ago and didn't use it in
    practice since 1989 or 1990, so can easily miss something.

    I would think that it is possible to code your example in Standard
    Ada83, but not in "checked" subset of it.

    C or Forth (and in contrast to Java), ideally
    in safe Rust. Here it is in C (untested):

    struct tree {
    int key; /* or some other key type */
    int val; /* or some other value type */
    struct tree *left;
    struct tree *right;
    };

    struct tree **search(struct tree **rootp, int key)
    {
    while ((root=*rootp)!=NULL && key != root->key) {
    if (key < root->key)
    rootp = &(root->left);
    else
    rootp = &(root->right);
    }
    return rootp;
    }

    void insert(struct tree **rootp, int key, int val)
    {
    struct tree **nodep = **search(rootp, key);
    struct tree *node = *nodep;
    if (node == NULL) {
    node = malloc(sizeof(tree));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    *nodep = node;
    }
    node->val = val;
    }

    Passing a reference to a mutable field is what is done in these
    languages, and I don't know an efficient and similarly compact
    technique that does not pass a reference to a mutable field.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Sun Oct 11 08:41:44 2026
    From Newsgroup: comp.arch

    Terje Mathisen <[email protected]> schrieb:

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable array.

    Even if it isn't, the fact that I start by verifying that the slice
    extends at least 7 bytes past the p index means that all of p,
    p+1,p+2...p+6 must be valid, right?

    Range checking is very difficult for a compiler, see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=VRP .
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Sun Oct 11 09:27:04 2026
    From Newsgroup: comp.arch

    Michael S <[email protected]> writes:
    On Tue, 06 Oct 2026 08:29:19 GMT
    [email protected] (Anton Ertl) wrote:
    My goal was to implement search tree insertion as efficiently and
    compactly as in Pascal,

    I suppose, you mean some extended Pascal dialect here, because I can not >imagine how you code an example below in Standard Pascal.
    Then again, I learned Pascal ~40 years ago and didn't use it in
    practice since 1989 or 1990, so can easily miss something.

    Page 75 and 76 of my copy of "Programming in Modula-2" (Second Edition)
    contain this:

    TreePtr = POINTER TO TreeNode;
    TreeNode =
    Record
    key: CARDINAL;
    data: ...
    left, right: TreePtr;
    END;

    PROCEDURE search(VAR p: TreePtr; x: CARDINAL): TreePtr;
    BEGIN
    IF p # NIL THEN
    IF p^.key < x THEN
    RETURN search(p^.right,x)
    ELSIF p^.key > x THEN
    RETURN search(p^.left, x)
    ELSE
    RETURN p
    END
    ELSE (* not found, hence insert *)
    NEW(p);
    WITH p^ DO
    key := x;
    left := NIL;
    right := NIL;
    END;
    RETURN p
    END
    END search

    You would invoke this with

    search(root,needle)^.data := ...;

    in order to actually change the field "data", performing the insert functionality.

    One would not want to use this procedure for lookup, given that it
    inserts a node with an uninitialized data field when the key is not
    found. Wirth gives a loop implementation of lookup before the
    PROCEDURE search.

    Note that the "VAR" in front of the p makes p a reference parameter
    rather than a value parameter (like x), which is key to making this
    work for insertion. This language feature is also present in Pascal;
    the Pascal syntax is different, but I don't think that prevents
    implementing this approach.

    Using recursion is necessary here because Modula-2 and Pascal support references to variables or fields only for parameters. But the
    recursive calls are tail-recursive (I don't think that Wirth's
    Modula-2 or Parcal compilers optimized tail recursion, though).

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch,comp.programming on Sun Oct 11 15:40:12 2026
    From Newsgroup: comp.arch

    [email protected] (Dan Cross) writes:
    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote: >>[email protected] (Dan Cross) writes:
    [Anton Ertl:]
    I do not know what you are referring to when you write, the
    "fact that you did not find it"; did you not notice that I gave
    you _two_ programs, one of which was pretty much the same as the
    C program you gave? What, exactly, are you referring to?

    I was referring to your safe Rust solution. However, I did not look
    at it closely enough, especially at your insert, to realize that what
    you had done is to turn search into lookup, and that you do not call
    search from insert, like the C version does.

    I just saw that there is a similar match in your insert as in search
    (and the non-recursive Java version would also have that) and set out
    to refine an existing linked-list implementation with insertion at the
    end into a tree insertion that does not repeat the match, and I used
    your match code from search for the parts that the linked-list
    implementation does not do.

    The list variant has the walk to the end separated from the actual
    insertion, and the C tree variant also does it that way, and I wanted
    the Rust version to do the same, but could not get it past the Rust
    compiler. The insertion code did not look particularly nice, either,
    checking for something that is alreadu known in the search code. so
    to work around the compiler problem, I eventually decided to inline
    the search code into insert, and I could get that to compile.

    For testing I then added a lookup, which was derived from the search
    part of insert, without mut stuff, so it's full circle back at your
    search.

    Anyway, yes, your insert does not work the Java version, but like the
    C version with search() inlined into insert(), and like the insert I
    finally arrived at.

    Which leads to the question? Can an experienced Rust programmer do a
    search that works like the C search(), and an insert that calls
    search() and then uses the reference to either update a node or insert
    a new one?

    Like your code and Java code, this has a separate type Tree with a
    field root and with its own new function, while in C a "struct tree *" >>variable can serve as root pointer; and this leads to an additional
    line each in insert and lookup. Can we do without most of that
    (havibf a constructor is useful) and just use Subtree instead of Tree?

    Sure. I do not think it is an interesting exercise, though.

    I tried it for a while, and did not get anything but more and more
    compiler errors.

    The integration of search in insert led to a duplication of most of
    the search code in lookup. However, requiring a mutable reference is
    not necessary for the lookup code, so a search that takes a mutable >>reference and returns a mutable reference is too restrictive for
    lookup. But maybe a more experienced Rust programmer can write a
    generic search that takes an &mut tree for insert, and a & for lookup.

    If it offends you that much, you could use a macro.

    Good to know that there is this option.

    By analogy, in your C, how would it affect things if you wanted
    to `const` qualify the tree pointer for lookups?

    C works perfectly fine without "const", so why would I want to do that?

    By contrast, Rust is picky about "mut". If I add it everywhere where
    the syntax allows it (which might be the analogon of leaving "const"
    away), the borrow checker will complain about programs that it
    accepted before adding all the "mut"s.

    I think that, in order to be the rightful heir, safe Rust should be
    able to do everything one can do in the other language that's not >>inherently unsafe, and do it without needing to duplicate code of
    other nastyness.

    I think that criteria is both silly and arbitrary, and has no
    basis in actual practice. You are asserting that an arbitrary
    implementation detail of this one program is a useful yardstick
    by which to measure a language's applicability to an entire
    problem domain,

    Not one detail, the collection of all the details. This is one.

    You may wonder about "inherently unsafe": I have no
    definition for that yet, but I would not kno why inserting a node into
    a search tree would ever fall into that category,

    Actually, I think perhaps it's the other way around: you do not
    seem to know what "unsafe" means in a Rust context. It is
    merely a statement that the compiler cannot infer that the
    "unsafe" code meets the language's memory safety criteria, and
    that the programmer is asserting that it does;

    I understand that.

    that is exactly
    the same as every line in a C program.

    The decisive difference is that in a C program there is no end to an
    unsafe block, where I have to satisfy safety criteria that are beyond
    my experience level.

    I'm afraid that sounds like a problem for you to deal with, not
    a problem with Rust.

    Sure, and the way I deal with that is to not use unsafe Rust.

    What I mean is the automatic dropping, and the fact that the Box stuff
    only occurs in the type declarations, but is automatically
    dereferenced or otherwise automatically handled; "let x = ..."
    produces a type automatically (but I guess you could put a type
    declaration there, though).

    Drop and box as a smart pointer is simply "RAII", which has been
    in C++ since the 1980s. Hindley and Milner created the type
    inference rules you are referring to here in the 1970s.

    And?

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Sun Oct 11 18:14:17 2026
    From Newsgroup: comp.arch


    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable array.

    Can I get a definition of what mutable in Rust means ??

    Intuition would imply the inverse of opposite of const from C.

    Even if it isn't, the fact that I start by verifying that the slice
    extends at least 7 bytes past the p index means that all of p,
    p+1,p+2...p+6 must be valid, right?
    ----------

    Terje

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Sun Oct 11 20:56:23 2026
    From Newsgroup: comp.arch

    MitchAlsup wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable
    array.

    Can I get a definition of what mutable in Rust means ??

    It does mean that the function which receives a non-mut parameter, or
    itself declares som non-mut local variables, cannot modify any of them.

    So yes, very similar to "not const".

    You can discuss if its a good idea to make non-mutability the default
    instead of the opposite. Personally I like that part.

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Michael S@[email protected] to comp.arch on Sun Oct 11 22:43:57 2026
    From Newsgroup: comp.arch

    On Sun, 11 Oct 2026 09:27:04 GMT
    [email protected] (Anton Ertl) wrote:

    Michael S <[email protected]> writes:
    On Tue, 06 Oct 2026 08:29:19 GMT
    [email protected] (Anton Ertl) wrote:
    My goal was to implement search tree insertion as efficiently and
    compactly as in Pascal,

    I suppose, you mean some extended Pascal dialect here, because I can
    not imagine how you code an example below in Standard Pascal.
    Then again, I learned Pascal ~40 years ago and didn't use it in
    practice since 1989 or 1990, so can easily miss something.

    Page 75 and 76 of my copy of "Programming in Modula-2" (Second
    Edition) contain this:

    TreePtr = POINTER TO TreeNode;
    TreeNode =
    Record
    key: CARDINAL;
    data: ...
    left, right: TreePtr;
    END;

    PROCEDURE search(VAR p: TreePtr; x: CARDINAL): TreePtr;
    BEGIN
    IF p # NIL THEN
    IF p^.key < x THEN
    RETURN search(p^.right,x)
    ELSIF p^.key > x THEN
    RETURN search(p^.left, x)
    ELSE
    RETURN p
    END
    ELSE (* not found, hence insert *)
    NEW(p);
    WITH p^ DO
    key := x;
    left := NIL;
    right := NIL;
    END;
    RETURN p
    END
    END search

    You would invoke this with

    search(root,needle)^.data := ...;

    in order to actually change the field "data", performing the insert functionality.

    One would not want to use this procedure for lookup, given that it
    inserts a node with an uninitialized data field when the key is not
    found. Wirth gives a loop implementation of lookup before the
    PROCEDURE search.

    Note that the "VAR" in front of the p makes p a reference parameter
    rather than a value parameter (like x), which is key to making this
    work for insertion. This language feature is also present in Pascal;
    the Pascal syntax is different, but I don't think that prevents
    implementing this approach.

    Using recursion is necessary here because Modula-2 and Pascal support references to variables or fields only for parameters. But the
    recursive calls are tail-recursive (I don't think that Wirth's
    Modula-2 or Parcal compilers optimized tail recursion, though).

    - anton


    Thank you. I forgot about call by reference feature of Pascal.
    But even if I was not forgetting about it, using recursive function
    call to emulate & operator of C probably would not cross my mind.



    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Mon Oct 12 07:47:06 2026
    From Newsgroup: comp.arch

    On 11/10/2026 20:56, Terje Mathisen wrote:
    MitchAlsup wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable
    array.

    Can I get a definition of what mutable in Rust means ??

    It does mean that the function which receives a non-mut parameter, or
    itself declares som non-mut local variables, cannot modify any of them.

    So yes, very similar to "not const".

    It sounds more powerful than "not const" in C.

    This is perfectly reasonable C code :

    void change(const int * p, int x) {
    int * q = (int *) p;
    *q = x;
    }

    Calling "change" with a pointer to data that was defined with a const-qualified type is undefined behaviour. Other than that, the
    behaviour is fully defined.

    So you may not change the value of a variable /defined/ as "const" in C
    - any attempt to do so is UB, and unless you go out of your way (such as
    with pointer casts), accidental attempts will usually be flagged as compile-time errors.

    But if you receive a "pointer to const" parameter to a function, you can
    still write via a cast version of the pointer if the original object was
    not defined as "const". (So objects defined as "const" may be placed in read-only memory of some kind, and trying to change them might cause a
    crash, signal, bus-error, etc.)

    (In C++, members of a class can be marked "mutable" so that they can be changed without even needing any pointer cast shenanigans. This means
    you can have classes where instances are "logically constant" in a
    function call - their user-visible value does not change - but with
    aspects that do change, such as caching the results of a calculation, or re-ordering elements in the underlying data structure for a set.)

    The weaker promise of "const" in C and C++ blocks some possible compiler optimisations of code, but allows some additional user flexibility.

    I think (and you will hopefully correct me if I am wrong!) in Rust if a function receives a non-mutable reference as a parameter, it is not
    allowed to cast it to a mutable reference.


    You can discuss if its a good idea to make non-mutability the default instead of the opposite. Personally I like that part.


    It certainly sounds good to me.



    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Stefan Monnier@[email protected] to comp.arch,comp.programming on Sun Oct 11 14:26:48 2026
    From Newsgroup: comp.arch

    Which leads to the question? Can an experienced Rust programmer do a
    search that works like the C search(), and an insert that calls
    search() and then uses the reference to either update a node or insert
    a new one?

    IIUC it can but at the cost of making `search` take a `&mut` reference.
    I get the impression that you'd otherwise need "mutability
    polymorphism", i.e. a single piece of code that can be instantiated
    either for `&` or for `&mut`. You can probably avoid writing the code
    twice, tho, by abusing macros (aka "template polymorphism").


    === Stefan
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Tim Rentsch@[email protected] to comp.arch on Mon Oct 12 01:49:46 2026
    From Newsgroup: comp.arch

    [email protected] (Dan Cross) writes:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:

    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.

    Without knowing what methods you have tried, what books you have
    already explored, what you found dissatisfying about them, and
    why, it is impossible to give you a good answer.

    Fortunately some other people found it possible to give
    helpful replies.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Mon Oct 12 11:25:32 2026
    From Newsgroup: comp.arch

    David Brown <[email protected]> schrieb:

    But if you receive a "pointer to const" parameter to a function, you can still write via a cast version of the pointer if the original object was
    not defined as "const". (So objects defined as "const" may be placed in read-only memory of some kind, and trying to change them might cause a crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address. (We used to call
    this "Variation der Konstanten", which translates to "variation
    of parameters", from ODE theory.

    On modern systems, you get a bus error:

    $ ./a.out

    Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

    Backtrace for this error:
    #0 0x72b109a23e59 in ???
    #1 0x72b109a22e75 in ???
    #2 0x72b10964532f in ???
    at ./signal/../sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c:0
    #3 0x401172 in foo_
    at /tmp/crash.f90:6
    #4 0x40118f in MAIN__
    at /tmp/crash.f90:1
    #5 0x401226 in main
    at /tmp/crash.f90:3


    But it would be interesting to be able to put arguments into
    storage which can be changed by the caller, but not the callee,
    sort of a read-only stack, but I am not sure how one would
    efficiently implement that.

    (Modern Fortran offers INTENT for that, so

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    INTEGER, INTENT(IN) :: N
    N = 42
    END

    gets you

    error.f90:7:7:

    7 | N = 42
    | 1~
    Error: Dummy argument 'n' with INTENT(IN) in variable definition context (assignment) at (1)
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 12 13:05:41 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:
    [email protected] (Dan Cross) writes:

    In article <[email protected]>,
    Tim Rentsch <[email protected]> wrote:

    Terje Mathisen <[email protected]> writes:

    [on learning Rust]

    Quite often, when "fighting the borrow checker", typically because
    I'm updating a complicated data structure in two locations at the
    same time, I'll fall back to "write Fortran in Rust", with every
    access indexed.

    This is BTW one of those problems the brand new upcoming borrow
    checker is supposed to be far better at figuring out: If I'm
    updating totally separate parts of the same larger structure,
    then it should be OK to mutate both at the same time.

    I do realize that there are far more Rusty ways to solve this
    type of issue, they just don't come naturally to me yet.

    (Please excuse my pursuing this non-arch topic...)

    Question for any Rusters in the group --

    What is the best was to learn the Rust programming language?
    Which books or other documents are the best introductory or
    reference materials?

    Disclaimer: My experience with Rust is more than zero but
    not a lot more than zero.

    Without knowing what methods you have tried, what books you have
    already explored, what you found dissatisfying about them, and
    why, it is impossible to give you a good answer.

    Fortunately some other people found it possible to give
    helpful replies.

    Oh? Which ones? I saw few responses to your question, and the
    only other references given were to the standard library source
    code or, "Learning Rust With Entirely Too Many Linked Lists" (https://rust-unofficial.github.io/too-many-lists/), by Aria
    Desires.

    What did you find useful about those, and how, for you, did they
    improve on the references you had already reviewed?

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Mon Oct 12 16:20:48 2026
    From Newsgroup: comp.arch

    On 12/10/2026 13:25, Thomas Koenig wrote:
    David Brown <[email protected]> schrieb:

    But if you receive a "pointer to const" parameter to a function, you can
    still write via a cast version of the pointer if the original object was
    not defined as "const". (So objects defined as "const" may be placed in
    read-only memory of some kind, and trying to change them might cause a
    crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address. (We used to call
    this "Variation der Konstanten", which translates to "variation
    of parameters", from ODE theory.


    Yes, that "feature" of old FORTRAN is rather famous.

    Forth is arguably even better for this :

    : 21 42 ;
    21 .
    42 ok

    That is - you can redefine the word "21" to put the value 42 on the
    stack. Then if you ask for the number "21", you get the value 42. It
    is not an oddity of the implementation, but the way the language works.





    On modern systems, you get a bus error:

    $ ./a.out

    Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

    Backtrace for this error:
    #0 0x72b109a23e59 in ???
    #1 0x72b109a22e75 in ???
    #2 0x72b10964532f in ???
    at ./signal/../sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c:0
    #3 0x401172 in foo_
    at /tmp/crash.f90:6
    #4 0x40118f in MAIN__
    at /tmp/crash.f90:1
    #5 0x401226 in main
    at /tmp/crash.f90:3


    But it would be interesting to be able to put arguments into
    storage which can be changed by the caller, but not the callee,
    sort of a read-only stack, but I am not sure how one would
    efficiently implement that.

    (Modern Fortran offers INTENT for that, so

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    INTEGER, INTENT(IN) :: N
    N = 42
    END

    gets you

    error.f90:7:7:

    7 | N = 42
    | 1~
    Error: Dummy argument 'n' with INTENT(IN) in variable definition context (assignment) at (1)


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 12 15:41:29 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable
    array.

    Can I get a definition of what mutable in Rust means ??

    First, it is important to note that this is a logical concept,
    and has no bearing on the physical properties of the computer
    that the program is running on.

    Intuition would imply the inverse of opposite of const from C.

    Here's a detailed explanation.

    By default, variables in Rust are immutable; that is, their
    value may not change. For example, one cannot assign to them,
    and to try and do so is a compile-time error:

    ? let foo = something();
    ? foo = something_else(); // Error: `foo` is immutable.

    "Mutable", then, means the opposite: the value _can_ change.
    Mutability is intorduced via the `mut` keyword:

    let mut foo = something();
    foo = something_else(); // Ok: `foo` is mutable.

    In Rust, this extends to references, which have specific
    properties: references are non-nullable, and can only refer to
    valid, initialized objects; references are also prohibited from
    outliving the object they refer to, and furthermore, they carry
    a notion of mutability themselves: a mutable reference can be
    used to change its referrent; immutable references cannot.
    Immutable references are created using the `&` operator; mutable
    references use the `&mut` operator.

    Importantly, while immutable references can be created to
    mutable objects, the inverse is not true: mutable references
    cannot be created to immutable objects. So, for example this
    does not compile:

    ? let foo = something();
    ? let foomut = &mut foo; // `foo` is immutable

    But this is ok:

    let mut foo = something();
    let foomut = &mut foo;

    As is this:

    let mut foo = something();
    let fooref = &foo; // Immutuable reference
    let barref = &foo; // Ok; immutable reference

    Having multiple immutable references to an object is fine, but
    the language requires that _mutable_ references to an object
    are mutually exclusive with any other reference to that object.
    In Rust, we say that we "borrow" a reference to an object, and
    the compiler has a thing called, "the borrow checker" that
    checks that all of these properties hold.

    So this will fail to compile:

    ? let mut foo = something();
    ? let foomut = &mut foo; // Ok: only mutable
    ? let barmut = &mut foo // Fail: a mut ref to foo exists

    As will this:

    ? let mut foo = something();
    ? let foomut = &mut foo; // Ok; only reference.
    ? let fooref = &foo; // Fail: a mut ref to foo exists

    And as will this:

    ? let mut foo = something();
    ? let fooref = &foo; // Ok: immutable reference.
    ? let barref = &foo; // Ok: multiple immut. refs ok
    ? let foomut = &mut foo; // Fail: exiting ref already

    References also have a property called a "lifetime" that is tied
    to the object they are borrowed from; the rule is that a
    reference is not allowed to outlive its referrent. So this,
    again, is a compile time error:

    ? let foo = {
    ? let bar = something();
    ? &bar // Error; bar going out of scope
    ? };

    Here, I created a new scope (indicated by the curly braces) and
    bound some object to the name `bar` in that scope, and attempted
    to preserve a reference to that object outside of that scope.
    However, that object only exists in that scope, so when the
    scope ends, so too does the lifetime of the object; trying to
    use a reference to it beyond that point violates the life time
    rules enforced by the borrow checker.

    But this serves to introduce another aspect of this, which is
    that scopes and lifetimes matter with respect to how the
    compiler treats these rules.

    Take the following:

    struct Obj(u32); // Tuple struct containing a u32
    impl Obj {
    fn print(&self) {
    println!("Value in obj is: {}", self.0);
    }
    }

    Here, I have defined a trivial struct (a "tuple struct"
    specifically) that contains a simple datum; an unsigned 32-bit
    integer. And I have written an "implementation" of that type
    that provides a "print" method that takes `&self`; that is,
    takes an immutable reference to an instance of that object and
    prints the integer.

    Now, consider this code:

    let mut foo = Obj(42);
    foo.print();
    let foomut = &mut foo;
    foomut.print();

    What should happen here? The first three lines are fine; I
    create the object, and I invoke its `.print()` method, which
    takes an immutable reference to `foo`, and then I borrow a
    *mutable* reference to foo, which is fine (the immutable
    reference that `.print()` borrowed is gone at that point).

    But in the 4th line I try to invoke `.print()` through through
    the mutable borrow. However, as we have seen, `.print()` takes
    an immutable reference, and I'm clearly holding a *mutable*
    reference, which appears to violate the borrowing rules.
    However, this code compiles just fine: what happened?

    As it turns out, the compiler allows a mutable reference to be
    _coerced_ into an immutable reference, which is what is
    happening here. The compiler is allows this since it can infer
    that nothing else can take an mut reference to the original
    object while the mut ref exists, and since the mut ref is in
    use, nothing can indirect through it and modify the underlying
    object. So things like this are fine:

    0 fn scopes() {
    1 let mut foo = Obj(42);
    2 foo.print();
    3 let foomut = &mut foo;
    4 foomut.print();
    5 foo = Obj(32);
    6 foo.print();
    7 let barmut = &mut foo;
    8 barmut.print();
    9 barmut.0 = 22;
    10 foo.print();
    11 }

    This also illustrates a couple of other aspects of the type
    system: moves and non-lexical lifetimes.

    Prior to 2018, the lifetimes of Rust objects followed the
    lexical scopes of those objects, and this code would have failed
    compilation on line 10, since the `&mut barmut` mutable borrow
    from `foo` still existed in the lexical scope where we called
    `foo.print()`. But since 2018, the borrow checker was changed
    so that lifetimes reflect the flow graph of the program, not
    just the lexical structure; in this case, the compiler can infer
    that nothing in the program uses `barmut` after line 9, so
    calling `foo.print()` on line 10 is fine; the borrow still
    "exists" but is inactive by then.

    On the other hand, if I rewrite the last few lines as:

    ? foo = Obj(32);
    ? let barmut = &mut foo;
    ? foo.print();
    ? barmut.0 = 22;
    ? barmut.print();

    Then this fails to compile; the immutable borrow taken for
    `foo.print()` conflicts with the mutable borrow taken on the
    previous line.

    By default, all assignments in Rust have _move_ semantics: after
    the assignment, the source is considered destroyed, and any
    references to it are no longer valid. That's why one can assign
    to `foo` and on line 5 and then take a mut reference to it on
    line 7; the assignment on 5 implicitly dropped the original
    `foo` object created on line 1 and replaced it with a new
    object, so the mut reference it created on line 3 is invalid;
    an attempt to use it subsequently would fail to compile.

    One can define a `copy` operator for types that should not have
    move semantics; this is usually applied to trivial types (like
    integers).

    One may ask, "why bother with all of this stuff?" and the answer
    is that Rust is designed for a multicore world. There is one
    other wrinkle here that gives Rust a lot of power, and that's
    how all of this stuff interacts with data races: safe Rust code
    is data race free.

    Rust does not mandate a particular shape or type of (software)
    thread, but because of the "mut ref ^ immut ref" rule, enforced
    by the borrow checker at compile-time, safe Rust programs cannot
    race on access to data, even in the face of multithreading.

    Rust handles this by exposing two "marker traits", `Sync` and
    `Send`. To quote the _Rustinomicon_, "A type is `Send` if it is
    safe to send it to another thread" and "A type is `Sync` if it
    is safe to share between threads (T is `Sync` if and only if
    `&T` is `Send`)." That is, a type can be _shared_ between
    threads if it is safe to send an immutable reference to it from
    one thread to another, and the compiler can check borrows
    against the lifetimes of objects, hence, can detect data races
    _statically, at compile time_. (https://doc.rust-lang.org/nomicon/send-and-sync.html)

    Those of us familiar with writing multithreaded programs will
    recognize the power of these constructs: these rules, taken
    together, statically eliminate entire categories of common
    memory and concurrency problems.

    Anyway, that what "mutable" means, in a nutshell, and why it is
    important.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 12 15:47:56 2026
    From Newsgroup: comp.arch

    In article <11ahs8q$38ir0$[email protected]>,
    David Brown <[email protected]> wrote:
    [snip]
    I think (and you will hopefully correct me if I am wrong!) in Rust if a >function receives a non-mutable reference as a parameter, it is not
    allowed to cast it to a mutable reference.

    That is correct. There is no way to do that in safe Rust, and
    shenanigans that do it using unsafe are UB (UB in safe Rust is a
    compile-time error).

    As I mentioned in my other response, going the other direction
    is ok: it is possible to coerce a mutable reference to an
    immutable reference, so that you can e.g. invoke a method that
    takes `&self` on an object to which you have an `&mut` borrow.

    You can discuss if its a good idea to make non-mutability the default
    instead of the opposite. Personally I like that part.

    It certainly sounds good to me.

    It is! Being explicit about when things are mutable is very
    helpful.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Mon Oct 12 16:12:03 2026
    From Newsgroup: comp.arch

    On Mon, 12 Oct 2026 11:25:32 -0000 (UTC), Thomas Koenig
    <[email protected]> wrote:

    David Brown <[email protected]> schrieb:

    But if you receive a "pointer to const" parameter to a function, you can
    still write via a cast version of the pointer if the original object was
    not defined as "const". (So objects defined as "const" may be placed in
    read-only memory of some kind, and trying to change them might cause a
    crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address.

    And the proper way to correct this error is obvious. Not by putting
    constants in a segment marked read-only. But in properly implementing
    language semantics.

    So the values of constants are stored in memory.

    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a
    variable. So you copy it to a temporary, and pass the address of the
    temporary to the function. Not doing that was the bug.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Mon Oct 12 17:10:33 2026
    From Newsgroup: comp.arch

    David Brown <[email protected]> writes:
    Forth is arguably even better for this :

    : 21 42 ;
    21 .
    42 ok

    That is - you can redefine the word "21" to put the value 42 on the
    stack. Then if you ask for the number "21", you get the value 42. It
    is not an oddity of the implementation, but the way the language works.

    Yes, but used like this, it's only a curiosity; nobody does this
    except to show that it can be done.

    However, there are cases where this is practically relevant:

    hex \ change to base #16
    add \ is this a dictionary word, or a number?

    The classic ways to avoid this has been to

    1) prefix hex numbers with 0 if they would otherwise start with a
    letter. Dictionary words rarely start with digits, and when they
    do, they don't conflict with hex numbers.

    2) Use "," at the end of assembly language mnemonics, e.g. "add,":
    Benefits: 1) Avoids conflicts with hex numbers. 2) For postfix
    assemblers (e.g. "r0 r1 r2 add,", the "," nicely separates the
    instruction from the next. But a significant number of Forth
    assemblers do not follow this convention.

    In modern Forth another way is to avoid changing BASE, and just prefix
    hex numbers with "$", e.g., "$add".

    When you do, e.g.,

    hex
    : add ;

    in Gforth, Gforth tells you: "warning: defined literal add as word".
    It does not tell you that when you define "add" when the base is
    decimal, however.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Mon Oct 12 18:37:47 2026
    From Newsgroup: comp.arch


    [email protected] (Dan Cross) posted:

    First off: thank you dan for a well done explanation.

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable
    array.

    Can I get a definition of what mutable in Rust means ??

    --------snip of what I understand clearly------------

    So this will fail to compile:

    ? let mut foo = something();
    ? let foomut = &mut foo; // Ok: only mutable
    ? let barmut = &mut foo // Fail: a mut ref to foo exists

    Is this supposed to work across threads ?? so, only 1 thread has write
    access at any given point in time. If so, how does one thread hand-off mutability to another ??

    --------------snip again--------


    By default, all assignments in Rust have _move_ semantics: after
    the assignment, the source is considered destroyed, and any
    references to it are no longer valid. That's why one can assign
    to `foo` and on line 5 and then take a mut reference to it on
    line 7; the assignment on 5 implicitly dropped the original
    `foo` object created on line 1 and replaced it with a new
    object, so the mut reference it created on line 3 is invalid;
    an attempt to use it subsequently would fail to compile.

    This is clever.

    One can define a `copy` operator for types that should not have
    move semantics; this is usually applied to trivial types (like
    integers).

    One may ask, "why bother with all of this stuff?" and the answer
    is that Rust is designed for a multicore world. There is one
    other wrinkle here that gives Rust a lot of power, and that's
    how all of this stuff interacts with data races: safe Rust code
    is data race free.

    See question above.

    Rust does not mandate a particular shape or type of (software)
    thread, but because of the "mut ref ^ immut ref" rule, enforced
    by the borrow checker at compile-time, safe Rust programs cannot
    race on access to data, even in the face of multithreading.

    Rust handles this by exposing two "marker traits", `Sync` and
    `Send`. To quote the _Rustinomicon_, "A type is `Send` if it is
    safe to send it to another thread" and "A type is `Sync` if it
    is safe to share between threads (T is `Sync` if and only if
    `&T` is `Send`)." That is, a type can be _shared_ between
    threads if it is safe to send an immutable reference to it from
    one thread to another, and the compiler can check borrows
    against the lifetimes of objects, hence, can detect data races
    _statically, at compile time_. (https://doc.rust-lang.org/nomicon/send-and-sync.html)

    Those of us familiar with writing multithreaded programs will
    recognize the power of these constructs: these rules, taken
    together, statically eliminate entire categories of common
    memory and concurrency problems.

    Anyway, that what "mutable" means, in a nutshell, and why it is
    important.

    Thanks again for a complete explanation.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Mon Oct 12 18:40:00 2026
    From Newsgroup: comp.arch


    [email protected] (John Savard) posted:

    On Mon, 12 Oct 2026 11:25:32 -0000 (UTC), Thomas Koenig <[email protected]> wrote:

    David Brown <[email protected]> schrieb:

    But if you receive a "pointer to const" parameter to a function, you can >> still write via a cast version of the pointer if the original object was >> not defined as "const". (So objects defined as "const" may be placed in >> read-only memory of some kind, and trying to change them might cause a
    crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address.

    And the proper way to correct this error is obvious. Not by putting
    constants in a segment marked read-only. But in properly implementing language semantics.

    So the values of constants are stored in memory.

    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a variable. So you copy it to a temporary, and pass the address of the temporary to the function.

    And you think the language designers in 1956 could have figured that out ???

    Not doing that was the bug.

    Some would disagree.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Mon Oct 12 21:05:18 2026
    From Newsgroup: comp.arch

    On 12/10/2026 19:10, Anton Ertl wrote:
    David Brown <[email protected]> writes:
    Forth is arguably even better for this :

    : 21 42 ;
    21 .
    42 ok

    That is - you can redefine the word "21" to put the value 42 on the
    stack. Then if you ask for the number "21", you get the value 42. It
    is not an oddity of the implementation, but the way the language works.

    Yes, but used like this, it's only a curiosity; nobody does this
    except to show that it can be done.


    Of course :-)

    But it /is/ a cool curiosity!

    It is a natural consequence of having substantial freedom to define and redefine almost anything as a "word". And I can imagine it having a
    variety of uses, even for things like words that are all digits or
    otherwise mean something. However, my experience of Forth is quite
    minimal - just playing around a little - and I have no knowledge of
    using it for real code.

    However, there are cases where this is practically relevant:

    hex \ change to base #16
    add \ is this a dictionary word, or a number?


    I did not know about the "hex" word, so this is a new detail for me.
    Yes, I can see that having potential for confusion.

    The classic ways to avoid this has been to

    1) prefix hex numbers with 0 if they would otherwise start with a
    letter. Dictionary words rarely start with digits, and when they
    do, they don't conflict with hex numbers.

    2) Use "," at the end of assembly language mnemonics, e.g. "add,":
    Benefits: 1) Avoids conflicts with hex numbers. 2) For postfix
    assemblers (e.g. "r0 r1 r2 add,", the "," nicely separates the
    instruction from the next. But a significant number of Forth
    assemblers do not follow this convention.

    In modern Forth another way is to avoid changing BASE, and just prefix
    hex numbers with "$", e.g., "$add".


    That looks clearer to me. Using $ for hex numbers is, I think, the
    first convention I learned (it was used in BBC Basic, and by its 6502 assembler).

    When you do, e.g.,

    hex
    : add ;

    in Gforth, Gforth tells you: "warning: defined literal add as word".
    It does not tell you that when you define "add" when the base is
    decimal, however.

    - anton

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch,comp.programming on Mon Oct 12 19:13:09 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote: >[email protected] (Dan Cross) writes:
    In article <[email protected]>,
    Anton Ertl <[email protected]> wrote: >>>[email protected] (Dan Cross) writes:
    [Anton Ertl:]
    I do not know what you are referring to when you write, the
    "fact that you did not find it"; did you not notice that I gave
    you _two_ programs, one of which was pretty much the same as the
    C program you gave? What, exactly, are you referring to?

    I was referring to your safe Rust solution. However, I did not look
    at it closely enough, especially at your insert, to realize that what
    you had done is to turn search into lookup, and that you do not call
    search from insert, like the C version does.

    Clearly.

    Forgive my saying it, but bluntly, this calls into question your
    entire reading of my post.

    I just saw that there is a similar match in your insert as in search
    (and the non-recursive Java version would also have that) and set out
    to refine an existing linked-list implementation with insertion at the
    end into a tree insertion that does not repeat the match, and I used
    your match code from search for the parts that the linked-list
    implementation does not do.

    The list variant has the walk to the end separated from the actual
    insertion, and the C tree variant also does it that way, and I wanted
    the Rust version to do the same, but could not get it past the Rust
    compiler. The insertion code did not look particularly nice, either, >checking for something that is alreadu known in the search code. so
    to work around the compiler problem, I eventually decided to inline
    the search code into insert, and I could get that to compile.

    For testing I then added a lookup, which was derived from the search
    part of insert, without mut stuff, so it's full circle back at your
    search.

    Anyway, yes, your insert does not work the Java version, but like the
    C version with search() inlined into insert(), and like the insert I
    finally arrived at.

    Yes. I take it that here, you are describing the process by
    which you arrived at the program you posted in your response to
    me.

    Which leads to the question? Can an experienced Rust programmer do a
    search that works like the C search(), and an insert that calls
    search() and then uses the reference to either update a node or insert
    a new one?

    Given that you did not read the first program in my post
    carefully, I wonder whether you read the _second_ program in my
    post carefully, or rather, closed over it like the first. I
    suggest having a look at that.

    Like your code and Java code, this has a separate type Tree with a
    field root and with its own new function, while in C a "struct tree *" >>>variable can serve as root pointer; and this leads to an additional
    line each in insert and lookup. Can we do without most of that
    (havibf a constructor is useful) and just use Subtree instead of Tree?

    Sure. I do not think it is an interesting exercise, though.

    I tried it for a while, and did not get anything but more and more
    compiler errors.

    *shrug* Is it useful as anything other than an academic
    exercise?

    The integration of search in insert led to a duplication of most of
    the search code in lookup. However, requiring a mutable reference is
    not necessary for the lookup code, so a search that takes a mutable >>>reference and returns a mutable reference is too restrictive for
    lookup. But maybe a more experienced Rust programmer can write a
    generic search that takes an &mut tree for insert, and a & for lookup.

    If it offends you that much, you could use a macro.

    Good to know that there is this option.

    By analogy, in your C, how would it affect things if you wanted
    to `const` qualify the tree pointer for lookups?

    C works perfectly fine without "const", so why would I want to do that?

    Are you joking? What if you have a `const struct tree` and want
    to take a pointer to it?

    More generally this is the sort of thing I'm referring to; Rust
    is a different language than C, with different semantics, and
    it imposes different constraints on programs than C. One can,
    if motivated, find some program that one can express in C, but
    that cannot be expressed the exact same way in Rust. But why is
    that notable? One could well invert this: I can define some
    program that I express in Rust and ask why it cannot be
    expressed in the exact same way in C. But by itself, that's not
    terribly interesting; what matters more is whether there are
    problems, or perhaps more importantly categories of _problems_,
    that are expressible in one and not the other. I don't think
    there really are; the solutions might differ in some ways, but
    that's to be expected because the languages themselves are
    different.

    By contrast, Rust is picky about "mut". If I add it everywhere where
    the syntax allows it (which might be the analogon of leaving "const"
    away), the borrow checker will complain about programs that it
    accepted before adding all the "mut"s.

    That's right. The language imposes semantic constraints on
    mutability and references; see my other post about this. But
    surely this is not surprising?

    I think that, in order to be the rightful heir, safe Rust should be
    able to do everything one can do in the other language that's not >>>inherently unsafe, and do it without needing to duplicate code of
    other nastyness.

    I think that criteria is both silly and arbitrary, and has no
    basis in actual practice. You are asserting that an arbitrary >>implementation detail of this one program is a useful yardstick
    by which to measure a language's applicability to an entire
    problem domain,

    Not one detail, the collection of all the details. This is one.

    All what details? Your statemtn was that, "safe Rust shoudl be
    able to do everything one can do in the other language that's
    not inherently unsafe". But missing here is your definition of
    what you mean by, "unsafe". I gave you Rust's definition: the
    safety of a program is determined by whether or not the compiler
    can infer that the program upholds the rules of the language vis
    borrows, mutability, lifetimes, and so on; that is, whether the
    borrow checker can do its job. But there are cases where the
    programmer has some insight into the context of the program that
    the borrow checker just can't see into; hence, `unsafe` as a
    part of the language.

    You may wonder about "inherently unsafe": I have no
    definition for that yet, but I would not kno why inserting a node into
    a search tree would ever fall into that category,

    Actually, I think perhaps it's the other way around: you do not
    seem to know what "unsafe" means in a Rust context. It is
    merely a statement that the compiler cannot infer that the
    "unsafe" code meets the language's memory safety criteria, and
    that the programmer is asserting that it does;

    I understand that.

    Forgive me, but I'm not so sure. Under that definition, the
    entirety of a C program is `unsafe`; it's up to the programmer
    to uphold the invariants of the language, and there isn't much
    in the way of requirements on the _language itself_ to help you
    out. In this way, you seem to be putting a greater burden on
    Rust than you do on C.

    that is exactly
    the same as every line in a C program.

    The decisive difference is that in a C program there is no end to an
    unsafe block, where I have to satisfy safety criteria that are beyond
    my experience level.

    In seeming contradiction to what I wrote above, I think that it
    _is_ harder to write correct `unsafe` Rust code than to write
    correct C code, which is one reason Rust programmers try to
    isolate `unsafe` code as much as possible. The flip side is
    that, if you do, the language gives you hard guarantees that you
    can rely on to write correct programs; entire categories of
    errors are eliminated by construction.

    The thing that you wrote that does not follow is that `unsafe`
    Rust code is never allowed to _violate_ those safety criteria;
    it's just that the programmer takes on burden of upholding them,
    with less assistance from the language. It is not an escape
    hatch to YOLO your program.

    I'm afraid that sounds like a problem for you to deal with, not
    a problem with Rust.

    Sure, and the way I deal with that is to not use unsafe Rust.

    *shrug* That's on you, not on Rust.

    What I mean is the automatic dropping, and the fact that the Box stuff >>>only occurs in the type declarations, but is automatically
    dereferenced or otherwise automatically handled; "let x = ..."
    produces a type automatically (but I guess you could put a type >>>declaration there, though).

    Drop and box as a smart pointer is simply "RAII", which has been
    in C++ since the 1980s. Hindley and Milner created the type
    inference rules you are referring to here in the 1970s.

    And?

    ...and so it's not "automagic". It's simply an application of
    well-known, pre-existing, techniques.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch,comp.programming on Mon Oct 12 19:14:14 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    Stefan Monnier <[email protected]> wrote:
    Which leads to the question? Can an experienced Rust programmer do a
    search that works like the C search(), and an insert that calls
    search() and then uses the reference to either update a node or insert
    a new one?

    IIUC it can but at the cost of making `search` take a `&mut` reference.
    I get the impression that you'd otherwise need "mutability
    polymorphism", i.e. a single piece of code that can be instantiated
    either for `&` or for `&mut`. You can probably avoid writing the code
    twice, tho, by abusing macros (aka "template polymorphism").

    ...or using interior mutability with e.g., a `Cell`.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From scott@[email protected] (Scott Lurndal) to comp.arch on Mon Oct 12 19:21:34 2026
    From Newsgroup: comp.arch

    MitchAlsup <[email protected]d> writes:



    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a
    variable. So you copy it to a temporary, and pass the address of the
    temporary to the function.

    And you think the language designers in 1956 could have figured that out ???


    For the most part, everything was in memory in 1956 (aside from an accumulator). The constant would likely have been either in the
    instruction stream itself (e.g. burroughs medium systems supported
    1 to 6 digit constants in the A operand field for most instructions)
    or in memory.

    The electrodata 220 (1958) had:
    "A" register (10 digits plus sign digit) First operand
    "R" register (10 digits plus sign digit) (extension of A register for product/quotient)
    "D" register (10 digits plus sign digit) Second operand
    "B" regsiter (4 digits) Index register
    "C" register (10 digits) Current instruction
    "P" register (4 digits) Program Counter
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Mon Oct 12 19:54:41 2026
    From Newsgroup: comp.arch

    According to MitchAlsup <[email protected]d>: [email protected] (John Savard) posted:
    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    And you think the language designers in 1956 could have figured that out ???

    The original FORTRAN compiler didn't have user defined subroutines, only
    the infamous ASSIGN statement, so it didn't come up.

    By 1958 FORTRAN II had subroutines using call by reference so I suspect someone smashed a constant argument shortly thereafter.
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 12 21:07:55 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    [email protected] (Dan Cross) posted:

    First off: thank you dan for a well done explanation.

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable >> >> array.

    Can I get a definition of what mutable in Rust means ??

    --------snip of what I understand clearly------------

    So this will fail to compile:

    ? let mut foo = something();
    ? let foomut = &mut foo; // Ok: only mutable
    ? let barmut = &mut foo // Fail: a mut ref to foo exists

    Is this supposed to work across threads ?? so, only 1 thread has write
    access at any given point in time.

    Yes, that's right.

    If so, how does one thread hand-off mutability to another ??

    Typically, you use a mutual exclusion primitive of one kind or
    another to serialize access to mutable state. For example, the
    standard library includes the usual complement of Mutex,
    reader/writer locks, condition variables, barriers, reference
    counters, thread-safe channels, and so on. For bare-metal
    programs we have to implement these ourselves, but there are
    some third-party implementations that can help (for example, the
    `spin` crate implements spinlocks, providing, an interface that
    looks close to `std::sync::Mutex`).

    Something I should have mentioned earlier is that Rust object
    has a single, well-defined owner, and that references to that
    object are borrowed _from the owner_.

    So, using `Mutex` as an example, we make the `Mutex` itself the
    owner of the object: the `.lock()` method lets threads borrow
    mutable references to the object, and the serialization inherent
    in the Mutex type and the locking protocol enforces the
    "mut ^ immut" semantics across threads.

    Similarly, a `RwLock` can hand out multiple immutable refs to
    the object, but keeps track of them, and only lets one borrower
    access a `mut ref` at a time, and only when there are no
    outstanding immutable refs to it.

    In fact, it's slightly more complex in that `mutex.lock()`
    actually returns a proxy object that holds onto the reference;
    its return type is actually, `LockResult<LockGuard<'_, T>>`,
    where `'_` is an anonymous lifetime, and `T` is the type of the
    thing protected by the `Mutex`. `LockResult` lets someone
    detect a lock that has been poisoned; the `LockGuard` is the
    thing that holds the ref. When it goes otu of scope, it
    automatically unlocks the mutex, and it implements a trait
    called `Deref` to forward uses on to the ref to the locked
    object.

    This all intersects with the `Send`/`Sync` stuff; `Mutex` is
    `Sync`, and `Mutex` objects themselves are treated as immutable.
    So to share access to a `Foo` between threads, I use something
    like a `Mutex<Foo>`. This is an example of "Interior
    Mutability", where something immutable (the `Mutex` object
    itself) can hold onto something that is mutable.

    This raises a question: if all this machinery is required for
    safe access to shared data across threads anyway, then why all
    the language-level rules? The answer is hinted at by the mutex
    object itself is immutable, we can hand out references to it
    easily. Since it owns the (mutable) object the lock protects,
    it can decide how to hand out references to the object.

    --------------snip again--------
    By default, all assignments in Rust have _move_ semantics: after
    the assignment, the source is considered destroyed, and any
    references to it are no longer valid. That's why one can assign
    to `foo` and on line 5 and then take a mut reference to it on
    line 7; the assignment on 5 implicitly dropped the original
    `foo` object created on line 1 and replaced it with a new
    object, so the mut reference it created on line 3 is invalid;
    an attempt to use it subsequently would fail to compile.

    This is clever.

    One can define a `copy` operator for types that should not have
    move semantics; this is usually applied to trivial types (like
    integers).

    One may ask, "why bother with all of this stuff?" and the answer
    is that Rust is designed for a multicore world. There is one
    other wrinkle here that gives Rust a lot of power, and that's
    how all of this stuff interacts with data races: safe Rust code
    is data race free.

    See question above.

    See above; hopefully that helps?

    There's one other thing here that may be interesting: how does
    one implement any of this stuff _in Rust_? In particular, how
    does one implement interior mutability?

    The answer is using something called the `UnsafeCell`: https://doc.rust-lang.org/std/cell/struct.UnsafeCell.html

    `UnsafeCell` allows mutation of an object (the thing owned by
    the cell) through a shared (what up to now I've been calling an
    "immutable") reference to the cell. It's the one place that
    permits that behavior.

    The implementatin of a spinlock in the `spin` crate is an
    interesting read to understand the mechanics here: https://codeberg.org/zesterer/spin/src/branch/master/src/mutex/spin.rs

    Rust does not mandate a particular shape or type of (software)
    thread, but because of the "mut ref ^ immut ref" rule, enforced
    by the borrow checker at compile-time, safe Rust programs cannot
    race on access to data, even in the face of multithreading.

    Rust handles this by exposing two "marker traits", `Sync` and
    `Send`. To quote the _Rustinomicon_, "A type is `Send` if it is
    safe to send it to another thread" and "A type is `Sync` if it
    is safe to share between threads (T is `Sync` if and only if
    `&T` is `Send`)." That is, a type can be _shared_ between
    threads if it is safe to send an immutable reference to it from
    one thread to another, and the compiler can check borrows
    against the lifetimes of objects, hence, can detect data races
    _statically, at compile time_.
    (https://doc.rust-lang.org/nomicon/send-and-sync.html)

    Those of us familiar with writing multithreaded programs will
    recognize the power of these constructs: these rules, taken
    together, statically eliminate entire categories of common
    memory and concurrency problems.

    Anyway, that what "mutable" means, in a nutshell, and why it is
    important.

    Thanks again for a complete explanation.

    Happy to do it.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Mon Oct 12 21:10:31 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:
    [email protected] (John Savard) posted:
    On Mon, 12 Oct 2026 11:25:32 -0000 (UTC), Thomas Koenig
    <[email protected]> wrote:
    David Brown <[email protected]> schrieb:
    But if you receive a "pointer to const" parameter to a function, you can >> >> still write via a cast version of the pointer if the original object was >> >> not defined as "const". (So objects defined as "const" may be placed in >> >> read-only memory of some kind, and trying to change them might cause a >> >> crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address.

    And the proper way to correct this error is obvious. Not by putting
    constants in a segment marked read-only. But in properly implementing
    language semantics.

    So the values of constants are stored in memory.

    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a
    variable. So you copy it to a temporary, and pass the address of the
    temporary to the function.

    And you think the language designers in 1956 could have figured that out ???

    I don't think that's the right framing.

    Not doing that was the bug.

    Some would disagree.

    That it's a bug? Clearly, changing the value of some constant
    is surprising, at best.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Lawrence =?iso-8859-13?q?D=FFOliveiro?=@[email protected] to comp.arch,comp.programming on Mon Oct 12 21:15:26 2026
    From Newsgroup: comp.arch

    On Sun, 11 Oct 2026 14:26:48 -0400, Stefan Monnier wrote:

    You can probably avoid writing the code twice, tho, by abusing
    macros (aka "template polymorphism").

    Why would that be “abusing”? Aren’t macros designed for just such a purpose?
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Mon Oct 12 22:35:57 2026
    From Newsgroup: comp.arch


    [email protected] (Dan Cross) posted:

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    [email protected] (Dan Cross) posted:

    First off: thank you dan for a well done explanation.

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable >> >> array.

    Can I get a definition of what mutable in Rust means ??

    --------snip of what I understand clearly------------

    So this will fail to compile:

    ? let mut foo = something();
    ? let foomut = &mut foo; // Ok: only mutable
    ? let barmut = &mut foo // Fail: a mut ref to foo exists

    Is this supposed to work across threads ?? so, only 1 thread has write >access at any given point in time.

    Yes, that's right.

    If so, how does one thread hand-off mutability to another ??

    Typically, you use a mutual exclusion primitive of one kind or
    another to serialize access to mutable state. For example, the
    standard library includes the usual complement of Mutex,
    reader/writer locks, condition variables, barriers, reference
    counters, thread-safe channels, and so on. For bare-metal
    programs we have to implement these ourselves, but there are
    some third-party implementations that can help (for example, the
    `spin` crate implements spinlocks, providing, an interface that
    looks close to `std::sync::Mutex`).

    Something I should have mentioned earlier is that Rust object
    has a single, well-defined owner, and that references to that
    object are borrowed _from the owner_.

    I was expecting one "thread" to hold (own?) the object in question
    and was unsure how mutablility was handed out across other threads.

    I had imagined that then current mut owner had its 'pointer' go
    out of block and that somehow his no longer having a mutable ref
    to the object somehow got propagated automagically by his leaving
    of the block {which seems untenable}.

    falling back on standard ways of passing this stuff around adds hope.
    {{less Ada rendezvous like}}

    So, using `Mutex` as an example, we make the `Mutex` itself the
    owner of the object: the `.lock()` method lets threads borrow
    mutable references to the object, and the serialization inherent
    in the Mutex type and the locking protocol enforces the
    "mut ^ immut" semantics across threads.

    Since I am a CPU guy, I would like to see the asm from any modern
    architecture perform this handoff ! <please> ?

    Similarly, a `RwLock` can hand out multiple immutable refs to
    the object, but keeps track of them, and only lets one borrower
    access a `mut ref` at a time, and only when there are no
    outstanding immutable refs to it.

    Sounds like reference counting...

    In fact, it's slightly more complex in that `mutex.lock()`
    actually returns a proxy object that holds onto the reference;
    its return type is actually, `LockResult<LockGuard<'_, T>>`,
    where `'_` is an anonymous lifetime, and `T` is the type of the
    thing protected by the `Mutex`. `LockResult` lets someone
    detect a lock that has been poisoned; the `LockGuard` is the
    thing that holds the ref. When it goes otu of scope, it
    automatically unlocks the mutex, and it implements a trait
    called `Deref` to forward uses on to the ref to the locked
    object.

    Another level of indirection to handle a more complicated issue...

    This all intersects with the `Send`/`Sync` stuff; `Mutex` is
    `Sync`, and `Mutex` objects themselves are treated as immutable.
    So to share access to a `Foo` between threads, I use something
    like a `Mutex<Foo>`. This is an example of "Interior
    Mutability", where something immutable (the `Mutex` object
    itself) can hold onto something that is mutable.

    This raises a question: if all this machinery is required for
    safe access to shared data across threads anyway, then why all
    the language-level rules? The answer is hinted at by the mutex
    object itself is immutable, we can hand out references to it
    easily. Since it owns the (mutable) object the lock protects,
    it can decide how to hand out references to the object.

    We have the same problems with coherent caches and there are 3
    normal solutions {MESI, MOESI, and Directories}

    --------------snip again--------
    By default, all assignments in Rust have _move_ semantics: after
    the assignment, the source is considered destroyed, and any
    references to it are no longer valid. That's why one can assign
    to `foo` and on line 5 and then take a mut reference to it on
    line 7; the assignment on 5 implicitly dropped the original
    `foo` object created on line 1 and replaced it with a new
    object, so the mut reference it created on line 3 is invalid;
    an attempt to use it subsequently would fail to compile.

    This is clever.

    One can define a `copy` operator for types that should not have
    move semantics; this is usually applied to trivial types (like
    integers).

    One may ask, "why bother with all of this stuff?" and the answer
    is that Rust is designed for a multicore world. There is one
    other wrinkle here that gives Rust a lot of power, and that's
    how all of this stuff interacts with data races: safe Rust code
    is data race free.

    See question above.

    See above; hopefully that helps?

    Yes, quite a lot. Thanks for taking the time.

    I ask these detailed questions to see if there is some tweak to
    my ISA that could allow a less costly solution with equivalent
    semantics.

    There's one other thing here that may be interesting: how does
    one implement any of this stuff _in Rust_? In particular, how
    does one implement interior mutability?

    I care about ISA semantics, where some small-moderate tweak might
    shrink the code path significantly.

    The answer is using something called the `UnsafeCell`: https://doc.rust-lang.org/std/cell/struct.UnsafeCell.html

    `UnsafeCell` allows mutation of an object (the thing owned by
    the cell) through a shared (what up to now I've been calling an
    "immutable") reference to the cell. It's the one place that
    permits that behavior.

    The implementatin of a spinlock in the `spin` crate is an
    interesting read to understand the mechanics here: https://codeberg.org/zesterer/spin/src/branch/master/src/mutex/spin.rs

    I will read these later.

    Rust does not mandate a particular shape or type of (software)
    thread, but because of the "mut ref ^ immut ref" rule, enforced
    by the borrow checker at compile-time, safe Rust programs cannot
    race on access to data, even in the face of multithreading.

    Rust handles this by exposing two "marker traits", `Sync` and
    `Send`. To quote the _Rustinomicon_, "A type is `Send` if it is
    safe to send it to another thread" and "A type is `Sync` if it
    is safe to share between threads (T is `Sync` if and only if
    `&T` is `Send`)." That is, a type can be _shared_ between
    threads if it is safe to send an immutable reference to it from
    one thread to another, and the compiler can check borrows
    against the lifetimes of objects, hence, can detect data races
    _statically, at compile time_.
    (https://doc.rust-lang.org/nomicon/send-and-sync.html)

    Those of us familiar with writing multithreaded programs will
    recognize the power of these constructs: these rules, taken
    together, statically eliminate entire categories of common
    memory and concurrency problems.

    Anyway, that what "mutable" means, in a nutshell, and why it is
    important.

    Thanks again for a complete explanation.

    Happy to do it.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Mon Oct 12 22:37:34 2026
    From Newsgroup: comp.arch


    [email protected] (Dan Cross) posted:

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:
    [email protected] (John Savard) posted:
    On Mon, 12 Oct 2026 11:25:32 -0000 (UTC), Thomas Koenig
    <[email protected]> wrote:
    David Brown <[email protected]> schrieb:
    But if you receive a "pointer to const" parameter to a function, you can
    still write via a cast version of the pointer if the original object was
    not defined as "const". (So objects defined as "const" may be placed in
    read-only memory of some kind, and trying to change them might cause a >> >> crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address.

    And the proper way to correct this error is obvious. Not by putting
    constants in a segment marked read-only. But in properly implementing
    language semantics.

    So the values of constants are stored in memory.

    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a
    variable. So you copy it to a temporary, and pass the address of the
    temporary to the function.

    And you think the language designers in 1956 could have figured that out ???

    I don't think that's the right framing.

    Not doing that was the bug.

    Some would disagree.

    That it's a bug? Clearly, changing the value of some constant
    is surprising, at best.

    Allows one to have 1 {sin(), cos() ...} subroutine and change the value of pi so one then has {sinpi(), cospi(), ...}

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Tue Oct 13 00:35:05 2026
    From Newsgroup: comp.arch

    According to Dan Cross <[email protected]>:
    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a
    variable. So you copy it to a temporary, and pass the address of the
    temporary to the function.

    And you think the language designers in 1956 could have figured that out ???

    I don't think that's the right framing.

    Not doing that was the bug.

    Some would disagree.

    That it's a bug? Clearly, changing the value of some constant
    is surprising, at best.

    I wasn't writing programs in 1958, having had a prior committment in
    nursery school, but I was writing programs in the late 1960s.

    I think you would find the attitude was yes, you could do that, but it
    would be stupid, so don't. Remember that FORTRAN ran on a 70x with 32K
    words of memory, S/360 subset FORTRAN E ran on a 360/30 with 32K bytes
    and full FORTRAN G ran on a 360/40 with 128K bytes. Every word was
    precious, and wasting three or four words creating a temporary to
    defend against an obvious programmer error would have been silly.
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Niklas Holsti@[email protected] to comp.arch on Tue Oct 13 12:03:13 2026
    From Newsgroup: comp.arch

    On 2026-10-12 20:10, Anton Ertl wrote:
    David Brown <[email protected]> writes:
    Forth is arguably even better for this :

    : 21 42 ;
    21 .
    42 ok

    That is - you can redefine the word "21" to put the value 42 on the
    stack. Then if you ask for the number "21", you get the value 42. It
    is not an oddity of the implementation, but the way the language works.

    Yes, but used like this, it's only a curiosity; nobody does this
    except to show that it can be done.

    Forth originated in astronomy, if I remember correctly. I do remember
    that in an astronomy-domain Forth program, where it was often necessary
    to transform the celestial coordinates of stars from the standard
    catalog epoch (the start of the year 1950) to the current time, the word 1950.0 was defined to perform that transformation.

    That Forth system did not have floating-point support, so 1950.0 could
    not be understood as a floating-point literal. But if the program had
    been ported to a Forth system with floating-point support, confusion
    could have resulted.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Tue Oct 13 10:27:12 2026
    From Newsgroup: comp.arch

    Niklas Holsti <[email protected]d> writes:
    Forth originated in astronomy, if I remember correctly. I do remember
    that in an astronomy-domain Forth program, where it was often necessary
    to transform the celestial coordinates of stars from the standard
    catalog epoch (the start of the year 1950) to the current time, the word >1950.0 was defined to perform that transformation.

    That is unlikely, as explained below.

    That Forth system did not have floating-point support, so 1950.0 could
    not be understood as a floating-point literal. But if the program had
    been ported to a Forth system with floating-point support, confusion
    could have resulted.

    Most Forth systems recognize strings like 1950.0 as double-cell
    numbers. That's expecially true in early systems, where 16-bit cells
    were too small for the arithmetic needs of many applications
    (including in astronomy). In early systems fixed-point was used a
    lot, and 1950.0 is interpreted as 19500 with a scale factor of 1/10.

    Only in recent times with 32-bit and 64-bit cells roll-your-own system implementors have chosen to abandon double-cell numbers, but
    mainstream systems (even with 64-bit cells) still recognize 1950.0 as double-cell number. Just on 2026-10-05 someone complained on
    comp.lang.forth <[email protected]>:

    |Maybe there's a mechanism in Gforth that stops annoying nags when
    |printing numbers?
    |
    |Gforth 0.7.9_20200709
    |
    |1. d.
    |*terminal*:1:1: warning: '1.' is a double-cell integer; type `help' for more info1 ok

    The warning is not about printing the double-cell number, but about
    the "1.". But anyway, it shows that double-cell numbers are still
    being used.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Andy Valencia@[email protected] to comp.arch on Tue Oct 13 09:09:52 2026
    From Newsgroup: comp.arch

    [email protected] (Anton Ertl) writes:
    hex \ change to base #16
    add \ is this a dictionary word, or a number?

    The classic ways to avoid this has been to

    1) prefix hex numbers with 0 if they would otherwise start with a
    letter. Dictionary words rarely start with digits, and when they
    do, they don't conflict with hex numbers.

    Alternately, Forth could adopt practices which have become broad,
    de facto standards. So, referencing above, instead hex numbers
    have a 0xadd prefix, and you don't switch the base of the system.

    While you're at it, start the transition to "//" as the comment
    token. From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    Andy Valencia
    Home page: https://www.vsta.org/andy/
    To contact me: https://www.vsta.org/contact/andy.html
    No AI was used in the composition of this message
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Tue Oct 13 16:31:23 2026
    From Newsgroup: comp.arch

    Andy Valencia <[email protected]> writes:
    [email protected] (Anton Ertl) writes:
    hex \ change to base #16
    add \ is this a dictionary word, or a number?

    The classic ways to avoid this has been to

    1) prefix hex numbers with 0 if they would otherwise start with a
    letter. Dictionary words rarely start with digits, and when they
    do, they don't conflict with hex numbers.

    Alternately, Forth could adopt practices which have become broad,
    de facto standards. So, referencing above, instead hex numbers
    have a 0xadd prefix, and you don't switch the base of the system.

    I don't change the base for inputting numbers, and use the
    standardized prefix $ instead ($add), as mentioned. A number of
    systems recognize numbers with the prefix 0x as hex numbers, too.
    Finally, you can add user-defined recognizers in a number of Forth
    systems, and recognize 0x-prefixed numbers on these systems.

    While you're at it, start the transition to "//" as the comment
    token.

    You can define that yourself if you want:

    : // 0 parse 2drop ; immediate

    But unlike for the 0x prefix, few Forth systems have implemented //
    comments, so demand for that is low. And why should there be demand
    for it?

    For the 0x prefix, the demand comes from interoperating with
    C-oriented tools that output hex numbers with a 0x prefix. But
    comments have no such interoperability issues.

    From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    Most programming languages use something else for comments: Unix
    shells, Lisp, Makefiles, assembly language, Postscript and Ada come to
    my mind. Maybe some of them also accept //, but I have yet to see a
    program in one of these languages that uses such a comment.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Niklas Holsti@[email protected] to comp.arch on Tue Oct 13 20:00:18 2026
    From Newsgroup: comp.arch

    On 2026-10-13 13:27, Anton Ertl wrote:
    Niklas Holsti <[email protected]d> writes:
    Forth originated in astronomy, if I remember correctly. I do remember
    that in an astronomy-domain Forth program, where it was often necessary
    to transform the celestial coordinates of stars from the standard
    catalog epoch (the start of the year 1950) to the current time, the word
    1950.0 was defined to perform that transformation.

    That is unlikely, as explained below.

    I am rather sure that my memory is correct.

    This was quite a long time ago -- some time in the 1980s, IIRC.

    That Forth system did not have floating-point support, so 1950.0 could
    not be understood as a floating-point literal. But if the program had
    been ported to a Forth system with floating-point support, confusion
    could have resulted.

    Most Forth systems recognize strings like 1950.0 as double-cell
    numbers. That's expecially true in early systems, where 16-bit cells
    were too small for the arithmetic needs of many applications
    (including in astronomy).

    OK, I did not know about that double-cell interpretation, so my comments
    about floating point are mistaken.

    In early systems fixed-point was used a lot, and 1950.0 is
    interpreted as 19500 with a scale factor of 1/10.
    The Forth program in question represented angles as fixed-point
    fractions (0 <= x < 1) of a full circle. I don't remember the number of
    bits used.

    Even if that Forth system by default interpreted 1950.0 as a double-cell number, apparently it was still possible to override that interpretation
    by defining a word called 1950.0.

    But I trust we agree that such word names are not to be recommended.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Niklas Holsti@[email protected] to comp.arch on Tue Oct 13 20:02:29 2026
    From Newsgroup: comp.arch

    On 2026-10-13 19:09, Andy Valencia wrote:
    [email protected] (Anton Ertl) writes:
    hex \ change to base #16
    add \ is this a dictionary word, or a number?

    The classic ways to avoid this has been to

    1) prefix hex numbers with 0 if they would otherwise start with a
    letter. Dictionary words rarely start with digits, and when they
    do, they don't conflict with hex numbers.

    Alternately, Forth could adopt practices which have become broad,
    de facto standards. So, referencing above, instead hex numbers
    have a 0xadd prefix, and you don't switch the base of the system.

    While you're at it, start the transition to "//" as the comment
    token. From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    Ada uses "--".

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Tue Oct 13 19:50:47 2026
    From Newsgroup: comp.arch

    On 13/10/2026 19:02, Niklas Holsti wrote:
    On 2026-10-13 19:09, Andy Valencia wrote:
    [email protected] (Anton Ertl) writes:
    hex \ change to base #16
    add \ is this a dictionary word, or a number?

    The classic ways to avoid this has been to

    1) prefix hex numbers with 0 if they would otherwise start with a
        letter.  Dictionary words rarely start with digits, and when they >>>     do, they don't conflict with hex numbers.

    Alternately, Forth could adopt practices which have become broad,
    de facto standards.  So, referencing above, instead hex numbers
    have a 0xadd prefix, and you don't switch the base of the system.

    0x prefixes for hex is certainly common, but $ prefixes for hex have
    been around for a long time too and are used in Pascal. Some languages
    use a suffix, like 0addH. Ada has 16#add#, while VHDL has x"add". Some BASICs use &. So again, there are plenty of options from existing practice.


    While you're at it, start the transition to "//" as the comment
    token.  From memory, who's holding out?  Only Forth, Smalltalk, and
    COBOL come to mind.

    Ada uses "--".


    As does Lua, VHDL, Haskell, and SQL. And lots of languages use #, TeX
    has %, several languages use ; and there are many others.

    <https://en.wikipedia.org/wiki/Comparison_of_programming_languages_(syntax)#Comments>



    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From scott@[email protected] (Scott Lurndal) to comp.arch on Tue Oct 13 18:12:40 2026
    From Newsgroup: comp.arch

    [email protected] (Anton Ertl) writes:
    Andy Valencia <[email protected]> writes:

    From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    Most programming languages use something else for comments: Unix
    shells, Lisp, Makefiles, assembly language, Postscript and Ada come to
    my mind. Maybe some of them also accept //, but I have yet to see a
    program in one of these languages that uses such a comment.

    And one can simply pipe them through cpp before sending
    the source to the compiler to remove comments starting with '//'.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Tue Oct 13 18:40:18 2026
    From Newsgroup: comp.arch

    John Savard <[email protected]> schrieb:
    On Mon, 12 Oct 2026 11:25:32 -0000 (UTC), Thomas Koenig
    <[email protected]> wrote:

    David Brown <[email protected]> schrieb:

    But if you receive a "pointer to const" parameter to a function, you can >>> still write via a cast version of the pointer if the original object was >>> not defined as "const". (So objects defined as "const" may be placed in >>> read-only memory of some kind, and trying to change them might cause a
    crash, signal, bus-error, etc.)

    In FORTRAN, it used to be possible on systems such as /360 to write

    CALL FOO(21)
    WRITE (*,*) 21
    END

    SUBROUTINE FOO(N)
    N = 42
    END

    and get a printout of 42 (21 is only half the truth,
    remember!) because the compiler put the constants in read/write
    storage and arguments were passed via address.

    And the proper way to correct this error is obvious. Not by putting
    constants in a segment marked read-only. But in properly implementing language semantics.

    First, those semantics have to be defined... but I thought you were
    a great fan of all things /360, so I assumed you would like this :-)

    So the values of constants are stored in memory.

    However, when a constant is passed to a function as an argument,
    clearly that constant should be treated as an *expression*, not as a variable. So you copy it to a temporary, and pass the address of the temporary to the function. Not doing that was the bug.

    That would penalize correct code, which is a no-no.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Stefan Monnier@[email protected] to comp.arch on Tue Oct 13 23:24:50 2026
    From Newsgroup: comp.arch

    While you're at it, start the transition to "//" as the comment
    token. From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    There are many many more, including a little known language named after something like a snake or some British comedy. In that language `//` is
    the integer division operation.


    === Stefan
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Wed Oct 14 09:56:52 2026
    From Newsgroup: comp.arch

    On 14/10/2026 05:24, Stefan Monnier wrote:
    While you're at it, start the transition to "//" as the comment
    token. From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    There are many many more, including a little known language named after something like a snake or some British comedy. In that language `//` is
    the integer division operation.


    The language was named for the comedy group. The group in question does
    not have an explanation for the name, and just like the sound of it.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Wed Oct 14 08:02:38 2026
    From Newsgroup: comp.arch

    Niklas Holsti <[email protected]d> writes:
    On 2026-10-13 13:27, Anton Ertl wrote:
    Niklas Holsti <[email protected]d> writes:
    Forth originated in astronomy, if I remember correctly. I do remember
    that in an astronomy-domain Forth program, where it was often necessary
    to transform the celestial coordinates of stars from the standard
    catalog epoch (the start of the year 1950) to the current time, the word >>> 1950.0 was defined to perform that transformation.

    That is unlikely, as explained below.

    I am rather sure that my memory is correct.

    In early systems fixed-point was used a lot, and 1950.0 is
    interpreted as 19500 with a scale factor of 1/10.
    The Forth program in question represented angles as fixed-point
    fractions (0 <= x < 1) of a full circle. I don't remember the number of
    bits used.

    Yes, that was customary for angles, as mentioned in the HOPL-II paper
    "The Evolution of Forth" <https://www.forth.com/resources/forth-programming-language/>:

    |Moore expressed angles internally as 14-bit, 15-bit or 30-bit
    |fixed-point binary fractions.

    Even if that Forth system by default interpreted 1950.0 as a double-cell >number, apparently it was still possible to override that interpretation
    by defining a word called 1950.0.

    Sure it's possible, but there is no reason why one would call a
    conversion word 1950.0. What would have been more in line with
    practice is to define a word 1950.0 which does what writing 1950.0
    without such a word does (if the BASE is right): to push a double-cell
    number 19500. The benefit of defining such a word in an
    indirect-threaded code implementation (usual for Forth in those times)
    is that invoking the word costs only one cell, whereas a double-cell
    constant costs 4 cells. The word amortizes its memory once more than
    three occurences are in play.

    The disadvantage of doing that for numbers with more than one digit is
    that the BASE no longer influences the number that is pushed by
    writing 1950.0. But it can easily be that, for this application, this
    was acceptable. EPOCH still might have been a better name, though.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Wed Oct 14 08:50:43 2026
    From Newsgroup: comp.arch

    [email protected] (Anton Ertl) writes:
    What would have been more in line with
    practice is to define a word 1950.0 which does what writing 1950.0
    without such a word does (if the BASE is right): to push a double-cell
    number 19500.
    ...
    EPOCH still might have been a better name, though.

    One possible reason for not calling it EPOCH would be that the
    programmer noticed that there were many uses of 1950.0 around the
    source code and then decided to save memory by defining a named word
    1950.0. Calling that word EPOCH would have required to search and
    replace all occurences of 1950.0, and who knows what editing tools the programmers had at their disposal (probably the classic Forth block
    editor, which I have never used); in any case, calling the word 1950.0
    avoided the need for this step.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From George Neuner@[email protected] to comp.arch on Wed Oct 14 12:14:30 2026
    From Newsgroup: comp.arch

    On Tue, 13 Oct 2026 23:24:50 -0400, Stefan Monnier
    <[email protected]> wrote:


    Andy Valencia wrote:
    While you're at it, start the transition to "//" as the comment
    token. From memory, who's holding out? Only Forth, Smalltalk, and
    COBOL come to mind.

    @Andy
    Seriously, no thoughts on block comments? Shouldn't every language be
    forced to use /* */


    There are many many more, ...

    Indeed.
    https://gist.github.com/dk949/88b2652284234f723decaeb84db2576c

    I saw a few examples that were wrong (some addressed by replies), and
    a couple I know to be implementation dependent, but this looks to be a reasonable start on a survey.


    MMV but I think too many people in this forum assume that C is the
    only language that matters.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Wed Oct 14 18:07:31 2026
    From Newsgroup: comp.arch

    On Wed, 14 Oct 2026 12:14:30 -0400, George Neuner
    <[email protected]> wrote:

    Seriously, no thoughts on block comments? Shouldn't every language be
    forced to use /* */

    Using pairs of special characters for *any* purpose, from ** for
    exponentiation in FORTRAN onwards, and including <= and >= as
    relational operators, vastly complicates the syntax decoding of
    languages.

    Of course, even at
    http://www.quadibloc.com/comp/lngint.htm
    I couldn't _entirely_ avoid compounds of special characters. So what I
    did was adopt systematic rules:
    Only ?, @, and & can begin multi-character strings which are to be
    treated as a single character. @" is used for //, and @< and @> are
    used for /* and */. (The @ sign is generally used only for tokens
    which are "outside" of actual statements, at the upper level of the
    syntax.) &LE and &GE are used for <= and >=.

    Incidentally, C didn't invent /* and */; it got them from PL/I,
    although no doubt PL/I got them from some representation or other of
    ALGOL.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Wed Oct 14 18:50:19 2026
    From Newsgroup: comp.arch

    John Savard <[email protected]> schrieb:
    On Wed, 14 Oct 2026 12:14:30 -0400, George Neuner
    <[email protected]> wrote:

    Seriously, no thoughts on block comments? Shouldn't every language be >>forced to use /* */

    Using pairs of special characters for *any* purpose, from ** for exponentiation in FORTRAN onwards, and including <= and >= as
    relational operators, vastly complicates the syntax decoding of
    languages.

    Do learn a bit about regular expressions and how they are
    implemented using state machines. Knowing about state machines
    is also fundamental when thinking about computer architectures
    and how to iplement them.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Wed Oct 14 15:45:41 2026
    From Newsgroup: comp.arch

    On 2026-Oct-14 14:07, John Savard wrote:
    On Wed, 14 Oct 2026 12:14:30 -0400, George Neuner
    <[email protected]> wrote:

    Seriously, no thoughts on block comments? Shouldn't every language be
    forced to use /* */

    Using pairs of special characters for *any* purpose, from ** for exponentiation in FORTRAN onwards, and including <= and >= as
    relational operators, vastly complicates the syntax decoding of
    languages.

    Of course, even at
    http://www.quadibloc.com/comp/lngint.htm
    I couldn't _entirely_ avoid compounds of special characters. So what I
    did was adopt systematic rules:
    Only ?, @, and & can begin multi-character strings which are to be
    treated as a single character. @" is used for //, and @< and @> are
    used for /* and */. (The @ sign is generally used only for tokens
    which are "outside" of actual statements, at the upper level of the
    syntax.) &LE and &GE are used for <= and >=.

    Incidentally, C didn't invent /* and */; it got them from PL/I,
    although no doubt PL/I got them from some representation or other of
    ALGOL.

    John Savard

    Most languages are not as bad as pre-FORTRAN-90.
    Lexing ** into a single token is a 1 character lookahead.

    Old FORTRAN is difficult to parse because
    a) it did not separate the language into lexical and syntax levels
    b) it was white space oblivious except in quoted strings
    c) there were no reserved words - keywords were statement
    position and sometimes semantic context dependent.
    d) statements were based on punch cards BUT because
    continuation cards used the absence of a C in column 6
    to indicate the start of a new statement, parsers have to
    always look ahead to see in the next card has a C and
    then "unget" the card if not.

    The classic ambiguous parsing example being

    DO I = 1,100
    DOI = 1.100

    depends on the presence or absence of a "naked comma" later in the
    statement to decide whether there is a keyword DO and a variable I,
    or a variable DOI. A "naked comma" is a comma not enclosed inside
    a quoted character or string, or inside a possibly nested parenthesis.
    There are other examples like keywords in FORMAT statements.

    People learned a lot of what not to do in language design
    from old FORTRAN.


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Terje Mathisen@[email protected] to comp.arch on Wed Oct 14 23:39:20 2026
    From Newsgroup: comp.arch

    Anton Ertl wrote:
    Niklas Holsti <[email protected]d> writes:
    On 2026-10-13 13:27, Anton Ertl wrote:
    Niklas Holsti <[email protected]d> writes:
    Forth originated in astronomy, if I remember correctly. I do remember
    that in an astronomy-domain Forth program, where it was often necessary >>>> to transform the celestial coordinates of stars from the standard
    catalog epoch (the start of the year 1950) to the current time, the word >>>> 1950.0 was defined to perform that transformation.

    That is unlikely, as explained below.

    I am rather sure that my memory is correct.

    In early systems fixed-point was used a lot, and 1950.0 is
    interpreted as 19500 with a scale factor of 1/10.
    The Forth program in question represented angles as fixed-point
    fractions (0 <= x < 1) of a full circle. I don't remember the number of
    bits used.

    Yes, that was customary for angles, as mentioned in the HOPL-II paper
    "The Evolution of Forth" <https://www.forth.com/resources/forth-programming-language/>:

    Garmin uses (used?) fixed-point only in their GPSs, with circles stored
    as a scaled 0:32 value with an implied period in front. Since the Earth
    is 40 000 km around (over the poles, about 1/300 more along equator),
    this corresponds to about 9mm precision which they probably thought was
    more than good enough for a consumer handheld device.

    Terje
    --
    - <Terje.Mathisen at tmsw.no>
    "almost all programming can be viewed as an exercise in caching"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Wed Oct 14 22:43:32 2026
    From Newsgroup: comp.arch


    [email protected] (John Savard) posted:

    On Wed, 14 Oct 2026 12:14:30 -0400, George Neuner
    <[email protected]> wrote:

    Seriously, no thoughts on block comments? Shouldn't every language be >forced to use /* */

    Using pairs of special characters for *any* purpose, from ** for exponentiation in FORTRAN onwards, and including <= and >= as
    relational operators, vastly complicates the syntax decoding of
    languages.

    On the other hand using « » ÷ ≠ ≤ ≥ ≡ would destroy the 1 char == 1 byte
    philosophy of compilers and assemblers.

    Of course, even at
    http://www.quadibloc.com/comp/lngint.htm
    I couldn't _entirely_ avoid compounds of special characters. So what I
    did was adopt systematic rules:
    Only ?, @, and & can begin multi-character strings which are to be
    treated as a single character. @" is used for //, and @< and @> are
    used for /* and */. (The @ sign is generally used only for tokens
    which are "outside" of actual statements, at the upper level of the
    syntax.) &LE and &GE are used for <= and >=.

    Incidentally, C didn't invent /* and */; it got them from PL/I,
    although no doubt PL/I got them from some representation or other of
    ALGOL.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Thu Oct 15 06:17:50 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> writes:
    Old FORTRAN is difficult to parse because
    a) it did not separate the language into lexical and syntax levels
    b) it was white space oblivious except in quoted strings
    c) there were no reserved words - keywords were statement
    position and sometimes semantic context dependent.
    d) statements were based on punch cards BUT because
    continuation cards used the absence of a C in column 6
    to indicate the start of a new statement, parsers have to
    always look ahead to see in the next card has a C and
    then "unget" the card if not.

    Given that the original FORTRAN worked on an IBM 704, I think that the designers made it as easy (and with as little memory) to parse as they
    could, given the state of the art at the time. Concerning the points:

    a) I guess that the division into lexical and syntax levels had not
    been developed at the time; and even later, e.g., Wirth does not
    make this distinction in his language definitions, using EBNF down
    to the lexical level (although he does define reserved words).

    b) Without this distinction, if you ignore white space at all, you
    either ignore it everywhere, or give it a special role in your
    syntax. Ignoring is probably simpler.

    c) The programming language community took quite a while until it
    arrived at reserved words. E.g., while Algol 60 was specified in
    BNF, being modern in that regard, it does not have reserved words,
    just symbols that are distinct from all identifiers; i.e., you
    could have an identifier like IF in Algol 60, distinct from the
    symbol 'IF', and it's up to the implementation how to distinguish
    them. Anyway, without a) reserved words make no sense.

    d) Does the original FORTRAN support continuation cards?

    The classic ambiguous parsing example being

    DO I = 1,100
    DOI = 1.100

    depends on the presence or absence of a "naked comma" later in the
    statement to decide whether there is a keyword DO and a variable I,
    or a variable DOI. A "naked comma" is a comma not enclosed inside
    a quoted character or string, or inside a possibly nested parenthesis.

    My guess as to how this stuff was parsed:

    Deal with the first columns as appropriate. For the rest: Remove any
    white space. Use a backtracking parser that tries to match a rule at
    a time; for the DO statement, simply compare the start with "DO". If
    a rule does not match, look at the next rule.

    The result is messy, especially when you want to extend the language.

    People learned a lot of what not to do in language design
    from old FORTRAN.

    And probably also what to do. Backus was on the Algol-60 committee,
    and the B in BNF stands for his name. I would not be surprised if the structure of the parser in the FORTRAN compiler gave him ideas for
    BNF.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Thu Oct 15 18:15:01 2026
    From Newsgroup: comp.arch

    On Wed, 14 Oct 2026 22:43:32 GMT, MitchAlsup
    <[email protected]d> wrote:

    On the other hand using « » ÷ ≠ ≤ ≥ ≡ would destroy the 1 char == 1 byte
    philosophy of compilers and assemblers.

    I'm suspecting that you're posting from the computer that was unable
    to have been called a McIntosh due to the pre-existing audio amplifier
    brand.

    In any case, that's hardly an issue, since a compiler using characters
    outside of ISO 8859-1 could always perform a pre-pass which translates
    all characters meaningful to the language to a custom character set
    which includes them. There's also the option of restricting oneself to
    the Basic Multilingual Plane, and just not using UTF-8 but instead
    coding for a 1 char == 1 halfword philosophy instead.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Fri Oct 16 06:35:33 2026
    From Newsgroup: comp.arch

    Anton Ertl <[email protected]> schrieb:
    EricP <[email protected]> writes:
    Old FORTRAN is difficult to parse because
    a) it did not separate the language into lexical and syntax levels
    b) it was white space oblivious except in quoted strings
    c) there were no reserved words - keywords were statement
    position and sometimes semantic context dependent.
    d) statements were based on punch cards BUT because
    continuation cards used the absence of a C in column 6
    to indicate the start of a new statement, parsers have to
    always look ahead to see in the next card has a C and
    then "unget" the card if not.

    Given that the original FORTRAN worked on an IBM 704, I think that the designers made it as easy (and with as little memory) to parse as they
    could, given the state of the art at the time. Concerning the points:

    a) I guess that the division into lexical and syntax levels had not
    been developed at the time;

    They did not even have recursive descent parsing, and nobody had
    parsed formulas with operator precedence before. See https://en.wikipedia.org/wiki/Operator-precedence_parser#Full_parenthesization .

    and even later, e.g., Wirth does not
    make this distinction in his language definitions, using EBNF down
    to the lexical level (although he does define reserved words).

    Also in modern language definition. Separating lexing and parsing
    is a matter of convenience, especially since regular expressions
    have been introduced, and speed.

    b) Without this distinction, if you ignore white space at all, you
    either ignore it everywhere, or give it a special role in your
    syntax. Ignoring is probably simpler.

    The added complication of separate handling of whitespace is minor.
    For the original FORTRAN, they skipped them because card punch operators
    often made mistakes with spaces.

    c) The programming language community took quite a while until it
    arrived at reserved words.

    Reserved words make the compiler writer's job easier (somewhat,
    but not tremendously), but not having them is a solved problem.
    Users are left holding the bag if a new version of the language
    introduces new keywords.

    Same with intrinsics (or standard functions, or whatever...) if
    a new version of the language introduces new ones, users can run
    into problems.

    E.g., while Algol 60 was specified in
    BNF, being modern in that regard, it does not have reserved words,
    just symbols that are distinct from all identifiers; i.e., you
    could have an identifier like IF in Algol 60, distinct from the
    symbol 'IF', and it's up to the implementation how to distinguish
    them. Anyway, without a) reserved words make no sense.

    d) Does the original FORTRAN support continuation cards?

    Yes, it would have extremely restricted otherwise.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Stefan Monnier@[email protected] to comp.arch on Thu Oct 15 15:42:54 2026
    From Newsgroup: comp.arch

    Given that the original FORTRAN worked on an IBM 704, I think that the designers made it as easy (and with as little memory) to parse as they
    could, given the state of the art at the time. Concerning the points:

    Indeed, the issue being that the "state of the art" (in terms of parsing knowledge) was very primitive. It's actually in part in response to
    the painful experience of Fortran that BNF appeared.


    === Stefan
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Fri Oct 16 12:46:57 2026
    From Newsgroup: comp.arch

    On 2026-Oct-15 02:17, Anton Ertl wrote:
    EricP <[email protected]> writes:
    Old FORTRAN is difficult to parse because
    a) it did not separate the language into lexical and syntax levels
    b) it was white space oblivious except in quoted strings
    c) there were no reserved words - keywords were statement
    position and sometimes semantic context dependent.
    d) statements were based on punch cards BUT because
    continuation cards used the absence of a C in column 6
    to indicate the start of a new statement, parsers have to
    always look ahead to see in the next card has a C and
    then "unget" the card if not.

    Given that the original FORTRAN worked on an IBM 704, I think that the designers made it as easy (and with as little memory) to parse as they
    could, given the state of the art at the time. Concerning the points:

    a) I guess that the division into lexical and syntax levels had not
    been developed at the time; and even later, e.g., Wirth does not
    make this distinction in his language definitions, using EBNF down
    to the lexical level (although he does define reserved words).

    b) Without this distinction, if you ignore white space at all, you
    either ignore it everywhere, or give it a special role in your
    syntax. Ignoring is probably simpler.

    c) The programming language community took quite a while until it
    arrived at reserved words. E.g., while Algol 60 was specified in
    BNF, being modern in that regard, it does not have reserved words,
    just symbols that are distinct from all identifiers; i.e., you
    could have an identifier like IF in Algol 60, distinct from the
    symbol 'IF', and it's up to the implementation how to distinguish
    them. Anyway, without a) reserved words make no sense.

    d) Does the original FORTRAN support continuation cards?

    Yes. Fortran's continuation card is LL(1) and behaves the same as
    an ISA with start bits - you have to look ahead to find out
    that you didn't need to look ahead, and unget the card.

    Statement terminators like ; act like ISA stop bits and don't need lookahead.

    The classic ambiguous parsing example being

    DO I = 1,100
    DOI = 1.100

    depends on the presence or absence of a "naked comma" later in the
    statement to decide whether there is a keyword DO and a variable I,
    or a variable DOI. A "naked comma" is a comma not enclosed inside
    a quoted character or string, or inside a possibly nested parenthesis.

    My guess as to how this stuff was parsed:

    Deal with the first columns as appropriate. For the rest: Remove any
    white space. Use a backtracking parser that tries to match a rule at
    a time; for the DO statement, simply compare the start with "DO". If
    a rule does not match, look at the next rule.

    The result is messy, especially when you want to extend the language.

    A backtracking parser sounds like way overkill.

    I am under the impression that many Fortran 77 compilers used
    hand written lexer and a recursive decent parser.
    The parser passes some local context to the lexer to let it know,
    for example, which set of keywords are active at that point.
    Anyway, that is how I would do it.

    The first thing a Fortran compiler does is scan ahead loading card
    statements into a card buffer array from the (possibly nested INCLUDE)
    source files until it sees a non-continue card, which it ungets.

    Then it checks the format of the cards is ok and extracts things like
    a label from columns 2:5.

    Compacting out comments and white space is unnecessary -
    the hand written lexer knows to skip over them.
    There is a method called "Sale's Algorithm" which was published
    in CACM in the 1960's which describes how to lex up to Fortran 77.

    The parser knows which statement it is working on so can tell the
    lexer what options it should handle at the point.
    There could be a post pass over the AST to patch up ambiguous
    constructs that require semantic context to fix.

    After that it is pretty standard AST processing.
    The only semantic difference I can think of immediately is
    Fortran's parenthesis rule that forces an order of evaluation
    and would interact with optimizations like common subexpressions.

    People learned a lot of what not to do in language design
    from old FORTRAN.

    And probably also what to do. Backus was on the Algol-60 committee,
    and the B in BNF stands for his name. I would not be surprised if the structure of the parser in the FORTRAN compiler gave him ideas for
    BNF.

    - anton

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Fri Oct 16 17:10:24 2026
    From Newsgroup: comp.arch

    Thomas Koenig <[email protected]> writes:
    Anton Ertl <[email protected]> schrieb:
    and even later, e.g., Wirth does not
    make this distinction in his language definitions, using EBNF down
    to the lexical level (although he does define reserved words).

    Also in modern language definition. Separating lexing and parsing
    is a matter of convenience, especially since regular expressions
    have been introduced, and speed.

    Speed, yes, but it is also a matter of differences in handling of
    ambiguities. Scanning resolves ambiguities with longest input match,
    whereas parsing tends to avoid ambiguities, with only few exceptions
    (like the dangling else in some languages, but not, e.g., Algol 60,
    which is the first language that used BNF).

    As for convenience, for various compilers I read that either the
    scanner or the parser (or both) were hand-written (I don't remember
    which one was more frequent). It certainly does not seem much work to
    write a scanner for a typical programming language by hand (you
    recognize keywords by table lookup of things that will be classified
    as identifiers if they are not in the table). For the parser of a
    language that is not LL(1), it's more work, but writing it by hand
    allows you to do various things that, e.g., bison does not support
    (ANTLR has support for "syntactic predicates" and "semantic
    predicates", which probably is good enough for a lot of cases; it was
    written with C++ in mind).

    b) Without this distinction, if you ignore white space at all, you
    either ignore it everywhere, or give it a special role in your
    syntax. Ignoring is probably simpler.

    The added complication of separate handling of whitespace is minor.
    For the original FORTRAN, they skipped them because card punch operators >often made mistakes with spaces.

    That's a good reason, but it seems to have disappeared with the next
    generation of programming languages. Were programmers later punching
    their own cards, or were they writing programs in a way that made
    spaces obvious?

    c) The programming language community took quite a while until it
    arrived at reserved words.

    Reserved words make the compiler writer's job easier (somewhat,
    but not tremendously), but not having them is a solved problem.
    Users are left holding the bag if a new version of the language
    introduces new keywords.

    I don't think they worried about program maintainence across several
    Fortran versions when they worked on the original Fortran. It does
    not contain subroutines.

    d) Does the original FORTRAN support continuation cards?

    Yes, it would have extremely restricted otherwise.

    The original FORTRAN was extremely restricted.

    When I used Microsoft BASIC on the C64, where Commodore's line editor
    prevented lines longer than 80 characters, including line numbers, I
    did not find that particularly restrictive, especially given all the
    other limitations of that version of BASIC, such as no graphics or
    sound support apart from POKE and SYS. BASIC itself supported lines
    up to 252 bytes or so, and IIRC the keywords were already tokenized
    there, but you could not input such long lines on Commodore computers.

    What in FORTRAN makes single 66-char lines (plus line number)
    extremely restricted?

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From anton@[email protected] (Anton Ertl) to comp.arch on Fri Oct 16 17:56:05 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> writes:
    On 2026-Oct-15 02:17, Anton Ertl wrote:
    My guess as to how this stuff was parsed:

    Deal with the first columns as appropriate. For the rest: Remove any
    white space. Use a backtracking parser that tries to match a rule at
    a time; for the DO statement, simply compare the start with "DO". If
    a rule does not match, look at the next rule.

    The result is messy, especially when you want to extend the language.

    A backtracking parser sounds like way overkill.

    A backtracking parser is relatively easy and the memory needs for
    parsing just a single statement are small, too. For an example of how
    short a parser generator for a backtracking parser framework can be,
    look at the second example in
    <https://bernd-paysan.de/screenful.html>.

    I am under the impression that many Fortran 77 compilers used
    hand written lexer and a recursive decent parser.
    The parser passes some local context to the lexer to let it know,
    for example, which set of keywords are active at that point.
    Anyway, that is how I would do it.

    The reason why I think of a backtracking parser is that it seems the
    easiest way to be able to handle both

    DO101I=1.100

    and

    DO101I=1,100

    (examples given on <https://en.wikipedia.org/wiki/Fortran#Fixed_layout_and_punched_cards>)

    There is a method called "Sale's Algorithm" which was published
    in CACM in the 1960's which describes how to lex up to Fortran 77.

    Interesting, but did the FORTRAN team come up with that? According to <https://fortran-lang.discourse.group/t/has-anyone-played-with-sales-fortran-classification-algorithm-lately/7802>
    the algorithm was published in 1971.

    After that it is pretty standard AST processing.
    The only semantic difference I can think of immediately is
    Fortran's parenthesis rule that forces an order of evaluation
    and would interact with optimizations like common subexpressions.

    Searching around, I found little about "order of evaluation" when it
    comes to parenthesis in FORTRAN, although I remember hearing or
    reading about the significance of parentheses in FORTRAN several
    times. But does it have any relevance beyond determining that a+(b+c)
    is evalutated as

    t=b+c
    r=a+t

    rather than

    t=a+b
    r=t+c

    If not, that rule just disallows reassociation, but reassociation is
    not something I have seen in common subexpression elimination (or,
    more generally, partial redundancy elimination) algorithms anyway.

    - anton
    --
    'Anyone trying for "industrial quality" ISA should avoid undefined behavior.'
    Mitch Alsup, <[email protected]>
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Fri Oct 16 18:55:36 2026
    From Newsgroup: comp.arch

    [email protected] (Anton Ertl) posted:

    Thomas Koenig <[email protected]> writes:
    Anton Ertl <[email protected]> schrieb:
    ------------
    What in FORTRAN makes single 66-char lines (plus line number)
    extremely restricted?

    You just setup the key-punch so tab skips the first 6 characters
    <to the continuation indicator> and have the key-punch eject the
    card when you get to character 72.

    {{There was a machine where it would punch the sequence number
    in col[73-80] so you did not have to worry about that until the
    program was in good shape.}}

    - anton
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Fri Oct 16 20:35:33 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> schrieb:
    On 2026-Oct-15 02:17, Anton Ertl wrote:
    EricP <[email protected]> writes:
    Old FORTRAN is difficult to parse because
    a) it did not separate the language into lexical and syntax levels
    b) it was white space oblivious except in quoted strings
    c) there were no reserved words - keywords were statement
    position and sometimes semantic context dependent.
    d) statements were based on punch cards BUT because
    continuation cards used the absence of a C in column 6
    to indicate the start of a new statement, parsers have to
    always look ahead to see in the next card has a C and
    then "unget" the card if not.

    Given that the original FORTRAN worked on an IBM 704, I think that the
    designers made it as easy (and with as little memory) to parse as they
    could, given the state of the art at the time. Concerning the points:

    a) I guess that the division into lexical and syntax levels had not
    been developed at the time; and even later, e.g., Wirth does not
    make this distinction in his language definitions, using EBNF down
    to the lexical level (although he does define reserved words).

    b) Without this distinction, if you ignore white space at all, you
    either ignore it everywhere, or give it a special role in your
    syntax. Ignoring is probably simpler.

    c) The programming language community took quite a while until it
    arrived at reserved words. E.g., while Algol 60 was specified in
    BNF, being modern in that regard, it does not have reserved words,
    just symbols that are distinct from all identifiers; i.e., you
    could have an identifier like IF in Algol 60, distinct from the
    symbol 'IF', and it's up to the implementation how to distinguish
    them. Anyway, without a) reserved words make no sense.

    d) Does the original FORTRAN support continuation cards?

    Yes. Fortran's continuation card is LL(1) and behaves the same as
    an ISA with start bits - you have to look ahead to find out
    that you didn't need to look ahead, and unget the card.

    Statement terminators like ; act like ISA stop bits and don't need lookahead.

    The classic ambiguous parsing example being

    DO I = 1,100
    DOI = 1.100

    depends on the presence or absence of a "naked comma" later in the
    statement to decide whether there is a keyword DO and a variable I,
    or a variable DOI. A "naked comma" is a comma not enclosed inside
    a quoted character or string, or inside a possibly nested parenthesis.

    My guess as to how this stuff was parsed:

    Deal with the first columns as appropriate. For the rest: Remove any
    white space. Use a backtracking parser that tries to match a rule at
    a time; for the DO statement, simply compare the start with "DO". If
    a rule does not match, look at the next rule.

    The result is messy, especially when you want to extend the language.

    A backtracking parser sounds like way overkill.

    I haven't worked on a Fortran 77 compiler, but gfortran, a Fortran
    20xx compiler, does exactly that.

    It tries matching constructs until they are accepted. If somethig
    does not match, an error message is queued, and the next
    match is tried. If everything failes, then the last error message
    is output, and the next statement is looked at.

    But that is only a part. After the AST has been generated, a
    lot of checks are run, and details are filled in (the so-called
    resolution stage). Then, some manipulation of the AST and finally
    transfer to gcc's GENERIC, a C-like intermediate language. This is
    where things like scalarization are handled. GCC later lowers the
    GENERIC to GIMPLE, which is an SSA variant.

    I am under the impression that many Fortran 77 compilers used
    hand written lexer and a recursive decent parser.
    The parser passes some local context to the lexer to let it know,
    for example, which set of keywords are active at that point.
    Anyway, that is how I would do it.

    I think John L. wrote a FORTRAN compiler once.


    The first thing a Fortran compiler does is scan ahead loading card
    statements into a card buffer array from the (possibly nested INCLUDE)
    source files until it sees a non-continue card, which it ungets.

    Then it checks the format of the cards is ok and extracts things like
    a label from columns 2:5.

    Compacting out comments and white space is unnecessary -
    the hand written lexer knows to skip over them.
    There is a method called "Sale's Algorithm" which was published
    in CACM in the 1960's which describes how to lex up to Fortran 77.

    The parser knows which statement it is working on so can tell the
    lexer what options it should handle at the point.
    There could be a post pass over the AST to patch up ambiguous
    constructs that require semantic context to fix.

    After that it is pretty standard AST processing.
    The only semantic difference I can think of immediately is
    Fortran's parenthesis rule that forces an order of evaluation
    and would interact with optimizations like common subexpressions.

    The gfortran front end has INTRINSIC_PARENTHESES , which it then
    translates into PAREN_EXPR in the intermediate language.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From George Neuner@[email protected] to comp.arch on Fri Oct 16 18:57:53 2026
    From Newsgroup: comp.arch

    On Fri, 16 Oct 2026 17:10:24 GMT, [email protected]
    (Anton Ertl) wrote:


    As for convenience, for various compilers I read that either the
    scanner or the parser (or both) were hand-written (I don't remember
    which one was more frequent).

    Scanners. Parser are a lot of work.

    It certainly does not seem much work to
    write a scanner for a typical programming language by hand (you
    recognize keywords by table lookup of things that will be classified
    as identifiers if they are not in the table). For the parser of a
    language that is not LL(1), it's more work, but writing it by hand
    allows you to do various things that, e.g., bison does not support

    Bison is limited to single (token) lookahead, but since it can
    backtrack, and also create LR(1), SLR(1), and GLR(1) parsers as well
    as the (yacc-alike) LALR(1) ... well, I've never actually encountered
    a situation that Bison couldn't be contorted into handling.
    [Of course the contortions required can make the grammar feel
    unnatural.]

    And Bison doesn't generate lexers (scanners) so they have to be
    provided separately.


    (ANTLR has support for "syntactic predicates" and "semantic
    predicates", which probably is good enough for a lot of cases; it was
    written with C++ in mind).

    Again, I've never encountered any situation ANTLR can't handle. ANTLR
    can generate both parsers and lexers (scanners), and because they are
    recursive descent they tend to be easier to debug (when necessary)
    than are Bison/Flex state machines.


    Early versions of ANTLR were LL(k): you had to specify the maximum
    lookahead required by any rule in your grammar else the parser would
    not work correctly [if it could be generated at all].

    Starting from version 3, ANTLR became LL(*) which has per-rule
    lookahead figured out by the tool. LL(*) is easier to use, but its
    grammars are incompatible with the earlier LL(k) versions. The LL(*)
    versions support parser AND lexer in a single grammar file, whereas
    the early versions required them to be separate.

    Also starting from version 3, ANTLR is written in Java. It can
    generate code for different target languages (and there are runtime
    libraries available for the various targets), but the tool itself
    requires a JVM to execute.


    Current version is 4.13.2.

    I keep 2.7.7 around for Windows and Linux specifically because it was
    NOT written in Java. Being LL(k), it is a bit harder to use, and it
    targets only C or C++, but AFAIK it is the last version of ANTLR to be available as a simple executable rather than a jar.


    [I am aware that Java programs (at least theoretically) can be
    compiled directly to executable. B ut I don't know offhand what Java
    compilers can do that, and AFAICT nobody has done it for any modern
    versions of ANTLR.

    If anybody knows differently, I'm happy to hear that I'm mistaken.]
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Sat Oct 17 02:03:08 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote: >[email protected] (Dan Cross) posted:
    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:
    [email protected] (Dan Cross) posted:
    First off: thank you dan for a well done explanation.

    In article <[email protected]>,
    MitchAlsup <[email protected]d> wrote:

    Terje Mathisen <[email protected]> posted:

    Dan Cross wrote:
    ---------

    My old asm/C brain _knows_ that a slice of something with a known
    address and total length is effectively the same thing as an unmutable
    array.

    Can I get a definition of what mutable in Rust means ??

    --------snip of what I understand clearly------------

    So this will fail to compile:

    ? let mut foo = something();
    ? let foomut = &mut foo; // Ok: only mutable
    ? let barmut = &mut foo // Fail: a mut ref to foo exists

    Is this supposed to work across threads ?? so, only 1 thread has write
    access at any given point in time.

    Yes, that's right.

    If so, how does one thread hand-off mutability to another ??

    Typically, you use a mutual exclusion primitive of one kind or
    another to serialize access to mutable state. For example, the
    standard library includes the usual complement of Mutex,
    reader/writer locks, condition variables, barriers, reference
    counters, thread-safe channels, and so on. For bare-metal
    programs we have to implement these ourselves, but there are
    some third-party implementations that can help (for example, the
    `spin` crate implements spinlocks, providing, an interface that
    looks close to `std::sync::Mutex`).

    Something I should have mentioned earlier is that Rust object
    has a single, well-defined owner, and that references to that
    object are borrowed _from the owner_.

    I was expecting one "thread" to hold (own?) the object in question
    and was unsure how mutablility was handed out across other threads.

    I think it may be easier to think of software objects as the
    owners, and threads as scopes. You can certainly have data that
    "lives" within the context of a single thread and shared with
    other threads, but the fundamental mechanism is the same: you
    share some some kind of immutable reference to the (software)
    object that actually owns the data and ask it to borrow the
    actual data.

    I had imagined that then current mut owner had its 'pointer' go
    out of block and that somehow his no longer having a mutable ref
    to the object somehow got propagated automagically by his leaving
    of the block {which seems untenable}.

    falling back on standard ways of passing this stuff around adds hope.
    {{less Ada rendezvous like}}

    Yeah. The thing that makes Rust interesting with respect to
    these primitives is how you build them, using interior
    mutability and the sharing rules of the language to make
    guarantees about the correctness of the locking protocol.

    In particular, if you take a resource you want to share, and you
    hide it "inside" of some other object that logically "owns" the
    resource, and then make it so that the only way you can access
    it is via some interface provided by the owning object, then you
    can enforce that nothing goes behind the back of the locking
    protocol and accesses the shared resource independently.

    Put another way, if the only way to get access to the resource
    is through a proxy object returned by the owner's `.lock()`
    method, and the owner makes sure that only one of those exists
    at a time, then you've defacto serialized access to the
    resource. Moreover, this is now enforced statically, by the
    _compiler_, at build time. Your runtime cost on top of the
    usual way is zero.

    So, using `Mutex` as an example, we make the `Mutex` itself the
    owner of the object: the `.lock()` method lets threads borrow
    mutable references to the object, and the serialization inherent
    in the Mutex type and the locking protocol enforces the
    "mut ^ immut" semantics across threads.

    Since I am a CPU guy, I would like to see the asm from any modern >architecture perform this handoff ! <please> ?

    I mentioned this earlier but it bears repeating: all of this is
    logical, at the level of the language.

    Within the context of the Rust standard library, when targeting
    an existing platform, the library primitives are usually
    implemented in terms of familiar system interfaces: futexs on
    Linux; pthreads and `pthread_mutex_t` where that's applicable;
    whatever Windows does on Windows, and so on.

    When you're writing bare metal code, you can roll your own, but
    lots of folks just use the `spin` crate.

    Once it's compiled, big chunks of it inline and it all lowers
    down to more or less standard instruction sequences. The Rust
    compiler generally does pretty well about optimizing the
    abstractions away, meaning you're mostly left with "normal"
    CAS instructions, barriers, and loads/stores.

    See the end of the deep-dive into a spin-Mutex implementation,
    with notes on how that maps to a real ISA.

    Similarly, a `RwLock` can hand out multiple immutable refs to
    the object, but keeps track of them, and only lets one borrower
    access a `mut ref` at a time, and only when there are no
    outstanding immutable refs to it.

    Sounds like reference counting...

    Kinda. The property of a read/write lock is that multiple
    readers can access the shared thing at one time, but only one
    writer (sounds familiar with respect to the borrow checker rules
    with respect to mutability, right?).

    Building robust, scalable, production-quality RW locks is
    surprisingly hard.

    Of course, you have to keep track of the number of readers
    actively holding the lock at any given time, but if we naively
    admit more readers any time that number is >0, we run the risk
    of starving writers; a writer may pend waiting on the lock while
    readers just keep appearing. Similarly, if you've got a high
    priority writer blocked behind a bunch of readers, you're open
    to priority inversion; what if one of the readers blocks? The
    point is, we also need to keep track of pending writers and the
    priorities of whatever is contending for the lock.

    In fact, it's slightly more complex in that `mutex.lock()`
    actually returns a proxy object that holds onto the reference;
    its return type is actually, `LockResult<LockGuard<'_, T>>`,
    where `'_` is an anonymous lifetime, and `T` is the type of the
    thing protected by the `Mutex`. `LockResult` lets someone
    detect a lock that has been poisoned; the `LockGuard` is the
    thing that holds the ref. When it goes otu of scope, it
    automatically unlocks the mutex, and it implements a trait
    called `Deref` to forward uses on to the ref to the locked
    object.

    Another level of indirection to handle a more complicated issue...

    Well, it's more about correctness than addressing a more complex
    issue, per se. Forgive me if I've already posted this link, but
    this is a good explanation of what's going on: https://cliffle.com/blog/rust-mutexes/

    This all intersects with the `Send`/`Sync` stuff; `Mutex` is
    `Sync`, and `Mutex` objects themselves are treated as immutable.
    So to share access to a `Foo` between threads, I use something
    like a `Mutex<Foo>`. This is an example of "Interior
    Mutability", where something immutable (the `Mutex` object
    itself) can hold onto something that is mutable.

    This raises a question: if all this machinery is required for
    safe access to shared data across threads anyway, then why all
    the language-level rules? The answer is hinted at by the mutex
    object itself is immutable, we can hand out references to it
    easily. Since it owns the (mutable) object the lock protects,
    it can decide how to hand out references to the object.

    We have the same problems with coherent caches and there are 3
    normal solutions {MESI, MOESI, and Directories}

    Right. Interestingly, little of this solves those problems, but
    the way we implement e.g., locking can influence how the
    software interacts with those things. See below.

    [snip]

    Acknowleding the mention you made about about how this all maps
    to the ISA level, let's take a look at an actual program. Here
    is an implementation of a simple spinlock:

    ```
    //! Spin locks in Rust.

    #![no_std]

    use core::cell::UnsafeCell;
    use core::marker::{Send, Sync};
    use core::ops::{Deref, DerefMut};
    use core::sync::atomic::{AtomicBool, Ordering};

    pub struct Lock<T: ?Sized> {
    held: AtomicBool,
    data: UnsafeCell<T>,
    }

    impl<T> Lock<T> {
    /// Creates a new `Lock` that owns an object of type T.
    pub const fn new(value: T) -> Lock<T> {
    Lock {
    held: AtomicBool::new(false),
    data: UnsafeCell::new(value),
    }
    }

    /// Consumes the lock and returns the contained object.
    pub fn into_inner(self) -> T {
    self.data.into_inner()
    }
    }

    impl<T: ?Sized> Lock<T> {
    /// Acquires the lock. Returns a LockGuard that will
    /// release the lock when dropped.
    pub fn lock(&self) -> LockGuard<'_, T> {
    while self.held.load(Ordering::Relaxed) || self.held.swap(true, Ordering::AcqRel) {
    core::hint::spin_loop();
    }
    LockGuard {
    data: unsafe { &mut *self.data.get() },
    lock: self,
    }
    }

    /// Releases the lock, assuming it is held. Private, so
    /// that the only way to unlock is through the `drop` method
    /// of a `LockGuard<T>` (that itself can only exist as the
    /// result of a successful call to `lock`), or by calling
    /// the unsafe `force_unlock` method.
    fn unlock(&self) {
    self.held.store(false, Ordering::Release);
    }

    /// Force-releases the lock.
    ///
    /// Suppose that a lock guard holding the lock is leaked
    /// (e.g., via `std::mem::forget()`); its `drop` method
    /// can never be called to unlock. This function provides
    /// an unsafe escape hatch to release the lock.
    ///
    /// # Safety
    ///
    /// Callers must ensure that the lock is currently locked,
    /// that the underlying object is in a state that it is
    /// safe to unlock, and that nothing else will try to unlock
    /// the current lock-holder later; leaked LockGuards must
    /// be forgotten: their `drop` methods must never be
    /// invoked, and their borrows to the lock-owned data must
    /// be gone.
    pub unsafe fn force_unlock(&self) {
    self.unlock();
    }
    }

    unsafe impl<T: ?Sized + Send> Send for Lock<T> {}
    unsafe impl<T: ?Sized + Send> Sync for Lock<T> {}

    pub struct LockGuard<'a, T: ?Sized> {
    lock: &'a Lock<T>,
    data: &'a mut T,
    }

    impl<T: ?Sized> Deref for LockGuard<'_, T> {
    type Target = T;
    fn deref(&self) -> &T {
    self.data
    }
    }

    impl<T: ?Sized> DerefMut for LockGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
    self.data
    }
    }

    impl<T: ?Sized> Drop for LockGuard<'_, T> {
    fn drop(&mut self) {
    self.lock.unlock();
    }
    }
    ```

    What's going on here? A couple of notes. First, the lock
    itself is pretty simple: it uses a single atomic boolean for
    the lock bit, and holds the data protected by the lock in an
    `UnsafeCell` (which it owns, meaning that it owns the data).

    The first Rust-related thing that may be unfamiliar is that the
    `Lock` type is "generic" over some type `T`; we say little about
    what `T` is, but we use the `?Sized` construct to _remove_ the
    constraint that the size of `T` must be a compile-time constant.

    Rust has types where this is not the case, like slices (`[T]`),
    which it describes as "dynamic views" into a "contiguous
    sequence" of objects of some type; since the view is dynamic,
    the number is not knowable at compile time (it is a runtime
    property). But we want to be able to lock slices, so we use
    `?Sized`. Because the size of `T` cannot be known at compile
    time, the `UnsafeCell` that owns it comes last in the structure
    definition.

    Note the two `impl`s: these are where we define methods on
    `Lock<T>`. The `.lock()` and `.unlock()` (and `force_unlock()`)
    methods don't care what size the type is; they only operate on
    the lock bit or references to `T` (via the interface of the
    unsafe cell that owns the `T`), so we allow `?Sized` for `T`.

    On the other hand, the `.new()` and `.into_inner()` methods _do_
    care about the size of T: `.new` is a constructor that takes an
    instance of T _by value_, which by default _moves_ the T object;
    it moves it into the `UnsafeCell` (again, by value, so now the
    `UnsafeCell` owns the T), creates a new `Lock<T>` that takes
    ownership of the `UnsafeCell` (...and a new `AtomicBool`
    initialized to `false` for the lock bit: false=="unlocked").

    `.into_inner()` is a bit more interesting: this takes the `Lock`
    by value, which moves it, thus consuming it; it then extracts
    the T object from the `UnsafeCell` (via the cell's
    `.into_inner()` method) and returns the value; we can do this
    when we're done with the lock and want to discard it and
    retrieve the value it had protected.

    The `unsafe impl Send...` and `unsafe impl Sync...` tell the
    language that we are implementing the `Send` and `Sync` traits
    I mentioned the other day, for `Lock<T>`. These are `unsafe`
    because we are taking the responsibility for ensuring that the
    rules around send and sync are upheld, and not leaving that to
    the compiler.

    There's also `.force_unlock`, but let me talk about `LockGuard`
    first.

    Note that `.lock()` doesn't return e.g., a reference to the
    locked object in the `UnsafeCell` owned by the lock. Instead,
    it returns an onject of type `LockGuard<T>`, that has two data
    members, both of which are references: one (a mutable borrow,
    hence unique) to the object protected by the lock, and the
    other, an immutable reference to the `Lock` that owns the
    object.

    A `LockGuard<'a, T>` is a proxy object through which we access
    the T object protected by the lock. We've talked about the
    generic "type" parameter, `T`, already, but what is this `'a`
    thing? That is a _lifetime_ parameter: since `LockGuard`'s
    data includes _references_, we need to tell the compiler how
    long those can live, and what relationship they share (if any).
    Here, we say, "these references share some lifetime, that we
    call `'a`."

    We then provide `impl`'s of three _traits_: a trait is basically
    an interface, and describes methods and "associated types". We
    "implement" it for types in order to expose the behavior the
    trait describes, for those types. In this case, we impl
    `Deref`, `DerefMut`, and `Drop`.

    The first two allow us to use the `*` syntax to indirect through
    the `LockGuard<T>` and access the object; hence, when we write,
    `let guard = whatever.lock(); dosomething(*guard);`, we are
    invoking the `.deref()` method we impl'ed from the `Deref` trait
    on guard, and taking _its_ return value and passing that to
    `dosomething`: here, that's an `&T`: an immutable borrow of the
    T. `DerefMut` is similar, but used for mutable borrows; the
    caveat with `DerefMut` is that the guard object must, itself, be
    `mut` to invote `.deref_mut()` on it; the compiler will complain
    if not.

    The last trait, `Drop`, exposes the `.drop()` method that gets
    automatically invoked when the `LockGuard<T>` object is
    destroyed (either by going out of scope, or by passing it to the
    `drop()` _function_, which takes its argument by value). In the
    case of `LockGuard`, the `.drop` calls `self.lock.unlock();`,
    unlocking the lock we've been holding, and giving another thread
    a chance to grab the lock: note that `Lock<T>`'s `.unlock()`
    method is _not_ marked `pub`: it is a private interface.
    `LockGuard` can call it because it's defined in the same
    _module_ as `Lock`, however; hence, the only way to invoke
    `.unlock()` on the spinlock is via dropping the `LockGuard`.

    I said I'd mention `.force_unlock()`, and now's the time:
    everything I just said is a shameless lie. Well, not exactly,
    but it is possible to "leak" a `LockGuard` so that it's
    `.drop()` method is never called; the first is by passing it to, `std::mem::forget`:
    https://doc.rust-lang.org/std/mem/fn.forget.html

    Using `std::mem::forget`, it's possible to "leak" a locked
    object. So, the `.force_unlock` method is provided as an
    escape hatch for situations where you know that the guard that
    proxied a lock is gone and not coming back, but you still want
    to be able unlock the lock. Thus, it is both _public_ and also
    marked `unsafe`: this means that anyone who can get a reference
    to the `lock` can call it, but they _must_ do so from inside an
    `unsafe` block: the programmer takes on the responsibility of
    ensuring that the invariants the lock requires are upheld if
    they call it, and the compiler washes its hands of the
    consequences.

    Let's look at man example program that uses this:

    ```
    fn main() {
    let lock = spinlock::Lock::new(1);
    let mut v = lock.lock();
    *v -= 1;
    std::mem::forget(v);
    unsafe {
    lock.force_unlock();
    }
    let val = lock.into_inner();
    println!("val = {val}");
    let v = threaded_add(16, 128);
    println!("v = {v}");
    }

    #[inline(never)]
    fn threaded_add(nthread: u16, niter: u16) -> u32 {
    let lock = spinlock::Lock::new(0);
    std::thread::scope(|scope| {
    for _ in 0..nthread {
    scope.spawn(|| count(&lock, niter));
    }
    });
    lock.into_inner()
    }

    #[inline(never)]
    fn count(lock: &spinlock::Lock<u32>, niter: u16) {
    for _k in 0..niter {
    let mut v = lock.lock();
    *v += 1;
    }
    }

    #[cfg(test)]
    #[test]
    fn threaded_add_works() {
    assert_eq!(threaded_add(16, 1024), 16384);
    }
    ```

    Ok, so there are a couple of things going on here; first, I
    create a new spinlock around an unsigned 32-bit integer
    containing the value `1`, then I lock it and assign the result
    to a mutable LockGuard, `v`; I use that to subtract 1 from the
    value of the locked object, and then I `std::mem::forget` the
    guard. I unlock the object via calling `.force_unlock()` in an
    `unsafe { ... }` block, and then I consume the lock and get
    the integer's counter from `.into_inner()` and print it out.

    Next, I invoke this `threaded_add`, which creates a locked
    counter, creates a few threads, and in each of those threads,
    runs a loop that repeatedly locks the counter, adds one to it,
    and drops the lock guard (implicitly, by letting it go out of
    scope at the at the end of the loop body). Note that I broke
    the counting loop out into a function annotated with the,
    `[inline(never)]` _attribute_: this is just so I can find it
    easily in assembly language output when I dump out the
    instructions with `objdump` below.

    All of the threads I create are "scoped", which means that they
    are definitely reaped when the scope ends, and thus can't hold
    references to the lock after they complete; this way, I can
    safely consume the lock in with `.into_inner()` and return the
    value of the counter from `threaded_add`.

    Back in `main`, I capture the value returned from `threaded_add`
    and _bind_ it to a variable I'm calling `v`; you may notice that
    is the same name I had used for the `LockGuard` I created
    earlier this was deliberate to demonstrate that `let` creates
    lexical bindings, which can be replaced. Ordinarily, one would
    not usually do this as a matter of good programming practice,
    but it _can_ be useful (for example, when casting a value to
    some compatible but different type).

    There's also a `#[test]` function that runs automatically when
    I build and run in test mode. It just invokes `threaded_add`
    with a fixed number of threads and iterations for each, and
    asserts the final output.

    When run, this program prints:

    val = 0
    v = 2048

    Which is what I expected.

    So what does this look like at the machine code level? Let's
    take a look at an excerpt from an optimized build, for x86_64.
    This is the complete compiled output for `count`, shown above:

    ```
    0000000000019aa0 <spinlock::count::hd8ea4fab4f0cabbe>:
    19aa0: 66 85 f6 test %si,%si
    19aa3: 74 2a je 19acf <spinlock::count::hd8ea4fab4f0cabbe+0x2f>
    19aa5: 55 push %rbp
    19aa6: 48 89 e5 mov %rsp,%rbp
    19aa9: 31 c0 xor %eax,%eax
    19aab: eb 05 jmp 19ab2 <spinlock::count::hd8ea4fab4f0cabbe+0x12>
    19aad: cc int3
    19aae: cc int3
    19aaf: cc int3
    19ab0: f3 90 pause
    19ab2: 0f b6 0f movzbl (%rdi),%ecx
    19ab5: 84 c9 test %cl,%cl
    19ab7: 75 f7 jne 19ab0 <spinlock::count::hd8ea4fab4f0cabbe+0x10>
    19ab9: b1 01 mov $0x1,%cl
    19abb: 86 0f xchg %cl,(%rdi)
    19abd: 84 c9 test %cl,%cl
    19abf: 75 ef jne 19ab0 <spinlock::count::hd8ea4fab4f0cabbe+0x10>
    19ac1: ff 47 04 incl 0x4(%rdi)
    19ac4: ff c0 inc %eax
    19ac6: c6 07 00 movb $0x0,(%rdi)
    19ac9: 66 39 f0 cmp %si,%ax
    19acc: 75 e4 jne 19ab2 <spinlock::count::hd8ea4fab4f0cabbe+0x12>
    19ace: 5d pop %rbp
    19acf: c3 retq
    ```

    Here, `%rdi` is the reference to the `Lock<T>` and `%rsi` holds
    `niter`. The first couple of instructions short-circuit an
    argument of zero iterations, then do the usual function prologue
    stuff and jump into the spin loop, (aligned on a 16 byte
    boundary); there's the relaxed load at 0x19ab2, a test to see
    if it's zero, and if not, jump back to the `PAUSE` (the `std::hint::spin_loop()` call in the code). Otherwise, we
    `xchg` the contents of the lock bit with a 1 (the
    `self.lock.swap(true, Ordering::AcqRel)` in the code) and test
    again; if we didn't win, it's a jump back to the pause and try
    again. Otherwise, we increment the 32-bit word at offset 0x4
    from the beginning of the lock (the shared counter), increment
    the iteration count (in `%ax`), compare against the limit, and
    either jump back to the locking loop, or do the usual epilogue
    and return.

    Note that all of the abstraction: the `.lock`, `LockGuard`, and
    so on, is inlined and removed by the compiler. We're left with
    pretty reasonable code.

    The only other interesting bits of code are in `threaded_add`,
    and there's not that much that's relevant there: the `Lock<T>`
    holding a `u32` initialized to zero and the lock at is created
    on the stack alongside some space for a few other temporary
    registers, and the object itself is "created" with two
    instructions:

    ```
    196fd: 48 83 ec 78 sub $0x78,%rsp
    ...
    19707: c6 45 b0 00 movb $0x0,0xffffffffffffffb0(%rbp)
    1970b: c7 45 b4 00 00 00 00 movl $0x0,0xffffffffffffffb4(%rbp) ```

    Plumbing that into the threads that get created is a little
    tedious, but not terribly interesting; it's just a couple of
    `mov`s and `jmp`/`callq`s. The ending `.into_inner()` call is
    just:

    ```
    19892: 8b 45 b4 mov 0xffffffffffffffb4(%rbp),%eax ```

    Right before cleaning up the stack, popping callee saved
    registers, and the `retq`.

    The full code is at https://github.com/dancrossnyc/spinlock.

    Hope this helps?

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Sat Oct 17 03:49:53 2026
    From Newsgroup: comp.arch

    According to EricP <[email protected]>:
    I am under the impression that many Fortran 77 compilers used
    hand written lexer and a recursive decent parser.
    The parser passes some local context to the lexer to let it know,
    for example, which set of keywords are active at that point.
    Anyway, that is how I would do it.

    I wrote a production FORTRAN 77 compiler. It had a hand written lexer
    and a yacc parser.

    It made a prepass over each statement to combine continuation cards and to tell whether it's an assignment statement or statement function (they're syntactically the same) or something else. Once it knows that, the lexer is straightforward using feedback from the parser to know what tokens are allowed at each point. As I recall there were about a dozen states, most of which were simple, e.g., after a DO keyword the next thing has to be a digit string
    which is a statement number.

    The prepass was needed to disambiguate DO10I=1.10 and also to handle stuff like this:

    10 FORMAT(I10,A5) // a format statement

    20 FORMAT(I10,A5) = 42 // an assignment statement
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Sat Oct 17 03:56:25 2026
    From Newsgroup: comp.arch

    According to Anton Ertl <[email protected]>:
    The reason why I think of a backtracking parser is that it seems the
    easiest way to be able to handle both

    DO101I=1.100

    and

    DO101I=1,100

    At least when I did it, backtracking parsers were big and ugly
    compared to little yacc parsers. I used Sale's algorithm
    although I didn't know that's what it was called when I reinvented it.

    There is a method called "Sale's Algorithm" which was published
    in CACM in the 1960's which describes how to lex up to Fortran 77.

    Interesting, but did the FORTRAN team come up with that?

    I think we can assume they did since it's by far the easiest way to
    parse card image FORTRAN. If anyone really cares I believe the
    assembler source of some early compilers is available online.

    R's,
    John
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Sat Oct 17 04:01:17 2026
    From Newsgroup: comp.arch

    According to Anton Ertl <[email protected]>:
    As for convenience, for various compilers I read that either the
    scanner or the parser (or both) were hand-written (I don't remember
    which one was more frequent). ...

    Lexers are usually much slower than parsers because they're O(N) in
    the number of characters in the source code while parsers are O(N) in
    the number of tokens the lexer returned. So they're worth some effort
    to speed up.

    There are also tools like re2c which turns regular expressions into
    C code which tends to run a lot faster than the table lookup loop
    in flex parsers.
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Sat Oct 17 08:51:00 2026
    From Newsgroup: comp.arch

    On Wed, 14 Oct 2026 15:45:41 -0400, EricP
    <[email protected]> wrote:

    People learned a lot of what not to do in language design
    from old FORTRAN.

    Oh, yes. My language, EXALT, which I previously called FALCON, before
    learning the name was taken (several times), did learn from the

    DO 7 I = 1.100

    problem.

    It still takes stuff from FORTRAN.

    In it, a line with _any_ letter in Column 1 is a comment. So the
    comments can start two characters earlier!

    But assignment statements have to start with the keyword LET... or its abbreviation, which is just ".".

    Also FORTRAN-like: no semicolons needed at the end of the line.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Sat Oct 17 10:06:21 2026
    From Newsgroup: comp.arch

    On 2026-Oct-16 13:56, Anton Ertl wrote:
    EricP <[email protected]> writes:
    On 2026-Oct-15 02:17, Anton Ertl wrote:
    My guess as to how this stuff was parsed:

    Deal with the first columns as appropriate. For the rest: Remove any
    white space. Use a backtracking parser that tries to match a rule at
    a time; for the DO statement, simply compare the start with "DO". If
    a rule does not match, look at the next rule.

    The result is messy, especially when you want to extend the language.

    A backtracking parser sounds like way overkill.

    A backtracking parser is relatively easy and the memory needs for
    parsing just a single statement are small, too. For an example of how
    short a parser generator for a backtracking parser framework can be,
    look at the second example in
    <https://bernd-paysan.de/screenful.html>.

    I am under the impression that many Fortran 77 compilers used
    hand written lexer and a recursive decent parser.
    The parser passes some local context to the lexer to let it know,
    for example, which set of keywords are active at that point.
    Anyway, that is how I would do it.

    The reason why I think of a backtracking parser is that it seems the
    easiest way to be able to handle both

    DO101I=1.100

    and

    DO101I=1,100

    (examples given on <https://en.wikipedia.org/wiki/Fortran#Fixed_layout_and_punched_cards>)

    F77 already had structured IF THEN ELSE IF THEN ELSE END IF.
    DEC F77 enhanced DO drops the loop end statement number and
    used END DO instead, and added DO WHILE END DO.
    (This one change alone de-rats-nests the source tremendously by
    eliminating all the loop label numbers so that label numbers
    were only used for unstructured GOTO and FORMAT statements
    which are infrequent.)

    DEC F77 had a number of enhancements which made the language
    more friendly, like tab in column 1 skips to the start of a
    statement or ! anywhere is a comment to end of line.
    These were adopted by many other F77 compiler vendors
    presumably because they wanted to get their customers.
    Later many became ANSI standard.

    There is a method called "Sale's Algorithm" which was published
    in CACM in the 1960's which describes how to lex up to Fortran 77.

    Interesting, but did the FORTRAN team come up with that? According to <https://fortran-lang.discourse.group/t/has-anyone-played-with-sales-fortran-classification-algorithm-lately/7802>
    the algorithm was published in 1971.

    After that it is pretty standard AST processing.
    The only semantic difference I can think of immediately is
    Fortran's parenthesis rule that forces an order of evaluation
    and would interact with optimizations like common subexpressions.

    Searching around, I found little about "order of evaluation" when it
    comes to parenthesis in FORTRAN, although I remember hearing or
    reading about the significance of parentheses in FORTRAN several
    times. But does it have any relevance beyond determining that a+(b+c)
    is evalutated as

    t=b+c
    r=a+t

    rather than

    t=a+b
    r=t+c

    If not, that rule just disallows reassociation, but reassociation is
    not something I have seen in common subexpression elimination (or,
    more generally, partial redundancy elimination) algorithms anyway.

    Parenthesis can be used to force an order of execution.
    The parenthesis ordering rule is also data type dependent.
    It applies to integer, logical (also integer) and real but not strings.

    #1 a + b + c Unordered
    #2 (a+b) + c a+b before +c
    #3 a + (b+c) b+c before +a

    This would interact with common subexpression elimination.
    Above would not be able to optimize all 3 expressions to 1.
    It can replace #1 with #2 or #3, but not both.

    I'm not sure but I think parenthesis also could block optimizations
    like a*b+a*c => a*(b+c) but (a*b)+(a*c) might not be optimized.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Sat Oct 17 10:06:56 2026
    From Newsgroup: comp.arch

    On 2026-Oct-16 13:56, Anton Ertl wrote:
    EricP <[email protected]> writes:
    There is a method called "Sale's Algorithm" which was published
    in CACM in the 1960's which describes how to lex up to Fortran 77.

    Interesting, but did the FORTRAN team come up with that? According to <https://fortran-lang.discourse.group/t/has-anyone-played-with-sales-fortran-classification-algorithm-lately/7802>
    the algorithm was published in 1971.

    I have a copy of a summary web page saved which seems to have disappeared
    but this is what it said:

    by Keith Seymour, 1998
    "Sale's algorithm

    Sale's algorithm (Giles, pers. communication) was published in CACM in the sixties - with an update for Fortran 77 republished in the eighties. Sale's algorithm requires a prepass of each statement to determine whether or not the statement begins with a keyword. The prepass is simple and can be done while scanning to remove comments and white space, and catenating continuation statements together, prior to normal lexical processing.

    During the prepass, any characters in a Hollerith, character string (that is, between quotes or apostrophes), and any characters between matching parenthesis are ignored. Those characters are irrelevant to the purpose of the prepass. Of the remaining characters, the scanner must keep track of whether there are any equal signs (=) and whether there are any commas (,). Given the results of the prepass, the lexer should work according to the following rules, illustrated with examples.

    If there was neither a comma nor an equal sign, the statement must begin with a keyword:

    REAL X
    No comma or equal sign, so a keyword must be first and lexer should find the REAL keyword;

    FORMAT(I5,F10.3)
    No comma or equal sign (none outside parenthesis), lexer should find keyword FORMAT.

    If there was an equal sign, but not a comma, the statements must not begin with a keyword:

    REAL X = 5
    No comma, but an equal sign, so no keyword is allowed, lexer should find identifier REALX;

    FORMAT(I5) = 7
    An equal sign, lexer finds identifier FORMAT;

    DO 10 I = 1.10
    Famous, no comma but equal sign is present, identifier DO10I found.

    If there was a comma (equal sign or not), the statement must begin with a keyword:

    DO 10 I = 1, 10
    Both comma and equal sign, keyword DO found.

    However, there are still exceptions, This is resolved by collecting more information during the Sale's prepass. If the statement contains parenthetical lists or expressions, look at the first non-blank character after the close of the first parenthetical list/expression and record whether it's a letter. Now add two more rules to the lexer:

    If there was a letter following a parenthetical, then the statement must begin with the keyword IF (very specific rule).

    IF(LOGFLG) X = 5.0
    Has an equal sign, but begins with keyword!

    If there is such a letter, but no equal sign, the statement must end with the keyword THEN (another very specific rule).

    IF(LOGFLG) THEN
    No comma or equal, so IF keyword is found, but there is a second keyword.

    Another lexically ambiguous situation occurs with the FUNCTION keyword. To resolve this, it must be known whether this is the first statement of a procedure, or whether it's a subsequent statement. If this is the first statement in the source, or if it's the first statement after an END statement, then the form with an identifier inside the parenthesis is a FUNCTION declaration. In all other circumstances, the statement declares an array.

    This will always declare an array.

    INTEGER FUNCTION A(5)
    An array declaration FUNCTIONA of length 5 (this is not ambiguous).

    If this is the first statement in the source, or if it's the first statement after an END statement, a function is declared.

    INTEGER FUNCTION A(I)
    Either an array or a function is declared.

    Except for the keywords inside of an I/O control list (not implemented in f2j), these rules specify how to find all Fortran 77 keywords. Except for THEN and FUNCTION, all non-I/O keywords must be the first token of the statement they're in (with the caveat that they can be the first token of the sub-statement controlled by a logical IF - to which rules 1 to 3 still apply).

    Sale's algorithm resolves many of the issues that are frequently cited as major difficulties of lexically scanning Fortran. The algorithm can be implemented during a prepass to quickly identify most of the information that would normally require lookahead when during lexing. From there on, FORTRAN can be quickly and efficiently parsed with the same compiler tools used for more modern languages. The algorithm is easy to code, fast, and the rest of Fortran's syntax is fairly regular once resolved in the lexer."


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Sat Oct 17 14:58:53 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> schrieb:

    DEC F77 had a number of enhancements which made the language
    more friendly, like tab in column 1 skips to the start of a
    statement

    That didn't make it into the language, and also makes calculation
    of the line length a bit awkward. When should the line end?
    Remember, collumn 73-80 are ignored.

    or ! anywhere is a comment to end of line.

    Was that a DEC extension? At least it was adapted in Fortran 90.

    These were adopted by many other F77 compiler vendors
    presumably because they wanted to get their customers.

    They are a rat's nest. Yes, many were convenient, but compilers
    more or less have to keep supporting them for legacy code.
    gfortran has -fdec, which in turn enables around ten extensions,
    but not everything is implemented, for example variable formats
    or the infamous Q format descriptor.

    And, of course, the (in)famous PARAMETER X=20 snafu.

    Parenthesis can be used to force an order of execution.
    The parenthesis ordering rule is also data type dependent.
    It applies to integer, logical (also integer) and real but not strings.

    Not sure what use they would be for strings... but where did you get
    that information from?
    #1 a + b + c Unordered
    #2 (a+b) + c a+b before +c
    #3 a + (b+c) b+c before +a

    This would interact with common subexpression elimination.
    Above would not be able to optimize all 3 expressions to 1.
    It can replace #1 with #2 or #3, but not both.

    I'm not sure but I think parenthesis also could block optimizations
    like a*b+a*c => a*(b+c) but (a*b)+(a*c) might not be optimized.

    The rule is simple: In Fortran, parameters have to be honored,
    otherwise the rules of standard mathematics apply. So,
    a + b - a can be optimized to b.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Sat Oct 17 17:16:39 2026
    From Newsgroup: comp.arch

    John Levine <[email protected]> posted:

    According to EricP <[email protected]>:
    I am under the impression that many Fortran 77 compilers used
    hand written lexer and a recursive decent parser.
    The parser passes some local context to the lexer to let it know,
    for example, which set of keywords are active at that point.
    Anyway, that is how I would do it.

    I wrote a production FORTRAN 77 compiler. It had a hand written lexer
    and a yacc parser.

    It made a prepass over each statement to combine continuation cards and to tell
    whether it's an assignment statement or statement function (they're syntactically the same) or something else. Once it knows that, the lexer is straightforward using feedback from the parser to know what tokens are allowed
    at each point. As I recall there were about a dozen states, most of which were
    simple, e.g., after a DO keyword the next thing has to be a digit string which is a statement number.

    The prepass was needed to disambiguate DO10I=1.10 and also to handle stuff like this:

    10 FORMAT(I10,A5) // a format statement

    20 FORMAT(I10,A5) = 42 // an assignment statement

    Which could be written::

    20 FORMAT(I10,A5)
    -
    c comment getting in the way
    -
    - = 42
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Stefan Monnier@[email protected] to comp.arch on Sat Oct 17 11:09:49 2026
    From Newsgroup: comp.arch

    John Levine [2026-10-17 03:56:25] wrote:
    According to Anton Ertl <[email protected]>:
    Interesting, but did the FORTRAN team come up with that?
    I think we can assume they did since it's by far the easiest way to
    parse card image FORTRAN. If anyone really cares I believe the
    assembler source of some early compilers is available online.

    Does make one wonder, tho: why didn't they adjust the syntax to avoid
    those ambiguities, instead of spending all that effort to try and
    resolve them in the compiler?


    === Stefan
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Sun Oct 18 02:03:13 2026
    From Newsgroup: comp.arch

    On Sat, 17 Oct 2026 17:16:39 GMT, MitchAlsup
    <[email protected]d> wrote:

    Which could be written::

    20 FORMAT(I10,A5)
    -
    c comment getting in the way
    -
    - = 42

    Yes, but _that_ part is trivial. Take care of continuation cards and
    comments in a pass of their own before touching the language proper.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From EricP@[email protected] to comp.arch on Sun Oct 18 10:55:58 2026
    From Newsgroup: comp.arch

    On 2026-Oct-17 10:58, Thomas Koenig wrote:
    EricP <[email protected]> schrieb:

    DEC F77 had a number of enhancements which made the language
    more friendly, like tab in column 1 skips to the start of a
    statement

    That didn't make it into the language, and also makes calculation
    of the line length a bit awkward. When should the line end?
    Remember, collumn 73-80 are ignored.

    That shouldn't be a problem.
    A tab in columns 1 to 6 meant jump to column 7, the statement start.

    DEC also optionally allowed lines up to 132 characters -
    the full width of a VT100 in widescreen mode!

    or ! anywhere is a comment to end of line.

    Was that a DEC extension? At least it was adapted in Fortran 90.

    Yes

    There were lots more. 32 character variables (up from the standard 8).
    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    These were adopted by many other F77 compiler vendors
    presumably because they wanted to get their customers.

    They are a rat's nest. Yes, many were convenient, but compilers
    more or less have to keep supporting them for legacy code.
    gfortran has -fdec, which in turn enables around ten extensions,
    but not everything is implemented, for example variable formats
    or the infamous Q format descriptor.

    And, of course, the (in)famous PARAMETER X=20 snafu.

    I don't know that one. Do tell...

    Parenthesis can be used to force an order of execution.
    The parenthesis ordering rule is also data type dependent.
    It applies to integer, logical (also integer) and real but not strings.

    Not sure what use they would be for strings... but where did you get
    that information from?

    I have a 1988 copy of the DEC Fortran manual.
    I was looking at it for a specific set of parenthesis rules
    but no such luck. But it did say that parenthesis do not affect
    the value of a character string.

    My point being that whereas in many languages the parser can
    toss parenthesis, in F77 expressions they may be significant,
    and that significance may be semantic dependent.

    #1 a + b + c Unordered
    #2 (a+b) + c a+b before +c
    #3 a + (b+c) b+c before +a

    This would interact with common subexpression elimination.
    Above would not be able to optimize all 3 expressions to 1.
    It can replace #1 with #2 or #3, but not both.

    I'm not sure but I think parenthesis also could block optimizations
    like a*b+a*c => a*(b+c) but (a*b)+(a*c) might not be optimized.

    The rule is simple: In Fortran, parameters have to be honored,
    otherwise the rules of standard mathematics apply. So,
    a + b - a can be optimized to b.


    Yes but determining if two expressions are equivalent
    and therefore candidates for elimination can require
    some massaging first.

    #4 a + ((b+c))

    the first ( is significant, the second ( is redundant,
    and #4 should match #3 and not #2.

    But the following string expressions are all equivalent

    #5 ('ABC')//('DE')//'F'
    #6 'ABC'//('DE'//'F')
    #7 'ABC'//'DE'//'F'


    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Sun Oct 18 17:56:12 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> posted:

    On 2026-Oct-17 10:58, Thomas Koenig wrote:
    EricP <[email protected]> schrieb:

    DEC F77 had a number of enhancements which made the language
    more friendly, like tab in column 1 skips to the start of a
    statement

    That didn't make it into the language, and also makes calculation
    of the line length a bit awkward. When should the line end?
    Remember, collumn 73-80 are ignored.

    That shouldn't be a problem.
    A tab in columns 1 to 6 meant jump to column 7, the statement start.

    DEC also optionally allowed lines up to 132 characters -
    the full width of a VT100 in widescreen mode!

    or ! anywhere is a comment to end of line.

    Was that a DEC extension? At least it was adapted in Fortran 90.

    Yes

    There were lots more. 32 character variables (up from the standard 8).
    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    These were adopted by many other F77 compiler vendors
    presumably because they wanted to get their customers.

    They are a rat's nest. Yes, many were convenient, but compilers
    more or less have to keep supporting them for legacy code.
    gfortran has -fdec, which in turn enables around ten extensions,
    but not everything is implemented, for example variable formats
    or the infamous Q format descriptor.

    And, of course, the (in)famous PARAMETER X=20 snafu.

    I don't know that one. Do tell...

    Parenthesis can be used to force an order of execution.
    The parenthesis ordering rule is also data type dependent.
    It applies to integer, logical (also integer) and real but not strings.

    Not sure what use they would be for strings... but where did you get
    that information from?

    I have a 1988 copy of the DEC Fortran manual.
    I was looking at it for a specific set of parenthesis rules
    but no such luck. But it did say that parenthesis do not affect
    the value of a character string.

    My point being that whereas in many languages the parser can
    toss parenthesis, in F77 expressions they may be significant,
    and that significance may be semantic dependent.

    #1 a + b + c Unordered
    #2 (a+b) + c a+b before +c
    #3 a + (b+c) b+c before +a

    This would interact with common subexpression elimination.
    Above would not be able to optimize all 3 expressions to 1.
    It can replace #1 with #2 or #3, but not both.

    I'm not sure but I think parenthesis also could block optimizations
    like a*b+a*c => a*(b+c) but (a*b)+(a*c) might not be optimized.

    The rule is simple: In Fortran, parameters have to be honored,
    otherwise the rules of standard mathematics apply. So,
    a + b - a can be optimized to b.


    Yes but determining if two expressions are equivalent
    and therefore candidates for elimination can require
    some massaging first.

    #4 a + ((b+c))

    the first ( is significant, the second ( is redundant,
    and #4 should match #3 and not #2.

    But the following string expressions are all equivalent

    #5 ('ABC')//('DE')//'F'
    #6 'ABC'//('DE'//'F')
    #7 'ABC'//'DE'//'F'


    I remember concatenation as || not //.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Robert Finch@[email protected] to comp.arch on Sun Oct 18 22:54:24 2026
    From Newsgroup: comp.arch

    On 2026-10-17 4:51 a.m., John Savard wrote:
    On Wed, 14 Oct 2026 15:45:41 -0400, EricP
    <[email protected]> wrote:

    People learned a lot of what not to do in language design
    from old FORTRAN.

    Oh, yes. My language, EXALT, which I previously called FALCON, before learning the name was taken (several times), did learn from the

    DO 7 I = 1.100

    problem.

    It still takes stuff from FORTRAN.

    In it, a line with _any_ letter in Column 1 is a comment. So the
    comments can start two characters earlier!

    But assignment statements have to start with the keyword LET... or its abbreviation, which is just ".".

    Also FORTRAN-like: no semicolons needed at the end of the line.

    John Savard

    I like the C/C++ style comments. It is handy to have the two types of comments. // and /* */. I use // for almost all comments, then /* */ to comment out a block of code for debugging. First saw // in COMOL.

    On some assemblers a block comment starts with the word “comment” beginning in the first character of the line followed by the selected delimiter character.

    comment ~
    This is a comment
    ~

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Mon Oct 19 08:39:10 2026
    From Newsgroup: comp.arch

    On Sun, 18 Oct 2026 22:54:24 -0400, Robert Finch <[email protected]>
    wrote:

    I like the C/C++ style comments. It is handy to have the two types of >comments. // and /* */. I use // for almost all comments, then /* */ to >comment out a block of code for debugging.

    It should be noted that C originally only had /* and */. // was added
    with C++, and then proved so popular that it was then added to some
    versions of C.

    I included an equivalent of // in my language; I think I used @' or @"
    for it, so as to fit into my scheme of strict prefix-property
    decoding. That is: since / means divide, it is always and only divide;
    * means multiply, so it is always and only multiply. Exponentiation
    becomes ^, and @" or whatever becomes the end-of-line comment symbol
    since @ always precedes another special character to make a new token.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Mon Oct 19 08:42:41 2026
    From Newsgroup: comp.arch

    On Mon, 19 Oct 2026 08:39:10 GMT, [email protected] (John Savard)
    wrote:

    I included an equivalent of // in my language; I think I used @' or @"
    for it, so as to fit into my scheme of strict prefix-property
    decoding.

    I just checked. I needed @' and @" for handling quotes embedded in
    quoted strings, so it is actually @# that was the equivalent of //. /*
    and */ have @< and @> as their equivalents.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Mon Oct 19 18:09:55 2026
    From Newsgroup: comp.arch

    EricP <[email protected]> schrieb:
    On 2026-Oct-17 10:58, Thomas Koenig wrote:
    EricP <[email protected]> schrieb:

    DEC F77 had a number of enhancements which made the language
    more friendly, like tab in column 1 skips to the start of a
    statement

    That didn't make it into the language, and also makes calculation
    of the line length a bit awkward. When should the line end?
    Remember, collumn 73-80 are ignored.

    That shouldn't be a problem.
    A tab in columns 1 to 6 meant jump to column 7, the statement start.

    As long as your editor shows this correctly, and does not indent
    up to column 8...

    DEC also optionally allowed lines up to 132 characters -
    the full width of a VT100 in widescreen mode!

    Nice way to get vendor lock-in. But it made it into Fortran 90,
    but only in free format. Fixed format stayed at the limits of the
    IBM 704, which could only read 72 characters from a punched card
    because thy used the accumulator and their MQ register for it to
    read a line.


    or ! anywhere is a comment to end of line.

    Was that a DEC extension? At least it was adapted in Fortran 90.

    Yes

    There were lots more. 32 character variables (up from the standard 8).

    Six, actually - the 704 strikes again. But when Fortran 77
    was defined, 36-bit machines with six-bit characters still were
    still around, so it was handy to have one word as a symbol
    (for compiler writers, not so much for users...)

    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    OS/360 calling conventions is one thing they got wrong.

    These were adopted by many other F77 compiler vendors
    presumably because they wanted to get their customers.

    They are a rat's nest. Yes, many were convenient, but compilers
    more or less have to keep supporting them for legacy code.
    gfortran has -fdec, which in turn enables around ten extensions,
    but not everything is implemented, for example variable formats
    or the infamous Q format descriptor.

    And, of course, the (in)famous PARAMETER X=20 snafu.

    I don't know that one. Do tell...

    A pre-version of FORTRAN 77 had PARAMETER X = 20, and DEC
    pre-implemented this. The committee then settled on
    PARAMETER (X=20) .

    Can't blame them too much, I sort of made the same mistake again.


    Parenthesis can be used to force an order of execution.
    The parenthesis ordering rule is also data type dependent.
    It applies to integer, logical (also integer) and real but not strings.

    Not sure what use they would be for strings... but where did you get
    that information from?

    I have a 1988 copy of the DEC Fortran manual.
    I was looking at it for a specific set of parenthesis rules
    but no such luck. But it did say that parenthesis do not affect
    the value of a character string.

    My point being that whereas in many languages the parser can
    toss parenthesis, in F77 expressions they may be significant,
    and that significance may be semantic dependent.

    They are significant, especially in floating point. Assume (a -
    b) + c, where a + c can overflow or cause cancellation.

    This allows the progammer control over association where it is
    important.


    #1 a + b + c Unordered
    #2 (a+b) + c a+b before +c
    #3 a + (b+c) b+c before +a

    This would interact with common subexpression elimination.
    Above would not be able to optimize all 3 expressions to 1.
    It can replace #1 with #2 or #3, but not both.

    I'm not sure but I think parenthesis also could block optimizations
    like a*b+a*c => a*(b+c) but (a*b)+(a*c) might not be optimized.

    The rule is simple: In Fortran, parameters have to be honored,
    otherwise the rules of standard mathematics apply. So,
    a + b - a can be optimized to b.


    Yes but determining if two expressions are equivalent
    and therefore candidates for elimination can require
    some massaging first.

    #4 a + ((b+c))

    the first ( is significant, the second ( is redundant,
    and #4 should match #3 and not #2.

    But the following string expressions are all equivalent

    #5 ('ABC')//('DE')//'F'
    #6 'ABC'//('DE'//'F')
    #7 'ABC'//'DE'//'F'

    This should actually be the same. But today's Fortran does
    not specia-case strings anyway:

    10.1.8

    Integrity of parentheses

    1 The rules for evaluation specified in 10.1.5 state certain
    conditions under which a processor can evaluate an expres- sion
    that is different from the one specified by applying the rules
    given in 10.1.2 and the rules for interpretation specified in
    10.1.5. However, any expression in parentheses shall be treated
    as a data entity.

    NOTE

    For example, in evaluating the expression A + (B – C) where A, B, and
    C are of numeric types, the difference of B and C shall be evaluated
    before the addition operation is performed; the processor shall not
    evaluate the mathematically equivalent expression (A + B) – C.
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Tue Oct 20 01:43:23 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    John Savard <[email protected]> wrote:
    On Sun, 18 Oct 2026 22:54:24 -0400, Robert Finch <[email protected]>
    wrote:

    I like the C/C++ style comments. It is handy to have the two types of >>comments. // and /* */. I use // for almost all comments, then /* */ to >>comment out a block of code for debugging.

    It should be noted that C originally only had /* and */. // was added
    with C++, and then proved so popular that it was then added to some
    versions of C.

    Sorry, a minor historical quibble: `/* ... */` came to C via
    PL/1, which the creators of C had used on the Multics project.
    The `//` comment delimiters actually came from BCPL, C's
    almost-immediate antecedent, but for whatever reason they
    preferred the PL/1 style.

    C++ picked resurrected the BCPL-style delimiters, up, and as you
    noted, they were subsequently added into C. Incidentally, BCPL
    gained the PL/1-style comment delimiters sometime after C became
    popular.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Tue Oct 20 03:50:19 2026
    From Newsgroup: comp.arch

    According to Thomas Koenig <[email protected]>:
    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    OS/360 calling conventions is one thing they got wrong.

    My PDP-11 Unix F77 used the C compiler's back end, so recursion
    worked fine. I made variables automatic (stack) unless you used
    a SAVE statement, which made sense in the PDP-11's limited address space.

    It is quite possible to do recursion with the OS/360 calling convention.
    Their PL/I compilers did it and I believe the various ALGOL compilers,
    which had to support recursion, also used the standard calling sequence.
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Bill Findlay@[email protected] to comp.arch on Tue Oct 20 20:53:55 2026
    From Newsgroup: comp.arch

    On 20/10/2026 04:50, John Levine wrote:
    According to Thomas Koenig <[email protected]>:
    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    OS/360 calling conventions is one thing they got wrong.

    My PDP-11 Unix F77 used the C compiler's back end, so recursion
    worked fine. I made variables automatic (stack) unless you used
    a SAVE statement, which made sense in the PDP-11's limited address space.

    It is quite possible to do recursion with the OS/360 calling convention. Their PL/I compilers did it and I believe the various ALGOL compilers,
    which had to support recursion, also used the standard calling sequence.


    Brian Wichmann (of Whetstone Benchmark fame) gives the following
    figures for Ackermann's function, a stress test for recursion.

    ALGOL 60, by architecture, mean orders/call, * = estimate

    RMCS, VAX 12.5
    XALGOL, B6700 16.0
    RMCS, PE 3200 17.5
    XALGOL B5500 19.5
    ICL, ICL 2900 Series 19.5
    Manchester Uni., ICL 1900 Series 33.5

    System/3?0, by language and compiler

    ALGOL 60, Edinburgh Uni. /360 21.0
    PL/I OPT v1.2.1, IBM /360 61 *
    ALGOL W, Stanford Uni. /360 74 *
    ALGOL 60, Delft Uni. /360 142 *
    PL/I F level v5.4, IBM /360 212 *
    ALGOL 60, IBM F level /360 820 *

    Make of that what you will.

    And for amusement:

    DEC VAX: Language, Compiler

    Ada, York Ada global function 8
    C optimised 9
    C 10
    Ada, York Ada nested function 11
    Ada, RMCS compiler 12.5

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Thomas Koenig@[email protected] to comp.arch on Tue Oct 20 20:15:48 2026
    From Newsgroup: comp.arch

    Bill Findlay <[email protected]> schrieb:
    On 20/10/2026 04:50, John Levine wrote:
    According to Thomas Koenig <[email protected]>:
    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    OS/360 calling conventions is one thing they got wrong.

    My PDP-11 Unix F77 used the C compiler's back end, so recursion
    worked fine. I made variables automatic (stack) unless you used
    a SAVE statement, which made sense in the PDP-11's limited address space.

    It is quite possible to do recursion with the OS/360 calling convention.
    Their PL/I compilers did it and I believe the various ALGOL compilers,
    which had to support recursion, also used the standard calling sequence.


    Brian Wichmann (of Whetstone Benchmark fame) gives the following
    figures for Ackermann's function, a stress test for recursion.

    ALGOL 60, by architecture, mean orders/call, * = estimate

    I understand "ALGOL 60", but not the rest.

    What orders?
    --
    This USENET posting was made without artificial intelligence,
    artificial impertinence, artificial arrogance, artificial stupidity,
    artificial flavorings or artificial colorants.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From scott@[email protected] (Scott Lurndal) to comp.arch on Tue Oct 20 20:44:45 2026
    From Newsgroup: comp.arch

    Thomas Koenig <[email protected]> writes:
    Bill Findlay <[email protected]> schrieb:
    On 20/10/2026 04:50, John Levine wrote:
    According to Thomas Koenig <[email protected]>:
    DEC F77 used a stack and allowed recursion if you were careful
    whereas IBM F77 did not (it used static allocation for all local
    variables so recursion stomped on their values).

    OS/360 calling conventions is one thing they got wrong.

    My PDP-11 Unix F77 used the C compiler's back end, so recursion
    worked fine. I made variables automatic (stack) unless you used
    a SAVE statement, which made sense in the PDP-11's limited address space. >>>
    It is quite possible to do recursion with the OS/360 calling convention. >>> Their PL/I compilers did it and I believe the various ALGOL compilers,
    which had to support recursion, also used the standard calling sequence. >>>

    Brian Wichmann (of Whetstone Benchmark fame) gives the following
    figures for Ackermann's function, a stress test for recursion.

    ALGOL 60, by architecture, mean orders/call, * = estimate

    I understand "ALGOL 60", but not the rest.

    What orders?

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Tue Oct 20 22:49:07 2026
    From Newsgroup: comp.arch

    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal)
    wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred
    to as its "order code".

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From John Levine@[email protected] to comp.arch on Wed Oct 21 03:28:44 2026
    From Newsgroup: comp.arch

    According to Bill Findlay <[email protected]>:
    System/3?0, by language and compiler

    ALGOL 60, Edinburgh Uni. /360 21.0
    PL/I OPT v1.2.1, IBM /360 61 *
    ALGOL W, Stanford Uni. /360 74 *
    ALGOL 60, Delft Uni. /360 142 *
    PL/I F level v5.4, IBM /360 212 *
    ALGOL 60, IBM F level /360 820 *

    That looks about right. PL/I F was the original compiler which generated lousy code, later replaced by PL/I OPT. ALGOL F seems to have been written with the attitude, you want an Algol compiler? Here's an Algol compiler, suck it up. It did GETMAIN and FREEMAIN system calls to allocate storage whenever it entered or
    left a block. Someone at Princeton patched it to do what PL/I did, allocate large blocks and suballocate within them, which vastly improved its performance.
    --
    Regards,
    John Levine, [email protected], Primary Perpetrator of "The Internet for Dummies",
    Please consider the environment before reading this e-mail. https://jl.ly
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From quadibloc@[email protected] (John Savard) to comp.arch on Wed Oct 21 14:10:49 2026
    From Newsgroup: comp.arch

    On Wed, 21 Oct 2026 03:28:44 -0000 (UTC), John Levine
    <[email protected]> wrote:

    ALGOL F seems to have been written with the
    attitude, you want an Algol compiler? Here's an Algol compiler, suck it up.

    Yes. Unlike ALGOL W you had to indicate the keywords by putting quote
    marks around them - although some halfway-serious ALGOL 60 compilers
    for other architectures also did this.
    I presume that while ALGOL was not in any way a priority at IBM, they
    felt that some European customers, at least, might insist on an ALGOL
    compiler being available for the computer in order to consider
    purchasing a System/360.
    Given this, it's somewhat odd that the IBM 7030 or STRETCH went to the
    length of including the special characters of publication-level ALGOL
    in its character set. But perhaps in the years between the STRETCH and
    the 360, the bright hopes for ALGOL that may have existed at its
    inception had dimmed.

    John Savard
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Oct 21 20:11:30 2026
    From Newsgroup: comp.arch

    In article <[email protected]>,
    John Savard <[email protected]> wrote:
    On Wed, 21 Oct 2026 03:28:44 -0000 (UTC), John Levine
    <[email protected]> wrote:

    ALGOL F seems to have been written with the
    attitude, you want an Algol compiler? Here's an Algol compiler, suck it up.

    Yes. Unlike ALGOL W you had to indicate the keywords by putting quote
    marks around them - although some halfway-serious ALGOL 60 compilers
    for other architectures also did this.

    I don't believe that is true. The descriptions of AGOL W that
    are publicly available do not indicate that quote marks were
    required around keywords; I have only seen that on Algol 60
    code.

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From cross@[email protected] (Dan Cross) to comp.arch on Wed Oct 21 20:38:32 2026
    From Newsgroup: comp.arch

    In article <11bb69i$j4n$[email protected]>,
    Dan Cross <[email protected]> wrote:
    In article <[email protected]>,
    John Savard <[email protected]> wrote:
    On Wed, 21 Oct 2026 03:28:44 -0000 (UTC), John Levine
    <[email protected]> wrote:

    ALGOL F seems to have been written with the
    attitude, you want an Algol compiler? Here's an Algol compiler, suck it up. >>
    Yes. Unlike ALGOL W you had to indicate the keywords by putting quote
    marks around them - although some halfway-serious ALGOL 60 compilers
    for other architectures also did this.

    I don't believe that is true. The descriptions of AGOL W that
    are publicly available do not indicate that quote marks were
    required around keywords; I have only seen that on Algol 60
    code.

    Oops, I misread: you said _unlike_ Algol W, and you are
    completely correct. Apologies!

    - Dan C.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Bill Findlay@[email protected] to comp.arch on Wed Oct 21 23:06:04 2026
    From Newsgroup: comp.arch

    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal)
    wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred
    to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From scott@[email protected] (Scott Lurndal) to comp.arch on Thu Oct 22 00:02:25 2026
    From Newsgroup: comp.arch

    Bill Findlay <[email protected]> writes:
    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal)
    wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred
    to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)

    Perhaps you might, then, enlighten your yank
    cousins by providing the UK viewpoint.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Bill Findlay@[email protected] to comp.arch on Thu Oct 22 23:43:15 2026
    From Newsgroup: comp.arch

    On 22/10/2026 01:02, Scott Lurndal wrote:
    Bill Findlay <[email protected]> writes:
    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal)
    wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred
    to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)

    Perhaps you might, then, enlighten your yank
    cousins by providing the UK viewpoint.

    I do, from time to time, and am generally ignored.
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From MitchAlsup@[email protected] to comp.arch on Fri Oct 23 00:27:07 2026
    From Newsgroup: comp.arch

    Bill Findlay <[email protected]> posted:

    On 22/10/2026 01:02, Scott Lurndal wrote:
    Bill Findlay <[email protected]> writes:
    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal)
    wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred
    to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)

    Perhaps you might, then, enlighten your yank
    cousins by providing the UK viewpoint.

    I do, from time to time, and am generally ignored.

    That is ALMOST the Casandra Syndrome:: "always tell the truth,
    nobody ever believes"
    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Bill Findlay@[email protected] to comp.arch on Fri Oct 23 03:46:14 2026
    From Newsgroup: comp.arch

    On 23/10/2026 01:27, MitchAlsup wrote:
    Bill Findlay <[email protected]> posted:

    On 22/10/2026 01:02, Scott Lurndal wrote:
    Bill Findlay <[email protected]> writes:
    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal) >>>>> wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred >>>>> to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)

    Perhaps you might, then, enlighten your yank
    cousins by providing the UK viewpoint.

    I do, from time to time, and am generally ignored.

    That is ALMOST the Casandra Syndrome:: "always tell the truth,
    nobody ever believes"

    Quite so.
    However, it all goes into an archive and will continue
    being available to those with curiosity and broad minds.

    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From Michael S@[email protected] to comp.arch on Fri Oct 23 11:21:15 2026
    From Newsgroup: comp.arch

    On Fri, 23 Oct 2026 03:46:14 +0100
    Bill Findlay <[email protected]> wrote:

    On 23/10/2026 01:27, MitchAlsup wrote:
    Bill Findlay <[email protected]> posted:

    On 22/10/2026 01:02, Scott Lurndal wrote:
    Bill Findlay <[email protected]> writes:
    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott
    Lurndal) wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead
    referred to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)

    Perhaps you might, then, enlighten your yank
    cousins by providing the UK viewpoint.

    I do, from time to time, and am generally ignored.

    That is ALMOST the Casandra Syndrome:: "always tell the truth,
    nobody ever believes"

    Quite so.
    However, it all goes into an archive and will continue
    being available to those with curiosity and broad minds.


    Archives?
    Google's usenet archives became very unpleasant to use in recent months.
    Are there alternatives?




    --- Synchronet 3.22a-Linux NewsLink 1.2
  • From David Brown@[email protected] to comp.arch on Fri Oct 23 12:49:05 2026
    From Newsgroup: comp.arch

    On 23/10/2026 04:46, Bill Findlay wrote:
    On 23/10/2026 01:27, MitchAlsup wrote:
    Bill Findlay <[email protected]> posted:

    On 22/10/2026 01:02, Scott Lurndal wrote:
    Bill Findlay <[email protected]> writes:
    On 20/10/2026 23:49, John Savard wrote:
    On Tue, 20 Oct 2026 20:44:45 GMT, [email protected] (Scott Lurndal) >>>>>> wrote:

    Archaic terminology for "instructions".

    The early Burroughs systems (Electrodata 220, Burroughs 300,
    B3500) all called them 'orders' rather than instructions.

    In fact, the processor test suite for the B3500 and successors
    was called all-orders.

    At one time, the _instruction set_ of a computer was instead referred >>>>>> to as its "order code".
    The US-centrism is strong in these replies.
    Sigh. (I have taken that over from Nick.)

    Perhaps you might, then, enlighten your yank
    cousins by providing the UK viewpoint.

    I do, from time to time, and am generally ignored.

    That is ALMOST the Casandra Syndrome:: "always tell the truth,
    nobody ever believes"

    Quite so.
    However, it all goes into an archive and will continue
    being available to those with curiosity and broad minds.


    What do you think is going in the archives here? All I see is an unsubstantiated claim of "US centrism" for something that doesn't really matter very much anyway. I have no idea what terms were used at that
    time, so I have no idea if you are right or not - but you should justify
    your accusations with verifiable facts, or at the very least, anecdotes
    and personal experience.

    I am quite confident that our friends across the pond are not being intentionally US-centric about their history, but it is natural that
    their own experiences and what they have heard is somewhat biased - just
    like it is for the rest of us. I occasionally get corrected for my UK
    bias of history, and I appreciate such corrections - I think most people appreciate corrections more than unhelpful accusations.

    --- Synchronet 3.22a-Linux NewsLink 1.2