Improve error messages for named/indexed lookups in WASM bindings.

PiperOrigin-RevId: 862682001
Change-Id: I6381af64b724de0ab5aee43cdc89ce463b590cf1
This commit is contained in:
Google DeepMind
2026-01-29 04:52:48 -08:00
committed by Copybara-Service
parent f6796f36b2
commit 59c5017073
4 changed files with 118 additions and 5 deletions
+49 -2
View File
@@ -19,6 +19,7 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
@@ -27,6 +28,7 @@
#include <cstring> // NOLINT
#include <memory>
#include <optional> // NOLINT
#include <sstream>
#include <string> // NOLINT
#include <string_view>
#include <vector>
@@ -102,6 +104,51 @@ __attribute__((constructor)) void InitMuJoCoErrorHandler() {
mju_user_error = ThrowMujocoErrorToJS;
}
// Generates a descriptive error message for when a key lookup fails.
// The message includes the invalid name and a list of valid names of the
// specified object type currently present in the model.
//
// Arguments:
// model: Pointer to the mjModel.
// objtype: The mjOBJ_* enum value representing the object type.
// count: The number of objects of the given type in the model.
// name: The invalid name that was looked up.
//
// Returns:
// A string containing the error message.
std::string KeyErrorMessage(const mjModel* model, int objtype, int count,
std::string_view name, std::string_view accessor_name) {
std::vector<std::string> valid_names;
valid_names.reserve(count);
for (int i = 0; i < count; ++i) {
const char* n = mj_id2name(model, objtype, i);
if (n) {
valid_names.push_back(n);
}
}
std::sort(valid_names.begin(), valid_names.end());
std::ostringstream message;
message << "Invalid name '" << name << "' for " << accessor_name
<< ". Valid names: [";
for (size_t i = 0; i < valid_names.size(); ++i) {
message << "'" << valid_names[i] << "'";
if (i < valid_names.size() - 1) {
message << ", ";
}
}
message << "]";
return message.str();
}
std::string IndexErrorMessage(int index, int count,
std::string_view accessor_name) {
std::ostringstream message;
message << "Invalid index " << index << " for " << accessor_name
<< ". Valid indices from 0 to " << count - 1;
return message.str();
}
template <size_t N>
val MakeValArray(const char* (&strings)[N]) {
val result = val::array();
@@ -5299,13 +5346,13 @@ struct MjModel {
if (val.isString()) { \
int id = mj_name2id(ptr_, OBJTYPE, val.as<std::string>().c_str()); \
if (id == -1) { \
mju_error("Invalid name, MjModel." #accessor_name " not found"); \
mju_error("%s", KeyErrorMessage(ptr_, OBJTYPE, ptr_->nfield, val.as<std::string>(), #accessor_name).c_str()); \
} \
return MjModel##Name##Accessor(ptr_, id); \
} else if (val.isNumber()) { \
int id = val.as<int>(); \
if (id < 0 || id >= ptr_->nfield) { \
mju_error_i("Invalid id %d for MjModel." #accessor_name, id); \
mju_error("%s", IndexErrorMessage(id, ptr_->nfield, #accessor_name).c_str()); \
} \
return MjModel##Name##Accessor(ptr_, id); \
} else { \
+2 -2
View File
@@ -396,13 +396,13 @@ def build_struct_header(
if (val.isString()) { \\
int id = mj_name2id(ptr_, OBJTYPE, val.as<std::string>().c_str()); \\
if (id == -1) { \\
mju_error("Invalid name, MjModel." #accessor_name " not found"); \\
mju_error("%s", KeyErrorMessage(ptr_, OBJTYPE, ptr_->nfield, val.as<std::string>(), #accessor_name).c_str()); \\
} \\
return MjModel##Name##Accessor(ptr_, id); \\
} else if (val.isNumber()) { \\
int id = val.as<int>(); \\
if (id < 0 || id >= ptr_->nfield) { \\
mju_error_i("Invalid id %d for MjModel." #accessor_name, id); \\
mju_error("%s", IndexErrorMessage(id, ptr_->nfield, #accessor_name).c_str()); \\
} \\
return MjModel##Name##Accessor(ptr_, id); \\
} else { \\
+47
View File
@@ -19,6 +19,7 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
@@ -27,6 +28,7 @@
#include <cstring> // NOLINT
#include <memory>
#include <optional> // NOLINT
#include <sstream>
#include <string> // NOLINT
#include <string_view>
#include <vector>
@@ -102,6 +104,51 @@ __attribute__((constructor)) void InitMuJoCoErrorHandler() {
mju_user_error = ThrowMujocoErrorToJS;
}
// Generates a descriptive error message for when a key lookup fails.
// The message includes the invalid name and a list of valid names of the
// specified object type currently present in the model.
//
// Arguments:
// model: Pointer to the mjModel.
// objtype: The mjOBJ_* enum value representing the object type.
// count: The number of objects of the given type in the model.
// name: The invalid name that was looked up.
//
// Returns:
// A string containing the error message.
std::string KeyErrorMessage(const mjModel* model, int objtype, int count,
std::string_view name, std::string_view accessor_name) {
std::vector<std::string> valid_names;
valid_names.reserve(count);
for (int i = 0; i < count; ++i) {
const char* n = mj_id2name(model, objtype, i);
if (n) {
valid_names.push_back(n);
}
}
std::sort(valid_names.begin(), valid_names.end());
std::ostringstream message;
message << "Invalid name '" << name << "' for " << accessor_name
<< ". Valid names: [";
for (size_t i = 0; i < valid_names.size(); ++i) {
message << "'" << valid_names[i] << "'";
if (i < valid_names.size() - 1) {
message << ", ";
}
}
message << "]";
return message.str();
}
std::string IndexErrorMessage(int index, int count,
std::string_view accessor_name) {
std::ostringstream message;
message << "Invalid index " << index << " for " << accessor_name
<< ". Valid indices from 0 to " << count - 1;
return message.str();
}
template <size_t N>
val MakeValArray(const char* (&strings)[N]) {
val result = val::array();
+20 -1
View File
@@ -1879,7 +1879,26 @@ describe('MuJoCo WASM Bindings', () => {
it('should throw an error for invalid geom names in model', () => {
expect(() => {
model!.geom('badgeom');
}).toThrowError('MuJoCo Error: Invalid name, MjModel.geom not found');
})
.toThrowError(
`MuJoCo Error: Invalid name 'badgeom' for geom. Valid names: \['mybox', 'myplane'\]`);
});
// Corresponds to
// bindings_test.py:test_named_indexing_invalid_index_in_model
it('should throw an error for invalid geom indices in model', () => {
const numGeoms = model!.ngeom;
expect(() => {
model!.geom(numGeoms);
})
.toThrowError(
`MuJoCo Error: Invalid index 3 for geom. Valid indices from 0 to 2`);
expect(() => {
model!.geom(-1);
})
.toThrowError(
`MuJoCo Error: Invalid index -1 for geom. Valid indices from 0 to 2`);
});
// Corresponds to bindings_test.py:test_named_indexing_geom_size