Stream: bugs

Topic: ✔ `Roc check` stack overflow (migration to PI)


view this post on Zulip Ian McLerran (Jan 06 2025 at 18:28):

I'm in progress migrating some source code to PI, and when I run roc check on my source file, I get the following error message:

thread '<unknown>' has overflowed its stack
fatal runtime error: stack overflow
[1]    12551 abort      roc check Toolkit/FileSystem.roc

view this post on Zulip Ian McLerran (Jan 06 2025 at 18:30):

Here is the updated source:

module { pathFromStr, pathToStr, listDir!, isDir!, readFile!, writeUtf8! } -> [
    listDirectory,
    listFileTree,
    readFileContents,
    writeFileContents,
]

import json.Json
import InternalTools exposing [Tool, buildTool]

## Expose name, handler and tool for listDirectory.
listDirectory : { name : Str, handler! : Str => Result Str *, tool : Tool }
listDirectory = {
    name: listDirectoryTool.function.name,
    handler!: listDirectoryHandler!,
    tool: listDirectoryTool,
}

## Tool definition for the listDirectory function
listDirectoryTool : Tool
listDirectoryTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a directory. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    buildTool "listDirectory" "List the contents of a directory" [pathParam]

## Handler for the listDirectory tool
listDirectoryHandler! : Str => Result Str _
listDirectoryHandler! = \args ->
    decoded : Decode.DecodeResult { path : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Ok "Failed to decode args"

        Ok { path } ->
            if path |> Str.contains ".." then
                Ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Ok "Invalid path: must be a relative path"
            else
                listDir! (pathFromStr path)
                |> Result.withDefault []
                |> List.map pathToStr
                |> Str.joinWith "\n"
                |> Ok

## Expose name, handler and tool for listFileTree.
##
## This tool will allow the model to list the contents of a directory, and all subdirectories.
listFileTree : { name : Str, handler : Str => Result Str *, tool : Tool }
listFileTree = {
    name: listFileTreeTool.function.name,
    handler!: listFileTreeHandler!,
    tool: listFileTreeTool,
}

## Tool definition for the listFileTree function
listFileTreeTool : Tool
listFileTreeTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a directory. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    buildTool "listFileTree" "List the contents of a directory and all subdirectories" [pathParam]

## Handler for the listFileTree tool
listFileTreeHandler! : Str => Result Str _
listFileTreeHandler! = \args ->
    decoded : Decode.DecodeResult { path : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Ok "Failed to decode args"

        Ok { path } ->
            if path |> Str.contains ".." then
                Ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Ok "Invalid path: must be a relative path"
            else
                dirContents = path |> pathFromStr |> listDir! |> Result.withDefault []
                fileTreeHelper! dirContents "" 0

## Recursive helper function for listFileTreeHandler
fileTreeHelper! : List path, Str, U64 => Result Str _
fileTreeHelper! = \paths, accumulation, depth ->
    prependNewline = \str -> if Str.isEmpty str then str else Str.concat "\n" str
    appendNewline = \str -> if Str.isEmpty str then str else Str.concat str "\n"
    buildStr = \previous, current, subcontents -> "$(appendNewline previous)$(current)$(subcontents)"

    when paths is
        [] ->
            Ok accumulation

        [path, .. as pathsTail] ->
            if pathToStr path |> Str.contains "/." then
                fileTreeHelper! pathsTail accumulation depth
            else if try isDir! path then
                subcontents = try fileTreeHelper! (listDir! path) "" (depth + 1) |> prependNewline
                newString = buildStr accumulation (pathToStr path) subcontents
                fileTreeHelper! pathsTail newString depth
            else
                newString = buildStr accumulation (pathToStr path) ""
                fileTreeHelper! pathsTail newString depth

## Expose name, handler and tool for readFileContents.
##
## This tool will allow the model to read the contents of a file.
readFileContents : { name : Str, handler! : Str => Result Str *, tool : Tool }
readFileContents = {
    name: readFileContentsTool.function.name,
    handler!: readFileContentsHandler!,
    tool: readFileContentsTool,
}

## Tool definition for the readFileContents function
readFileContentsTool : Tool
readFileContentsTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a directory. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    buildTool "readFileContents" "Read the contents of a file. Must be a plain text file (any extension)." [pathParam]

## Handler for the readFileContents tool
readFileContentsHandler! : Str => Result Str _
readFileContentsHandler! = \args ->
    decoded : Decode.DecodeResult { path : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Ok "Failed to decode args"

        Ok { path } ->
            if path |> Str.contains ".." then
                Ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Ok "Invalid path: must be a relative path"
            else
                path
                |> pathFromStr
                |> readFile!
                |> Result.withDefault "Failed to read file"
                |> Ok

## Expose name, handler and tool for writeFileContents.
##
## This tool will allow the model to write content to a file.
writeFileContents : { name : Str, handler! : Str => Result Str *, tool : Tool }
writeFileContents = {
    name: writeFileContentsTool.function.name,
    handler!: writeFileContentsHandler!,
    tool: writeFileContentsTool,
}

## Tool definition for the writeFileContents function
writeFileContentsTool : Tool
writeFileContentsTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a file. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    contentParam = {
        name: "content",
        type: "string",
        description: "The full text content to write to the file. This must be the full content of the file.",
        required: Bool.true,
    }
    buildTool
        "writeFileContents"
        """
        Write the text content to a file. Any existing file at the specified path will be overwritten. If the file does not exist, it will be created, but parent directories must exist.
        """
        [pathParam, contentParam]

## Handler for the writeFileContents tool
writeFileContentsHandler! : Str => Result Str _
writeFileContentsHandler! = \args ->
    decoded : Decode.DecodeResult { path : Str, content : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Ok "Failed to decode args"

        Ok { path, content } ->
            if path |> Str.contains ".." then
                Ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Ok "Invalid path: must be a relative path"
            else
                path
                |> pathFromStr
                |> writeUtf8! content
                |> Result.try \_ -> Ok "File successfully updated."
                |> Result.onErr handleWriteErr
                |> Result.withDefault "Error writing to file"
                |> Ok

handleWriteErr = \err ->
    when err is
        FileWriteErr _ NotFound -> Ok "File not found"
        FileWriteErr _ AlreadyExists -> Ok "File already exists"
        FileWriteErr _ Interrupted -> Ok "Write interrupted"
        FileWriteErr _ OutOfMemory -> Ok "Out of memory"
        FileWriteErr _ PermissionDenied -> Ok "Permission denied"
        FileWriteErr _ TimedOut -> Ok "Timed out"
        FileWriteErr _ WriteZero -> Ok "Write zero"
        FileWriteErr _ (Other str) -> Ok str

view this post on Zulip Ian McLerran (Jan 06 2025 at 18:31):

And original:

## A collection of prebuilt tools for interacting with the file system. For safety reasons, the tools in this module are limited to working in the current working directory and its subdirectories.
## ```
## # USAGE:
## # Tool list to initialize the client
## tools = [listDirectory, listFileTree, readFileContents, writeFileContents ]
## # Tool handler map is passed to Tools.handleToolCalls!
## toolHandlerMap = Dict.fromList [
##     (listDirectory.name, listDirectory.handler),
##     (listFileTree.name, listFileTree.handler),
##     (readFileContents.name, readFileContents.handler),
##     (writeFileContents.name, writeFileContents.handler),
## ]
## client = Client.init { apiKey, model: "tool-capable/model", tools }
## #...
## messages = Chat.appendUserMessage previousMessages newMessage
## response = Http.send (Chat.buildHttpRequest client messages {}) |> Task.result!
## updatedMessages = updateMessagesFromResponse response messages
##     |> Tools.handleToolCalls! client toolHandlerMap
## ```
module { pathFromStr, pathToStr, listDir, isDir, readFile, writeUtf8 } -> [
    listDirectory,
    listFileTree,
    readFileContents,
    writeFileContents,
]

import json.Json
import InternalTools exposing [Tool, buildTool]

## Expose name, handler and tool for listDirectory.
listDirectory : { name : Str, handler : Str -> Task Str *, tool : Tool }
listDirectory = {
    name: listDirectoryTool.function.name,
    handler: listDirectoryHandler,
    tool: listDirectoryTool,
}

## Tool definition for the listDirectory function
listDirectoryTool : Tool
listDirectoryTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a directory. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    buildTool "listDirectory" "List the contents of a directory" [pathParam]

## Handler for the listDirectory tool
listDirectoryHandler : Str -> Task Str _
listDirectoryHandler = \args ->
    decoded : Decode.DecodeResult { path : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Task.ok "Failed to decode args"

        Ok { path } ->
            if path |> Str.contains ".." then
                Task.ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Task.ok "Invalid path: must be a relative path"
            else
                listDir (pathFromStr path)
                    |> Task.result!
                    |> Result.withDefault []
                    |> List.map pathToStr
                    |> Str.joinWith "\n"
                    |> Task.ok

## Expose name, handler and tool for listFileTree.
##
## This tool will allow the model to list the contents of a directory, and all subdirectories.
listFileTree : { name : Str, handler : Str -> Task Str *, tool : Tool }
listFileTree = {
    name: listFileTreeTool.function.name,
    handler: listFileTreeHandler,
    tool: listFileTreeTool,
}

## Tool definition for the listFileTree function
listFileTreeTool : Tool
listFileTreeTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a directory. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    buildTool "listFileTree" "List the contents of a directory and all subdirectories" [pathParam]

## Handler for the listFileTree tool
listFileTreeHandler : Str -> Task Str _
listFileTreeHandler = \args ->
    decoded : Decode.DecodeResult { path : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Task.ok "Failed to decode args"

        Ok { path } ->
            if path |> Str.contains ".." then
                Task.ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Task.ok "Invalid path: must be a relative path"
            else
                dirContents = path |> pathFromStr |> listDir |> Task.result! |> Result.withDefault []
                fileTreeHelper dirContents "" 0

## Recursive helper function for listFileTreeHandler
fileTreeHelper : List path, Str, U64 -> Task Str _
fileTreeHelper = \paths, accumulation, depth ->
    prependNewline = \str -> if Str.isEmpty str then str else Str.concat "\n" str
    appendNewline = \str -> if Str.isEmpty str then str else Str.concat str "\n"
    buildStr = \previous, current, subcontents -> "$(appendNewline previous)$(current)$(subcontents)"

    when paths is
        [] ->
            Task.ok accumulation

        [path, .. as pathsTail] ->
            if pathToStr path |> Str.contains "/." then
                fileTreeHelper pathsTail accumulation depth
            else if isDir! path then
                subcontents = fileTreeHelper! (listDir! path) "" (depth + 1) |> prependNewline
                newString = buildStr accumulation (pathToStr path) subcontents
                fileTreeHelper pathsTail newString depth
            else
                newString = buildStr accumulation (pathToStr path) ""
                fileTreeHelper pathsTail newString depth

## Expose name, handler and tool for readFileContents.
##
## This tool will allow the model to read the contents of a file.
readFileContents : { name : Str, handler : Str -> Task Str *, tool : Tool }
readFileContents = {
    name: readFileContentsTool.function.name,
    handler: readFileContentsHandler,
    tool: readFileContentsTool,
}

## Tool definition for the readFileContents function
readFileContentsTool : Tool
readFileContentsTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a directory. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    buildTool "readFileContents" "Read the contents of a file. Must be a plain text file (any extension)." [pathParam]

## Handler for the readFileContents tool
readFileContentsHandler : Str -> Task Str _
readFileContentsHandler = \args ->
    decoded : Decode.DecodeResult { path : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Task.ok "Failed to decode args"

        Ok { path } ->
            if path |> Str.contains ".." then
                Task.ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Task.ok "Invalid path: must be a relative path"
            else
                path
                    |> pathFromStr
                    |> readFile
                    |> Task.result!
                    |> Result.withDefault "Failed to read file"
                    |> Task.ok

## Expose name, handler and tool for writeFileContents.
##
## This tool will allow the model to write content to a file.
writeFileContents : { name : Str, handler : Str -> Task Str *, tool : Tool }
writeFileContents = {
    name: writeFileContentsTool.function.name,
    handler: writeFileContentsHandler,
    tool: writeFileContentsTool,
}

## Tool definition for the writeFileContents function
writeFileContentsTool : Tool
writeFileContentsTool =
    pathParam = {
        name: "path",
        type: "string",
        description: "The relative unix style path to a file. `..` is not allowed. Must begin with `.`",
        required: Bool.true,
    }
    contentParam = {
        name: "content",
        type: "string",
        description: "The full text content to write to the file. This must be the full content of the file.",
        required: Bool.true,
    }
    buildTool
        "writeFileContents"
        """
        Write the text content to a file. Any existing file at the specified path will be overwritten.
        If the file does not exist, it will be created, but parent directories must exist.
        """
        [pathParam, contentParam]

## Handler for the writeFileContents tool
writeFileContentsHandler : Str -> Task Str _
writeFileContentsHandler = \args ->
    decoded : Decode.DecodeResult { path : Str, content : Str }
    decoded = args |> Str.toUtf8 |> Decode.fromBytesPartial Json.utf8
    when decoded.result is
        Err _ ->
            Task.ok "Failed to decode args"

        Ok { path, content } ->
            if path |> Str.contains ".." then
                Task.ok "Invalid path: `..` is not allowed"
            else if path |> Str.startsWith "/" then
                Task.ok "Invalid path: must be a relative path"
            else
                path
                    |> pathFromStr
                    |> writeUtf8 content
                    |> Task.result!
                    |> Result.try \_ -> Ok "File successfully updated."
                    |> Result.onErr handleWriteErr
                    |> Result.withDefault "Error writing to file"
                    |> Task.ok

handleWriteErr = \err ->
    when err is
        FileWriteErr _ NotFound -> Ok "File not found"
        FileWriteErr _ AlreadyExists -> Ok "File already exists"
        FileWriteErr _ Interrupted -> Ok "Write interrupted"
        FileWriteErr _ OutOfMemory -> Ok "Out of memory"
        FileWriteErr _ PermissionDenied -> Ok "Permission denied"
        FileWriteErr _ TimedOut -> Ok "Timed out"
        FileWriteErr _ WriteZero -> Ok "Write zero"
        FileWriteErr _ (Other str) -> Ok str

view this post on Zulip Anton (Jan 06 2025 at 18:35):

Here is the updated source:

Can you put it up on a branch because standalone it fails with:

── UNRECOGNIZED PACKAGE in temp.roc ────────────────────────────────────────────

This module is trying to import from `json`:

27│  import json.Json

view this post on Zulip Ian McLerran (Jan 06 2025 at 18:44):

Okay, here is the branch: https://github.com/imclerran/roc-ai/tree/roc-check-overflow

File in question is package/Toolkit/FileSystem.roc

view this post on Zulip Anton (Jan 06 2025 at 19:04):

    frame #1938: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1939: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1940: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1941: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1942: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1943: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1944: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1945: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1946: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1947: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1948: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1949: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1950: 0x000055555aa2b3ca roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:354:36
    frame #1951: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1952: 0x000055555aa2bf87 roc`unwrap_suffixed_expression_if_then_else_help at suffixed.rs:390:36
    frame #1953: 0x000055555aa22460 roc`unwrap_suffixed_expression at suffixed.rs:135:17
    frame #1954: 0x000055555aa2ccc5 roc`unwrap_suffixed_expression_when_help at suffixed.rs:573:56
    frame #1955: 0x000055555aa2248b roc`unwrap_suffixed_expression at suffixed.rs:132:31
    frame #1956: 0x000055555aa2e278 roc`unwrap_suffixed_expression_defs_help at suffixed.rs:763:19
    frame #1957: 0x000055555aa22368 roc`unwrap_suffixed_expression at suffixed.rs:126:31
    frame #1958: 0x000055555aa23f8f roc`unwrap_suffixed_expression_closure_help at suffixed.rs:226:19
    frame #1959: 0x000055555aa2233d roc`unwrap_suffixed_expression at suffixed.rs:139:17
    frame #1960: 0x000055555a9ad36c roc`desugar_value_def_suffixed at desugar.rs:435:19
    frame #1961: 0x000055555a9ad1f8 roc`desugar_defs_node_values at desugar.rs:384:26
    frame #1962: 0x000055555a8379bd roc`canonicalize_module_defs at module.rs:271:5
    frame #1963: 0x00005555593aa337 roc`canonicalize_and_constrain at file.rs:5159:29
    frame #1964: 0x00005555593b549e roc`roc_load_internal::file::run_task::h36ba1a08da18b512 at file.rs:6290:31
    frame #1965: 0x00005555593e395d roc`roc_load_internal::file::load_multi_threaded::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::hdbaf994ac2af5d99 at file.rs:2075:33

view this post on Zulip Sam Mohr (Jan 06 2025 at 19:04):

Solution: use purity inference

view this post on Zulip Sam Mohr (Jan 06 2025 at 19:05):

It sounds stupid, but that code is gonna get ripped out soon

view this post on Zulip Anton (Jan 06 2025 at 19:05):

Yep, it's great :)

view this post on Zulip Sam Mohr (Jan 06 2025 at 19:05):

When Task is removed

view this post on Zulip Anton (Jan 06 2025 at 19:07):

Going to leave this here for future stack overflows:

  1. First, launch LLDB with your Roc executable:
lldb ./target/debug/roc
  1. Once in LLDB, set the arguments for the program:
(lldb) settings set -- target.run-args "check" "/home/username/gitrepos/roc-ai/package/Toolkit/FileSystem.roc"
  1. Set LLDB to stop on crashes:
(lldb) process handle -p true -s false -n false SIGSEGV SIGBUS
  1. Run the program:
(lldb) run
  1. When it crashes, you can examine the stack trace:
(lldb) bt

view this post on Zulip Anton (Jan 06 2025 at 19:08):

Purity inference migration tips are here in the migration guide:
https://github.com/roc-lang/basic-cli/releases/tag/0.18.0

view this post on Zulip Notification Bot (Jan 06 2025 at 19:58):

Ian McLerran has marked this topic as resolved.

view this post on Zulip Notification Bot (Jan 06 2025 at 20:58):

Ian McLerran has marked this topic as unresolved.

view this post on Zulip Ian McLerran (Jan 06 2025 at 20:59):

Okay, update to this bug -- if I expose any of my effectful functions in the module exports, the stack overflow is not encountered and it parses the file successfully. I simply encounter an error telling me I have a type mismatch in one of my functions.

view this post on Zulip Ian McLerran (Jan 06 2025 at 21:02):

AND: if I add a try statement before listDir! on line 125, the whole file checks with 0 errors and 0 warnings.

view this post on Zulip Ian McLerran (Jan 06 2025 at 21:04):

So the stack overflow is encounted entirely because there are no idents with ! exposed, even though the Module is syntactically correct PI, and with the try keyword introduced, a fully correct roc module.

view this post on Zulip Ian McLerran (Jan 06 2025 at 21:05):

I will push up the addition of the try keyword, and a commented out export. Simply uncomment that export to see that this is the only difference between a stack overflow and 0 errors, 0 warnings.

view this post on Zulip Notification Bot (Jan 06 2025 at 21:18):

Ian McLerran has marked this topic as resolved.


Last updated: Jul 06 2025 at 12:14 UTC