Skip to content

Commit e42437c

Browse files
committed
Improve circular reference printing
1 parent c7e61e1 commit e42437c

File tree

3 files changed

+158
-145
lines changed

3 files changed

+158
-145
lines changed

src/gleam/bit_array.gleam

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ pub fn base16_decode(input: String) -> Result(BitArray, Nil)
170170

171171
/// Converts a bit array to a string containing the decimal value of each byte.
172172
///
173+
/// Use this over `string.inspect` when you have a bit array you want printed
174+
/// in the array syntax even if it is valid UTF-8.
175+
///
173176
/// ## Examples
174177
///
175178
/// ```gleam
@@ -184,7 +187,6 @@ pub fn inspect(input: BitArray) -> String {
184187
inspect_loop(input, "<<") <> ">>"
185188
}
186189

187-
@external(javascript, "../gleam_stdlib.mjs", "bit_array_inspect")
188190
fn inspect_loop(input: BitArray, accumulator: String) -> String {
189191
case input {
190192
<<>> -> accumulator

src/gleam_stdlib.mjs

Lines changed: 154 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -670,147 +670,179 @@ export function bitwise_shift_right(x, y) {
670670
}
671671

672672
export function inspect(v) {
673-
const t = typeof v;
674-
if (v === true) return "True";
675-
if (v === false) return "False";
676-
if (v === null) return "//js(null)";
677-
if (v === undefined) return "Nil";
678-
if (t === "string") return inspectString(v);
679-
if (t === "bigint" || Number.isInteger(v)) return v.toString();
680-
if (t === "number") return float_to_string(v);
681-
if (v instanceof UtfCodepoint) return inspectUtfCodepoint(v);
682-
if (v instanceof BitArray) return `<<${bit_array_inspect(v, "")}>>`;
683-
if (v instanceof RegExp) return `//js(${v})`;
684-
if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`;
685-
if (v instanceof globalThis.Error) return `//js(${v.toString()})`;
686-
if (v instanceof Function) {
687-
const args = [];
688-
for (const i of Array(v.length).keys())
689-
args.push(String.fromCharCode(i + 97));
690-
return `//fn(${args.join(", ")}) { ... }`;
691-
}
673+
return new Inspector().inspect(v);
674+
}
675+
676+
class Inspector {
677+
#references = new Set();
678+
679+
inspect(v) {
680+
const t = typeof v;
681+
if (v === true) return "True";
682+
if (v === false) return "False";
683+
if (v === null) return "//js(null)";
684+
if (v === undefined) return "Nil";
685+
if (t === "string") return this.#string(v);
686+
if (t === "bigint" || Number.isInteger(v)) return v.toString();
687+
if (t === "number") return float_to_string(v);
688+
if (v instanceof UtfCodepoint) return this.#utfCodepoint(v);
689+
if (v instanceof BitArray) return this.#bit_array(v);
690+
if (v instanceof RegExp) return `//js(${v})`;
691+
if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`;
692+
if (v instanceof globalThis.Error) return `//js(${v.toString()})`;
693+
if (v instanceof Function) {
694+
const args = [];
695+
for (const i of Array(v.length).keys())
696+
args.push(String.fromCharCode(i + 97));
697+
return `//fn(${args.join(", ")}) { ... }`;
698+
}
692699

693-
try {
694-
if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`;
695-
if (v instanceof List) return inspectList(v);
696-
if (v instanceof CustomType) return inspectCustomType(v);
697-
if (v instanceof Dict) return inspectDict(v);
698-
if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`;
699-
return inspectObject(v);
700-
} catch (e) {
701-
if (e instanceof RangeError) return "//js(circular)";
702-
throw e;
700+
if (this.#references.size === this.#references.add(v).size) {
701+
return "//js(circular reference)";
702+
}
703+
704+
if (Array.isArray(v))
705+
return `#(${v.map((v) => this.inspect(v)).join(", ")})`;
706+
if (v instanceof List) return this.#list(v);
707+
if (v instanceof CustomType) return this.#customType(v);
708+
if (v instanceof Dict) return this.#dict(v);
709+
if (v instanceof Set)
710+
return `//js(Set(${[...v].map((v) => this.inspect(v)).join(", ")}))`;
711+
return this.#object(v);
703712
}
704-
}
705713

706-
function inspectString(str) {
707-
let new_str = '"';
708-
for (let i = 0; i < str.length; i++) {
709-
const char = str[i];
710-
switch (char) {
711-
case "\n":
712-
new_str += "\\n";
713-
break;
714-
case "\r":
715-
new_str += "\\r";
716-
break;
717-
case "\t":
718-
new_str += "\\t";
719-
break;
720-
case "\f":
721-
new_str += "\\f";
722-
break;
723-
case "\\":
724-
new_str += "\\\\";
725-
break;
726-
case '"':
727-
new_str += '\\"';
728-
break;
729-
default:
730-
if (char < " " || (char > "~" && char < "\u{00A0}")) {
731-
new_str +=
732-
"\\u{" +
733-
char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") +
734-
"}";
735-
} else {
736-
new_str += char;
737-
}
714+
#object(v) {
715+
const name = Object.getPrototypeOf(v)?.constructor?.name || "Object";
716+
const props = [];
717+
for (const k of Object.keys(v)) {
718+
props.push(`${this.inspect(k)}: ${this.inspect(v[k])}`);
738719
}
720+
const body = props.length ? " " + props.join(", ") + " " : "";
721+
const head = name === "Object" ? "" : name + " ";
722+
return `//js(${head}{${body}})`;
739723
}
740-
new_str += '"';
741-
return new_str;
742-
}
743-
744-
function inspectDict(map) {
745-
let body = "dict.from_list([";
746-
let first = true;
747-
map.forEach((value, key) => {
748-
if (!first) body = body + ", ";
749-
body = body + "#(" + inspect(key) + ", " + inspect(value) + ")";
750-
first = false;
751-
});
752-
return body + "])";
753-
}
754-
755-
function inspectObject(v) {
756-
const name = Object.getPrototypeOf(v)?.constructor?.name || "Object";
757-
const props = [];
758-
for (const k of Object.keys(v)) {
759-
props.push(`${inspect(k)}: ${inspect(v[k])}`);
724+
725+
#dict(map) {
726+
let body = "dict.from_list([";
727+
let first = true;
728+
map.forEach((value, key) => {
729+
if (!first) body = body + ", ";
730+
body = body + "#(" + this.inspect(key) + ", " + this.inspect(value) + ")";
731+
first = false;
732+
});
733+
return body + "])";
760734
}
761-
const body = props.length ? " " + props.join(", ") + " " : "";
762-
const head = name === "Object" ? "" : name + " ";
763-
return `//js(${head}{${body}})`;
764-
}
765-
766-
function inspectCustomType(record) {
767-
const props = Object.keys(record)
768-
.map((label) => {
769-
const value = inspect(record[label]);
770-
return isNaN(parseInt(label)) ? `${label}: ${value}` : value;
771-
})
772-
.join(", ");
773-
return props
774-
? `${record.constructor.name}(${props})`
775-
: record.constructor.name;
776-
}
777-
778-
export function inspectList(list) {
779-
if (list instanceof Empty) {
780-
return "[]";
735+
736+
#customType(record) {
737+
const props = Object.keys(record)
738+
.map((label) => {
739+
const value = this.inspect(record[label]);
740+
return isNaN(parseInt(label)) ? `${label}: ${value}` : value;
741+
})
742+
.join(", ");
743+
return props
744+
? `${record.constructor.name}(${props})`
745+
: record.constructor.name;
781746
}
782747

783-
let char_out = 'charlist.from_string("';
784-
let list_out = "[";
748+
#list(list) {
749+
if (list instanceof Empty) {
750+
return "[]";
751+
}
785752

786-
let current = list;
787-
while (current instanceof NonEmpty) {
788-
let element = current.head;
789-
current = current.tail;
753+
let char_out = 'charlist.from_string("';
754+
let list_out = "[";
790755

791-
if (list_out !== "[") {
792-
list_out += ", ";
756+
let current = list;
757+
while (current instanceof NonEmpty) {
758+
let element = current.head;
759+
current = current.tail;
760+
761+
if (list_out !== "[") {
762+
list_out += ", ";
763+
}
764+
list_out += this.inspect(element);
765+
766+
if (char_out) {
767+
if (Number.isInteger(element) && element >= 32 && element <= 126) {
768+
char_out += String.fromCharCode(element);
769+
} else {
770+
char_out = null;
771+
}
772+
}
793773
}
794-
list_out += inspect(element);
795774

796775
if (char_out) {
797-
if (Number.isInteger(element) && element >= 32 && element <= 126) {
798-
char_out += String.fromCharCode(element);
799-
} else {
800-
char_out = null;
776+
return char_out + '")';
777+
} else {
778+
return list_out + "]";
779+
}
780+
}
781+
782+
#string(str) {
783+
let new_str = '"';
784+
for (let i = 0; i < str.length; i++) {
785+
const char = str[i];
786+
switch (char) {
787+
case "\n":
788+
new_str += "\\n";
789+
break;
790+
case "\r":
791+
new_str += "\\r";
792+
break;
793+
case "\t":
794+
new_str += "\\t";
795+
break;
796+
case "\f":
797+
new_str += "\\f";
798+
break;
799+
case "\\":
800+
new_str += "\\\\";
801+
break;
802+
case '"':
803+
new_str += '\\"';
804+
break;
805+
default:
806+
if (char < " " || (char > "~" && char < "\u{00A0}")) {
807+
new_str +=
808+
"\\u{" +
809+
char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") +
810+
"}";
811+
} else {
812+
new_str += char;
813+
}
801814
}
802815
}
816+
new_str += '"';
817+
return new_str;
803818
}
804819

805-
if (char_out) {
806-
return char_out + '")';
807-
} else {
808-
return list_out + "]";
820+
#utfCodepoint(codepoint) {
821+
return `//utfcodepoint(${String.fromCodePoint(codepoint.value)})`;
809822
}
810-
}
811823

812-
export function inspectUtfCodepoint(codepoint) {
813-
return `//utfcodepoint(${String.fromCodePoint(codepoint.value)})`;
824+
#bit_array(bits) {
825+
let acc = "<<";
826+
if (bits.bitSize === 0) {
827+
return acc;
828+
}
829+
830+
for (let i = 0; i < bits.byteSize - 1; i++) {
831+
acc += bits.byteAt(i).toString();
832+
acc += ", ";
833+
}
834+
835+
if (bits.byteSize * 8 === bits.bitSize) {
836+
acc += bits.byteAt(bits.byteSize - 1).toString();
837+
} else {
838+
const trailingBitsCount = bits.bitSize % 8;
839+
acc += bits.byteAt(bits.byteSize - 1) >> (8 - trailingBitsCount);
840+
acc += `:size(${trailingBitsCount})`;
841+
}
842+
843+
acc += ">>";
844+
return acc;
845+
}
814846
}
815847

816848
export function base16_encode(bit_array) {
@@ -843,27 +875,6 @@ export function base16_decode(string) {
843875
return new Ok(new BitArray(bytes));
844876
}
845877

846-
export function bit_array_inspect(bits, acc) {
847-
if (bits.bitSize === 0) {
848-
return acc;
849-
}
850-
851-
for (let i = 0; i < bits.byteSize - 1; i++) {
852-
acc += bits.byteAt(i).toString();
853-
acc += ", ";
854-
}
855-
856-
if (bits.byteSize * 8 === bits.bitSize) {
857-
acc += bits.byteAt(bits.byteSize - 1).toString();
858-
} else {
859-
const trailingBitsCount = bits.bitSize % 8;
860-
acc += bits.byteAt(bits.byteSize - 1) >> (8 - trailingBitsCount);
861-
acc += `:size(${trailingBitsCount})`;
862-
}
863-
864-
return acc;
865-
}
866-
867878
export function bit_array_to_int_and_size(bits) {
868879
const trailingBitsCount = bits.bitSize % 8;
869880
const unusedBitsCount = trailingBitsCount === 0 ? 0 : 8 - trailingBitsCount;

test/gleam/string_test.gleam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ fn circular_reference() -> Dynamic
12491249
@target(javascript)
12501250
pub fn inspect_circular_reference_test() {
12511251
assert string.inspect(circular_reference())
1252-
|> string.starts_with("#(1, 2, 3, #")
1252+
== "#(1, 2, 3, //js(circular reference))"
12531253
}
12541254

12551255
@target(javascript)

0 commit comments

Comments
 (0)