بنقرة واحدة
fix-lua-binding-api-gotchas
Common Lua binding API issues — comparison operators, item/get, __tostring, scalar inputs
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Common Lua binding API issues — comparison operators, item/get, __tostring, scalar inputs
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Add device information query functions through HAL → ins:: API → language bindings
Add --device all/--timer/--info CLI flags to demos across all languages for competition scoring
Add composite signal operators using existing primitives (no dedicated backend kernels needed)
How to port a cusignal Python/CUDA module to ins::signal C++ using Insight7 primitives and HAL
Profile and optimize binding language (Lua/Julia/Python) demo performance to match or exceed C++ baseline
busted loads _insight.so from ~/.luarocks/lib first, not build/ — must copy .so after rebuild
| name | fix-lua-binding-api-gotchas |
| description | Common Lua binding API issues — comparison operators, item/get, __tostring, scalar inputs |
| source | auto-skill |
| extracted_at | 2026-06-03T15:00:00.000Z |
In Lua, ==, <, <=, >, >= on Array objects return native Lua booleans, NOT Insight Array objects.
-- ❌ WRONG — c is a Lua boolean, not an Array
local c = (a == b)
assert.are.equal(3, c.numel) -- ERROR: attempt to index field 'numel' (a nil value)
-- ✅ CORRECT — use Insight native functions
local c = ins.equal(a, b)
assert.are.equal(3, c.numel)
Available comparison functions:
ins.equal(a, b) instead of a == bins.not_equal(a, b) instead of a ~= bins.less(a, b) instead of a < bins.less_equal(a, b) instead of a <= bins.greater(a, b) instead of a > bins.greater_equal(a, b) instead of a >= bWhy: Sol2 registers __eq/__lt/__le metamethods that return bool for Lua compatibility. The Insight comparison functions return Array.
item() vs get() — scalar extractionarr:item() — returns double for 0-dimensional scalar arrays ONLY. Takes NO arguments. Returns nil for multi-element arrays.arr:get(idx) — returns double for element at 0-based index. Works on any array.get() must handle ALL dtypes (CRITICAL BUG FIX)The get() method in insight_lua.cpp originally only handled F64/F32/I64/I32.
Missing BOOL/I8/I16/U8 cases caused default: return 0.0 — CFAR detections
always returned 0 because the bool detection array was read as all-zeros.
// ❌ ORIGINAL — BOOL falls through to default → always 0
switch (cpu.dtype()) {
case DType::F64: return cpu.data<double>()[idx];
case DType::F32: return (double)cpu.data<float>()[idx];
case DType::I64: return (double)cpu.data<int64_t>()[idx];
case DType::I32: return (double)cpu.data<int32_t>()[idx];
default: return 0.0; // BOOL, I8, I16, U8 all hit this!
}
// ✅ CORRECT — handle all dtypes
switch (cpu.dtype()) {
case DType::F64: return cpu.data<double>()[idx];
case DType::F32: return (double)cpu.data<float>()[idx];
case DType::I64: return (double)cpu.data<int64_t>()[idx];
case DType::I32: return (double)cpu.data<int32_t>()[idx];
case DType::I16: return (double)cpu.data<int16_t>()[idx];
case DType::I8: return (double)cpu.data<int8_t>()[idx];
case DType::U8: return (double)cpu.data<uint8_t>()[idx];
case DType::BOOL: return cpu.data<bool>()[idx] ? 1.0 : 0.0;
default: return 0.0;
}
Symptom: ca_cfar returns threshold correctly but detections are all 0.
tostring(detections) shows true values but detections:get(idx) returns 0.
Why: sol2's get<int64_t>() on a bool array reads 8 bytes from a 1-byte-per-element
array, causing garbage values or zeros.
-- ❌ WRONG — item(idx) doesn't work, item() only for scalars
local sum_val = ins.sum(arr)
print(sum_val:item(0)) -- nil! item doesn't take an index
-- ❌ WRONG — item() on multi-element array returns nil
local data = ins.from_table({1.0, 2.0, 3.0})
print(data:item()) -- nil! not a scalar
-- ✅ CORRECT — use get() for element access
local sum_val = ins.sum(arr)
print(sum_val:get(0)) -- works
local data = ins.from_table({1.0, 2.0, 3.0})
print(data:get(0)) -- 1.0
print(data:get(1)) -- 2.0
Common mistake in tests: Writing c:item(0) when you mean c:get(0). The item() function ignores its argument and returns nil for non-scalar arrays. Always use get(idx) for indexed access.
ins.item(arr, idx) does NOT exist as module functionitem and get are methods on the Array usertype, not module-level functions.
-- ❌ WRONG
local v = ins.item(arr, 0)
-- ✅ CORRECT
local v = arr:item(0) -- or arr:get(0)
__tostring metamethod — uses C++ ins::to_string()The __tostring metamethod calls insight_array_tostring() (C API) which delegates to ins::to_string(). Output matches C++ format:
print(arr)
-- Array(shape=[2, 2], dtype=float64, place=cpu:0,
-- [[1., 2.],
-- [3., 4.]])
If you see <insight.Array shape=...> (old format), the binding is using the fallback path. Ensure insight_array_tostring is linked correctly.
When binding a C++ function with default parameters via sig["func"] = &ns::func, sol2 does NOT pass the defaults. This causes segfaults (not exceptions).
// ❌ WRONG — default bool append=true not passed, segfaults
sig["write_bin"] = &signal::write_bin;
// ✅ CORRECT — use lambda + sol::optional
sig["write_bin"] = [](const std::string &file, const Array &data,
sol::optional<bool> append) {
signal::write_bin(file, data, append.value_or(true));
};
Lua signal functions (mel2hz, hz2mel, etc.) are registered via sol2 and expect const Array&, not Lua numbers.
-- ❌ WRONG — passes scalar, crashes
local hz = ins.signal.mel2hz(1000.0)
-- ✅ CORRECT — wrap scalar in Array
local hz_arr = ins.signal.mel2hz(ins.from_table({1000.0}))
local hz = hz_arr:get(0)
Window functions that take integer M (hann, hamming, etc.) work with plain integers since sol2 auto-converts int → int64_t.
Busted defaults to *_spec.lua pattern. For test_*.lua files:
busted --pattern="test_.+%.lua" .
-- ❌ WRONG — raw table causes segfault
ins.tile(a, {2})
-- ✅ CORRECT
ins.tile(a, ins.Shape({2}))
Similarly, reshape is a method:
-- ❌ WRONG
ins.reshape(a, {2, 3})
-- ✅ CORRECT
a:reshape({2, 3})
load_backend returns boolean, does NOT throwUnlike Python (which throws py::value_error), Lua's load_backend catches
C++ exceptions internally and returns true/false:
-- ❌ WRONG — pcall always succeeds (load_backend returns false, not throw)
local ok, _ = pcall(function() ins.load_backend("cuda") end)
return ok -- always true!
-- ✅ CORRECT — check return value
local ok, result = pcall(function() return ins.load_backend("cuda") end)
return ok and result
Same pattern for Julia:
# ❌ WRONG — always returns true
function gpu_available()
try
Insight.load_backend("cuda")
return true
catch
return false
end
end
# ✅ CORRECT — check return value
function gpu_available()
try
return Insight.load_backend("cuda")
catch
return false
end
end
--no-auto-insulate requiredBusted's auto-insulate feature creates sandboxed environments per test file,
which breaks require("_insight") for sol2 Array usertypes. The numel
property returns userdata instead of number when run alongside other tests.
# ❌ WRONG — auto-insulate breaks Array usertypes
busted --pattern="test_.+%.lua" .
# ✅ CORRECT — disable sandboxing
busted --no-auto-insulate --pattern="test_.+%.lua" .
-- ❌ WRONG — fs is not a parameter
M.fm_demod = _wrap({ "x", "fs" }, function(x, fs)
return sig.fm_demod(x, fs)
end)
-- ✅ CORRECT — C++ takes axis (default -1)
M.fm_demod = _wrap({ "x", "axis" }, function(x, axis)
return sig.fm_demod(x, axis or -1)
end)
-- ❌ WRONG — "lowpass" goes to window param
ins.signal.firwin(11, {0.3}, "lowpass", true)
-- ✅ CORRECT — nil for window, then pass_zero
ins.signal.firwin(11, {0.3}, nil, "lowpass", true)
-- ❌ WRONG — passes float where int expected
ins.signal.morlet(5.0)
-- ✅ CORRECT
ins.signal.morlet(16, 5.0) -- M=16, w=5.0
ins.signal.morlet2(16, 1.0, 5.0) -- M=16, s=1.0, w=5.0
-- ❌ WRONG — data first
ins.signal.write_bin(data_array, "/tmp/out.bin")
-- ✅ CORRECT — filepath first
ins.signal.write_bin("/tmp/out.bin", data_array)
-- append defaults to true; pass false to overwrite:
ins.signal.write_bin("/tmp/out.bin", data_array, false)
from_table requires 1-based indexingLua tables with 0-based keys are SILENTLY ignored by from_table. Both the
Lua wrapper (#tbl, for i = 1, len) and C++ flatten_lua_table (for i=1 to t.size())
only read 1-based keys.
-- ❌ WRONG — 0-based loop, from_table skips index 0
local t = {}
for i = 0, N-1 do
t[i] = math.sin(i) -- t[0] is ignored!
end
local arr = ins.from_table(t) -- numel = N-1, missing first element
-- ✅ CORRECT — 1-based loop
local t = {}
for i = 1, N do
t[i] = math.sin(i-1)
end
local arr = ins.from_table(t) -- numel = N
For 2D arrays, use nested tables:
-- ❌ WRONG — flat table, from_table sees 1D
local flat = {}
for p = 0, N_PULSES-1 do
for i = 0, N-1 do
flat[p*N + i + 1] = data[p][i]
end
end
local arr = ins.from_table(flat) -- shape {N_PULSES*N}, not {N_PULSES, N}!
-- ✅ CORRECT — nested table
local nested = {}
for p = 1, N_PULSES do
nested[p] = {}
for i = 1, N do
nested[p][i] = data[p-1][i-1]
end
end
local arr = ins.from_table(nested) -- shape {N_PULSES, N}
Why: Lua tables can have 0-based keys, but from_table and #tbl ignore them.
This causes subtle off-by-one bugs and segfaults when downstream code assumes
the full array size.
When a C++ function exists but isn't bound to Lua, add it in insight_lua.cpp:
// Direct binding (no default params)
m["conj"] = &conj;
m["flatnonzero"] = &flatnonzero;
m["masked_select"] = &masked_select;
// Lambda wrapper (with default params — sol2 doesn't propagate C++ defaults)
m["searchsorted"] = [](const Array &x, const Array &v,
sol::optional<std::string> side) {
return searchsorted(x, v, side.value_or("left"));
};
m["device_count"] = [](int kind) {
return static_cast<int>(device_count(static_cast<DeviceKind>(kind)));
};
// Table-based vector params (fft2/ifft2)
m["fft2"] = [](const Array &x, sol::optional<sol::table> s,
sol::optional<sol::table> axes, sol::optional<std::string> norm) {
std::vector<int64_t> s_vec;
if (s) { for (auto &kv : *s) s_vec.push_back(kv.second.as<int64_t>()); }
std::vector<int> axes_vec = {-2, -1};
if (axes) { axes_vec.clear(); for (auto &kv : *axes) axes_vec.push_back(kv.second.as<int>()); }
return fft::fft2(x, s_vec, axes_vec, norm.value_or("backward"));
};
to() must wrap device transfer in try/catch (sol2 lambda limitation)sol2 does NOT catch C++ exceptions from lambdas. If Array::to(Place) throws
(e.g., GPU backend not available), the exception crosses the sol2 boundary
and causes std::terminate — Lua's pcall cannot catch it.
Fix: Wrap to() with sol::this_state + luaL_error:
array_type["to"] = sol::overload(
[](const Array &a, const Place &p, sol::this_state ts) -> Array {
try {
return a.to(p);
} catch (const std::exception &e) {
luaL_error(ts, "%s", e.what());
return Array(); // never reached
}
},
[](const Array &a, DType dt) { return a.to(dt); }
);
Why sol::this_state: sol2 automatically injects lua_State* when the
lambda has a sol::this_state parameter. luaL_error then raises a proper
Lua error that pcall can catch.
Symptom: terminate called after throwing an instance of 'ins::Exception'
when calling a:to(ins.GPUPlace(0)) without GPU backend. Process crashes
instead of Lua error.
Also applies to: Any C++ function bound via sol2 lambda that can throw
exceptions crossing the FFI boundary (e.g., ins.init(), ins.set_device()).
a:to(1) matches DType overload, NOT device transferProblem: to() has overloads for Place, DType, and Place+DType. When
Lua calls a:to(1), sol2 matches the DType overload first (since 1 is a
number), converting the array to DType 1 (= BOOL) instead of transferring to GPU.
Fix: Add an explicit int → Place overload BEFORE the DType overload:
array_type["to"] = sol::overload(
[](const Array &a, const Place &p, sol::this_state ts) -> Array { ... },
// ADD THIS: int device_type → Place conversion
[](const Array &a, int device_type, sol::this_state ts) -> Array {
Place p = device_type == 1 ? GPUPlace(0) : CPUPlace();
return a.to(p);
},
[](const Array &a, DType dt) { return a.to(dt); }, // DType AFTER int
...
);
Why: sol2 overload resolution is first-match. int is more specific than
DType (which is also int32_t), so it must come first.
Problem: Lua 5.3 distinguishes 1e7 (float) from 10000000 (integer).
sol2 rejects floats for int64_t parameters: "not a numeric type that fits
exactly an integer".
Fix: Use integer literals or math.floor():
-- ❌ WRONG in Lua 5.3
local a = ins.randn({1e7}, ins.float32) -- float rejected
-- ✅ CORRECT
local a = ins.randn({10000000}, ins.float32) -- integer literal
-- or
local N = math.floor(1e7)
local a = ins.randn({N}, ins.float32)