#4901·baml

[bug] Node bridge 0.19.0: a host callable returning an integral number for a `float` panics with HostContractViolation

Author: BenSpexCreated Sep 16, 2026Updated Sep 16, 2026

Summary

With @boundaryml/baml-bridge 0.19.0, a JavaScript host callable whose declared return type is float (or a class with a float field) panics when it returns an integral JS number such as 612 or 1. The bridge encodes Number.isInteger values as int, and the engine's host contract check rejects int for float. JavaScript has no way to mark a number as a float, so a Node host cannot return a whole-number float at all. The same value passed as a plain function argument is accepted and coerced.

Because it is a panic, BAML code cannot catch it, and the whole call fails.

Environment

  • BAML toolchain 0.19.0 (baml.toml: [package] name = "host-float-repro" and [toolchain] version = "0.19.0"; baml check passes), @boundaryml/baml-bridge 0.19.0
  • Node v24.11.1, Linux x86_64

Repro

baml_src/main.baml:

baml
class Size {
    width: float,
}

// A host callable whose declared return holds a float field.
function MeasureClass(measure: () -> Size throws never) -> float throws never {
    measure().width
}

// A host callable whose declared return is a bare float.
function MeasureFloat(measure: () -> float throws never) -> float throws never {
    measure()
}

// A plain argument, not a host callable.
function EchoSize(size: Size) -> float {
    size.width
}

repro.mjs (run with BRIDGE=<path to @boundaryml/baml-bridge/dist/index.js> node repro.mjs):

javascript
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const bridge = await import(process.env.BRIDGE);
const dir = new URL('./baml_src', import.meta.url).pathname;
const handle = bridge.BamlRuntime.initializeRuntime(dir, { 'main.baml': readFileSync(join(dir, 'main.baml'), 'utf8') });
class Size { constructor(init) { Object.assign(this, init); } }
bridge.setTypeMap(bridge.BamlTypeMap.fromLazyEntries({ classes: { 'user.Size': () => Size }, enums: {}, typeAliases: {} }));
const run = async (label, fn, kwargs) => {
  try { console.log(label, '->', JSON.stringify((await bridge.callFunction(handle, fn, kwargs)).result())); }
  catch (e) { console.log(label, '-> ERROR', String(e?.message ?? e).split('\n')[0]); }
};
await run('host returns class, width 612.5', 'MeasureClass', { measure: () => new Size({ width: 612.5 }) });
await run('host returns class, width 612  ', 'MeasureClass', { measure: () => new Size({ width: 612 }) });
await run('host returns float 0.5         ', 'MeasureFloat', { measure: () => 0.5 });
await run('host returns float 1           ', 'MeasureFloat', { measure: () => 1 });
await run('argument class, width 612      ', 'EchoSize', { size: new Size({ width: 612 }) });
process.exit(0);

Output

host returns class, width 612.5 -> 612.5
host returns class, width 612   -> ERROR baml panic: baml.panics.HostContractViolation: host callable returned a value of type `int` that does not match the declared return type `float`
host returns float 0.5          -> 0.5
host returns float 1            -> ERROR baml panic: baml.panics.HostContractViolation: host callable returned a value of the wrong type: host callable returned a value of type `int` that does not match the declared return type `float` (expected float)
argument class, width 612       -> 612

Expected

An integral JS number returned where the declared type is float is accepted as a float, the same way a function argument is coerced. The contract check could widen int to float when the declared type is float. Alternatively, the bridge could encode host return values against the declared return type.