I'm basically trying to reinvent a subset of Erlang with Nim. Currently I'm trying to implement the "let it crash" philosophy. However, there are lots of unexpected things happening. For example:
echo 1/0
prints inf. Why and how? What are the other obvious exceptions that are automatically handled by Nim? Is / operator actually a function call then?
echo (1 div 0)
produces a proper exception:
./bin/let_it_crash
/home/ceremcem/projects/aktos/nim-examples/let_it_crash.nim(1) let_it_crash
/home/ceremcem/.choosenim/toolchains/nim-2.2.4/lib/system/fatal.nim(53) sysFatal
Error: unhandled exception: division by zero [DivByZeroDefect]
make: *** [Makefile:17: let_it_crash] Error 1
Okay. I'm expecting that. However, I'm also expecting to catch this exception by try/except block, which is not the case:
try:
echo (1 div 0)
except:
echo "something went wrong"
Does not print "something went wrong":
./bin/let_it_crash
/home/ceremcem/projects/aktos/nim-examples/let_it_crash.nim(2) let_it_crash
/home/ceremcem/.choosenim/toolchains/nim-2.2.4/lib/system/fatal.nim(53) sysFatal
Error: unhandled exception: division by zero [DivByZeroDefect]
make: *** [Makefile:17: let_it_crash] Error 1
It says "unhandled exception". How should I handle this exception? What are the other exceptions that I can't handle with try/except block?
When Nim (or C, C++, Rust, etc.) compiles a floating-point division, it typically just translates it directly into a single CPU instruction (DIVSD on x86). The CPU itself is producing the inf result.
If that's the case, it's partially understandable. But I was trying to divide integers 1 by 0. Nim must have translated my code to 1.0 / 0.0 even though I explicitly typed integers. Is that also correct? If yes, why would Nim convert my int s to float s magically?
Because of this: https://nim-lang.github.io/Nim/system.html#/%2Cint%2Cint
Also pay attention to the convertible rules in the manual: https://nim-lang.org/docs/manual.html#type-relations-convertible-relation
Basically:
– Float literals like 2.0 are float (64-bit); ints are polymorphic. – float64 and float32 are implicitly convertible both ways; int literals convert to many numeric types.
Exceptions that end with Defect signal "undefined behavior" in your application, and integer division by 0 is one of these things (overflows, out-of-range list access etc are other examples) - for this category of errors, you have to do the error checking up-front rather than letting it happen and trying to catch it with an exception handler - this is similar to panics and asserts in other languages.
The runtime is able to generate such post-factum panics for some issues in your program but not all - nil access and several other forms of undefined behavior are not covered, it's a best-effort approach, and except: does not catch it.
Sometimes, you might be able to catch them with except Defect: .. but this is not reliable.
So I'm not able to try/catch all errors which means I can not implement the "let it crash" approach because some defects are allowed to bring down whole application. What I was trying to do is something like:
import std/strutils
import times
import std/os
type
Coroutine = iterator(): int {.closure.}
Scheduler = object
coroutines: array[2, Coroutine]
sleepUntil: array[2, float]
template sleep(ms: int): untyped =
yield ms
proc addTask*(self: var Scheduler, coroutine: Coroutine) =
for i in 0..<self.coroutines.len:
if self.coroutines[i] == nil:
self.coroutines[i] = coroutine
self.sleepUntil[i] = 0.0
break
proc run*(self: var Scheduler) =
let startTime = epochTime()
while true:
let currentTime = epochTime() - startTime
for i in 0..<self.coroutines.len:
if self.coroutines[i] != nil and currentTime >= self.sleepUntil[i]:
try:
let sleepTime = self.coroutines[i]()
if sleepTime > 0:
self.sleepUntil[i] = currentTime + (sleepTime.float / 1000.0)
except:
echo "Something went wrong with coroutine:", i, ": ", getCurrentExceptionMsg()
self.sleepUntil[i] = currentTime + (2)
os.sleep(10)
# Coroutine with while true loop and yield - marked as closure
iterator helloCoroutine(): int {.closure.} =
var a: array[0..2, string] = ["1", "2", "x"]
var i = 0
while true:
echo "hello(", getTime().format("HH:mm:ss"), ")", " parse:", a[i].parseInt()
i += 1
sleep 1000
iterator worldCoroutine(): int {.closure.} =
while true:
echo "world(", getTime().format("HH:mm:ss"), ")"
sleep 1500
when isMainModule:
var sched = Scheduler()
sched.addTask(helloCoroutine)
sched.addTask(worldCoroutine)
sched.run()
See helloCoroutine which will fail in 3rd iteration and it is caught within the scheduler. I understand that I can't catch all types of exceptions or defects.
You can't. Some operations (like memory access) are inherently unsafe. Other are made unsafe for performance reasons. For the latter I could imagine a macro that would automatically insert checks around them and throw exceptions, but segfaults can still take down your application.
In general, you'll need to both restrict what the programmer can do and add runtime overhead to make everything recoverable, i.e. something like python.