Oak

debug.oak

← Standard library See on GitHub ↗

// libdebug contains utilities useful for debugging and inspecting runtime
// values of Oak programs.

{
	println: stdPrintln
	default: default
	range: range
	toHex: toHex
	map: map
	each: each
	some: some
	every: every
	values: values
	reduce: reduce
	entries: entries
} := import('std')
{
	letter?: letter?
	digit?: digit?
	join: join
	padStart: padStart
} := import('str')
math := import('math')
{
	sort!: sort!
} := import('sort')
{
	format: format
} := import('fmt')

// _validIdent? reports if the given string is a valid name for an Oak
// identifier. It's used to check if a string can appear without quotes as an
// object key, or if it must be displayed as a string.
fn _validIdent?(s) s |> every(fn(c, i) if i {
	0 -> letter?(c) | c = '_' | c = '?' | c = '!'
	_ -> letter?(c) | digit?(c) | c = '_' | c = '?' | c = '!'
})

// _numeral? reports if the given string represents a positive integer, used
// for similar purposes to _validIdent?.
fn _numeral?(s) s |> every(digit?)

// _primitive? reports whether the given Oak value x is of a primitive or
// function type, or a composite type composed of other Oak values.
fn _primitive?(x) if type(x) {
	:null, :empty, :bool, :int, :float, :string, :atom
	// functions are considered "primitives" for the purpose of
	// inspect-printing because they're printed as `fn { ... }`
	:function -> true
	_ -> false
}

// inspect is a utility to pretty-print Oak data structures. Unlike the
// string() builtin (used in std.println), inspect formats its input with
// customizable, nested indentation and spacing for readability in an output
// log or console output.
fn inspect(x, options) {
	{
		indent: indentUnit
		depth: depth
		maxLine: maxLine
		maxList: maxList
		maxObject: maxObject
	} := options |> default({})

	indentUnit := indentUnit |> default('  ')
	depth := depth |> default(-1)
	maxLine := maxLine |> default(80)
	maxList := maxList |> default(16)
	maxObject := maxObject |> default(3)

	fn inspectObjectKey(key) if {
		_validIdent?(key), _numeral?(key) -> key
		_ -> inspectLine(key, -1)
	}

	fn inspectAbbreviated(x) if type(x) {
		:list -> '[ {{0}} items... ]' |> format(len(x))
		:object -> '{ {{0}} entries... }' |> format(len(x))
	}

	fn inspectLine(x, depth) if type(x) {
		:null, :empty, :bool, :int, :float -> string(x)
		:string -> '\'' + (x |> map(fn(c) if c {
			'\\' -> '\\\\'
			'\'' -> '\\\''
			'\n' -> '\\n'
			'\r' -> '\\r'
			'\f' -> '\\f'
			'\t' -> '\\t'
			_ -> if {
				// hex-format non-printable bytes
				codepoint(c) < 32
				codepoint(c) > 126 -> '\\x' << toHex(codepoint(c)) |> padStart(2, '0')
				_ -> c
			}
		})) + '\''
		:atom -> if _validIdent?(payload := string(x)) {
			true -> ':' + string(payload)
			_ -> 'atom({{0}})' |> format(inspectLine(payload))
		}
		:function -> 'fn { ... }'
		:list -> '[' + (x |> map(fn(y) inspectLine(y, depth)) |> join(', ')) + ']'
		:object -> if len(x) {
			0 -> '{}'
			_ -> '{ ' + {
				entries(x) |>
					sort!(0) |>
					map(fn(entry) inspectObjectKey(entry.0) + ': ' + inspectLine(entry.1, depth)) |>
					join(', ')
			} + ' }'
		}
	}

	fn inspectMulti(x, indent, depth) {
		innerIndent := indent + indentUnit
		if type(x) {
			:list -> x |> reduce('[', fn(lines, item) {
				lines << '\n' + innerIndent + inspectAny(item, innerIndent, depth)
			}) << '\n' + indent + ']'
			:object -> entries(x) |> sort!(0) |> reduce('{', fn(lines, entry) {
				lines << '\n' + innerIndent + inspectObjectKey(entry.0) + ': ' +
					inspectAny(entry.1, innerIndent, depth)
			}) << '\n' + indent + '}'
		}
	}

	fn inspectAny(x, indent, depth) {
		line := inspectLine(x, depth - 1)
		overflows? := len(line) + len(indent) > maxLine
		if {
			_primitive?(x) -> line
			depth = 0 -> inspectAbbreviated(x)
			overflows? -> inspectMulti(x, indent, depth - 1)
			type(x) = :list -> if {
				len(x) > maxList
				x |> some(fn(y) !_primitive?(y)) -> inspectMulti(x, indent, depth - 1)
				_ -> line
			}
			type(x) = :object -> if {
				len(x) > maxObject
				x |> values() |> some(fn(y) !_primitive?(y)) -> inspectMulti(x, indent, depth - 1)
				_ -> line
			}
		}
	}

	inspectAny(x, '', depth)
}

// println is a shorthand function to print the output of `inspect`. Note that
// debug.println is not a drop-in replacement for std.println, because
// std.println is variadic, but debug.println takes one argument and one
// (optional) options object.
fn println(x, options) stdPrintln(inspect(x, options))

// bar draws a histogram bar as a Unicode string, with 1 character representing
// a value of "1". Very small but non-zero values are rounded up to the
// smallest representable unit, as distinguishing them from zero is usually
// more important at that scale than quantitative correctness.
//
// bar is used to render a histogram in histo.
fn bar(n) {
	n := math.max(n * 8, 0)
	whole := int(n / 8)
	rem := n % 8
	graph := range(whole) |> map(fn '█') |> join() + if math.round(rem) {
		0 -> ''
		1 -> '▏'
		2 -> '▎'
		3 -> '▍'
		4 -> '▌'
		5 -> '▋'
		6 -> '▊'
		7 -> '▉'
		8 -> '█'
	}
	if graph = '' & n > 0 {
		true -> '▏'
		_ -> graph
	}
}


// histo draws a histogram from a given list of numbers using Unicode symbols,
// using bar to draw each bar.
fn histo(xs, opts) if len(xs) {
	0 -> ''
	_ -> {
		opts := opts |> default({})
		min := opts.min |> default(math.min(xs...))
		max := opts.max |> default(math.max(xs...))
		bars := opts.bars |> default(10) |> math.min(len(xs)) |> math.max(1)
		label := opts.label |> default(?)
		cols := opts.cols |> default(80)
		unit := (max - min) / bars

		buckets := range(bars) |> map(fn 0)
		xs |> each(fn(x) if x >= min & x < max -> {
			i := int((x - min) / unit)
			buckets.(i) := buckets.(i) + 1
		})
		maxcount := math.max(buckets...)

		labels := buckets |> map(string)
		maxlen := math.max(labels |> map(len)...)
		if label = :start -> labels := labels |> map(fn(l) l |> padStart(maxlen, ' '))
		buckets |> map(fn(n, i) {
			b := n |> math.scale(0, maxcount, 0, cols) |> bar()
			if label {
				:start -> labels.(i) + ' ' + b
				:end -> b + ' ' + labels.(i)
				_ -> b
			}
		}) |> join('\n')
	}
}