Skip to content

Commit

Permalink
Keep track of callstack (#502)
Browse files Browse the repository at this point in the history
Whenever a function is called, a 64-bit integer value is pushed onto an
"array" - this array is actually just a stack-allocated global of bytes
that is meant to emulate the callstack. The 64-bit integer represents an
encoding of the function being called, the file in which it was called,
and the line within that file on which it was called. After the function
returns, the value is "popped" from this array.
  • Loading branch information
kengorab authored Nov 16, 2024
1 parent 6282840 commit 11686ea
Show file tree
Hide file tree
Showing 8 changed files with 559 additions and 110 deletions.
29 changes: 16 additions & 13 deletions projects/compiler/example.abra
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import foo from "process"
import "process" as process

println(foo())
func foo() {
print("hello ")
bar()
}

var capturedInt = 11
type FooWithCaptures {
i: Int
func bar() {
print("world")
baz()
}

func foo_(self): Int = self.i + capturedInt
func foo2(self, a = capturedInt): Int = self.i + a
func fooStatic(): Int = capturedInt
func baz() {
println("!")
println(process.callstack())
}

val fooWithCaptures = FooWithCaptures(i: 12)
/// Expect: 23
println(fooWithCaptures.foo_())
capturedInt = 17
println(capturedInt)
val arr = [1].map((i, _) => {
foo()
i + 1
})
394 changes: 309 additions & 85 deletions projects/compiler/src/compiler.abra

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions projects/compiler/src/qbe.abra
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,23 @@ export type ModuleBuilder {

val dataName = name ?: "_${self._data.length}"
val data = QbeData(name: dataName, align: None, kind: QbeDataKind.String(string))
val ptr = self.addData(data)
val (ptr, _) = self.addData(data)
self._globalStrs[string] = ptr

ptr
}

func addData(self, data: QbeData): Value {
func addData(self, data: QbeData): (Value, Int) {
val idx = self._data.length
self._data.push(data)
Value.Global(data.name, QbeType.Pointer)
val dataGlobal = Value.Global(data.name, QbeType.Pointer)
(dataGlobal, idx)
}

func setData(self, idx: Int, newData: QbeDataKind) {
if self._data[idx] |data| {
data.kind = newData
}
}
}

Expand All @@ -72,6 +80,7 @@ export enum QbeDataKind {
Zeros(size: Int)
Constants(values: (QbeType, Value)[])
String(str: String)
Strings(strings: String[])

func encode(self, file: File) {
match self {
Expand All @@ -91,6 +100,12 @@ export enum QbeDataKind {
QbeDataKind.String(str) => {
file.write("b \"$str\", b 0")
}
QbeDataKind.Strings(strings) => {
for str, idx in strings {
file.write("b \"$str\", b 0, ")
}
file.write("b 0")
}
}
}
}
Expand Down Expand Up @@ -802,6 +817,7 @@ export enum Value {
Global(name: String, ty: QbeType)
Int32(value: Int)
Int(value: Int)
IntU64(value: Int)
Float(value: Float)

func encode(self, file: File) {
Expand All @@ -811,6 +827,7 @@ export enum Value {
Value.Int32(value) => file.write(value.toString())
Value.Int(value) => file.write(value.toString())
Value.Float(value) => file.write("d_$value")
Value.IntU64(value) => file.write(value.unsignedToString())
}
}

Expand All @@ -819,6 +836,7 @@ export enum Value {
Value.Global(_, ty) => ty
Value.Int32 => QbeType.U32
Value.Int => QbeType.U64
Value.IntU64 => QbeType.U64
Value.Float => QbeType.F64
}

Expand All @@ -828,6 +846,7 @@ export enum Value {
Value.Global(name, _) => "\$$name"
Value.Int32(v) => v.toString()
Value.Int(v) => v.toString()
Value.IntU64(v) => v.unsignedToString()
Value.Float(v) => v.toString()
}
}
Expand Down
74 changes: 74 additions & 0 deletions projects/compiler/test/compiler/process_callstack.abra
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import "process" as process

func foo() {
print("hello ")
bar()
}

func bar() {
print("world")
baz()
}

func baz() {
println("!")
println(process.getStackTrace())
}

val arr = [1].map((i, _) => {
foo()
i + 1
})

/// Expect: hello world!
/// Expect: Stack trace:
/// Expect: at getStackTrace (%TEST_DIR%/compiler/process_callstack.abra:15)
/// Expect: at baz (%TEST_DIR%/compiler/process_callstack.abra:10)
/// Expect: at bar (%TEST_DIR%/compiler/process_callstack.abra:5)
/// Expect: at foo (%TEST_DIR%/compiler/process_callstack.abra:19)
/// Expect: at <expression> (%STD_DIR%/prelude.abra:592)
/// Expect: at Array.map (%TEST_DIR%/compiler/process_callstack.abra:18)

type OneTwoThreeIterator {
_count: Int = 1

func next(self): Int? {
val v = self._count
if v > 3 {
println(process.getStackTrace())
None
} else {
self._count += 1
return Some(v)
}
}
}

val iter = OneTwoThreeIterator()
for i in iter {
println(i)
}

/// Expect: 1
/// Expect: 2
/// Expect: 3
/// Expect: Stack trace:
/// Expect: at getStackTrace (%TEST_DIR%/compiler/process_callstack.abra:38)
/// Expect: at OneTwoThreeIterator.next (%TEST_DIR%/compiler/process_callstack.abra:48)

func returnOneButAlsoPrintStackTraceForSomeReason(): Int {
println(process.getStackTrace())
1
}

type TypeWithFieldInitializer {
i: Int = returnOneButAlsoPrintStackTraceForSomeReason()
}

val _ = TypeWithFieldInitializer(i: 14)
val _ = TypeWithFieldInitializer()

/// Expect: Stack trace:
/// Expect: at getStackTrace (%TEST_DIR%/compiler/process_callstack.abra:60)
/// Expect: at returnOneButAlsoPrintStackTraceForSomeReason (%TEST_DIR%/compiler/process_callstack.abra:65)
/// Expect: at TypeWithFieldInitializer (%TEST_DIR%/compiler/process_callstack.abra:69)
3 changes: 2 additions & 1 deletion projects/compiler/test/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,8 @@ const COMPILER_TESTS = [
{ test: "compiler/sets.abra" },
{ test: "compiler/match.abra" },
{ test: "compiler/try.abra" },
{ test: 'compiler/process.abra', args: ['-f', 'bar', '--baz', 'qux'], env: { FOO: 'bar' } }
{ test: "compiler/process.abra", args: ['-f', 'bar', '--baz', 'qux'], env: { FOO: 'bar' } },
{ test: "compiler/process_callstack.abra" },
]

async function main() {
Expand Down
2 changes: 2 additions & 0 deletions projects/compiler/test/test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class TestRunner {
if (!match) return null

const expectation = match[1]
.replaceAll('%TEST_DIR%', __dirname)
.replaceAll('%STD_DIR%', process.env.ABRA_HOME)
return [idx + 1, expectation]
})
.filter(line => !!line)
Expand Down
12 changes: 12 additions & 0 deletions projects/std/src/_intrinsics.abra
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ export func argc(): Int
@Intrinsic("argv")
export func argv(): Pointer<Pointer<Byte>>

@Intrinsic("__callstack")
export func callstack(): Pointer<Int>

@Intrinsic("__callstackp")
export func callstackPtr(): Int

@Intrinsic("modulenames")
export func moduleNames(): Pointer<Byte>

@Intrinsic("functionnames")
export func functionNames(): Pointer<Byte>

@Intrinsic("u64_to_string")
export func u64ToString(i: Int): String

Expand Down
130 changes: 122 additions & 8 deletions projects/std/src/process.abra
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import "./_intrinsics" as intrinsics
import Pointer, Byte from "./_intrinsics"
import "libc" as libc

export type Uname {
sysname: String
nodename: String
release: String
version: String
machine: String
}

var _args: String[]? = None
export func args(): String[] {
if _args |args| return args
Expand All @@ -36,6 +28,14 @@ export func getEnvVar(name: String): String? {
Some(String(length: len, _buffer: str))
}

export type Uname {
sysname: String
nodename: String
release: String
version: String
machine: String
}

var _uname: Uname? = None
export func uname(): Uname {
if _uname |uname| return uname
Expand Down Expand Up @@ -87,3 +87,117 @@ export func uname(): Uname {

@noreturn
export func exit(status = 1) = libc.exit(status)

export func getStackTrace(message = "Stack trace:"): String {
val frames = callstack()
val lines = [message]
// Skip first frame, which will be the call to `callstack()` itself
for frame in frames[1:] {
lines.push(" at ${frame.callee} (${frame.file}:${frame.line})")
}
lines.join("\n")
}

export type StackFrame {
callee: String
file: String
line: Int
}

export func callstack(): StackFrame[] {
val s = intrinsics.callstack()
var sp = (intrinsics.callstackPtr() / 8).floor() - 1

val moduleNames = getModuleNames()
val functionNames = getFunctionNames()

val stack = Array.withCapacity<StackFrame>(sp)
while sp >= 0 {
val frame = s.offset(sp).load()

val line = frame && 0xffff

val modId = (frame >> 16) && 0xff
val modName = if modId == 0 {
"<builtin>"
} else if moduleNames[modId - 1] |name| {
name
} else {
"unknown"
}

val fnId = (frame >> 32) && 0xff
val fnName = if fnId == 0 {
"<expression>"
} else if functionNames[fnId - 1] |name| {
name
} else {
"unknown"
}

stack.push(StackFrame(callee: fnName, file: modName, line: line))
sp -= 1
}

stack
}

var _moduleNames: String[]? = None
func getModuleNames(): String[] {
if _moduleNames |moduleNames| moduleNames

val moduleNames: String[] = []
val buf = intrinsics.moduleNames()
var idx = 0
var len = 0
while true {
val byte = buf.offset(idx).load().asInt()
if byte == 0 {
if len == 0 break

val str = String.withLength(len)
str._buffer.copyFrom(buf.offset(idx - len), len)
moduleNames.push(str)

idx += 1
len = 0
continue
}

idx += 1
len += 1
}

_moduleNames = Some(moduleNames)
moduleNames
}

var _functionNames: String[]? = None
func getFunctionNames(): String[] {
if _functionNames |functionNames| functionNames

val functionNames: String[] = []
val buf = intrinsics.functionNames()
var idx = 0
var len = 0
while true {
val byte = buf.offset(idx).load().asInt()
if byte == 0 {
if len == 0 break

val str = String.withLength(len)
str._buffer.copyFrom(buf.offset(idx - len), len)
functionNames.push(str)

idx += 1
len = 0
continue
}

idx += 1
len += 1
}

_functionNames = Some(functionNames)
functionNames
}

0 comments on commit 11686ea

Please sign in to comment.