Skip to content

Commit 008599b

Browse files
authored
handle big ints better (#6768)
## 📝 Summary <!-- Provide a concise summary of what this pull request is addressing. If this PR fixes any issues, list them here by number (e.g., Fixes #123). --> ![CleanShot 2025-10-14 at 12 55 09](https://github.com/user-attachments/assets/7a95d3f6-44a6-439c-b883-34596506f92f) ![CleanShot 2025-10-14 at 12 55 30](https://github.com/user-attachments/assets/bbece524-3e7e-492c-8375-8638fcdbe83d) note that floats with big ints don't work well with tables, so I convert them to strings (which is the default rendering if using mo.plain) before: ![CleanShot 2025-10-14 at 12 56 50](https://github.com/user-attachments/assets/f7bc6a9f-2e0e-4b3c-95be-405be76ad5ec) after: ![CleanShot 2025-10-14 at 16 14 13](https://github.com/user-attachments/assets/8bc07c49-d486-4987-b61c-93dedca94a1d) ## 🔍 Description of Changes <!-- Detail the specific changes made in this pull request. Explain the problem addressed and how it was resolved. If applicable, provide before and after comparisons, screenshots, or any relevant details to help reviewers understand the changes easily. --> ## 📋 Checklist - [x] I have read the [contributor guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md). - [ ] For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on [Discord](https://marimo.io/discord?ref=pr), or the community [discussions](https://github.com/marimo-team/marimo/discussions) (Please provide a link if applicable). - [x] I have added tests for the changes made. - [x] I have run the code and verified that it works as expected.
1 parent ebc0ac2 commit 008599b

File tree

15 files changed

+289
-152
lines changed

15 files changed

+289
-152
lines changed

frontend/src/components/editor/output/JsonOutput.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ const LEAF_RENDERERS = {
194194
"video/": (value: string) => <VideoOutput src={value} />,
195195
"text/html:": (value: string) => <HtmlOutput html={value} inline={true} />,
196196
"text/plain+float:": (value: string) => <span>{value}</span>,
197+
"text/plain+bigint:": (value: string) => <span>{value}</span>,
197198
"text/plain+set:": (value: string) => <span>set{value}</span>,
198199
"text/plain+tuple:": (value: string) => <span>{value}</span>,
199200
"text/plain:": (value: string) => <CollapsibleTextOutput text={value} />,
@@ -351,6 +352,9 @@ function pythonJsonReplacer(_key: string, value: unknown): unknown {
351352
if (typeof value === "object") {
352353
return value;
353354
}
355+
if (typeof value === "bigint") {
356+
return `${REPLACE_PREFIX}${value}${REPLACE_SUFFIX}`;
357+
}
354358
if (Array.isArray(value)) {
355359
return value;
356360
}
@@ -359,6 +363,11 @@ function pythonJsonReplacer(_key: string, value: unknown): unknown {
359363
if (value.startsWith("text/plain+float:")) {
360364
return `${REPLACE_PREFIX}${leafData(value)}${REPLACE_SUFFIX}`;
361365
}
366+
if (value.startsWith("text/plain+bigint:")) {
367+
// Use BigInt to avoid precision loss
368+
const number = BigInt(leafData(value));
369+
return `${REPLACE_PREFIX}${number}${REPLACE_SUFFIX}`;
370+
}
362371
if (value.startsWith("text/plain+tuple:")) {
363372
// replace first and last characters [] with ()
364373
return `${REPLACE_PREFIX}(${leafData(value).slice(1, -1)})${REPLACE_SUFFIX}`;

frontend/src/components/editor/output/__tests__/json-output.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,49 @@ describe("getCopyValue", () => {
218218
`,
219219
);
220220
});
221+
222+
it("should handle bigint", () => {
223+
const bigint = String(BigInt(2 ** 64));
224+
const value = `text/plain+bigint:${bigint}`;
225+
const result = getCopyValue(value);
226+
expect(result).toMatchInlineSnapshot(`"18446744073709551616"`);
227+
228+
const nestedBigInt = {
229+
key1: bigint, // this will be just a string
230+
key2: `text/plain+bigint:${bigint}`, // this will convert to number
231+
key3: true,
232+
};
233+
const nestedResult = getCopyValue(nestedBigInt);
234+
expect(nestedResult).toMatchInlineSnapshot(
235+
`
236+
"{
237+
"key1": "18446744073709551616",
238+
"key2": 18446744073709551616,
239+
"key3": True
240+
}"
241+
`,
242+
);
243+
244+
const bigintRaw = BigInt(2 ** 64);
245+
const bigintRawResult = getCopyValue(bigintRaw);
246+
expect(bigintRawResult).toMatchInlineSnapshot(`"18446744073709551616"`);
247+
248+
const nestedBigIntRaw = {
249+
key1: bigintRaw, // raw number
250+
key2: `text/plain+bigint:${bigintRaw}`,
251+
key3: true,
252+
};
253+
const nestedBigIntRawResult = getCopyValue(nestedBigIntRaw);
254+
expect(nestedBigIntRawResult).toMatchInlineSnapshot(
255+
`
256+
"{
257+
"key1": 18446744073709551616,
258+
"key2": 18446744073709551616,
259+
"key3": True
260+
}"
261+
`,
262+
);
263+
});
221264
});
222265

223266
describe("determineMaxDisplayLength", () => {

frontend/src/components/editor/package-alert.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ const ExtrasSelector: React.FC<ExtrasSelectorProps> = ({
463463
<button
464464
className="hover:bg-muted/50 rounded text-sm px-1 transition-colors border border-muted-foreground/30 hover:border-muted-foreground/60 min-w-0 flex-1 truncate text-left"
465465
title={`Selected extras: ${selectedExtras.join(", ")}`}
466+
type="button"
466467
>
467468
{selectedExtras.join(",")}
468469
</button>
@@ -523,6 +524,7 @@ const ExtrasSelector: React.FC<ExtrasSelectorProps> = ({
523524
!canSelectExtras && "opacity-50 cursor-not-allowed",
524525
)}
525526
title={canSelectExtras ? "Add extras" : "Loading extras..."}
527+
type="button"
526528
>
527529
<PlusIcon className="w-3 h-3 shrink-0" />
528530
</button>
@@ -619,6 +621,7 @@ const StreamingLogsViewer: React.FC<StreamingLogsViewerProps> = ({
619621
<button
620622
onClick={() => setIsExpanded(!isExpanded)}
621623
className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
624+
type="button"
622625
>
623626
{isExpanded ? (
624627
<ChevronDownIcon className="w-4 h-4" />

frontend/src/custom.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface JSON {
1616
text: string,
1717
reviver?: (this: any, key: string, value: any) => any,
1818
): unknown;
19+
20+
rawJSON(value: string): unknown;
1921
}
2022

2123
// Improve type inference for Array.filter with BooleanConstructor

frontend/src/utils/__tests__/json-parser.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ it("can fail to jsonParseWithSpecialChar", () => {
5555
expect(jsonParseWithSpecialChar("[nan]")).toMatchInlineSnapshot("{}");
5656
});
5757

58+
it("can parse bigInts", () => {
59+
const bigint = JSON.stringify({ bigint: { $bigint: "123456" } });
60+
expect(jsonParseWithSpecialChar(bigint)).toEqual({ bigint: BigInt(123_456) });
61+
62+
const arrayOfBigInts = JSON.stringify([{ $bigint: "123456" }]);
63+
expect(jsonParseWithSpecialChar(arrayOfBigInts)).toEqual([BigInt(123_456)]);
64+
65+
const nestedBigInt = JSON.stringify({ bigint: [{ $bigint: "123456" }] });
66+
expect(jsonParseWithSpecialChar(nestedBigInt)).toEqual({
67+
bigint: [BigInt(123_456)],
68+
});
69+
});
70+
5871
it("can convert json to tsv", () => {
5972
expect(jsonToTSV([])).toEqual("");
6073

frontend/src/utils/json/json-parser.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
import type { JsonString } from "./base64";
44

5+
declare global {
6+
interface BigInt {
7+
toJSON(): unknown;
8+
}
9+
}
10+
11+
// Treat BigInts as numbers
12+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON#using_json_numbers
13+
BigInt.prototype.toJSON = function () {
14+
return JSON.rawJSON(this.toString());
15+
};
16+
517
/**
618
* Parse an attribute value as JSON.
719
* This also handles NaN, Infinity, and -Infinity.
@@ -12,7 +24,7 @@ export function jsonParseWithSpecialChar<T = unknown>(
1224
// This regex handling is expensive and often not needed.
1325
// We try to parse with JSON.parse first, and if that fails, we use the regex.
1426
try {
15-
return JSON.parse(value) as T;
27+
return JSON.parse(value, (_key, value) => sanitizeBigInt(value)) as T;
1628
} catch {
1729
// Do nothing
1830
}
@@ -36,7 +48,7 @@ export function jsonParseWithSpecialChar<T = unknown>(
3648
);
3749
return JSON.parse(value, (_key, v) => {
3850
if (typeof v !== "string") {
39-
return v;
51+
return sanitizeBigInt(v);
4052
}
4153
if (v === `${CHAR}NaN${CHAR}`) {
4254
return Number.NaN;
@@ -47,7 +59,7 @@ export function jsonParseWithSpecialChar<T = unknown>(
4759
if (v === `${CHAR}-Infinity${CHAR}`) {
4860
return Number.NEGATIVE_INFINITY;
4961
}
50-
return v;
62+
return sanitizeBigInt(v);
5163
}) as T;
5264
} catch {
5365
return {} as T;
@@ -63,3 +75,16 @@ export function jsonToTSV(json: Record<string, unknown>[]) {
6375
const values = json.map((row) => keys.map((key) => row[key]).join("\t"));
6476
return `${keys.join("\t")}\n${values.join("\n")}`;
6577
}
78+
79+
/** Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json */
80+
function sanitizeBigInt(value: unknown): unknown {
81+
if (
82+
value !== null &&
83+
typeof value === "object" &&
84+
"$bigint" in value &&
85+
typeof value.$bigint === "string"
86+
) {
87+
return BigInt(value.$bigint);
88+
}
89+
return value;
90+
}

marimo/_output/data/data.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,20 +185,26 @@ def any_data(data: Union[str, bytes, io.BytesIO], ext: str) -> VirtualFile:
185185
raise ValueError(f"Unsupported data type: {type(data)}")
186186

187187

188+
# JavaScript's safe integer limits
189+
MAX_SAFE_INTEGER = 9007199254740991
190+
MIN_SAFE_INTEGER = -9007199254740991
191+
BIGINT_KEY = "$bigint"
192+
193+
194+
def is_bigint(value: int | float) -> bool:
195+
return value > MAX_SAFE_INTEGER or value < MIN_SAFE_INTEGER
196+
197+
188198
def sanitize_json_bigint(
189199
data: Union[str, dict[str, Any], list[dict[str, Any]]],
190200
) -> str:
191-
"""Sanitize JSON bigint to a string.
201+
"""Sanitize JSON big numbers to a string.
192202
193203
This is necessary because the frontend will round ints larger than
194204
Number.MAX_SAFE_INTEGER to Number.MAX_SAFE_INTEGER.
195205
"""
196206
from json import dumps, loads
197207

198-
# JavaScript's safe integer limits
199-
MAX_SAFE_INTEGER = 9007199254740991
200-
MIN_SAFE_INTEGER = -9007199254740991
201-
202208
def convert_key(key: Any) -> Any:
203209
# Keys must be str, int, float, bool, or None
204210
if key is None:
@@ -212,9 +218,12 @@ def convert_bigint(obj: Any) -> Any:
212218
return {convert_key(k): convert_bigint(v) for k, v in obj.items()} # type: ignore
213219
elif isinstance(obj, list):
214220
return [convert_bigint(item) for item in obj] # type: ignore
215-
elif isinstance(obj, int) and (
216-
obj > MAX_SAFE_INTEGER or obj < MIN_SAFE_INTEGER
217-
):
221+
elif isinstance(obj, int) and is_bigint(obj):
222+
# If the value is outside the safe integer range, convert it to an object with a $bigint key
223+
# Frontend will convert the object back to an integer.
224+
return {BIGINT_KEY: str(obj)}
225+
elif isinstance(obj, float) and is_bigint(obj):
226+
# Decimals are not handled currently, we just convert them to strings.
218227
return str(obj)
219228
else:
220229
return obj

marimo/_output/formatters/structures.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from marimo._messaging.mimetypes import KnownMimeType
1010
from marimo._output import formatting
11+
from marimo._output.data.data import is_bigint
1112
from marimo._output.formatters.formatter_factory import FormatterFactory
1213
from marimo._output.formatters.repr_formatters import maybe_get_repr_formatter
1314
from marimo._plugins.stateless.inspect import inspect
@@ -26,6 +27,8 @@ def _leaf_formatter(
2627
if isinstance(value, str):
2728
return value
2829
if isinstance(value, int):
30+
if is_bigint(value):
31+
return f"text/plain+bigint:{value}"
2932
return value
3033
# floats are still converted to strings because JavaScript
3134
# can't reliably distinguish between them (eg 1 and 1.0)

marimo/_smoke_tests/tables/complex_types.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import marimo
22

3-
__generated_with = "0.15.5"
3+
__generated_with = "0.16.5"
44
app = marimo.App(width="medium")
55

66

@@ -110,11 +110,23 @@ def _(df):
110110

111111
@app.cell
112112
def _(mo, pd, pl):
113+
complex = [1 + 2j, 2 + 3j]
114+
113115
additional_types_pd = pd.DataFrame(
114-
{"complex": [1 + 2j, 2 + 3j], "bigint": [2**64, 2**127]}
116+
{
117+
"complex": complex,
118+
"bigint": [2**64, 2**127],
119+
"list_big_ints": [[1253397962952480469], [1253397962952480469]],
120+
"large_floats": [[125339796295248046.9], [-12533979629524804.69]],
121+
}
115122
)
116123
additional_types_pl = pl.DataFrame(
117-
{"complex": [1 + 2j, 2 + 3j], "bigint": [2**64, 2**65]}
124+
{
125+
"complex": complex,
126+
"bigint": [2**64, -(2**65)],
127+
"list_big_ints": [[1253397962952480469], [1253397962952480469]],
128+
"large_floats": [[125339796295248046.9], [-12533979629524804.69]],
129+
}
118130
)
119131
mo.vstack([additional_types_pd, additional_types_pl])
120132
return

0 commit comments

Comments
 (0)