@@ -158,18 +158,188 @@ void qts_dump(JSContext *ctx, JSValueConst value) {
158158 putchar ('\n' );
159159}
160160
161- void copy_prop_if_needed (JSContext * ctx , JSValueConst dest , JSValueConst src , const char * prop_name ) {
161+ // ----------------------------------------------------------------------------
162+ // QTS_Dump helpers
163+
164+ // Forward declaration
165+ JSBorrowedChar * QTS_GetString (JSContext * ctx , JSValueConst * value );
166+
167+ // Special non-enumerable properties to include when dumping objects (e.g., Error properties)
168+ static const char * QTS_DUMP_SPECIAL_PROPS [] = {
169+ "name" , "message" , "stack" , "fileName" , "lineNumber"
170+ };
171+ #define QTS_DUMP_SPECIAL_PROPS_COUNT (sizeof(QTS_DUMP_SPECIAL_PROPS) / sizeof(QTS_DUMP_SPECIAL_PROPS[0]))
172+
173+ // Returns true if prop should be added to the clone:
174+ // - Property exists (own or inherited) and has a non-undefined value
175+ // - Property is NOT already an own enumerable property (which JSON.stringify would include)
176+ static bool qts_should_copy_special_prop (JSContext * ctx , JSValueConst obj , const char * prop_name ) {
162177 JSAtom prop_atom = JS_NewAtom (ctx , prop_name );
163- JSValue dest_prop = JS_GetProperty (ctx , dest , prop_atom );
164- if (JS_IsUndefined (dest_prop )) {
165- JSValue src_prop = JS_GetProperty (ctx , src , prop_atom );
166- if (!JS_IsUndefined (src_prop ) && !JS_IsException (src_prop )) {
167- JS_SetProperty (ctx , dest , prop_atom , src_prop );
178+
179+ // First check if property exists at all (including inherited)
180+ JSValue val = JS_GetProperty (ctx , obj , prop_atom );
181+ if (JS_IsException (val ) || JS_IsUndefined (val )) {
182+ JS_FreeAtom (ctx , prop_atom );
183+ JS_FreeValue (ctx , val );
184+ return false; // Property doesn't exist or is undefined
185+ }
186+ JS_FreeValue (ctx , val );
187+
188+ // Property exists - check if it's an own enumerable property
189+ JSPropertyDescriptor desc ;
190+ int ret = JS_GetOwnProperty (ctx , & desc , obj , prop_atom );
191+ JS_FreeAtom (ctx , prop_atom );
192+
193+ if (ret != 1 ) {
194+ // Not an own property - but it exists (inherited), so we should copy it
195+ return true;
196+ }
197+
198+ // It's an own property - check if enumerable
199+ bool is_enumerable = (desc .flags & JS_PROP_ENUMERABLE );
200+
201+ // Free descriptor values
202+ JS_FreeValue (ctx , desc .value );
203+ if (desc .flags & JS_PROP_GETSET ) {
204+ JS_FreeValue (ctx , desc .getter );
205+ JS_FreeValue (ctx , desc .setter );
206+ }
207+
208+ // Copy if NOT enumerable (enumerable props are already included via JSON.stringify)
209+ return !is_enumerable ;
210+ }
211+
212+ // Creates clone with special props made enumerable, or JS_UNDEFINED if not needed
213+ static JSValue qts_dump_create_clone (JSContext * ctx , JSValueConst obj ) {
214+ if (!JS_IsObject (obj )) return JS_UNDEFINED ;
215+
216+ // Check if any special props need copying
217+ bool needs_clone = false;
218+ for (size_t i = 0 ; i < QTS_DUMP_SPECIAL_PROPS_COUNT ; i ++ ) {
219+ if (qts_should_copy_special_prop (ctx , obj , QTS_DUMP_SPECIAL_PROPS [i ])) {
220+ needs_clone = true;
221+ break ;
222+ }
223+ }
224+ if (!needs_clone ) return JS_UNDEFINED ;
225+
226+ // Create clone with all enumerable props
227+ JSValue clone = JS_NewObject (ctx );
228+ if (JS_IsException (clone )) return clone ;
229+
230+ JSPropertyEnum * tab ;
231+ uint32_t len ;
232+ if (JS_GetOwnPropertyNames (ctx , & tab , & len , obj , JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY ) < 0 ) {
233+ JS_FreeValue (ctx , clone );
234+ return JS_EXCEPTION ;
235+ }
236+
237+ for (uint32_t i = 0 ; i < len ; i ++ ) {
238+ JSValue val = JS_GetProperty (ctx , obj , tab [i ].atom );
239+ if (!JS_IsException (val )) {
240+ JS_DefinePropertyValue (ctx , clone , tab [i ].atom , val , JS_PROP_C_W_E );
241+ }
242+ }
243+ JS_FreePropertyEnum (ctx , tab , len );
244+
245+ // Add special props (inherited or non-enumerable) as enumerable
246+ for (size_t i = 0 ; i < QTS_DUMP_SPECIAL_PROPS_COUNT ; i ++ ) {
247+ if (qts_should_copy_special_prop (ctx , obj , QTS_DUMP_SPECIAL_PROPS [i ])) {
248+ JSAtom atom = JS_NewAtom (ctx , QTS_DUMP_SPECIAL_PROPS [i ]);
249+ JSValue val = JS_GetProperty (ctx , obj , atom );
250+ if (!JS_IsException (val ) && !JS_IsUndefined (val )) {
251+ JS_DefinePropertyValue (ctx , clone , atom , val , JS_PROP_C_W_E );
252+ } else {
253+ JS_FreeValue (ctx , val );
254+ }
255+ JS_FreeAtom (ctx , atom );
168256 }
257+ }
258+
259+ return clone ;
260+ }
261+
262+ #define QTS_DUMP_FALLBACK_SIZE 2048
263+
264+ #ifndef QTS_USE_QUICKJS_NG
265+ // Simple fixed buffer for JS_PrintValue output (bellard/quickjs only).
266+ typedef struct qts_debugbuf {
267+ char * buf ;
268+ size_t size ;
269+ size_t pos ;
270+ } qts_debugbuf ;
271+
272+ static void qts_debugbuf_write (void * opaque , const char * data , size_t len ) {
273+ qts_debugbuf * s = opaque ;
274+ size_t remaining = s -> size - s -> pos - 1 ; // -1 for null terminator
275+ if (len > remaining ) len = remaining ;
276+ if (len > 0 ) {
277+ memcpy (s -> buf + s -> pos , data , len );
278+ s -> pos += len ;
279+ s -> buf [s -> pos ] = '\0' ;
280+ }
281+ }
282+ #endif
283+
284+ // Returns a fallback string when JSON serialization fails
285+ static JSBorrowedChar * qts_dump_get_fallback (JSContext * ctx , JSValueConst obj , JSValue exception ) {
286+ char buf [QTS_DUMP_FALLBACK_SIZE ];
287+ size_t pos = 0 ;
288+ buf [0 ] = '\0' ;
289+
290+ #ifndef QTS_USE_QUICKJS_NG
291+ // bellard/quickjs: Use JS_PrintValue with show_hidden for rich debug output
292+ qts_debugbuf dbuf = { buf , sizeof (buf ), 0 };
293+
294+ JSPrintValueOptions options ;
295+ JS_PrintValueSetDefaultOptions (& options );
296+ options .show_hidden = true;
297+ options .max_depth = 3 ;
298+ options .max_string_length = 200 ;
299+ options .max_item_count = 20 ;
300+
301+ JS_PrintValue (ctx , qts_debugbuf_write , & dbuf , obj , & options );
302+ pos = dbuf .pos ;
303+ #else
304+ // quickjs-ng: JS_PrintValue not available, use toString() instead
305+ JSValue to_string_val = JS_ToString (ctx , obj );
306+ if (JS_IsException (to_string_val )) {
307+ JS_FreeValue (ctx , JS_GetException (ctx ));
308+ pos = snprintf (buf , sizeof (buf ), "(toString failed)" );
169309 } else {
170- JS_FreeValue (ctx , dest_prop );
310+ const char * str = JS_ToCString (ctx , to_string_val );
311+ if (str ) {
312+ size_t len = strlen (str );
313+ if (len > sizeof (buf ) - 100 ) len = sizeof (buf ) - 100 ; // Reserve space for error info
314+ memcpy (buf , str , len );
315+ buf [len ] = '\0' ;
316+ pos = len ;
317+ JS_FreeCString (ctx , str );
318+ }
319+ JS_FreeValue (ctx , to_string_val );
171320 }
172- JS_FreeAtom (ctx , prop_atom );
321+ #endif
322+
323+ // Add separator and error info
324+ const char * err_msg = NULL ;
325+ if (!JS_IsUndefined (exception ) && !JS_IsNull (exception )) {
326+ JSValue msg = JS_GetPropertyStr (ctx , exception , "message" );
327+ if (!JS_IsException (msg ) && JS_IsString (msg )) {
328+ err_msg = JS_ToCString (ctx , msg );
329+ }
330+ JS_FreeValue (ctx , msg );
331+ }
332+
333+ size_t remaining = sizeof (buf ) - pos - 1 ;
334+ snprintf (buf + pos , remaining , "\n---\nnot JSON serializable: %s" ,
335+ err_msg ? err_msg : "(unknown error)" );
336+
337+ if (err_msg ) JS_FreeCString (ctx , err_msg );
338+
339+ JSValue result_str = JS_NewString (ctx , buf );
340+ JSBorrowedChar * result = QTS_GetString (ctx , & result_str );
341+ JS_FreeValue (ctx , result_str );
342+ return result ;
173343}
174344
175345JSValue * jsvalue_to_heap (JSValueConst value ) {
@@ -815,40 +985,27 @@ JSValue *QTS_ResolveException(JSContext *ctx, JSValue *maybe_exception) {
815985}
816986
817987MaybeAsync (JSBorrowedChar * ) QTS_Dump (JSContext * ctx , JSValueConst * obj ) {
818- JSValue obj_json_value = JS_JSONStringify (ctx , * obj , JS_UNDEFINED , JS_UNDEFINED );
819- if (!JS_IsException (obj_json_value )) {
820- const char * obj_json_chars = JS_ToCString (ctx , obj_json_value );
821- JS_FreeValue (ctx , obj_json_value );
822- if (obj_json_chars != NULL ) {
823- JSValue enumerable_props = JS_ParseJSON (ctx , obj_json_chars , strlen (obj_json_chars ), "<dump>" );
824- JS_FreeCString (ctx , obj_json_chars );
825- if (!JS_IsException (enumerable_props )) {
826- // Copy common non-enumerable props for different object types.
827- // Errors:
828- copy_prop_if_needed (ctx , enumerable_props , * obj , "name" );
829- copy_prop_if_needed (ctx , enumerable_props , * obj , "message" );
830- copy_prop_if_needed (ctx , enumerable_props , * obj , "stack" );
831- copy_prop_if_needed (ctx , enumerable_props , * obj , "fileName" );
832- copy_prop_if_needed (ctx , enumerable_props , * obj , "lineNumber" );
833-
834- // Serialize again.
835- JSValue enumerable_json = JS_JSONStringify (ctx , enumerable_props , JS_UNDEFINED , JS_UNDEFINED );
836- JS_FreeValue (ctx , enumerable_props );
837-
838- JSBorrowedChar * result = QTS_GetString (ctx , & enumerable_json );
839- JS_FreeValue (ctx , enumerable_json );
840- return result ;
841- }
842- }
988+ // Try to create clone with special props; returns JS_UNDEFINED if not needed
989+ JSValue clone = qts_dump_create_clone (ctx , * obj );
990+ JSValue to_serialize = JS_IsUndefined (clone ) ? * obj : clone ;
991+
992+ // Serialize exactly once
993+ JSValue json = JS_JSONStringify (ctx , to_serialize , JS_UNDEFINED , JS_UNDEFINED );
994+
995+ if (!JS_IsUndefined (clone )) {
996+ JS_FreeValue (ctx , clone );
843997 }
844998
845- IF_DEBUG {
846- qts_log ("Error dumping JSON:" );
847- js_std_dump_error (ctx );
999+ if (JS_IsException (json )) {
1000+ JSValue exception = JS_GetException (ctx );
1001+ JSBorrowedChar * result = qts_dump_get_fallback (ctx , * obj , exception );
1002+ JS_FreeValue (ctx , exception );
1003+ return result ;
8481004 }
8491005
850- // Fallback: convert to string
851- return QTS_GetString (ctx , obj );
1006+ JSBorrowedChar * result = QTS_GetString (ctx , & json );
1007+ JS_FreeValue (ctx , json );
1008+ return result ;
8521009}
8531010
8541011JSValue qts_resolve_func_data (
0 commit comments