A view tells microscope what one of your types stands for: a vector as its elements, a tagged union as the value it holds, a queue as the tasks on it. This tutorial writes views for a small C program and a Rust one, both in microscope's own tests, so everything here runs. Views is the reference for the language and says where microscope finds view files; the programs here carry their views themselves.
The program keeps integers in a vector of its own:
// A vector of integers: `n` of them at `data`, with room for `cap`.
typedef struct {
int *data;
size_t n;
size_t cap;
} intvec;Printed as stored, a vector shows a pointer and two numbers. Its view says what they mean:
# A vector of integers: `n` of them at `data`, with room for `cap`.
view c intvec {
check n <= cap
show sequence(n) for i in range(n) => data[i]
field capacity = cap
}
view c intvecnames the type: its language, then a pattern of its name.check n <= capstates what must hold of a vector that is whole. One that breaks it shows as stored, with the check that failed, never as a plausible vector of nine elements.show sequence(n) for i in range(n) => data[i]says the vector isnelements, theith atdata[i].field capacity = capadds a child beside the elements.
(intvec) numbers = len=3 [10, 20, 30]
(intvec) broken = {data = 0x00007fffffff74b0, n = 9, cap = 4} <view tutorial.views[0]:4 `c intvec`: check `n <= cap` failed: `n` is 9, `cap` is 4>
numbers[1] and len(numbers) now work in any expression, such as a
breakpoint's condition: break push if len(numbers) > 100.
A value holds one of three kinds of thing, which its kind says:
// A value of one of three kinds.
enum kind { VAL_INT, VAL_STR, VAL_NIL };
typedef struct {
enum kind kind;
union {
long i;
struct {
const char *ptr;
size_t len;
} s;
} u;
} value;A match chooses what to show by the kind:
# A tagged union: what it holds depends on its kind.
view c value {
show match kind {
VAL_INT => value(u.i)
VAL_STR => text(u.s.ptr, u.s.len)
VAL_NIL => empty("nil")
}
}
value(u.i) shows the integer as the value, text shows the characters
the string points to, and empty shows a value that holds nothing. A kind
no arm names is not guessed at: the value shows as stored, saying what its
kind was.
(value) count = 42
(value) name = "microscope"
(value) nothing = nil
Tasks wait on a run queue through a node each task embeds, as the Linux kernel's lists do:
// Tasks on a run queue, linked through the node each embeds.
struct list_head {
struct list_head *next, *prev;
};
struct task {
int pid;
struct list_head run_node;
};
struct run_queue {
struct list_head tasks;
size_t nr;
};The queue's view walks the nodes and finds each task from its node:
# A run queue: its tasks, each found from the node that links it.
view c run_queue {
show sequence(nr) for n in list(&tasks, p => p->next) if n != &tasks
=> container_of(n, task, run_node)->pid
}
list(&tasks, p => p->next)is the nodes from the queue's own, each node'snextafter it, until it comes back around.if n != &tasksleaves out the queue's own node, which is no task's.container_of(n, task, run_node)is the task whoserun_nodenis.
(run_queue) queue = len=2 [7, 8]
A list that leads back to a node it has already shown, or holds fewer
nodes than nr says, shows as a problem rather than a list.
This program carries its views, so whoever debugs it gets them:
#include "microscope_views.h"
MICROSCOPE_VIEWS_FILE("tests/fixtures/c/tutorial/tutorial.views");microscope_views.h is in microscope's sdk/c. The path is read when the
program is built, relative to where the compiler runs.
The program keeps a tree whose nodes keep their children in a list:
// A tree whose nodes keep their children in a list.
typedef struct node {
int value;
struct node *child;
struct node *sibling;
} node;
struct tree {
node *root;
size_t count;
};No generator walks it, each node before its children: list follows one
link and inorder two, and this walk needs a stack of the siblings still
to come. A kernel walks it instead, yielding each node's address:
#include "microscope_kernel.h"
// The deepest tree it walks: a node waits here for each ancestor whose next
// sibling is still to come.
#define MAX_DEPTH 64
MICROSCOPE_KERNEL_EXPORT int32_t run(const uint64_t *arguments, int32_t count) {
if (count != 3)
return 1;
uint64_t child = arguments[1];
uint64_t sibling = arguments[2];
uint64_t waiting[MAX_DEPTH];
int depth = 0;
uint64_t node = arguments[0];
while (node != 0) {
if (!microscope_yield(&node, 1))
return 0;
uint64_t first = microscope_load_u64(node + child);
uint64_t next = microscope_load_u64(node + sibling);
if (first == 0) {
node = next;
} else {
if (next != 0) {
if (depth == MAX_DEPTH)
return 2;
waiting[depth++] = next;
}
node = first;
}
if (node == 0 && depth > 0)
node = waiting[--depth];
}
return 0;
}microscope_kernel.h, in microscope'ssdk/c, declaresmicroscope_readand the loads built on it, which read the program's memory, andmicroscope_yield, which yields an item of words and says whether microscope wants another.runtakes the view's arguments, here the root and where a node keeps its links, and returns 0 when it is done. Anything else is a failure, which shows the value as stored, with the numberrunreturned.zig cc --target=wasm32-freestanding -Os -nostdlib -Wl,--no-entry -Isdk/c tree.c -o tree.wasmbuilds it, as doesclang --target=wasm32with the same options.
The view calls the kernel by name, and presents each node it yields:
# A tree whose nodes keep their children in a list. No generator walks it,
# since that takes a stack, so the tree kernel the program carries does,
# yielding each node before its children.
view c tree {
show sequence(count) for at in kernel("tree", root, offsetof(node, child), offsetof(node, sibling))
=> ((node *)at)->value
}
(tree) family = len=5 [1, 2, 3, 4, 5]
The program carries the kernel with its views, and its source with it, so
that microscope views check shows the kernel as the source it is built from:
MICROSCOPE_KERNEL("tree", "tests/fixtures/c/tutorial/tree.c", "tutorial-tree.wasm");Zig kernels use sdk/zig/microscope_kernel.zig, and Rust ones the
microscope-views crate's kernel module, as microscope's test program in
tests/fixtures/rust/embedded-views does for the same tree.
views record runs.txt family records the kernel runs that present
family, and microscope views replay runs.txt --kernel tree.wasm runs a new
build of the kernel on them, saying where it first does something else.
In Rust, the microscope-views crate in microscope's sdk/rust does the same:
microscope_views::microscope_views_file!("tests/fixtures/rust/embedded-views/main.views");
/// Tags, which their view presents as the names they hold.
pub struct Tags {
names: Vec<&'static str>,
}
/// A temperature in degrees Celsius.
pub struct Celsius(f64);# Tags, as the names they hold.
view rust embedded_views::Tags {
show value(names)
}
# A temperature, a newtype around its degrees.
view rust embedded_views::Celsius {
show value(self.0)
summary "{self.0}°C"
}
value(names) shows the tags as the Vec they hold, which the built-in
view of Vec presents, so Tags has elements of its own. summary
writes the one line a value shows as.
(Tags) tags = len=2 ["red", "green"]
(Celsius) temperature = 21.5°C
A view names a Rust type by its path from its crate, here
embedded_views.
A view that names a member the type lacks, or an expression that does not
make sense for its members, does not bind, and the next view whose pattern
names the type is tried. microscope views explain PROGRAM TYPE says, without
running the program, which view presents a type and why each one before it
did not bind:
$ microscope views explain build/test-programs/tutorial intvec
intvec in /…/build/test-programs/tutorial
presented by tutorial.views[0]:4 `c intvec`
views tried, in order:
tutorial.views[0]:4 `c intvec`: binds
microscope views check PROGRAM does so for every type a view names, and fails
when a view you gave it binds nothing, so it can guard a project's views in
its own tests.
microscope's own views are the files in views/, one per library. Contributing
one is two changes: the view, and a line in that language's containers
fixture, under tests/fixtures, that declares a value of the type and says
what it must show:
std::vector<int> ints = {1, 2, 3}; // VIEW: ints => len=3 [1, 2, 3]The tests check every marker in every build of the fixture, and that every
built-in view presents some marked value. Go's other library types are in
tests/fixtures/go/stdlib, which prints each marker as the program runs,
from what Go itself says of the value, such as its String method's text,
so the markers stay true when the toolchain moves.