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
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
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
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
Okay, here is the branch: https://github.com/imclerran/roc-ai/tree/roc-check-overflow
File in question is package/Toolkit/FileSystem.roc
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
Solution: use purity inference
It sounds stupid, but that code is gonna get ripped out soon
Yep, it's great :)
When Task is removed
Going to leave this here for future stack overflows:
lldb ./target/debug/roc
(lldb) settings set -- target.run-args "check" "/home/username/gitrepos/roc-ai/package/Toolkit/FileSystem.roc"
(lldb) process handle -p true -s false -n false SIGSEGV SIGBUS
(lldb) run
(lldb) bt
Purity inference migration tips are here in the migration guide:
https://github.com/roc-lang/basic-cli/releases/tag/0.18.0
Ian McLerran has marked this topic as resolved.
Ian McLerran has marked this topic as unresolved.
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.
AND: if I add a try
statement before listDir!
on line 125, the whole file checks with 0 errors and 0 warnings.
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.
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
.
Ian McLerran has marked this topic as resolved.
Last updated: Jul 06 2025 at 12:14 UTC