33#include "helpers.h"
44#include "lang_specs.h"
55#include "foundation/constants.h"
6- #include "foundation/platform .h" // safe_realloc (frees old on failure)
7- #include "foundation/log .h" // cbm_log_warn
6+ #include "foundation/log .h" // cbm_log_error
7+ #include "foundation/mem_core .h" // cbm_realloc/cbm_free -- walk_defs stack
88#include "extract_node_stack.h"
99#include "simhash/minhash.h"
1010#include "semantic/ast_profile.h"
1111#include "tree_sitter/api.h" // TSNode, ts_node_*
12- #include <stdint.h> // uint32_t
12+ #include <limits.h> // INT_MAX
13+ #include <stdint.h> // uint32_t, SIZE_MAX
1314#include <stdio.h> // snprintf (ObjectScript storage/trigger sidecars)
14- #include <stdlib.h> // getenv, atoi
15+ #include <stdlib.h> // malloc/free (child-collection scratch)
1516#include <string.h>
1617#include <ctype.h>
1718
@@ -1416,7 +1417,8 @@ static bool is_route_string_kind(const char *kind) {
14161417 strcmp (kind , "interpreted_string_literal" ) == 0 ;
14171418}
14181419
1419- static const char * route_path_from_string_node (CBMArena * a , TSNode node , const char * source ) {
1420+ static const char * route_path_from_string_node (CBMArena * a , TSNode node , const char * source ,
1421+ bool allow_relative ) {
14201422 if (!is_route_string_kind (ts_node_type (node ))) {
14211423 return NULL ;
14221424 }
@@ -1428,30 +1430,41 @@ static const char *route_path_from_string_node(CBMArena *a, TSNode node, const c
14281430 if (plen >= PAIR_CHARS && (path [0 ] == '"' || path [0 ] == '\'' )) {
14291431 path = cbm_arena_strndup (a , path + SKIP_CHAR , (size_t )(plen - PAIR_CHARS ));
14301432 }
1431- return (path && path [0 ] == '/' ) ? path : NULL ;
1433+ if (!path || path [0 ] == '/' || !allow_relative ) {
1434+ return (path && path [0 ] == '/' ) ? path : NULL ;
1435+ }
1436+ /* JAX-RS @Path values are relative URI templates; a leading slash is
1437+ * optional and ignored by the framework. Route nodes use absolute-looking
1438+ * paths consistently, so normalize a non-empty relative value here. An
1439+ * empty @Path("") means "the class path itself": leave it unset so the
1440+ * caller falls back exactly as it does for a method without @Path. */
1441+ return path [0 ] ? cbm_arena_sprintf (a , "/%s" , path ) : NULL ;
14321442}
14331443
14341444static const char * find_route_path_literal (CBMArena * a , TSNode node , const char * source ,
1435- int max_depth ) {
1445+ int max_depth , bool allow_relative ) {
14361446 if (ts_node_is_null (node ) || max_depth < 0 ) {
14371447 return NULL ;
14381448 }
1439- const char * path = route_path_from_string_node (a , node , source );
1449+ const char * path = route_path_from_string_node (a , node , source , allow_relative );
14401450 if (path || max_depth == 0 ) {
14411451 return path ;
14421452 }
14431453 uint32_t nc = ts_node_named_child_count (node );
14441454 for (uint32_t i = 0 ; i < nc && i < DECORATOR_SCAN_LIMIT ; i ++ ) {
1445- path = find_route_path_literal (a , ts_node_named_child (node , i ), source , max_depth - 1 );
1455+ path = find_route_path_literal (a , ts_node_named_child (node , i ), source , max_depth - 1 ,
1456+ allow_relative );
14461457 if (path ) {
14471458 return path ;
14481459 }
14491460 }
14501461 return NULL ;
14511462}
14521463
1453- // Extract route path from decorator arguments (first string that starts with /).
1454- static const char * extract_route_path_from_args (CBMArena * a , TSNode args , const char * source ) {
1464+ // Extract route path from decorator arguments. Generic mappings keep only
1465+ // slash-prefixed strings; JAX-RS @Path additionally accepts relative templates.
1466+ static const char * extract_route_path_from_args (CBMArena * a , TSNode args , const char * source ,
1467+ bool allow_relative ) {
14551468 /* Every argument is checked. Java and Kotlin put no order on annotation
14561469 * attributes, so `path` can sit anywhere in the list. Stopping early left
14571470 * a real route unread and formed no Route node. Each argument's own
@@ -1464,7 +1477,8 @@ static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const
14641477 * @GetMapping(path = {"/orders"})
14651478 * Walk a bounded subtree and keep the first string literal that is
14661479 * path-shaped, while ignoring non-route literals such as media types. */
1467- const char * path = find_route_path_literal (a , arg , source , CBM_DESCENDANT_MAX_DEPTH );
1480+ const char * path =
1481+ find_route_path_literal (a , arg , source , CBM_DESCENDANT_MAX_DEPTH , allow_relative );
14681482 if (path ) {
14691483 return path ;
14701484 }
@@ -1610,7 +1624,7 @@ static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char
16101624
16111625 TSNode args = find_decorator_args (dchild );
16121626 if (!ts_node_is_null (args )) {
1613- const char * path = extract_route_path_from_args (a , args , source );
1627+ const char * path = extract_route_path_from_args (a , args , source , false );
16141628 if (path ) {
16151629 * out_path = path ;
16161630 * out_method = method ;
@@ -1690,7 +1704,7 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char
16901704 TSNode args = annotation_args_node (annotation );
16911705 const char * path = NULL ;
16921706 if (!ts_node_is_null (args )) {
1693- path = extract_route_path_from_args (a , args , source );
1707+ path = extract_route_path_from_args (a , args , source , false );
16941708 }
16951709 * out_path = path ? path : "/" ;
16961710 * out_method = method ;
@@ -1740,7 +1754,7 @@ static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source
17401754 if (!* out_jax_path && strcmp (name , "Path" ) == 0 ) {
17411755 TSNode args = annotation_args_node (child );
17421756 if (!ts_node_is_null (args )) {
1743- * out_jax_path = extract_route_path_from_args (a , args , source );
1757+ * out_jax_path = extract_route_path_from_args (a , args , source , true );
17441758 }
17451759 continue ;
17461760 }
@@ -1750,7 +1764,7 @@ static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source
17501764 * out_method = method ;
17511765 TSNode args = annotation_args_node (child );
17521766 if (!ts_node_is_null (args )) {
1753- * out_map_path = extract_route_path_from_args (a , args , source );
1767+ * out_map_path = extract_route_path_from_args (a , args , source , false );
17541768 }
17551769 }
17561770 }
@@ -5147,7 +5161,8 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
51475161
51485162 def .decorators = extract_decorators (a , child , ctx -> source , ctx -> language , spec );
51495163 extract_route_from_decorators (a , child , ctx -> source , spec , & def .route_path , & def .route_method );
5150- if (def .route_path && (ctx -> language == CBM_LANG_JAVA || ctx -> language == CBM_LANG_KOTLIN )) {
5164+ if (def .route_path && (ctx -> language == CBM_LANG_JAVA || ctx -> language == CBM_LANG_KOTLIN ||
5165+ ctx -> language == CBM_LANG_SCALA )) {
51515166 const char * prefix = spring_class_route_prefix (a , class_node , ctx -> source , spec );
51525167 def .route_path = join_route_paths (a , prefix , def .route_path );
51535168 }
@@ -7195,74 +7210,75 @@ typedef struct {
71957210 * single ~160 KB C-stack frame. That overflowed small thread stacks (the
71967211 * pre-2026-03 Windows 1 MB main thread) on the definitions pass, and its
71977212 * `top < 4096` push guards SILENTLY DROPPED every top-level definition past
7198- * 4096. Use a growable heap stack instead: a tiny initial footprint that doubles
7199- * on demand, bounded by a generous, env-configurable ceiling that WARNs (once)
7200- * rather than dropping — so a file with thousands of top-level defs is fully
7201- * extracted, and a pathological one degrades to a warned, bounded skip instead
7202- * of an OOM or a stack overflow. */
7213+ * 4096. It became a growable stack bounded by an 8M-frame ceiling
7214+ * (env CBM_WALK_DEFS_MAX) that still stopped pushing once reached — and since
7215+ * children are pushed last-to-first, a file wider than the ceiling lost its
7216+ * FIRST definitions. No work cap may decide graph content, so there is no
7217+ * ceiling: the stack doubles on demand, and only an allocation failure stops
7218+ * the walk. That failure is loud — logged at error level and reported as a
7219+ * per-file extract error (result->has_error) — never a silent partial file. */
72037220typedef struct {
72047221 walk_defs_frame_t * data ;
72057222 int top ;
72067223 int cap ;
7207- const char * path ; // for the WARN when the ceiling is hit (may be NULL)
7208- bool warned ;
7224+ const char * path ; // for the error log on allocation failure (may be NULL)
7225+ bool failed ; // growth failed: pending frames dropped, walk drains
72097226 /* The per-file traversal scratch (ctx->scratch) when there is one: frames
72107227 * then come from memory the thread reuses file after file, where a malloc
72117228 * of 256 frames per file was 28 k allocations and 255 MB never written on
72127229 * the Go corpus (waste sanitizer, 2026-09-17). Growth copies into a
72137230 * doubled buffer and abandons the old one to the arena, like TSNodeStack.
7214- * NULL: the heap, freed by the walk. */
7231+ * NULL: the memory core (class EXTRACT), released by the walk. */
72157232 CBMArena * arena ;
72167233} wd_stack_t ;
72177234
7218- // Generous safety ceiling (frames), env-overridable via CBM_WALK_DEFS_MAX.
7219- // Realistic files never approach this; it only bounds a pathological/adversarial
7220- // file so extraction degrades to a warned skip rather than unbounded memory.
7221- static int wd_stack_max (void ) {
7222- const char * e = getenv ("CBM_WALK_DEFS_MAX" );
7223- if (e ) {
7224- int v = atoi (e );
7225- if (v > 0 ) {
7226- return v ;
7227- }
7228- }
7229- return 8 * 1024 * 1024 ; // 8M frames (~320 MB) default
7230- }
7231-
7232- static void wd_push (wd_stack_t * s , TSNode node , const char * enclosing_qn ) {
7233- if (s -> top >= s -> cap ) {
7234- int ncap = s -> cap ? s -> cap * 2 : 256 ;
7235- if (ncap > wd_stack_max ()) {
7236- if (!s -> warned ) {
7237- char lim [24 ];
7238- snprintf (lim , sizeof (lim ), "%d" , wd_stack_max ());
7239- cbm_log_warn ("extract.walk_defs_capped" , "limit" , lim , "path" ,
7240- s -> path ? s -> path : "" );
7241- s -> warned = true;
7242- }
7243- return ; // bounded: stop growing (warned, not silent)
7244- }
7245- walk_defs_frame_t * nd = NULL ;
7235+ enum { WD_STACK_INITIAL = 256 };
7236+
7237+ /* Double `s` (or give it its first WD_STACK_INITIAL frames). Returns false,
7238+ * after logging, when the doubled size is not representable or cannot be
7239+ * allocated; the old buffer is left intact so the walk can still release it. */
7240+ static bool wd_grow (wd_stack_t * s ) {
7241+ walk_defs_frame_t * nd = NULL ;
7242+ int ncap = WD_STACK_INITIAL ;
7243+ bool fits = true;
7244+ if (s -> cap > 0 ) {
7245+ fits = s -> cap <= INT_MAX / 2 ;
7246+ ncap = fits ? s -> cap * 2 : s -> cap ;
7247+ }
7248+ fits = fits && (size_t )ncap <= SIZE_MAX / sizeof (walk_defs_frame_t );
7249+ if (fits ) {
7250+ size_t bytes = (size_t )ncap * sizeof (walk_defs_frame_t );
72467251 if (s -> arena ) {
7247- nd = (walk_defs_frame_t * )cbm_arena_alloc (s -> arena ,
7248- (size_t )ncap * sizeof (walk_defs_frame_t ));
7252+ nd = (walk_defs_frame_t * )cbm_arena_alloc (s -> arena , bytes );
72497253 if (nd && s -> top > 0 ) {
72507254 memcpy (nd , s -> data , (size_t )s -> top * sizeof (walk_defs_frame_t ));
72517255 }
72527256 } else {
7253- nd = safe_realloc (s -> data , (size_t )ncap * sizeof (walk_defs_frame_t ));
7254- }
7255- if (!nd ) {
7256- /* OOM — safe_realloc already freed the old buffer. Bail cleanly: drop
7257- * pending frames so the walk_defs loop drains and exits without a NULL
7258- * deref; extraction keeps whatever was already emitted. */
7259- s -> data = NULL ;
7260- s -> cap = 0 ;
7261- s -> top = 0 ;
7262- return ;
7257+ nd = (walk_defs_frame_t * )cbm_realloc (CBM_MEM_CLASS_EXTRACT , s -> data , bytes );
72637258 }
7264- s -> data = nd ;
7265- s -> cap = ncap ;
7259+ }
7260+ if (!nd ) {
7261+ char pending [24 ];
7262+ snprintf (pending , sizeof (pending ), "%d" , s -> top );
7263+ cbm_log_error ("extract.walk_stack_alloc_failed" , "walker" , "walk_defs" , "pending" , pending ,
7264+ "path" , s -> path ? s -> path : "" );
7265+ return false;
7266+ }
7267+ s -> data = nd ;
7268+ s -> cap = ncap ;
7269+ return true;
7270+ }
7271+
7272+ static void wd_push (wd_stack_t * s , TSNode node , const char * enclosing_qn ) {
7273+ if (s -> failed ) {
7274+ return ;
7275+ }
7276+ if (s -> top >= s -> cap && !wd_grow (s )) {
7277+ /* Drop every pending frame so the walk_defs loop drains and exits;
7278+ * walk_defs then marks the file's result as an extract error. */
7279+ s -> failed = true;
7280+ s -> top = 0 ;
7281+ return ;
72667282 }
72677283 s -> data [s -> top ++ ] = (walk_defs_frame_t ){node , enclosing_qn };
72687284}
@@ -8070,7 +8086,11 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
80708086 wd_push_children_reverse (& s , node , frame .enclosing_class_qn );
80718087 }
80728088 if (!s .arena ) {
8073- free (s .data );
8089+ cbm_free (CBM_MEM_CLASS_EXTRACT , s .data );
8090+ }
8091+ if (s .failed ) {
8092+ ctx -> result -> has_error = true;
8093+ ctx -> result -> error_msg = "definitions walk: stack allocation failed" ;
80748094 }
80758095}
80768096
0 commit comments