Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b5eedc5
Add official toml-lang/toml-test conformance suite for TOML
dylan-conway Jun 28, 2026
d7f449f
Tighten TOML conformance suite assertions
dylan-conway Jun 28, 2026
ddb5799
Target TOML v1.1.0 in the conformance suite
dylan-conway Jun 28, 2026
f09d4c5
Rewrite the TOML parser for v1.1.0 conformance
dylan-conway Jun 28, 2026
e1ae2e5
Fix clippy needless_borrows_for_generic_args in TOML parser
dylan-conway Jun 28, 2026
dbee64c
Test invalid-encoding TOML inputs as raw bytes; update types and docs
dylan-conway Jun 28, 2026
730730b
Add hand-written TOML API tests; name end-of-file in parse errors
dylan-conway Jun 28, 2026
2355ad2
Test recursion limits on deep dotted keys and table headers
dylan-conway Jun 28, 2026
ebd56aa
Calibrate recursion-test depths for release builds and test speed
dylan-conway Jun 28, 2026
a22d259
Name the fix when a TOML value is an unquoted string
dylan-conway Jun 28, 2026
1d47354
Address review: inline-table keyval-sep, integer diagnostics, types
dylan-conway Jun 28, 2026
6212898
Pin lone-surrogate semantics at the TOML.parse input boundary
dylan-conway Jun 28, 2026
71733f1
Borrow source bytes for escape-free basic strings
dylan-conway Jun 28, 2026
3832785
Address review nits: redacted message suffix, literal-string dedup
dylan-conway Jun 28, 2026
ffe7732
Escape Windows backslashes in test bunfig cache paths
dylan-conway Jun 28, 2026
20a6024
docs: drop redundant integer callout from the conformance sentence
dylan-conway Jun 28, 2026
4b47b95
Restructure the TOML parser into a token-based scanner/parser
dylan-conway Jun 28, 2026
8c9e842
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 28, 2026
0c6dd68
Address review: CRLF diagnostics, inf/nan bare words, test gaps
dylan-conway Jun 28, 2026
c4c02b0
Merge remote-tracking branch 'origin/main' into claude/toml-test-suite
dylan-conway Jun 28, 2026
913de47
Add TOML.stringify
dylan-conway Jun 28, 2026
ee8f2a5
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 28, 2026
948224d
Address review: +0 sign fix, aot diagnostics, dead redact hook
dylan-conway Jun 28, 2026
aeb9ff4
Merge branch 'claude/toml-test-suite' of github.com:oven-sh/bun into …
dylan-conway Jun 28, 2026
684109f
TOML.stringify: format Dates through JSC's toISOString binding
robobun Jun 29, 2026
dc048a4
Use the TOML 1.1 spec's H notation for Unicode escapes in docs and co…
robobun Jun 29, 2026
b891a33
TOML.stringify: declare the return type as string | undefined
robobun Jun 29, 2026
a61df4c
test: drop two redundant as-any casts on TOML.stringify calls
robobun Jun 29, 2026
0a80949
Merge remote-tracking branch 'origin/main' into claude/toml-test-suite
robobun Jul 2, 2026
69860e5
toml: generate the --check comparand beside the committed suite
robobun Jul 2, 2026
de8961d
toml: build the deep dotted-key test input with Buffer.alloc
robobun Jul 2, 2026
0476d40
ini: drop the dangling MAX_DOTTED_KEY_SEGMENTS cross-reference
robobun Jul 2, 2026
59d86a7
toml: add stringify(parse) round-trip coverage for every type
robobun Jul 2, 2026
38cfba6
Merge origin/main into claude/toml-test-suite
robobun Jul 4, 2026
00e1edb
Merge origin/main into claude/toml-test-suite
robobun Jul 7, 2026
dbac342
Merge origin/main into claude/toml-test-suite
robobun Jul 8, 2026
2efa976
Merge origin/main into claude/toml-test-suite
robobun Jul 10, 2026
aba8757
Merge origin/main (ancestry link; file changes are in the preceding c…
robobun Jul 10, 2026
3b9c945
Merge branch 'main' into claude/toml-test-suite
dylan-conway Jul 10, 2026
4230961
paths: inline the now-dead result binding in normalize_buf
robobun Jul 10, 2026
71edf45
toml: rename subarray-offset padding bytes to avoid the hygiene-gate …
robobun Jul 10, 2026
ace42f9
Merge origin/main into claude/toml-test-suite
robobun Jul 11, 2026
71f6523
Merge branch 'main' into claude/toml-test-suite
dylan-conway Jul 16, 2026
970d41b
docs: use ASCII hyphen-minus in the TOML integer range
robobun Jul 16, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 38 additions & 6 deletions docs/runtime/toml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ console.log(data);

#### Supported TOML Features

Bun's TOML parser supports the [TOML v1.0 specification](https://toml.io/en/v1.0.0), including:
Bun's TOML parser implements the full [TOML v1.1.0 specification](https://github.com/toml-lang/toml/releases/tag/1.1.0) and passes the complete official [toml-test](https://github.com/toml-lang/toml-test) conformance suite.

- **Strings**: basic (`"..."`) and literal (`'...'`), including multi-line
- **Integers**: decimal, hex (`0x`), octal (`0o`), and binary (`0b`)
- **Floats**
- **Strings**: basic (`"..."`) and literal (`'...'`), including multi-line, with all escapes (`\uHHHH`, `\UHHHHHHHH`, and TOML 1.1's `\xHH` and `\e`)
- **Integers**: decimal, hex (`0x`), octal (`0o`), and binary (`0b`). Integers that cannot be represented losslessly as a JavaScript number — outside ±(2^53 - 1) — throw
- **Floats**: including `inf` and `nan`
- **Booleans**: `true` and `false`
- **Date/times**: offset date-time, local date-time, local date, and local time, returned as strings of their source text
- **Arrays**: including mixed types and nested arrays
- **Tables**: standard (`[table]`) and inline (`{ key = "value" }`)
- **Tables**: standard (`[table]`) and inline (`{ key = "value" }`), including TOML 1.1 multi-line inline tables
- **Array of tables**: `[[array]]`
- **Dotted keys**: `a.b.c = "value"`
- **Comments**: using `#`
Expand Down Expand Up @@ -82,16 +83,47 @@ role = "backend"

#### Error Handling

`Bun.TOML.parse()` throws if the TOML is invalid:
`Bun.TOML.parse()` throws a `SyntaxError` if the TOML is invalid:

```ts
try {
Bun.TOML.parse("invalid = = =");
} catch (error) {
console.error("Failed to parse TOML:", error.message);
// Failed to parse TOML: TOML Parse error: Expected a value but found '='
}
```

### `Bun.TOML.stringify()`

Serialize a JavaScript object to a TOML document. Scalar keys come first,
followed by `[table]` and `[[array-of-tables]]` sections:

```ts
Bun.TOML.stringify({
name: "app",
server: { host: "localhost", port: 8080 },
points: [{ x: 1 }, { x: 2 }],
});
// name = "app"
//
// [server]
// host = "localhost"
// port = 8080
//
// [[points]]
// x = 1
//
// [[points]]
// x = 2
```

The top-level value must be an object — a TOML document is a table. `Date`
values become TOML offset date-times. Because TOML cannot represent them,
`null` values, `BigInt`, and circular structures throw; `undefined`,
function, and symbol properties are skipped (inside arrays they throw,
since TOML arrays cannot have holes).

---

## Module Import
Expand Down
51 changes: 42 additions & 9 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,14 +777,47 @@ declare module "bun" {
*/
namespace TOML {
/**
* Parse a TOML string into a JavaScript object.
* Parse a TOML (v1.1.0) document into a JavaScript object.
*
* Date/time values parse as strings of their source text. Integers
* outside `Number.MAX_SAFE_INTEGER` throw, since they cannot be
* represented losslessly as JavaScript numbers.
*
* @category Utilities
*
* @param input The TOML string to parse
* @param input The TOML document to parse, as a string or UTF-8 bytes
* @returns A JavaScript object
* @throws {SyntaxError} If the input is not valid TOML
*/
export function parse(input: string): object;
export function parse(
input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike | Blob,
): object;

/**
* Serialize a JavaScript object to a TOML document.
*
* The top-level value must be an object (a TOML document is a table).
* `Date` values become TOML offset date-times. `null`, `BigInt`, and
* circular structures throw, since TOML cannot represent them;
* `undefined`, function, and symbol properties are skipped (inside
* arrays they throw, since TOML arrays cannot have holes).
*
* @category Utilities
*
* @param input The JavaScript object to serialize.
* @param replacer Not supported; pass `undefined` or `null`.
* @param space Accepted for signature parity with `YAML.stringify` and
* `JSON5.stringify`, but ignored: TOML output is line-oriented.
* @returns A TOML document string, or `undefined` if the input is `undefined`, a function, or a symbol.
*
* @example
* ```js
* import { TOML } from "bun";
* TOML.stringify({ name: "app", server: { port: 8080 } });
* // 'name = "app"\n\n[server]\nport = 8080\n'
* ```
*/
export function stringify(input: unknown, replacer?: undefined | null, space?: string | number): string | undefined;
}

/**
Expand Down Expand Up @@ -869,7 +902,7 @@ declare module "bun" {
* Bun.JSONL.parse('{bad}\n'); // throws SyntaxError
* ```
*/
export function parse(input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike): unknown[];
export function parse(input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike): unknown[];

/**
* Parse a JSONL chunk, designed for streaming use.
Expand Down Expand Up @@ -904,7 +937,7 @@ declare module "bun" {
* ```
*/
export function parseChunk(
input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike,
input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike,
start?: number,
end?: number,
): ParseChunkResult;
Expand Down Expand Up @@ -1303,7 +1336,7 @@ declare module "bun" {
* ```
*/
export function html(
input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike,
input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike,
options?: Options,
): string;

Expand Down Expand Up @@ -1379,7 +1412,7 @@ declare module "bun" {
* ```
*/
export function ansi(
input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike,
input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike,
theme?: AnsiTheme,
): string;

Expand Down Expand Up @@ -1421,7 +1454,7 @@ declare module "bun" {
* ```
*/
export function render(
input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike,
input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike,
callbacks?: RenderCallbacks,
options?: Options,
): string;
Expand Down Expand Up @@ -1470,7 +1503,7 @@ declare module "bun" {
* ```
*/
export function react(
input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike,
input: string | NodeJS.TypedArray | DataView<ArrayBufferLike> | ArrayBufferLike,
components?: ComponentOverrides,
options?: ReactOptions,
): import("./jsx.d.ts").JSX.Element;
Expand Down
153 changes: 7 additions & 146 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1249,7 +1249,7 @@ impl Default for Object {
}
}

/// used in TOML parser to merge properties.
/// Dotted-key path used by the INI parser (`get_or_put_object`).
Comment thread
robobun marked this conversation as resolved.
///
/// Node types are lifetime-free, so `next` is a raw `*mut Rope`
/// into the bump arena. Segments are bulk-freed at arena reset.
Expand Down Expand Up @@ -1280,7 +1280,7 @@ impl Rope {

/// Re-borrow `next` as `Option<&Rope>`. Same `StoreRef` arena contract:
/// the pointee is a bump allocation valid until arena reset. Centralises
/// the one `unsafe` so the `set_rope`/`get_or_put_*`/`get_rope` walkers
/// the one `unsafe` so the `get_or_put_object`/`get_rope` walkers
/// don't repeat `if !next.is_null() { unsafe { &*next } }` at every hop.
#[inline]
pub fn next_ref<'a>(&self) -> Option<&'a Rope> {
Expand Down Expand Up @@ -1315,7 +1315,7 @@ pub struct RopeQuery<'a> {

// ── live Object accessor surface ───────────────────────────────────────────
// Adapted to the current `Vec` API (`append(v)`, `slice()`, `slice_mut()`).
// `set_rope`/`get_or_put_array`/sort helpers stay in the gated impl below.
// Sort helpers stay in the gated impl below.
impl Object {
pub const EMPTY: Object = Object {
properties: bun_alloc::AstAlloc::vec(),
Expand Down Expand Up @@ -1464,15 +1464,10 @@ pub fn own_key_property_flags(key: &Expr) -> crate::flags::PropertySet {

// `toJS` alias deleted — lives in `js_parser_jsc` extension trait.
impl Object {
pub fn set(&mut self, key: Expr, _bump: &Bump, value: Expr) -> Result<(), SetError> {
let head_key = match key.data.e_string() {
Some(s) => s.data,
None => return Err(SetError::Clobber),
};
if self.has_property(&head_key) {
return Err(SetError::Clobber);
}
// `&mut self` so the borrow checker tracks the write.
/// Appends a property without checking for an existing key. Callers that
/// need duplicate detection must check `as_property` with UTF-8 bytes
/// first — a UTF-16 EString key's raw `data` view is not byte-comparable.
pub fn append_property(&mut self, key: Expr, value: Expr) {
VecExt::append(
&mut self.properties,
G::Property {
Expand All @@ -1482,140 +1477,6 @@ impl Object {
..G::Property::default()
},
);
Ok(())
}

// this is terribly, shamefully slow
pub fn set_rope(&mut self, rope: &Rope, bump: &Bump, value: Expr) -> Result<(), SetError> {
let head_key = match rope.head.data.e_string() {
Some(s) => s.data,
None => return Err(SetError::Clobber),
};
if let Some(existing) = self.get(&head_key) {
match existing.data {
crate::expr::Data::EArray(mut array) => {
let Some(next) = rope.next_ref() else {
array.push(bump, value)?;
return Ok(());
};

if let Some(last) = array.items.last_mut() {
if !matches!(last.data, crate::expr::Data::EObject(_)) {
return Err(SetError::Clobber);
}
last.data
.e_object_mut()
.unwrap()
.set_rope(next, bump, value)?;
return Ok(());
}

array.push(bump, value)?;
return Ok(());
}
crate::expr::Data::EObject(mut object) => {
if let Some(next) = rope.next_ref() {
object.set_rope(next, bump, value)?;
return Ok(());
}

return Err(SetError::Clobber);
}
_ => {
return Err(SetError::Clobber);
}
}
}

let mut value_ = value;
if let Some(next) = rope.next_ref() {
let mut obj = Expr::init(Object::default(), rope.head.loc);
obj.data
.e_object_mut()
.unwrap()
.set_rope(next, bump, value)?;
value_ = obj;
}

VecExt::append(
&mut self.properties,
G::Property {
key: Some(rope.head),
value: Some(value_),
flags: own_key_property_flags(&rope.head),
..G::Property::default()
},
);
Ok(())
}

pub fn get_or_put_array(&mut self, rope: &Rope, bump: &Bump) -> Result<Expr, SetError> {
let head_key = match rope.head.data.e_string() {
Some(s) => s.data,
None => return Err(SetError::Clobber),
};
if let Some(existing) = self.get(&head_key) {
match existing.data {
crate::expr::Data::EArray(mut array) => {
let Some(next) = rope.next_ref() else {
return Ok(existing);
};

if let Some(last) = array.items.last_mut() {
if !matches!(last.data, crate::expr::Data::EObject(_)) {
return Err(SetError::Clobber);
}
return last
.data
.e_object_mut()
.unwrap()
.get_or_put_array(next, bump);
}

return Err(SetError::Clobber);
}
crate::expr::Data::EObject(mut object) => {
let Some(next) = rope.next_ref() else {
return Err(SetError::Clobber);
};
return object.get_or_put_array(next, bump);
}
_ => {
return Err(SetError::Clobber);
}
}
}

if let Some(next) = rope.next_ref() {
let mut obj = Expr::init(Object::default(), rope.head.loc);
let out = obj
.data
.e_object_mut()
.unwrap()
.get_or_put_array(next, bump)?;
VecExt::append(
&mut self.properties,
G::Property {
key: Some(rope.head),
value: Some(obj),
flags: own_key_property_flags(&rope.head),
..G::Property::default()
},
);
return Ok(out);
}

let out = Expr::init(Array::default(), rope.head.loc);
VecExt::append(
&mut self.properties,
G::Property {
key: Some(rope.head),
value: Some(out),
flags: own_key_property_flags(&rope.head),
..G::Property::default()
},
);
Ok(out)
}

/// Assumes each key in the property is a string
Expand Down
Loading
Loading