[bug] Node bridge 0.19.0: a host callable returning an integral number for a `float` panics with HostContractViolation
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 checkpasses),@boundaryml/baml-bridge0.19.0 - Node v24.11.1, Linux x86_64
Repro
baml_src/main.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):
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 -> 612Expected
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.
Source: BoundaryML/baml