Skip to content

Commit a349653

Browse files
authored
bigint (#3526)
Adds the `bigint` primitive type, which represents an arbitrary-precision signed integer with sign-extended two's complement bitwise semantics. Like `string`, it is an immutable heap-allocated object. - Includes `bigint` literals: like other popular languages with built-in arbitrary-size integers, we use the `n` suffix as in `123n` or `9999999999999999999999999n` - `int` can always be safely widened into `bigint`. We only do this implicitly for binary ops: `1n + 2 == 3n` - Operations that can produce very large `bigint`s (such as left shift) may throw an `AllocFailure` panic if the size of the `bigint` would exceed `1 << 28` bits (about 33.6MB). For example: `1n << (1n << 29n)` will panic. This allows programs to recover in BAML instead of crashing from an out-of-memory error or other allocation problems. - Helpful diagnostics for malformed `bigint` literals like `123N` or `1.23n` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** - Added Bigint primitive with literal syntax (e.g. 42n), int→bigint widening, full arithmetic/bitwise/shift ops, comparisons, and typed bytecode/VM support. - Bigint standard library: parse, to_json, abs/min/max/clamp/isqrt/pow/ilog/random. * **Runtime / VM** - Heap/object representation, allocation paths, serializers, and new panics (negative-bit-shift, alloc failure) for bigint. * **FFI / SDK / Protobuf** - End-to-end bigint round‑trip support in bridges and SDKs (Python, Node, protobufs). * **Tests** - Extensive new and updated tests covering syntax, coercion, ops, serialization, edge cases. * **Chores** - Workspace dependencies and codegen/type mappings added for bigint support. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/BoundaryML/baml/pull/3526) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a6f67a4 commit a349653

258 files changed

Lines changed: 12369 additions & 1501 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

baml_language/Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

baml_language/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ salsa = { version = "0.26", default-features = false, features = [
195195
] }
196196
serde = { version = "1.0.197", features = [ "derive" ] }
197197
serde-wasm-bindgen = "0.6"
198-
serde_json = { version = "1.0.113", features = [ "preserve_order" ] }
198+
serde_json = { version = "1.0.113", features = [ "arbitrary_precision", "preserve_order" ] }
199199
serde_yaml = "0.9"
200200
similar = { version = "2.4.0", features = [ "inline" ] }
201201
smol_str = { version = "0.3", features = [ "serde" ] }
@@ -244,6 +244,7 @@ vfs = "0.12"
244244
wasm-logger = "0.2.0"
245245
web-time = "1.1.0"
246246
getrandom = { version = "0.2" }
247+
num-bigint = { version = "0.4", features = [ "rand", "serde" ] }
247248
rsa = { version = "0.9", default-features = false, features = [ "std", "pem" ] }
248249
sha2 = { version = "0.10", features = [ "oid" ] }
249250

baml_language/crates/baml_base/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ workspace = true
1818

1919
[dependencies]
2020
ariadne = { workspace = true }
21+
num-bigint = { workspace = true }
2122
salsa = { workspace = true }
2223
serde = { workspace = true }
2324
smol_str = { workspace = true }

baml_language/crates/baml_base/src/core_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ impl fmt::Display for MediaKind {
241241
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
242242
pub enum Literal {
243243
Int(i64),
244+
Bigint(num_bigint::BigInt),
244245
Float(String),
245246
String(String),
246247
Bool(bool),
@@ -251,6 +252,7 @@ impl fmt::Display for Literal {
251252
match self {
252253
Literal::String(s) => write!(f, "{s:?}"),
253254
Literal::Int(i) => write!(f, "{i}"),
255+
Literal::Bigint(n) => write!(f, "{n}n"),
254256
Literal::Float(s) => write!(f, "{s}"),
255257
Literal::Bool(b) => write!(f, "{b}"),
256258
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/// Arbitrary-precision signed integer.
2+
///
3+
/// # Bitwise on negatives
4+
///
5+
/// `&`, `|`, `^` use two's-complement semantics — bigints are treated as if
6+
/// they had an infinite sign-extended bit string. So `(-1n) & 1n == 1n`,
7+
/// `(-1n) | 0n == -1n`, `(-1n) ^ 0n == -1n`. This matches JavaScript `BigInt`
8+
/// and Python `int`.
9+
///
10+
/// # Panics
11+
///
12+
/// Bigint operators and methods can raise the following unrecoverable
13+
/// panics. These are not declared via `throws` because they signal that
14+
/// the program tried to do something invalid (rather than a recoverable
15+
/// runtime condition):
16+
///
17+
/// - `baml.panics.DivisionByZero` — `a / 0n` or `a % 0n`.
18+
/// - `baml.panics.NegativeBitShift` — `a << -1n` or `a >> -1n`. The shift
19+
/// count must be non-negative.
20+
/// - `baml.panics.AllocFailure` — the operand or result would require
21+
/// more than ~268M bits (`bigint`'s workspace cap). Sources:
22+
/// * `a * b`, `a << n`, `a.pow(n)` where the predicted result exceeds
23+
/// the cap.
24+
/// * `bigint.parse(s)` where `s` carries more decimal digits than the
25+
/// cap permits.
26+
class Bigint {
27+
//baml:mut_vm
28+
function to_json(self) -> baml.json.json {
29+
$rust_function
30+
}
31+
// ─── Comparisons / clamping ───────────────────────────────────────────────
32+
33+
/// Returns the absolute value of `self`.
34+
/// # Examples
35+
/// ```
36+
/// (-7n).abs() // 7n
37+
/// (3n).abs() // 3n
38+
/// (0n).abs() // 0n
39+
/// ```
40+
function abs(self) -> bigint {
41+
$rust_function
42+
}
43+
44+
/// Returns the smaller of `self` and `other`.
45+
///
46+
/// # Examples
47+
/// ```
48+
/// (3n).min(5n) // 3n
49+
/// (3n).min(3) // 3n (note we cast up from `int` to `bigint` here)
50+
/// (-2n).min(0n) // -2n
51+
/// ```
52+
function min(self, other: bigint) -> bigint {
53+
$rust_function
54+
}
55+
56+
/// Returns the larger of `self` and `other`.
57+
///
58+
/// # Examples
59+
/// ```
60+
/// (3n).max(5n) // 5n
61+
/// (3n).max(3) // 3n (note we cast up from `int` to `bigint` here)
62+
/// (-2n).max(0n) // 0n
63+
/// ```
64+
function max(self, other: bigint) -> bigint {
65+
$rust_function
66+
}
67+
68+
/// Clamps `self` into the range `[min, max]`.
69+
///
70+
/// Equivalent to `self.min(max).max(min)`. Callers should pass `min <= max`;
71+
/// if `min > max` the result is always `min` (the lower-clamp wins because
72+
/// it runs after the upper-clamp).
73+
///
74+
/// # Examples
75+
/// ```
76+
/// (5n).clamp(0n, 10n) // 5n
77+
/// (-3n).clamp(0, 10) // 0n (note we cast up from `int` to `bigint` here)
78+
/// (15n).clamp(0n, 10n) // 10n
79+
/// ```
80+
function clamp(self, min: bigint, max: bigint) -> bigint {
81+
$rust_function
82+
}
83+
84+
// ─── Math ─────────────────────────────────────────────────────────────────
85+
86+
/// Returns the integer square root of `self` — that is, the largest `bigint`
87+
/// `r` such that `r * r <= self`.
88+
///
89+
/// Throws `InvalidArgument` if `self` is negative.
90+
///
91+
/// # Examples
92+
/// ```
93+
/// (10n).isqrt() // 3n
94+
/// (16n).isqrt() // 4n
95+
/// (0n).isqrt() // 0n
96+
/// (-1n).isqrt() // throws
97+
/// ```
98+
function isqrt(self) -> bigint throws root.errors.InvalidArgument {
99+
$rust_function
100+
}
101+
102+
/// Returns `self ** exp`.
103+
///
104+
/// `0 ** 0` returns `1`, by convention.
105+
///
106+
/// If `exp` is negative the result is `0`, since the mathematical value
107+
/// `self ** -n = 1 / self ** n` is in `(-1, 1)` for `|self| > 1` and rounds
108+
/// to zero. (For `self == 1` or `self == -1` the value is `±1` which rounds
109+
/// to itself; this implementation still returns `0` for negative exponents
110+
/// uniformly.)
111+
///
112+
/// # Examples
113+
/// ```
114+
/// (2n).pow(10n) // 1024n
115+
/// (2n).pow(0n) // 1n
116+
/// (0n).pow(0n) // 1n (convention)
117+
/// (2n).pow(-1n) // 0n
118+
/// (10n).pow(100n) // a very large number
119+
/// (-2n).pow(3n) // -8n
120+
/// ```
121+
function pow(self, exp: bigint) -> bigint {
122+
$rust_function
123+
}
124+
125+
/// Returns the integer logarithm of `self` in the given `base`, rounded
126+
/// down — i.e. the largest `n` such that `base ** n <= self`.
127+
///
128+
/// Throws `InvalidArgument` if `self <= 0` or `base < 2`.
129+
///
130+
/// # Examples
131+
/// ```
132+
/// (1000n).ilog(10n) // 3n
133+
/// (1024n).ilog(2n) // 10n
134+
/// (1n).ilog(10n) // 0n
135+
/// (0n).ilog(10n) // throws
136+
/// (10n).ilog(1n) // throws
137+
/// ```
138+
function ilog(self, base: bigint) -> bigint throws root.errors.InvalidArgument {
139+
$rust_function
140+
}
141+
142+
// ─── Parsing ──────────────────────────────────────────────────────────────
143+
144+
/// Parses `text` as a base-ten signed integer.
145+
///
146+
/// Accepts an optional leading `+` or `-` sign followed by one or more
147+
/// ASCII digits. No surrounding whitespace, no underscore separators,
148+
/// no hex / octal / binary prefix (`0x` / `0o` / `0b`), no Unicode digits,
149+
/// no scientific notation. Callers needing those should preprocess the
150+
/// string.
151+
///
152+
/// Throws `ParseError` if `text` is empty or contains a non-digit character.
153+
///
154+
/// # Examples
155+
/// ```
156+
/// bigint.parse("42") // 42n
157+
/// bigint.parse("-7") // -7n
158+
/// bigint.parse("+0") // 0n
159+
/// bigint.parse("") // throws — empty
160+
/// bigint.parse("12a") // throws — non-digit
161+
/// bigint.parse("0x2a") // throws — hex prefix not accepted
162+
/// bigint.parse("1_000") // throws — underscores not accepted
163+
/// bigint.parse(" 5 ") // throws — whitespace not allowed; trim first
164+
/// bigint.parse("99999999999999999999") // 99999999999999999999n
165+
/// ```
166+
function parse(text: string) -> bigint throws root.errors.ParseError {
167+
$rust_function
168+
}
169+
170+
/// Returns a uniformly distributed random integer in the half-open range
171+
/// `[lower, upper)`.
172+
///
173+
/// Throws `InvalidArgument` if `lower >= upper` (the range would be empty).
174+
/// Uses the host's cryptographic entropy source — no user-supplied RNG
175+
/// handle.
176+
///
177+
/// # Examples
178+
/// ```
179+
/// bigint.random(0n, 10n) // some value in {0, 1, ..., 9}
180+
/// bigint.random(-5n, 5n) // some value in {-5, -4, ..., 4}
181+
/// bigint.random(0n, 1n) // always 0 (single-element range)
182+
/// bigint.random(5n, 5n) // throws — empty range
183+
/// bigint.random(10n, 0n) // throws — lower > upper
184+
/// ```
185+
function random(lower: bigint, upper: bigint) -> bigint throws root.errors.InvalidArgument {
186+
$rust_function
187+
}
188+
}

baml_language/crates/baml_builtins2/baml_std/baml/ns_panics/panics.baml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,11 @@ class HostUnavailable {
6363
message string
6464
}
6565

66-
type Panic = DivisionByZero | IndexOutOfBounds | MapKeyNotFound | StackOverflow | AssertionFailed | Unreachable | Cancelled | UserPanic | Exit | AllocFailure | HostUnavailable
66+
/// The right operand of a bigint shift (`<<` / `>>`) was negative.
67+
/// The type system can't rule this out because the shift count is a
68+
/// runtime `bigint` value.
69+
class NegativeBitShift {
70+
message string
71+
}
72+
73+
type Panic = DivisionByZero | IndexOutOfBounds | MapKeyNotFound | StackOverflow | AssertionFailed | Unreachable | Cancelled | UserPanic | Exit | AllocFailure | HostUnavailable | NegativeBitShift

baml_language/crates/baml_builtins2/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pub const ALL: &[BuiltinFile] = &[
8484
builtin!("baml", "containers.baml"),
8585
builtin!("baml", "core.baml"),
8686
builtin!("baml", "int.baml"),
87+
builtin!("baml", "bigint.baml"),
8788
builtin!("baml", "float.baml"),
8889
builtin!("baml", "bool.baml"),
8990
builtin!("baml", "null.baml"),

0 commit comments

Comments
 (0)