arturo-lang / arturo

Simple, expressive & portable programming language for efficient scripting
http://arturo-lang.io
MIT License
675 stars 29 forks source link

[VM/errors] general cleanup needed #1592

Open github-actions[bot] opened 3 months ago

github-actions[bot] commented 3 months ago

[VM/errors] general cleanup needed

https://github.com/arturo-lang/arturo/blob/4bed4542b3306aaeb55fe7d11f5193b2f90c3fef/src/vm/errors.nim#L30

#  they have all been temporarily disabled due to the error system rewrite
#  labels: error handling, unit-test, critical

# TODO(VM/errors) make sure we catch/template all system errors
#  for example, there are regex-related errors or other that
#  can trigger uncatchable errors (but I don't remember right now
#  how to reproduce). We should treat them all uniformly and with a nice
#  message, etc ;-)
#  labels: error handling, enhancement

# TODO(VM/errors) add support for VM "warnings"?
#  provided that they could be turned off by default (or not?), 
#  we could have warnings alongside errors: like errors, just not
#  killing the execution, etc. One such warning could be a "deprecated"
#  warning, for features that are to be eliminated + suggestions on
#  how to deal with it
#  labels: error handling, enhancement, open discussion

# TODO(VM/errors) general cleanup needed
#  labels: error handling,cleanup

#=======================================
# Libraries
#=======================================

when not defined(WEB):
    import re, terminal
import sequtils, strformat, strutils, sugar, std/with

import helpers/strings
import helpers/terminal

import vm/values/custom/verror

#=======================================
# Types
#=======================================

type
    ReturnTriggered*    = ref object of Defect
    BreakTriggered*     = ref object of Defect
    ContinueTriggered*  = ref object of Defect

#=======================================
# Constants
#=======================================

const
    MaxIntSupported     = $(sizeof(int) * 8)
    ReplContext         = " <repl> "

    UseUnicodeChars     = true

    HorizLine           = when UseUnicodeChars: "\u2550" else: "="
    VertLine            = when UseUnicodeChars: "\u2503" else: "|"
    VertLineD           = when UseUnicodeChars: "\u2551" else: "|"
    ArrowRight          = when UseUnicodeChars: "\u25ba" else: ">"
    LeftBracket         = when UseUnicodeChars: "\u2561" else: "["
    RightBracket        = when UseUnicodeChars: "\u255E" else: "]"

#=======================================
# Variables
#=======================================

var
    CurrentContext* : string    = ReplContext
    CurrentPath*    : string    = ""
    CurrentLine*    : int       = 0
    ExecStack*      : seq[int]  = @[]

#=======================================
# Helpers
#=======================================

# Check environment

proc isRepl(): bool =
    return CurrentContext == ReplContext

proc getCurrentContext(e: VError): string =
    if e.kind == CmdlineErr: return ""

    if CurrentContext == ReplContext: return CurrentContext
    return " <script> "

proc getMaxWidth(): int =
    when not defined(WEB):
        terminalWidth()
    else:
        80

# General formatting

proc processPseudomarkdown(s: string): string =
    result = 
        when not defined(WEB):
            s.replacef(re"_([^_]+)_",fmt("{bold()}$1{resetColor}"))
        else:
            s

proc formatMessage(s: string): string =
    var ret = s.processPseudomarkdown()
               #.replacef(re":([a-z]+)",fmt("{fg(magentaColor)}:$1{resetColor}"))

    ret = indent(strip(dedent(ret)), 2)

    return ret

func lineTrunc(s: string, padding: int): string =
    let lines = s.splitLines()
    if lines.len > 5:
        result = lines[0..4].join("\n")
        result &= "\n" & "..."
        result = strip(result)
    else:
        result = s

    result = indent(result, padding)

func `~~`*(s: string, inputs: seq[string]): string =
    var replacements: seq[string]
    var finalS = s
    when not defined(WEB):
        for line in s.splitLines():
            for found in line.findAll(re"\$[\$#]"):
                if found=="$$":
                    let ind = line.realFind("$$")
                    replacements.add(strip(lineTrunc(strip(inputs[replacements.len]),ind)))
                else:
                    replacements.add(inputs[replacements.len])
    else:
        replacements = inputs

    finalS = finalS.replace("$$", "$#")
    return finalS % replacements    

# Error messages

proc printErrorHeader(e: VError) =
    let preHeader = 
        fg(redColor) & "{HorizLine}{HorizLine}{LeftBracket} ".fmt & 
        bold(redColor) & (e.kind.label) & 
        fg(redColor) & " {RightBracket}".fmt

    let postHeader = 
        getCurrentContext(e) & 
        "{HorizLine}{HorizLine}".fmt & 
        resetColor()

    let middleStretch = getMaxWidth() - preHeader.realLen() - postHeader.realLen()

    echo ""
    echo preHeader & repeat(HorizLine, middleStretch) & postHeader

proc printErrorKindDescription(e: VError) =
    if e.kind.description != "":
        echo ""
        echo indent(e.kind.description, 2) & resetColor

proc printErrorMessage(e: VError) =
    echo ""
    echo strip(indent(dedent(formatMessage(e.msg)), 2), chars={'\n'})

proc printCodePreview(e: VError) =
    when not defined(NOERRORLINES):
        if e.context.file == "":
            e.context.line = CurrentLine
            e.context.file = CurrentPath

        if (not isRepl()) and (e.kind != CmdlineErr) and (e.kind != ProgramErr) :
            echo ""
            let codeLines = readFile(e.context.file).splitLines()
            const linesBeforeAfter = 2
            let lineFrom = max(0, e.context.line - (linesBeforeAfter+1))
            let lineTo = min(len(codeLines)-1, e.context.line + (linesBeforeAfter-1))
            echo "  " & fg(grayColor) & "{VertLine} ".fmt & bold(grayColor) & "File: " & fg(grayColor) & e.context.file
            echo "  " & fg(grayColor) & "{VertLine} ".fmt & bold(grayColor) & "Line: " & fg(grayColor) & $(e.context.line)
            echo "  " & fg(grayColor) & "{VertLine} ".fmt & resetColor
            for lineNo in lineFrom..lineTo:
                var line = codeLines[lineNo]
                var pointerArrow = "{VertLineD} ".fmt
                var lineNum = $(lineNo+1)
                var lineNumPre = ""
                var lineNumPost = ""
                if lineNo == e.context.line-1: 
                    pointerArrow = "{VertLineD}".fmt & fg(redColor) & "{ArrowRight}".fmt & fg(grayColor)
                    line = bold(grayColor) & line & fg(grayColor)
                    lineNumPre = bold(grayColor)
                    lineNumPost = fg(grayColor)
                echo "  " & fg(grayColor) & "{VertLine} ".fmt & lineNumPre & align(lineNum,4) & lineNumPost & " {pointerArrow} ".fmt & line & resetColor

proc printHint(e: VError) =
    if e.hint != "":
        let wrappingWidth = min(100, int(0.8 * float(getMaxWidth() - 2 - 6)))
        let hinter = "  " & "\e[4;97m" & "Hint" & resetColor() & ": "
        echo ""
        if e.hint.contains("\n"):
            echo (hinter & "$$") ~~ @[e.hint.processPseudomarkdown()]
        else:
            echo hinter & wrapped(strip(dedent(e.hint)).splitLines().join(" "), wrappingWidth, delim="\n        ")

#=======================================
# Methods
#=======================================

proc showError*(e: VError) =
    with e:
        printErrorHeader()
        printErrorKindDescription()
        printErrorMessage()
        printCodePreview()
        printHint()

    if (not isRepl()) or e.hint=="":
        echo ""

func panic(error: VError) =
    raise error

proc panic(throw: bool, error: VError) =
    if throw:
        panic(error)
    else:
        showError(error)
        quit(1)

#=======================================
# Constructors
#=======================================

#------------------------
# Command-line Errors
#------------------------

proc Error_ScriptNotExists*(name: string) =
    panic(false):
        toError CmdlineErr, """
            Given script doesn't exist:
                _$#_
        """ ~~ @[name]

proc Error_UnrecognizedOption*(name: string) =
    panic(false):
        toError CmdlineErr, """
            Unrecognized command-line option:
                _$#_
        """ ~~ @[name]

proc Error_UnrecognizedPackageCommand*(name: string) =
    panic(false):
        toError CmdlineErr, """
            Unrecognized _package_ command:
                _$#_
        """ ~~ @[name]

proc Error_NoPackageCommand*() =
    panic(false):
        toError CmdlineErr, """
            No _package_ command command given -
            have a look at the options below
        """

proc Error_ExtraneousParameter*(subcmd: string, name: string) =
    panic(false): 
        toError CmdlineErr, """
            Extraneous parameter for _$#_:
                $#
        """ ~~ @[subcmd, name]

proc Error_NotEnoughParameters*(name: string) =
    panic(false):
        toError CmdlineErr, """
            Not enough parameters for _$#_ -
            consult the help screen below
        """ ~~ @[name]

#------------------------
# Package Errors
#------------------------

func Error_PackageNotFound*(pkg: string) =
    panic:
        toError PackageErr, """
            Package not found:
                _$#_
        """ ~~ @[pkg]

func Error_PackageRepoNotCorrect*(repo: string) =
    panic:
        toError PackageErr, """
            Package repository url not correct:
                $#
        """ ~~ @[repo]

func Error_PackageRepoNotFound*(repo: string) =
    panic:
        toError PackageErr, """
            Package repository not found:
                $#
        """ ~~ @[repo]

func Error_CorruptRemoteSpec*(pkg: string) =
    panic:
        toError PackageErr, """
            Corrupt spec file for remote package:
                _$#_
        """ ~~ @[pkg]

func Error_PackageNotValid*(pkg: string) =
    panic:
        toError PackageErr, """
            Invalid package:
                _$#_
        """ ~~ @[pkg]

func Error_PackageUnknownError*(pkg: string) =
    panic:
        toError PackageErr, """
            Unexpected error while installing package:
                _$#_
        """ ~~ @[pkg]

func Error_PackageInvalidVersion*(vers: string) =
    panic:
        toError PackageErr, """
            Error parsing package version:
                _$#_
        """ ~~ @[vers]

#------------------------
# Conversion Errors
#------------------------

func Error_CannotConvert*(arg,fromType,toType: string) =
    panic:
        toError ConversionErr, """
            Got value:
                $$

            Conversion to given type is not supported:
                :$#
        """ ~~ @[arg, toType.toLowerAscii()]

func Error_ConversionFailed*(arg,fromType,toType: string, hint: string="") =
    panic:
        toError ConversionErr, """
            Got value:
                $$

            Failed while trying to convert to:
                :$#
        """ ~~ @[arg, toType.toLowerAscii()], hint

func Error_CannotConvertDifferentDimensions*(convFrom, convTo: string) =
    panic:
        toError ConversionErr, """
            Trying to convert quantity with property:
                $#

            To:
                $#
        """ ~~ @[convFrom, convTo]

#------------------------
# Syntax Errors
#------------------------

func Error_MissingClosingSquareBracket*() =
    panic:
        toError SyntaxErr, """
            Issue found when trying to parse:
                Block

            Missing:
                closing square bracket (`]`)
        """ ~~ @[]

func Error_MissingClosingParenthesis*() =
    panic:
        toError SyntaxErr, """
            Issue found when trying to parse:
                Inline

            Missing:
                closing parenthesis (`)`)
        """ ~~ @[]

func Error_StrayClosingSquareBracket*() =
    panic:
        toError SyntaxErr, """
            Found extraneous block symbol:
                closing square bracket (`]`)
        """ ~~ @[]

func Error_StrayClosingCurlyBracket*() =
    panic: 
        toError SyntaxErr, """
            Found extraneous block symbol:
                closing curly bracket (`}`)
        """ ~~ @[]

func Error_StrayClosingParenthesis*() =
    panic:
        toError SyntaxErr, """
            Found extraneous block symbol:
                closing parenthesis (`)`)
        """ ~~ @[]

func Error_UnterminatedString*(strtype: string) =
    var strt = strtype
    if strt!="": strt &= " "
    var missing: string
    if strt=="":
        missing = "closing double quote (`\"`)"
    elif strt=="verbatim":
        missing = "closing color-bracket (`:}`)"
    else:
        missing = "closing curly bracket (`}`)"
    panic:
        toError SyntaxErr, """
            Issue found when trying to parse:
                String

            Missing:
                $$
        """ ~~ @[missing]

func Error_NewlineInQuotedString*() =
    panic:
        toError SyntaxErr, """
            Issue found when trying to parse:
                String

            Quoted string contains:
                newline (`\n`)
        """ ~~ @[], "For multiline strings, you could use either: curly blocks `{..}` or triple `-` templates"

#------------------------
# Assertion Errors
#------------------------

func Error_AssertionFailed*(context: string) =
    panic:
        toError AssertionErr, """
            Tried:
                $$
        """ ~~ @[context]

func Error_AssertionFailed*(context: string, message: string) =
    panic: 
        toError AssertionErr, """
            Unable to ensure:
                $$

            Tried: 
                $$
        """ ~~ @[message, context]

#------------------------
# Arithmetic Errors
#------------------------

func Error_IntegerParsingOverflow*(number: string) =
    let hint = "Up to $#-bit integers supported" ~~ @[MaxIntSupported]
    panic: 
        toError ArithmeticErr, """
            Couldn't parse Integer value

            From value: 
                $$
        """ ~~ @[number], hint

func Error_IntegerOperationOverflow*(operation: string, argA, argB: string) =
    let hint = "Up to $#-bit integers supported" ~~ @[MaxIntSupported]
    panic: 
        toError ArithmeticErr, """
            Number operation overflow

            Attempted operation: 
                _$#_

            Between: 
                $$

            And:
                $$
        """ ~~ @[operation, argA, argB], hint

func Error_IntegerSingleOperationOverflow*(operation: string, arg: string) =
    let hint = "Up to $#-bit integers supported" ~~ @[MaxIntSupported]
    panic: 
        toError ArithmeticErr, """
            Number operation overflow

            Attempted operation: 
                _$#_

            With: 
                $$
        """ ~~ @[operation, arg], hint

func Error_NumberOutOfSupportedRange*(operation: string, arg: string) =
    let hint = "Up to $#-bit integers supported" ~~ @[MaxIntSupported]
    panic: 
        toError ArithmeticErr, """
            Number operator out of valid range

            Attempted operation: 
                _$#_

            With: 
                $$
        """ ~~ @[operation, arg], hint

func Error_DivisionByZero*(arg: string) =
    panic:
        toError ArithmeticErr, """
            Division by zero

            With value:
                $$
        """ ~~ @[arg]

func Error_ZeroDenominator*() =
    panic: 
        toError(ArithmeticErr, """
            Cannot create Rational value

            Denominator is zero!
        """ ~~ @[])

#------------------------
# Index Errors
#------------------------

func Error_InvalidIndex*(indx: int, value: string, hint: string = "") =
    panic:
        toError IndexErr, """
            Invalid index: 
                $$

            For value:
                $$
        """ ~~ @[$indx, value], hint

func Error_InvalidKey*(key: string, value: string, hint: string = "") =
    panic:
        toError IndexErr, """
            Invalid key: 
                $$

            For value:
                $$
        """ ~~ @[key, value], hint

func Error_OutOfBounds*(indx: int, value: string, maxRange: int, what: string = "Block") =
    var items = 
        if what=="String": "characters" 
        else: "items"

    let hint = """Given $# contains $# $#; so, a valid index should fall within 0..$#""" ~~ @[what, $(maxRange+1), items, $(maxRange)]
    panic:
        toError IndexErr, """
            Index out of bounds: 
                $$

            For value:
                $$
        """ ~~ @[$indx, value], hint

func Error_KeyNotFound*(sym: string, collection: string, alter: seq[string]) =
    let sep = "\n" & "\b\b\b\b\b\bor... "
    let hint = "Perhaps you meant... $$" ~~ @[alter.map((x) => "_" & x & "_ ?").join(sep)]
    panic:
        toError IndexErr, """
            Key not found: 
                $#

            For value:
                $$
        """ ~~ @[sym, collection], hint

func Error_UnsupportedKeyType*(keyType: string, value: string, expectedTypes: seq[string]) =
    let expected = expectedTypes.join(", ")
    let plural = 
        if expectedTypes.len > 1: "s"
        else: ""
    panic:
        toError IndexErr, """
            Unsupported key: 
                $$

            For value:
                $$

            Expected type$#:
                $$
        """ ~~ @[keyType, value, plural, expected]
#------------------------
# System Errors
#------------------------

func Error_FileNotFound*(path: string) =
    panic:
        toError SystemErr, """
            File not found: 
                $#
        """ ~~ @[path], errCode=ENOENT

#------------------------
# VM Errors
#------------------------

func Error_StackUnderflow*() =
    panic:
        toError VMErr, """
            Stack underflow
        """

func Error_ConfigNotFound*(gkey: string, akey: string) =
    panic:
        toError VMErr, """
            Configuration not found for: $#
            you can either supply it globally via `config`
            or using the option: .$#
        """ ~~ @[gkey, akey]

# TODO(VM/errors) unify all unsafe/MINI errors
#  there are different errors that are thrown when e.g.
#  the build is a MINI build, and a given feature is not
#  available, or it's the "safe"-playground version
#  and a different set of features is disabled.
#  An example of that would also be the `SqliteDisabled`
#  error; all of these should be mirror in ONE error
#  template with instructions on how to solve it, e.g.
#  install the full build
#  labels: enhancement, error handling

func Error_OperationNotPermitted*(operation: string) =
    panic:
        toError VMErr, """
            Unsafe operation: $#
            not permitted in online playground
        """ ~~ @[operation]

#------------------------
# UI Errors
#------------------------

func Error_CompatibleBrowserNotFound*() =
    panic:
        toError UIErr, """
            Could not find any Chrome-compatible browser installed
        """

func Error_CompatibleBrowserCouldNotOpenWindow*() =
    panic:
        toError UIErr, """
            Could not open a Chrome-compatible browser window
        """

#------------------------
# Name Errors
#------------------------

func Error_SymbolNotFound*(sym: string, alter: seq[string]) =
    let sep = "\n" & "\b\b\b\b\b\bor... "
    let hint = "Perhaps you meant... $$" ~~ @[alter.map((x) => "_" & x & "_ ?").join(sep)]
    panic:
        toError NameErr, """
            Identifier not found: 
                _$#_
        """ ~~ @[sym], hint

func Error_AliasNotFound*(sym: string) =
    panic: 
        toError NameErr, """
            Alias not found: 
                _$#_
        """ ~~ @[sym]

func Error_ColorNameNotFound*(color: string, alter: seq[string]) =
    let sep = "\n" & "\b\b\b\b\b\bor... "
    let hint = "Perhaps you meant... $$" ~~ @[alter.map((x) => "_" & x & "_ ?").join(sep)]
    panic:
        toError NameErr, """
            Color name not recognized: 
                _$#_
        """ ~~ @[color], hint

#------------------------
# Value Errors
#------------------------

func Error_CannotModifyConstant*(sym: string) =
    panic:
        toError ValueErr, """
            Readonly constants cannot be modified in-place

            Received: 
                $#

        """ ~~ @[sym]

func Error_PathLiteralMofifyingString*() =
    panic:
        toError ValueErr, """ 
            In-place modification of strings
            through PathLiteral values is not supported
        """

func Error_IncompatibleBlockSize*(functionName: string, got: int, expected: string) =
    panic: 
        toError ValueErr, """
            Cannot perform:
                _$#_

            Incompatible block size: 
                $#

            Expected: 
                $#
        """ ~~ @[functionName, $got, expected]

func Error_NotEnoughArguments*(functionName:string, functionArity: int) =
    panic:
        toError ValueErr, """
            Not enough parameters

            Cannot perform:
                _$#_

            Required: 
                $#
        """ ~~ @[functionName, $functionArity]

func Error_RangeWithZeroStep*() =
    panic:
        toError ValueErr, """
            Problem when creating Range

            Attribute step can't be 0!
        """

func Error_IncorrectNumberOfArgumentsForInitializer*(typeName: string, got: int, expected: seq[string]) =
    panic:
        toError ValueErr, """
            Cannot initialize object of type: 
                _:$#_

            Wrong number of parameters: 
                $#

            Expected: 
                $# $#
        """ ~~ @[typeName, $got, $(expected.len), expected.join(", ")]

func Error_MissingArgumentForInitializer*(typeName: string, missing: string) =
    panic:
        toError ValueErr, """
            Cannot initialize object of type:
                _:$#_

            Missing field: 
                $#
        """ ~~ @[typeName, missing]

func Error_IncompatibleQuantityOperation*(operation: string, argA, argB, kindA, kindB: string) =
    panic: 
        toError ValueErr, """
            Incompatible operation between quantities

            Attempted: 
                $#

            With: 
                $#
        """ ~~ @[operation, truncate(argA & " (" & kindA & ") " & argB & " (" & kindB & ")", 60)]

#------------------------
# Type Errors
#------------------------

func Error_IncompatibleValueType*(functionName: string, tp: string, expected: string) =
    panic: 
        toError TypeErr, """
            Cannot perform: 
                _$#_

            Incompatible value type for:
                $#

            Expected: 
                $#
        """ ~~ @[functionName, tp, expected]

func Error_IncompatibleBlockValue*(functionName: string, val: string, expected: string) =
    panic: 
        toError TypeErr, """
            Cannot perform:
                _$#_

            Incompatible value inside block parameter:
                $#

            Expected: 
                $#
        """ ~~ @[functionName, val, expected]

func Error_IncompatibleBlockValueAttribute*(functionName: string, attributeName: string, val: string, expected: string) =
    panic: 
        toError TypeErr, """
            Cannot perform: 
                _$#_ _$#_

            Incompatible value inside block:
                $#

            Expected: 
                $#
        """ ~~ @[functionName, attributeName, val, expected]

func Error_UsingUndefinedType*(typeName: string) =
    panic: 
        toError TypeErr, """
            Undefined or unknown type:
                _:$#_
        """ ~~ @[typeName], "Before using it, you should make sure it has been properly initialized using `define`"

func Error_UnsupportedParentType*(typeName: string) =
    panic:
        toError TypeErr, """
            Encountered type:
                _:$#_

            Subtyping is not supported!
        """ ~~ @[typeName]

func Error_WrongArgumentType*(functionName: string, actual: string, val: string, paramPos: string, accepted: string) =
    panic:
        toError TypeErr, """
            Cannot call function:
                _$#_ $$

            Wrong argument (at position $#): 
                $$

            Expected: 
                $#
        """ ~~ @[functionName, actual, paramPos, val, accepted]

func Error_WrongAttributeType*(functionName: string, attributeName: string, val: string, accepted: string) =
    panic:
        toError TypeErr, """
            Cannot call function:
                _$#_ $$

            Wrong attribute value: 
                $$

            Expected: 
                $#
        """ ~~ @[functionName, attributeName, val, accepted]

func Error_CannotStoreKey*(key: string, valueKind: string, storeKind: string) =
    panic:
        toError TypeErr, """
            Unsupported value type: $#
            For store of type: $#
            When storing key: $#
        """ ~~ @[valueKind, storeKind, key]

func Error_InvalidOperation*(operation: string, argA, argB: string) =
    if argB != "":
        panic:
            toError TypeErr, """
                Invalid operation _$#_
                Between: $#
                    and: $#
            """ ~~ @[operation, argA, argB]
    else:
        panic:
            toError TypeErr, """
                Invalid operation _$#_
                With: $#
            """ ~~ @[operation, argA, argB]

#------------------------
# Library Errors
#------------------------

func Error_SqliteDisabled*() =
    panic:
        toError LibraryErr, """
            SQLite not available in MINI builds
            if you want to have access to SQLite-related functionality,
            please, install Arturo's full version
        """

func Error_LibraryNotLoaded*(path: string) =
    panic:
        toError LibraryErr, """
            Dynamic library could not be loaded:
                $#
        """ ~~ @[path]

func Error_LibrarySymbolNotFound*(path: string, sym: string) =
    panic:
        toError LibraryErr, """
            Symbol not found: 
                $#

            In library: 
                $#
        """ ~~ @[sym, path]

func Error_ErrorLoadingLibrarySymbol*(path: string, sym: string) =
    panic:
        toError LibraryErr, """
            Error loading symbol: 
                $#

            From library: 
                $#
        """ ~~ @[sym, path]

# Program errors

func ProgramError_panic*(message: string, code: int) =
    panic:
        toError ProgramErr, message, errCode=code

# TODO Re-establish stack trace debug reports
#  labels: vm, error handling

5fb739f4cbac5d4c2083d803bf30e1d130268f8b