Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve debugging of Mint values. #704

Merged
merged 2 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Sija marked this conversation as resolved.
Show resolved Hide resolved

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