openlang

CPS sum overflows the stack in Debug, and in Release only the second function does

Hello all,

I am working through continuation-passing style as an exercise, using a small sensor tool for my data work. loadReadings generates a list and stands in for a CSV loader I have not written yet. Then I total the values in two ways. Both are written so the recursive call is the last thing that happens, as I understand it.

let rec sumK (xs: int list) (k: int -> int) =
    match xs with
    | [] -> k 0
    | x :: rest -> sumK rest (fun s -> k (s + x))

let rec sumCheckedK (xs: Reading list) (k: int -> int) =
    match xs with
    | [] -> k 0
    | r :: rest ->
        try
            if r.Value < 0 then
                failwithf "faulty reading from %s" r.Sensor
            sumCheckedK rest (fun s -> k (s + r.Value))
        with ex ->
            log ex.Message
            sumCheckedK rest k

In main I call sumK values id, then sumCheckedK readings id, with the default 1,000,000 readings (every 250000th is a faulty -1).

  • dotnet run (Debug): dies almost immediately with Stack overflow. and a huge repeating trace, before any total.
  • dotnet run -c Release: the sumK total prints correctly, then Stack overflow. again, with a repeating frame mentioning sumCheckedK.
  • With -- 10000 both builds work.

My first suspicion was the list construction, but the Release run shows that is fine. Then I wondered whether the JIT gives up on deeply nested closures. I do not set anything special in the fsproj (net8.0, .NET 8 SDK, Rider on an M-series Mac).

Is this one problem or two? I would rather understand it than just enlarge the thread stack.

The conversation continues 2

Follow the reasoning
  1. Permalink

    Two problems, I think.

    First, Debug turns off tail calls by default, so sumK overflows there. I assume your fsproj has no <Tailcalls> line, so Debug is using that default. I had the same thing once with a client project: it worked in Release and died in Debug, and I lost one afternoon.

    Second, sumCheckedK calls itself inside try. That is not a tail position, so Release also keeps a frame for each element. Move the recursive call outside the try.

  2. Permalink

    Also sumCheckedK rest k inside with is not a tail call either. Make try only decide valid or faulty, returning a bool or option, then do both recursive calls after it. I fixed mine the same way.

Other sides of the same question

  1. Same Int, two targets, two answers

    Same source, same JSON file, different result per target. This is bad. cohort_join reads a JSON export from our tracking syst…

    Replies 0Latest activity gleam
  2. pure host function gets cached, write to its argument just vanishes

    Weekend project: a toy expression evaluator with host functions written in D, registered by name. I added a memo cache in the…

    Replies 2Latest activity d
  3. same parseServices, bad line only breaks at build time?

    Changed auto to enum for the built-in table since it never changes, right? Now smtp 25 (no proto) breaks dub build, error end…

    Replies 2Latest activity d