Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modules and imports

Harn supports splitting code across files using import and top-level fn declarations.

Importing files

import "lib/helpers.harn"

The extension is optional — these are equivalent:

import "lib/helpers.harn"
import "lib/helpers"

Import paths are resolved relative to the current file’s directory. If main.harn imports "lib/helpers", it looks for lib/helpers.harn next to main.harn.

Writing a library file

Library files contain top-level fn declarations:

// lib/math.harn

fn double(x) {
  return x * 2
}

fn clamp(value, low, high) {
  if value < low { return low }
  if value > high { return high }
  return value
}

When imported, these functions become available in the importing file’s scope.

Using imported functions

import "lib/math"

pipeline default(task) {
  log(double(21))        // 42
  log(clamp(150, 0, 100)) // 100
}

Importing pipelines

Imported files can also contain pipelines, which are registered globally by name:

// lib/analysis.harn
pipeline analyze(task) {
  log("Analyzing: ${task}")
}
import "lib/analysis"

pipeline default(task) {
  // the "analyze" pipeline is now registered and available
}

Standard library modules

Harn includes built-in modules that are compiled into the interpreter. Import them with the std/ prefix:

import "std/text"
import "std/collections"
import "std/math"
import "std/path"
import "std/json"

std/text

Text processing utilities for LLM output and code analysis:

FunctionDescription
extract_paths(text)Extract file paths from text, filtering comments and validating extensions
parse_cells(response)Parse fenced code blocks from LLM output. Returns [{type, lang, code}]
filter_test_cells(cells, target_file?)Filter cells to keep code blocks and write_file calls
truncate_head_tail(text, n)Keep first/last n lines with omission marker
detect_compile_error(output)Check for compile error patterns (SyntaxError, etc.)
has_got_want(output)Check for got/want test failure patterns
format_test_errors(output)Extract error-relevant lines (max 20)

std/collections

Collection utilities and store helpers:

FunctionDescription
filter_nil(dict)Remove entries where value is nil, empty string, or “null”
store_stale(key, max_age_seconds)Check if a store key’s timestamp is stale
store_refresh(key)Update a store key’s timestamp to now

std/math

Extended math utilities:

FunctionDescription
clamp(value, lo, hi)Clamp a value between min and max
lerp(a, b, t)Linear interpolation between a and b by t (0..1)
map_range(value, in_lo, in_hi, out_lo, out_hi)Map a value from one range to another
deg_to_rad(degrees)Convert degrees to radians
rad_to_deg(radians)Convert radians to degrees
sum(items)Sum a list of numbers
avg(items)Average of a list of numbers (returns 0 for empty lists)
import "std/math"

log(clamp(150, 0, 100))         // 100
log(lerp(0, 10, 0.5))           // 5
log(map_range(50, 0, 100, 0, 1)) // 0.5
log(sum([1, 2, 3, 4]))          // 10
log(avg([10, 20, 30]))          // 20

std/path

Path manipulation utilities:

FunctionDescription
ext(path)Get the file extension without the dot
stem(path)Get the filename without extension
normalize(path)Normalize path separators (backslash to forward slash)
is_absolute(path)Check if a path is absolute
list_files(dir)List files in a directory (one level)
list_dirs(dir)List subdirectories in a directory
import "std/path"

log(ext("main.harn"))          // "harn"
log(stem("/src/main.harn"))    // "main"
log(is_absolute("/usr/bin"))   // true

let files = list_files("src")
let dirs = list_dirs(".")

std/json

JSON utility patterns:

FunctionDescription
pretty(value)Pretty-print a value as indented JSON
safe_parse(text)Safely parse JSON, returning nil on failure instead of throwing
merge(a, b)Shallow-merge two dicts (keys in b override keys in a)
pick(data, keys)Pick specific keys from a dict
omit(data, keys)Omit specific keys from a dict
import "std/json"

let data = safe_parse("{\"x\": 1}")   // {x: 1}, or nil on bad input
let merged = merge({a: 1}, {b: 2})    // {a: 1, b: 2}
let subset = pick({a: 1, b: 2, c: 3}, ["a", "c"])  // {a: 1, c: 3}
let rest = omit({a: 1, b: 2, c: 3}, ["b"])          // {a: 1, c: 3}

Selective imports

Import specific functions from any module:

import { extract_paths, parse_cells } from "std/text"

Import behavior

  1. The imported file is parsed and executed
  2. Pipelines in the imported file are registered by name
  3. Non-pipeline top-level statements (fn declarations, let bindings) are executed, making their values available
  4. Circular imports are detected and skipped (each file is imported at most once)
  5. The working directory is temporarily changed to the imported file’s directory, so nested imports resolve correctly

Pipeline inheritance

Pipelines can extend other pipelines:

pipeline base(task) {
  log("Step 1: setup")
  log("Step 2: execute")
  log("Step 3: cleanup")
}

pipeline custom(task) extends base {
  override fn setup() {
    log("Custom setup")
  }
}

If the child pipeline has override declarations, the parent’s body runs with the overrides applied. If the child has no overrides, the child’s body replaces the parent’s entirely.

Organizing a project

A typical project structure:

my-project/
  main.harn
  lib/
    context.harn      # shared context-gathering functions
    agent.harn        # shared agent utility functions
    helpers.harn      # general-purpose utilities
// main.harn
import "lib/context"
import "lib/agent"
import "lib/helpers"

pipeline default(task, project) {
  let ctx = gather_context(task, project)
  let result = run_agent(ctx)
  finalize(result)
}