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 the program terminates with a Panic that names the source location, for example 'Panic: Requirement failed: `v>10` at ./myfile.ek9:8:7'. A require breach is a contract failure, not an exception: try/catch cannot catch it. 'require' is also the tool for postconditions and invariants in production code. 'assert' is a test construct: it is allowed in @Test and dev source files, and is rejected in ordinary production source (E81015).

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 include debugging information: the JSR-45 source map and the line and local variable tables that let a debugger step through your .ek9 source and show its variables. Panic messages already carry the source location without -cg; -cg is what 'ek9 -d' and 'ek9 -dap' need.

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

EK9 ships a debugger. 'ek9 -dap' runs the compiler as a Debug Adapter Protocol server, so an editor such as VS Code can set breakpoints in .ek9 source, show the EK9 stack and local variables, and step over, into and out. 'ek9 -d <port>' runs a program built with debugging information (-cg) in debug mode on a port. Use diagnostic output and the testing framework alongside it.

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?

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

    // A plain predicate, so a bad value can be REJECTED rather than fataly breaching a require
    inRange() as pure
      -> score as Integer
      <- rtn as Boolean?

      lowestValid <- 0
      highestValid <- 100
      rtn: score? and score >= lowestValid and score <= highestValid

  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}`)

      // require is a CONTRACT, not an exception: a breach is a Panic that terminates the
      // program, and try/catch CANNOT catch it. So test the value BEFORE constructing.
      candidate <- -1
      if inRange(candidate)
        stdout.println(`Constructed: ${GameScore(candidate)}`)
      else
        stdout.println(`Rejected before construction: ${candidate}`)

      // try/catch is for thrown Exceptions, which ARE catchable
      try
        throw Exception("recoverable problem")
      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(`Parsed value: ${badParsed}`)
      else
        stdout.println("Invalid input detected via ? check")

Common mistakes

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

Incorrect:

unused <- validateScore(85)
      stdout.println(`Score 85 valid: true`)

Correct:

valid <- validateScore(85)
      stdout.println(`Score 85 valid: ${valid}`)
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 always active (assert only in test and dev source), stdout.println with $ operator, -cg debugging information, built-in black-box testing, LSP real-time errors, a DAP debugger (-dap, -d).

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