diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index 65e08abc..175f7414 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include // NOLINT #include #include // NOLINT +#include #include // NOLINT #include #include @@ -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 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 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().c_str()); \ if (id == -1) { \ - mju_error("Invalid name, MjModel." #accessor_name " not found"); \ + mju_error("%s", KeyErrorMessage(ptr_, OBJTYPE, ptr_->nfield, val.as(), #accessor_name).c_str()); \ } \ return MjModel##Name##Accessor(ptr_, id); \ } else if (val.isNumber()) { \ int id = val.as(); \ 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 { \ diff --git a/wasm/codegen/generators/structs.py b/wasm/codegen/generators/structs.py index 5cb91fc9..1dae82cf 100644 --- a/wasm/codegen/generators/structs.py +++ b/wasm/codegen/generators/structs.py @@ -396,13 +396,13 @@ def build_struct_header( if (val.isString()) { \\ int id = mj_name2id(ptr_, OBJTYPE, val.as().c_str()); \\ if (id == -1) { \\ - mju_error("Invalid name, MjModel." #accessor_name " not found"); \\ + mju_error("%s", KeyErrorMessage(ptr_, OBJTYPE, ptr_->nfield, val.as(), #accessor_name).c_str()); \\ } \\ return MjModel##Name##Accessor(ptr_, id); \\ } else if (val.isNumber()) { \\ int id = val.as(); \\ 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 { \\ diff --git a/wasm/codegen/templates/bindings.cc b/wasm/codegen/templates/bindings.cc index a50adc35..dadcdbc2 100644 --- a/wasm/codegen/templates/bindings.cc +++ b/wasm/codegen/templates/bindings.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include // NOLINT #include #include // NOLINT +#include #include // NOLINT #include #include @@ -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 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 val MakeValArray(const char* (&strings)[N]) { val result = val::array(); diff --git a/wasm/tests/bindings_test.ts b/wasm/tests/bindings_test.ts index faa3dc3e..3b524964 100644 --- a/wasm/tests/bindings_test.ts +++ b/wasm/tests/bindings_test.ts @@ -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