Skip to content

Commit aef9c2a

Browse files
authored
feat: add location information to function call context (#492)
1 parent ab3d48c commit aef9c2a

File tree

9 files changed

+129
-22
lines changed

9 files changed

+129
-22
lines changed

assets/tests/push_children/adding_empty_list_does_nothing.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ local entity = world.spawn()
22

33
world.push_children(entity, {})
44

5-
assert(#world.get_children(entity) == 0)
5+
assert(#world.get_children(entity) == 0)

crates/bevy_mod_scripting_asset/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ categories.workspace = true
1212
readme.workspace = true
1313

1414
[dependencies]
15+
bevy_mod_scripting_display = { workspace = true }
16+
bevy_mod_scripting_derive = { workspace = true }
1517
bevy_reflect = { workspace = true }
1618
bevy_asset = { workspace = true }
1719
bevy_log = { workspace = true }

crates/bevy_mod_scripting_asset/src/language.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
//! Defines supported scripting languages and their file extensions.
22
3+
use bevy_mod_scripting_derive::DebugWithTypeInfo;
34
use serde::{Deserialize, Serialize};
45
use std::borrow::Cow;
56

67
/// Represents a scripting language. Languages which compile into another language should use the target language as their language.
7-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
8+
#[derive(
9+
Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize, DebugWithTypeInfo,
10+
)]
11+
#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")]
812
pub enum Language {
913
/// The Rhai scripting language
1014
Rhai,

crates/bevy_mod_scripting_bindings/src/error.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! Error types for the bindings
22
use crate::{
3-
Namespace, ReflectBaseType, ReflectReference, access_map::ReflectAccessId,
3+
FunctionCallContext, Namespace, ReflectBaseType, ReflectReference, access_map::ReflectAccessId,
44
script_value::ScriptValue,
55
};
66
use bevy_ecs::entity::Entity;
7+
use bevy_mod_scripting_asset::Language;
78
use bevy_mod_scripting_derive::DebugWithTypeInfo;
89
use bevy_mod_scripting_display::{
910
DebugWithTypeInfo, DisplayWithTypeInfo, GetTypeInfo, OrFakeId, PrintReflectAsDebug,
@@ -148,6 +149,8 @@ pub enum InteropError {
148149
on: Box<Namespace>,
149150
/// The error that occurred
150151
error: Box<InteropError>,
152+
/// The context at the time of the call
153+
context: Box<Option<FunctionCallContext>>,
151154
},
152155
/// An error occurred when converting a function argument
153156
FunctionArgConversionError {
@@ -353,11 +356,13 @@ impl InteropError {
353356
function_name: impl Display,
354357
on: Namespace,
355358
error: InteropError,
359+
context: Option<FunctionCallContext>,
356360
) -> Self {
357361
Self::FunctionInteropError {
358362
function_name: Box::new(function_name.to_string()),
359363
on: Box::new(on),
360364
error: Box::new(error),
365+
context: Box::new(context),
361366
}
362367
}
363368

@@ -423,11 +428,16 @@ impl InteropError {
423428
}
424429

425430
/// Creates a new missing function error.
426-
pub fn missing_function(function_name: impl Display, on: Namespace) -> Self {
431+
pub fn missing_function(
432+
function_name: impl Display,
433+
on: Namespace,
434+
context: Option<FunctionCallContext>,
435+
) -> Self {
427436
Self::FunctionInteropError {
428437
function_name: Box::new(function_name.to_string()),
429438
on: Box::new(on),
430439
error: Box::new(InteropError::str("Function not found")),
440+
context: Box::new(context),
431441
}
432442
}
433443
}
@@ -563,10 +573,14 @@ impl DisplayWithTypeInfo for InteropError {
563573
function_name,
564574
on,
565575
error,
576+
context,
566577
} => {
567578
write!(
568579
f,
569-
"Error in function {} on {}: {}",
580+
"Error {} in function {} on {}: {}",
581+
context
582+
.clone()
583+
.unwrap_or(FunctionCallContext::new(Language::Unknown)),
570584
function_name,
571585
WithTypeInfo::new_with_opt_info(on, type_info_provider),
572586
error

crates/bevy_mod_scripting_bindings/src/function/script_function.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,56 @@ pub trait ScriptFunctionMut<'env, Marker> {
4040

4141
/// The caller context when calling a script function.
4242
/// Functions can choose to react to caller preferences such as converting 1-indexed numbers to 0-indexed numbers
43-
#[derive(Clone, Debug, Reflect)]
43+
#[derive(Clone, Reflect, DebugWithTypeInfo)]
44+
#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")]
4445
#[reflect(opaque)]
4546
pub struct FunctionCallContext {
4647
language: Language,
48+
location_context: Option<LocationContext>,
49+
}
50+
51+
impl std::fmt::Display for FunctionCallContext {
52+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53+
f.write_str("in language: ")?;
54+
self.language.fmt(f)?;
55+
if let Some(context) = &self.location_context {
56+
f.write_str(", at line: ")?;
57+
context.line.fmt(f)?;
58+
f.write_str(", at column: ")?;
59+
context.col.fmt(f)?;
60+
}
61+
Ok(())
62+
}
63+
}
64+
65+
#[derive(Clone, Reflect, DebugWithTypeInfo)]
66+
#[debug_with_type_info(bms_display_path = "bevy_mod_scripting_display")]
67+
/// Describes a location within a script
68+
pub struct LocationContext {
69+
/// The line number
70+
pub line: u32,
71+
/// The column number
72+
pub col: u32,
4773
}
4874

4975
impl FunctionCallContext {
5076
/// Create a new FunctionCallContext with the given 1-indexing conversion preference
5177
pub const fn new(language: Language) -> Self {
52-
Self { language }
78+
Self {
79+
language,
80+
location_context: None,
81+
}
82+
}
83+
84+
/// Creates a new function call context with location information
85+
pub const fn new_with_location(
86+
language: Language,
87+
location_context: Option<LocationContext>,
88+
) -> Self {
89+
Self {
90+
language,
91+
location_context,
92+
}
5393
}
5494

5595
/// Tries to access the world, returning an error if the world is not available
@@ -68,6 +108,11 @@ impl FunctionCallContext {
68108
pub fn language(&self) -> Language {
69109
self.language.clone()
70110
}
111+
112+
/// Returns call location inside the script if available
113+
pub fn location(&self) -> Option<&LocationContext> {
114+
self.location_context.as_ref()
115+
}
71116
}
72117

73118
#[derive(Reflect, Clone, DebugWithTypeInfo)]
@@ -158,12 +203,13 @@ impl DynamicScriptFunction {
158203
profiling::scope!("Dynamic Call ", self.name().deref());
159204
let args = args.into_iter().collect::<VecDeque<_>>();
160205
// should we be inlining call errors into the return value?
161-
let return_val = (self.func)(context, args);
206+
let return_val = (self.func)(context.clone(), args);
162207
match return_val {
163208
ScriptValue::Error(e) => Err(InteropError::function_interop_error(
164209
self.name(),
165210
self.info.namespace,
166211
e,
212+
Some(context),
167213
)),
168214
v => Ok(v),
169215
}
@@ -195,12 +241,13 @@ impl DynamicScriptFunctionMut {
195241
let args = args.into_iter().collect::<VecDeque<_>>();
196242
// should we be inlining call errors into the return value?
197243
let mut write = self.func.write();
198-
let return_val = (write)(context, args);
244+
let return_val = (write)(context.clone(), args);
199245
match return_val {
200246
ScriptValue::Error(e) => Err(InteropError::function_interop_error(
201247
self.name(),
202248
self.info.namespace,
203249
e,
250+
Some(context),
204251
)),
205252
v => Ok(v),
206253
}
@@ -737,6 +784,7 @@ mod test {
737784
function_name,
738785
on,
739786
error,
787+
..
740788
} = gotten
741789
{
742790
assert_eq!(*function_name, expected_function_name);

crates/bevy_mod_scripting_bindings/src/world.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ impl<'w> WorldAccessGuard<'w> {
619619
return Err(InteropError::missing_function(
620620
name.to_string(),
621621
Namespace::OnType(type_id),
622+
Some(context.clone()),
622623
));
623624
}
624625
};
@@ -660,6 +661,7 @@ impl WorldAccessGuard<'_> {
660661
"field missing and no default provided: '{}'",
661662
descriptor.into()
662663
)),
664+
None,
663665
)
664666
})?;
665667
return Ok(default_data.default().into_partial_reflect());
@@ -820,6 +822,7 @@ impl WorldAccessGuard<'_> {
820822
"construct",
821823
Namespace::OnType(TypeId::of::<World>()),
822824
InteropError::str("missing 'variant' field in enum constructor payload"),
825+
None,
823826
)
824827
})?;
825828

@@ -834,6 +837,7 @@ impl WorldAccessGuard<'_> {
834837
variant_name,
835838
enum_info.type_path()
836839
)),
840+
None,
837841
)
838842
})?;
839843

crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,11 @@ impl UserData for LuaReflectReference {
325325
let iter_func = world
326326
.lookup_function([TypeId::of::<ReflectReference>()], "iter")
327327
.map_err(|f| {
328-
InteropError::missing_function(f, TypeId::of::<ReflectReference>().into())
328+
InteropError::missing_function(
329+
f,
330+
TypeId::of::<ReflectReference>().into(),
331+
Some(LUA_CALLER_CONTEXT),
332+
)
329333
})
330334
.map_err(IntoMluaError::to_lua_error)?;
331335

@@ -347,7 +351,11 @@ impl UserData for LuaReflectReference {
347351
let func = world
348352
.lookup_function([TypeId::of::<ReflectReference>()], "display")
349353
.map_err(|f| {
350-
InteropError::missing_function(f, TypeId::of::<ReflectReference>().into())
354+
InteropError::missing_function(
355+
f,
356+
TypeId::of::<ReflectReference>().into(),
357+
Some(LUA_CALLER_CONTEXT),
358+
)
351359
})
352360
.map_err(IntoMluaError::to_lua_error)?;
353361
let out = func
@@ -389,10 +397,12 @@ impl UserData for LuaStaticReflectReference {
389397
},
390398
Err(key) => key,
391399
};
392-
Err(
393-
InteropError::missing_function(format!("{key:#?}"), type_id.into())
394-
.into_lua_err(),
400+
Err(InteropError::missing_function(
401+
format!("{key:#?}"),
402+
type_id.into(),
403+
Some(LUA_CALLER_CONTEXT),
395404
)
405+
.into_lua_err())
396406
},
397407
);
398408
}

crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::{
55

66
use bevy_mod_scripting_asset::Language;
77
use bevy_mod_scripting_bindings::{
8-
error::InteropError, function::script_function::FunctionCallContext, script_value::ScriptValue,
8+
LocationContext, error::InteropError, function::script_function::FunctionCallContext,
9+
script_value::ScriptValue,
910
};
1011
use bevy_platform::collections::HashMap;
1112
use mlua::{FromLua, IntoLua, Value, Variadic};
@@ -140,18 +141,32 @@ impl IntoLua for LuaScriptValue {
140141
ScriptValue::Reference(r) => LuaReflectReference::from(r).into_lua(lua)?,
141142
ScriptValue::Error(script_error) => return Err(mlua::Error::external(script_error)),
142143
ScriptValue::Function(function) => lua
143-
.create_function(move |_lua, args: Variadic<LuaScriptValue>| {
144+
.create_function(move |lua, args: Variadic<LuaScriptValue>| {
145+
let loc = lua.inspect_stack(1).map(|debug| LocationContext {
146+
line: debug.curr_line().try_into().unwrap_or_default(),
147+
col: 0,
148+
});
144149
let out = function
145-
.call(args.into_iter().map(Into::into), LUA_CALLER_CONTEXT)
150+
.call(
151+
args.into_iter().map(Into::into),
152+
FunctionCallContext::new_with_location(Language::Lua, loc),
153+
)
146154
.map_err(IntoMluaError::to_lua_error)?;
147155

148156
Ok(LuaScriptValue::from(out))
149157
})?
150158
.into_lua(lua)?,
151159
ScriptValue::FunctionMut(function) => lua
152-
.create_function(move |_lua, args: Variadic<LuaScriptValue>| {
160+
.create_function(move |lua, args: Variadic<LuaScriptValue>| {
161+
let loc = lua.inspect_stack(0).map(|debug| LocationContext {
162+
line: debug.curr_line() as u32,
163+
col: 0,
164+
});
153165
let out = function
154-
.call(args.into_iter().map(Into::into), LUA_CALLER_CONTEXT)
166+
.call(
167+
args.into_iter().map(Into::into),
168+
FunctionCallContext::new_with_location(Language::Lua, loc),
169+
)
155170
.map_err(IntoMluaError::to_lua_error)?;
156171

157172
Ok(LuaScriptValue::from(out))

crates/languages/bevy_mod_scripting_rhai/src/bindings/reference.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,11 @@ impl IntoIterator for RhaiReflectReference {
261261
let iter_func = world
262262
.lookup_function([TypeId::of::<ReflectReference>()], "iter")
263263
.map_err(|f| {
264-
InteropError::missing_function(f, TypeId::of::<ReflectReference>().into())
264+
InteropError::missing_function(
265+
f,
266+
TypeId::of::<ReflectReference>().into(),
267+
Some(RHAI_CALLER_CONTEXT),
268+
)
265269
})?;
266270

267271
iter_func.call(
@@ -562,6 +566,7 @@ impl CustomType for RhaiReflectReference {
562566
InteropError::missing_function(
563567
f,
564568
TypeId::of::<ReflectReference>().into(),
569+
Some(RHAI_CALLER_CONTEXT),
565570
)
566571
})?;
567572

@@ -592,6 +597,7 @@ impl CustomType for RhaiReflectReference {
592597
InteropError::missing_function(
593598
f,
594599
TypeId::of::<ReflectReference>().into(),
600+
Some(RHAI_CALLER_CONTEXT),
595601
)
596602
})?;
597603

@@ -639,8 +645,12 @@ impl CustomType for RhaiStaticReflectReference {
639645
};
640646

641647
Err::<_, Box<EvalAltResult>>(
642-
InteropError::missing_function(format!("{key:#?}"), type_id.into())
643-
.into_rhai_error(),
648+
InteropError::missing_function(
649+
format!("{key:#?}"),
650+
type_id.into(),
651+
Some(RHAI_CALLER_CONTEXT),
652+
)
653+
.into_rhai_error(),
644654
)
645655
});
646656
}

0 commit comments

Comments
 (0)