Skip to content

Commit

Permalink
Improve debugging of Mint values. (#704)
Browse files Browse the repository at this point in the history
  • Loading branch information
gdotdesign authored Nov 17, 2024
1 parent 64e1590 commit 942c4b6
Show file tree
Hide file tree
Showing 70 changed files with 706 additions and 315 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
crystal 1.14.0
mint 0.19.0
mint 0.20.0
nodejs 20.10.0
yarn 1.22.19
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ development-release:
crystal build src/mint.cr -o mint-dev --static --no-debug --release
mv ./mint-dev ~/.bin/

src/assets/runtime.js: $(shell find runtime/src -type f)
src/assets/runtime.js: \
$(shell find runtime/src -type f) \
runtime/index.js
cd runtime && make index

src/assets/runtime_test.js: $(shell find runtime/src -type f)
src/assets/runtime_test.js: \
$(shell find runtime/src -type f) \
runtime/index_testing.js \
runtime/index.js
cd runtime && make index_testing

# This builds the binary and depends on files in some directories.
Expand Down
12 changes: 12 additions & 0 deletions core/source/Debug.mint
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
/* This module provides functions for debugging purposes. */
module Debug {
/*
Returns a nicely formatted version of the value. Values of Mint types
preserve their original name.
Debug.inspect("Hello World!") -> "Hello World!"
Debug.inspect(Maybe.Nothing) -> Maybe.Nothing
Debug.inspect({ name: "Joe", age: 37 }) -> User { name: "Joe", age: 37 }
*/
fun inspect (value : a) : String {
`#{%inspect%}(#{value})`
}

/*
Logs an arbitrary value to the windows console.
Expand Down
1 change: 1 addition & 0 deletions runtime/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from "./src/program";
export * from "./src/portals";
export * from "./src/variant";
export * from "./src/styles";
export * from "./src/debug";
128 changes: 128 additions & 0 deletions runtime/src/debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import indentString from "indent-string";
import { isVnode } from './equality';
import { Variant } from './variant';
import { Name } from './symbols';

const render = (items, prefix, suffix, fn) => {
items =
items.map(fn);

const newLines =
items.size > 3 || items.filter((item) => item.indexOf("\n") > 0).length;

const joined =
items.join(newLines ? ",\n" : ", ");

if (newLines) {
return `${prefix.trim()}\n${indentString(joined, 2)}\n${suffix.trim()}`;
} else {
return `${prefix}${joined}${suffix}`;
}
}

const toString = (object) => {
if (object.type === "null") {
return "null";
} else if (object.type === "undefined") {
return "undefined";
} else if (object.type === "string") {
return `"${object.value}"`;
} else if (object.type === "number") {
return `${object.value}`;
} else if (object.type === "boolean") {
return `${object.value}`;
} else if (object.type === "element") {
return `<${object.value.toLowerCase()}>`
} else if (object.type === "variant") {
if (object.items) {
return render(object.items, `${object.value}(`, `)`, toString);
} else {
return object.value;
}
} else if (object.type === "array") {
return render(object.items, `[`, `]`, toString);
} else if (object.type === "object") {
return render(object.items, `{ `, ` }`, toString);
} else if (object.type === "record") {
return render(object.items, `${object.value} { `, ` }`, toString);
} else if (object.type === "unknown") {
return `{ ${object.value} }`;
} else if (object.type === "vnode") {
return `VNode`;
} else if (object.key) {
return `${object.key}: ${toString(object.value)}`;
} else if (object.value) {
return toString(object.value);
}
}

const objectify = (value) => {
if (value === null) {
return { type: "null" };
} else if (value === undefined) {
return { type: "undefined" };
} else if (typeof value === "string") {
return { type: "string", value: value };
} else if (typeof value === "number") {
return { type: "number", value: value.toString() };
} else if (typeof value === "boolean") {
return { type: "boolean", value: value.toString() };
} else if (value instanceof HTMLElement) {
return { type: "element", value: value.tagName };
} else if (value instanceof Variant) {
const items = [];

if (value.record) {
for (const key in value) {
if (key === "length" || key === "record" || key.startsWith("_")) {
continue;
};

items.push({
value: objectify(value[key]),
key: key
});
}
} else {
for (let i = 0; i < value.length; i++) {
items.push({
value: objectify(value[`_${i}`])
});
};
}

if (items.length) {
return { type: "variant", value: value[Name], items: items };
} else {
return { type: "variant", value: value[Name] };
}
} else if (Array.isArray(value)) {
return {
items: value.map((item) => ({ value: objectify(item) })),
type: "array"
};
} else if (isVnode(value)) {
return { type: "vnode" }
} else if (typeof value == "object") {
const items = [];

for (const key in value) {
items.push({
value: objectify(value[key]),
key: key
});
};

if (Name in value) {
return { type: "record", value: value[Name], items: items };
} else {
return { type: "object", items: items };
}
} else {
return { type: "unknown", value: value.toString() };
}
}

export const inspect = (value) => {
return toString(objectify(value))
}
5 changes: 3 additions & 2 deletions runtime/src/decoders.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import indentString from "indent-string";
import { Name } from "./symbols";

// Formats the given value as JSON with extra indentation.
const format = (value) => {
Expand Down Expand Up @@ -317,8 +318,8 @@ export const decodeMap = (decoder, ok, err) => (input) => {
};

// Decodes a record, using the mappings.
export const decoder = (mappings, ok, err) => (input) => {
const object = {};
export const decoder = (name, mappings, ok, err) => (input) => {
const object = {[Name]: name};

for (let key in mappings) {
let decoder = mappings[key];
Expand Down
4 changes: 2 additions & 2 deletions runtime/src/equality.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file contains code to have value equality instead of reference equality.
// We use a `Symbol` to have a custom equality functions and then use these
// functions when comparing two values.
export const Equals = Symbol("Equals");
import { Equals } from './symbols';

/* v8 ignore next 3 */
if (typeof Node === "undefined") {
Expand Down Expand Up @@ -123,7 +123,7 @@ Map.prototype[Equals] = function (other) {
};

// If the object has a specific set of keys it's a Preact virtual DOM node.
const isVnode = (object) =>
export const isVnode = (object) =>
object !== undefined &&
object !== null &&
typeof object == "object" &&
Expand Down
2 changes: 2 additions & 0 deletions runtime/src/symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const Equals = Symbol("Equals");
export const Name = Symbol('Name');
7 changes: 7 additions & 0 deletions runtime/src/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect, useRef, useMemo } from "preact/hooks";
import { signal } from "@preact/signals";

import { compare } from "./equality";
import { Name } from "./symbols";

// This finds the first element matching the key in a map ([[key, value]]).
export const mapAccess = (map, key, just, nothing) => {
Expand Down Expand Up @@ -87,6 +88,10 @@ export const access = (field) => (value) => value[field];
// Identity function, used in encoders.
export const identity = (a) => a;

// Creates an instrumented object so we know which record it belongs to.
export const record = (name) => (value) => ({ [Name]: name, ...value})

// A component to lazy load another component.
export class lazyComponent extends Component {
async componentDidMount() {
let x = await this.props.x();
Expand All @@ -102,8 +107,10 @@ export class lazyComponent extends Component {
}
}

// A higher order function to lazy load a module.
export const lazy = (path) => async () => load(path)

// Loads load a module.
export const load = async (path) => {
const x = await import(path)
return x.default
Expand Down
8 changes: 5 additions & 3 deletions runtime/src/variant.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Equals, compareObjects, compare } from "./equality";
import { compareObjects, compare } from "./equality";
import { Equals, Name } from "./symbols";

// The base class for variants.
class Variant {
export class Variant {
[Equals](other) {
if (!(other instanceof this.constructor)) {
return false;
Expand All @@ -27,10 +28,11 @@ class Variant {

// Creates an type variant class, this is needed so we can do proper
// comparisons and pattern matching / destructuring.
export const variant = (input) => {
export const variant = (input, name) => {
return class extends Variant {
constructor(...args) {
super();
this[Name] = name
if (Array.isArray(input)) {
this.length = input.length;
this.record = true;
Expand Down
77 changes: 77 additions & 0 deletions runtime/tests/debug.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { variant, newVariant, inspect, record } from "../index_testing";
import { expect, test, describe } from "vitest";

test("inspecting null", () => {
expect(inspect(null)).toBe("null");
});

test("inspecting undefined", () => {
expect(inspect(undefined)).toBe("undefined");
});

test("inspecting string", () => {
expect(inspect("Hello")).toBe(`"Hello"`);
});

test("inspecting number", () => {
expect(inspect(0)).toBe(`0`);
});

test("inspecting boolean", () => {
expect(inspect(true)).toBe(`true`);
});

test("inspecting boolean", () => {
expect(inspect({props: {}, type: {}, ref: {}, key: {},"__": {}})).toBe(`VNode`);
});

test("inspecting object", () => {
expect(inspect({ name: "Joe" })).toBe(`{ name: "Joe" }`);
});

test("inspecting element", () => {
expect(inspect(document.createElement("div"))).toBe(`<div>`);
});

test("inspecting element", () => {
expect(inspect(document.createElement("div"))).toBe(`<div>`);
});

test("inspecting variant", () => {
const Test = variant(0, `Test`)
expect(inspect(newVariant(Test)())).toBe(`Test`);
});

test("inspecting variant (with parameters)", () => {
const Test = variant(1, `Test`)
expect(inspect(newVariant(Test)("Hello"))).toBe(`Test("Hello")`);
});

test("inspecting variant (with named parameters)", () => {
const Test = variant(["a", "b"], `Test`)
expect(inspect(newVariant(Test)("Hello", "World!"))).toBe(`Test(a: "Hello", b: "World!")`);
});

test("inspecting record", () => {
const Test = record(`Test`)
expect(inspect(Test({ a: "Hello", b: "World!"}))).toBe(`Test { a: "Hello", b: "World!" }`);
});

test("inspecting array", () => {
expect(inspect(["Hello", "World!"])).toBe(`["Hello", "World!"]`);
});

test("inspecting unkown", () => {
expect(inspect(Symbol("WTF"))).toBe(`{ Symbol(WTF) }`);
});

test("inspecting nested", () => {
expect(inspect({ a: "Hello", b: "World!", nested: { x: "With new line!\nYes!"}})).toBe(`{
a: "Hello",
b: "World!",
nested: {
x: "With new line!
Yes!"
}
}`);
});
File renamed without changes.
14 changes: 9 additions & 5 deletions spec/compilers/access
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ component Main {
}
}
--------------------------------------------------------------------------------
export const A = () => {
return {
name: `test`
}.name
};
import { record as A } from "./runtime.js";

export const
a = A(`X`),
B = () => {
return a({
name: `test`
}).name
};
24 changes: 15 additions & 9 deletions spec/compilers/access_deep
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ component Main {
}
}
--------------------------------------------------------------------------------
import { signal as A } from "./runtime.js";
import {
signal as B,
record as A
} from "./runtime.js";

export const
a = A({
level1: {
level2: {
a = A(`Level2`),
b = A(`Level1`),
c = A(`Locale`),
d = B(c({
level1: b({
level2: a({
name: `Test`
}
}
}),
B = () => {
return a.value.level1.level2.name
})
})
})),
C = () => {
return d.value.level1.level2.name
};
Loading

0 comments on commit 942c4b6

Please sign in to comment.