How do I debug an EK9 program?

← Debugging and Troubleshooting · Ref: Q246

EK9 provides several strategies for finding and preventing bugs, emphasising compile-time detection over runtime debugging. The compiler catches the majority of defects before your program ever runs.

COMPILE-TIME DEFECT PREVENTION

The compiler is your primary debugging tool. EK9's type system, flow analysis, and enforced guard patterns catch 80-90% of defects at compile time. Error messages include the file, line, column, and a specific error code. Use 'ek9 -E1' for visual error display with source snippets and carets pointing to the exact location. Use 'ek9 -h EXXXXX' to look up any error code for a detailed explanation.

REQUIRE AND ASSERT

The 'require' keyword validates preconditions at the start of methods and functions. It is always active and cannot be disabled:

  require value?
  require count > 0

If the condition fails, an exception is thrown with the source location. Use 'assert' for postconditions and invariants inside method bodies.

DIAGNOSTIC OUTPUT

Use stdout.println() to print values during development. The $ operator converts any value to a string, and string interpolation with backtick strings makes this easy:

  stdout.println(`Debug: value=${value}, count=${count}`)

DEBUG INSTRUMENTATION (-cg flag)
Compile with 'ek9 -cg' to enable debug instrumentation. This embeds source file, line, and column information into assertion messages and exception stack traces. Without -cg, assertion failures show generic messages. With -cg, they show exact source locations like './myfile.ek9:42:5'.

BLACK-BOX TESTING

EK9 has built-in black-box testing. Create an expected_output.txt file alongside your .ek9 file. Run with 'ek9 -t' and the test runner compares actual output against expected output line by line. This catches regressions without writing any test assertions.

TEST DIRECTIVE

Mark methods with @Test to create unit tests. Use assert, assertThrows, and assertDoesNotThrow inside test methods. Run all tests with 'ek9 -t myfile.ek9'.

LANGUAGE SERVER (LSP)

Run 'ek9 -ls' for real-time error detection in your editor. The LSP provides hover documentation, go-to-definition, and completions. Errors appear as you type, before you even save the file.

PLATFORM DEBUGGER

A platform debugger (edb) is planned but not yet available. The compiler generates JSR-45 SMAP data and LocalVariableTable entries so that standard JVM debuggers can step through EK9 source code. In the meantime, use diagnostic output and the testing framework.

See Q134 for try/catch error handling. See Q29 for tri-state (unset variable) semantics. See Q155 for writing unit tests. See Q247 for reading error messages. See Q252 for verbose compilation modes. See Q322 for profiling to find performance bottlenecks.

Example

defines module qa.debugging.program

  defines function

    validateScore() as pure
      -> score as Integer
      <- rtn as Boolean: false

      require score?
      require score >= 0
      require score <= 100
      rtn: true

  defines class

    GameScore
      points <- Integer()

      GameScore()
        -> initialPoints as Integer
        require initialPoints?
        require initialPoints >= 0
        this.points :=: initialPoints

      addPoints()
        -> amount as Integer
        require amount?
        require amount > 0
        points += amount

      operator $ as pure
        <- rtn as String: `GameScore(${points})`

      override operator ? as pure
        <- rtn as Boolean: points?

  defines program

    DebuggingDemo()
      stdout <- Stdout()

      // Diagnostic output with $ operator
      score <- GameScore(10)
      stdout.println(`Initial: ${score}`)

      score.addPoints(5)
      stdout.println(`After adding 5: ${score}`)

      // Validation with require
      valid <- validateScore(85)
      stdout.println(`Score 85 valid: ${valid}`)

      // Try/catch for error handling
      try
        badScore <- GameScore(-1)
        stdout.println(`Should not reach: ${badScore}`)
      catch
        -> ex as Exception
        stdout.println(`Caught: ${ex.reason()}`)

      // Guard pattern for safe access
      parsed <- Integer("42")
      if parsed?
        stdout.println(`Parsed value: ${parsed}`)

      badParsed <- Integer("not a number")
      if badParsed?
        stdout.println("Should not print")
      else
        stdout.println("Invalid input detected via ? check")

Common mistakes

E50001 — If a declared variable is never referenced after assignment, the compiler rejects it. Every variable must be used. See ek9 -h E50001 for details.

Incorrect:

unused <- validateScore(85)

Correct:

valid <- validateScore(85)
Other ways to ask this
  • What debugging tools does EK9 provide?
  • How do I find bugs in my EK9 code?
  • What is the EK9 debugging workflow?

Coming from another language?

Java: IDE debuggers (IntelliJ, Eclipse), System.out.println, JUnit assertions, stack traces. Python: pdb debugger, print(), pytest, traceback. Rust: dbg!() macro, println!(), cargo test, LLDB/GDB. Go: fmt.Println, Delve debugger, go test. C++: GDB/LLDB, cerr, assert macro. Kotlin: IDE debuggers, println, JUnit. EK9: require/assert always active, stdout.println with $ operator, -cg debug instrumentation, built-in black-box testing, LSP real-time errors, platform debugger planned.

Keywords: println, debugging, diagnostic, error, lsp, troubleshoot, assert, trace, require, program, error-message, output, bug, debug, instrumentation, testing